JVM学习2-垃圾回收机制
- 1 从JVM角度看对象在内存中的创建
-
- 1.1 对象创建的流程
- 1.2 给对象分配内存
- 1.3 分配内存的线程安全性问题
- 1.4 对象的结构
- 1.4 对象的访问定位
- 2 垃圾回收
- 2.1垃圾回收概述
- 2.2 引用计数法
- 2.3 可达性分析法
- 3 回收策略
-
- 3.1标记-清除算法
- 3.2 复制算法
- 3.3 标记-整理-清除算法
- 3.4 分代收集算法
- 4 垃圾收集器
-
- 4.1 Serial收集器
- 4.2 ParNew收集器
- 4.2 Parallel Scavenge收集器
- 4.3 CMS收集器
- 4.4 G1收集器
1 从JVM角度看对象在内存中的创建
1.1 对象创建的流程
通过New 创建一个对象会在堆内存中开辟空间

1.2 给对象分配内存
分配内存就是指针移动的过程
1、指针碰撞:堆内存是规整的,可用内存和不可用内存是分开的。
2、空闲列表:堆内存维护一张表来记录哪些内存(内存编号)没有使用。
使用哪一种分配方式,是由内存中的堆是否规整决定的,而是否规整是由垃圾回收策略决定的。如果垃圾回收器带有压缩功能就使用指针碰撞,否则就使用空闲列表。
1.3 分配内存的线程安全性问题
如果多个对象同时创建,内存分配也就是堆内存指针的指向就会有线程安全性问题。
1、同步,加锁,安全但是执行效率太低。
2、本地线程分配缓冲。也就是在堆内存中为每一个线程分配这个线程独有的区域。TLAB
1.4 对象的结构
Header (对象头Mark Word)
1、自身运行时数据,如哈希值 (Obect类的hash是Native方法),GC分代年龄(为垃圾回收机制使用 ),锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。
2、类型指针:对象指向它的类的元数据的指针,虚拟机通过它来确定这个对象时哪个类的实例。并不是所有对象都保留对象指针。数组对象还会有记录长度的数据。
3、InstanceData:数据的实例,真正存储在对象的引用侧率,HotSpot的分配侧率是相同宽度的数据放在一起。所以有可能出现父类中定义的变量在子类之前。
4、Padding占位符:自动内存管理要求对象的启事地址必须是8个字节的整数倍,对象头是8个字节,如果对象的实例数据不是8个字节的整数倍,需要padding补齐。
1.4 对象的访问定位
Java虚拟机只规定了由栈内存中一个引用类型执行堆内存的一块地址,但这块地址并不一定是这个对象本身,可以是对象本身,也可是其他的一块内存区域。对象的访问定位有两种实现方式:使用句柄、和使用指针。
HotSpot使用的是指针。
2 垃圾回收
2.1垃圾回收概述
1、如何判定对象为垃圾对象
引用计数法
可达性分析法
2、如何回收
回收策略:标记清除、复制算法、标记整理、分代收集算法
垃圾回收器:serial、Parnew、Cms、G1
3、何时回收
2.2 引用计数法
顾名思义:在对象中添加一个引用计数器、当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时候、计数器的值减1.(将引用置为null.),虽然效率高,但是不会使用,因为这样计数会存在问题,如果栈中的引用置为null,但是堆中的内存又指向堆中别的对象,垃圾回收器只将直接引用的对象清除,而间接不会被清除。
通过代码来验证JVM是否采用的这种方法判断一个对象是否为垃圾对象。需要配置JVM参数
-verbose:gc 打印垃圾回收的日志信息
-XX:+PrintGCDetails 详细的GC信息
public class Test01 {
private Object instacnce;
//让对象创建时占用一定大小的空间,回收前后有明显的变化
public Test01(){
byte[] b = new byte[20*1024*1024];
}
public static void main(String[] args) {
Test01 test01 = new Test01();
Test01 test02 = new Test01();
//互相引用
test01.instacnce = test02;
test02.instacnce = test01;
//引用置为null
test01 = null;
test02 = null;
//手动调用GC
System.gc();
}
}
可以看到内存有很大的变化,说明,JVM并不是使用的引用计数器算法
[GC (System.gc()) [PSYoungGen: 23808K->728K(38400K)] 44288K->21208K(125952K), 0.0080063 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 728K->0K(38400K)] [ParOldGen: 20480K->632K(87552K)] 21208K->632K(125952K), [Metaspace: 3276K->3276K(1056768K)], 0.0165887 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
Heap
PSYoungGen total 38400K, used 333K [0x00000000d5d00000, 0x00000000d8780000, 0x0000000100000000)
eden space 33280K, 1% used [0x00000000d5d00000,0x00000000d5d534a8,0x00000000d7d80000)
from space 5120K, 0% used [0x00000000d7d80000,0x00000000d7d80000,0x00000000d8280000)
to space 5120K, 0% used [0x00000000d8280000,0x00000000d8280000,0x00000000d8780000)
ParOldGen total 87552K, used 632K [0x0000000081600000, 0x0000000086b80000, 0x00000000d5d00000)
object space 87552K, 0% used [0x0000000081600000,0x000000008169e298,0x0000000086b80000)
Metaspace used 3282K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 359K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
2.3 可达性分析法
可以解决引用计数器法的问题。定义了一个GCRoot,如果顺着GCRoot一直往下走,可以通过引用链找到这个对象,说明这个对象是可用的,否则回收掉这个对象。
可以作为GCRoots的对象有哪些?
1、虚拟机栈,也就是局部变量表
2、方法区的类属性所引用的对象
3、方法区中常量所引用的对象
4、本地方法栈中引用的对象
3 回收策略
3.1标记-清除算法
对可以清除的,正在使用的,未被使用的进行标记,但是这样会带来一个问题,就是内存中出现越来越多的不连续的内存空间。导致如果要存储一个大对象时再出发一次垃圾回收算法,又带来性能问题。
回收之前和回收之后的内存图,红色是使用的,黄色是标记为可以被清除的。白色表示未被使用的
3.2 复制算法
将堆内存分为下面的几大块,垃圾回收器主要关注新生代
1. 新生代
Eden 伊甸园
Survivor 存活区
Tenured Gen
2. 老年代
当对象第一次创建,放在堆中,第一次执行垃圾回收,将存活的对象复制到Survivor1中,将Eden中的空间全部恢复为未使用,第二次垃圾回收,会选择的将Survivor1中仍然存活的对象放在Tenured Gen中,然后将Survivor1剩余的对象放在另一个Survovord2空间中,然后将Survivor1内存清空。再将Eden中存活的对象放在Survivor1中。如此循环,提高内存的使用率、
如果Eden不够用还会借用Survivor内存。Survivor不够用,还会使用内存担保借用TenuredGen。
3.3 标记-整理-清除算法
复制算法如果存活超过百分之10,就需要内存担保,这样对新生代 来说是可行的,但是对于老年代,每次存活很可能超过百分之90,这样会让效率变得慢。所以标记整理算法主要是针对老年代。
将需要回收的内存往右移动,右边有不需要回收的往左边移动,这样右边就全都是需要回收的内存。
3.4 分代收集算法
分代收集算法并不是一种新的算法,而是将复制算法和标记整理算法进行结合,内存分为新生代和老年代,根据两者之间不同的特点,针对新生代和回收率较高的内存区域,选择复制算法,对于老年代和回收率低的选择标记整理清除算法。
4 垃圾收集器
4.1 Serial收集器
垃圾回收器在虚拟机的规范中并没有作出要求,所以不同的公司可以实现不同的收集器。不同的收集器适用的环境不同,垃圾回收器也会影响性能。
JDK1.3之前主要就是使用的Serial收集器,也是最基本,发展最悠久的单线程的垃圾收集器。适用于 桌面应用程序,用于新生代内存复制算法。
4.2 ParNew收集器
多线程的收集器。用于客户端性能不如Serial收集器,在JDK1.5,SUN公司开发了CMS收集器,这款收集器真正做到了扔垃圾和打扫卫生同时进行的功能。但是CMS是用于回收老年代内存,如果使用CMS回收老年代内存,新生代内存就必须使用Serial或ParNew。也就是CMS和Parial(后边会提到)是不能一同工作的。至于其他的收集器和Serial使用了大量系相同的代码,使用了复制算法
4.2 Parallel Scavenge收集器
使用的复制算法,针对的是新生代内存,新生代收集器,多线程收集器。似乎和ParNew差不多。不同点是最初设计的关注点上,ParNew关注的是缩短垃圾回收的时间。Parallel是达到一个可控制的吞吐量。
吞吐量是指CPU用于运行用户代码的时间与CPU消耗的总时间的比值。所以,计算吞吐量的公式:吞吐量 = (计算用户代码的时间) / (执行用户代码的时间 + 垃圾回收所占用的时间)
例如,虚拟机一共运行了100分钟,那么垃圾回收时间用了1分钟,吞吐量是99%,
-XX:MaxGCPauseMillis 垃圾收集器最大停顿时间
设置100ms,10秒停顿一次, 1ms,1秒停顿一次。这是一个杠杆,需要一个合理的值
-XX:GCTimeRatio 吞吐量大小
(0,100)
设置99 就是垃圾回收时间为1%
4.3 CMS收集器
Concurrent Mark Sweep并发标记清除收集器,采用的垃圾回收算法是标记清除算法,第一步标记、第二步清除,标记清除算法会产生垃圾碎片、性能比较低。虽然它的效率比较低,但是可以通过一定的手段来提高效率,CMS收集器用于老念代的收集,和ParNew 一起工作。
并发和并行的理解。
并发是指垃圾打扫和产生垃圾同时进行
并行是指打扫垃圾是打扫垃圾,产生垃圾产生垃圾,不过都是有多个线程同时执行
下面的两个图分别表示并行与并发。绿色表示垃圾回收,红色表示产生垃圾
工作过程:
1) 初始标记
2) 并发标记
3) 重新标记
4) 并发清理
CMS并不是完全并发的一种垃圾收集器,而是在标记和清除等两个比较消耗时间的过程中使用并发执行。CPU占用率高,吞吐量提升不上去。
1、初始标记是标记GCRoot能直接关联到的对象,速度很快
2、并发标记是接着往下找
3、重新标记是为了修正并发标记期间,因用户程序运作而导致产生变动的那一部分对象的标记记录,是对并发标记的修正。
4、并发清理,把标记的对象进行清理
优点:
1)并发收集
2)低停顿
缺点:
1)占用大量的CPU资源,因为有线程的开销
2)无法处理浮动垃圾
3)出现Concurrent Mode Faliure:在并发清理时候,用户线程产生的对象,需要存放到一块临时的内存空间,如果这块内存过大,会浪费空间,如果给的过小就会出现此错误,进而触发Serial收集器来收集,导致更费时间
4)产生空间碎片
4.4 G1收集器
Garbage First ,当今最为强大的收集器。Jdk9默认采用此收集器,最早在2004年,Sun公司实验室,发表了G1的论文,但是不是主流的,主流的依然是spot,JDK6中集成到了JVM中,JDK7正式放入JDK中。
结合了前面几种收集器的优势:并发、分代收集、空间整合以及可预测的停顿。并行和并发:充分利用多核CPU,缩短停顿时间。前面已经说过:并行能减少停顿时间、并发能提高速度。G1的分代收集与之前的分代有区别:并不是严格的分为新生代老年代的物理隔离,而是分为一块一块的内存。G1还有一个特点是空间整合,类似于标记-整理-清除算法那样。可预测的停顿。
步骤:
1)初始标记
2)并发标记
3)最终标记
4)筛选回收:通过Remembersert来记录对象在不同内存区域的变化,在判断一个对象是否为垃圾的时候可以避免全表扫描。