天天看点

深入理解Java虚拟机——垃圾收集器与内存分配策略

在Java堆里面存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。

下面3点参考:https://www.cnblogs.com/parryyang/p/5748711.html

整个JVM内存总共划分为三代:新生代(Young Generation)、老年代(Old Generation)、持久代(Permanent Generation)

1、年轻代:所有新生成的对象首先都放在年轻代内存中。年轻代的目标就是尽可能快速的手机掉那些生命周期短的对象。年轻代内存分为一块较大的Eden空间和两块较小的Survior空间,每次使用Eden和其中的一块Survior.当回收时,将Eden和Survior中还存活的对象一次性拷贝到另外一块Survior空间上,最后清理Eden和刚才用过的Survior空间。

2、年老代:在年轻代经历了N次GC后,仍然存活的对象,就会被放在老年代中。因此可以认为老年代存放的都是一些生命周期较长的对象。

3、持久代:基本固定不变,用于存放静态文件,例如Java类和方法。持久代对GC没有显著的影响。持久代可以通过-XX:MaxPermSize=<N>进行设置。

JDK1.8已经去掉永久代,将原本放在永久代中的类信息,编译后的代码,放入Metaspace元空间(本地内存),常量池放入堆内存中。

GC什么时候开始进行?

此处整理自https://blog.csdn.net/u013309822/article/details/80346913

GC经常发生的区域是堆区,堆区还可以分为新生代、老年代(默认比例1:2),新生代还分为一个Eden区和两个Survivor区。(默认比例8:1:1)

对象有限在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快。

Full GC,发生在老年代的GC,当老年代没有足够空间时即发生Full GC,发生 Full GC一般都会有一次Minor GC。

大对象直接进入老年代(对象大小大于Eden区域的一半则默认为大对象),如果很长的字符串数组,虚拟机提供一个 XX:PretenureSizeThreadhold参数,令大于这个参数值的对象直接在老年代分配,避免Eden区和连个Survivor区发生大量的内存拷贝。

发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则进行一次Full GC,如果小于,则查看 HandlePromotionFailure 设置是否允许担保失败,如果允许,那只会进行一次 Minor GC,如果不允许,则改为进行一次 Full GC。

后面讲到内存分配的时候会再次讲解

如何判断对象死去?可以被GC?

1.引用计数法(已经没有使用):给对象中添加一个引用计数器,每当一个地方引用它时,计数器值就加1,;引用失效时,计数器值就减1,;任何时刻计数器为0的对象就是不可能再被使用的。(判定效率高,实现也很简单,但是主流的Java虚拟机里没有选用引用计数法来管理内存,最主要的原因是它很难解决对象之间相互循环引用的问题)

public class People {

    public Object instance = null;
    
}

public class Test {

    public static void main(String[] args) {
        Person person1 = new Person();
        Person person2 = new Person();
        //相互引用
        person1.instance = person2;
        person2.instance = person1;

        person1 = null;
        person2 = null;
        System.gc();
    }

}
           

如果使用引用计数法的话,这里的person1和person2将不能被回收。

2.可达性分析算法:通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象GC Roots没有任何引用链相连时,则证明此对象时不可用的。

在Java中,可作为GC Roots的对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。JDK1.2以后,Java将引用分为强引用、软引用、弱引用、虚引用,强度以此递减。

深入理解Java虚拟机——垃圾收集器与内存分配策略

图片来自:https://www.cnblogs.com/zhiqianye/p/6204110.html

垃圾收集算法

图片来自https://www.cnblogs.com/parryyang/p/5748711.html

标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。(最基础的收集算法,标记和清除的效率都不高,会产生大量不连续的内存碎片)

深入理解Java虚拟机——垃圾收集器与内存分配策略

先介绍以下finalize()方法:

protected void finalize() throws Throwable { }
           

它是Object类的一个protected方法,子类可以覆盖该方法以是实现资源的清理工作,GC在回收对象之前会调用该方法。

标记过程:要宣告一个对象死亡,至少要经历两次标记过程,如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,这两种情况都视为“没有必要执行”,对象直接被回收。如果这个对象有必要执行finalize()方法,那么这个对象将会放置在一个叫F-Queue的队列中,并在稍后由虚拟机自动建立、优先级低的Finalizer线程去执行它,finalize()方法是对象逃脱死亡的最后一个机会,稍后GC将对F-Queue中的对象进行第二次标记,如果对象在finalize()方法中重新与引用链上的任何一个对象建立关联,就能逃脱死亡(下次GC的时候由于已经执行过一次finalize方法,就没有这个机会了,直接GC回收),如果没有,这个对象就会被GC回收了。

复制算法:(对象存活率不高的情况下,解决了效率问题)它将可用内存按容量划分为大小相等的两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,缺点时将内存缩小为原来的一半,代价太高。

深入理解Java虚拟机——垃圾收集器与内存分配策略

标记-整理算法:与标记-清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理端边界的内存。

深入理解Java虚拟机——垃圾收集器与内存分配策略

分代收集算法:根据对象存活周期的不同将内存分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法进行回收。

理解 枚举根节点、安全点、安全区域,为垃圾收集器做铺垫

枚举根节点:

可作为GC Roots的节点主要分布在全局性的引用(例如常量和或类静态变量)和执行上下文(例如栈帧中的本地变量表)中,在进行可达性分析时,不可以出现在分析过程中,对象引用关系还在不断地变化这样的情况,所以GC进行时,必须停顿所有Java执行线程,枚举根节点时也是必须要停顿的。

安全点:

安全点是在程序执行期间的所有GC Roots已知并且所有堆对象的内容一致的点,当线程运行到安全点时,JVM可以安全地进行操作。在HotSpot中,安全点的位置主要在:1.方法返回之前;2.调用某个方法之后;3.抛出异常的位置;4.循环的末尾。为什么把这些位置设置为安全点呢?主要目的是避免程序长时间无法进入safepoint,JVM在做GC之前要等所有的应用线程进入到安全点后VM线程才能分派GC任务,如果有线程一直没有进入安全点,就会导致GC时JVM停顿时间延长。

VM参数:-XX:+PrintGCApplicationStoppedTime 可以打印出系统停顿的时间,如果存在停顿时间特别长的情况,大概率原因是当发生GC时,有线程迟迟进入不到安全点,导致其他已经在安全点停止的线程也一直等待,这里需要分析业务代码中是否存在有界的大循环逻辑,可能在JIT优化时,这些循环操作没有插入safepoint检查。(JIT编译器:即时编译器)

Total time for which application threads were stopped: 0.0000638 seconds, Stopping threads took: 0.0000279 seconds
Total time for which application threads were stopped: 0.0000619 seconds, Stopping threads took: 0.0000261 seconds
Total time for which application threads were stopped: 0.0000804 seconds, Stopping threads took: 0.0000189 seconds
Total time for which application threads were stopped: 0.0000706 seconds, Stopping threads took: 0.0000185 seconds
Total time for which application threads were stopped: 0.0002907 seconds, Stopping threads took: 0.0001170 seconds
Total time for which application threads were stopped: 0.0001155 seconds, Stopping threads took: 0.0000238 seconds
Total time for which application threads were stopped: 0.0001465 seconds, Stopping threads took: 0.0000506 seconds
Total time for which application threads were stopped: 0.0000910 seconds, Stopping threads took: 0.0000449 seconds
Total time for which application threads were stopped: 0.0076313 seconds, Stopping threads took: 0.0000287 seconds
           

安全区域:

如果GC时,某个线程处于不执行状态呢?比如线程处于sleep状态或者blocked状态,这时候线程无法响应JVM的中断请求,走到安全的地方去挂起,对于这种情况就需要“安全区域”来解决。安全区域可以看成扩展了的安全点,在这个区域中任何地方GC都是安全的。当线程执行到安全区域中的代码时,首先标识自己进入到了安全区域,在这段时间里,JVM要发起GC时,根据这个标识,就不用管这部分线程了,当线程要离开安全区域时,它要检查系统是否已经完成了根节点枚举(或者整个GC过程),如果完成了,线程继续执行,否则它就必须等待,直到收到可以安全地离开安全区域的信号为止。

发生GC时,如何中断线程?

抢先式中断(不使用):在GC发生时,首先把所有线程全部中断,如果线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上(现在几乎没有虚拟机采用抢先式中断来暂停线程从而相应GC事件)

主动式中断:GC需要中断线程的时候,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志位true时就自己中主动挂起,轮询标志的地方和安全点是重合的(不会一直去轮询),另外加上创建对象需要分配内存的地方也会有轮询。

垃圾收集器

1.Serial收集器

Serial收集器是最基本、最早的收集器,JDK1.3.1之前是虚拟机新生代收集器的唯一选择。这个收集器是一个单线程的收集器,它只会使用一个CPU、一条收集线程去完成垃圾收集工作,并且在垃圾收集过程中,必须暂停其他所有工作线程,直到它收集结束。直到现在,它依然是虚拟机运行在Client模式下的默认新生代收集器,优点:简单、高效,对于限定单个CPU的环境,Serial收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。

图片来自http://www.cnblogs.com/xiaoxi/p/6486852.html

深入理解Java虚拟机——垃圾收集器与内存分配策略

2.ParNew收集器

其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为完全一样,它是许多运行在Server模式下的虚拟机中首选的新生代收集器,除了Serial收集器外,目前只有ParNew收集器能与CMS收集器配合工作。ParNew收集器在单CPU的环境下绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术(一个物理CPU分成两个逻辑CPU,模拟双核心)实现的两个CPU环境中都不能百分之百地保证超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境下,可以使用 -XX:ParallelGCThreads参数来限制垃圾收集的线程数。

图片来自http://www.cnblogs.com/xiaoxi/p/6486852.html

深入理解Java虚拟机——垃圾收集器与内存分配策略

3.Parallel Scavenge收集器

是新生代收集器,使用复制算法,并行的多线程收集器。Parallel Scavenge收集器关注的不是尽可能缩短用户线程的GC停顿时间,而是达到一个可控制的吞吐量,吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间),如果虚拟机总共运行了100分钟,其中垃圾收集1分钟,那么吞吐量就是99%。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMills参数以及直接设置吞吐量大小的 -XX:GCTimeRatio参数。

MaxGCPauseMills允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值,不过需要注意的是:不是如果把这个参数设置得小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换去的(例:系统把新生代调小一点,收集300MB新生代肯定比收集500MB快吧,这也直接导致了垃圾收集发生得更频繁一些,原来10s收集一次,每次停顿100ms,现在变成5s收集一次,每次停顿70ms,停顿时间确实下降了,但是吞吐量也降下来了)。

GCTimeRatio参数的值是一个 >0 , <100的整数,也就是垃圾收集时间占总时间的比率。

图片来自http://www.cnblogs.com/xiaoxi/p/6486852.html

深入理解Java虚拟机——垃圾收集器与内存分配策略

4.Serial Old收集器

是Serial收集器的老年代版本,同样也是一个单线程收集器,使用“标记-整理”算法。

5.Parallel Old收集器

Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,在注重吞吐量和CPU资源敏感的场合,可以优先考虑Parallel Scavenge加Parallel Old收集器。

6.CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。(针对老年代的GC收集器)目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,CMS收集器就非常符合这类应用的需求。

CMS收集器是基于“标记-清除”算法实现的,整个过程分为四个步骤:

1.初始标记;2.并发标记;3.重新标记;4.并发清除;

其中初始标记和重新标记这两个步骤仍然需要“Stop The World”,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记则是为了修正并发标记期因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍微长一些,但远比并发标记的时间短。

由于整个过程耗时最长的并发标记和并发清除过程,收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS收集器的优点:并发收集,低停顿。

缺点:

1.CMS收集器对CPU资源非常敏感;

CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降,但是当CPU不足4个的时候,CMS对用户程序的影响就可能变得很大,比如2个CPU,就需要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低50%。

2.CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生;

由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理它们,只好留到下一次GC时再清理掉,这部分垃圾就称为“浮动垃圾”。也是由于再垃圾收集阶段,用户线程还需要运行,必须留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时用户程序运行,在JDK1.5的默认配置下,CMS收集器当老年代使用了68%的空间后就会激活,这是一个偏保守的设置,如果在应用中老年代增长不是很快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便减低内存回收次数从而获取更好的性能,在JDK1.6中,CMS收集器的启动阈值已经提高到92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿的时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容器导致大量“Concurrent Mode Failure”失败,性能反而降低。

3.CMS是一款基于“标记-清除”算法实现的垃圾收集器,意味着收集结束时会由大量空间碎片产生。

空间碎片过多时,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大得连续空间来分配当前对象,不得不提前出发一次Full GC,为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数(默认开启)用于在CMS收集器顶不住要进行Full GC时开启内存碎片得合并整理过程,内存整理得过程时无法并发得,空间碎片的问题没有了,但是停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数适用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认为0,表示每次进入Full GC时都要进行碎片整理)。

图片来自http://www.cnblogs.com/xiaoxi/p/6486852.html

深入理解Java虚拟机——垃圾收集器与内存分配策略

7.G1收集器

G1收集器是一款面向服务端应用的垃圾收集器。

特点:

1.并发与并行;

G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“stop the world”停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

2.分代收集;

虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

3.空间整合;

与CMS的“标记-清理”不同,从整体上看,G1是基于“标记-整理”算法实现的收集器,从局部(两个Region)上来看是基于“复制”算法实现的,这两种算法都不会产生内存空间碎片,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

4.可预测的停顿;

G1能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器,Java堆的内存布局就与其他收集器有很大的差别,它将整个Java堆分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Region的集合。

G1收集器之所以能够建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。

G1收集器收集垃圾的步骤:

1.初始标记;

2.并发标记;

3.最终标记;

4.筛选回收;

理解GC日志

内存分配与回收策略

对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程有限分配在TLAB上,少数情况下也可能直接分配在老年代中。

对象优先在Eden分配

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

虚拟机提供-XX:PrintGCDetails这个参数,告诉虚拟机在发生垃圾收集时打印内存回收日志,并在进程退出的时候输出当前的内存各区域分配情况,在实际应用中,内存回收日志一般是打印到文件后通过日志工具进行分析。

实例代码分析:

public class Test4 {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] area1, area2, area3, area4;
        area1 = new byte[2 * _1MB];
        area2 = new byte[2 * _1MB];
        area3 = new byte[2 * _1MB];
        area4 = new byte[4 * _1MB];
    }
}
           

虚拟机参数为:

-verbose:gc               表示输出虚拟机中GC的详细情况.
-Xms20M                   堆最小20m
-Xmx20M                   堆最大20m
-Xmn10M                   新生代10m
-XX:+PrintGCDetails       表示在控制台上打印出GC具体细节
-XX:SurvivorRatio=8       新生代中Eden区与一个Survivor区的空间比例为8:1
           

日志:(JDK1.8)

[GC (Allocation Failure) [PSYoungGen: 6984K->776K(9216K)] 6984K->4880K(19456K), 0.0027869 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 7129K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 77% used [0x00000000ff600000,0x00000000ffc344b8,0x00000000ffe00000)
  from space 1024K, 75% used [0x00000000ffe00000,0x00000000ffec2020,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
 Metaspace       used 3265K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 357K, capacity 388K, committed 512K, reserved 1048576K
           

首先简单分析下这段GC日志:

PSYoungGen                   新生代使用Parallel Scavenge收集器;

6984K->776K(9216K)      GC前该内存区域已使用6984K ---> GC后该内存区域已使用776K(该内存区域总量为9216K);

6984K->4880K(19456K)  GC前Java堆已使用容量6984K ---> GC后Java堆已使用容量4880K(Java堆总容量19456K);(为什么??)

0.0027869 secs                该段内存区域GC耗时0.0027869秒;

eden space 8192K           新生代eden区域一共分配了8MB;

from space 1024K           新生代其中一块survivor区域一共分配了1MB;

to space 1024K                新生代另一块survivor区域一共分配了1MB;

实际上在执行到area3的时候就发生了第一次Minor GC,将原本的4MB分到了老年代中,然后执行area4的时候,Eden里只有area3,就把area4的4MB分配到了Eden中。

日志(JDK1.6)

[GC [DefNew: 6632K->152K(9216K), 0.0049425 secs] 6632K->6296K(19456K), 0.0049633 secs]
 [Times: user=0.01 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4411K [0x33200000, 0x33c00000, 0x33c00000)
  eden space 8192K,  52% used [0x33200000, 0x33628fd8, 0x33a00000)
  from space 1024K,  14% used [0x33b00000, 0x33b26018, 0x33c00000)
  to   space 1024K,   0% used [0x33a00000, 0x33a00000, 0x33b00000)
 tenured generation   total 10240K, used 6144K [0x33c00000, 0x34600000, 0x34600000)
   the space 10240K,  60% used [0x33c00000, 0x34200030, 0x34200200, 0x34600000)
 compacting perm gen  total 12288K, used 364K [0x34600000, 0x35200000, 0x38600000)
   the space 12288K,   2% used [0x34600000, 0x3465b340, 0x3465b400, 0x35200000)
    ro space 10240K,  54% used [0x38600000, 0x38b78290, 0x38b78400, 0x39000000)
    rw space 12288K,  55% used [0x39000000, 0x3969d1c0, 0x3969d200, 0x39c00000)
           

根据DefNew可知,JDK1.6使用的Serial收集器,是在执行到 area4 = new byte[4 * _1MB] 时会发生一次Minor GC,新生代由6632K(area 1,2,3分配的6MB)变成了152K,而总的Java堆的使用由6632K变成了6296K,几乎没有变化(因为引用没有消失,对象还是有效的),这次GC发生的原因是给area4分配内存的时候,发现Eden已经被占用了6MB,剩余空间已不足以分配area4所需的4MB,因此发生Minor GC,GC期间虚拟机又发现已有的3个2MB大小的对象无法全部放入Survivor空间(Survivor空间只有1MB大小),所以通过分配担保机制提前转移到老年代去。所以对象area1,2,3分配到了老年代,area4分配到新生代的Eden区域。

(没搞懂,jdk1.6和1.8为什么会有这样的区别,代码一样,参数一样,只是jdk默认使用的垃圾收集器不一样,有没有大佬愿意回答下这个问题。。。感谢!!!)

大对象直接进入老年代

所谓大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

虚拟机提供了一个 -XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。

-XX:PretenureSizeThreshold参数只对Serial和ParNew收集器有效。

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

虚拟机给每个对象定义了一个年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且Survivor能够容纳的话,将被移动到Survivor空间中,并且年龄设为1,对象在Survivor区每熬过一次Minor GC,年龄就增加1,当它的年龄增加到一定的程度(默认15),就会晋升到老年代。这个阈值可以通过-XX:MaxTenuringThreshold设置。

动态对象年龄判定

虚拟机并不是永远地要求对象地年龄必须达到了MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄所有对象大小地总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

空间担保分配

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象空间,如果这个条件成立,那么Minor GC可以确保安全。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时就改进行一次Full GC。

博客内容整理来自《深入理解Java虚拟机 JVM高级特性与最佳实践》

jvm