《深入理解Java虚拟机》学习笔记
一、Java虚拟机(JVM)概述
- 定义与功能
- 作为Java程序运行的中间层,将字节码转换为机器码,实现跨平台运行。
- 核心功能:类加载、内存管理、垃圾回收、字节码执行、运行时优化。
- JVM架构组件
- 类加载器子系统:加载、验证、解析类文件。
- 执行引擎:解释器、JIT编译器、垃圾收集器。
- 本地接口:调用本地方法库(如Native方法)。
- 版本与特性演变
- 不同JDK版本(JDK7、JDK8、JDK11等)在内存模型、垃圾收集器上的重大变化(如永久代→元空间,G1成为默认GC)。
二、Java内存区域与内存溢出
- 运行时数据区域(按线程共享/隔离划分)
- 线程共享区域:
- 堆(Heap):
- 存储所有对象实例和数组。
- 分为新生代(Eden区、From/To Survivor区)和老年代。
- 溢出场景:内存泄漏、大对象分配失败。
- 方法区(Method Area):
- 存储类信息(类结构、常量池、静态变量)。
- JDK7前:永久代(PermGen),易因动态加载类导致溢出。
- JDK8+:元空间(Metaspace),使用本地内存,减少永久代问题。
- 运行时常量池:
- 存放字面量(如字符串常量)、符号引用(类/方法名)。
- 堆(Heap):
- 线程隔离区域:
- 程序计数器(PC Register):
- 记录线程当前执行指令地址,线程切换后恢复执行位置。
- 虚拟机栈(Java Stack):
- 每个方法对应一个栈帧,存储局部变量、操作数栈、方法调用信息。
- 溢出场景:递归过深、方法调用栈过深(StackOverflowError)。
- 本地方法栈(Native Method Stack):
- 为Native方法服务,溢出处理类似Java栈。
- 程序计数器(PC Register):
- 直接内存(Direct Memory):
- 通过NIO分配,不受堆限制,但需手动管理,溢出导致OOM。
- 线程共享区域:
- 内存溢出异常(OOM)分析
- 常见原因:
- 堆:对象创建过多且无法回收(内存泄漏)。
- 栈:递归/线程过多导致栈深度超过限制。
- 方法区:动态生成大量类(如动态代理、反射)。
- 直接内存:NIO操作超出系统限制。
- 排查工具:jmap、MAT(Memory Analyzer)分析堆dump,jstack分析线程栈。
- 常见原因:
三、垃圾回收机制
- 垃圾回收目标:识别并回收“不可达对象”(通过可达性分析算法)。
- 可达性分析:
- GC Roots:虚拟机栈引用、静态变量、常量、本地方法栈引用、JNI引用。
- 垃圾回收算法:
- 标记-清除(Mark-Sweep):
- 标记存活对象,清除未标记对象,易产生内存碎片。
- 复制(Copying):
- 将内存分为两块,存活对象复制到另一块,适用于新生代(如Eden→Survivor)。
- 标记-整理(Mark-Compact):
- 标记后移动存活对象到一端,清理剩余空间,减少碎片,常用于老年代。
- 分代收集:
- 新生代:复制算法(高存活率低,快速回收)。
- 老年代:标记-整理或标记-清除(低存活率,需处理碎片)。
- 标记-清除(Mark-Sweep):
- 垃圾收集器:
- Serial/Serial Old:单线程,适合内存小、响应要求低的场景。
- Parallel Scavenge/Parallel Old:多线程,关注吞吐量(后台任务)。
- CMS(Concurrent Mark Sweep):
- 低停顿,但并发阶段CPU消耗高,易产生碎片(需Full GC清理)。
- G1(Garbage First):
- 分区域管理,优先回收高价值区域,兼顾延迟与吞吐量,适合大堆内存。
- ZGC(Z Garbage Collector):
- JDK11引入,低延迟(<10ms),支持TB级堆内存。
- 垃圾回收触发时机:
- Minor GC:新生代满(Eden区分配不足)。
- Major GC/Full GC:老年代满、永久代/元空间满、System.gc()触发。
- 引用类型:
- 强引用:普通引用,不回收。
- 软引用:内存不足时回收(如缓存)。
- 弱引用:下次GC时回收。
- 虚引用:仅用于跟踪回收事件(配合引用队列使用)。
四、类加载机制
- 类加载过程:
- 加载:获取字节码,生成Class对象。
- 验证:字节码格式、元数据、字节码合法性检查。
- 准备:为静态变量分配内存,初始化默认值(如int=0)。
- 解析:将符号引用转为直接引用(如方法/字段地址)。
- 初始化:执行静态代码块,初始化静态变量为实际值。
- 类加载器:
- 双亲委派模型:
- 加载类时先委托父类加载器,防止核心类被篡改(如自定义java.lang.Object)。
- 加载器类型:
- 启动类加载器(Bootstrap):加载核心类库。
- 扩展类加载器(Ext):加载扩展库($JAVA_HOME/lib/ext)。
- 应用类加载器(App):加载应用类路径。
- 自定义加载器:用于热部署、加密类等场景。
- 双亲委派模型:
- 类加载器破坏场景:
- 线程上下文加载器(Thread Context ClassLoader):打破双亲委派,如SPI机制。
- 动态加载(如OSGI、模块化系统)的类加载器自定义策略。
五、字节码执行与即时编译(JIT)
- 字节码执行引擎:
- 解释执行:逐条解释字节码,速度慢。
- 即时编译(JIT):将热点代码(如循环、高频方法)编译为本地机器码,提升性能。
- JIT优化技术:
- 逃逸分析:判断对象是否逃逸到方法外,优化内存分配(如栈上分配)。
- 锁消除:去除不必要的同步代码。
- 标量替换:将对象拆分为基本类型,减少堆分配。
- 分层编译:
- 从解释执行→C1编译(简单优化)→C2编译(高级优化),逐步提升效率。
- 热点探测:
- 基于计数器(方法调用次数、循环次数)触发编译阈值。
六、性能优化与故障排查
- 性能调优工具:
- 监控工具:jstat(GC统计)、jmap(堆快照)、jstack(线程栈)、VisualVM、Arthas。
- 日志分析:GC日志(-XX:+PrintGCDetails)、堆dump(-XX:HeapDumpOnOutOfMemoryError)。
- 调优参数:
- 内存设置:
-Xms
(初始堆)、-Xmx
(最大堆)、-XX:MetaspaceSize
(元空间)。 - GC选择:
-XX:+UseG1GC
、-XX:ParallelGCThreads
。 - 日志与调试:
-XX:+PrintFlagsFinal
(查看参数)、-XX:+HeapDumpOnCtrlBreak
。
- 内存设置:
- 优化策略:
- 减少Full GC:调整新生代/老年代比例,避免大对象直接进入老年代。
- 代码优化:减少临时对象创建、使用对象池、避免频繁的装箱/拆箱。
- 并发优化:选择合适的垃圾收集器(如G1 vs CMS)。
七、Java内存模型与并发处理
- 内存模型(JMM):
- 定义多线程访问共享变量的规则,解决可见性、有序性问题。
- 核心概念:主内存(共享变量存储)、工作内存(线程私有缓存)。
- 同步机制:
synchronized
:保证原子性、可见性,通过锁机制实现。volatile
:禁止指令重排序,确保变量修改立即同步到主内存。
- 锁优化:
- 偏向锁:单线程场景优化,减少锁竞争。
- 轻量级锁:无竞争时自旋等待。
- 重量级锁:锁竞争激烈时升级,阻塞线程。
- 原子性操作:使用
AtomicXXX
类(如AtomicInteger)实现无锁并发。
八、高效并发的实现原理
- JVM内存模型与线程:
- 每个线程有自己的栈和程序计数器,共享堆和方法区。
- 线程安全与可见性:
- 先行发生原则(Happens-Before):定义操作之间的顺序关系。
- 线程池配置:
- 参数调优:核心线程数、最大线程数、队列大小。
- 监控线程状态(如死锁、阻塞)。
九、其他高级特性
- 字节码与Class文件结构:
- Class文件格式(魔数、版本号、常量池、字段表、方法表)。
- 字节码指令集(如aload、invokevirtual)。
- 编译与优化:
- 语法糖:泛型擦除、自动装箱/拆箱、条件编译。
- 即时编译触发条件与优化层级。
- JVM故障排查:
- CPU占用高:使用jstack定位线程状态(如死循环)。
- 内存泄漏:MAT分析对象引用链。
- 线程死锁:jconsole或jstack检测死锁线程。
十、实战建议与最佳实践
- 内存溢出排查流程:
- 捕获OOM异常→生成堆dump→分析大对象/引用链→优化代码或配置。
- 调优实践:
- 压测工具(如JMeter)模拟负载,结合GC日志调整参数。
- 使用JVM参数模板(如微服务场景的G1配置)。
- 代码优化示例:
- 使用
StringBuilder
代替+
拼接字符串。 - 避免在循环中创建对象。
- 使用
总结:
理解JVM底层机制是解决性能问题、优化代码、排查故障的基础。需结合具体场景(如高并发、大数据处理)灵活应用调优策略,并关注JDK新版本特性(如JDK17的垃圾收集器改进)。
参考资料:
- 《深入理解Java虚拟机》(周志明著,第三版)
- Oracle官方JVM文档、OpenJDK源码