Week 9 作业 01
垃圾回收原理
Java 堆中存放着几乎所有的对象实例,回收之前要确定其是否存活着,有两种思路可以判断对象存活
引用计数算法
如果有其他对象对其引用,则计数器加一,引用失效的时候,计数器减一。计数器为 0 时则可以被回收。
弊端:无法解决对象互相引用的问题,他们是内存中的孤岛,但却无法被回收 例如: A.a = B;B.b = A
可达性分析算法
通过一系列称为GC Roots
的对象作为起始点,然后向下搜索,走过的路径为引用链,如果一个对象到 GC Roots 没有任何引用链,则说明此对象是不可达(不可用)的。
四种引用
强引用
普通的引用方式, A a = new A()
, 只要强引用还存在,GC 就不会回收。
软引用
描述有用但非必需的对象。在发生内存溢出异常之前,会把软引用的对象列为回收范围第二次再回收。如果第二次回收没有足够的内存,才抛出异常。 可以使用 SoftReference
弱引用
GC 总会回收掉被弱引用关联的对象。 WeakReference
虚引用(幽灵引用,影子引用)
最弱的引用。能在被 GC 回收时得到一个通知。 phantomReference
垃圾回收算法
标记-清除算法(Mark-Sweep)
标记阶段标记出所有要回收的对象,标记完成后统一回收。
缺点:效率不高;内存不连续,导致之后需要分配大对象时无法找到足够的连续内存而再次进行垃圾回收。
复制算法(Copying)
将可用内存按容量划分为大小相等的两块,每次只用其中一块。用完一块后将上面还存活的对象复制到另外一块上。这样就没有碎片的问题了。
缺点:内存被缩小为之前的一半,代价太高。
但实际商业上新生代都是按照复制算法进行回收的,因为大多对象都是朝生夕死,所以并不是按 1:1 划分,而是将内存分为较大的 Eden 空间和两块较小的 Survivor 空间。比例为 8:1:1,这样可用空间就大大提升了。
标记-整理算法(Mark-Compact)
与标记-整理算法一样,但是不是直接对可回收对象进行清理,而是让所有存活的向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法
本质上不是一种算法,而是根据对象存活周期的不同将内存划分为几块。Java 堆一般分为新生代和老年代。会根据不同代的特点选用不同的算法。
垃圾收集器
垃圾收集器是内存回收的具体实现,下面列举的是基于 JDK 7 的 HotSpot 虚拟机。其中 CMS 和 G1 仅做简单介绍,但这两个收集器都是相对比较复杂的收集器。
Serial(新生代) / Serial Old(老年代)
是一个单线程的收集器,在收集的时候必须暂停其他所有的工作线程(Stop the World),这样是为了避免一边打扫垃圾又一边制造垃圾
Serial 收集器到现在为止依然是 Client 模式下的默认新生代收集器,因为简单且高效。
ParNew
多线程,其余与 Serial 一样。CMS(一个老年代的收集器)只能与 ParNew 或者 Serial 配合使用。
Parallel Scavenge(新生代) / Parallel Old(老年代)
看起来与 ParNew 一样,但他的目标是吞吐量,即 CPU 用于运行用户代码的时间与 CPU 的总运行时间的比值。通常被称为吞吐量优先的收集器
CMS
目标是以获取最短回收停顿时间为目标的收集器。
G1
当今收集器技术发展的最前沿成果,主要面向服务端应用,目标是替换掉 CMS 收集器,特点有:
并行与并发,可以缩短 Stop-The-World 时间
分代收集,但是不用与其他的收集器配合,自己就可以管理整个堆
空间整合
可预测的停顿
内存分配与回收策略实践
这部分重要了解几个内存分配的常见情况,同时熟悉虚拟机参数的配置。
对象优先在 Eden 分配
Code Gist
虚拟机参数设置:-verbose:gc -Xms20M -Xms20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
Eden 区属于新生代,优先在此区域分配,如果没有足够的空间会执行一次 Minor GC。Gist 里的代码展示了分配 3 个 2MB 大小,一个 4MB 大小的对象,可以看到第一次执行了 Minor GC,结果是 6MB 的对象移入了老年代,4MB 的新对象放入了 Eden 区。 -XX:SurvivorRatio=8
是将 Eden 与 Survivor 区域比值设置为 8:1
可以看到经过一次 GC,新生代中有 6M 的内容被移入了老年代。
Terminal1
大对象直接进入老年代
Code Gist
虚拟机参数设置:-verbose:gc -Xms20M -Xms20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728
通过设置 -XX:PretenureSizeThreshold=3145728
可以使超过 3M 的对象直接在老年代进行分配,运行完代码可以看到,4M 的内容直接在老年代上了。
长期存活的对象将进入老年代
Code Gist
虚拟机给每个对象设置了一个年龄(Age)计数器,如果对象在 Eden 区出生且经过一次 Minor GC 还存活,就将其放入 Survivor 区,并使年龄增加 1,对象在 Survivor 区域每熬过一次 Minor GC 年龄都会增加 1,当增加到默认值 15 岁时就会晋升入老年代中。
这个默认阈值可以通过 -XX:MaxTenuringThreshold
设置
从运行结果上可以看到,尽管 alloc1 只要 256KB,但在第二次 GC 的时候还是被移入了老年代
Terminal2
动态对象年龄判定
这个实际上是对上一条的优化,有时候并不是要求对象的年龄必须到了 MaxTenuringThreshold
才能晋升老年代,如果 Survivor 空间中相同年龄所有对象大小的总和大于 SUrvivor 空间的一半,则年龄大于或等于该年龄的对象直接进入老年代。
对象的 finalize() 函数可以作为拯救自己的手段,也就是在的对象 finalize() 中使其与 GC Roots 从新建立关系,这样就不会被收集。但这是一个投机取巧的办法,不鼓励使用。而且 finalize()方法也只会被执行一次。
JVM 垃圾回收原理及实践 | Hack | Yifan (zhuyifan2013.github.io)
JVM 垃圾回收器工作原理及使用实例介绍 – IBM Developer
评论