JVM内存结构详解

本文详细介绍Java虚拟机(JVM)的内存结构,包括程序计数器、虚拟机栈、本地方法栈、Java堆、方法区等运行时数据区域,以及OutOfMemoryError异常的产生原因和示例代码。

1. 前言

JVM 内存结构指的就是运行时数据区域。

本文会先介绍运行时数据区域的组成部分,然后通过代码演示在运行时数据区域发生 OutOfMemeoryError 异常的情况。

2. 正文

2.1 运行时数据区域

Java 虚拟机在执行 Java 程序的过程中把它所管理的内存划分为若干个不同的数据区域,这些区域就称为运行时数据区域。
运行时数据区域有:程序计数器,虚拟机栈,本地方法栈,Java 堆,方法区。
下面对每块区域进行说明:

2.1.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,可以把它看作是当前线程所执行的字节码的行号指示器。

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

另外,我们知道 Java 虚拟机的多线程是通过时间片轮转的方式来实现的,也就是说在任一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。那么,线程切换后是如何恢复到正确的执行位置的呢?正是依靠程序计数器,每条线程都有一个独立的程序计数器,每条线程独立存储,互不影响。因此,程序计数器是线程私有的,它保证了多线程切换的正常执行。

举例来说明一下,现在小明在写文章,写文章不免要去查看资料,这里就有两个任务:写文章,查看资料。在任一确定的时刻,小明要么是在写文章,要么是在查看资料。在一段时间内,小明是在写文章和查看资料之间来回切换的。那么,小明是怎样保证查看资料后继续写文章到哪里了呢?小明从写文章切换到查看资料时,就把写文章的进度存放了;等从查看资料切换回写文章时,就取出原来存放的写文章进度,继续从那里往下写。对应程序的概念,写文章作为线程A,查看资料作为线程B,存放写文章进度的是程序计数器。

对于 Java 方法和本地方法,程序计数器记录的值是不同的:如果线程正在执行的是一个 Java 方法,那么程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果线程正在执行的是一个本地(Native)方法,那么程序计数器的值则为空(Undefined)。

程序计数器这块内存区域是唯一一个在《Java虚拟机规范》里没有规定任何 OutOfMemory Error 的区域。

2.1.2 Java 虚拟机栈

Java 虚拟机栈(Java Virtual Machine Stack)也是线程私有的,生命周期与线程相同。Java 虚拟栈描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧(Stack Frame)。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。

栈帧里存储局部变量表、操作数栈、动态链接、方法出口等信息。

在《Java 虚拟机规范》里,对这块区域规定了两类异常情况:

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;

如果 Java 虚拟机容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError异常。

2.1.3 本地方法栈

本地方法栈(Native Method Stack),它的作用和 Java 虚拟机栈非常相似,区别是它们的服务对象不同,虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

有的虚拟机(如 Hot-Spot 虚拟机)直接把本地方法栈和虚拟机栈合二为一。

与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowErrorOutOfMemoryError异常。

2.1.4 Java 堆

Java 堆(Java Heap)是虚拟机中所管理的内存中最大的一块。

Java 堆被所有线程共享,在虚拟机启动时被创建。

Java 堆的唯一作用是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。《Java 虚拟机规范》对 Java 堆的描述是:“所有的对象实例以及数组都应当在堆上分配”。换句话说,Java 世界里有些对象实例并不在 Java 堆里分配内存。

到这里,我们应该知道,不应该再说 Java 世界里所有的对象实例都在 Java 堆分配内存。

Java 堆这块内存区域是由垃圾收集器管理的,所以在一些资料中 Java 堆也被称作“GC 堆”(Garbage Collected Heap)。

Java 堆既可以被实现成固定大小的,也可以是可扩展的。如果 Java 堆中没有内存来完成实例分配,并且堆也无法再扩展时,Java 虚拟机就将抛出 OutOfMemoryError 异常。

2.1.5方法区

方法区(Method Area)是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。

另外,介绍一下运行时常量池和直接内存:

2.1.6 运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中有类的版本、字段、方法、接口等描述信息,除这些信息之外,还有一项信息是常量池表(Constant Pool Table),用于存放编译器生成的各种字面量与符号引用,而这部分内容将在类加载完成后存放到方法区的运行时常量池中。

运行时常量池和 Class 文件常量池相比,有一个重要特征:具备动态性。Java 语言并不要求常量一定只有编译期产生,也就是说,并非预置入 Class 文件常量池的内容才能进入方法区的运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的是String 类的 intern 方法。

我们知道运行时常量池是方法区的一部分,自然要受到方法区内存的限制,当运行时常量池无法再申请到内存时就会抛出 OutOfMemoryError 异常。

2.1.7 直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域。

直接内存的分配不受 Java 堆大小的限制,但是还是要受到本机总内存大小以及处理器寻址空间的限制。

这部分内存会被频繁地使用,而且也可能导致 OutOfMemoryError 异常的出现。

2.2 OutOfMemoryError 异常

在《Java 虚拟机规范》的规定里,除了程序计数器之外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError异常的可能。

2.2.1 Java堆溢出

Java 堆用于存储对象实例,只要保证不断创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除掉这些对象,那么随着创建对象的数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。

这是 Java 堆内存溢出异常测试代码:

/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * -Xms20m -Xmx20m 表示设置堆的最小值参数 -Xms为20m,设置堆的最大值参数 -Xmx 为20m,
 * 两者设置为一样的值就避免了堆自动扩展,就限制堆的大小为 20M;
 * -XX:+HeapDumpOnOutOfMemoryError 让虚拟机在内存溢出时 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 java_pid1696.hprof ...
Heap dump file created [28281568 bytes in 0.162 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at com.java.advanced.features.jvm.HeapOOM.main(HeapOOM.java:17)

根据内存中导致 OOM 的对象是否是必要的,可以区分出是出现了内存泄露(Memory Leak)还是出现了内存溢出(Memory Overflow)。

内存泄露是指不再需要的对象却没有被垃圾回收机制回收,也就是说它们还存在到 GC Roots 的引用链。

内存溢出是指内存中的对象确实是必须存活的,是程序所需要的,但是超过了内存限制而导致的。

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

当栈帧太大或者虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot 虚拟机抛出的都是StackOverflowError异常。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

willwaywang6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值