【原创】JVM 的垃圾回收与算法
程序员的成长之路
互联网/程序员/成长/职场
阅读本文大概需要 7 分钟。
1.GC 思维导图
需要xmind文件可以从公众号加我微信。
2.如何确定垃圾
引用计数法
在 Java 中,引用和对象是有关联的,如果要操作对象则必须用引用进行。
每个对象有一个引用计数器,当对象被引用一次则计数器加 1,当对象引用失效一次则计数器减 1,对于计数器为 0 的对象意味着是垃圾对象,可以被 GC 回收。
画外音:循环引用的问题
先来看一段代码
如果采用引用技术算法,上述代码中 ojb1 和 obj2 指向的对象已经不可能再被访问,彼此互相引用对方导致引用技术都不为 0,最终无法被 GC 回收,可达性算法能解决这个问题。
看下这个过程中的 Java 内存模型:
再回到前面代码 GcDemo 的 main 方法共分为 6 个步骤:
Step1:GcObject 实例 1 的引用计数加 1,实例 1 的引用计数 = 1;
Step2:GcObject 实例 2 的引用计数加 1,实例 2 的引用计数 = 1;
Step3:GcObject 实例 2 的引用计数加 1,实例 2 的引用计数 = 2;
Step4:GcObject 实例 1 的引用计数加 1,实例 1 的引用计数 = 2;
执行完 Step 4,则 GcObject 实例 1 和 实例 2 的引用计数都等于 2。
继续往下执行:
Step5:栈帧中 obj1 不再指向 Java 堆,GcObject 实例 1 的引用计数减 1,结果为 1;
Step5:栈帧中 Obj2 不再指向 Java 堆,GcObject 实例 2 的引用计数减 1,结果为 1。
到此,发现 GcObject 实例 1 和 实例 2 的计数引用都不为 0,那么如果采用的引用计数算法的话,那么这两个实例锁占的内存将得不到释放,这边产生了内存泄漏。
可达性分析
为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。这是目前主流的虚拟机都是采用 GC Roots Tracing 算法,比如 Sun 的 Hotspot 虚拟机便是采用该算法。
该算法那的核心是,通过一系列的“GC roots”对象作为起始点。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。这里涉及两个概念,一是 GC Roots,一是可达性。
画外音:不可达的对象也并非是‘非死不可’的,暂时是‘缓刑’阶段。
要真正判断一个对象死亡要经历两次标记过程:如果对象在进行根搜索后发现对象不可达,那它将会进行被第一次标记并且进行筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖 finalize() 方法或者 finalize() 方法已经被虚拟机掉用过,这两种情况都视为‘没有必要执行’。
如果对象被认为有必要执行 finalize() 方法,那么这个方法会被放置在一个名为 F-Queue 的队列之中,并在稍后由一条由虚拟机自动建立的、低优先级的 Finalizer 线程去执行。这里的‘执行’也只是指虚拟机会触发这个方法,但并不承诺一定会执行。
finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 会对 F-Queue 中的对象进行第二次小规模的标记,如果对象在finalize() 中重新与引用链上的任何一个对象建立了关联,就会被移出‘即将回收’集合,如果没有移出,那就真的离死亡不远了。
finalize() 方法只会被系统自动调用一次。
可以作为 GC Roots 的对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中的静态变量和常量所引用的对象
本地方法栈JNI中的引用的对象。
关于可达性的对象,便是能与 GC Roots 构成连通图的对象,如下图:
从上图,reference1、reference2、reference3 都是 GC Roots,可以看出:
reference1 -> 对象实例1;
reference2 -> 对象实例2;
reference3 -> 对象实例4;
reference3 -> 对象实例4 -> 对象实例 6;
对象实例 1、2、4、6 都是具有 GC Roots 可达性,也就是存货对象,不能被 GC 回收的对象。
而对于对象实例 3、5 直接虽然连通,但并没有任何一个 GC Roots 与之相连,这便是 GC Roots 不可达的对象,需要 GC 回收的垃圾对象。
画外音:再回头看看前面的代码实例(GcDemo),GcObject 实例 1 和 实例 2 虽然在引用计数上都不为 0,但从可达性算法来看,都是 GC Roots 不可达的对象。对于对象之间循环引用的情况,引用计数算法,GC 无法回收这两个对象,而可达性算法则可以正确回收。
3.标记清除算法(Mark - Sweep)
这是最基础的垃圾回收算法,分为两个阶段,标记和清除。
标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可用空间的问题。
4.复制算法(copying)
为了解决 Mark - Sweep 算法内存碎片化的缺陷而被提出的算法。
按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清理掉,如图:
这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。
且存活对象增多的话,Copying 算法的效率会大大降低。
5.标记整理算法(Mark-Compact)
结合了以上两个算法,为了避免缺陷而提出。
标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端,然后清除端边界外的对象。
6.分代收集算法
分代收集法是目前大部分 JVM 所采用的方法, 其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老年代(Tenured/Old Generation)和新生代(Young Generation)。
老年代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
新生代与复制算法
目前大部分 JVM 的 GC 对于新生代都采用 Copying 算法,因为新生代中每次来及回收都要回收大部分对象,即要复制的操作比较少,但通常并不按照 1:1 来划分来划分新生代。
画外音:新生代 GC(Minor GC)
指发生在新生代的垃圾收集动作,所有的 Minor GC 都会触发全世界的暂停(stop-the-world),停止应用程序的线程,不过这个过程非常短暂。因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。
一般将新生代划分为一块较大的 Eden 空间和两个较少的 Survivor 空间(From Space,To Space),每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中。
画外音:Eden 占新生代 8/10 空间、两个 Survivor 分别占 1/10 空间。
在执行机制上 JVM 提供了串行 GC(SerialGC)、并行回收 GC(ParallelScavenge)和并行 GC(ParNew)。
老年代与标记整理算法
老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。
画外音:老年代 GC(Major GC / Full GC)
指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。
在执行机制上 JVM 提供了串行 GC(Serial MSC)、并行 GC(Parallel MSC) 和并发 GC(CMS)。
7.垃圾回收的过程
1. Java 虚拟机提到过处于方法区的永久代(Permanet Generation),它用来存储 Class 类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
画外音:1.8 开始已经将 永久代 移除,改为了元空间。
2. 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分配到老年代。
3. 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 Minor GC,进行 GC 后,Eden Space 和 From Space 区的存活对象就会被挪到 To Space,然后 Eden Space 和 From Space 进行清理。
4. 如果 To Space 无法足够存储某个对象,则将这个对象存储到老年代。
5. 在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。
6. 当对象在 Survivor 区躲过一次 GC 后,其年龄就会 +1。默认情况下年龄到达 15 的对象会被移到老年代中。
8.垃圾收集器(简略版)
新生代:
串行GC(SerialGC)
在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。
并行回收GC(ParallelScavenge)
在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。
并行GC(ParNew)
与老年代的并发GC配合使用。
老年代:
串行GC(Serial MSC)
client模式下的默认GC方式,可通过-XX:+UseSerialGC强制指定。每次进行全部回收,进行Compact,非常耗费时间。
并行GC(Parallel MSC)(吞吐量大,但是GC的时候响应很慢)
server模式下的默认GC方式,也可用-XX:+UseParallelGC=强制指定。可以在选项后加等号来制定并行的线程数。
并发GC(CMS)(响应比并行gc快很多,但是牺牲了一定的吞吐量)
使用CMS是为了减少GC执行时的停顿时间,垃圾回收线程和应用线程同时执行,可以使用-XX:+UseConcMarkSweepGC=指定使用,后边接等号指定并发线程数。
CMS每次回收只停顿很短的时间,分别在开始的时候(Initial Marking),和中间(Final Marking)的时候,第二次时间略长。
CMS一个比较大的问题是碎片和浮动垃圾问题(Floating Gabage)。碎片是由于CMS默认不对内存进行Compact所致,可以通过-XX:+UseCMSCompactAtFullCollection。
不管做什么,只要坚持下去就会不一样!
<END>
推荐阅读:
超越99.9%人类玩家,微软专业十段麻将AI论文细节首次公布
微信扫描二维码,关注我的公众号
写留言
朕已阅