通过前一节的内容我们了解了,我们的Java程序都是运行在JVM上面的,那么JVM具体内部是怎样划分的呢?我们来具体了解一下
JVM内存区域可以被划分为这样几个核心部分:运行时数据区、直接内存、执行引擎、本地库接口和垃圾回收系统
- 运行时数据区:是内存的核心部分,堆、方法区、虚拟机栈等等都在这个区域内,真正存储数据的地方
- 直接内存:用于NIO的直接缓冲区分配,绕过JVM堆,用作大文件IO、图像处理等
- 执行引擎:执行字节码指令,将Java代码转换为机器指令,主要包括解释器、即时编译器(JIT)、垃圾回收器、本地方法接口等
- 本地库接口:提供Java代码调用本地方法(C、C++)的能力
- 垃圾回收系统:用于回收堆内存中不再使用的对象,常见垃圾收集器包括CMS、Serial、G1等等
现在我们对这几个核心区域有了一定了解,接下来我们着重介绍一下运行时数据区和垃圾回收系统这两部分
一、运行时数据区
运行时数据区(Runtime Data Areas)分为五个核心部分:堆、方法区、虚拟机栈、本地方法栈、程序计数器
1.1 堆
堆是JVM管理的最大一块内存区域,用于存储所有对象实例和数组。它是垃圾回收的主要区域,所有线程共享堆内存
1.1.1 堆的内存结构
堆采用分代设计,主要分为年轻代和老年代
年轻代 :
Eden区(伊甸园区):新创建的对象首先分配在Eden区
Survivor0区(From区):存放第一次Minor GC后存活的对象
Survivor1区(To区):存放第二次Minor GC后存活的对象
老年代
存放长期存活的对象,包括:
经历过多次Minor GC仍然存活的对象(默认年龄阈值15次)
大对象直接进入老年代(避免在年轻代频繁复制)
年轻代空间不足时,部分对象会提前晋升到老年代
1.1.2 对象在堆中的内存布局

总结起来包括三部分:
- 对象头:包括标记字段(存储哈希码、GC分代年龄、锁状态等)和类型指针(指向方法区中的类元数据)
- 实例数据:存储对象的字段值
- 对齐填充:保证对象的大小是8字节的倍数
1.1.3 堆的内存分配策略
指针碰撞(Bump the Pointer):内存规整时,通过移动指针分配内存
空闲列表(Free List):内存不规整时,维护空闲内存列表
TLAB(Thread Local Allocation Buffer):每个线程预先分配一小块内存,减少竞争
1.2 方法区
方法区存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
1.2.1 方法区存储的具体内容
- 类型信息:类的全限定名;类的直接父类的全限定名;类的修饰符(public、abstract、final等);类实现的接口列表
- 运行时常量池:字面量(字符串、final常量值等);符号引用(类和接口的全限定名、字段的名称和描述符、方法的名称和描述符)
- 字段信息:字段名称;字段类型;字段修饰符
- 方法信息:方法名称;方法返回类型;方法参数数量和类型;方法修饰符;方法字节码、操作数栈、局部变量表大小;异常表
- 类变量(静态变量):JDK7及以前在方法区,JDK8+在堆中(但概念上仍属于类信息)
- 指向类加载器的引用
- 指向Class对象的引用
- 方法表(虚方法表):提高虚方法调用的效率
1.2.2 JDK版本演变
- JDK 1.6及以前:
- 方法区实现为永久代(PermGen)
- 字符串常量池在永久代
- 容易发生PermGen Space OOM
- JDK 1.7:
- 字符串常量池移到堆中
- 静态变量移到堆中
- JDK 1.8+:
- 永久代被移除,改为元空间(Metaspace)
- 元空间使用本地内存,不再受JVM堆大小限制
- 字符串常量池在堆中,类元数据在元空间
1.2.3 元空间特性
- 内存位置:使用本地内存
- 自动扩展:默认情况下,元空间可动态扩展,直到耗尽系统内存
- 压缩:支持类指针压缩,节省内存
1.3 虚拟机栈
虚拟机栈是线程私有的,生命周期与线程相同。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息
栈帧结构大体长这样

1.3.1 方法调用/返回时的栈帧变化
方法调用时:
- 为被调用方法创建新的栈帧
- 将参数值复制到新栈帧的局部变量表
- 将返回地址压入调用者栈帧的操作数栈
- 程序计数器跳转到被调用方法的字节码起始位置
方法返回时:
- 将返回值压入调用者栈帧的操作数栈(如果有返回值)
- 弹出当前栈帧
- 程序计数器跳转到返回地址
1.3.2 栈溢出问题
StackOverflowError:线程请求的栈深度超过JVM允许的最大深度
常见原因:无限递归、递归深度过大
相关JVM参数:-Xss设置每个线程栈大小
1.4 程序计数器
程序计数器是线程私有的小块内存空间,可以看作是当前线程所执行的字节码的行号指示器
它起到一个类似书签的作用,告诉程序现在执行到了哪里,接下来要执行哪里
每个线程都有自己的程序计数器,它是唯一不会发生OOM(OutOfMemoryError)的内存区域
1.5 本地方法栈
本地方法栈为JVM调用native方法服务,为Native方法的执行提供栈空间、为C/C++等本地代码提供函数调用栈、支持JNI调用等等
以上五个部分可以分为两大类别:线程共享区域和线程私有区域
其中堆和方法区是线程共享区域(所有线程共同访问),程序计数器、虚拟机栈、本地方法栈是程序私有区域(每个线程独立拥有)
讲到这里,我们就不得不提到有关内存划分非常重要的一个问题:JVM调优
鉴于JVM调优又是一个比较大的话题,为了保证整篇文章内容的一致性,这里先打个TODO,下一篇再来介绍