JVM是什么#
jvm是把Java代码转换为本地机器码的一个编译器
有哪些编译器#
- 前端编译器:我们常用的javac工具,把Java代码编译成字节码
- JIT编译器:把字节码编译成机器码
- AOT编译器:把Java代码直接编译成机器码
编译质量上:JIT > AOT > 前端 ; 编译速度上:前端 > AOT > JIT
这三个编译器互相协作,使jvm 的编译质量和速度达到最优
JVM内存区域#

程序计数器#
Java线程在执行字节码时候的 行号指示器,由于每个线程都有自己的指示器,所以是线程私有的,生命周期与线程的生命周期一致
虚拟机栈#
每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧 (Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。
虚拟机栈的操作只有两个,入栈和出栈。当调用一个新的方法时,就构建一个栈帧压入到栈中,而一个方法执行结束,就会有一个栈帧出栈,遵循 先进后出或者后进先出 的原则。
本地方法栈#
为本地方法执行时候提供的内存空间
堆#
Java堆是被所有线程共享的一块内存区域。“几乎”所有的对象实例都在这里分配内存。也是垃圾回收发生的地方(但不是唯一一个垃圾回收的地方,方法区也可以)
堆的大小可以 通过 -Xmx 和 -Xms 控制,如果堆中没有内存完成实例分配,并且堆无法再扩展时,就会抛出 OutOfMemoryError 异常。
方法区#
用来存放被虚拟机加载的类的信息、常量、静态变量
方法区的大小和堆空间一样,可以选择固定大小也可选择可扩展,方法区的大小决定了系统可以放多少个类,如果系统类太多,导致方法区溢出,虚拟机同样会抛出内存溢出OutOfMemoryError错误。JVM 关闭后方法区即被释放。
方法区的垃圾回收#
回收的内容:方法区中废弃的常量和不再使用的类型
- 常量回收条件:常量的回收条件比较简单,只要常量池中的常量没有被引用就可以回收
- 类型回收条件:这个条件相对来说比较苛刻,需要满足以下三个条件才能回收:
- 该类所有的实例都被回收
- 加载该类的类加载器也被回收
- 无法在任何地方通过反射调用该类
垃圾回收#
判断垃圾的方式#
引用计数法#
只要对象被引用,对象头中的计数器就 + 1,那些计数器 为0 的就被当成垃圾。
致命缺陷:循环引用 三个对象互相引用技术器都是1 但是没有被其他对象引用
可达性分析法#
从GC Root 出发,所有可达的对象都是存活的对象,那些不可达的对象都被当作垃圾回收
GC Root包括以下内容:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中常量引用的对象
- 方法区中静态属性引用的对象
- 被同步锁持有的对象
- Java虚拟机内部的引用
并发情况下的可达性分析法#
三色标记法#
我们在判断对象的可达性的时候使用的是三色标记法。即:黑色、白色、灰色
- 黑色:被扫描过后判定存活的对象,黑色的代表所有对象引用都被扫描过
- 灰色:对象被扫描过,但是对象上引用至少还有一个对象是没有被扫描过的
- 白色:尚未被扫描过,扫描开始阶段虽然都是白色,但是结束后仍然是白色的对象,就被判断是垃圾
并发情况下导致的对象消失的问题#
并发情况下一下两种情况同时满足会导致对象消失,情况如下:
- 并发标记的时候,新增了一条或者多条黑色对象到白色对象的引用
- 并发标记的时候,删除了所有灰色到白色对象的直接或间接引用

情况1:
扫描过程中, 还未扫描的 对象A 的灰色引用被移除了,同时 对象A 又被黑色引用了。由于黑色对象是已经被扫描的了,他引用的对象是不会再扫描,而 对象A 的灰色引用被移除后,往下扫描的时候是不会扫描到对象A,这就导致对象A被回收了
情况2
黑色对象引用了 A,A引用了B。如果黑色对象切断了对A 的引用,但是引用了B,这就导致AB都会被回收
解决方法#
既然两者需要同时满足才会导致对象消失,那么我就破坏其中一个条件即可
- 增量更新:当新增了
黑色对象到白色对象的引用的时候,就把黑色的对象变成灰色,这样就会重新扫描灰色对象,就不会遗漏掉新增的引用对象了。 - 原始快照:无论扫描过程中对象怎么发生变化,都按照开始扫描的时候的对象图快照来进行扫描
垃圾回收算法#
标记清除#
- 方式:把标记为垃圾的对象都清除(把要清除的对象地址放入空闲列表中,如果下次来对象的时候空间满足的情况下,直接放进去)

- 缺点:很明显的缺点就是对象不连续,导致内存碎片过多
- 解决方法:标记整理法
标记整理法#
- 方式:把存活的对象整理到到内存的一端,按照顺序排放,然后清除所有边界外的对象

- 缺点:涉及到对象的移动,效率差
- 解决方法:复制法
复制法#
- 方式:内存空间一分为二,把使用中的空间的存活的对象都移动到空闲的一侧,删除所有正在使用的内存块的对象,然后交换两个内存的位置,完成垃圾回收

- 缺点:占用双倍的空间
分代收集算法#

- 当前虚拟机垃圾收集都是采用分代收集算法
- 根据对象的存活周期将对象分为新生代和老年代
- 新创建的对象通常分配在伊甸园(Eden)。当 Eden 空间不足时,触发 Minor GC(新生代 GC),此过程会 Stop-The-World。GC 会将 Eden 和当前 Survivor From 区中存活的对象复制到 Survivor To 区,同时这些对象的年龄加 1。默认情况下,对象年龄达到 15 后会被晋升到老年代;但如果 Survivor 空间不足或满足动态年龄判定条件,也可能提前晋升。
- 如果对象内存大于伊甸园的话,就直接放到老年代
垃圾收集器#

serial收集器#
Serial [sɪriəl] 连续的,串行
- 新生代单线程收集器,会触发STW
- 复制算法
Serial Old 收集器#
- 显而易见他是 serial 的老年代版本,也是单线程收集器
- 采用 标记整理算法
ParNew 收集器#
- serial 的多线程并行版本
- 采用 复制算法
parallel scavenge 收集器#
- 新生代收集器 注重吞吐量(用户运行代码的时间 / 用户运行代码的时间+垃圾回收的时间)也就是说垃圾回收时间越长,吞吐量就越小。
- 支持多线程
parallel old 收集器#
- parallel scavenge 的老年代版本 ,标记整理算法
- 支持多线程
CMS 垃圾收集器#
CMS 是以停顿时间短为目标的收集器,基于标记清楚算法实现的
整个回收过程分为四个步骤:
- 初始标记:标记与GC Root 直接关联的对象,会触发 STW
- 并发标记:进行可达性分析,标记所有可达的对象,耗时间,是并发执行的
- 重新标记:标记那些并发标记过程中,发生变化的对象(这是防止并发导致对象丢失),为了准确的标记到可达的对象,这个阶段是会触发 STW
- 并发清除:清除标记的垃圾
优点:低停顿,并发收集
缺点也很明显:
- 标记清除算法的缺点,会产生大量的浮动垃圾和空间碎片
- 并且为了追求低停顿,导致垃圾回收时间长,吞吐量变小了
Garbage First 收集器#

- 该收集器把Java堆分成大小相等的独立区域(region),这些区域可以根据需要扮演新生代老年代空间。
- region中的 humongous 用来存放大的对象,只要是超过了region 的一半就认为是大对象
回收过程#
- 初始标记:标记GC Root 直接关联的对象,并修改tams指针的位置(TAMS标志着Region的空白区域,新对象要在TAMS标记的区域中分配内存)方便后续的用户线程分配对象。短时间停顿
- 并发标记:可达性分析,这里采用的是快照的方式
- 最终标记:处理并发标记遗留的对象
- 筛选回收:负责更新region 的统计数据,对region的回收成本和价值进行排序,按照用户期望进行回收,可以选择多个region作为一个回收集,回收的时候把部分region存活的对象复制到空的region,然后清除旧的region。涉及到对象的移动,要暂停用户线程
如何解决Region跨时代引用问题#
G1在Region中维护了一个自己的记忆集,本质是一个哈希表,一个双向的卡表,记录了别人指向我的和我指向别人的。
对象#
对象的创建#
- 虚拟机遇到new指令后,先去常量池检查有没有该类的符号引用,并检查该类有没有被类加载器初始化,如果没有先执行类加载过程
- 通过类加载之后,给对象分配内存,如果内存是规整的就通过
指针碰撞的方式来分配内存,如果不是连续的话,虚拟机需要维护一个空闲列表来记录每个内存的大小 - 分配内存完毕后,除了对象头外,所有的都初始化 0 值
- 对对象进行一系列的设置,把对象的元数据信息、哈希码、GC分代年龄存放在对象头
- 执行init方法,对象创建完毕
对象的内存布局#
对象在堆内存中的存储布局可以分为三个部分:对象头、实例数据、对其填充
- 对象头:
- mark word:哈希码、锁标志信息、GC 分代年龄等
- 类型指针:对象指向他类型元数据的指针,Java通过这个指针判断他是哪个类的实例
- 实例数据:存储的真正的有效信息,我们定义的各种字段内容,不论是父类还是子类定义的都有记录
- 对其填充:对象起始地址是8 字节的整数倍,占位符
4.3 对象的访问方式#
主流的访问方式主要有使用句柄和直接指针两种
类加载机制#
什么情况需要初始化类#
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始 化,则需要先触发其初始化阶段。
- 使用new关键字实例化对象的时候。 ·
- 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外) 的时候。
- ·调用一个类型的静态方法的时候。
- 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需 要先触发其初始化。
- 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先 初始化这个主类。
- 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解 析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句 柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
- 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有 这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
类加载阶段#

- 加载:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入 口。
- 验证
- 文件格式验证:
- 是否以魔数0xCAFEBABE开头、
- 主次版本号的是否在虚拟机接受范围之内
- 元数据验证:
- 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)、
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- 字节码验证
- 符号引用验证:符号引用中的类、字段、方法的可访问性(private、protected、public)是否可被当 前类访问。
- 文件格式验证:
- 准备:准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初 始值的阶段,
- 解析:Java虚拟机将常量池内的符号引用替换为直接引用的过程
- 初始化:执行
<clinit>()方法的过程。之前赋的零值的变量进行赋值,
类加载器#
-
BootstrapClassLoader(启动类加载器):c++`编写,加载java核心库
-
ExtClassLoader (标准扩展类加载器):java`编写,加载扩展库
-
AppClassLoader(系统类加载器)Java编写,加载程序所在的目录
-
CustomClassLoader(用户自定义类加载器)
双亲委派模型#
防止重复加载某一个类+ 避免核心类被篡改
- 打破双亲委派机制:
自定义ClassLoader,重写loadClass方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)
Tomcat 为实现隔离性,每个 webappClassLoader 加载自己目录下的 class 文件,而不会传递给父类加载器,打破了双亲委派机制。
常见Jvm参数#
堆内存相关#
| 参数 | 说明 |
|---|---|
-Xms<size> | 设置 JVM 初始堆内存大小(如 -Xms512m 表示初始 512MB) |
-Xmx<size> | 设置 JVM 最大堆内存大小(如 -Xmx4g 表示最大 4GB) |
-XX:NewRatio=<n> | 设置老年代与新生代的比例(如 -XX:NewRatio=2 表示老年代<新生代>新生代> = 2<1>1>) |
-XX:NewSize=<size> / -XX:MaxNewSize=<size> | 手动设置新生代初始/最大大小(通常不建议与 -Xmn 同时使用) |
-Xmn<size> | 直接设置新生代大小(如 -Xmn1g) |
建议:生产环境中通常将
-Xms和-Xmx设为相同值,避免运行时动态扩容带来的性能抖动。
垃圾回收器(GC)相关#
| 参数 | 说明 |
|---|---|
-XX:+UseSerialGC | 使用串行 GC(适合单核 CPU 或小内存应用) |
-XX:+UseParallelGC | 使用并行 GC(吞吐量优先,默认在 JDK8 及以前) |
-XX:+UseG1GC | 使用 G1 垃圾回收器(低延迟,推荐 JDK8u40+、JDK9+ 默认) |
-XX:+UseZGC | 使用 ZGC(超低延迟,JDK11+ 实验性,JDK15+ 正式支持) |
-XX:+UseShenandoahGC | 使用 Shenandoah GC(低停顿,需额外开启,OpenJDK 特有) |
GC 日志与监控#
| 参数 | 说明 |
|---|---|
-Xloggc:<file> | 指定 GC 日志输出文件(JDK8 及以前) |
-XX:+PrintGC | 打印基本 GC 信息 |
-XX:+PrintGCDetails | 打印详细 GC 信息 |
-XX:+PrintGCTimeStamps | 在 GC 日志中添加时间戳 |
-Xlog:gc*:file=<file>:time | JDK9+ 的统一日志格式(替代旧参数) |
元空间(Metaspace)相关(JDK8+)#
| 参数 | 说明 |
|---|---|
-XX:MetaspaceSize=<size> | 触发 Metaspace GC 的初始阈值 |
-XX:MaxMetaspaceSize=<size> | 限制 Metaspace 最大大小(防止内存溢出) |
注意:永久代(PermGen)在 JDK8 中已被 Metaspace 替代。
其他常用参数#
| 参数 | 说明 |
|---|---|
-server | 启用“服务器”模式(默认在 64 位 JVM 中启用,优化吞吐量) |
-XX:+HeapDumpOnOutOfMemoryError | 发生 OOM 时自动生成堆转储文件 |
-XX:HeapDumpPath=<path> | 指定堆转储文件路径 |
-XX:+UseCompressedOops | 启用压缩普通对象指针(节省内存,64 位 JVM 默认开启) |
-D<name>=<value> | 设置系统属性(如 -Dfile.encoding=UTF-8) |
示例组合(生产环境常见配置)#
java -server \ -Xms4g -Xmx4g \ -Xmn1g \ -XX:+UseG1GC \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/logs/heapdump.hprof \ -Xloggc:/logs/gc.log \ -XX:+PrintGCDetails \ -XX:+PrintGCTimeStamps \ -jar myapp.jar