GC相关
概述
GC需要完成的三件事情:
1.那些对象需要回收
2.什么时候回收
3.如何回收
那些对象需要回收?
在堆中几乎存放着所有的对象实例,垃圾收集器子啊对堆进行回收前,第一件事情就是要确定这些对象中那些还有用,那些已经死去了(不可能再被任何途径使用的对象)
判断对象是否有用的方法
1.引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。
但是这个方法有一个最难以解决的问题:对象之间循环引用
2.可达性分析计算
通过一系列的GC Roots作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象当GC Roots没有任何引用链相连时,则说明这个对象已经死了。
在JAVA中,可作为GC Roots的对象包括以下几种:
- 虚拟机中栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
什么是引用
如果reference类型的数据中存储的数值代表的另外一块内存的起始地址,就称这块内存代表着一个引用。
强软弱虚
在JDK1.2之后,Java对引用的概念进行了扩充
- 强引用:在代码中普遍存在,类似于"Objec o = new Object();"。只要强引用存在,垃圾收集器就不会回收对象。
- 软引用:软引用用来描述一些有用但非必要的对象(缓存)。在系统将要发生内存溢出异常之前,就把这些对象回收。
- 弱引用:非必须对象。只要发生GC,就会被回收。
- 虚引用:为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。PhantomReference
对象的自我救赎
即使在可达性分析算法中不可达的对象,也不是直接就被回收的。要宣告一个对象死亡,至少要经历两次标记过程。
若这个对象有必要执行finalize()方法,会去执行该方法,若在该方法内重新与引用链关联,就可以逃过第二次“即将回收”的集合。
但是finalize方法只会被执行一次,所以该对象也只能拯救自己一次。
也不能保证一定可以拯救成功,因为虚拟机虽然会触发这个方法,但是不一定保证这个方法运行结束。这样做的原因是:若一个对象的finalize方法执行缓慢,将很有可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。
但是不建议大家重写finalize方法,因为它不稳定,运行代价高。使用try-finally更好。
回收方法区
在方法区中进行垃圾回收的效率很低,远低于堆。
方法区,也叫永久代,垃圾回收主要回收两部分的内容:
1.废弃常量:回收废弃常量与回收堆内存非常相似。列如,一个字符串"abc"已经在字符串常量池里了,但是当前系统没有任何一个String对象指向它了,如果此时发生内存回收,而且必要的话,这个"abc"常量就会被系统清理出常量池。常量池中其他类、方法也于此类似。
2.无用的类:满足下列三个条件,才可以算是无用的类
- 该类所有的实例都被回收了。也就是Java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收了。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
但就算满足了上面三个方法也不一定会被回收。
在大量使用反射、动态代理、CGLIB等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,以保证永久代不会溢出。
垃圾搜集算法
标记-清除算法(定点清理)
最基本收集算法(Mark-Sweep)
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
两个不足:
- 效率低下:标记和清除两个效率都不高
- 空间问题:会产生大量不连续内存碎片
复制算法(新生代采用这种算法)
将内存划分为大小相同的两块,每次只使用一块内存,当这一块使用完了,就把还活着的内存复制到另一块内存上,再把自己上面所有的内存释放掉。
因为每次只是针对半个区域的内存进行回收,内存分配时也不用考虑内存碎片的问题。实现简单,运行高效。
唯一的问题就是会损失一半的内存。
现在的商用虚拟机都采用这种收集算法来回收新生代。但是并不是把新生代的内存划分为两半。因为大部分的新生代都活不过第一次GC,所以将新生代的内存划分为三个部分,较大的部分叫Eden,两块小的部分叫Survivor。每次只使用一个Survivor和一个Eden。当回收时,将Eden和Survivor中存活的对象放到另外一个Survivor中,最后清理掉Eden和刚才使用过的Survivor。
HotSpan中默认的Eden和Survivor的比例是8:1,只会有百分之十的内存被浪费掉。
当然,我们不能保证每次GC后Survivor中都能存储的下,这个时候就需要依赖老年代进行分配担保了。若Survivor中内存不够了,这些对象将直接进入老年代。
标记-整理算法(老年代采用这种算法)
标记过程和标记-清除算法一样,后续步骤让存活下来的对象都向一端移动,然后直接清理掉边界以外的内存。
HotSpot的算法实现
枚举根节点
在执行系统停顿下来后,并不需要一个不漏的检查完所有的GC Roots。虚拟机应当是有办法直接得知那些地方存放着对象引用。
在HotSpot的实现中,是使用一组称为OopMap的数据结构与来达到这个目的的。在类加载的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中那些位置是引用。这样GC在扫描的时候就可以直接得知这些信息了。
安全点
程序只有运行在安全点的时候才能运行GC。安全点的选择是以程序“是否具有让程序长事时间执行的特征”,例如:方法调用、循环跳转、异常跳转等,所有具有这些功能的指令才会产生Safepoint。
如何在GC时让所有线程都“跑”到安全点呢?
两种方案
- 抢断式中断:GC发生时,先将所有线程都中断了,再看被中断的线程是否再安全点上,若不在就恢复线程让它跑到安全点上。
- 主动式中断:设置一个标准,让所有的线程都去轮询这个标准,发现中断标志为真时就挂起。轮询标志的地方和安全点是重合的。
安全区域
安全点似乎解决了如何进入的GC的问题,但实际情况却不一定。
安全点保证了在程序执行时,在不太长的时间就能遇见可以进入的安全点,若是程序没有抢到执行权呢?这时候就需要安全区域来解决了。
安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中任意位置开始GC都是安全的。在线程执行到了安全区域的中的代码时,首先标识自己以已经进入了安全区,那样,当这段时间里JVM要发起GC时,就不用管这条线程了。要离开安全区时,要检查系统是否已经完成了根节点枚举,如果完成了,那线程就继续执行,否则就必须要等待知道收到可以安全离开安全区域的信号为止。
垃圾收集器
Serial收集器
新生代收集器,单线程收集器。在进行垃圾收集时,必须暂停其他所有线程。虚拟机运行在Client模式下默认新生代收集器。
可以和CMS、SerialOld联用
ParNew收集器
可以理解为Serial的多线程版本,在进行垃圾收集时,也必须暂停其他所有线程,但是可以多条线程并发进行垃圾收集。
只能和CMS收集器配合使用。
并不是说ParNew在收集垃圾时可以多线程并发进行,它的效率就一定比Serial高。当单CPU或者俩CPU时,Serial要好,随着CPU数量越来越多,ParNew的效率会高很多。
Parallel Scavenge 收集器
基本和ParNew收集器差不多。但是它的关注点于其他的收集器不同。CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。Parallel Scavenge收集器的目的是达到一个可控的吞吐量(CPU用于用户代码的时间与CPU总消耗的时间的比值)
停顿时间越短越适合需要与用户交互的程序。
越高的吞吐量则可以高效率的利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算。
Parallel Scavenge 收集器提供俩参数用于精确控制吞吐量。分别是控制最大垃圾收集器停顿的时间-XX:MaxGCPauseMillis以及直接设置吞吐量大小的-XX:GCTimeRatio
停顿时间长短和吞吐量是矛盾的。
Parallel Scavenge 也被称为吞吐量优先 收集器。还有一个参数-XX:+UseAdaptiveSizePolicy。这是一个开关参数,当这个参数打开之后,就不用手动指定,新生代大小、Eden和Survivor的比例、直升老年代对象大小之类的细节参数了。收集器会动态的调节这些参数以提供合适的停顿时间或者最大的吞吐量。这种调节方式被称为GC自适应的调节策略。
可以和Serial Old 和Parallel Old 连用。
Serial Old
Serial 收集器的老年收集版本,同样是一个单线程收集器,使用标记-整理算法。这个收集器的意义也是在于给Client模式下的虚拟机使用。如果在Server模式下和Parallel Scavenge收集器连用,或者作为CMS收集器的后备预案。
Parallel Old
Parallel Scavenge收集器老年版本,使用多线程和标记-整理算法。这个收集器在1.6版本才出现。在它出现之前Parallel Scavenge收集器处于一个很尴尬的位置,因为它只能和Serial Old联用。这样他的性能会被拖累,不能体现出吞吐量优先的特性。
CMS收集器
Concurrent Mark Sweep收集器是一种以最短回收停顿为目标的收集器。基于标记-清除算法。整个GC过程分为四部分
- 初始标记
- 并发标记
- 重新标记
- 并发清除
在初始标记、重新标记这两个步骤任需要“Stop The World”。
初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。
并发标记阶段就算进行GC Roots Tracing的过程,而重新标记阶段就是为了修正并发标记阶段因为用户程序继续运作而导致标记产生变动的那一部分对象标记记录,这个阶段的停顿时间一般会比初始标记长一点,但是远比并发标记的时间短。
由于整个过程中耗时最长的并发标记和并发清理过程都可以和用户线程一起工作,所以总体上来说,CMS收集器的回收过程可以说是与用户线程一起并发执行的。
CMS收集器有三个明显的缺点
- 吞吐量低
- 无法处理浮动垃圾
- 标记-清理算法产生的内存碎片化。
G1收集器
G1是一款面向服务端的垃圾收集器。
G1的特点:
- 并发与并行:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“stop the world”时间,其他部分可以通过并发让Java程序继续执行。
- 分代收集
- 空间整合:不会产生空间碎片。整体来看是采用标记-整理算法、局部来看是采用复制算法。
- 可预测停顿:可以指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
使用G1时Java堆内存布局和其他收集器有很大的差别。它将整个Java堆内存划分为多个大小相同的Region,虽然还保留着新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,他们都是一部分Region的集合。
G1只所以可以实现可预测停顿,是因为它可以有计划的避免在整个Java堆中进行安全区域的垃圾收集。
G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
使用Rememberd Set来避免全堆扫描。G1中每个Region都有一个与之对应的Rememberd Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region中,如果是,便通过CardTable把相关的引用信息记录到被引用对象所属的Region的Rememberd Set之中。
G1的操作也大致可以分为四个部分:
- 初始标记:标记一下GC Roots能直接关联到的对象
- 并发标记:GC Roots开始进行可达性分析
- 最终标记:修正并发标记过程中产生的垃圾
- 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望GC停顿时间来指定回收计划(这一阶段是可以并发的,但是因为只回收一部分Region,时间是用户可控的,而且停顿用户线程将大幅度提高收集效率,所以没有选择并发执行)
内存分配与回收策略
对象优先分配在Eden
大对象直接进入老年代
长期存活的对象进入老年代
动态对象年纪判断:若Surviour中相同年纪的对象大于Survivor空间的一半,年龄大于等于该年龄的对象直接进入老年代
空间分配担保:在发生Minor GC之前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象空间总和,若成立则这次Minor GC是安全的。若不成立,查看HandlePromotionFailure是否允许担保失败,这时会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,若大于则尝试一次Mionr GC(有风险的),若小于则直接进行Full GC。