简要回顾各垃圾回收器的核心思想

人一老 就容易忘事


Serial 新生代单线程 复制到老年代

ParNew 新生代多线程 复制到老年代

Parallel Scavenge 新生代多线程 复制到老年代
关注点在 停顿时间 ,因此可以设置最大回收停顿时间,自动调节各代大小
和G1都没有用传统的GC框架,因此无法和老年代CMS共用


Serial Old 老年代单线程 标记整理

Parallel Old Parallel Scavenge的老年版,标记整理
配合Parallel Scavenge 可以只关注吞吐量

CMS 老年代并发 标记清除 1.4.1
初始标记,标记GC Root(初始标记的root并不包括年轻代,结合并发标记阶段看实际上年轻代也是GC Root的一部分)直达对象,阻塞
并发标记,从初始标记的结果开始爬可达性树,非阻塞
重新标记,并发标记完成后,修正在并发标记期间而变化的记录,阻塞
并发清除

缺点:
需要多核环境
并发清除时产生的垃圾只能留到下次,如果并发清除时内存不足只能Full GC
标记清除带来空间碎片,可以设置在Full GC前整理(默认开启)


G1 管理整个GC堆 但是依然有分代概念 6u14面世 7u4正式推出
准备替代CMS
G1暂停时需要复制对象,CMS暂停时只需要扫描对象,因此6G以下CMS不一定比G1差。推荐6G以上堆,可达到0.5s以下GC时间。

可用对象占用50%以上堆时
对象分配/变老速率显著变化时(CMS并发标记时,如果依然在高速分配内存,会导致很久的remark),
较长时间的GC/压缩发生时(0.5-1s以上)
建议切到G1,可以获得收益

设计理念:
停顿可预测:可以避免收集整个堆,而是跟踪每个Region中的垃圾大小和回收所需时间(也就是价值),优先回收价值高的Region,也是名字的来由
无碎片:内存分为等大的Region,整体为标记整理,实际是复制Region。
并行:扫描对象和复制对象分开运行,并且扫描对象的【初始标记】会借用复制对象的【年轻代复制】步骤。
空间换时间:使用Remembered Set记录老年代指向年轻代的指针,以及使用Collection Set记录需要收集的Region。

收集器部分

复制对象的两种运行模式:
Young gc:新生代满时运行,扫描所有年轻代的Region,找出半死的和全死的Region构成Collection Set。
并行的干掉死透的,并且把半死的Region中活对象复制到别的Region中。通过控制年轻代个数来控制开销

Mixed gc:扫描所有年轻代Region,和 全局并发标记 得出的高价值老年代Region构成Collection Set
。根据用户指定开销来调节价值范围,当mixed gc也清不出足够内存,老年代填满,就会用Searial Old 的核心代码 来Full GC。System.gc()也是Full GC,XX:+ExplicitGCInvokesConcurrent 会改为强行启动一次全局并发标记。

每个Region有对应的Remembered Set,只记录老年代到年轻代的 别的Region对本Region对象的引用,在写引用的时候阻塞,如果Region改变就把引用信息改到新Region的RSet中

全局并发标记的步骤:
初始标记,同CMS只扫描GC Root直达对象,压入扫描栈,阻塞

并发标记,弹栈,递归扫描对象图,还会扫描Write Barrier记录的引用(这些引用是每次改变引用时的老引用),非阻塞

重新标记,标记每个线程各自的Write Barrier(这个Barrier满了之后会丢到全局的去,而每个线程还有一个没满的Barrier),顺带处理弱引用,阻塞。只扫描SATB Buffer,而不是和CMS一样重新扫描整个GC Root,整个年轻代都会被扫描,可能会很慢

清理,类似于标记清理的清理阶段,但是不清理实际对象,而是计算每个Region的价值,根据用户要求的性能水平( -XX:MaxGCPauseMillis)优先清理价值高的,阻塞,所以如果要求的性能太高,反而容易造成垃圾堆积进而Full GC。

CSet永远包括年轻代,因此G1不维护年轻代出发的引用涉及的RSet更新

记录引用变化的部分

SATB Barrier
G1的设计思路是,一次GC开始时 活的对象认为是活的,并且记录为SATB(理解成快照) ,GC过程中新分配的都当做活对象。

GC中新分配的对象容易找,每个Region会记录两个TAMS指针(top-at-mark-start),此后的对象视为新分配的。

但是全局并发标记的并发标记过程中,由于和其他线程并行执行,会出现这种情况:

首先假设有一个对象的引用没有被标记过,记为A(其实就是白色状态)
前提1:给一个已经标记过、并且所有字段被标记完(黑色状态)的对象的字段赋值为A
前提2:并且所有 字段没有被标记完的引用(灰色状态) 到A的引用被删除了

这时候会出现A明明活着 却没有被标记到的情况,因此G1引入了两个WriteBarrier,在改变引用 前后 都会把老引用记下来,哪怕发生了前提2的事,A也会被标记下来。

Logging Barrier

G1为了尽量避免降低改变引用的性能,改变引用时 其实是将老引用加入一个队列,满了之后会被移到一个全局的SATB队列集合,然后换一个新的空队列。

而并发标记会定期检查全局SATB队列集合,当超过一定量时就把它们全部标记上,并且把它们压到标记栈上等后面进一步标记。

Remember Set

传统GC的
G1给每个Region维护一个Remembered Set,它记录别的Region指向本体的指针,并且这些指针分别在哪些Card Table的范围内。

维护Remembered Set的逻辑在改变引用 做,过滤掉从年轻代出发的引用 涉及的RSet维护。

维护RSet时也会采用Logging Barrier的设计思路,在全局队列集合超过一定量时,会取出若干个队列,并且更新RSet。


ZGC
并不是新货,而是Azul很久之前的Pauseless GC,而不如Zing VM的C4。
所有阶段都可以并发,很容易最大暂停控制在1ms内。

不标记对象,而是标记引用,访问引用时有Read Barrier,消耗读取引用时的性能,而干掉了STW。
Region有多种尺寸,根据对象大小分配
每次清理整个Region,因此没有RSet
支持NUMA,提高整体效率
没有分代(暂不完善,还在考虑是分代还是Thread Local GC作为前端),因此只有PGC的水平,遇到高速分配对象只能调大堆内存来喘息

初始扫描:只扫描全局变量和线程栈的指针,不扫描GC堆的指针
并发标记:递归对象图
移动对象:在移动过程中有forward table记录移动,并且活的对象移走后可以立即被释放,可以被其他扫描过程用来复制
修正指针:在修正时同时进行标记

关于finalize(),System.gc()和C#、C++的析构函数的一点笔记

2018-4-2 补充:

在阅读了深入理解JVM后,作者的这一段话彻底理清了这一块:
“需要说明的是,上面关于对象死亡时finalize()方法的描述(前文描述了在finalize()方法中让对象重新被引用,从而可以避免一次垃圾回收)可能带有悲情的艺术色彩,…笔者建议大家避免使用它,…而是Java刚诞生时为了使C/C++程序员更容易接受它做出的妥协。它运行代价高昂,不确定性太大,无法保证各个对象的调用顺序,有些教材描述它适合用来关闭外部资源,这完全是对这个方法用途的一种自我安慰,finalize()能做的,finally块等其他方式能做的更好、更及时,所以笔者建议大家完全可以忘掉Java中有这个方法的存在。”

这个方法实质是一种妥协,缺点见下文。

先说finalize。

在我之前的个人理解里,finalize方法并不会直接导致该对象被释放/回收;因此它不是C++意义上的析构函数。
(事实上析构函数的作用是定义对象被释放时候的行为,是对析构函数的调用,而不是析构函数本身导致对象被释放)
它用于 标记在对象被回收时应该做一些事情;例如释放其他资源,关闭连接等。

以下说明摘抄自1.6的API文档:

当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。子类重写 finalize 方法,以配置系统资源或执行其他清除。
finalize 的常规协定是:当 Java虚拟机已确定尚未终止的任何线程无法再通过任何方法访问此对象时,将调用此方法,除非由于准备终止的其他某个对象或类的终结操作执行了某个操作。finalize 方法可以采取任何操作,其中包括再次使此对象对其他线程可用;不过,finalize 的主要目的是在不可撤消地丢弃对象之前执行清除操作。例如,表示输入/输出连接的对象的 finalize 方法可执行显式 I/O 事务,以便在永久丢弃对象之前中断连接。
Object 类的 finalize 方法执行非特殊性操作;它仅执行一些常规返回。Object 的子类可以重写此定义。
Java 编程语言不保证哪个线程将调用某个给定对象的 finalize 方法。但可以保证在调用 finalize 时,调用 finalize 的线程将不会持有任何用户可见的同步锁定。如果 finalize 方法抛出未捕获的异常,那么该异常将被忽略,并且该对象的终结操作将终止。
在启用某个对象的 finalize 方法后,将不会执行进一步操作,直到 Java 虚拟机再次确定尚未终止的任何线程无法再通过任何方法访问此对象,其中包括由准备终止的其他对象或类执行的可能操作,在执行该操作时,对象可能被丢弃。
对于任何给定对象,Java 虚拟机最多只调用一次 finalize 方法。
finalize 方法抛出的任何异常都会导致此对象的终结操作停止,但可以通过其他方法忽略它。


JDK9的API中,该方法已经被废弃:

已过时。 finalize机制本质上是有问题的。finalize可能导致性能问题,死锁和挂起。
finalize方法中的错误可能导致资源泄漏; 如果 finalization不再需要,无法取消 ; 并且在不同对象的finalize方法的调用中没有指定排序。 此外,finalization的时间并不能得到保证。finalize方法可能只能在无限期的延迟之后,才调用到可终结的对象上。
如果一个类的实例持有非堆资源,那么它应提供一种方法,来实现这些资源的显式释放,如果适用,它们还应实现AutoCloseable 。
Cleaner和PhantomReference在对象变得不可达时提供更灵活和更有效的方式来释放资源。

API Note:
嵌入非堆资源的类,具有许多清除这些资源的选项。 该类必须确保每个实例的生命周期 比其嵌入的任何资源的寿命都要长。
当嵌入在对象中的资源正在使用时,可以使用Reference.reachabilityFence(java.lang.Object)来确保对象保持可访问。

一个子类应该避免覆盖finalize方法,除非子类嵌入在收集实例之前必须清理的非堆资源。
与构造函数不同,调用子类的finalize()不会自动调用父类的finalize()方法。如果一个子类覆盖了finalize,必须明确地调用超类终结器。
为了防止异常提前终止终结链,子类应该使用一个try-finally块来确保总是调用super.finalize() 。 例如:

1
2
3
4
5
6
7
8
@Override 
protected void finalize() throws Throwable {
try {
... // cleanup subclass state
} finally {
super.finalize();
}
}

结论:在1.9之后,我们不应该用这个方法来实现 让一个类被回收时做某些事 的功能。如果需要做某些事,应当显式的提供做这些事的方法,例如实现AutoCloseable接口,用虚引用和Cleaner等新版的功能。

再说System.gc()。

从入门开始,我便被各种书籍告知,不要手动调用System.gc()。那么这个方法到底做了什么呢?是立即发起垃圾回收,还是催促虚拟机进行一次垃圾回收呢?
看看API文档:

调用gc方法表明,Java虚拟机花费了回收未使用对象的努力,以使其当前占用的内存可用于快速重用。
当控件从方法调用返回时,Java虚拟机已经尽力从所有丢弃的对象中回收空间。

不知道具体JVM的实现是什么,至少这段话我的理解就是 立即发起一次垃圾回收……
由于垃圾回收会停止主程序活动,而且的确实际开发中也没有理由手动发起垃圾回收,总之还是不要用的比较好(

联动:C#的析构函数

学习finalize的时候,往往会同时提起析构函数这一C系语言的产物。
但是在C#中,析构函数和finalize的作用,在我的理解中是一致的:即 让这个类在被回收前做一些事情。

不过C#的析构函数并没有被废弃,微软同时建议使用IDispose接口,实现Dispose()方法。
不过和Java的AutoCloseable不同,Dispose方法需要被重载两次,第一次是表示该对象不执行析构函数:

1
2
3
4
5
6
7
public void Dispose()
{
// Dispose of unmanaged resources.
Dispose(true);
// Suppress finalization.
GC.SuppressFinalize(this);
}

第二次才是真正的执行逻辑,此时的重载是Dispose(bool),如果对它方法的调用是来自Dispose(),则参数为true;如果来自析构函数,则参数为false。因此需要对参数进行判断:当调用来自Dispose()时,释放非托管资源。

联动:C++的析构函数

水平有限,不做拓展.
大致可以看出也是在对象生命周期结束时被自动调用,但是由于没有自动内存管理,C++的析构函数会在如下几种情况被调用:

  • 使用 delete 运算符 显式解除 分配了使用 new 运算符分配的对象。 使用 delete 运算符解除分配对象时,将为“大多数派生对象” 或 “属于完整对象,但不是表示基类的子对象的对象”释放内存。 此“大多数派生对象”解除分配一定仅对虚拟析构函数有效。 在类型信息与实际对象的基础类型不匹配的多重继承情况下,取消分配可能失败。
  • 具有块范围的本地(自动)对象超出范围。
  • 临时对象的生存期结束。
  • 程序结束,并且存在全局或静态对象。
  • 使用析构函数的完全限定名显式调用了析构函数。