wky233 的个人博客

记录精彩的程序人生

Open Source, Open Mind,
Open Sight, Open Future!
  menu
40 文章
10233 浏览
0 当前访客
ღゝ◡╹)ノ❤️

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

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 (四) 堆内存模型及内存分配策略
作者:wky181
地址:https://www.wkyhky.site/articles/2019/10/21/1571622387012.html