JVM内存模型-五大区浅解

本文详细介绍了Java虚拟机(JVM)的五大内存区域:程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区的功能及运作原理。探讨了对象在Java中的分配与访问机制,并对比了句柄和直接指针两种访问方式的特点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

JVM五大区

这里写图片描述

 

一、程序计数器

程序计数器( Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。该区域是属于线程私有的,因为在多线程环境中CPU通过在不同的线程来高速切换,此时程序计数器需要记录当前线程执行到哪一步了,以便下一次CPU可以在这个记录点上继续执行。 每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为
此内存区域是唯一一个在Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域

 

二、java虚拟机栈

线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧( Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 

通常讲的堆内存和栈内存中的栈指的就是现在的虚拟机栈,或者说是虚拟机栈中局部变量表部分。局部变量表存放了编译期可知的8种基本数据类型.对象引用( reference 类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址) 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常 
如果虚拟机栈扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。

 

三、本地方法栈

与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native方法服务。有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

 

四、java堆

Java堆是 Java 虚拟机所管理的内存中最大的一块。 Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
这一点在java虚拟机规范中这样描述:所有的对象实例以及数组都要在堆上分配
Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆( ” Garbage Collected Heap)。如果从内存回收的角度看,Java堆又会划分为好几个区域(新时代,老年代,等等)如果从内存分配的角度看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区。但无论怎么去划分,无论那个区域,java堆中存储的依然是对象的实例
根据java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间内,只要逻辑上是连续的即可,就像我们的磁盘空间一样。
进一步划分的目的是为了更好地回收内存,或者更快地分配内存。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

 

五、方法区

与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
*HotSpot 虚拟机上很多人愿意把方法区称为“永久代”( Permanent Generation),本质上两者并不等价,仅仅是因为 HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载.在JDK1.7的HotSpot中,把原本放在永久代中的字符串常量池移出了。
 

运行时常量池

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

另:直接内存

直接内存并不是虚拟机规范定义的数据区的一部分,也不是虚拟机运行时数据区的一部分。但是这部分内存也被频繁的使用,而且也可能导致OutOfMemoryerror。
在JDK1.4中新加入了NIO类,引入了一种基于通道与缓冲区的I/O方式。它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场合中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

 

虽然直接内存的分配不受java堆大小的限制,但是会受到本机总内存大小的限制。在配置虚拟机参数时,经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError。

 

 

 

逻辑内存模型我们已经看到了,那当我们建立一个对象的时候是怎么进行访问的呢?

在Java 语言中,对象访问是如何进行的?对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区

域之间的关联关系,如下面的这句代码:

Object obj = new Object();

假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java 栈

的本地变量表中,作为一个reference 类型数据出现。而“new Object()”这部分的语义

将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值(Instance Data,对

象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布

局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java 堆中

还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地

址信息,这些类型数据则存储在方法区中。

由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有

定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此

不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。

如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference

中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的

具体地址信息,如下图所示。

 

 

 如果使用直接指针访问方式,

Java 堆对象的布局中就必须考虑如何放置访问类型

数据的相关信息,reference 中直接存储的就是对象地址,如下图所示

 

 

 这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference 中存

储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只

会改变句柄中的实例数据指针,而reference 本身不需要被修改。

使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开

销,由于对象的访问在Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的

执行成本。就本书讨论的主要虚拟机Sun HotSpot 而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。

 

 

 

 

 

<think>嗯,用户让我介绍JVM内存模型,还要画堆、栈、方法区的内存分配图,模拟OOM和StackOverflow的场景,并用VisualVM捕获日志分析原因。首先,我得确保自己对JVM内存结构有清晰的理解,可能得先回忆一下各个区域的作用和关系。 用户可能需要的是比较直观的解释,特别是内存分配图。堆、栈、方法区,这些部分的位置和功能要明确。堆是存放对象实例的,栈是方法执行时的内存模型,存放局部变量和方法调用,方法区则存类信息、常量、静态变量等。但要注意,不同版本的JVM可能有差异,比如方法区在JDK8之后被元空间取代,使用本地内存,这点需要提到吗?可能还是按照常见的结构来画,避免混淆。 接下来是模拟OOM和StackOverflow的场景。OOM通常发生在堆内存不足,而StackOverflow是栈溢出。需要写两个例子,一个不断创建对象导致堆溢出,另一个用递归调用导致栈溢出。要注意示例代码的正确性,比如在堆溢出的时候,可能需要用集合保持对象引用,否则可能被GC回收,无法触发OOM。而栈溢出的话,递归没有终止条件即可。 然后是用VisualVM捕获日志。需要指导用户如何启动VisualVM,监控JVM,设置参数比如堆内存大小,让OOM更快出现。比如使用-Xmx参数限制堆大小,然后运行程序,当发生OOM时,VisualVM会显示内存使用情况,生成堆转储文件,分析哪个对象占用了最多内存。对于栈溢出,可能需要在运行时看到栈深度,或者线程的栈跟踪信息。 分析原因时,堆OOM可能是因为内存泄漏或者确实需要更大的堆,而栈溢出通常是递归太深或者循环调用。需要提醒用户检查代码中的这些问题。 另外,用户可能需要了解如何配置JVM参数,比如-XX:+HeapDumpOnOutOfMemoryError让JVM在OOM时自动生成dump文件,方便分析。还有栈的大小可以通过-Xss来调整,但默认情况下栈溢出可能更多是代码问题。 最后,确保回答结构清晰,分点说明,步骤明确,示例代码正确,并且分析到位。可能需要检查是否有遗漏的部分,比如方法区也可能发生OOM,比如加载过多类的情况,但用户可能主要关注堆和栈,所以可以简要提一下方法区的OOM情况,但重点放在堆和栈上。</think>✅ JVM内存模型详解(附内存分配示意图) 一、内存结构示意图 ``` [ JVM内存布局 ] ┌──────────────────┐ │ 堆(Heap) │ ← 存储所有对象实例/数组 │ │ ├──────────────────┤ │ 方法区(Method) │ ← 类信息/常量/静态变量(JDK8后为元空间) │ Area │ ├──────────────────┤ │ 虚拟机栈(Stack) │ ← 线程私有(栈帧存储局部变量/操作数栈/动态链接) │ 程序计数器(PC) │ │ 本地方法栈(Native)│ └──────────────────┘ ``` 二、模拟OOM与StackOverflow场景 1. 堆内存溢出(OOM) ```java // 启动参数:-Xmx10m -XX:+HeapDumpOnOutOfMemoryError public class HeapOOM { public static void main(String[] args) { List<byte[]> list = new ArrayList<>(); while(true) { list.add(new byte[1024 * 1024]); // 每次分配1MB } } } ``` VisualVM分析步骤: - 查看堆内存曲线飙升 - 分析Heap Dump文件 - 定位占用最大的对象(本例是byte[]数组) 2. 栈溢出(StackOverflow) ```java public class StackOverflow { public static void recursiveCall() { recursiveCall(); // 无限递归 } public static void main(String[] args) { recursiveCall(); } } ``` VisualVM分析步骤: - 观察线程栈跟踪 - 查看最大调用深度(默认栈大小1MB) - 定位重复的栈帧(本例显示recursiveCall重复调用) 三、关键内存区域对比 ``` 区域 生命周期 线程共享 存储内容 异常类型 堆 与JVM共存亡 共享 对象实例 OutOfMemoryError 方法区 与JVM共存亡 共享 类元数据 OutOfMemoryError 虚拟机栈 与线程共存亡 私有 栈帧/局部变量 StackOverflowError ``` 四、问题排查建议 1. OOM常见原因: - 内存泄漏(未释放对象引用) - 数据量超过堆配置(-Xmx设置过小) - 大对象直接进入老年代 2. StackOverflow排查: - 检查递归终止条件 - 调整栈大小(-Xss参数) - 避免方法嵌套层级过深 、VisualVM使用技巧 1. 安装插件:工具->插件->安装Visual GC插件 2. 内存监控:抽样器->内存,查看对象分配情况 3. 线程分析:线程标签页查看实时栈跟踪 4. 堆转储分析:右键对应进程->堆Dump,使用OQL查询大对象 示例异常日志特征: ``` // OOM日志 java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid1234.hprof... // StackOverflow日志 java.lang.StackOverflowError at StackOverflow.recursiveCall(StackOverflow.java:3) at StackOverflow.recursiveCall(StackOverflow.java:3) ...(重复调用栈) ``` 可通过jstat命令实时监控: ```bash jstat -gcutil <pid> 1000 # 每秒打印GC统计 jstack <pid> > thread.txt # 导出线程快照 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值