1.简介
垃圾回收器的主要工作区域是堆内存,JDK1.8之后JVM的堆区分为年轻代和年老代,这样做可以优化GC性能,因为大多数对象存活时间都比较短,对象新创建时将其放入年轻代,年轻代空间满了之后进行回收,称为Minor GC,回收时扫描年轻代,可以腾出来大量的空间。年老代内存占满之后执行Full GC回收年老代的内存空间。
总内存=JVM内存+系统内存
JVM内存=堆+元数据空间+虚拟机栈+本地方法栈+程序计数器
堆=年轻代+年老代
年轻代=Eden区+Survivor区,Survivor区分为大小相等的两部分,默认Eden区和Survivor区的比例为8:2
2.GC原理
- 对象被创建时会放到年轻代的Eden区;
- Eden区满之后执行Minor GC,将仍存活的对象放入Survivor区;
- 多次Minor GC仍未被清除的对象会移动至年老区;
- 年老区空间满了之后会触发Full GC。
Minor GC的执行过程
-
所有新创建的对象都分配在Eden区,经过一次Minor GC存活下来的对象被移到From区;
-
之后的Minor GC,Eden区存活下来的对象会被移到To区,From中年龄小于阈值的也会被移到To区,年龄大于阈值的会被移到年老区,然后再将From区和To区互换,保证To区为空;当To区满了之后,就把To区的所有对象都移动到年老区;
-
在Survivor区中,每经过一次Minor GC年龄都会加1,年龄大于15(我看到的资料是这么说的)会被移动到Eden区。
3.如何判断对象可以回收
计数器算法
为每个对象都设置一个计数器,当其被引用时,计数器加1,如果某个对象的计数器为0,则代表这个对象可以回收。
缺点:无法解决循环引用的问题;计数器的使用会带来额外的开销。
可达性分析算法
以GC Roots对象作为起点,往下搜索,搜索的路径为引用链,没有被引用链连接的对象是不可用的,可以被回收,但是再彻底被回收之前还有一次存活的机会,和finalized函数相关,可以再查其它资料了解。
可以作为GC Roots的对象:
-
在虚拟机栈中(栈帧中的本地变量表)引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
-
在方法区中类静态属性引用的对象,譬如Java类的引用静态变量
-
在方法区中常量引用的对象,譬如字符串常量池里的引用
-
在本地方法栈中引用的对象
-
Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException,OutOfMemoryError)等,还有系统类加载器
-
所有被同步锁持有的对象
-
反应Java虚拟机内部情况的JMXBean。JVMTI中注册的回调、本地代码缓存等
补充:面试的时候有被面试官问到为什么这些对象可以作为GC Roots,当时的回答了它们可以永久存在,后被质疑栈的对象肯定不会永久存在。面试完成之后查了一些资料,自己也认真的思考了一下,认为这些对象之所以可以作为GC Roots是因为可以保证它们当前是一定在使用的,比如只要虚拟机栈中有某个局部变量,那么这个方法就还没有执行结束,因此它所引用的对象一定还不能被回收。如果有其它原因,欢迎沟通探讨。
4.垃圾回收算法
-
标记-清除算法:标记所有可以回收的空间,清除这些内存空间。效率比较低,回收后会产生大量不连续空间。
-
标记-复制算法:将内存空间划分为大小相同的两部分,一部分用于存放对象,标记该部分中哪个位置可以被清除,然后将所有存活的对象移动到另一块内存空间中,下次再统计另一块内存可以被清除的对象,并移动剩余的对象,如此往复。实现简单、运行效率高,但是内存利用率不高,在新生代中使用,Eden和Survivor比例是8:2。
-
标记整理算法:标记-复制算法存在的极端情况是对象都没死,在年老代有一定的几率出现,该算法把存活的对象往内存一端移动,回收另一端的内存。
-
分代收集
-
年轻代:标记-复制算法
-
年老代:标记-整理算法
-
5.垃圾收集器
针对年轻代和年老代不同的特点,设计不同的垃圾收集器。
5.1年轻代
-
Serial:单线程方式、使用“复制算法”
-
优点:没有线程交互开销,可以获得最高单线程收集效率
-
缺点:垃圾回收时需要停止其它所有工作线程
-
-
ParNew
-
Serial的多线程版本
-
-
Parallel Scavenge:并发多线程的方式,“复制算法”
-
特点:以“最优吞吐量”作为目标,可以修改参数使其满足“GC自适应调节策略”,不需要设定Eden和Survivor的比例,新生代大小等参数
-
优点:适用于注重吞吐量和CPU资源敏感的场合
-
缺点:不能保证垃圾回收时间太短
-
5.2年老代
-
Serial Old:Serial的年老代版本,单线程收集,使用“标记-整理算法”
-
使用:与Parallel Scavenge搭配使用;作为CMS收集器的预备方案适用
-
优点:简单
-
缺点:单线程,性能较差
-
-
Parallel Old:Parallel Scavenge的年老代版本,多线程回收,“标记-整理”算法
-
使用:与Parallel Scavenge搭配使用,避免Serial Old的性能拖累
-
优点:适用于注重吞吐量和CPU敏感的场合
-
缺点:不能保证回收时间最短
-
-
CMS(ConCurrent Mark Sweep):并发多线程回收、“标记-清除”算法,以最短回收时间为目标,使用最多
-
过程
-
初始标记:标记GC Roots直接关联的对象,耗时很短
-
并发标记:根据 GC Roots Tracing过程
-
重新标记:修正并发期间因为用户程序继续运作而导致标记产生变动的记录,时间略长于初始标记
-
并发清除
-
-
优点:最短回收时间
-
缺点:比较占用CPU资源,容易造成内存碎片,无法处理“浮动垃圾”,即并发清除期间产生的垃圾,因此等年老代空间还没有满的时候就应该启动垃圾回收过程,在这个过程中可能导致内存空间不够,因此需要Serial Old进行内存碎片的整理;此外,为了解决内存碎片的问题,提供了相应的参数在适当的时候(马上要进行FULL GC的时候)进行内存整理
-
5.3 G1收集器
将整个内存区域划分为多个region,分别属于不同的数据区域,包括Eden区、Survivor区、年老代等。
特点:面向服务端、并行与并发、分代收集、空间整合、可预测的停顿、使用“标记-整理”算法。比较完美,但是使用较少。
三种回收机制:
- Young GC:回收年轻代内存,Eden区满的时候触发,依旧活跃的对象晋升到Survivor区或年老代;
- Mixed GC:回收整个年轻代和一部分年老代,因为回收的年老代的大小可以指定,因此可以预测停顿时间。当年老代空间占用达到一定阈值之后启动Mixed GC,回收过程与CMS类似,包括初始标记、并发标记、重新标记和并发清除四部分。
- Full GC:Mixed GC来不及收集时导致年老代被填满时触发。
5.4 ZGC垃圾收集器
之前没有了解过这个垃圾收集器,在面试的时候被面试官问到知不知道,说这是一个比较新的垃圾收集器,效率很好,可以自己看一看。
-
JDK11推出的一款低延迟垃圾回收器,标记-复制方法,设计目标包括:
-
停顿时间不会超过10ms
-
停顿时间不会随着堆的大小或活跃对象的大小而增加,ZGC几乎所有暂停都只依赖于GC Roots集合大小
-
支持8MB~4TB级别的堆,仅适用于64位操作系统
-
-
G1垃圾回收周期,STW阶段包括初始标记、再标记、清理、复制。四个STW过程中,初始标记因为只标记GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。转移阶段要处理所有存活的对象,耗时会较长。因此,G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW。为什么转移阶段不能和标记阶段一样并发执行呢?主要是G1未能解决转移过程中准确定位对象地址的问题。
-
ZGC垃圾回收周期:STW阶段包括ZGC几乎所有暂停都只依赖于GC Roots集合大小,其中,初始标记和初始转移分别都只需要扫描所有GC Roots,其处理时间和GC Roots的数量成正比,一般情况耗时非常短;再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加。
-
关键技术:ZGC通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。大致原理描述如下:并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。那么,JVM是如何判断对象被移动过呢?就是利用对象引用的地址,即着色指针。
-
着色指针:一种将信息存储在指针中的技术,ZGC将对象存活信息存储在地址空间的42~45位中,这与传统的垃圾回收并将对象存活信息放在对象头中完全不同。
-
读屏障技术:读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。需要注意的是,仅“从堆中读取对象引用”才会触发这段代码。
-
6.触发Full GC的几种情况
- 调用system.gc()方法,不建议使用;
- 年老代空间不足,导致年老代空间不足的原因可能有多个,第一个是内存泄漏,第二个是大对象和大数组的创建,也可能是同时加载大量数据等等;
- 持久代空间不足,JDK1.8已经取消持久代;
- 使用CMS作为垃圾回收器进行垃圾回收时,浮动垃圾过多导致年老代溢出;
- 统计得到的Minor GC要移动到年老代的空间的大小大于年老代剩余的空间。