1. 堆的核心概述
- 一个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插件)
可以看到图中针对HeapDemo1进程,标记的Eden,Survivor0,Survivor1和Old Gen区大小之和为10m
而堆HeadDemo2进程,各部分大小之和为20m:
- 《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];
}
}
复制代码
1.1 内存细分
现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
- Java7及之前,堆内存逻辑上分为三部分:新生区+养老区+永久区
- Young Generation Space 新生区 (Young/New)
- 又被划分为Eden区和Survivor区
- Tenure Generation Space 养老区 (Old/Tenure)
- Permanent Space 永久区 (Perm)
- Young Generation Space 新生区 (Young/New)
- Java8及之后,堆内存逻辑上分为三部分:新生区+养老区+元空间
- Young Generation Space 新生区 (Young/New)
- 又被划分为Eden区和Survivor区
- Tenure Generation Space 养老区 (Old/Tenure)
- Meta Space 元空间 (Meta)
- Young Generation Space 新生区 (Young/New)
约定:新生区 = 新生代 = 年轻代,养老区 = 老年区 = 老年代,永久区 = 永久代
如果上述代码执行时加上参数-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
复制代码
注意:其中的PSYoungGen
为179200K
,计算方式同样是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
插件查看内存情况:
可以看到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)
复制代码
从抽样器中能够进一步查看到内存中的对象情况:
未完待续…