文章目录
前言
垃圾回收一般需要暂停所有线程的执行,叫stop-the-world。GC优化基本就是减少暂停次数和暂停时间。
一、回收哪里的垃圾
JVM的内存大致分为5个区,程序计数器,虚拟机栈,本地方法栈,堆,方法区。
程序计数器
顾名思义跟PC寄存器作用类似,每个线程独立存在,生命周期与线程一致。指示当前执行的方法,内存很小,忽略不计,没有垃圾。
虚拟机栈
栈空间,每个线程独立存在,保存方法参数或者方法内对象的引用。生命周期结束,比如方法执行完毕后内存会被释放,所以不需要垃圾管理。
本地方法栈
与虚拟机栈类似,对应native方法。不需要垃圾管理。
堆
对象的实际存储区域,比如在方法内new一个局部变量,在堆开辟内存,引用保存在虚拟机栈。也是垃圾管理的最主要的区域。
方法区
class文件和常量(JDK7开始字符串常量池在堆区)存储区域,属于垃圾管理范围。
二、确定哪些是垃圾
主要有两个算法,引用计数法,可达性分析法(根搜索算法)。基本所有垃圾回收器都采用可达性分析法。
引用计数法
记录每个引用的被引用个数,当引用个数为0时代表成为垃圾,应该被清理。
优点:
- 实现简单
- 引用减少至0时,实时回收内存,内存利用率高
- 回收操作可并发运行,无需暂停应用线程
缺点:
4. 无法解决环状引用,如a引用b,b引用a。两个都是垃圾却不会被清理(过于致命)
5. Space overhead: 每个对象需要格外空间存储引用次数
6. Speed overhead: 每次引用修改需要增加指令,且多线程情况下需要加锁保证原子操作
可达性分析法(根搜索算法)
确定根对象(GC ROOT),顺着根对象遍历,凡是被根对象直接或者间接引用的都不是垃圾,剩下就是垃圾。
Java中的GC ROOT对象有:
7. 虚拟机栈中引用的对象(本地变量表)
8. 方法区中静态属性引用的对象
9. 方法区中常量引用的对象
10. 本地方法栈中引用的对象(Native对象)
算法优点:
11. 不存在循环引用问题
12. 不存在引用计数法的空间和运行时开销
算法缺点:
13. 内存利用率低(在GC回收内存前,垃圾对象占用的内存无法复用
14. 设计复杂,若需要支持并发回收需要额外数据结构支撑
15. PauseTime: 根集枚举与引用Tracing过程一般需要暂停应用线程至少一次,大部分回收器要暂停2-3次
三、怎么回收垃圾
分代回收
新生代(Young generation):绝大多数最新被创建的对象都会被分配到这里,由于大部分在创建后很快变得不可达,很多对象被创建在新生代,然后“消失”。对象从这个区域“消失”的过程我们称之为:Minor GC 。
新生代又可以细分为Eden,survivor0,survivor1三个空间。默认比例8:1:1。
老年代(Old generation):
- 对象比较大,分配时直接分配在老年代。
- 多次(默认15)从新生代周期中存活了下来,会被拷贝到老年代。
- 动态对象年龄判定。
如果在Survivor区中从低年龄开始累计所有对象大小的总和大于Survivor区空间的一半时,那么年龄大于等于临界年龄的对象就可以直接进入老年代。 - 空间分配担保。
JDK 6 Update 24之前:在年轻代发生Minor GC前,虚拟机检查老年代最大可用连续空间是否大于新生代所有对象总空间,若大于,那这次Minor GC可以确保安全。若不成立,则虚拟机根据—XX:HandlePromotionFailure参数设定值是否允许担保失败,若允许,则检查是否大于历次晋升到老年代对象的平均大小,如果大于,则进行Minor GC(有风险,可能老年代空间不足分配新生代中的对象而被迫Full GC),如果小于或者设定不允许,那改为进行一次Full GC。
JDK 6 Update 24之后:XX:HandlePromotionFailure有这个参数,但不起作用,规则是只要老年代最大可用连续空间大于新生代所有对象总空间或者历次晋升的平均大小,就会Minor GC,否则Full GC
老年代区域分配的空间要比新生代多。也正由于其相对大的空间,发生在老年代的GC次数要比新生代少得多。对象从老年代中消失的过程,称之为:Major GC 或者 Full GC。
持久代(Permanent generation) 也称之为 方法区(Method area):用于保存类常量以及字符串常量。注意,这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC。发生在这个区域的GC事件也被算为 Major GC 。只不过在这个区域发生GC的条件非常严苛,必须符合以下三种条件才会被回收:
1、所有实例被回收
2、加载该类的ClassLoader 被回收
3、Class 对象无法通过任何途径访问(包括反射)
标记清除算法
确定垃圾后直接回收清除。优点是实现简单,GC耗时短。缺点是产生大量内存碎片。示意图如下。
标记复制算法
分两个空间,一个活动,一个空闲。确定活动空间中的存活对象,全部复制到空闲空间,活动空间全部回收。然后两者角色互换,活动变空闲,空闲变活动。
优点是没有内存碎片问题,缺点是一半的空间浪费(新生代中Eden始终作为活动空间,survivor0,survivor1每次垃圾回收后互换角色。所以实际只有十分之一的空间浪费)和复制对象的开销。示意图如下。
标记压缩算法
标记存活对象,然后将存活对象往一个方向(左端)复制整理。这样剩下的空间就是连续的。优点同样是没有内存碎片问题。和标记复制算法相比没有空间浪费,但是对象整理相比直接复制时间开销更大。示意图如下。
四、主要垃圾回收器
上面是垃圾回收主要涉及的算法或者说思想,下面是各个企业或者说项目的具体实现。
Serial
一个串行收集器。
Serial收集器是Java虚拟机中最基本、历史最悠久的收集器。在JDK1.3之前是Java虚拟机新生代收集器的唯一选择。目前也是ClientVM下ServerVM 4核4GB以下机器默认垃圾回收器。Serial收集器并不是只能使用一个CPU进行收集,而是当JVM需要进行垃圾回收的时候,需暂停所有的用户线程,直到回收结束。
使用算法:标记复制算法
SerialOld
SerialOld是Serial收集器的老年代收集器版本,它同样是一个单线程收集器,这个收集器目前主要用于Client模式下使用。如果在Server模式下,它主要还有两大用途:一个是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,另外一个就是作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。
使用算法:标记 - 整理算法
ParNew
ParNew其实就是Serial收集器的多线程版本。除了Serial收集器外,只有它能与CMS收集器配合工作。ParNew是许多运行在Server模式下的JVM首选的新生代收集器。但是在单CPU的情况下,它的效率远远低于Serial收集器。
使用算法:标记复制算法
ParallelScavenge
ParallelScavenge又被称为吞吐量优先收集器,和ParNew 收集器类似,是一个新生代收集器。
使用算法:复制算法
ParallelOld
ParallelOld是并行收集器,和SerialOld一样,ParallelOld是一个老年代收集器,是老年代吞吐量优先的一个收集器。这个收集器在JDK1.6之后才开始提供的,在此之前,ParallelScavenge只能选择SerialOld来作为其老年代的收集器,这严重拖累了ParallelScavenge整体的速度。而ParallelOld的出现后,“吞吐量优先”收集器才名副其实。
使用算法:标记 - 整理算法
CMS (Android4.4到Android8的默认收集器)
一个老年代收集器,全称 Concurrent Low Pause Collector(也有说Concurrent Mark Sweep),是JDK1.4后期开始引用的新GC收集器,在JDK1.5、1.6中得到了进一步的改进。它是对于响应时间的重要性需求大于吞吐量要求的收集器。对于要求服务器响应速度高的情况下,使用CMS非常合适。
CMS的一大特点,就是用两次短暂的暂停来代替串行或并行标记整理算法时候的长暂停。
使用算法:标记 - 清理
CMS的执行过程如下:
· 初始标记(STW initial mark)
在这个阶段,需要虚拟机停顿正在执行的应用线程,官方的叫法STW(Stop Tow World)。这个过程从根对象扫描直接关联的对象,并作标记。这个过程会很快的完成。
· 并发标记(Concurrent marking)
这个阶段紧随初始标记阶段,在“初始标记”的基础上继续向下追溯标记。注意这里是并发标记,表示用户线程可以和GC线程一起并发执行,这个阶段不会暂停用户的线程哦。
· 并发预清理(Concurrent precleaning)
这个阶段任然是并发的,JVM查找正在执行“并发标记”阶段时候进入老年代的对象(可能这时会有对象从新生代晋升到老年代,或被分配到老年代)。通过重新扫描,减少在一个阶段“重新标记”的工作,因为下一阶段会STW。
· 重新标记(STW remark)
这个阶段会再次暂停正在执行的应用线程,重新重根对象开始查找并标记并发阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致),并处理对象关联。这一次耗时会比“初始标记”更长,并且这个阶段可以并行标记。
· 并发清理(Concurrent sweeping)
这个阶段是并发的,应用线程和GC清除线程可以一起并发执行。
· 并发重置(Concurrent reset)
这个阶段任然是并发的,重置CMS收集器的数据结构,等待下一次垃圾回收。
CMS的缺点:
1、内存碎片。由于使用了 标记-清理 算法,导致内存空间中会产生内存碎片。不过CMS收集器做了一些小的优化,就是把未分配的空间汇总成一个列表,当有JVM需要分配内存空间的时候,会搜索这个列表找到符合条件的空间来存储这个对象。但是内存碎片的问题依然存在,如果一个对象需要3块连续的空间来存储,因为内存碎片的原因,寻找不到这样的空间,就会导致Full GC。
2、需要更多的CPU资源。由于使用了并发处理,很多情况下都是GC线程和应用线程并发执行的,这样就需要占用更多的CPU资源,也是牺牲了一定吞吐量的原因。
3、需要更大的堆空间。因为CMS标记阶段应用程序的线程还是执行的,那么就会有堆空间继续分配的问题,为了保障CMS在回收堆空间之前还有空间分配给新加入的对象,必须预留一部分空间。CMS默认在老年代空间使用68%时候启动垃圾回收。可以通过-XX:CMSinitiatingOccupancyFraction=n来设置这个阀值。
CMS 在Android的应用中,当对象分配因碎片而失败或者应用进入后台后会执行压缩,解决内存碎片问题。
粘性CMS(sticky-CMS)
sticky-CMS是ART的不移动分代垃圾回收器。它仅扫描堆中自上次GC后修改的部分,并且只能回收自上次GC后分配的对象。
CC(Concurrent Copying)
Android8开始默认垃圾收集器
CC 支持使用名为“RegionTLAB”的触碰指针分配器。此分配器可以向每个应用线程分配一个线程本地分配缓冲区 (TLAB),这样,应用线程只需触碰“栈顶”指针,而无需任何同步操作,即可从其 TLAB 中将对象分配出去。
CC 通过在不暂停应用线程的情况下并发复制对象来执行堆碎片整理。这是在读取屏障的帮助下实现的,读取屏障会拦截来自堆的引用读取,无需应用开发者进行任何干预。
GC 只有一次很短的暂停,对于堆大小而言,该次暂停在时间上是一个常量。
在 Android 10 及更高版本中,CC 会扩展为分代 GC。它支持轻松回收存留期较短的对象,这类对象通常很快便会无法访问。这有助于提高 GC 吞吐量,并显著延迟执行全堆 GC 的需要。
GarbageFirst(G1)
JDK1.7引入,1.9成为默认收集器
G1是面向服务器的一款垃圾收集器,主要针对于多核处理器的大内存机器,可以满足gc的停顿时间且保证吞吐量,一般8g以上推荐使用G1,G1抛弃了之前堆中严格的分代内存划分.G1对堆模型的处理转换成了如下图方式,将整个堆内存划分成一个个小的独立区域(Region),JVM最多可以有2048个Region,也可以用参数-XX:G1HeapRegionSize指定Region的大小,一般不推荐指定,虽然G1依然有分代内存划分,但抛弃了连续的分代,他们可以是一些不连续的Region集合,正因为这样,每一个Region区域的功能会发生变化,比如一个Region之前按是年轻代,在做完垃圾回收之后又变成了老年代。
整体采用标记-整理算法,局部是通过是通过复制算法
ZGC
最新的垃圾收集器。JDK11引入(实验性质),JDK15转正。
设计目标:
最大停顿时间不超过10ms:之所以能控制在10ms以下,是因为它的停顿时间主要跟Root扫描有关,而跟root数量和堆的大小没有关系。
最坏的情况下吞吐量相比G1会下降15%
五、参考博文
Java性能优化之JVM GC(垃圾回收机制)
JVM 垃圾回收算法与ART CC回收器实现概述
Android Runtime (ART) 和 Dalvik