前言
对JVM第三章的知识点进行总结,同时方便自己以后的回顾。
对象死活
引用计数算法
原理:给对象添加一个引用计数器,当有地方引用它时,计数器值就加1;当引用失效时,计数器值减1;任何时刻计数器值为0就表示该对象不可能在被使用。
优点:实现简单,判断效率高。
缺点:无法解决对象之间相互引用的问题。
可达性分析算法
原理:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
可作为GC Roots的对象:1、虚拟机栈(栈帧中的本地变量表)中引用的对象。
2、方法区中类静态属性引用的对象。
3、方法区中常量引用的对象。
4、本地方法栈中JNI(即一般说的Native方法)引用的对象。
引用类别
强引用:类似”Object obj = new Object()”,只要强引用在存在,垃圾收集永远不会回收被引用的对象。
软引用:用来描述一些有用但并非必需的对象。软引用关联着的对象在系统即将发生内存溢出异常之前会被列进回收范围之中进行第二次回收
如果这次回收还没有足够的内存,才会抛出内存溢出异常。JDK1.2之后,提供了SoftReference类来实现软引用。
弱引用:用来描述非必需对象,比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。JDK1.2之后,提供了WeakReference类来实现弱引用。
虚引用:也称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。JDK1.2之后,提供PhantomReference类来实现虚引用。
生存还是死亡
在可达性分析算法中不可达的对象(第一次标记),并非”非死不可“。当对象覆盖了finalize()方法并且对象的finalize()方法没有被执行过,则对象将被放置在一个F-Queue的队列中,并在稍后由一个虚拟机自动建立的,低优先级的Finalizer线程去执行它,finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次标记,如果对像在finalize()方法中拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那么在第二次标记时它将被移除出“即将回收”的集合;如果对象这个时候还没有逃脱,那基本上久真的要被回收了。(对象没有覆盖finalize()方法或者对象的finalize()方法已经被执行过了,则直接回收)
回收方法区
方法区的回收分为废弃常量和无用的类。
废弃常量:废弃常量的回收和Java堆中对象的回收非常类似。假如一个字符串“abc“已经进入了常量池中,当系统中没有任何一个String对象引用常量池中的”abc“常量,也没有其他地方引用这个字面量,如果发生内存回收,这个”abc“常量在必要的情况下就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
无用的类:(1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。(2)加载该类的ClassLoader已经被回收。(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法
标记-清除
分为”标记“和”清除“两个阶段:首先标记需要回收的对象,然后在标记完成后清除被标记的对象。
缺点:(1)标记和清除两个过程的效率都不高。(2)标记清除之后会产生大量不连续的内存碎片,在分配大对象的情况下,可能会因为没有足够大的连续内存空间而导致分配失败,从而导致提前出发一次垃圾收集动作。
复制算法
将内存分为大小向相同的两块,每次只使用其中一块。当这一块内存用完了,就对其进行垃圾回收,然后将存活下来的对象复制到另一块保留的内存中,最后将之前使用过的内存快一次清理掉。但是每次一半的空间作为保留区域导致内存的空间利用率非常低,在IBM公司的研究中,新生代中的对象98%是“朝生夕死”,所以不必按照1:1的比例来划分内存空间,而是将内存空间划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。这样就只有10%的保留空间会被浪费掉。
分配担保:如果另一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制直接进入老年代。
优点:实现简单,运行高效。
缺点:部分保留空间会被浪费掉。
标记-整理算法
由于老年代对象存活率很高,如果使用复制算法,则会进行大量的复制操作,效率会降低。所以根据老年代的特点,提出了标记-整理算法,首先标记出可回收的对象,然后将所有存活的对象都向一端移动,最后直接清理掉端边界以外的内存。
分代收集算法
根据不同代的特点选择适合的收集算法。在新生代中,每次垃圾收集都会有大批的对象死去,只有少量存活,所以选择复制算法。老年代中对象的存活率高,没有额外空间对它进行分配担保,就必须使用“标记——清除”或者“标记——整理”算法。
枚举根节点
在GC时需要Stop The World, 在进行可达性分析的时候不可能逐个检查在全局性引用(例如常量或类静属性)与执行上下文(例如栈帧中的本地变量表),因为很多应用仅仅方法区就有数百兆,如果逐个检测会消耗很多时间。在HotSpot实现中,在类加载的时候把对象内什么偏移量上是什么类型的数据计算出来并在特定的位置使用一组称为OopMap的数据结构来存储栈和寄存器中哪些位置是引用。
安全点(Safepoint)
由于可能导致引用关系变化,或者说OopMap内容变化的指令非常多,不可能为每条这样的指令生成对应的OopMap,这样GC的空间成本会变的非常高。所以HotSpot只在安全点生成OopMap,安全点需要满足的条件是“是否具有让程序长时间执行的特征”。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等
中断方式
在GC发生时,如何让所有线程都跑到安全点再停顿下来,分为抢先式中断和主动式中断。
抢先式中断:在GC发生时,首先把所有线程都中断,如果发现有线程没有在安全点上,就恢复该线程,让它跑到安全点上再中断。
主动式中断:在安全点上设置一个标志,各个线程执行时主动去轮询这个标志,当发现中断标志为真时就自己中断挂起。
安全区域(Safe Region)
安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域的任意地方开始GC都是安全的。线程在执行到Safe Region中的代码时,首相标记自己已经进入了Safe Region,当JVM要发起GC时,就不用管标识自己为Safe Region状态的线程。当线程要离开Safe Region时,需要检查系统是否已经完成GC,如果完成,那线程继续执行,否则就必须等待直到收到可以安全离开Safe Region的信号为止。
垃圾收集器
Serial收集器
工作时只会使用一个CPU或一条收集线程去完成垃圾收集工作,并且它工作时必须暂停其他所有的工作线程(Stop The World),直到它收集结束。
优点:简单高效
ParNew
是Serial收集器的多线程版,除了使用多条线程进行垃圾收集以外,其余的和Serial收集器完全一样
Parallel Scavenge
使用复制算法,并行多线程收集器,垃圾收集时需要Stop The World。与ParNew不用在于,Parallel Scavenge可以控制吞吐量。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),Parallel Scavenge提供两个参数分别控制最大垃圾收集停顿时间和直接设置吞吐量大小。这里需要注意的一点是,并不是最大的垃圾收集停顿时间设置的越小越好,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的,因为新生代的空间越小,垃圾收集的速度越快,但是这回导致新生代垃圾收集越发频繁,从而降低吞吐量。
serial old
老年代单线程收集器,使用“标记-整理”算法。收集器在Client模式下使用。如果在Server模式下,有两大用途:(1)在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用。(2)作为CMS的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old
是Parallel Scavenge的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
CMS
以最短回收回收停顿时间为目标的收集器,采用“标记-清除”算法。收集过程分为4个步骤,(1)初始标记。<Stop The World>(2)并发标记。(3)重新标记。<Stop The World>(4)并发清除。
(1)初始标记<Stop The World>:仅仅标记一下GC Roots能直接关联到的对象,速度很快。
(2)并发标记:进行GC Roots Tracing的过程,和用户线程一起进行。
(3)重新标记<Stop The World>:修正并发标记阶段,因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
(4)并发清除:清理掉死亡的对象,和用户线程一起进行。
缺点:
(1)虽然并发阶段不会导致用户线程停顿,但是会因为占用一部分线程而导致应用程序变慢,总吞吐量会降低。但CPU数量越少,对应用程序的影响越大。
(2)CMS无法处理浮动垃圾,浮动垃圾是指CMS并发清除阶段用户线程产生的新的垃圾,这部分垃圾出现在标记之后,所以只能留到下次GC时在清理。由于浮动垃圾的产生,所以在并发收集阶段,需要预留一部分空间给程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会导致“Concurrent Mode Failure”失败,这时虚拟机就会启动Serial Old收集器来重新进行老年代的垃圾收集,这样停顿的时间就会很长了。
(3)CMS采用的“标记-清除”算法会在收集结束时产生大量的空间碎片。往往会由于老年代没有足够大的连续空间,而导致对大对象的分配失败,最终导致提前促发一次Full GC。为了解决这一问题,CMS收集器提供了两个参数,一个参数用于控制是否在Full GC时开启内存碎片合并整理,另一个参数用于设置执行多少次不压缩的Full GC后,跟一次带压缩的Full GC。
G1
收集过程分为四个步骤,(1)初始标记。<Stop The World>(2)并发标记。(3)最终标记。<Stop The World>(4)筛选回收<Stop The World>
(1)初始标记<Stop The World>:标记GC Roots能直接关联到的对象。
(2)并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象,与用户线程并发执行。
(3)最终标记<Stop The World>:为了修正在并发标记阶段因为用户程序继续运作而导致标记产生变动的那一部分标记。
(4)筛选回收<Stop The World>:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划(回收哪些回收价值最高的Region)。
内存分配与回收策略
对象优先在Eden分配
大多数情况下,对象在新生代Eden区分配。当Eden去没有足够的空间进行分配是,虚拟机将发起一次Minor GC。
大对象直接进入老年区
大对象是指需要大量连续内存空降的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对于虚拟机的内存分配来说是个坏消息,尤其是那种朝生夕死的“短命”大对象,需要连续空间的大对象经常会导致还有不少空间时就提前触发垃圾回收以获取足够的空间存放他们。虚拟机提供了-XX:PretenureSizeThreshold参数,可以让大于这个设置的对象直接在老年代给配内存。这样就避免了在Eden和两个Survivor区之间发生大量的内存复制。
长期存活的对象将进去老年代
虚拟机为每个对象定义了一个对象年龄(Age)计数器。如果新生代eden区中的对象经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被转移到Survivor空间中,并且对象年龄设为1。往后该对象每在Survivor区中度过一次Minor GC,年龄就加一,当它的年龄达到一定程度(默认为15岁),就会被转移到老年代中。这个年龄阈值可通过参数-XX:MaxTenuringThreshold设置。
动态对象年龄判定
在Survivor空间中,当相同年龄的所有对象大小的总和大于Survivor空间的一半时,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
空间分配担保
为了预防在进行Minor GC后Eden中出现大量对象存活(最极端情况下,所有对象存活)并且Survivor中无法容纳这些对象。在进行Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于Eden中所有对象总空间,如果大于,则Minor GC可以安全的进行。如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,如果允许,那么继续检查老年代最大连续可用空间是否大于历届晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC;如果小于或HandllePromotionFailure不允许担保失败,则改为进行Full GC。