《深入理解Java虚拟机》读书笔记——内存分配与回收策略

概述

JVM采用分代的垃圾回收策略:不同对象的生命周期是不一样的。目前JVM分代主要是分三个年代:

  • 新生代:所有新创建的对象都首先在新生代进行内存分配。新生代具体又分为3个区,一个Eden区、一个From Survivor区和一个To Sruvivor区。大部分对象都被分配在Eden区,当Eden区满时,还存活的对象将被复制到From Survivor区,当From Survivor区满时,此区还存活的对象将被复制到To Survivor区。最后,当To Survivor区也满时,这时从From Survivor区复制过来并且还存活的对象将被复制到老年代。
  • 老年代:在年轻代中经历了N次(一般是15次)GC后依然存活的对象,就会被放到老年代当中。因此,可以认为老年代是存放一些生命周期较长的对象。
  • 持久代:用于存放静态文件,如Java类等。

具体如下图所示:
《《深入理解Java虚拟机》读书笔记——内存分配与回收策略》

内存分配与回收策略

接下来,通过具体示例代码和JVM参数配置来了解Java的内存分配和回收策略。

对象优先在Eden分配

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

虚拟机提供了-XX:+PrintGCDetails这个收集日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。

使用示例代码来分析GC过程,具体代码如下:

package jvm;

import java.lang.String;

/** * 执行的jvm参数:-verbose:gc -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 */
class GarbageCollection {
    public static final int ONEMB = 1024 * 1024;

    public static void testAllocation() {
        byte[] allocation1, allocation2, allocation3, allocation4;

        allocation1 = new byte[2 * ONEMB];
        allocation2 = new byte[2 * ONEMB];
        allocation3 = new byte[2 * ONEMB];
        allocation4 = new byte[4 * ONEMB];
        System.out.println("OK!");
    }


    public static void main(String[] args) {
        GarbageCollection.testAllocation();
    }
}

运行结果:

OK!
Heap
 PSYoungGen      total 9216K, used 7344K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 89% used [0x00000000ff600000,0x00000000ffd2c188,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 4096K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff000010,0x00000000ff600000)
 PSPermGen       total 21504K, used 2554K [0x00000000f9a00000, 0x00000000faf00000, 0x00000000fec00000)
  object space 21504K, 11% used [0x00000000f9a00000,0x00000000f9c7e8a0,0x00000000faf00000)

通过-Xmx20M限定了Java堆大小为20M,-Xmn限定了新生代的大小为10M,剩下的10M分给老年代。同时,使用-XX:SurvivorRatio=8决定了新生代Eden区与一个Survivor区的空间比例是8:1,在运行结果中也能看到“eden space 8192K, from space 1024K, to space 1024K”,新生代总共可用的空间是9216K(Eden区+1个Survivor区的总容量)。

执行testAllocation()中分配allocation4对象的语句时会发生一次Minor GC,这次GC的结果是新生代6651KB变成148KB,而总内存占用量则几乎没有减少(因此allocation1、allocation2、allocation3三个对象都是存活的,虚拟机几乎没有任何可以回收的对象)。这次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩余空间不足以分配allocation4所需要的4M内存,因此发生了Minor GC。GC期间虚拟机又发现已有的3个2MB大小的对象全部无法收入Survivor空间,所以只好通过担保机制提前转移到老年代去。

这次GC结束后,4MB的allocation4对象顺利分配在Eden中,因此程序执行完成的结果是Eden占用4MB(被allocation4占用),Survivor空间,老年代被占用6MB(被allocation1、allocation2、allocation3占用)。通过GC日志也能说明这一点。

(PS:不科学啊,JAVA7为神马结果和书上的不一致啊,我晕!!!)

大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说是一个坏消息,经常出现大对象容易导致内存还有不少空间的情况下就提前触发了GC以获取足够的连续空间来“安置”它们。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区以及两个Survivor区之间发生大量的内存复制。

示例代码如下:

package jvm;

import java.lang.String;

/** * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=1048576 */
class GarbageCollection {
    public static final int ONEMB = 1024 * 1024;

    public static void testAllocation() {
        byte[] allocation4;

        allocation4 = new byte[4 * ONEMB];  // 直接在老年代分配
        System.out.println("OK!");
    }


    public static void main(String[] args) {
        GarbageCollection.testAllocation();
    }
}

运行结果是:

OK!
Heap
 PSYoungGen      total 9216K, used 5296K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 64% used [0x00000000ff600000,0x00000000ffb2c120,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
 PSPermGen       total 21504K, used 2554K [0x00000000f9a00000, 0x00000000faf00000, 0x00000000fec00000)
  object space 21504K, 11% used [0x00000000f9a00000,0x00000000f9c7e888,0x00000000faf00000)

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

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代。为了做到这一点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁。当它的年龄增加到一定程度(默认是15岁),将会被晋升到老年代。对象晋升到老年代的阈值可以通过参数-XX:MaxTenuringThreshold设置。

空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC是确保安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,蒋尝试进行一次Minor GC,尽管这次GC是有风险的。如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
下面解释一下“冒险”是冒了什么风险?
这里的风险其实就是指:老年代的空间不一定能容纳青年代所有存活的对象,一旦不能容纳,那就还需要进行一次Full GC。

Minor GC and Full GC

Minor GC:从年轻代空间(包括Eden和Survivor区域)回收内存成为Minor GC。在发生Minor GC时候,有两处需要注意的地方:

  1. 当JVM无法为一个新的对象分配空间时会触发Minor GC,例如当Eden区满了,所以分配的频率越高,执行Minor GC的频率也可能越频繁。
  2. 所有的Minor GC都会触发“stop-the-world”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。

Full GC:对整个堆进行整理,包括Young Generation、Old Generation、Permanent Generation。Full GC因为需要对整个区进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/wzy_1988/article/details/47722649
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞