JVM垃圾收集器与内存分配策略

 以下内容为《深入理解Java虚拟机》的阅读笔记。


    在内存区域中,程序计数器、虚拟机栈与本地方法栈是跟随线程的生命周期的,所以内存的分配与回收是确定的。但是Java堆与方法区所需的内存分配与回收都是动态的,因此,垃圾收集器所关注的是这部分的内存。

对象的回收判断方法

引用计数法

    给对象添加一个引用计数器,每当一个地方引用它时,计数器加1,引用失效时,计数器减1,当计数器的值为0时,则代表该对象已不可能再使用,可以进行回收;该方法很难解决对象间循环引用的问题。

可达性分析算法

    通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用,可回收。

java中可作为GC Roots的对象有:

1.虚拟机栈(栈帧的本地变量表)中引用的对象

2.方法区中类静态属性引用的对象

3.方法区中常量引用的对象

4.本地方法栈中JNI引用的对象

    但是,在可达性分析算法中,至少要经历两次标记过程,才真正确定对象是可回收的。

    第一次,发现没有GC root引用链的时候,将会被标记并进行筛选,条件为该对象是否有必要执行的finalize()方法,对象没有覆盖finalize()方法或已被虚拟机调用过,则不会执行回收。如果被判断需要执行finalize(),那么对象会被加入F-Queue队列中,并开启Finalizer线程执行(低优先级),执行即触发该对象的finalize()方法,但不会等待该方法运行结束。然后,执行该方法的时候会进行第二次标记,此时,重新为对象设置引用关联的话,可避免被回收,否则,即真正被回收掉。

引用的分类

    强引用:直接使用new关键字创建,只要强引用还存在,垃圾收集器永远也不会回收被引用的对象。

    软引用:在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。利用SoftReference类实现软引用。

    弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生之前,GC时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。利用WeakReference实现弱引用

    虚引用:最弱的一种引用关系。对象是否有虚引用的存在,都不会对其生存时间造成影响,也无法通过虚引用获取一个对象实例。使用该引用只是为了在该对象被回收时收到系统通知。利用PhantomReference实现虚引用。

回收方法区

    主要回收两部分内容:废弃常量和无用的类。没有地方引用某一个常量的话,则该常量会被清理出常量池。

    但一个类要符合以下条件,才算是无用的类,才可以被回收掉:

1.该类的所有实例都已经被回收掉

2.加载该类的ClassLoader已经被回收

3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    即使符合以上条件,也可以通过-Xnoclassgc参数控制是否回收

垃圾收集算法

标记-清除算法

    该算法分为”标记“和”清除“两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

    不足:

1.标记和清除两个过程的效率不高

2.标记清除后会产生大量不连续的内存碎片;碎片太多时,在需要分配较大对象时,会导致无法找到较大的连续内存而触发另一次GC。

算法过程如下图所示:


复制算法

    为解决标记清除算法的效率问题,复制算法将内存容量分为相等的两块,每次只使用一块。当这块内存用完时,就将还存活的对象复制到另一块,然后将使用过的内存空间依次清理掉,这样就不会产生碎片多的问题。但每次只能使用一半空间的代价比较大。

    由于新生代的对象98%都是”朝生夕死的“,因此不需要按1:1的比例来划分内存空间;可以将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和一块Survivor。

    回收时,将Eden和Survivor中还存活的对象一次性复制到另外的Survivor上,然后清理Eden和使用过的Suvicor。

    算法过程如下图所示:

   

标记-整理算法

    与标记清除类似,但其后续步骤是先让存活对象向一段移动,然后直接清理掉边界以外的内存。

    算法过程如下图所示:


分代收集算法

    根据对象的存活周期的不同,将内存划分为几块。一般是把Java堆分为新生代和老年代,然后根据年代的特点使用适当的算法;如新生代中的对象存活的少,可采用复制算法,老年代的存活率高,可以采用标记-清理或标记-整理算法。

垃圾收集器

Serial收集器

    单线程的收集器,在其进行垃圾收集时,必须暂停其它所有的工作线程,直到收集结束。因为没有线程交互的开销,只做垃圾收集,所以比较简单而高效。

ParNew收集器

    是Serial收集器的多线程版本,使用多个线程进行垃圾回收。除Serial外,只有ParNew可与CMS收集器配合好工作。

Parallel Scavenge收集器

    并行的多线程收集器。该收集器的目标是达到一个可控制的吞吐量。

    吞吐量 = 运行用户代码的时间 / (运行用户代码 时间+ 垃圾收集时间)

该收集器提供两个参数用于精确控制吞吐量:

1.-XX:MaxGCPauseMillis:控制最大垃圾收集的停顿时间

2.-XX:GCTimeRatio:设置吞吐量的大小

    此外,该收集器还有一个-XX:UseAdaptiveSizePolicy的开关参数,当打开该参数后,虚拟机会根据当前系统的运行情况监控情况,动态地调整新生代的大小、Eden区与Survivor区的比例、晋升老年代的对象大小等参数以提供最合适的停顿时间或最大的吞吐量。

Serial Old收集器

    该收集器是Serial收集器的老年代版本,也是单线程的,使用标记-整理算法。

Parallel Old收集器

    是Paralle Scavenge的老年代版本,使用多线程和“标记-整理”算法。

CMS收集器

    全称是Concurrent Mark Sweep收集器,它是一种以获取最短回收停顿时间为目标的收集器。

基于“标记-清除”算法实现的。

整个过程分为4步:

1.初始标记(CMS initial mark,需停顿):

    仅仅是标记一下GC Roots能直接关联到的对象,速度很快。

2.并发标记(CMS concurrent mark):

    进行GC Roots Tracing的过程,与用户工作线程并发执行。

3.重新标记(CMS remark,需停顿):

    修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,停顿时间比初始标记稍长,但比并发标记短。

4.并发清除(CMS concurrent sweep):

    清除垃圾,与用户工作线程并发执行。

缺点:

    对CPU资源非常敏感:占用CPU资源导致应用程序变慢,总吞吐量降低。

    无法处理浮动垃圾:可能出现“Concurrent Mode Failure”而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,所以会有新的垃圾不断产生,这部分垃圾CMS无法在当次收集中处理完,所以称为浮动垃圾。

    基于“标记-清除”算法,会产生大量的碎片。

G1收集器

    Garbage-First收集器是面向服务端应用的垃圾收集器。它的特点是:

    1.并行与并发:利用多CPU、多核的环境下的硬件优势,使用多个CPU来缩短停顿时间,G1收集器可以通过并发的方法让Java程序继续执行。

    2.分代收集:与之前的收集器不同,G1将这个Java堆分为多个相等的独立区域(Region),虽然也区分新生代和老年代,但是它们不再是物理隔离的,都是一部分Region的集合。

    3.空间整合:从整体上看,G1是基于“标记-整理”算法实现的,从局部(两个Region之间)上看,是基于“复制”算法实现的,所以每次GC都不会产生碎片。

    4.可预测的停顿:建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

    G1在跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台护卫一个优先列表,然后根据允许的收集时间,优先回收价值最大的Region。

    G1中每个Region都有一个与之对应的Remembered Set 用来记录跨代引用,在GC时,也遍历该Set,保证不遗漏任何的可达引用分心。

运作步骤:

    1.初始标记:标记一下GC Roots能直接关联到的对象,并且修改TAMS(next top at start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿,耗时很短。

    2.并发标记:从GC Roots中开始对堆进行可达性分析,这阶段耗时较长,但可与用户程序并发

    3.最终标记:修正在并发标记期间因用户程序继续运行而导致标记产生变动的那一部分标记记录,这部分记录在线程Remembered Set Logs里,然后合并到Remembered Set中,这阶段需停顿,但可并行执行。

    4.筛选回收:对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间指定回收计划。

理解GC日志

    具体GC日志的格式是由收集器自身确定的,但一般来说,它们都有一些共有的特性:如下:

33.125: [GC [DefNew: 3324k->152k(3712k),0.0025925 secs] 3324k->152k(11904k),0.0031680 secs]

    第一个参数为GC发生的时间,是JVM启动以来经历过的秒数

    [GC ]或[Full GC]说明的是这次垃圾收集的类型,带有Full的代表是发生了Stop-The-World的

    DefNew代表GC的区域,此处样例使用的是Serial收集器,所以DefNew代表的是新生代,而ParNew收集器的新生代是ParNew

    第三个3324k->152k(3712k)代表的是GC前该内存区域的已使用容量->GC后该内存区域的使用容量(该内存区域的总容量)

    后面的 secs代表该内存区域的GC时间

    方括号外的3324k->152k(11904k)表示GC前Java堆的使用容量->GC后Java堆已使用的容量(java堆的容量)

    后面的secs同上

内存分配与回收策略

Minor GC:发生在新生代的垃圾收集动作,发生比较频繁,回收速度较快

Major GC/Full GC:发生在老年代的GC。

对象优先在Eden分配

    大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,会发起依次Minor GC

大对象直接进入老年代

    大对象是那些需要大量连续内存空间的Java对象(如一些很长的字符串和数组)。虚拟机提供了一个-XX:PretenureSizeThreshold参数(只对Serail、ParNew生效),令大于这个设置值的对象,直接在老年代中分配。

长期存活的对象将进入老年代

    虚拟机给每个对象定义了一个对象年龄(Age)计数器。在Eden区的对象,没经历依次Minor GC仍然存活,且被Survivor接纳的话,将移动到Survivor空间中,并将对象年龄设置为1,在该区域中,每熬过依次Minor GC,年龄值+1,当达到一定值时(默认为15,可通过参数-XX:MaxTenuringThreshold参数设置该值),将晋升到老年代中。

动态对象年龄判断

    如果Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代中,无须等到MaxTenuringThreshold中要求的值。

空间分配担保

    在Minor GC前,jvm会先检查老年代中最大可用的连续空间是否大于新生代所有对象的总空间,是则可以确保GC是安全的;否则,jvm会查看HandlePromotionFailure设置值是否允许担保失败,允许则会继续检查老年代最大的可用连续空间是否大于每次晋升到老年代对象的平均大小,是则尝试进行依次Minor GC,如果小于或HandlePromotionFailure设置不允许冒险,则会进行一次Full GC。

你可能感兴趣的:(JVM垃圾收集器与内存分配策略)