JVM系列(六)运行时数据区(堆)

1. 堆的核心概述

image.png

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域
  • Java堆区在JVM启动的时候即被创建,其空间大小也就确定了,是JVM管理的最大一块内存空间
    • 堆内存的大小是可以调节的

代码测试 HeapDemo1

/**
 * -Xms10m -Xmx10m
 */
public class HeapDemo1 {
    public static void main(String[] args) {
        System.out.println("start...");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end...");
    }
}
复制代码

HeapDemo2

/**
 * -Xms20m -Xmx20m
 */
public class HeapDemo2 {
    public static void main(String[] args) {
        System.out.println("start...");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("end...");
    }
}
复制代码

两段代码完全相同,只有运行时设置的堆空间不同。将两段程序同时执行后,打开jvisualvm(mac下直接终端执行jvisualvm命令即可)。双击HeapDemo1进程查看visual GC(首次运行jvisualvm时,可以选择 工具 -> 插件,安装相关的visual GC插件)

image.png

可以看到图中针对HeapDemo1进程,标记的Eden,Survivor0,Survivor1和Old Gen区大小之和为10m

image.png

而堆HeadDemo2进程,各部分大小之和为20m:

image.png

  • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)
  • 《Java虚拟机规范》中对Java对的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)
    • 这里应该强调一点:应该是”几乎“所有的对象实例都在这里分配内存——从实际使用角度看
  • 数组和对象可能永远不会存储在栈上(”栈上分配“除外),因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
  • 堆,是GC执行垃圾回收的重点区域

代码演示:

package com.nasuf.jvm;

public class SimpleHeap {
    private int id;

    public SimpleHeap(int id) {
        this.id = id;
    }

    public void show() {
        System.out.println("My ID is " + id);
    }

    public static void main(String[] args) {
        SimpleHeap s1 = new SimpleHeap(1);
        SimpleHeap s2 = new SimpleHeap(2);
        int[] arr = new int[10];
        Object[] arr1 = new Object[10];
    }
}
复制代码

image.png

image.png

1.1 内存细分

image.png
现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:

  • Java7及之前,堆内存逻辑上分为三部分:新生区+养老区+永久区
    • Young Generation Space 新生区 (Young/New)
      • 又被划分为Eden区和Survivor区
    • Tenure Generation Space 养老区 (Old/Tenure)
    • Permanent Space 永久区 (Perm)
  • Java8及之后,堆内存逻辑上分为三部分:新生区+养老区+元空间
    • Young Generation Space 新生区 (Young/New)
      • 又被划分为Eden区和Survivor区
    • Tenure Generation Space 养老区 (Old/Tenure)
    • Meta Space 元空间 (Meta)

约定:新生区 = 新生代 = 年轻代,养老区 = 老年区 = 老年代,永久区 = 永久代

image.png
如果上述代码执行时加上参数-XX:+PrintGCDetails,可以看到如下信息输出:

Heap
 PSYoungGen      total 6144K, used 942K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 5632K, 16% used [0x00000007bf980000,0x00000007bfa6bad0,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
  to   space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
 ParOldGen       total 13824K, used 0K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
  object space 13824K, 0% used [0x00000007bec00000,0x00000007bec00000,0x00000007bf980000)
 Metaspace       used 2657K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 287K, capacity 386K, committed 512K, reserved 1048576K

复制代码

如果切换成JDK7,则会有如下信息输出:

Heap
 PSYoungGen      total 6656K, used 869K [0x00000007ff900000, 0x0000000800000000, 0x0000000800000000)
  eden space 6144K, 14% used [0x00000007ff900000,0x00000007ff9d9418,0x00000007fff00000)
  from space 512K, 0% used [0x00000007fff80000,0x00000007fff80000,0x0000000800000000)
  to   space 512K, 0% used [0x00000007fff00000,0x00000007fff00000,0x00000007fff80000)
 ParOldGen       total 13824K, used 0K [0x00000007feb80000, 0x00000007ff900000, 0x00000007ff900000)
  object space 13824K, 0% used [0x00000007feb80000,0x00000007feb80000,0x00000007ff900000)
 PSPermGen       total 21504K, used 2656K [0x00000007f9980000, 0x00000007fae80000, 0x00000007feb80000)
  object space 21504K, 12% used [0x00000007f9980000,0x00000007f9c182c0,0x00000007fae80000)
复制代码

2.设置堆内存大小与OOM

  • Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,可以通过选项-Xmx-Xms来进行设置
    • -Xms用于表示堆区(即年轻代+老年代)的起始内存,等价于-XX:InitialHeapSize
      • -X是jvm的运行参数
      • ms: memory start
    • -Xmx用于表示堆区(即年轻代+老年代)的最大内存,等价于-XX:MaxHeapSize
      • mx: memory max
  • 一旦堆区中的内存大小超过-Xmx所指定的最大内存时,将会抛出OutOfMemoryError异常
  • 通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能
  • 默认情况下
    • 初始内存大小:物理电脑内存大小/64
    • 最大内存大小:物理电脑内存大小/4

代码演示(输出初始内存设定):

package com.nasuf.jvm;

public class HeapSpaceInitial {
    public static void main(String[] args) {
        // 返回Java虚拟机中的堆内存总量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        // 返回Java虚拟机试图使用的最大堆内存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms: " + initialMemory + "M");
        System.out.println("-Xmx: " + maxMemory + "M");

        System.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");
        System.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");
    }
}
复制代码

输出如下:

-Xms: 245M
-Xmx: 3641M
系统内存大小为:15.3125G
系统内存大小为:14.22265625G
复制代码
package com.nasuf.jvm;

public class HeapSpaceInitial {
    public static void main(String[] args) {
        // 返回Java虚拟机中的堆内存总量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        // 返回Java虚拟机试图使用的最大堆内存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms: " + initialMemory + "M");
        System.out.println("-Xmx: " + maxMemory + "M");

        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}
复制代码

手动设置参数:-Xms600m -Xmx600m后输出initialMemory和maxMemory:

-Xms: 575M
-Xmx: 575M
复制代码

此时查看:

$ jps
19459 Launcher
19507 Jps
19460 HeapSpaceInitial
72644
27743

# nasuf @ promote in /Library/Java/JavaVirtualMachines [16:01:45]
$ jstat -gc 19460
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
25600.0 25600.0  0.0    0.0   153600.0 12288.5   409600.0     0.0     4480.0 774.3  384.0   75.9       0    0.000   0      0.000    0.000
复制代码

其中:

  • S0C: S0 Capacity
  • S1C: S1 Capacity
  • S0U: S0 Used
  • S1U: S1 Used
  • EC: Eden Capacity
  • EU: Eden Used
  • OC: Old Capacity
  • OU: Old Used

计算(S0C + S1C + EC + OC) / 1024 = 600m即我们的初始设定值,而输出值为575m,是因为S0区和S1区只有一个在使用,所以我们可以计算(S0C + EC + OC) / 1024 = 575m,因此由575m计算得出的系统内存大小也会小于实际的系统内存大小。

另外,使用-XX:+PrintGCDetails参数运行也可以查看到内存情况:

Heap
 PSYoungGen      total 179200K, used 9216K [0x00000007b3800000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 153600K, 6% used [0x00000007b3800000,0x00000007b41001a0,0x00000007bce00000)
  from space 25600K, 0% used [0x00000007be700000,0x00000007be700000,0x00000007c0000000)
  to   space 25600K, 0% used [0x00000007bce00000,0x00000007bce00000,0x00000007be700000)
 ParOldGen       total 409600K, used 0K [0x000000079a800000, 0x00000007b3800000, 0x00000007b3800000)
  object space 409600K, 0% used [0x000000079a800000,0x000000079a800000,0x00000007b3800000)
 Metaspace       used 2658K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 287K, capacity 386K, committed 512K, reserved 1048576K
复制代码

注意:其中的PSYoungGen179200K,计算方式同样是eden space + from space或者eden space + to space

OutOfMemoryError测试

package com.nasuf.jvm;

import java.util.ArrayList;
import java.util.Random;

public class OOMTest {
    public static void main(String[] args) {
        ArrayList<Picture> list = new ArrayList<>();
        while(true) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(new Picture(new Random().nextInt(1024 * 1024)));
        }
    }
}

class Picture {
    private byte[] pixels;
    public Picture(int length) {
        this.pixels = new byte[length];
    }
}
复制代码

运行参数:-Xms600m -Xmx600m. 执行后使用jvisualvm中的Visual GC插件查看内存情况:

image.png

image.png
可以看到Old区不断被填充,直到填满为止无法进一步GC,导致下列OOM日志输出:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.nasuf.jvm.Picture.<init>(OOMTest.java:23)
	at com.nasuf.jvm.OOMTest.main(OOMTest.java:15)
复制代码

抽样器中能够进一步查看到内存中的对象情况:

image.png

未完待续…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享