简单了解 JVM 内存模型
共计 3273 个字符,预计需要花费 9 分钟才能阅读完成。
JVM 的内存区域主要分为如图所示的几个区域。
/>
1. 程序计数器(线程私有)
程序计数器的作用是存储当前线程执行的字节码指令的地址。并且在多线程环境下,每个线程都有一个独立的程序计数器。同时,此内存区域是唯一一个在Java虚拟机规范中没有规定任何 OOM 情况的区域。
2. Java虚拟机栈(线程私有)
栈主要用于存储局部变量、部分结果以及返回地址等,其中局部变量如果是对象,则存储的是对应的地址。另外,栈又分为栈帧,每个方法都会生成一个栈帧,方法执行结束后,对应的栈帧被弹出。
在 Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚
拟机所允许的深度,将抛出 StackOverflowError 异常:如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。
以下是代码通过递归调用导致 StackOverflowError:
public class JVMStackSOF {
public static int stackLength = 1;
/**
* VM 参数:-Xss128k
* 在单个线程下,无论是由于栈顿太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
* 如果测试时不限于单线程,通过不断地建立线程的方式倒到是可以产生内存溢出异常
*/
public static void main(String[] args) {
JVMStackSOF jvmStackSOF = new JVMStackSOF();
try {
jvmStackSOF.stackLeak();
}catch (Throwable e){
System.out.println("栈深度:" + JVMStackSOF.stackLength);
throw e;
}
}
public void stackLeak(){
stackLength++;
stackLeak();
}
}
以下代码理论上可以导致 JVM 虚拟机栈 OOM,但是似乎没有实现。
public class JVMStackOOM {
/**
* VM 参数:-Xss2M
*/
public static void main(String[] args) {
JVMStackOOM jvmStackOOM = new JVMStackOOM();
jvmStackOOM.stackLeakByThread();
}
public void stackLeakByThread(){
while (true){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
private void dontStop(){
while (true){}
}
}
3. 本地方法栈(线程私有)
与 Java 虚拟机栈类似,但为本地(Native)方法服务。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
4. Java堆(线程共享)
Java 堆是 JVM 管理的最大的一块内存区域,并且堆是线程共享的。它用于存储对象的实例,包括应用程序的对象和数组。
几乎所有的对象实例都在这里分配内存,在 Java 虚拟机规范中的指述是:所有的对象实例以及数组都要在堆上分配,但是随着 JIT 编译器的发展与逃逸分析折术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么绝对了。
堆内存也是最容易发生 OOM 的区域,所以在 JVM 调优中需要最注意堆内存大小的调整。
以下是一个简单实现 OOM 报错的代码:
public class HeapOOM {
public static class OOMObject{
}
/**
* VM 参数:-verbose:gc -Xms5M -Xmx5M -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:SurvivorRatio=8
* @param args
*/
public static void main(String[] args) {
List<OOMObject> heapOOMList = new ArrayList<>();
while (true){
heapOOMList.add(new OOMObject());
}
}
}
5. 元空间(线程共享)
在 JDK1.7 之前,HotSpot 虚拟机把方法区当成永久代(方法区的落地实现)来进行垃圾回收。而从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
元空间和永久代不同的地方在于:
-
存储位置不同:永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存。
-
存储内容不同:在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置人 Class 文件中常量池的内容才能进人方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 internO 方法。
6. 直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频紧地使用,而且也可能导致 OOM 异常出现。
在 JDK1.4 中新加人了 NIO(New Input/Output)类,引人了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
直接内存 OOM 的案例代码如下:
public class DirectMemoryOOM {
// 定义一个常量,表示1兆字节的大小
private static final int _1MB = 1024 * 1024;
/**
* VM 参数: -Xmx20M -XX:MaxDirectMemorySize=10M -XX:+HeapDumpOnOutOfMemoryError
* 由DirectMemory导致的内存溢出,一个明显的特征是在HeapDump文件中不会看见明显的异常,
* 如果发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因。
*/
public static void main(String[] args) throws Exception {
// 通过反射获取Unsafe类的实例
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
// 设置访问权限,允许访问私有字段
unsafeField.setAccessible(true);
// 获取Unsafe类的实例
Unsafe unsafe = (Unsafe) unsafeField.get(null);
// 无限循环,不断分配内存,导致直接内存溢出
while (true) {
// 使用Unsafe类的allocateMemory方法分配1兆字节的直接内存
unsafe.allocateMemory(_1MB);
}
}
}
提醒:本文发布于288天前,文中所关联的信息可能已发生改变,请知悉!
Tips:清朝云网络工作室