深入理解java虚拟机2——垃圾回收

1.判断对象是否可以被回收

1.1 引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;

优点:原理简单,判定效率很高。

缺点:两个对象相互引用对方,但都不再被访问,则无法回收。

深入理解java虚拟机2——垃圾回收_第1张图片

1.2 可达性分析算法

这个算法的基本思路就是通过 一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过 程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

可作为GC Roots的对象:

~线程方法堆栈中使用到的参数、局部变量、临时变量等。

~在方法区中类静态属性引用的对象。

~在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。

~在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

~Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton, OutOfMemoryError)等,还有系统类加载器。

~所有被同步锁(synchronized关键字)持有的对象。

~反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

2. 强软弱虚引用

强引用 类似“Object obj=new Object()”这种引用关系。 永远不会被回收
软引用 SoftReference softReference = new SoftReference(obj); 在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象
弱引用 weakReference weakReference = new weakReference(obj); 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定很快发现那些只具有弱引用的对象。
虚引用  

与其他几种引用都不同,虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存

强引用:

//只要 obj 指向 Object 对象,那它就永远都不会被 JVM 回收
Object obj = new Object();
//将 obj 置为 null,可以切断引用链,这样 obj 就会被 JVM 回收
obj = null;

软引用

String str = new String("abc");
SoftReference softReference = new SoftReference(str);

但是,上述例子中gc仅仅回收了str对象,softReference对象仍然存在,为了方便清除软引用对象本身,需关联引用队列。

如果软引用所引用对象被垃圾回收JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

//对象
String str = new String("abc");

//构建引用队列
ReferenceQueue queue = new ReferenceQueue<>();

//创建SoftReference对象时关联queue和该对象
SoftReference softreference = new SoftReference<>(str, queue);

虚引用:和软引用用法相同

3.生存和死亡

即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓 刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:

第一次标记判断是否该回收:没有GC Root的引用链——>应该要被回收

第二次标记判断是否要执行finalize()方法:回收:没有重写过finalize()方法或者finalize()方法已经被执行过;否则执行

4.垃圾回收

4.1 垃圾回收理论

(1)绝大多数对象是朝生夕灭的;

(2) 熬过越多次垃圾收集过程的对象就越难以消亡:因此划分 新生代(伊甸园+生存区From+生存区To) + 老年代

(3)跨代引用相对同代引用来说占极少数

4.2 安全区域

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化。在这个区域中任 意地方开始垃圾收集都是安全的。

4.3 记忆集和卡表

为了减少GC Root的扫描范围(如在老年代垃圾回收时,存在跨代指针,需要再扫描年轻代的GC ROOT),引入了记忆集。

记忆集:是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,是一种来解决对象跨代引用所带来的问题的概念(非实现方法)

卡表:记忆集的一种实现方式(HashMap和Map的关系),是一个字节数组

数组中的每个元素分别对应了一块地址范围(卡页card page)。HotSpot卡页大小是512字节,那如果卡表标识内存区域的起始地址是0x0000的话,数组CARD_TABLE的第0、1、2号元素,分别对应了 地址范围为0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡页内存块。

如果某个元素对应的卡页(范围地址中存在跨代指针),那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。

维护卡表的方式:写屏障。每次对“引用类型字段赋值”,都会像AOP切面一样加入写前和写后屏障更新卡表。

4.4 并发问题下如何标记GC ROOT

增量更新(Incremental Update)和原始快照(Snapshot At The Beginning, SATB)

增量更新:CMS; 原始快照:G1

4.5 垃圾回收算法

(1)标记清除(Mark-Sweep):首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

深入理解java虚拟机2——垃圾回收_第2张图片

优点:速度快

缺点:容易产生内存碎片,大对象无法分配而导致额外垃圾回收;执行效率不稳定,如果有大量对象且绝大多数需要回收,则需要大量的标记和清除。

(2)标记整理(Mark-Compact):多数用于老年代,会引发STW

先标记,再让所有对象都向内存空间一端移动,然后直接清理边界以外的内存。

深入理解java虚拟机2——垃圾回收_第3张图片

优点:无碎片

缺点:由于涉及存活对象的移动,效率低

(3)标记复制:多数用于新生代

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

优点:无碎片,只需移动堆顶指针按顺序分配;由于大量的对象是朝生夕灭的,复制的对象不需要很多。

缺点:内存减半,空间浪费。

(4)java实际分代

实际使用中,java采用分代收集,即由于不同对象生命周期不一样,可以将不同生命周期的对象分代,不同的代采取不同算法

深入理解java虚拟机2——垃圾回收_第4张图片

对象首先被分配在伊甸园区

新生代空间不足时,出发minor gc,伊甸园和From区存活的对象使用复制算法复制到To中,存活对象年龄+1,交换From和To

minor gc时会引发stop the world(stw),暂停其他用户线程,等垃圾回收结束,用户线程才恢复运行

当对象寿命超过阈值时,会晋升老年代,最大寿命是15

当老年代空间不足,会先尝试出发minor gc,如果空间仍然不足,会出发Full gc(新生代老年代都回收),stw时间更长

5.垃圾回收器

 

深入理解java虚拟机2——垃圾回收_第5张图片

注:由于维护和兼容性测试的成本,在JDK 8时将Serial+CMS、 ParNew+Serial Old这两个组合声明为废弃(JEP 173),并在JDK 9中完全取消了这些组合的支持

https://zhuanlan.zhihu.com/p/56047554

5.1 Serial收集器:新生代,复制算法,简单高效,适合单CPU

最古老的,最稳定,效率高的收集器,只使用一个线程去回收但其进行垃圾回收过程中可能会产生较长的停顿。虽然在收集垃圾的过程中需要暂停其他的工作线程,但是简单高效,对于单CPU环境来说,没有线程交互的开销(内存消耗小)可以获得最高的单线程垃圾收集效率,因此Serial垃圾回收器依然是Java虚拟机运行在Client模式下默认的新生代垃圾回收器

深入理解java虚拟机2——垃圾回收_第6张图片

5.2 Serial Old:老年代,标记整理算法,简单高效

开启串行收集器的JVM参数是-XX:+UseSerialGC

开启后会使用:Serial(Young区)+ Serial Old(Old区)的收集器组合。

在Server模式下:作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

5.3 ParNew:新生代,复制算法,Serial的多线程版

许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作。

ParNew 收集器在单CPU的环境不如Serial收集器,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。在多CPU环境下有优势,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。它默认开启的收集线程数与CPU的数量相同,可使用-XX:ParallerGCThreads参数设置。

深入理解java虚拟机2——垃圾回收_第7张图片

5.4 Parallel Scavenge收集器:新生代,多线程,复制算法,吞吐量优先

吞吐量:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间(用户体验),而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务

与ParNew的区别:还提供了一个参数-XX:+UseAdaptiveSizePolicy,开启后虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为GC自适应的调节策略(GC Ergonomics)

5.5 Parallel Old:老年代,整理标记,多线程,吞吐量优先

Parallel Old收集器是Parallel Scavenge收集器的老年代版本

注重吞吐量以及CPU资源敏感的场合适用

深入理解java虚拟机2——垃圾回收_第8张图片

5.6 CMS:老年代,标记清除,最短回收停顿时间,注重用户体验

CMS(Concurrent Mark Sweep)收集器工作的整个流程分为以下4个步骤:

  • 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  • 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。且由于1/4线程用来标记,会影响吞吐量。
  • 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
  • 并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象。会产生浮动垃圾

深入理解java虚拟机2——垃圾回收_第9张图片

建议:阈值设置70%到80%,如果不够,CMS运行期间预留的内存无法满足程序分配新对象的需要,会冻结用户线程退化成SerialOld

优点:STW时间少,用户体验好

缺点:

1. CPU资源敏感:CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。

2. 无法处理浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。

3.标记-清除算法导致的空间碎片:空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。提前Full GC。

5.7 G1收集器:整体基于标记整理;局部基于复制算法;运行期间不会产生碎片;可预测的停顿:

5.7.1 概述

上述的 GC 收集器将连续的内存空间划分为新生代、老生代和永久代,这种划分的特点是各代的存储地址(逻辑地址)是连续的。G1 (Garbage First) 的各代存储地址是不连续的,每一代都使用了 n 个不连续的大小相同的 region, 每个 region 占有一块连续的虚拟内存地址。Region中还有一类特殊的Humongous区域,专门用来存储大对象。(G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。)

深入理解java虚拟机2——垃圾回收_第10张图片

G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

5.7.2 特点

1.主要面向服务端应用:JDK9后,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS未来则会被废弃

2.分代收集:哪块内存中存放的垃圾数量最多,回收收益最大 来衡量:在后台维护一 个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些Region

3.空间整合(整体基于标记整理;局部基于复制算法

4.可预测的停顿(能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒)

5.7.3 几个细节

1. 跨Region引用问题:如何避免全堆扫描

多个 Region 之前的对象可能会有引用关系,在做可达性分析时需要扫描整个堆才能保证准确性,这显然降低了 GC 效率。

为避免全堆扫描,虚拟机为 G1 中每个 Region 维护了一个与之对应的 Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的Region的 Remembered Set 之中。当进行内存回收时,在GC根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏。

2.多线程下并发标记阶段如何保证收集线程和用户线程互不干扰

原始快照(SATB)算法:G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。

3.怎样建立起可靠的停顿预测模型

衰减均值(Decaying Average)为理论基础:在垃圾收集过程中,G1收集器会记 录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得 出平均值、标准偏差、置信度等统计信息。Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由 哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。

5.7.4 G1的运作步骤:

  • 初始标记(Initial Marking) :STW耗时短
    • 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
  • 并发标记(Concurrent Marking) :并发耗时长
    • 从GC Root 开始对堆中对象进行可达性分析递归扫描整个堆 里的对象图,找出要回收的对象找到存活对象,此阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。
  • 最终标记(Final Marking):STW耗时短
    • 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。
  • 筛选回收(Live Data Counting and Evacuation):STW耗时短
    •  首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

深入理解java虚拟机2——垃圾回收_第11张图片

5.7.5 G1小结

  • 理念变化:从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率 (Allocation Rate),而不追求一次把整个Java堆全部清理干净。
  • 对比CMS
    • 取代:在未来,G1收集器最终还是要取代CMS 的
    • 优点:可以指定最大停顿时间;按收益动态回收;不会有空间碎片
    • 缺点:如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载 (Overload)都要比CMS要高。
  • 内存占用:虽然G1和CMS都使用卡表来处理跨代指针,但G1的卡表实现更为复杂,而且 堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和 其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间;相比起来CMS的卡表就相当简单, 只有唯一一份,而且只需要处理老年代到新生代的引用,反过来则不需要,由于新生代的对象具有朝 生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的

6.垃圾回收器总结

收集器 串行/并行/并发 新生代/老年代 算法 目标 适用场景
Serial 串行 新生代 复制 响应速度优先 单CPU下的Client
Serial Old 串行 老年代 标记整理 响应速度优先

单CPU下的Client

CMS的退化

ParNew 并行 新生代 复制 响应速度优先

多CPU下的Server

配合CMS

Parallel Scavenge 并行 新生代 复制 吞吐量优先 在后台运算而不需要太多交互的任务
Parallel Old 并行 老年代 标记整理 吞吐量优先 在后台运算而不需要太多交互的任务
CMS 并发 老年代 标记清除 响应速度优先 集中在互联网站或B/S系统服务端上的JAVA应用
G1 并发 都有 局部复制,整体标记整理 响应速度优先 面向服务端应用,将来替换CMS

参照:

https://crowhawk.github.io/2017/08/15/jvm_3/

https://zhuanlan.zhihu.com/p/56047554

你可能感兴趣的:(java,JVM)