Java虚拟机的内存区域

得益于Java虚拟机的内存管理机制,Java程序员无需手动分配、释放内存,可以专注在自身功能模块的开发。但是懂得JVM的内存管理机制可以在实际开发中,避免一些问题。在排查故障时提供思路。

内存区域

Java不同于C、C++,Java程序员不需要自己手动管理内存。而是交给JVM(Java Virtual Machine)的自动内存管理机制。
JVM在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域。每个区域有各自的用途,以及创建和销毁的时机。有的区域随着JVM进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁

运行时数据区域

程序计数器

  • 作用:当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复都依赖于这个计数器完成
  • 特征:线程私有、没有规定OutOfMemoryError的区域

Java虚拟机栈

  • 作用:描述了Java方法执行的内存模型,方法运行时创建一个栈侦(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法调用到执行完成过程,就是一个栈侦在Java虚拟机栈中入栈到出栈的过程。局部变量表存放了编译器可知的基本数据类型、对象引用(不等同于对象本身,只是存放了对象起始地址的引用指针)
  • 特征:线程私有、规定了OutOfMemoryError、StackOverflowError

本地方法栈

  • 作用:与Java虚拟机栈类似,区别在于该内存区域是为执行Native方法服务的。在Sun HotSpot虚拟机中,本地方法栈和Java虚拟机栈合而为一。
  • 特征:线程私有、规定了OutOfMemoryError、StackOverflowError

Java堆(Java Heap)

  • 作用:存放对象实例(所有的对象实例以及数组都在堆上分配)
  • 特征:线程共享,垃圾收集器管理的主要区域(GC堆)。细分为新生代、老年代。再细分为Eden区、From Survivor区、To Survivor区。多个线程私有的分配缓冲区(Thread Local Allocation Buffer)。可以为不连续的内存区域。通过-Xmx -Xms来控制最大、最小内存,如果没有足够内存来分配对象,而且堆无法扩展,则抛出OutOfMemoryError。

方法区(Method Area)

  • 作用:存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码
  • 特征:线程共享,在HotSpot JVM中使用永久代来实现方法区,通过GC分代收集器管理这部分区域的内存;可以是不连续的内存、可以指定大小、选择不实现垃圾收集;当无法满足内存分配需求时,将抛出OutOfMemoryError;
    在HotSpot JVM中,一般称为“永久代”(Permanent Generation)==在JDK8已被移除==。
    在JDK8中,存储类的元数据已经由native heap(==指的是什么?==)来承担。这个区域被叫做MetaSpace,可以使用-XX:MaxMetaspaceSize=来限制内存使用大小,默认不限制。参考Oracle Blog

运行时常量池

  • 作用:Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。==属于方法区的一部分==
  • 特征:具备动态性,不要求常量一定只有在编译期才能产生,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新常量放入池中,==String类的intern()方法就是利用了这一点==

直接内存

  • 作用:不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。作为堆外内存使用,如NIO中使用通道(Channel)和缓冲区(Buffer)的I/O方式,可以使用native函数直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用,避免了在Java堆和Native堆来回复制数据
  • 特征:这部分内存,不受Java堆大小限制,仅受限于本机内存资源

虚拟机内存中对象数据的生命历程

对象如何创建?如何布局?如何访问?以下基于HotSpot虚拟机的常用内存区域Java堆内存区域,探索Java堆中对象分配、布局和访问的全过程

对象的创建

使用不同的GC收集器时,分配对象内存的算法有所不同

对象的内存布局

对象的访问

HotSpot JVM采用直接指针法来访问对象

OOM异常

Java堆溢出

-XX:SurvivorRatio=8设置两个Survivor区与一个Eden区的比值为2:8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError
* @author luhuancheng
* @date 2019/3/20
*/
public class HeapOOM {

static class OOMObject {

}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}

虚拟机栈和本地方法栈溢出

设置每个线程栈内存大小为128k

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* -Xss160k 设置线程栈容量大小,随着栈容量大小的值增大,递归的深度就越大
* @author luhuancheng
* @date 2019/3/23
*/
public class StackOverflowErrorExample {

private static int stackDepth = 1;

public void stackLeak() {
stackDepth++;
stackLeak();
}

public static void main(String[] args) {
try {
StackOverflowErrorExample stackOverflowErrorExample = new StackOverflowErrorExample();
stackOverflowErrorExample.stackLeak();
} catch (StackOverflowError e) {
System.out.println("stack depth is " + stackDepth);
throw e;
}
}

}

方法区和运行时常量池溢出

JDK8中方法区已经被废弃,取而代之的是MetaSpace,分配在native memory(本机内存)默认不限制大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* -XX:PermSize=10M -XX:MaxPermSize=10M(jdk8时已被废弃)
* -XX:MaxMetaspaceSize=10M(jdk8时采用这个配置设置元数据区)
* @author luhuancheng
* @date 2019/3/23
*/
public class RunTimeConstantPoolOOM {
public static void main(String[] args) {
// JDK8设置MaxMetaspaceSize=10M,达不到溢出效果,为什么?
// List<String> list = new ArrayList<>();
// int i = 0;
// while (true) {
// list.add(String.valueOf(i++).intern());
// }

/*
StringBuilder.toString()会在堆上构造一个String对象
String.intern()会从字符串常量池中获取与str1相同字面量的实例,存在则返回对象指针,不存在则将str1的引用记录到常量池中
由于此处的"计算机软件"是第一次出现,因此str1.intern()返回的是str1的引用。即str1.intern() == str1 结果为true
*/
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1); // true

/*
同理,但是此处的"java"已经不是第一次出现了,str2.intern()从常量池返回的引用时第一次加入常量池的引用,
与StringBuilder.toString()构造的String对象不是同一个。即str2.intern() == str2 结果为false
*/
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2); // false
}
}

直接内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* -Xmx20M -XX:MaxDirectMemorySize=10M
* @author luhuancheng
* @date 2019/3/23
*/
public class DirectMemoryOOM {

private static final int _1MB = 1024 * 1024;

public static void main(String[] args) throws IllegalAccessException {

Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);

Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
// 在MacOS JDK8环境下,无法引发OOM,为什么?
unsafe.allocateMemory(_1MB);
}
}
}

参考资料