8. 堆
Java虚拟机(JVM)堆是JVM内存模型的一部分,是用来存储对象实例和数组的内存区域。在Java应用程序运行时,所有的对象实例都在堆上分配。堆是由垃圾收集器管理的,这意味着当对象不再被引用时,垃圾收集器会自动回收这些对象占用的内存。本文的主要内容包括堆空间的内部结构、内存管理(分代)、对象的分配过程、逃逸分析、标量替换等内容。
栈管运行,堆管存储
- minor /ˈmaɪnə(r)/ adj.未成年的;次要的;较小的;小调的;二流的
- major /ˈmeɪdʒə(r)/ adj.主要的;重要的;主修的;较多的
- survivor /səˈvaɪvə(r)/ n.幸存者;生还者;残存物
- ratio /ˈreɪʃiəʊ/ n.比率,比例
- threshold /ˈθreʃhəʊld/ n.极限;临界值
8.1 堆的核心概念
堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含多个线程,他们是共享同一堆空间的。
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
- Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
- 堆内存的大小是可以调节的。
- 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。(可以通过操作系统相关课程中的虚拟内存章节做详细的了解。)
- 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。
-Xms10m:最小堆内存
-Xmx10m:最大堆内存
- HeapDemo
- HeapDemo1
/**
* -Xms10M -Xmx10M
*/
public class HeapDemo {
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end...");
}
}
VisualVM 内存空间
Eden Space + Survivor + Old Gen = 10MB
/**
* -Xms20M -Xmx20M
*/
public class HeapDemo1 {
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(10000000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end...");
}
}
Eden Space + Survivor + Old Gen = 20MB
VisualVM 中本身不带 VisualGC,这是 VisualVM 的一个插件,安装方式如下:
通过
Tools
->Plugins
->Available Plugins
可以安装此插件,安装完之后需要重启VisualVM。
- 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)
- 我要说的是:“几乎”所有的对象实例都在这里分配内存。——从实际使用角度看的。 因为还有一些对象是在栈上分配的
- 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
- 经过逃逸分析后如果对象没有逃逸,可能会直接在栈上分配此对象。
- 栈帧中的局部变量并不是在方法结束后就立即被回收的,而是触发了GC的时候,才会进行回收
- 如果堆中对象马上被回收,那么堆空间中的GC频率会特别高,用户线程就受到到影响,因为有 Stop the World (STW)
- 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
public class SimpleHeap {
public int id;
public SimpleHeap(int id) {
this.id = id;
}
public void show() {
System.out.println("");
}
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];
}
}
main
方法的字节码如下:
0 new #6 <com/atguigu/java/SimpleHeap>
3 dup
4 iconst_1
5 invokespecial #7 <com/atguigu/java/SimpleHeap.<init> : (I)V>
8 astore_1
9 new #6 <com/atguigu/java/SimpleHeap>
12 dup
13 iconst_2
14 invokespecial #7 <com/atguigu/java/SimpleHeap.<init> : (I)V>
17 astore_2
18 bipush 10
20 newarray 10 (int)
22 astore_3
23 bipush 10
25 anewarray #8 <java/lang/Object>
28 astore 4
30 return
new
会在堆空间中为对象分配内存。
8.1.1 堆内存细分
Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
- Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
- Tenure generation space 养老区 Old/Tenure
- Permanent Space永久区 Perm
Java 8及之后堆内存逻辑上分为三部分:新生区养老区+元空间
- Young Generation Space新生区 Young/New 又被划分为Eden区和Survivor区
- Tenure generation space 养老区 Old/Tenure
- Meta Space 元空间 Meta
约定:新生区 新生代 年轻代 、 养老区 老年区 老年代、 永久区 永久代
8.1.2 堆空间内部结构(JDK7)
8.1.3 堆空间内部结构(JDK8)
堆空间内部结构,JDK1.8之前从永久代 替换成 元空间
8.2 设置堆内存大小与OOM
8.2.1. 堆空间大小的设置
- Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项"-Xmx"和"-Xms"来进行设置。
-Xms
用于表示堆区的起始内存,等价于-XX:InitialHeapSize
-Xmx
则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
- 一旦堆区中的内存大小超过
-Xmx
所指定的最大内存时,将会抛出OutOfMemoryError
异常。 - 通常会将
-Xms
和-Xmx
两个参数配置相同的值,其目的是 为了能够在ava垃圾回收机 制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。 - 默认情况下
- 初始内存大小:物理电脑内存大小/64
- 最大内存大小:物理电脑内存大小/4
/**
* 1. 设置堆空间大小的参数
* -Xms 用来设置堆空间(年轻代+老年代)的初始内存大小
* -X 是 JVM 的运行参数
* ms 是 memory start
* -Xmx:用来设置堆空间(年轻代+老年代)的最大内存大小
*
* 2. 默认堆空间的大小
* 3. 手动设置:-Xms600m -Xmx600m
* 开发中建议将初始堆内存和最大的堆内存设置成相同的值。
*/
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:979M
-Xmx:14523M
系统内存大小为:61.1875G
系统内存大小为:56.73046875G
如果初始堆内存和最大堆内存设置的不一致,则在程序运行期间会不断的分配和释放内存(GC后会调整内存空间大小)。造成系统压力的同时,也会出现分配的物理内存碎片过多的问题。因为每次分配都有一定概率分配到不连续的物理内存上,久而久之就会出现很多不连续的物理内存空间。当然只是物理内存不连续,逻辑上还是连续的。
/**
* -Xms600m -Xmx600m
*/
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(10000000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
输出结果:
-Xms:575M
-Xmx:575M
为什么不是 600MB?
如何查看堆内存的内存分配情况
C:\Users\Administrator>jps
25040 Jps
5360 HeapSpaceInitial
C:\Users\Administrator>jstat -gc 5360
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT
25600.0 25600.0 0.0 0.0 153600.0 15360.1 409600.0 0.0 4480.0 775.7 384.0 76.0 0 0.000 0 0.000 - - 0.000
堆空间 = 新生代 + 老年代
- 老年代:OC(总量)、OU(已使用)
- 新生代:Eden、S0、S1
- Eden:EC(总量)、EU(已使用)
- S0:S0C(总量)、S0U(已使用)
- S1:S1C(总量)、S1U(已使用)
堆区的内存总量 = S0C + S1C + EC + OC = .
为什么是575?
因为S0和S1是二选一的,这里主要是因为垃圾回收用的是复制算法,S0 和 S1 始终有一个空间是空的。在新生代中,能够放对象的只能是伊甸园区(Eden)加上一个 Survivor 区(S0 或 S1)。如果去掉S1,重新计算,就是575.
堆区的内存总量 = S0C + EC + OC = .
Java 中新生代和老年代的默认比例是 1:2,也即新生代占堆内存的 1/3,而老年代占 2/3。新生代 Eden 区和 Survivor 区的比例是 8:1:1
jstat -gc <pid>
-XX:+PrintGCDetails
-XX:+PrintGCDetails
参数是在程序运行结束后打印。打印结果如下:
Heap
PSYoungGen total 179200K, used 12288K [0x00000000f3800000, 0x0000000100000000, 0x0000000100000000)
eden space 153600K, 8% used [0x00000000f3800000,0x00000000f44001b8,0x00000000fce00000)
from space 25600K, 0% used [0x00000000fe700000,0x00000000fe700000,0x0000000100000000)
to space 25600K, 0% used [0x00000000fce00000,0x00000000fce00000,0x00000000fe700000)
ParOldGen total 409600K, used 0K [0x00000000da800000, 0x00000000f3800000, 0x00000000f3800000)
object space 409600K, 0% used [0x00000000da800000,0x00000000da800000,0x00000000f3800000)
Metaspace used 3303K, capacity 4564K, committed 4864K, reserved 1056768K
class space used 358K, capacity 388K, committed 512K, reserved 1048576K
8.2.1 OutOfMemory举例
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];
}
}
Exception in thread "main" java.lang.OutofMemoryError: Java heap space
at com.atguigu. java.Picture.<init>(OOMTest. java:25)
at com.atguigu.java.O0MTest.main(OOMTest.java:16)
可以设置内存大小,内存设置的越小,OOM的速度越快。
-Xms10m -Xmx:10m
可以通过 VisualVM 中的 Visual GC 和抽样器(Sampler)来观察内存变化。
8.3 年轻代与老年代
- 存储在 JVM 中的 Java 对象可以被划分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
- 另外一类对象的生命周期却非常长,在某些极端的情况下还能够与 JVM 的生命周期保持一致。
- Java 堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(OldGen)
- 其中年轻代又可以划分为 Eden 空间、Survivor0 空间和 Survivor1 空间(有时也叫做 from 区、to 区)