1. JVM运行时数据区域
程序计数器(各线程私有)
一块较小的内存空间,可看成是当前线程执行字节码的行号指示器,用于记录当前线程字节码执行到哪儿了,这样线程切换的时候才能够恢复到正确的执行位置。Java虚拟机栈(各线程私有)
其生命周期和线程相同。Java虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧(局部变量表、操作数栈、动态链接、方法出口等信息),一个方法的调用直至完成对应着一个栈帧在虚拟机栈中的入栈到出栈。
局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(指针/句柄)、returnAddress类型。本地方法栈(各线程私有)
功能类似虚拟机栈,虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机使用的Native方法服务。Java堆(各线程共享)
Java堆在虚拟机启动时创建,是JVM管理的内存中最大的一块。用于存放所有的对象实例和数组。Java堆可以是物理上不连续的内存空间(逻辑上连续即可)。方法区(各线程共享)
用于存放已经被虚拟机加载的类信息和运行时常量池(动态常量池)内的数据。
类信息包含类的版本、字段、方法、接口等描述信息以及Class文件的常量池(静态常量池),Class文件的常量池用于存放编译期生成的各种字面量和符号引用。
运行时常量池在类加载后会将Class文件常量池中的内容放入运行时常量池中,同时运行期间产生的常量也可以放入到方法区的运行时常量池中(例如String的intern()方法)。
2. 直接内存
直接内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。NIO中引入了一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后用Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这块内存是直接分配在物理内存中的(Java堆外),不受Java堆大小的限制,但是受物理内存大小限制(物理限制和处理器寻址空间限制)。
3. 对象创建
HotSpot虚拟机中,对象的创建过程大致如下:
- 虚拟机遇到new指令时,先进行类加载检查
- 在Java堆中为对象分配内存空间
- 内存分配完成后,虚拟机将该内存空间都初始化为零值(对象头除外)
- 设置对象头信息
- 执行init方法,初始化对象
4. 对象内存布局
HotSpot虚拟机中,对象的内存布局可分为三个区域:对象头、实例数据和对齐填充。
- 对象头包含:a)用于存储对象自身的运行时数据(哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等);b)类型指针(对象指向它的类元数据的指针),虚拟机通过该指针确定对象是哪个类的实例。
- 实例数据:对象真正存储的有效信息(代码中定义的各种类型的字段内容),包括父类继承的和子类中定义的。
- 对齐填充:HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,对齐填充用于补全实例数据。
5. 对象访问
Java程序通过栈上的reference数据来操作堆上的具体对象。
-
使用句柄访问
在Java堆中划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,句柄中包含对象的实例地址和类型地址。
好处:reference中存储的是句柄地址是固定的,即使对象被移动了(例如GC)也不需要修改reference,只需修改句柄中的对象实例地址。 -
使用直接地址访问
reference中存储的是Java堆中对象实例地址,对象的布局中需要包含对象类型的相关信息。
好处:节省了一次指针定位的时间开销,速度更快。
6. OutOfMemoryError异常
- Java堆溢出
/**
* JVM启动参数:
* -verbose:gc 打印GC信息
* -Xms20m 最小堆内存
* -Xmx20m 最大堆内存,最大堆内存=最小堆内存时不会自动扩展堆
* -XX:+HeapDumpOnOutOfMemoryError 当内存溢出时Dump出当前内存中堆的转储快照
* -XX:HeapDumpPath=D:\dump 堆转储快照保存地址
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
// 异常信息
java.lang.OutOfMemoryError: Java heap space
Dumping heap to D:\dump\java_pid105876.hprof ...
Heap dump file created [26566702 bytes in 0.273 secs]
解决方法:
- 利用内存映像分析工具(例如Eclipse Memory Analyzer)分析Dump出来的堆转储快照,确认内存中的对象是否是必要的(即确定是内存泄露还是内存溢出)
- 如果是内存泄露,进一步通过工具查看泄露对象到GC Roots的引用链,从而定位泄露代码的位置
- 如果不存在内存泄露,则检查JVM的堆参数设置,检查代码中是否存在某些对象生命周期过长、持有状态时间过长等情况。
-
虚拟机栈和本地方法栈溢出
HotSpot虚拟机中不区分虚拟机栈和本地方法栈。
1)如果某个线程请求的栈深度大于虚拟机所允许的最大深度,则抛出StackOverflowError
/**
* 虚拟机栈StackOverflow
* -Xss128k 栈容量大小
*/
public class JVMStackSOF {
public void stackLeak() {
stackLeak();
}
public static void main(String[] args) {
JVMStackSOF sof = new JVMStackSOF();
sof.stackLeak();
}
}
// 异常信息
Exception in thread "main" java.lang.StackOverflowError
at oom.JVMStackSOF.stackLeak(JVMStackSOF.java:9)
at oom.JVMStackSOF.stackLeak(JVMStackSOF.java:9)
at oom.JVMStackSOF.stackLeak(JVMStackSOF.java:9)
出现StackOverflowError的原因:每个方法执行时都会往栈中压入一栈帧(各栈帧的大小也不一样),当不停的压入栈帧达到虚拟机允许的最大深度时就会抛出StackOverflowError。大多情况下,栈深达到1000-2000没问题,足够正常的方法调用。
2)如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError:
/**
* 虚拟机栈OutOfMemoryError
* -Xss2m 每个线程的栈内存
*/
public class JVMStackOOM {
private void keepRunning() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
keepRunning();
}
});
thread.start();
}
}
public static void main(String[] args) {
JVMStackOOM oom = new JVMStackOOM();
oom.stackLeakByThread();
}
}
// 异常信息
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
产生OutOfMemoryError的原因:操作系统给每个进程分配的内存是有限的(例如32位Windows限制为2G),那么:
所有线程的虚拟机栈+本地方法栈+程序计数器内存(可忽略)=进程内存限制-最大堆内存(Xmx)-最大方法区容量(MaxPermSize)。
当所有线程的虚拟机栈+本地方法栈内存超出限制后就会出现OutOfMemoryError。
方法区溢出
方法区用于存放类信息,如类名、访问修饰符、常量池、字段描述、方法描述等。如果在运行时产生大量的类填满方法区直至溢出,就会产生OutOfMemoryError。本机直接内存溢出
直接内存溢出,一个明显的特征是Heap Dump文件中没有明显的异常。如果直接或间接使用了NIO,而Dump文件很小,可以考虑是直接内存溢出。