
第一部分:类加载机制
1. 类加载的生命周期
类从被加载到JVM内存到卸载,共经历七个阶段。其中加载、验证、准备、解析、初始化 属于类加载过程,使用 和卸载则属于后续的生命周期。
加载
验证
准备
解析
初始化
使用
卸载
| 阶段 | 描述 |
|---|---|
| 加载 | 通过类的全限定名获取二进制字节流,在方法区生成对应的java.lang.Class对象,作为访问入口。 |
| 验证 | 确保字节流符合JVM规范(文件格式、元数据、字节码、符号引用验证),保证安全性。 |
| 准备 | 为类变量(static)分配内存并设置默认初始值(如0、null),但final static常量直接赋初始值。 |
| 解析 | 将常量池中的符号引用替换为直接引用(指向方法区的指针、偏移量等)。 |
| 初始化 | 执行类构造器<clinit>()方法,为静态变量赋予正确的初始值(程序员指定的值)。 |
| 使用 | 创建对象、调用静态方法等。 |
| 卸载 | 当类所有实例被回收、加载它的ClassLoader被回收、且Class对象无引用时,类可被卸载(条件苛刻)。 |
思考:类加载的七个阶段中,"准备"和"初始化"容易混淆------准备阶段只分配内存并设置默认值,初始化阶段才执行赋值代码。理解这个区别有助于排查静态变量初始化顺序问题。
2. 类加载器的分类
JVM提供了三层类加载器体系,外加自定义加载器:
类加载器层次
启动类加载器
Bootstrap ClassLoader
扩展类加载器
Extension ClassLoader
应用程序类加载器
Application ClassLoader
自定义类加载器
Custom ClassLoader
| 类加载器 | 实现 | 加载路径 |
|---|---|---|
| 启动类加载器 | C++实现,JVM的一部分 | JAVA_HOME/lib 目录中的核心类库(如rt.jar) |
| 扩展类加载器 | Java类(sun.misc.Launcher$ExtClassLoader) |
JAVA_HOME/lib/ext 目录 |
| 应用程序类加载器 | Java类(sun.misc.Launcher$AppClassLoader) |
ClassPath 环境变量指定的路径 |
| 自定义类加载器 | 继承ClassLoader,重写findClass() |
自定义来源(如网络、加密文件) |
思考:为什么需要多种类加载器?为了隔离不同类库的版本(如Tomcat为每个应用独立加载类),同时保证核心类库不被篡改。
3. 双亲委派模型的原理、优势及打破方式
原理
除了启动类加载器,每个类加载器都有一个父加载器。当收到类加载请求时,它先委派给父加载器去尝试加载,只有父加载器无法加载时才自己加载。
双亲委派流程
是
是
否
否
是
否
加载请求
当前类加载器
父加载器是否存在?
委派给父加载器
父加载器加载成功?
返回Class
当前加载器自行加载
加载成功?
抛出ClassNotFoundException
优势
- 安全性 :防止核心API被篡改(如自定义的
java.lang.Object永远无法加载,因为启动类加载器会优先加载)。 - 避免重复加载:父加载器加载过的类,子加载器不会再加载,保证类的唯一性。
打破方式
- 重写
loadClass方法 :不按双亲委派顺序,直接自己加载或逆向委派(如Tomcat的WebAppClassLoader优先加载/WEB-INF/classes下的类)。 - 线程上下文类加载器 :JDBC等SPI机制使用
Thread.currentThread().getContextClassLoader()打破双亲委派,让父加载器请求子加载器加载类。
思考:双亲委派是默认的安全机制,但在框架设计中(如热部署、模块隔离)往往需要打破它。打破时需小心类加载冲突和内存泄漏。
4. 类初始化的触发条件(主动引用 vs 被动引用)
主动引用(立即触发初始化)
new、getstatic、putstatic、invokestatic(实例化对象、读写静态变量、调用静态方法)。- 反射调用(
Class.forName())。 - 初始化子类时,如果父类未初始化,先触发父类初始化。
- 启动类(包含
main()方法的类)。 - JDK 7+ 中
MethodHandle解析结果对应的类。
被动引用(不会触发初始化)
- 通过子类引用父类的静态变量(只会触发父类初始化)。
- 定义类数组(如
Demo[] arr = new Demo[10])。 - 引用静态常量(编译时存入常量池,不会触发类初始化)。
java
public class Test {
static class Parent {
static int a = 1;
static { System.out.println("Parent init"); }
}
static class Child extends Parent {
static { System.out.println("Child init"); }
}
public static void main(String[] args) {
System.out.println(Child.a); // 输出:Parent init 1
// Child类并未初始化(被动引用)
}
}
思考:掌握主动/被动引用可以帮助优化启动速度,避免不必要的类加载。例如,使用常量代替静态变量可以减少类初始化开销。
第二部分:性能调优
1. JVM调优的流程与目标
调优目标
- 降低GC频率:减少Minor GC和Full GC次数。
- 减少停顿时间:降低单次GC引起的应用暂停。
- 提高吞吐量:让应用线程占用更多CPU时间。
- 避免OOM:保证系统稳定运行。
调优流程
未达标
达标
监控与分析
确定问题
调整参数
验证效果
固化配置
- 监控:使用工具收集GC日志、堆内存、线程状态。
- 分析:定位瓶颈(如GC频繁、内存泄漏、线程阻塞)。
- 调整:修改JVM参数、优化代码。
- 验证:对比调优前后的指标。
2. 常用JVM调优工具
| 工具 | 作用 |
|---|---|
| jps | 列出当前系统中所有Java进程(类似ps aux |
| jstat | 监控GC、类加载、JIT编译等统计信息(如jstat -gcutil pid) |
| jinfo | 查看和动态修改JVM参数(jinfo -flag) |
| jmap | 生成堆转储快照(heap dump),分析内存泄漏(jmap -dump:format=b,file=heap.hprof pid) |
| jstack | 打印线程堆栈,用于分析死锁、线程阻塞(jstack pid) |
| jconsole | JMX图形化监控,查看内存、线程、CPU等 |
| VisualVM | 多功能监控分析工具,支持插件扩展 |
思考 :这些工具是JVM调优的"听诊器",熟练使用它们能快速定位问题。例如,用
jstat发现Full GC频繁,再用jmap分析堆内存对象,找出内存泄漏源头。
3. 常见调优场景
场景一:堆内存调整
-
参数示例 :
-Xms4g -Xmx4g # 设置堆初始和最大为4G -Xmn2g # 新生代大小2G -XX:SurvivorRatio=8 # Eden与单个Survivor的比例(8:1:1) -XX:MaxMetaspaceSize=256m # 元空间上限 -
适用:根据应用内存需求调整,避免频繁扩容或GC。
场景二:垃圾收集器选择
- 高吞吐量 (如批处理):使用Parallel Scavenge + Parallel Old (
-XX:+UseParallelGC)。 - 低延迟 (如Web服务):使用G1 (
-XX:+UseG1GC,JDK9+默认)或CMS (-XX:+UseConcMarkSweepGC,已废弃但仍有应用)。 - 超大堆(>100G) :使用ZGC (
-XX:+UseZGC)。
场景三:线程池配合
- 线程池大小应与JVM参数协同,如:
- 避免创建过多线程导致栈内存溢出(
-Xss设置合理栈大小)。 - 结合
-XX:ParallelGCThreads设置GC线程数,避免与业务线程争抢CPU。
- 避免创建过多线程导致栈内存溢出(
思考:调优是权衡的艺术。增大堆内存可以减少GC频率,但可能增加单次GC停顿;选择G1可以控制停顿,但吞吐量可能略低于Parallel。没有万能配置,只有最适合当前场景的组合。
总结:JVM类加载与性能调优知识体系
JVM类加载与调优
类加载机制
生命周期
加载
验证
准备
解析
初始化
使用
卸载
类加载器
启动类加载器
扩展类加载器
应用类加载器
自定义类加载器
双亲委派
原理
优势:安全、避免重复
打破方式
初始化触发
主动引用
被动引用
性能调优
目标
降低GC频率
减少停顿
提高吞吐量
避免OOM
流程
监控
分析
调整
验证
常用工具
jps
jstat
jinfo
jmap
jstack
jconsole/VisualVM
调优场景
堆内存调整
GC收集器选择
线程池配合