GC面临的问题有三个:哪些内存需要回收、什么时候回收和怎么回收
-
哪些内存需要回收,一般有两种方法
-
引用计数
对每个对象都有个被引用的次数,单被引用的次数为0的时候,就表示对象需要被回收
引用计数的缺点是没有办法解决循环引用导致的内存泄露问题
-
可达性分析
现在主流的GC方法都使用可达性分析
以GC root为起点,遍历整个树(或者图),如果没有到达过某个对象,则表示这个对象需要被回收
GC Root表示引用链的起点,包括函数调用栈中引用的对象、static静态变量和常量的引用对象、方法区中常量引用的对象,本地方法栈中JNI引用的对象
也可以分为栈中的对象、永久代的对象、本地方法栈中JNI引用的对象
-
-
object什么时候会被回收
-
一般而言,只要没有引用指向object,就可以在gc的时候被回收(老年代需要在Full GC,永久代需要在设定的几次Full FC)。但是为了丰富引用的种类,以适应各种应用,JDK1.2中加入了4中引用,但是除了强引用,其生命周期会有所不同,生存能力递减。
-
强引用(Strong Reference)
即普通引用
-
软引用(Soft Reference)
当触发OutOfMemoryException之前,会触发第二次GC,回收这些Object
-
弱引用(Weak Reference)
当触发GC的时候,回收这些Object
-
虚引用(Phantom Reference)
对Object的生存无意义,当Object被回收时,会触发虚引用的通知
-
-
对已object在被回收的时候,还会有一个特例,就是定制Object的finalize方法,使之重新与GC Root关联,可以使Object逃脱回收
finalize方法在object被GC的时候会被执行
但是下次被GC的时候,不会再次执行finalize方法(finalize方法在生命周期内只会执行一次),所以不会再次逃脱回收
-
方法区或者Hotspot的永久代也会进行GC
普通常量池常量只要没有引用指向它,就可以被回收
-
类的回收判定需要满足3个条件
堆中不存在该类实例,即没有指向该类的引用(按照jvm的内存布局,类实例是会有指向类信息的pointer或者handler的。
加载该类的class loader已被回收。(class loader应该存在操纵其加载的类的方法,即还存在某种联系)
该类对应的java.lang.Class对象没有在任何地方被引用,在任何地方无法通过反射访问该类的方法(如果存在这种引用,则可以通过反射的机制,进行访问该类,这会导致错误,有点像race condition,持有缓存,内容却被改变了)
由这3个条件应该可以推论出:类实例、class loader、对应的java.lang.Class类和类信息都存在某种联系,使得可以通过这些东西操纵或者访问类信息。
-
-
对于内存怎么回收的问题,内存的回收算法一般分为三种
-
标记-清除
先对需要进行回收的内存进行标记,然后在进行清除
-
复制算法
将内存分为相等的两块,每次只使用其中一块,当当前使用的内存块使用完了之后,将存活的对象复制到另一内存块
Hotspot将新生代分为一个Eden和两个Survivor区,默认比例为8:1:1
minor gc的时候,将eden去和当前survivor区的存活对象复制到另一块survivor区
如果survivor区不足以放置存活的object,使用分配担保机制,部分object将直接进入老年代
-
标记-整理
先对存活的object进行标记,然后将object进行整理(统一往一端移动),最后将剩余的内存进行回收
-
分代收集
就是将内存分为不同的区域,一般分为新生代和老年代两部分,然后在不同的部分应用不同的收集算法
一般来说新生代使用复制算法,因为一般只会有少量object存活
老年代使用标记-清除或者标记-整理算法,因为对象存活率高,没有另外的内存进行分配担保
-
Hostspot的算法实现
-
枚举根节点
为了避免race condition的问题,这里需要进行stop the world的操作,保证一致性
为了提高标记的效率,降低stop the world的时间,商业vm一般都会使用准确式GC,会使用一些方法记录下引用的准确位置,避免全局内存扫描
Hostspot使用的是OopMap的数据结构来达到这个目的
OopMap的具体使用方法还需要深入了解
-
安全点
由于能导致OopMap变化的指令非常多,所以Hostspot设置安全点,只有在安全点才会生成OopMap,这也导致只有在安全点才能停顿下来进行GC
-
如何使所有线程进入安全点的方法有两个
-
抢先式中断,基本上不用
先把所有线程中断,如果线程不在安全点,则恢复线程让它到达安全点
-
主动式中断
设置标记位,标记是否正在进行根节点枚举
在安全点轮询标记位
-
-
安全区域
如果线程不运行,如sleep或者blocked状态,安全点就没法解决问题
安全区域指在这个区域内,代码引用关系不会变化
当线程到达安全区域时,会对自己进行标记,GC时,就不需要管安全状态的线程
当线程需要离开安全区域,需要检查根节点枚举标记位
-
-
-
垃圾收集器
-
Serial收集器
简单粗暴的串行收集器
适合内存不大,单CPU(可以避免线程交互开销),对stop the world不太敏感的client环境
-
ParNew收集器
Parallel New Generation,新生代并行收集器
-
Parallel Scavenge收集器
为控制吞吐量而生
通过控制stop the world的最大时间和gc时间的最大比例来控制gc时间,控制吞吐量
也可以使用自适应参数
算法主要控制stop the world的时间,但是代价是更频繁的gc和总体更长的gc时间总和
-
Serial Old收集器
老年代的串行收集器
使用的是标记-整体算法
-
Parallel Old收集器
老年代的并行收集器
和Parallel Scavenge收集器一起使用,应用于注重吞吐量和CPU敏感的场合
-
CMS收集器
Concurrent Mark Sweep,Mark Sweep,基于标记-清除算法
分为初始标记、并行标记、重新标记、并发清除4个阶段
其中初始标记和重新标记 都会stop the world
耗时最长的并发标记和并发清除都可以和用户线程一起运行
-
缺点
并CPU资源非常敏感,并发情况下,占用部分CPU资源,会导致吞吐量下降
-
无法处理浮动垃圾,可能出现“concurrent mode failed”而导致另一次Full GC
浮动垃圾,在并发清理阶段出现的垃圾,没法当次GC清除
需要预留老年代空间,给GC时,用户程序使用
-
标记-清除算法会导致内存碎片
可以设置是否在Full GC时整理内存,多少次Full GC整理一次内存
-
G1收集器
Garbage First
-
优点有:
并行与并发
分代手机
-
空间整合
整体来看是标记-整理算法,局部来看是标记-复制算法
-
可预测的停顿
通过设置GC时间的不得超过一定比例,几乎是实时Java(RTSJ)垃圾收集器的特征
-
G1实现
G1将内存分为多个相等的区域
老年代、新生代的概念还存在,但是改为了内存区域的集合,而不是固定的区域
G1跟踪各个Region区域里面垃圾堆积的价值(即花费时间回收内存的性价比),维护优先列表
由优先列表建立回收时间预测模型,优先收集性价比高的垃圾
-
由于分区域回收内存,G1的区域之间相互引用,导致可达性分析耗时的问题相比之前的收集器显得更加突出了
通过remember set来避免全堆扫描
remember set记录被其它区域引用的情况(待补充)
-