学习JVM
1. JVM概念
介于操作系统和Java应用程序之间。成为java的"翻译官,把字节码翻译成各种操作系统能识别的机器码,实现"一次编写,到处运行"。
【.java 源码】--(javac编译)-->【.class字节码】--(JVM解释)-->【机器码】
2. JVM的体系结构
整体流程:加载-->存储-->执行。
【加载.class文件到内存】--->【类加载子系统】--->【运行时数据区】--->【执行引擎】--->【本地方法入口】
- 类加载子系统,负责加载.class文件
- 运行时数据区,属于JVM的内存,PC寄存器,栈,堆,都属于这里。
- 执行引擎, 包括解释器,JIT编译器,GC,是真正的执行代码核心。
3. 类加载器
作用:把磁盘上的.class文件加载到JVM的方法区,并在堆中创建对应的Class对象
| 类加载器 | 实现语言 | 加载范围 |
|---|---|---|
| 启动类加载器(Bootstrap) | C/C++ | JDK 核心类(rt.jar 中的 java.lang/String 等) |
| 扩展类加载器(Extension) | Java | JRE/lib/ext 目录下的扩展类 |
| 应用程序类加载器(App) | Java | 自定义类(classpath 下的代码) |
| 自定义类加载器 | Java | 自定义类(classpath 下的代码) |
4. 双亲委派机制
类加载器的核心规则,先委托父类加载器加载,加载不到的类才到自己加载
- 流程(以加载自定义类com.test.MyClass为例)
App类加载器-->委托Extension-->委托Bootstrap-->Bootstrap找不到(只加载核心类)-->Extension不到-->App自己加载。 - 核心作用
- 安全,防止自定义java.lang.String替换JDK核心类。
- 唯一, 同一个类只会被加载一次,避免重复加载。
5. 沙箱安全机制
JVM的安全围栏,限制java程序的运行范围,防止恶意代码破坏系统核心。引用了一个Domain(域)的概念,JVM会将不同的代码加载到不同的域当中。
- 字节码校验器:检查字节码是否合规(比如禁止修改内存地址)。
- 类加载器+双亲委派:只加载可信类。
- 安全管理器(SecurityManager):控制程序权限(如禁止读写文件,执行系统命令等)
- 存取控制器(access controller) :控制核心API对操作系统的存取权限。
- 安全软件包(security package): java.security之下的包和类,允许用户为自己的应用增加安全特性。包括:
安全提供者; 消息摘要; 数字签名; 加密; 鉴别。
6. Native方法实现本地库底层调用
声明:native方法-->javah生成C头文件-->C/C++实现-->编译成动态库-->Java通过System.loadLibrary()加载。
注意:凡是代有native关键字的方法,说明java的作用范围达不到了,会去调用底层C语言库,即进入本地方法栈。JNI融合不同语言。
eg. private native void start0(); //本地方法常用于硬件驱动,网络系统中很少使用了。
7. PC寄存器(程序技术器)
- 作用:存储当前线程正在执行的JVM指令地址(如果是执行native方法,值为undefined);
- 特点:不会OOM(内存溢出),因为大小固定。
- 类比:像书签,随时可以在线程切换时从书签位置继续执行。
8. 方法区
所有线程共享的内存区,存储类的"元数据"(类名,方法,字段,常量池),静态变量,JIT编译后代码
版本变化:
- JDK1.7及之前:叫永久代,属于堆的一部分,大小有限。
- JDK1.8及之后:移除永久代,改用元空间(Metaspece),直接使用操作系统内存,只受OS内存限制,大幅减少内存溢出概率。
- JVM内部过程:
(方法区包含:static 数据和类,final,Class,常量池)
运行时数据区(Runtime Data Area)---> 方法区Method Area<--->执行引擎-垃圾重灾区--解释器interpreter --JIT编辑器--垃圾回收器
--->java栈 Stack
--->本地方法栈Native Method Stack<--->本地方法接口JNI(Java Native Interface)<---本地方法库。(不存在垃圾回收)
--->堆 Heap(垃圾重灾区)
--->程序计数器 Program Counter 不存在垃圾回收)
9. 栈(虚拟机栈)
线程私有:每个线程创建时分配一个栈,由多个栈帧组成。
- 栈帧: 每个方法调用时创建一个栈帧,存储局部变量,方法返回地址,方法执行完栈帧出栈。
- 栈的大小可以通过 -Xss设置,如-Xss1m.
- 递归过深,会触发栈溢出(StackOverflowError)。
- 无需GC, 栈帧的创建销毁都是自动的。
- 栈就像"弹夹",方法调用时进行"压弹",方法结束完,子弹弹出。容量有限,压太多会溢出。
10. 三种主流JVM
| JVM实现 | 开发商 | 特点 | 应用场景 |
|---|---|---|---|
| HotSpot | Oracle | 混合解释+JIT编译,性能优主流 | 绝大多数Java应用 |
| JRockit | BEA | 无解释器,全JIT编译,适合高并发 | 已被Oracle整合 |
| IBM J9 | IBM | 轻量,跨平台,内存占用少 | IBM WebSphere等 |
命令行执行:java -version,就会显示对应JVM,例如:Java HotSpot(TM) 64-Bit Server VM.
11. 堆
是JVM中最大的内存区域,所有线程共享,用于存储所有new出来的对象,也是GC(垃圾回收)核心区。一个JVM只有一个堆内存。
- 核心参数:
- -Xms: 堆的初始大小,建议和-Xmx一致,避免JVM动态调整;
- -Xmx: 堆最大大小,如-Xms4g -Xmx4g,堆固定4G;
- 特点:会触发OOM,因为对象太多,堆内存不足。(OutOfMemery)
- 比喻:就是JVM的大仓库,所有线程元素都往里放,满了就报错,GC是仓库清理工,清理没用的对象数据。
12. 新生区(新生代)
堆的一部分,专门存储刚创建的对象,分为 3 个区域(默认比例 8:1:1):
- Eden 区(伊甸园):新对象优先分配到这里;
- Survivor 0(S0)、Survivor 1(S1):Eden 满了触发 Minor GC(轻量 GC),存活对象移到 - - S0/S1,来回移动达到年龄阈值(默认 15)后,移到老年代。
13. 永久区(永久代)
⚠️ 仅 JDK 1.7 及之前存在,是方法区的实现,存储类元数据、静态变量、常量池。
- 问题:大小有限(-XX:PermSize/-XX:MaxPermSize),加载大量类时易 OOM;
- JDK 1.8 替代方案:元空间(Metaspace),使用操作系统内存,默认无上限,大幅减少 OOM。
14. 堆内存调优
核心目标: 减少GC频率/耗时, 提升程序性能,核心步骤+参数;
- 监控 GC:用jstat -gc 进程ID、jvisualvm、Arthas 查看 GC 次数 / 耗时;
- 调整核心参数(JDK 1.8):
- -Xms4g -Xmx4g:堆初始 / 最大大小设为 4G(固定大小避免动态调整);
- -Xmn2g:新生代设为 2G(占堆的 1/2,适合大多数场景);
- -XX:SurvivorRatio=8:Eden:S0:S1=8:1:1;
- -XX:+UseG1GC:使用 G1 收集器(适合大堆、低延迟);
- 分析 OOM:添加-XX:+HeapDumpOnOutOfMemoryError,导出堆快照用 MAT 工具分析内存泄漏。
15. GC(垃圾回收)
JVM 自动回收堆中 "无用对象" 的机制,无需程序员手动管理内存(对比 C++ 的new/delete)。
-
垃圾判定:主流用可达性分析(以 GC Roots 为起点,无引用链的对象是垃圾);
-
GC 类型:
- Minor GC:新生代 GC,频率高、耗时短;
- Major GC:老年代 GC,频率低、耗时长(通常伴随 Minor GC);
- Full GC:整堆 GC(新生代 + 老年代 + 元空间),耗时最长,应尽量避免;
-
核心收集器:Serial(串行)、Parallel(并行,JDK8 默认)、G1(JDK9 默认)。
16. JMM(Java内存模型)
⚠️ 不是 JVM 的内存区域(堆 / 栈),而是一套规范,解决多线程的可见性、原子性、有序性问题。
-
核心抽象:
- 主内存:所有线程共享,存储共享变量(对应堆);
- 工作内存:线程私有,存储共享变量副本(对应栈);
-
核心保障:
-
可见性:volatile关键字保证一个线程修改的变量,其他线程立刻可见;
-
原子性:synchronized/Atomic 类保证操作要么全执行,要么全不执行;
-
有序性:volatile/synchronized禁止指令重排,保证代码执行顺序和编写顺序一致。
-