HotSoprt是默认虚拟机
主要支持以下语言:
- Java:这是 JVM 最为常见和广泛支持的语言。
- Kotlin:一种在 Java 生态系统中越来越流行的静态类型编程语言,与 Java 具有良好的互操作性。
- Scala:一种融合了面向对象编程和函数式编程特点的语言。
- Groovy:一种动态类型的脚本语言,基于 Java 平台。
- Clojure:一种运行在 JVM 上的函数式编程语言。
运行时数据区 | ||
---|---|---|
方法区 (多线程共享) (JDK7前:方法区用永久代实现) (JDK8开始:类变量+class对象,放置Java堆中) | 虚拟机栈 (每个线程私有,互不影响) | 本地方法栈 (每个线程私有,互不影响) |
堆 (多线程共享) | 程序计数器 (每个线程私有,互不影响) (记录正在执行的虚拟机字节码指令地址) (如果正在执行本地方法此值为空) |
GC种类
新生代垃圾回收 (Minor GC) | 老年代垃圾回收 (Major or FUll GC) | 永久代/元空间垃圾回收 (PermGen Gc or Metaspace GC) |
针对新生代Eden+survivor 通常适用复制算法转移内存 | 针对老年代 一般用标记-清除/标记-压缩算法 | 元空间由操作系统负责,一般不会触发GC |
永久代→元空间
永久代 | 元空间 | |
---|---|---|
版本 | JDK8之前 | JDK8及之后 |
存储内容 | 类的元数据 运行时常量 方法区中的静态变量 | 类的元数据 运行时常量 方法区的静态变量 方法、字节码、常量池 |
主要区别 | 堆中 固定大小 | 本地内存,不在堆中,减少内存溢出风险 |
内存配置 | -XX:PermSize=64m(默认物理内存的1/64) -XX:MaxPermSize=128m(默认物理内存的1/4) | -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m |
用的什么编译器?
JIT编译器 | C1 | 编译快,优化低 适用客户端 |
---|---|---|
C2 | 编译满、优化高 适用服务器端。JDK11引入Graal与C1、C2共同适用 | |
shark | 早期为mac OS X上执行,之后逐渐停止维护 |
如何指定GC算法启动参数?
示例: -XX:+UseParalleGC 或 -XX:UseG1GC
对象循环引用如何回收?
只要不被GCRoot关联引用即可,用的可达性分析算法,非引用计数算法。
GC日志打印区别?
JDK9之前:-XX:PrintGC -XX:PrintGCDetails
JDK9之后:-Xlog:gc -Xlog:gc*
什么是安全点?
所有线程安全暂停,可安全枚举所有根状态。
什么是浮动垃圾?
原本A引用B,但在A标记为黑色后,A对B的引用断开,没有指向B的引用后,B应该为白色区域,但已经标记灰色区域。这种情况,B第一次GC不会被回收,这种情况称为浮动垃圾。
如何避免像netty这类使堆外内存过大导致OOM?
设置堆外内存上限:通过JVM参数-XX:MaxDirectMemorySize
设置堆外内存的上限,超过该值时触发Full GC。
对象的访问方式
句柄 | 直接指针 | |
---|---|---|
示意图 | ![]() | ![]() |
好处 | reference中存储的是稳定的句柄地址,在对象被移动时,只会改变句柄中的实例数据指针,而reference本身不需要被修改。 | 速度更快,节省了一次指针定位需要的时间开销,由于JAVA对象访问十分频繁,这类开销积小成多后也是一项非常可观的执行成本。Sun HotSpot虚拟机使用的就是这种访问方式。 |
实现区别 | JAVA堆中将会划分出来一块内存作为句柄池,reference中就是存储了对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。 | 相比较句柄的访问方式,JAVA堆中不会单独划分内存,reference中直接存储了对象地址,而对象中包含了对象类型数据的地址信息。 需随对象移动而变。 |
回收算法
回收算法 | 标记——清除 | 标记——整理 | 标记——复制 |
---|---|---|---|
示意图 | ![]() | ![]() | ![]() |
缺点 | 对象太多时内存碎片化严重,不易找到连续空间,浪费内存 | 因有整理步骤,消耗更多时间 | GC时占用双倍内存空间 |
优点 | 相对更快 | 准确式的垃圾回收方式,内存碎片少 | 相对更快,结果准确,内存碎片少 |
算法对比
CMS | G1GC | ZGC | |
---|---|---|---|
JDK 选择 | JDK6+ | JDK9+ | JDK15+ 推荐JKD21 |
GC Root 扫描内容 |
| ||
GC流程 |
|
|
|
![]() | ![]() | ![]() | |
初始标记 | 仅遍历被 GC-Roots + 新生代对象 直接引用 的老年代对象,所以很快 | 只需要扫描所有GC Roots,STW时间与GC Roots成正比,一般很短 | |
并发标记 | 对 GC-Roots + 新生代对象 间接引用的 老年代对象进行标记 | 遍历堆中所有对象 | |
重新/最终标记 |
| ||
GC目标 |
|
|
|
标记算法特点 | 增量更新 (将黑色对象重新标记为灰色) (不是一种存储方式,而是一种策略) | 原始快照(SATB) (不需要在重新标记阶段再深度扫描被删除引用对象,但可能造成更多浮动垃圾) | 染色指针 (指针存储,43~46位为染色信息,管理内存最大2^42=4TB [18位空闲][是否只能finalize()访问][2][1][0][存储的内存地址]) 内存多重映射 (多个虚拟机地址,映射到某个物理地址) 自愈能力 (通过1.并发标记2.并发预备重分配3.并发重分配(“自愈”)4.并发重映射) |
m0、m1和remapped 的变化过程如下: 一、初始状态 在 ZGC 启动时,m0 和 m1 都处于未使用状态,remapped 也为初始状态值(通常为 0)。 二、垃圾收集开始
三、转移过程中
四、下一次垃圾收集
| |||
新生代:标记-复制(不直接参与GC) 老年代:标记-整理(主要针对老年代进行GC) | 大部分region一样大 新生代:标记-复制(参与GC) 老年代:标记-整理(占用堆空间45%可能触发参与GC) | 标记-复制 动态创建删除region 有大中小三种region分别存放对象 (小256KB~2M,中256KB~4M,大4M~2M的倍数) | |
对象转移 | 转移是原子性保的,不存在转移时对象内容被更改 但对象引用可能更改,这时候ZGC利用读屏障来让用户获取正确的对象 | ||
三色标记 | 从GC Root开始遍历,记录在对象头中 黑色:已标记,引用都已标记处理 灰色:已标记,还有引用没扫描处理 白色:未标记,不可达 | ||
漏标问题 | 原本A→B,B→C,在A被标记为黑色后,A建立→C的引用,且B→C的引用断开。此时A已经被标记为黑色,则不会再遍历A的引用,且B→C的引用已经断开。这样会造成C未被标记,被垃圾回收,造成空指针问题。 | ||
漏标解决 | 重新扫描(Remark)阶段:
| Remembered Set(记忆集)和 Card Table(卡表)的作用
| 当 A 建立对 C 的引用时,读屏障会被触发。读屏障会检查对象的颜色状态,如果发现新的引用关系且目标对象(C)可能未被正确标记,会进行相应处理。
|
获取对象状态 | 每次GC扫描所有区域 对象存活信息在对象头中 | 每次GC扫描所有Region 对象存活信息在对象头中 | 对象存活信息记录在染色指针上42~45位 存在寄存器中,比内存更快 并发转移+标记阶段都用到: |
读写屏障 | 只有一份:老年代→新生代 的引用 写后屏障:维护并发标记的正确性、无浮动垃圾 实现方式:基于增量更新 | 因需要多张表,内存多占至少20% 写前屏障 写后屏障 实现方式:基于SATB | 读屏障:从队中读取对象引用时使用,在对象标记和转移过程中,用于确定对象引用地址是否满足条件,从而做出不同逻辑。 |
空间分布 |
| 堆空间:由无数Region组成,每个region内也分新生代、老年代
| 无严格意义的Eden、新生代、老年代等划分
|
图例来源于网络
主要参考书籍:
《深入理解java虚拟机》第3版 周志明 著
《深入Java虚拟机》——JVM G1GC的算法与实现 [日]中村成洋 著