JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战

垃圾回收

一、判断对象是否可以被回收

1、引用计数计数法

内容:在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加一;当引用失效时,计数器就减一;任何时刻计数器为零的对象都是不可能在被使用的

缺点:无法解决对象之间的循环引用问题。如下图所示对象A和对象B,他们之间相互引用,除此之外再无任何引用,则他们的引用计数器值都为1,但实际上这两个对象都不可能在被访问了,而且无法被回收。

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第1张图片

2、可达性分析算法

内容:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots之间没有任何的引用链相连(或者用图论的话来说就是从GC Root到这个对象不可达),则证明这个对象是不可能再被使用的

Java虚拟机中的垃圾回收器采用的可达性分析算法探索所有存活对象

GC Roots包含哪些对象?

  • 在虚拟机栈(栈桢中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数,局部变量,临时变量等
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
  • 在方法区中常量引用的对象,譬如字符串常量池(StringTable)里的引用
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(如NullPointException、OutOfMemoryError)等,还有系统类加载器
  • 所有被同步锁(synchronized)持有的对象
  • 反应Java虚拟机内部情况的JMXBean、JVMTI中的注册回调、本地代码缓存等

如何使用工具查看GC Roots:

  1. 通过jps命令查看当前程序进程
  2. 再使用jmap命令抓取当前程序的运行时的内存情况(jmap抓取前会执行依次GC)
jmap -dump:format=b,live,file=1.bin 进程id
  1. 然后使用Memory Analyzer对抓取的文件进行分析。

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第2张图片

3、Java中的四种引用

强引用

  • 无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
  • 类似Object obj = new Object()

软引用

  • 仅有软引用引用该对象时,在垃圾回收之后,如果内存仍然不足时再次触发垃圾回收,回收软引用对象
  • 通过SoftReference类来实现软引用
  • 可以配合引用队列来释放软引用自身

弱引用

  • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否够用都会回收弱引用对象
  • 通过WeakReference类来实现弱引用
  • 可以配合引用队列来释放弱引用自身

虚引用

  • 一个对象是否存在虚引用,不会对其生存时间产生印象,也无法通过虚引用来取得一个对象实例,设置虚引用的唯一目的是为了能在这个对象被收集器回收时收到一个系统通知
  • 通过PhantomReference类来实现弱引用
  • 必须配合引用队列使用,通常配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由ReferenceHandler线程调用虚引用的相关方法释放直接内存

终结引用(FinalReference):

  • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用其finalize方法,第二次GC才能回收被引用对象。
  • 但是Finalizer线程的优先级非常低,不要在finalize方法中进行资源释放等操作(可以使用try-finally代替)。

软引用使用示例

当使用容量大但是不那么重要的对象时(如图片资源),可以使用软引用

代码:

/**
 * 演示软引用的使用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class SoftReferenceTest {

    private static final int _4MB = 1024 * 1024 * 4;

    public static void main(String[] args) {
        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++){
            #创建软引用对象,引用大容量对象
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            #将软引用对象加入list
            list.add(ref);
            System.out.println(list.size());
        }

        System.out.println("循环结束:" + list.size());
        for (SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }
}

运行结果:

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第3张图片

清除软引用对象

清除掉没有引用对象的软引用对象(ref),主要使用引用队列ReferenceQueue进行实现。

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第4张图片

弱引用使用示例

代码:

/**
 * 演示软引用的使用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class SoftReferenceTest {

    private static final int _4MB = 1024 * 1024 * 4;

    public static void main(String[] args) {
        // list ---------->WeakReference---------->byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++){
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
            list.add(ref);
            for (WeakReference<byte[]> wr : list) {
                System.out.print(wr.get() + " ");
            }
            System.out.println();
        }
        System.out.println("循环结束:" + list.size());
    }
}

结果:
JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第5张图片

二、垃圾回收算法

1、标记-清除算法(Mark Sweep)

内容:首先根据对象到GC Roots之间是否有可达路径来标记需要回收的对象,再根据标记的结果对内存进行清除(将清除的内存的起始地址放入可分配内存表中)

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第6张图片

优点:速度快,简单

缺点

  • 内存空间碎片化:标记清除算法会产生大量不连续的内存碎片,可能会导致之后给较大对象分配内存时,虽然总内存数量足够,但是没有足够的连续内存而导致GC
  • 执行效率不稳定:标记和清除两个过程的执行效率都随对象的数量增长而降低

2、标记-整理算法(Mark Compact)

内容:首先根据对象到GC Roots之间是否有可达路径来标记需要回收的对象,之后让所有存活的对象都向内存空间的一端移动,然后清理掉边界以外的内存。
JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第7张图片
优点:不会出现内存碎片化的问题

缺点:垃圾回收效率没有标记清除高,因为需要移动存活对象,还要更新对存活对象引用(一般需要全部暂停用户应用程序才能进行)

3、标记-复制算法(Mark Copy)

内容:它将内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还存活的对象复制到另外一块,然后把已使用过的内存空间一次清理掉

优点:标记-复制算法优点也是不会产生内存碎片化的问题,分配内存时只需要移动堆顶指针按顺序分配即可

缺点:它的缺点也十分明显,就是可用内存缩小到了原来的一半

4、分代垃圾回收算法

分代收集理论的理论基础

  • 弱分代理论(Weak Generational Hypothesis):绝大数对象都是朝生夕灭的
  • 强分代理论(Strong Generational Hypothesis):熬过越多次垃圾回收的对象就越难以消亡
  • 跨代引用假说(International Reference Hypothesis):跨代引用相对于同代引用仅占极少数

分代

  • 新生代(Eden/Young):新的对象都在新生代内存区域中分配
  • 幸存区 To/From(Survivor To/From):对新生代和幸存区 From进行GC后,将新生代和幸存区 From中存活的对象复制到幸存区 To,然后幸存区 To和幸存区 From进行区域交换(Appel式回收:新生代/幸存区=8/1)
  • 老年代(Old/Tenured):用于存放在新生代和幸存区经过多次GC(最大15次)没有被回收而晋升的对象

GC分类

  • 部分收集(Partial GC):只对Java堆中的一部分进行的垃圾收集
    • 新生代收集(Minor GC/Young GC):针对新生代(还有幸存区)的垃圾收集
    • 老年代收集(Major GC/Old GC):针对老年代的垃圾收集,只有CMS收集器有该行为
    • 混合收集(Mixed GC):针对整个新生代和部分老年代的垃圾收集,只有G1收集器有该行为
  • 整堆回收(Full GC):收集整个Java堆和方法区的垃圾回收(相比Minor GC非常慢,尽量不要触发)

分代流程

  • 对象首先分配在新生代(Eden)
  • 新生代空间不足时触发Minor GC,新生代和幸存区 From中存活的对象复制到幸存区 To,存活对象的年龄加1并交换From和To区域
  • Minor GC采用标记-复制算法,复制过程由于改变的对象在内存中的位置,为了防止对象访问发生错乱需要进行Stop The World,也就是停止所有用户应用线程,等垃圾回收结束用户现场才恢复运行
  • 当对象的寿命(GC之后存活的次数)超过阙值时,会晋升到老年代,最大寿命15(4bit)
  • 大对象分配时,一般直接分配到老年区
  • 当老年代的空间也发生不足时,会先触发Minor GC,如果空间仍然不足,那么出发Full GC(STW的时间更长)

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第8张图片

垃圾回收相关参数

含义 参数
堆初始容量 -Xms
堆最大容量 -Xmx或-XX:MaxHeapSize=size
新生代容量 -Xmn或者-XX:NewSize=szie 和-XX:MaxNewSize=size
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio和-XX:UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阙值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
Full GC前Minor GC -XX+ScavengeBeforeFullGC

三、垃圾回收器

经典垃圾回收器的种类以及搭配关系如下图所示:

名词介绍(垃圾回收场景):

  • 并行(Parallel):并行描述的是多条垃圾回收线程之间的关心,说明同一时间有多条垃圾回收线程在协同工作,通常默认此时用户线程处于等待状态
  • 并发(Concurrent):并发描述的是垃圾回收线程与用户线程之间的关系,说明同一时间垃圾回收线程与用户线程都在运行
  • Stop The World(STW):指由于垃圾回收的进行导致用户线程停止

1、串行的垃圾回收器

特点:

  • 单线程
  • 适合堆内存较小,个人电脑

1)、Serial收集器

Serial收集器是最基础、历史最悠久的垃圾收集器。

  • 新生代
  • 采用标记-复制算法
  • 通常与Serial Old搭配

2)、Serial Old收集器

Serial Old是Serial的老年代版本

  • 老年代

  • 采用标记-清除算法

  • 搭配

    • 通常与Serial Old搭配
    • JDK5之前与Parallel Scavenge搭配使用
    • 作为CMS发生Concurrent Mode Failure(浮动垃圾过多)的后备方案

使用VM指令 -XX:+UseSerialGC=Serial+SerialOld开启Serial和Serial Old搭配

Serial和Serial Old运行示意图如下图所示,不能并发运行,会产生STW:

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第9张图片

2、吞吐量优先的垃圾回收器

特点:

  • 多线程,并行
  • 适合堆内存较大,多核cpu
  • 单位时间内STW的时间最短,让任务尽快完成

1)、Parallel Scavenge收集器(JDK8默认新生代收集器)

  • 新生代、多线程并行

  • 标记-复制算法

  • 目标是达到一个可控的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾回收时间),吞吐量和响应时间一般是相互矛盾的)

  • 具有自适应调节策略

  • VM参数设置

    • -XX:+UseAdaptiveSizePolicy该参数会自动设置新生代Eden和Surivor区的比例,虚拟机会根据当前系统运行情况手机性能监控信息,动态的调整参数以提供最合适的停顿时间或者最大吞吐量
    • -XX:ParallelGCThreads=n设置Parallel Scavenge收集器使用的线程数目,最好和CPU核数一致
    • -XX:MaxGCPauseMillis=ms设置一个大于0的毫秒值,垃圾回收器会尽力保证内存回收花费的时间不超过用户设置的值。而且垃圾回收停顿时间缩短是以牺牲吞吐量和新生代空间为代价的,所以这个指不是越小越好
    • -XXGCTimeRatio=ratio设置一个ratio,ratio代表用户线程运行时间占比,也就是设置该参数之后,那允许的最大垃圾收集时间占总时间比率1/(1+ratio)。同理ratio也不是越大越好,一般设为19,这时最大垃圾收集时间占总时间比率为5%。

2)、Parallel Old收集器(JDK8默认老年代收集器)

Parallel Old收集器是Parallel Scavenge的老年代版本

  • 支持多线程并行
  • 标记-清除算法
  • 和Parallel Scavenge搭配

Parallel Old和Parallel Scavenge运行示意图如下图所示,

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第10张图片

3、响应时间优先的垃圾回收器(CMS收集器)★

特点

  • 多线程
  • 堆内存较大,多核cpu
  • 尽可能让单次的STW时间更短

1)、CMS收集器

CMS(Concurrent Mark Sweep)是一款以获得最短回收停顿时间为目标的收集器,它运行在老年代,是基于标记-清除算法的。整个回收过程包括四个步骤★:

  • 初始标记(CMS initial mark):初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快
  • 并发标记(CMS concurrent mark):并发标记就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是能够与用户线程并发执行,不会产生STW
  • 重新标记(CMS remark):重新标记阶段是为了修正并发标记期间,因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录(书3.4.6节),这个阶段的停顿时间比初始标记时间长,但是也比并发标记时间短得多
  • 并发清除(CMS concurrent sweep):并发清楚阶段可以清除删除掉标记阶段判断已经死亡的对象,由于不需要移动存活对象,所以这个阶段也可以与用户线程并发执行

Concurrent Mark Sweep收集器运行示意图如下图所示:

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第11张图片

CMS的缺点:

  • CMS收集器对处理器资源非常敏感:在并发阶段,它虽然不会导致用户现场停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用程序变慢,降低吞吐量
  • 要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器重新进行老年代的垃圾回收,这样停顿时间就很长了
  • CMS是一款基于标记-清除的算法,会产生大量的空间碎片

CMS通常搭配ParNew新生代垃圾回收器使用。ParNew实际上是Serial收集器的多线程并行版本。

CMS常用VM参数:

  • -XX:+UseConcMarkSweepGC~-XX:+UseParNewGC~SerialOld:使用CMS搭配ParNew,并且使用Serial Old作为并发失败的后备收集器
  • -XX:ParallelGCThreads=n:设置ParNew并行执行时使用的线程数,通常为CPU核数
  • -XX:ConcGCThreads=n:设置CMS并发执行时使用的线程数,同为CPU核数/4
  • -XX:CMSInitiatingOccupancyFraction=precent:设置CMS在老年代内存使用多少时进行垃圾回收(由于并发清理阶段会产生浮动垃圾,所以不能等100%时在清理),通常为80%左右。太低会导致频繁的垃圾回收,太高会导致并发失败。
  • -XX:+CMSScavengeBeforeRemark:设置重新标记前进行Minor GC,减少标记时跨代引用的查找时间

4、G1(Garbage First)收集器(JDK9默认)★

G1(Garbage First)收集器开创了面向局部收集的设计思路和基于Region的内存布局形式。G1的特点包括★:

  • G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每个Region根据需要扮演新生代的Eden区、Survivor区或者老年代区域。除此之外G1还有一类特殊的Humongous区域,专门用来存储大对象,大对象指超过Region容量一半的对象。
  • G1收集器会区跟踪各个Region里面的垃圾堆积的价值大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定的允许收集停顿时间(可以使用VM参数-XX:MaxGCPauseMills设定,默认值为200毫秒)优先处理回收价值收益最大的那些Region。这也是“Garbage First”名称的由来
  • G1从整体上看是基于标记-整理算法,但是从局部(两个Region之间)来看是标记-复制算法。不论从整体还是局部都不会产生内存空间碎片
  • G1采用原始快照(SATB,书3.4.6节)算法来解决并发标记时用户线程更改对象引用的问题

G1收集器运作过程包括下面四个步骤★:

  • 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到对象,并且修改TAMS指针(G1为每个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中一部分空间划出来用来用于并发标记阶段分配新的对象,这块区域用两个TAMS标记)的值。这个阶段需要短暂的停顿,但耗时很短,而且可以借助Minor GC同步完成
  • 并发标记(Concurrent Marking):从GC Roots开始对堆中对象进行可达性分析,递归的扫描整个堆中对象图,找出要回收的对象,这个阶段耗时较长,但是可以与用户程序并发执行。对象图扫描完成之后,还要重新处理SATB记录的在并发标记阶段引用变动的对象
  • 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录
  • 筛选回收(Live Data Counting and Evaluation):负责更新Region的统计数据,对各不Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作沙及存括对象的移动,必须暂停用户线程,由多条收集器线程并行完成的。

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第12张图片

G1收集器的不足之处:G1无论是为了垃圾收集产生的**内存占用(Footprint)还是程序运行时的额外执行负载(Overload)**都要比CMS高。

  • 就内存占用来说,虽然G1和CMS都使用卡表来处理跨代指针,但G1的卡表(SATB)实现更为复杂,而且堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表。这导致G1的记忆集(和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间;相比起来CMS的卡表就相当简单,只有唯一一份, 而且只需要处理老年代到新生代的引用,反过来则不需要,由于新生代的对象具有朝生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的。
  • 在执行负载的角度上,同样由于两个收集器都使用到写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行同样的(由于G1的卡表结构复杂,其实是更烦琐的)卡表维护操作外,为了实现原始快照搜索( SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。油于G1对写屏障的复杂操作要比CMS消耗更多的运

CMS常用VM参数:

  • -XX:+UseG1GC:设置使用G1垃圾回收器,JDK9默认是G1
  • -XX:G1HeapRegionSize=size:设置G1中每个Region的大小,取值范围为1MB~32MB,且应为2的N次幂
  • -XX:MaxGCPauseMills=time:设置允许的垃圾回收停顿时间,默认是200毫秒。G1根绝此值选定需要收集那些Region和收集的个数

5、低延迟垃圾回收器

1)、Shenandoah回收器

2)、ZGC回收器

(待补充)

四、内存分配原则实战

1、对象优先分配在Eden区

    private static final int _1MB = 1024 * 1024;
	/**
     * 新生代 Minor GC测试
     * VM参数:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20m -Xmx20m -Xmn10m  -XX:SurvivorRatio=8
     *
     * -XX:+UseSerialGC 设置使用Serial+Serial Old, JDK8默认时Parallel Scavenge+Parallel Old
     * -XX:+PrintGCDetails 设置输出每次GC的详细信息,并在程序结束后输出内存各个区域的分配情况
     * -Xms20m -Xmx20m -Xmn10m  设置内存区域大小为20M,其中分给新生代10M
     * -XX:SurvivorRatio=8 设置新生代中Eden区和Survivor区的空间比率为8:1,
     * 也就是Eden区8192K,Survivor to和from都是1024K,新生代总共可用9216K(Eden区+1个Survivor区)
     */
    public static void testAllocation(){
        byte[] a1,a2,a3,a4; //注意是byte[],不是Byte[]

        //先手动执行一次Full GC用于清除其他垃圾,防止影响结果
        System.gc();

        //依次分别给a1,a2,a3,a4分配2MB,2MB,2MB,4MB的空间
        //a1,a2,a3直接分配到新生代Eden区
        System.out.println("a1");//分配前输出将要分配的编号
        a1 = new byte[2*_1MB];
        System.out.println("a2");
        a2 = new byte[2*_1MB];
        System.out.println("a3");
        a3 = new byte[2*_1MB];

        //a4分配时Eden区没有足够的内存发生了一次GC
        //而a1,a2,a3分配的区域仍具有引用无法回收,而且Survivor区from空间也不够
        //所以直接把a1,a2,a3移动了老年代,而在eden区为a4分配了内存空间
        System.out.println("a4");
        a4 = new byte[4* _1MB];
    }

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第13张图片

2、大对象直接进行老年代

    private static final int _1MB = 1024 * 1024;
	/**
     * 测试大对象直接在老年代分配
     * VM参数:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20m -Xmx20m -Xmn10m  -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728
     *
     * -XX:PretenureSizeThreshold=3145728 设置大对象的内存阙值为3M(不能直接写3M)
     * -XX:+UseSerialGC 设置使用Serial+Serial Old, 只有Serial和ParNew支持PretenureSizeThreshold参数
     */
    public static void testPretenureSizeThreshold() {

        //当设置大对象的阙值时3M时,6M的alloc引用的对象会直接分配到老年代
        byte[] alloc = new byte[6 * _1MB];
    }

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第14张图片

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

    /**
     * 测试长期存活的对象会进入老年代
     * VM参数:-XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20m -Xmx20m -Xmn10m  -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
     *
     * -XX:MaxTenuringThreshold 设置对象经过多少次GC进入老年代
     */
    public static void testTenuringThreshold(){
        byte[] a1,a2,a3;

        //先手动执行一次Full GC用于清除其他垃圾,防止影响结果
        System.gc();
        
        //为a1分配256K大小
        System.out.println("a1");
        a1 = new byte[_1MB / 4];

        System.out.println("a2");
        a2 = new byte[4 * _1MB];
        //a3第一次分配会导致GC,此时a1的生命值从0变为1,但仍在新生代中
        System.out.println("a3");
        a3 = new byte[4 * _1MB];
        a3 = null;
        //a3第二次分配也会导致GC,此时a1的生命值为1
        //当TenuringThreshold=1时,a1会进入到老年代
        //当TenuringThreshold=15时,a1生命值为1,仍然会在新生代
        System.out.println("a3");
        a3 = new byte[4 * _1MB];
    }

当TenuringThreshold=1的情况:

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第15张图片

当TenuringThreshold=15的情况:

JVM学习笔记(二):垃圾回收、垃圾回收算法、垃圾回收器(Serial、Parallel、CMC、G1)、内存分配原则实战_第16张图片

4、动态年龄判定(书P134)

关于上面的这个案例,细心的同学可能会发现,a2不管是当TenuringThreshold为1或者15都进入了老年代,这是为什么呢?

原因是GC后Eden区幸存的会进入Survivor To,而Survivor To只有1M空间放不下有4M的a2,所以a2直接进入了老年代。

其实不光是对象大于Survivor区域时会直接进入老年代,只要满足下面条件都会直接进入老年带:

如果Survivor空间中相同年龄的对象大小总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象都会直接进入老年代,无需等到-XX:MaxTenuringThreshold设置的年龄

5、空间分配担保(书P135)

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。

JDK1.6下的情况:如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure); 如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次Minor GC是有风险的:如果小于,或者XHadePomotio alure设置不允许冒险,那这时就要改为进行一次Full GC。

JDK1.8下的情况(JDK1.6之后的情况):-XX:HandlePromotionFailure参数被废弃,只要老年代的连续空间大于新生代对象总大小或者大约历次晋升的平均大小都会先执行Mionr GC,空间不足情况下在执行Full GC。

中相同年龄的对象大小总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象都会直接进入老年代,无需等到-XX:MaxTenuringThreshold设置的年龄**

5、空间分配担保(书P135)

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。

JDK1.6下的情况:如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure); 如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽管这次Minor GC是有风险的:如果小于,或者XHadePomotio alure设置不允许冒险,那这时就要改为进行一次Full GC。

JDK1.8下的情况(JDK1.6之后的情况):-XX:HandlePromotionFailure参数被废弃,只要老年代的连续空间大于新生代对象总大小或者大约历次晋升的平均大小都会先执行Mionr GC,空间不足情况下在执行Full GC。

你可能感兴趣的:(后端,JVM)