JVM (四) 堆内存模型及内存分配策略

本贴最后更新于 1641 天前,其中的信息可能已经时异事殊

JVM 堆内存模型

JVM 的堆分为两部分,分别为新生代和老年代

  • 新生代:
    新生代又分为两个区。一个伊甸(Eden)区,一个幸存者(survivor)区,幸存者区又分为 so 和 s1 区,新生代采用的是复制算法,因为新生的对象很多,大部分都是朝生息死的,所以采用复制算法最合理,新生的对象都要先放在伊甸区,在 Eden 区满的时候,就要进行 Minor GC 了。将 Eden 区存活的对象复制到 so 或者 s1 区,然后清空 Eden 区。之后如果 Eden 区再满的时候,就把 Eden 和非空的幸存者区里存活的对象放入另一个幸存者区,然后清空,so 和 s1 互相交替,如此循环。
    参数控制 -XX:SurvivorRatio 用来设置新生代中 Eden 空间和 from/to 空间的比例
    默认 -XX:SurvivorRatio=8 Eden:From:To 8:1:1
  • 老年代
    经过一定次数的 Minor GC 后,在新生代存活下来的对象会进入老年代,大对象也会直接进入老年代,老年代采用的是标记-清除法或者标记-整理法。因为老年代大部分都是存活时间比较长的对象。当老年代满的时候,就会进行 Full GC。其回收速度一般会比 Minor GC 慢十倍以上 。
    参数控制 -XX:MaxTenuringThreshold 设置对象经过几次 GC 进入老年代,默认 15
    参数控制 -XX: PretenureSizeThreshold 设置指定大小 如果超过这个大小对象直接进入老年代 (前提使用 ParNew 或者 Serial 收集器)。
    参数控制 -XX: NewRatio 设置新生代和老年代的比例

image.png

JVM 常用参数

JVM 参数 说明
-XX:+PrintGC 使用这个参数,虚拟机启动后,只要遇到 GC 就会打印日志
-XX:MaxTenuringThreshold 设置对象经过几次 GC 进入老年代,默认 15
-XX:+PrintGCDetails 可以查看详细信息,包括各区情况
-Xms 设置 java 程序启动时初始堆大小
-Xmx 设置程序能获得的最大堆大小
-Xss 指定每个线程的最大深度
-Xmn 可以设置新生代的大小
-XX:SurvivorRatio 用来设置新生代中 Eden 空间和 from/to 空间的比例
-XX: NewRatio 设置新生代和老年代的比例
-XX:+PrintCommandLineFlags 将虚拟机显式和隐式的参数输出
-XX:-UseTLAB 禁止为本地线程分配缓冲

对象先进入伊甸区

参数控制 -XX:+PrintGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:SurvivorRatio=8 -XX:-UseTLAB

public class Lecture02 {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] byte1, byte2, byte3, byte4;
        byte1 = new byte[2 * _1MB];
        byte2 = new byte[2 * _1MB];
        byte3 = new byte[2 * _1MB];
        byte4 = new byte[3 * _1MB];
    }
}

GC 日志如下:

[GC (Allocation Failure) [DefNew: 8096K->670K(9216K), 0.0060545 secs] 8096K->6814K(19456K), 0.0060974 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 3746K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  37% used [0x00000000fec00000, 0x00000000fef01168, 0x00000000ff400000)
  from space 1024K,  65% used [0x00000000ff500000, 0x00000000ff5a79e8, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)

在执行到最后一行代码的时候,此时 Eden 区已经放入了 3 个 2M 的对象,而 Eden 的最大空间大约是 8M。再最后一个 3M 的对象想进入 Eden 区时发现空间不足,所以发生了一次 Minor GC ,但是因为 Eden 区的三个对象都是 2M,而 s1 或 s0 空间只有 1M。所以三个对象直接进入了老年代。最后一个 3M 对象进入 Eden 区。从 GC 日志上可以看出 eden space 8192K, 37% used。Eden 区用了大约 3M,tenured generation total 10240K, used 6144K 老年代用了大约 6M。如果把 byte4 = new byte[3 * _1MB];
这一行代码注释掉,就不会发生 GC 了。注意禁用缓存,不然会影响结果。

大对象直接进入老年代

当对象大小超过指定大小,就会直接进入老年代,用参数 -XX:PretenureSizeThreshold 设置指定的大小
参数控制:-XX:PretenureSizeThreshold=2m -XX:+PrintGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:SurvivorRatio=8 -XX:-UseTLAB -XX:+UseSerialGC

public class Lecture03 {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] b = new byte[5 * _1MB];
    }
}

打印日志如下:

Heap
 def new generation   total 9216K, used 1923K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  23% used [0x00000000fec00000, 0x00000000fede0f40, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 5120K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)

没有发生 GC, tenured generation total 10240K, used 5120K,老年代使用了大约 5M,注意前提使用 ParNew 或者 Serial 收集器和禁用线程缓存,不然影响结果。

对象到达指定年龄,进入老年代

-XX:MaxTenuringThreshold,设置对象经过几次 GC 进入老年代,默认 15,每经过一次 GC 对像的年龄就加一,当到达指定年龄就会进入老年代。我测试的指定大小为 3
参数控制:-XX:+PrintGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=3 -XX:PretenureSizeThreshold=10m -XX:-UseTLAB

public class Lecture04 {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        //设置的长期存活对象,不要超过整个s0区的大小,否则会直接进入老年代
        byte[] b1 = new byte[_1MB/4];
        //将Eden 区塞满,触发MinorGC
       for (int i = 0; i < 20; i++) {
            for (int j = 0; j < 10; j++) {
                byte[] b = new byte[_1MB];
            }
        }
    }
}

GC 部分日志如下

[GC (Allocation Failure) [PSYoungGen: 7328K->1016K(9216K)] 7328K->1024K(19456K), 0.0012449 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 8184K->1016K(9216K)] 8192K->1024K(19456K), 0.0007303 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 8184K->984K(9216K)] 8192K->992K(19456K), 0.0006177 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 8152K->0K(9216K)] 8160K->948K(19456K), 0.0007460 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 7168K->0K(9216K)] 8116K->948K(19456K), 0.0003422 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 7168K->0K(9216K)] 8116K->948K(19456K), 0.0002871 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

可见在经历三次 GC 后,存活的对象都进入老年代了,新生代已经没有对象了,因为达到了指定年龄,进入老年代了。

  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...