说在前面我是一个刚接触 JVM 的新手。这篇文章是我在啃资料、看视频、反复问自己这个玩意儿到底有什么用之后整理出来的笔记。我不会说这个很简单——因为对我真不简单。如果你也是刚开始学希望能帮到你。我是怎么开始学这个的事情是这样的。之前写 Java 代码遇到过一个报错叫OutOfMemoryError: Java heap space我当时看了两眼一黑只知道哦内存不够了。但是内存不够了为什么会是这个报错为什么有的报错叫StackOverflowError有的叫Metaspace我完全不知道。后来跟朋友聊他说你先不要管报错你先搞清楚 JVM 把内存分成了哪几块。好我就从这个开始学。JVM 把内存分成了五块JDK 8我先说结论然后再一块一块说。JDK 8 里面JVM 运行时内存分成了五个区域程序计数器Program Counter RegisterJava 虚拟机栈JVM Stack本地方法栈Native Method StackJava 堆Heap元空间Metaspace还有一个叫直接内存Direct Memory这个不属于 JVM 管但也算内存的一部分后面会提。乍一看五个名字挺吓人的但别怕我一个一个拆开讲。程序计数器——最小的一块也是唯一不会溢出的地方这个东西的名字听着很厉害其实它的功能挺简单记录当前线程执行到哪一行字节码了。你可以把它理解成你看书的时候手里夹的那张书签。你读的是哪一页、哪一行书签帮你记着。线程也一样CPU 把它的时间片切走了它要去干别的线程等切回来的时候程序计数器帮它找到刚才读到哪儿了。有两个点我觉得有意思如果执行的是Native 方法也就是本地方法比如 C 写的代码程序计数器的值是undefined。因为 native 方法走的是本地指令不是 JVM 的字节码没法用这个计数器来记。它是 JVM 五大区域里唯一不会发生 OutOfMemoryError的地方。规范里就没给它留这个异常。为什么它是线程私有的因为每个线程各读各的代码各走各的执行路径。如果共用一个计数器切换回来就不知道切到谁那儿了。所以每个线程自己管自己的。Java 虚拟机栈——和方法调用关系最密切的一块栈在数据结构里是什么意思大家应该知道——先进后出。Java 虚拟机栈也是一样。每个方法被调用的时候会往栈里压入一个栈帧Stack Frame方法执行完栈帧弹出。一个栈帧里面装的东西包括局部变量表方法里定义的局部变量就放这儿操作数栈做运算的时候临时放数据的地方动态链接涉及到多态、方法调用时用到的符号引用转直接引用方法出口方法执行完了之后返回到哪儿去我当时学到这里脑子里冒出的第一个问题是那这个栈到底多大答案是不确定。取决于 JVM 的参数设置-Xss。但不管设置多大如果方法调用的层数太深比如无限递归栈深度就超过上限了会抛StackOverflowError。还有一种是栈扩展的时候没成功抛OutOfMemoryError。这个场景比较少见一般是线程数量太多导致的。这也是线程私有的——每个线程有自己的虚拟机栈。本地方法栈——和上面那个差不多但不是一回事Java 虚拟机栈是为 Java 方法服务的。那如果调了一个 native 方法呢走的不是 Java 的字节码不能走 Java 虚拟机栈——所以单独搞了一个本地方法栈。不过 HotSpot 虚拟机就是我们最常用的那个 Oracle JDK 和 OpenJDK 带的虚拟机里面本地方法栈和 Java 虚拟机栈合并到一起了。所以实际用的时候感受不到是两个。同样可能抛StackOverflowError和OutOfMemoryError。Java 堆——最大的一块也是 GC 最忙的地方堆是 JVM 里最大的一块内存。所有的对象实例和数组都在这里分配。它是线程共享的JVM 启动的时候就创建好了。堆里面还被划分成了几个子区域为了垃圾回收效率优化新生代Young Generation里面又分三个部分——一个 Eden 区、两个 Survivor 区S0 和 S1老年代Old Generation / Tenured Generation我刚学到这里的时候很不理解为什么堆还要分这么多区域后来知道这是为了分代回收——大部分对象活不了多久就死了比如循环里 new 的对象新生代回收频率高但快少部分对象命硬会晋升到老年代回收频率低。这是一种典型的二八定律思路。堆不够用的时候抛的异常大家都眼熟OutOfMemoryError: Java heap space。元空间Metaspace——取代了方法区的新东西先说方法区Method Area是什么。方法区是 JVM 规范里的一个逻辑区域用来存放已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码缓存这个区虽然是堆的逻辑组成部分但有个别名叫非堆Non-Heap。所以它虽然是堆的概念但不算堆的实体。在 JDK 8 之前方法区的实现叫永久代PermGen在 JVM 堆里面。到 JDK 8永久代被干掉了换成了元空间Metaspace。关键的区别是元空间使用了本地内存不再是 JVM 堆的一部分了。这意味着它的大小不再受 JVM 堆大小的限制而是受本机物理内存的限制。元空间不够用的时候抛OutOfMemoryError: Metaspace。运行时常量池——方法区的一部分常量池这个词可能你在反编译 class 文件的时候见过。编译器把类里的字面量比如字符串常量 “hello”、final 常量等和符号引用类名、方法名、字段名的引用放到这个池子里。运行时常量池就是这些信息在运行时的存放位置是方法区的一部分。还有一个特点动态性。不是只有编译期生成的东西才能放进去。你在代码里调String.intern()也可以在运行时把字符串扔到常量池里。内存不够的时候也会抛 OutOfMemoryError。直接内存——不属于 JVM但 JVM 可以操作直接内存是操作系统的本地内存不归 JVM 管。那它跟 JVM 有什么关系Java 里有一个 NIONew I/O的 API可以通过ByteBuffer.allocateDirect()分配一块堆外内存。这块内存的好处是在进行 I/O 操作比如读写文件、网络通信时数据不用在堆内存和本地内存之间来回拷贝性能好很多。很多高性能框架比如Netty就是基于这个原理做优化的。但是——它是本地内存所以也受物理内存上限的限制。用得不好也会报OutOfMemoryError: Direct buffer memory。五种内存溢出一张表总结学完之后我把五种溢出情况整理了一下溢出区域异常信息常见原因堆溢出Java heap spacenew 大对象、内存泄漏、GC 回收不掉栈溢出StackOverflowError/OutOfMemoryError递归太深、线程开太多元空间溢出Metaspace加载类太多、动态类生成、CGLIB 等直接内存溢出Direct buffer memorydirect buffer 没回收、一直分配程序计数器那行没写——因为它是唯一不会溢出的。最后想说的这些东西我一开始觉得是背概念背完就忘。后来我发现真正帮我理解的不是背而是带着问题去查“为什么栈会溢出”——因为我写过递归忘记写终止条件“堆溢出是什么时候遇到的”——因为我导了一个超大 Excel 没分页“元空间溢出在什么场景出现”——因为看到过用 CGLIB 动态代理搞出来的案例如果你是新手我的建议是不要硬背分区名称。先把以下两个记住就行线程私有的程序计数器、Java 虚拟机栈、本地方法栈线程共享的堆、元空间方法区剩下的遇到具体的报错再去翻。等报错见多了分区图自然就长在脑子里了。
JVM 内存到底分了哪几块——我的学习笔记
发布时间:2026/7/1 10:17:16
说在前面我是一个刚接触 JVM 的新手。这篇文章是我在啃资料、看视频、反复问自己这个玩意儿到底有什么用之后整理出来的笔记。我不会说这个很简单——因为对我真不简单。如果你也是刚开始学希望能帮到你。我是怎么开始学这个的事情是这样的。之前写 Java 代码遇到过一个报错叫OutOfMemoryError: Java heap space我当时看了两眼一黑只知道哦内存不够了。但是内存不够了为什么会是这个报错为什么有的报错叫StackOverflowError有的叫Metaspace我完全不知道。后来跟朋友聊他说你先不要管报错你先搞清楚 JVM 把内存分成了哪几块。好我就从这个开始学。JVM 把内存分成了五块JDK 8我先说结论然后再一块一块说。JDK 8 里面JVM 运行时内存分成了五个区域程序计数器Program Counter RegisterJava 虚拟机栈JVM Stack本地方法栈Native Method StackJava 堆Heap元空间Metaspace还有一个叫直接内存Direct Memory这个不属于 JVM 管但也算内存的一部分后面会提。乍一看五个名字挺吓人的但别怕我一个一个拆开讲。程序计数器——最小的一块也是唯一不会溢出的地方这个东西的名字听着很厉害其实它的功能挺简单记录当前线程执行到哪一行字节码了。你可以把它理解成你看书的时候手里夹的那张书签。你读的是哪一页、哪一行书签帮你记着。线程也一样CPU 把它的时间片切走了它要去干别的线程等切回来的时候程序计数器帮它找到刚才读到哪儿了。有两个点我觉得有意思如果执行的是Native 方法也就是本地方法比如 C 写的代码程序计数器的值是undefined。因为 native 方法走的是本地指令不是 JVM 的字节码没法用这个计数器来记。它是 JVM 五大区域里唯一不会发生 OutOfMemoryError的地方。规范里就没给它留这个异常。为什么它是线程私有的因为每个线程各读各的代码各走各的执行路径。如果共用一个计数器切换回来就不知道切到谁那儿了。所以每个线程自己管自己的。Java 虚拟机栈——和方法调用关系最密切的一块栈在数据结构里是什么意思大家应该知道——先进后出。Java 虚拟机栈也是一样。每个方法被调用的时候会往栈里压入一个栈帧Stack Frame方法执行完栈帧弹出。一个栈帧里面装的东西包括局部变量表方法里定义的局部变量就放这儿操作数栈做运算的时候临时放数据的地方动态链接涉及到多态、方法调用时用到的符号引用转直接引用方法出口方法执行完了之后返回到哪儿去我当时学到这里脑子里冒出的第一个问题是那这个栈到底多大答案是不确定。取决于 JVM 的参数设置-Xss。但不管设置多大如果方法调用的层数太深比如无限递归栈深度就超过上限了会抛StackOverflowError。还有一种是栈扩展的时候没成功抛OutOfMemoryError。这个场景比较少见一般是线程数量太多导致的。这也是线程私有的——每个线程有自己的虚拟机栈。本地方法栈——和上面那个差不多但不是一回事Java 虚拟机栈是为 Java 方法服务的。那如果调了一个 native 方法呢走的不是 Java 的字节码不能走 Java 虚拟机栈——所以单独搞了一个本地方法栈。不过 HotSpot 虚拟机就是我们最常用的那个 Oracle JDK 和 OpenJDK 带的虚拟机里面本地方法栈和 Java 虚拟机栈合并到一起了。所以实际用的时候感受不到是两个。同样可能抛StackOverflowError和OutOfMemoryError。Java 堆——最大的一块也是 GC 最忙的地方堆是 JVM 里最大的一块内存。所有的对象实例和数组都在这里分配。它是线程共享的JVM 启动的时候就创建好了。堆里面还被划分成了几个子区域为了垃圾回收效率优化新生代Young Generation里面又分三个部分——一个 Eden 区、两个 Survivor 区S0 和 S1老年代Old Generation / Tenured Generation我刚学到这里的时候很不理解为什么堆还要分这么多区域后来知道这是为了分代回收——大部分对象活不了多久就死了比如循环里 new 的对象新生代回收频率高但快少部分对象命硬会晋升到老年代回收频率低。这是一种典型的二八定律思路。堆不够用的时候抛的异常大家都眼熟OutOfMemoryError: Java heap space。元空间Metaspace——取代了方法区的新东西先说方法区Method Area是什么。方法区是 JVM 规范里的一个逻辑区域用来存放已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码缓存这个区虽然是堆的逻辑组成部分但有个别名叫非堆Non-Heap。所以它虽然是堆的概念但不算堆的实体。在 JDK 8 之前方法区的实现叫永久代PermGen在 JVM 堆里面。到 JDK 8永久代被干掉了换成了元空间Metaspace。关键的区别是元空间使用了本地内存不再是 JVM 堆的一部分了。这意味着它的大小不再受 JVM 堆大小的限制而是受本机物理内存的限制。元空间不够用的时候抛OutOfMemoryError: Metaspace。运行时常量池——方法区的一部分常量池这个词可能你在反编译 class 文件的时候见过。编译器把类里的字面量比如字符串常量 “hello”、final 常量等和符号引用类名、方法名、字段名的引用放到这个池子里。运行时常量池就是这些信息在运行时的存放位置是方法区的一部分。还有一个特点动态性。不是只有编译期生成的东西才能放进去。你在代码里调String.intern()也可以在运行时把字符串扔到常量池里。内存不够的时候也会抛 OutOfMemoryError。直接内存——不属于 JVM但 JVM 可以操作直接内存是操作系统的本地内存不归 JVM 管。那它跟 JVM 有什么关系Java 里有一个 NIONew I/O的 API可以通过ByteBuffer.allocateDirect()分配一块堆外内存。这块内存的好处是在进行 I/O 操作比如读写文件、网络通信时数据不用在堆内存和本地内存之间来回拷贝性能好很多。很多高性能框架比如Netty就是基于这个原理做优化的。但是——它是本地内存所以也受物理内存上限的限制。用得不好也会报OutOfMemoryError: Direct buffer memory。五种内存溢出一张表总结学完之后我把五种溢出情况整理了一下溢出区域异常信息常见原因堆溢出Java heap spacenew 大对象、内存泄漏、GC 回收不掉栈溢出StackOverflowError/OutOfMemoryError递归太深、线程开太多元空间溢出Metaspace加载类太多、动态类生成、CGLIB 等直接内存溢出Direct buffer memorydirect buffer 没回收、一直分配程序计数器那行没写——因为它是唯一不会溢出的。最后想说的这些东西我一开始觉得是背概念背完就忘。后来我发现真正帮我理解的不是背而是带着问题去查“为什么栈会溢出”——因为我写过递归忘记写终止条件“堆溢出是什么时候遇到的”——因为我导了一个超大 Excel 没分页“元空间溢出在什么场景出现”——因为看到过用 CGLIB 动态代理搞出来的案例如果你是新手我的建议是不要硬背分区名称。先把以下两个记住就行线程私有的程序计数器、Java 虚拟机栈、本地方法栈线程共享的堆、元空间方法区剩下的遇到具体的报错再去翻。等报错见多了分区图自然就长在脑子里了。