JVM - 虚拟机
目录
1. JVM组成原理
1.1 程序计数器
- 每个线程都私有一份PC
- 内部保存字节码的行号, 用于记录
正在执行的字节码指令的地址
1.2 堆 —–>实例/数组
- 在线程共享区, 主要用来存储对象实例、数组等
- 分为年轻代、老年代
- 年轻代分为Eden、S0、S1 (8:1:1)
### java 7/8的变化
java7中堆的方法区/永久代(存储类信息)
被移动到本地内存并且改名为元空间, 就是防止后续类太多导致OOM
1.3 虚拟机栈 —–>运行时内存
每个线程运行时所需要的内存,称为虚拟机栈每个栈由多个栈帧 (frame) 组成,
对应着每次方法调用时, 所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 一个栈帧一般为1024K(1M)
- 方法内的局部变量是线程安全的, 每个线程有一个局部变量表
线程安全?
没暴露出来 -> 安全
1 | public static void method(){ |
作为参数(传递or返回) -> 不安全
1 | public static void method(String str){ //在传递过程中被线程共享, 可能修改 |
栈内存溢出
- 栈帧过多导致内存溢出 (递归调用不当)
- 栈帧过大导致内存溢出
垃圾回收
垃圾回收是否涉及栈内存?
垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放, 不用GC回收
栈内存越大越好?
内存一定,栈内存分配越大,栈数就越少;但单个栈内可容纳的栈帧就越多
堆栈的区别
1.4 方法区 ——>类/常量
- 线程共享, 大小默认无上限
- 存类信息+运行时常量池
- jvm启动时创建, 关闭时释放
- 如果方法区域中的内存无法满足分配请求,则会抛出 OutOfMemoryError: Metaspace
常量池
- 可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
1.5 直接内存 ——>I/O数据
- 堆外内存, 虚拟机的系统内存
- 由 Java 通过 NIO 等方式申请使用,不属于 JVM 规范中的运行时数据区。
- 分配回收成本较高, 读写性能好, 不受JVM内存回收管理
- 直接内存本质上就是一块由Java 向操作系统申请的本地内存
ByteBuffer这类堆外缓冲区、I/O 传输数据
常规IO –> 需要拷贝
NIO –> 直接访问
2. 类加载器
2.1 what & how
- 找类 : 去磁盘、classpath、jar 包里找
.class - 读字节码 : 把二进制字节流读进来
- 把它交给 JVM, 最终变成 JVM 能识别的
Class<?>
2.1.1 双亲委派机制
加载一个类时, JVM会先层层委托上一级类加载器先加载, 如果上级不能加载再往下传递
好处
- 父类加载后则无需重复加载, 避免类被重复加载
- 保证类库的API不会被修改
2.2 类装载流程
加载:查找和导入 class 文件。通过类的全限定名找到对应字节流(来源可以是磁盘、jar 包、网络、动态生成),解析成方法区的运行时数据结构,并在堆上生成对应的 java.lang.Class 对象作为访问入口。类加载器遵循双亲委派机制执行此过程。
验证:保证加载类的准确性。
分四个子阶段依次进行:
文件格式验证(魔数0xCAFEBABE、版本号)、元数据验证(继承关系合法性)、
字节码验证(数据流与控制流合法性,最复杂)、符号引用验证(引用目标存在且有访问权限)。
任一环节不通过抛VerifyError。
准备:为类变量(static 变量) , 分配内存并设置初始值。
注意这里赋的是零值(
int为0,引用类型为null),不是你写的初始值。
例外:static final编译期常量直接在此阶段赋真实值。真正的赋值逻辑要到初始化阶段的<clinit>里才执行
解析:把类中的符号引用转换为直接引用。
符号引用是常量池里的一段字符串描述(如
"com/example/User"),直接引用是真实的内存地址或偏移量。解析可以在初始化之后懒执行(动态解析),支撑了 Java 的多态和动态绑定。
初始化:对类的静态变量、静态代码块执行初始化操作。
JVM 执行编译器自动生成的
<clinit>()方法,该方法将所有static变量赋值语句和static {}代码块按源码顺序合并执行。父类的<clinit>先于子类执行,JVM 加锁保证有且仅执行一次,天然线程安全。只有”主动使用”(new、访问 static 字段/方法、反射、子类触发、JVM 启动主类)才触发此阶段。
使用:JVM 开始从入口方法开始执行用户的程序代码。类已完全就绪,对象可以被正常实例化(此时执行 <init>() 即构造方法)、方法可以被调用、字段可以被访问。JIT 编译器会在此阶段对热点代码进行即时编译优化。
卸载:当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象。
触发条件非常苛刻:该类的所有实例都已被 GC 回收、加载该类的
ClassLoader已被回收、该类的Class对象没有任何地方被引用。Bootstrap 加载的核心类(String、Object等)永远不会被卸载,只有用户自定义ClassLoader加载的类才有可能被卸载。
3. 垃圾回收
垃圾: 某个对象没有任何引用指向它, 说明它没用了, 会被回收
3.1 回收时
引用计数法
添加一个字段ref, 统计指向它的引用, 一旦ref为0, 就会被回收
循环引用
a.next=b;
b.next=a;
这样即使没有别的引用指向a/b, 二者互相引用, 也会导致无法被回收, 从而引起OOM
可达性分析算法
GC Root -> 局部变量/static/final static/native
凡是从GC Root出发能够到达的对象都是存活的对象, 其他将会被回收
3.2 垃圾回收算法
新生代对象死亡率极高,用复制算法一次 Minor GC 就解决;
老年代对象相对稳定,用整理算法保证大对象有连续空间。
标记-清除
标记-复制 (teen)
标记-整理 (old)
3.3 分代垃圾回收
把堆按对象生命周期分区分为年轻代+老年代
- 将年轻代分为Eden+S0+S1
- 老年代 : 年轻代 为 2 : 1
Minor GC
- 发生在年轻代
- STW时间更短
- 标记Eden+From, 复制到To
Mixed GC
- 发送在年轻代+部分老年代
- G1持有
Full GC
- 发生在整个堆
- STW时间更长, 应尽量避免
3.4 垃圾回收器
按age分
按type分
CMS
并发, 标记-清除老年代
G1
分Region
Garbage First
短STW
Mixed GC
并发标记![]()
1. 初始标记 Initial Mark
- 短暂停顿 STW
- 先把从 GC Roots 直接能摸到的活对象记下来
2. 并发标记 Concurrent Mark
- 和用户线程一起跑
- 把整个堆里活对象关系大致摸清楚
3. 最终标记 Remark
- 短暂停顿 STW
- 修正并发阶段漏掉的那点引用变化
4. 筛选回收 Evacuation / Cleanup
- 挑出回收收益高的 Region
- 把存活对象复制到新 Region
- 老 Region 直接腾空回收
四大引用
强引用 (有则GC永不回收)
static / final static / local / native
软引用 (看内存)
弱引用 (GC就回收)
虚引用 (get不到, 只通知)
JVM实践
调优的本质是在这几个维度做取舍:
- 内存大小(给多少堆)
- GC 收集器选择(选什么算法)
- 停顿时间目标(能忍多久 STW)
- 线程栈大小(多少线程能跑)。
调优参数?
参数维度
参数列表
调优工具
内存泄漏
CPU


