这篇文章主要是我看了深入理解JAVA虚拟机这本书后做的总结笔记,也是对知识点的一个巩固和复习。
java垃圾回收机制可以用3个词来概括:where,when和how。
where即运行时内存分布情况。
when代表何时进行垃圾回收。
how表示如何回收对象。
在jvm中,“几乎”所有的对象都被分配在堆上(在即时编译优化中,如果一个对象没有逃逸出方法,那么可以分配在栈上,随着方法的出栈一同消亡),这里也是GC管理的内存区域。
在进行垃圾回收之前,首先第一步要判断的是一个对象是否还“活着”。常用的方法主要有两种:引用计数法和可达性分析法。
引用计数法的原理是在对象中添加一个引用计数器,当有一个地方对该对象进行引用时,就加1,引用失效时则减1。是一种比较简单有效的方法。但是JAVA中没有采用这种方法来进行判断,并且这种方法有缺陷,比如两个相互引用但是没有其他引用的对象就不会被判断为“死亡”对象。
可达性分析法主要是从被称为“GC Roots”的根对象开始,根据引用关系进行搜索,搜索走过的路径被称为“引用链”。当一个对象在引用图中不可达时,说明这个对象已经死亡。
在JAVA中,固定可作为GC Roots的对象有以下几种:
除了这些,还有一些其他“临时性”的对象,比如在分代垃圾收集器中,年轻代中对象可能有年老代的引用,所以可能需要把年老代的对象也加入GC Roots中。
无论是引用计数法还是可达性分析法,都离不开“引用”,在Java中一共有4种引用方式:
如果一个对象存在多种引用关系,需要使用“单弱多强”的规则进行判断,即单条引用链的强弱由最弱小的关系决定,多条引用链以最强引用链的关系来决定。
当一个对象被判断为不可达对象的时候,至少要经历两次标记过程:
在两次标记之后才会“死亡”。
这里的执行指的是开始运行,而非运行结束,主要是防止有些对象的finalize方法执行缓慢或者死循环。
所以,如果对象需要拯救自己,则在finalize方法中进行关联即可(但是该方法只能使用一次,因为第二次会被判断为虚拟机已经执行过该方法)。
一般方法区的回收性价比较低,并且判断条件苛刻。在方法区中主要对废弃的常量和不再使用的类型进行回收。
常量回收主要判断是否有地方进行引用。
类型回收需要同时满足以下3个条件:
垃圾回收算法主要分为两类:“引用计数式垃圾收集”和“追踪式垃圾收集”。
在Java中只涉及到后者,所以后面的算法都是“追踪式垃圾收集”。
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论,这个理论建立在两个分代假说之上:
以及一个推论:存在相互引用关系的对象应该倾向于同时消亡。
所以大多数GC都把Java堆划分成不同区域,每次只回收其中一个或者某些区域。根据回收的区域不同可以分为:
在HotSpot中采用OopMap的数据结构来保存引用信息,可以快速完成根节点枚举。
HotSpot只在安全点的时候生成OopMap,所有线程发现中断位为真时,在最近的一个安全点上中断,然后开始进行垃圾收集。
有些线程处于Sleep之类不执行状态的时候,无法响应中断请求。安全区域指的是某一代码片段不会发生引用改变,当线程进入安全区域之后进行声明,虚拟机不需要去管理在安全区域之内的线程。当线程需要离开安全区域时,需要判断虚拟机是否完成了根节点枚举。
为了解决跨代引用的问题,新生代或者区域中会有记忆集的数据结构用来存放引用信息,进行根节点枚举的时候只要把记忆集中的对象加入。HotSpot中记忆集记录精度为卡精度,此时记忆集被称为卡表。每个卡页中存放了多个对象,只要有一个对象存在跨代引用指针,就把整个卡页加入根节点。
由于引用变化的时候卡表需要更新,所有引入了写屏障的概念,在引用对象赋值之前或者之后进行额外的动作,分别称为“写前屏障”和“写后屏障”,在G1之前的处理器都采用写后屏障。
在较新的垃圾收集器中,只在根节点枚举的之后进行一个短暂的停顿,之后则并发地进行遍历。
遍历的时候采用3色遍历的方法:
并发遍历的时候存在两个问题:浮动垃圾和对象消失。
浮动垃圾的产生是由于引用关系改变后,一个消亡对象被误判为黑色。
对象消失则是一个被引用对象被误判为白色。
浮动垃圾的问题只需在下次垃圾回收的时候再回收就行,但是对象消亡的问题会导致程序错误。
当且仅当以下两个条件同时满足的时候,对象消亡才会发生:
为了解决这个问题,只需要破坏其中一个条件即可,所有有了两种解决方案:增量更新和原始快照。
增量更新指的是,每次黑色对象增加白色对象引用的时候记录下来,然后等并发扫描结束之后再对这些新增的引用进行一次扫描,即黑色对象重新变为灰色对象。
原始快照指的是,每次灰色对象删除白色对象引用的时候把这些引用记录下来,并发结束之后再以这些旧引用重新进行扫描,也就是先按照开始扫描的对象图快照来进行搜索。
针对年老代,采用标记清除的方式,分为四个步骤:
目标是整个内存区域,G1把内存区域划分为Region(区域),垃圾回收的衡量标准不再是新生代或者年老代,而是哪一块区域存在的垃圾比较多。
Region中还存在Humongous区域,专门存放大对象。
G1在后台维护一个回收时间和回收空间的列表,根据用户给出停顿时间不同进行回收。
G1一共分为4个步骤:
与G1类似,但是有一些不同:支持并发整理的算法,默认不使用分代收集,使用矩阵而非记忆集来维护引用关系。
具体实现步骤如下:
采用Region布局,ZGC的Region是可以动态销毁和创建的,分为3种Region:
ZGC的核心是他的染色体指针技术,即把少量信息存放在指针上,ZGC一共采用了4个标志位:三色标记状态(占2位)、是否进入重分配集(即被移动过)、是否只能通过finalize方法才能访问。
但是指针变动会导致地址发生变化,所以在Linux和x86-64平台上,ZGC采用了多重映射的方法将多个虚拟地址映射到同一个物理地址上。
ZGC收集器相比G1收集器,多出来的步骤主要如下:
以上内容大致概括了书中第三章的内容,但是很多地方仅仅只是做了一个归纳,没有展开分析。
关于G1垃圾收集器,有一篇博客写的非常好和详细:面试官问我关于G1怎么知道你是什么时候的垃圾。
接下来几篇文章应该都是有关于JVM的,都是一些面经上热点问题的相关知识点和拓展,在完成JVM部分之后会继续写有关于JAVA并发和一些基础知识点的文章,最后完成有关于操作系统数据库和计算机网络部分的文章。