(一) JDK8 JVM结构详细介绍
一、JVM整体架构
JDK8的JVM采用分层设计,主要分为以下几个部分:
- 类加载子系统(Class Loader Subsystem)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地方法接口(Native Method Interface)
- 垃圾回收器(Garbage Collector)
二、详细结构介绍
1. 类加载子系统(Class Loader Subsystem)
作用:负责将.class文件加载到JVM中,并转换成运行时数据结构。
组成:
- 启动类加载器(Bootstrap ClassLoader)
- 加载JRE核心类库(如rt.jar)
- 由C++实现,是JVM的一部分
- 扩展类加载器(Extension ClassLoader)
- 加载JRE扩展目录(jre/lib/ext)中的类
- 由Java实现
- 应用程序类加载器(Application ClassLoader)
- 加载用户类路径(ClassPath)上的类
- 由Java实现
类加载过程:
- 加载(Loading):查找并加载类的二进制数据
- 连接(Linking):
- 验证(Verification):确保类文件符合JVM规范
- 准备(Preparation):为静态变量分配内存并设置默认值
- 解析(Resolution):将符号引用转换为直接引用
- 初始化(Initialization):执行类构造器()方法
2. 运行时数据区(Runtime Data Area)
作用:JVM运行时用于存储数据的区域,分为线程共享和线程私有两部分。
线程共享区域:
- 方法区(Method Area)
- 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 在JDK8中,方法区被**元空间(Metaspace)**取代
- 堆(Heap)
- 所有对象实例和数组都在堆上分配内存
- 是垃圾收集器管理的主要区域
- 分为新生代(Young Generation)和老年代(Old Generation)
- 新生代又分为Eden区、Survivor0区、Survivor1区
线程私有区域:
- 程序计数器(Program Counter Register)
- 当前线程所执行的字节码的行号指示器
- 线程私有,唯一不会发生OutOfMemoryError的区域
- Java虚拟机栈(Java Virtual Machine Stacks)
- 存储栈帧(Stack Frame),每个方法执行时都会创建一个栈帧
- 包含局部变量表、操作数栈、动态链接、方法出口等信息
- 可能抛出StackOverflowError和OutOfMemoryError
- 本地方法栈(Native Method Stack)
- 为Native方法服务
- 与虚拟机栈类似,但服务于Native方法
3. 执行引擎(Execution Engine)
作用:执行字节码,将字节码解释或编译后执行。
组成:
- 解释器(Interpreter)
- 逐条解释执行字节码
- 启动速度快,执行速度慢
- 即时编译器(JIT Compiler)
- 将热点代码编译成机器码执行
- 执行速度快,启动速度慢
- 包括:
- C1编译器(Client模式)
- C2编译器(Server模式)
- Graal编译器(JDK9+可选)
- 垃圾回收器(Garbage Collector)
- 负责回收堆内存中的无用对象
- JDK8默认使用:
- Serial收集器(单线程)
- Parallel Scavenge收集器(多线程,吞吐量优先)
- CMS收集器(并发标记清除,低延迟)
- Serial Old收集器(老年代单线程)
- Parallel Old收集器(老年代多线程)
4. 本地方法接口(Native Method Interface)
作用:与本地方法库交互的接口,允许Java代码调用本地(Native)方法(如C/C++代码)。
5. 垃圾回收器(Garbage Collector)
作用:自动管理内存,回收不再使用的对象。
JDK8常用垃圾回收器:
- Serial收集器:单线程,适合客户端应用
- Parallel Scavenge收集器:多线程,吞吐量优先
- CMS收集器:并发标记清除,低延迟
- G1收集器:JDK9默认,但JDK8也可使用,面向服务端应用
三、JDK8关键变化
-
元空间(Metaspace)取代永久代(PermGen)
- 方法区实现从永久代改为元空间
- 元空间使用本地内存(Native Memory),默认无大小限制
- 解决了永久代的内存溢出问题
-
字符串常量池位置变化
- 字符串常量池从永久代移到堆中
-
垃圾回收器优化
- G1收集器在JDK8中可用(但默认不是)
- CMS收集器仍然是低延迟场景的首选
四、JVM内存模型图示
+---------------------+
| 类加载子系统 |
+---------------------+
|
v
+---------------------+
| 运行时数据区 |
| +----------------+ |
| | 方法区 | |
| +----------------+ |
| +----------------+ |
| | 堆 | |
| | +-----------+ | |
| | | 新生代 | | |
| | | Eden | | |
| | | Survivor0 | | |
| | | Survivor1 | | |
| | +-----------+ | |
| | +-----------+ | |
| | | 老年代 | | |
| | +-----------+ | |
| +----------------+ |
| +----------------+ |
| | 程序计数器 | |
| +----------------+ |
| +----------------+ |
| | Java栈 | |
| +----------------+ |
| +----------------+ |
| | 本地方法栈 | |
| +----------------+ |
+---------------------+
|
v
+---------------------+
| 执行引擎 |
+---------------------+
|
v
+---------------------+
| 本地方法接口 |
+---------------------+
五、总结
JDK8的JVM结构在保持JVM基本架构不变的基础上,主要变化在于:
- 方法区实现从永久代改为元空间
- 字符串常量池位置调整到堆中
- 垃圾回收器优化(G1可用但非默认)
这些变化提高了JVM的内存管理效率和稳定性,特别是解决了永久代常见的内存溢出问题。
(二) JDK8元空间相比永久代的具体优势
1. 内存管理方式
- 永久代:使用JVM堆内存,大小固定(通过
-XX:PermSize
和-XX:MaxPermSize
参数设置) - 元空间:使用本地内存(Native Memory),默认无大小限制(除非设置
-XX:MaxMetaspaceSize
)
2. 解决内存溢出问题
- 永久代:容易因加载过多类或常量导致
OutOfMemoryError: PermGen space
错误 - 元空间:由于使用本地内存且默认无限制,基本解决了永久代的内存溢出问题
3. 动态扩展能力
- 永久代:大小固定,无法动态调整(除非重启JVM)
- 元空间:可以根据需要动态扩展,更适应现代应用的需求
4. 内存使用效率
- 永久代:内存分配和回收效率较低
- 元空间:使用本地内存管理机制,内存分配和回收效率更高
5. 类加载器卸载
- 永久代:类加载器卸载时,相关类的元数据可能无法完全释放
- 元空间:类加载器卸载时,相关类的元数据可以更彻底地释放
6. 参数配置简化
- 永久代:需要配置多个参数(
-XX:PermSize
、-XX:MaxPermSize
等) - 元空间:只需配置
-XX:MaxMetaspaceSize
(可选),参数更简单
7. 内存泄漏风险降低
- 永久代:更容易因类加载器泄漏导致内存泄漏
- 元空间:由于使用本地内存管理,类加载器泄漏导致的内存泄漏风险更低
8. 与JVM整体内存管理集成更好
- 永久代:作为JVM堆的一部分,与堆内存管理相对独立
- 元空间:作为本地内存的一部分,与JVM整体内存管理集成更好
9. 适应现代应用需求
- 永久代:设计较早,难以适应现代应用大量动态加载类的需求
- 元空间:设计更现代,能更好地适应动态类加载和卸载的需求
10. 性能优化空间更大
- 永久代:内存管理机制相对简单,优化空间有限
- 元空间:使用本地内存管理机制,有更大的性能优化空间
这些优势使得JDK8的元空间相比永久代在内存管理、稳定性和性能方面都有显著提升,特别是解决了永久代常见的内存溢出问题。
(三) JDK8中元空间和堆内存的区别
1. 存储内容不同
- 堆内存:存储Java对象实例和数组,是所有线程共享的内存区域
- 元空间:存储类的元数据信息(如类结构、方法代码、常量池等),也是线程共享的区域
2. 内存管理方式不同
- 堆内存:由JVM的垃圾回收器(GC)管理,会定期进行垃圾回收
- 元空间:使用本地内存(Native Memory),默认情况下不进行垃圾回收(除非类加载器被卸载)
3. 大小限制不同
- 堆内存:大小可通过
-Xms
(初始堆大小)和-Xmx
(最大堆大小)参数设置 - 元空间:默认无大小限制(使用本地内存),但可通过
-XX:MaxMetaspaceSize
设置最大值
4. 内存分配位置不同
- 堆内存:位于JVM进程的堆区域
- 元空间:位于JVM进程的本地内存(Native Memory)区域
5. 内存回收机制不同
- 堆内存:有完整的垃圾回收机制(如新生代、老年代GC)
- 元空间:
- 默认情况下不进行垃圾回收
- 当类加载器被卸载时,相关元数据会被回收
- 可通过
-XX:MetaspaceGCInterval
等参数控制元空间GC行为(但通常不需要)
6. 内存溢出错误不同
- 堆内存:内存不足时会抛出
OutOfMemoryError: Java heap space
- 元空间:内存不足时会抛出
OutOfMemoryError: Metaspace
7. 参数配置不同
- 堆内存:使用
-Xms
、-Xmx
等参数配置 - 元空间:使用
-XX:MetaspaceSize
(初始大小)、-XX:MaxMetaspaceSize
(最大大小)等参数配置
8. 内存使用特点不同
- 堆内存:内存使用量随应用程序运行动态变化
- 元空间:内存使用量主要取决于加载的类数量和大小,相对稳定(除非动态加载大量类)
9. 内存泄漏风险不同
- 堆内存:容易因对象引用未释放导致内存泄漏
- 元空间:内存泄漏风险较低,主要可能因类加载器泄漏导致
10. JVM实现方式不同
- 堆内存:是JVM规范明确要求的内存区域
- 元空间:是JDK8对方法区(永久代)的实现替代,属于JVM实现细节
总结:堆内存用于存储对象实例,由GC管理;元空间用于存储类元数据,使用本地内存,默认不GC,主要解决永久代的内存溢出问题。
(四) JDK8中元空间内存泄漏的常见原因
1. 类加载器泄漏
- 最常见原因:类加载器未被正确卸载,导致其加载的所有类元数据无法释放
- 典型场景:
- Web应用服务器中,Servlet容器未正确销毁Web应用对应的类加载器
- 动态生成类的框架(如Groovy、JRuby)中,动态生成的类加载器未被释放
- 自定义类加载器未实现正确的卸载逻辑
2. 动态生成大量类
- 原因:应用程序动态生成大量类(如使用ASM、CGLIB、Javassist等字节码操作库)
- 典型场景:
- AOP框架(如Spring AOP)动态生成代理类
- ORM框架(如Hibernate)动态生成实体类
- 动态语言运行时(如Groovy、JRuby)动态生成类
3. 字符串常量池泄漏
- 原因:大量字符串被放入字符串常量池且未被回收
- 典型场景:
- 大量使用
String.intern()
方法且未释放 - 应用程序缓存大量字符串常量
- 大量使用
4. 元数据缓存未清理
- 原因:应用程序缓存了大量的类元数据信息
- 典型场景:
- 反射频繁使用的类信息被缓存
- 动态代理生成的类信息被长期缓存
- 框架或库缓存了大量的类元数据信息
5. 类加载器层次结构问题
- 原因:复杂的类加载器层次结构导致类加载器无法被卸载
- 典型场景:
- 自定义类加载器设计不当,形成循环引用
- 类加载器之间存在强引用关系,无法被垃圾回收
6. 框架或库的实现问题
- 原因:某些框架或库存在类加载器管理问题
- 典型场景:
- 某些第三方库未正确释放类加载器
- 框架在重新加载配置或模块时未正确清理旧的类加载器
7. 热部署或热加载实现不当
- 原因:热部署或热加载机制未正确清理旧的类加载器和类元数据
- 典型场景:
- 开发环境中的热部署工具未正确清理旧的类加载器
- 生产环境中的热加载机制实现不当
8. 内存泄漏检测工具不足
- 原因:缺乏有效的元空间内存泄漏检测工具
- 典型场景:
- 开发人员未使用专门的工具检测元空间内存泄漏
- 内存泄漏问题在运行时才被发现
9. JVM参数配置不当
- 原因:元空间大小设置不合理
- 典型场景:
-XX:MaxMetaspaceSize
设置过小,导致频繁触发元空间GC- 未设置
-XX:MaxMetaspaceSize
,导致元空间无限增长
10. 应用程序设计问题
- 原因:应用程序设计不当导致类加载器无法释放
- 典型场景:
- 应用程序长期持有类加载器引用
- 应用程序未正确实现类加载器的生命周期管理
总结:元空间内存泄漏的主要原因是类加载器未被正确卸载,导致其加载的类元数据无法释放。常见场景包括Web应用服务器、动态生成类的框架、字符串常量池滥用等。预防措施包括正确实现类加载器生命周期管理、合理使用动态类生成、监控元空间使用情况等。
(五) JDK8垃圾回收器对比
1. Serial收集器(单线程)
- 特点:
- 单线程收集,简单高效
- 采用复制算法
- 进行垃圾收集时,必须暂停所有工作线程(Stop The World)
- 适用场景:
- 客户端应用
- 单CPU环境
- 小型应用
- 参数:
-XX:+UseSerialGC
2. Parallel Scavenge收集器(多线程,吞吐量优先)
- 特点:
- 多线程收集,使用复制算法
- 关注吞吐量(Throughput),即CPU用于运行用户代码的时间与CPU总消耗时间的比值
- 自适应调节策略(可自动调整新生代大小、晋升老年代对象年龄等参数)
- 适用场景:
- 后台运算为主的应用
- 多CPU环境
- 对吞吐量要求较高的应用
- 参数:
-XX:+UseParallelGC
(新生代)、-XX:+UseParallelOldGC
(老年代)
3. CMS收集器(并发标记清除,低延迟)
- 特点:
- 以获取最短回收停顿时间为目标
- 并发收集、低停顿
- 采用"标记-清除"算法
- 分为四个阶段:初始标记、并发标记、重新标记、并发清除
- 会产生内存碎片
- 适用场景:
- 互联网站或B/S系统服务端
- 对响应时间有较高要求的应用
- 参数:
-XX:+UseConcMarkSweepGC
- 缺点:
- 并发阶段会占用CPU资源
- 无法处理浮动垃圾(Floating Garbage)
- 会产生内存碎片
4. Serial Old收集器(单线程,老年代)
- 特点:
- 单线程收集,使用标记-整理算法
- 是Serial收集器的老年代版本
- 适用场景:
- 客户端应用
- 与Serial收集器搭配使用
- 参数:
-XX:+UseSerialGC
(自动使用)
5. Parallel Old收集器(多线程,老年代)
- 特点:
- 多线程收集,使用标记-整理算法
- 是Parallel Scavenge收集器的老年代版本
- 关注吞吐量
- 适用场景:
- 后台运算为主的应用
- 多CPU环境
- 与Parallel Scavenge收集器搭配使用
- 参数:
-XX:+UseParallelOldGC
6. G1收集器(面向服务端应用)
- 特点:
- 并行与并发:使用多个CPU来缩短Stop-The-World停顿时间
- 分代收集:仍然保留分代概念
- 空间整合:基于标记-整理算法,不会产生内存碎片
- 可预测的停顿:可以指定最大停顿时间(
-XX:MaxGCPauseMillis
) - 将堆划分为多个大小相等的Region
- 优先回收价值最大的Region
- 适用场景:
- 服务端应用
- 大内存应用
- 对停顿时间有要求的应用
- 参数:
-XX:+UseG1GC
- 优点:
- 并行与并发
- 分代收集
- 空间整合
- 可预测的停顿
- 缺点:
- 实现复杂
- 内存占用相对较高
对比总结
收集器 | 线程数 | 算法 | 目标 | 适用场景 | 是否产生碎片 | 备注 |
---|---|---|---|---|---|---|
Serial | 单线程 | 复制 | 吞吐量 | 客户端 | 是 | 简单高效 |
Parallel Scavenge | 多线程 | 复制 | 吞吐量 | 后台运算 | 是 | 自适应调节 |
CMS | 多线程 | 标记-清除 | 低延迟 | 互联网服务 | 是 | 会产生浮动垃圾 |
Serial Old | 单线程 | 标记-整理 | 吞吐量 | 客户端 | 否 | 与Serial搭配 |
Parallel Old | 多线程 | 标记-整理 | 吞吐量 | 后台运算 | 否 | 与Parallel搭配 |
G1 | 多线程 | 标记-整理 | 低延迟+可预测 | 服务端 | 否 | 面向大内存 |
选择建议:
- 客户端应用:Serial或Parallel Scavenge
- 后台运算应用:Parallel Scavenge + Parallel Old
- 互联网服务:CMS(JDK8)或G1(JDK9+推荐)
- JDK8中G1可用但非默认,CMS是低延迟场景的首选