Skip to content

JVM ​

配套知识点:JVM 深入

怎么用这份笔记

  • 分级:★★★ 必须吃透 / ★★☆ 了解即可(记结论就行) / ★☆☆ 可以跳过(一句话看一眼)
  • 【简历亮点,高频追问】:简历里写过的技术,面试官会顺着往下挖,这段必须能展开讲
  • 每个考点按「定义 → 原理 → 要点 / 易错点」组织,只讲知识点

1. JVM 内存区域划分,哪些会 OOM ★★★ ​

JVM 运行时数据区按访问方式分为线程私有和线程共享两类,另有一块堆外的直接内存。

  • 程序计数器:线程私有,记录当前线程下一条要执行的字节码地址;执行 Java 方法时存字节码偏移量,执行 native 方法时为空。它是唯一不会 OOM 的区域。
  • 虚拟机栈:线程私有,生命周期与线程相同,管理 Java 方法执行。每个方法执行时创建一个栈帧,存局部变量表、操作数栈、动态链接、返回地址;栈大小由 -Xss 决定,决定了方法调用的可达深度。深度超限抛 StackOverflowError;栈可动态扩展但扩展时没有内存则抛 OutOfMemoryError。
  • 本地方法栈:线程私有,为 native 方法(C/C++)服务,HotSpot 中与虚拟机栈合并实现。
  • 堆:线程共享,存放对象实例和数组,是垃圾回收的主要区域,分新生代(Eden + S0 + S1)和老年代;空间不足且回收不掉时抛 OutOfMemoryError: Java heap space。
  • 方法区 / 元空间:线程共享,存放被加载类的元数据、静态变量、常量、即时编译后的代码;JDK 8 起由本地内存中的元空间实现。动态生成类过多(CGLIB 代理、频繁热部署、大量反射)会使其持续增长,直到 Metaspace OOM。类只有在所有实例被回收、且加载它的 ClassLoader 也被回收后才可能被卸载。
  • 直接内存:堆外内存,NIO 的 ByteBuffer.allocateDirect 使用,不受堆大小约束,超过 -XX:MaxDirectMemorySize 时抛 Direct buffer memory。

易错点:

  • 会 OOM 的是堆、元空间、直接内存;栈以 StackOverflowError 为主,仅在栈可动态扩展且扩展失败时才是 OOM。
  • 元空间默认没有上限,在容器里可能吃满物理内存把进程拖垮,必须显式设 -XX:MaxMetaspaceSize;堆用 -Xms / -Xmx 显式指定并设成相等,避免容器内存超限被 OOM Killer 杀掉。

补充(原文详解)

1.JVM内存模型简介

JVM定义了不同运行时数据区,他们是用来执行应用程序的。某些区域随着JVM启动及销毁,另外一些区域的数据是线程性独立的,随着线程创建和销毁。jvm内存模型总体架构图如下:(摘自oracle 官方网站)

JVM 内存区域划分,哪些会 OOM

JVM在执行Java程序时,会把它管理的内存划分为若干个的区域,每个区域都有自己的用途和创建销毁时间。如下图所示,可以分为两大部分,线程私有区和共享区。下图是根据自己理解画的一个 JVM内存模型架构图:

JVM 内存区域划分,哪些会 OOM

JVM内存分为线程私有区和线程共享区

线程私有区 ​

1、程序计数器

当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,不免发生线程切换。这时,每个线程就需要一个属于自己的计数器来记录下一条要运行的指令。如果执行的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为

空。

2、虚拟机栈

线程私有的,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一个桢栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法, -Xss参数可以设置虚拟机栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则

抛出stackOverflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出

OutofMemoryError。 使用jclasslib工具可以查看class类文件的结构。下图为栈帧结构图:

线程私有区

3、本地方法栈

与虚拟机栈作用相似。但它不是为Java方法服务的,而是本地方法(C语言)。由于规范对这块没有

强制要求,不同虚拟机实现方法不同。

线程共享区 ​

1、方法区

线程共享的,用于存放被虚拟机加载的类的元数据信息,如常量、静态变量和即时编译器编译后的代码。若要分代,算是永久代(老年代),以前类大多“static”的,很少被卸载或收集,现回收废弃常量和无用的类。其中运行时常量池存放编译生成的各种常量。(如果hotspot虚拟机确定一个类的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收)

2、堆

存放对象实例和数组,是垃圾回收的主要区域,分为新生代和老年代。刚创建的对象在新生代的 Eden区中,经过GC后进入新生代的S0区中,再经过GC进入新生代的S1区中,15次GC后仍存在就进入老年代。这是按照一种回收机制进行划分的,不是固定的。若堆的空间不够实例分配,则

OutOfMemoryError。

线程共享区

即图中的Eden + From Space( s0) + To Space(s1)存放新生的对象有两个,存放每次垃圾回收后存活的对象(s0+s1)Tenured Generation 即图中的Old Space主要存放应用程序中生命周期长的存活对象Young Generation EdenSurvivor Space Old Generation

2. 对象创建过程、内存布局、对象头 ★★☆ ​

new 一个对象分五步:

  1. 类加载检查:检查指令参数能否在常量池中定位到类的符号引用,未加载则先触发类加载。
  2. 分配内存:堆规整时用指针碰撞(把分界指针向空闲方向移动对象大小的距离);堆不规整时用空闲列表(维护可用内存块列表,找一块足够大的分配并更新列表)。HotSpot 还用 TLAB(Thread Local Allocation Buffer)让线程先在堆中预分配的一小块线程私有缓冲里分配,用尽后再走 CAS 申请新块,减少分配时的锁竞争。
  3. 内存清零:把除对象头外的空间初始化为 0,保证字段有默认值。
  4. 设置对象头:写入 Mark Word 和类型指针。
  5. 执行构造器:按程序真正初始化字段。

对象在内存中分三块:

  • 对象头:Mark Word 存对象自身的运行时数据——hashCode、GC 分代年龄、锁状态标志、偏向线程 ID、偏向时间戳等,这些信息复用同一块位,随锁标志位变化而改变含义;另一部分是类型指针,指向类元数据,用于确定对象属于哪个类;数组对象还额外记录数组长度。
  • 实例数据:对象真正存储的有效信息,包括父类继承下来的和自己定义的字段。
  • 对齐填充:JVM 要求对象起始地址是 8 字节的整数倍,不足时补齐。

Mark Word 中的锁状态标志就是 synchronized 锁升级(偏向锁 → 轻量级锁 → 重量级锁)的载体,是锁升级与对象头两块知识共用的底层结构。


3. 如何判断对象是垃圾;四种引用 ★★★ ​

判断对象是否存活有两种算法:

  • 引用计数:每个对象维护一个引用计数,新增引用 +1、引用释放 -1,归零即可回收。实现简单,但无法解决循环引用——A、B 互引时两者计数都不为 0,永远不会被回收。
  • 可达性分析:从 GC Roots 出发向下搜索,走过的路径称为引用链;到 GC Roots 没有任何引用链相连的对象判定为不可用。HotSpot 采用这种。GC Roots 主要包括虚拟机栈中局部变量表引用的对象、静态变量引用的对象、常量引用的对象、native 引用的对象。

四种引用由强到弱:

  • 强引用:普通的 new,只要引用还在,GC 绝不回收,宁可 OOM。
  • 软引用(SoftReference):内存不足时才回收,适合做缓存。
  • 弱引用(WeakReference):只要发生 GC 就回收。ThreadLocal 的 key 就是弱引用,Entry 的 key 被回收后 value 仍可能被强引用链挂住,所以用完必须 remove;Netty 的 FastThreadLocal 也是为规避 ThreadLocal 的性能与内存泄漏问题而设计。
  • 虚引用(PhantomReference):拿不到对象本身,只在对象被回收时收到通知,NIO 释放直接内存就是靠它配合 Cleaner 实现。

注意:JVM 的软/弱引用缓存无法控制容量和淘汰策略,做本地缓存仍需显式容量上限或直接使用 Redis 这类外部缓存。


4. GC 三大算法 ★★★ ​

  • 标记清除(Mark-Sweep):先标记出所有需要回收的对象,标记完成后统一清除。实现简单,但会产生内存碎片。
  • 复制(Copying):把可用内存分成大小相等的两块,只用其中一块;该块用完时把存活对象复制到另一块,再整体清空原块。没有碎片,但浪费空间。新生代实际采用 Eden : S0 : S1 = 8:1:1,只浪费 10%;且新生代对象大多朝生夕死,存活对象少,复制成本低。
  • 标记整理(Mark-Compact):标记过程与标记清除相同,但不清除,而是把所有存活对象向一端移动,再清理边界以外的内存。没有碎片,但移动对象成本高,需要 STW。

结论:新生代用复制算法,老年代用标记整理。G1 的新生代回收就是把存活对象从 Eden 复制到 Survivor,Eden 区满时触发 Young GC 即是这个过程。


5. 常见垃圾收集器(重点 G1)★★★ 【简历亮点,高频追问】 ​

演进路线:

  • Serial:新生代单线程收集器,复制算法,简单高效,适合小内存、单核客户端;Serial Old 是其老年代版本,用标记整理。
  • ParNew:Serial 的多线程版本,新生代并行复制,多核下表现优于 Serial,早期常与 CMS 搭配。
  • Parallel Scavenge / Parallel Old:新生代并行复制、老年代并行标记整理,目标是高吞吐量(吞吐量 = 用户线程时间 /(用户线程时间 + GC 线程时间)),适合对交互要求不高的后台计算型应用,JDK 8 默认。
  • CMS(Concurrent Mark Sweep):老年代并行收集器,以获取最短回收停顿时间为目标,采用标记清除,并发标记并发清理。缺点是有内存碎片、有浮动垃圾,并发阶段会与应用线程抢 CPU,且可能出现 Concurrent Mode Failure,JDK 9 起已废弃。
  • G1(Garbage First):JDK 7 引入、JDK 9 起默认。把堆切成大小相等的 Region,不再物理划分新生代与老年代,而是逻辑分配;回收范围覆盖整个 Java 堆,基于标记整理,不产生碎片;每次优先回收垃圾最多的 Region,因此得名 Garbage First;停顿时间可预测,用 -XX:MaxGCPauseMillis 设定目标。
  • ZGC:JDK 11 引入,以低延迟为首要目标。基于动态 Region、着色指针、读屏障、内存多重映射实现并发的标记整理;支持 TB 级堆(最大 4T),停顿时间不超过 10ms,额外内存开销小;缺点是有浮动垃圾。

G1 关键机制:

  • 堆被划分成若干 Region,一部分用作新生代、一部分用作老年代,另有专门存放巨型对象的分区;仍是分代收集器,只是取消了物理隔离。
  • 回收流程:初始标记(标记 GC Roots 直接关联对象,STW)→ 并发标记(与应用线程并发执行)→ 最终标记(完成三色标记周期,STW)→ 筛选回收(优先选择可回收空间大的 Region 做复制/清理,用复制算法整合碎片)。
  • 新生代回收采用并行复制,会 STW;老年代回收会把年轻代一并回收。
  • 实际调优主要靠 -XX:MaxGCPauseMillis 设停顿目标,并配合 GC 日志观察回收效果。

选型:新项目一般 G1 起步,只有超大堆、极低延迟要求才上 ZGC。


补充(原文详解)

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了 7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、 PraNew、 Parallel

Scavenge,回收老年代的收集器包括Serial Old、 Parallel Old、CMS,还有用于回收整个Java堆的 G1收集器。不同收集器之间的连线表示它们可以搭配使用。

常见垃圾收集器★★★ 【简历亮点,高频追问】

Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;

ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核 CPU环境下有着比Serial更好的表现;

Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;

Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;

Parallel Old收集器 (标记-整理算法):老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;

CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

G1(Garbage First)收集器 (标记-整理算法):Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

ZGC ( Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器。它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器。在 JDK 11 新加入,还在实验阶

段,主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms。优点:低停顿,高吞吐

量, ZGC 收集过程中额外耗费的内存小。 缺点:浮动垃圾

目前使用的非常少,真正普及还是需要写时间的。

新生代收集器:Serial、 ParNew、 Parallel Scavenge

老年代收集器: CMS、Serial Old、 Parallel Old

整堆收集器: G1, ZGC (因为不涉年代不在图中)。

6. 类加载全过程;双亲委派 ★★★ ​

类加载分五步,其中验证、准备、解析合称连接:

  1. 加载:通过类的完全限定名查找字节码文件,读入后生成对应的 Class 对象。
  2. 验证:校验文件格式、元数据、字节码、符号引用,确保 Class 文件符合当前虚拟机要求、不会危害虚拟机安全。
  3. 准备:为 static 修饰的类变量分配内存并设零值,如 static int i = 1 此时 i 仍为 0;final 静态常量在编译期就已分配,不在此阶段。
  4. 解析:把常量池中的符号引用替换为直接引用(指向目标的指针或相对偏移量)。
  5. 初始化:执行 <clinit>,即静态代码块与静态变量赋真正初值;先初始化父类再初始化当前类,只有对类主动使用时才触发(new 实例、访问静态方法或静态变量、Class.forName 反射、某子类初始化)。

双亲委派:加载类时先把请求委托给父加载器,层层向上直到顶层的启动类加载器;父加载器能完成加载就成功返回,不能才由子加载器自己尝试。

好处:

  • 避免类的重复加载;
  • 防止 Java 核心 API 被篡改,自己写一个 java.lang.String 不会被加载。

破坏双亲委派的典型场景:

  • JDBC / SPI:DriverManager 由启动类加载器加载,但驱动实现类在 classpath 中,启动类加载器看不到,于是通过线程上下文类加载器反向委派去加载;JDK 的 SPI、Spring 的 SpringFactoriesLoader 都是这个套路。
  • Tomcat 类隔离:每个 Web 应用一个类加载器,先尝试自己加载再交给父加载器,用于隔离不同应用的依赖版本。

易错点:准备阶段只给静态变量赋零值,初始化阶段才赋真正的初值;Java 自带加载器加载的类在虚拟机生命周期内不会被卸载,只有自定义加载器加载的类才可被卸载。


补充(原文详解)

加载过程

其中验证,准备,解析合称链接

加载通过类的完全限定名,查找此类字节码文件,利用字节码文件创建Class对象.

验证确保Class文件符合当前虚拟机的要求,不会危害到虚拟机自身安全.

准备进行内存分配,为static修饰的类变量分配内存,并设置初始值(0或null).不包含final修饰的静态变量,因为final变量在编译时分配.

解析将常量池中的符号引用替换为直接引用的过程.直接引用为直接指向目标的指针或者相对偏移量等.

初始化主要完成静态块执行以及静态变量的赋值.先初始化父类,再初始化当前类.只有对类主动使用时才会初始化.

触发条件包括,创建类的实例时,访问类的静态方法或静态变量的时候,使用Class.forName反射类的时候,或者某个子类初始化的时候.

Java自带的加载器加载的类,在虚拟机的生命周期中是不会被卸载的,只有用户自定义的加载器加载的类才可以被卸.

1、加载机制-双亲委派模式

类加载全过程;双亲委派

双亲委派模式,即加载器加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器. 父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载.*

优点:

  1. 避免类的重复加载

  2. 避免Java的核心API被篡改

2、分代回收

分代回收基于两个事实:大部分对象很快就不使用了,还有一部分不会立即无用,但也不会持续很长时间.

年轻代->标记-复制老年代->标记-清除

3、回收算法

a、 G1算法

1.9后默认的垃圾回收算法,特点保持高回收率的同时减少停顿.采用每次只清理一部分,而不是清理全部的增量式清理,以保证停顿时间不会过长

其取消了年轻代与老年代的物理划分,但仍属于分代收集器,算法将堆分为若干个逻辑区域(region),一部分用作年轻代,一部分用作老年代,还有用来存储巨型对象的分区.

同CMS相同,会遍历所有对象,标记引用情况,清除对象后会对区域进行复制移动,以整合碎片空间.

年轻代回收: 并行复制采用复制算法,并行收集,会StopTheWorld.

老年代回收: 会对年轻代一并回收

初始标记完成堆root对象的标记,会StopTheWorld.并发标记 GC线程和应用线程并发执行. 最终标记完成三色标记周期,会StopTheWorld.复制/清楚会优先对可回收空间加大的区域进行回收

b、ZGC算法

前面提供的高效垃圾回收算法,针对大堆内存设计,可以处理TB级别的堆,可以做到10ms以下的回收停顿时间.

类加载全过程;双亲委派

着色指针

读屏障

并发处理

基于region

内存压缩(整理)

roots标记:标记root对象,会StopTheWorld.并发标记:利用读屏障与应用线程一起运行标记,可能会发生StopTheWorld.清除会清理标记为不可用的对象. roots重定位:是对存活的对象进行移动,以腾出大块内存空间,减少碎片产生.重定位最开始会StopTheWorld,却决于重定位集与对象总活动集的比例. 并发重定位与并发标记类似.

7. OOM 常见场景与排查工具(jmap / jstack / jstat)★★★ 【简历亮点,高频追问】 ​

常见 OOM 场景:

  • 堆溢出:对象过多或存在大对象,报 Java heap space,多为内存泄漏或一次性加载数据过多。
  • 栈溢出:递归太深,报 StackOverflowError;线程数过多导致栈内存分配不出,报 unable to create native thread。
  • 元空间溢出:动态生成类过多(CGLIB 代理、热部署、大量反射),报 Metaspace。
  • GC 开销超限:GC 占用绝大部分 CPU 却回收不到多少空间,报 GC overhead limit exceeded。
  • 直接内存溢出:堆外内存超限,报 Direct buffer memory。

排查工具:

  • jps:查看 HotSpot 虚拟机进程及 PID。
  • jstat:查看类装载、内存、垃圾收集、JIT 编译等运行时数据,主要用来看 GC 频率和各代占用变化。
  • jmap:生成 heap dump 文件;配合 -XX:+HeapDumpOnOutOfMemoryError 可在 OOM 时自动导出。
  • jhat / MAT:分析 dump,MAT 的支配树(Dominator Tree)和 Histogram 用于定位占用最大的对象。
  • jstack:生成当前时刻的线程快照,用于查死锁和 CPU 占用高的线程。
  • jinfo:实时查看和调整虚拟机运行参数。

定位思路:

  • 堆 OOM:MAT 打开 dump,看 Histogram 中哪个类的实例数或占用最大,再点开它的 GC Roots,顺引用链找到一直持有它的代码。
  • CPU 飙高:top 找进程 → top -Hp 找线程 → 线程号转成十六进制 → 在 jstack 输出里按 nid 找到对应线程栈;常见原因是死循环、频繁 Full GC、锁竞争。
  • 生产上应常开 -XX:+HeapDumpOnOutOfMemoryError 留存现场,并靠监控先发现 GC 异常。

8. 逃逸分析、栈上分配 ★★☆ ​

逃逸分析(Escape Analysis)是 JIT 的一类跨函数全局数据流分析:分析对象指针的动态范围,判断对象的引用会不会「逃出」方法或线程(被返回、被全局引用、被其他方法或线程引用)。不逃逸的对象可以做三种优化:

  • 栈上分配:对象随方法结束直接销毁,不进堆,降低 GC 运行频率。
  • 标量替换:把对象拆成一个个基本类型变量,不再生成对象头,内存分配改在栈上,减少内存占用并降低 GC 频率。
  • 锁消除:发现对象只能被单线程访问时,去掉其上的 synchronized 同步。

易错点:HotSpot 默认开启逃逸分析,但并没有真正意义上的「栈上分配」,实际生效的是标量替换;该优化对多数业务代码的性能影响有限,属于理解结论即可的点。


9. JVM 常用调优参数 ★★☆ ​

按用途分类:

  • 堆:-Xms(初始堆)、-Xmx(最大堆),建议设成相等以避免动态扩容抖动;-Xmn 新生代大小、-XX:NewRatio 新老年代比例、-XX:SurvivorRatio Eden 与单个 Survivor 的比值。
  • 栈:-Xss,每个线程的栈大小。
  • 元空间:-XX:MaxMetaspaceSize 上限、-XX:MetaspaceSize 初始阈值。
  • 直接内存:-XX:MaxDirectMemorySize。
  • 晋升相关:-XX:PretenureSizeThreshold(超过该大小的对象直接进老年代)、-XX:MaxTenuringThreshold(在 Survivor 复制的最大年龄阈值)。
  • 选 GC:-XX:+UseG1GC、-XX:+UseParallelGC、-XX:ParallelGCThreads 并行线程数。
  • 停顿目标:-XX:MaxGCPauseMillis。
  • 日志与诊断:-XX:+PrintGCDetails(新版本为 -Xlog:gc*)、-XX:+PrintTLAB、-XX:+HeapDumpOnOutOfMemoryError 自动导出内存快照。
  • 禁止显式 GC:-XX:+DisableExplicitGC,防止误调用 System.gc() 触发 Full GC。

调优思路是先看监控和 GC 日志,确认瓶颈在内存、CPU 还是 GC,再动手,不要上来就加内存。

容器场景的坑:JVM 默认堆按宿主内存估算,识别不到容器的 memory limit,容易超限被 OOM Killer 杀掉。所以容器部署时堆、元空间、直接内存都要显式给上限,并给堆外内存留出富余。


补充(原文详解)

Sun JDK监控和故障处理命令有jps jstat jmapjhat jstack jinfo

jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。

jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

jmap,JVM Memory Map命令用于生成heap dump文件

jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内

置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看 jstack,用于生成java虚拟机当前时刻的线程快照。

jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。

补充(原文详解)

「堆栈内存相关」

-Xms 设置初始堆的大小

-Xmx 设置最大堆的大小

. -Xmn 设置年轻代大小,相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值 -Xss 每个线程的堆栈大小

-XX:NewSize 设置年轻代大小(for 1.3/1.4)

-XX:MaxNewSize 年轻代最大值(for 1.3/1.4)

-XX:NewRatio 年轻代与年老代的比值(除去持久代)

-XX:SurvivorRatio Eden区与Survivor区的的比值

-XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。 -XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到

老年代

「垃圾收集器相关」

-XX:+UseParallelGC:选择垃圾收集器为并行收集器。

-XX:ParallelGCThreads=20:配置并行收集器的线程数

-XX:+UseConcMarkSweepGC:设置年老代为并发收集。

-XX:CMSFullGCsBeforeCompaction=5 由于并发收集器不对内存空间进行压缩、整理, 所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行5次GC以后对内存空间进行压缩、整理。

-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片

「辅助信息相关」

-XX:+PrintGCDetails 打印GC详细信息

-XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照, 排查问题用

-XX:+DisableExplicitGC禁止系统System.gc(),防止手动误触发FGC造成问题. -XX:+PrintTLAB 查看TLAB空间的使用情况

10. JMM 与 JVM 内存模型的区别 ★★★ ​

两者完全不是一回事,最容易混淆:

  • JVM 内存模型(运行时数据区):描述「数据存在哪」,即堆、虚拟机栈、本地方法栈、程序计数器、方法区的划分。
  • JMM(Java 内存模型):描述「多线程如何安全读写共享变量」。它把内存抽象为主内存(所有线程共享,存放变量本体)和每个线程私有的工作内存(主内存的副本),线程的读写都在工作内存中进行,再由 JMM 规定何时同步回主内存。

JMM 要解决的三件事:

  • 原子性:由 synchronized、Lock、原子类保证;volatile 只保证单次读写的原子性,不保证复合操作(如 i++)。
  • 可见性:volatile 写立即刷回主内存并使其他线程的副本失效;synchronized 在释放锁前也会把工作内存刷回主内存。
  • 有序性:volatile 通过内存屏障禁止指令重排,synchronized 通过串行化保证;理论依据是 happens-before 规则。

易错点:一个「停止标志」没有声明 volatile,其他线程修改后,本线程读到的仍是工作内存里的旧值,循环停不下来。这是 JMM 可见性的经典问题,修复方式是加 volatile 或改用 AtomicBoolean。


11. 元空间 vs 永久代、字符串常量池位置 ★★☆ ​

  • 永久代:JDK 7 及以前 HotSpot 对方法区的实现,位于堆内,物理内存与堆连续,大小由 -XX:PermSize(初始)和 -XX:MaxPermSize(最大,默认 64M)限制。大小需要预先估准,估小了容易 OutOfMemoryError: PermGen space,典型场景是 Web 开发中大量 JSP 页面动态生成 class。
  • 元空间:JDK 8 起取代永久代,位于本地内存,与堆不再连续,理论上「机器内存有多大,元空间就能有多大」,默认没有上限,需要用 -XX:MaxMetaspaceSize 显式限制;-XX:MetaspaceSize 是初始阈值,达到后会触发 GC 做类型卸载并动态调整该值。
  • 字符串常量池:JDK 7 起已从永久代移到堆中。

替换的根本原因:永久代上限由 MaxPermSize 决定,但很难预估合适的值,用默认值容易 OOM;元空间改用本地内存后,能加载多少类的元数据由系统实际可用空间决定。方法区本身一直存在,只是实现从永久代换成了元空间。

实践要点:使用 CGLIB 等动态代理会持续产生代理类,都进入元空间,因此启动参数要固定给 -XX:MaxMetaspaceSize。


12. 直接内存(Netty 重点)★★☆ ​

直接内存是堆外内存,NIO 的 ByteBuffer.allocateDirect 分配的就是它。

  • 好处:不受堆大小和 GC 管理,省掉一次堆内到堆外的数据拷贝,做网络 IO 性能更好,Netty 默认就用堆外直接内存(PooledByteBufAllocator)。
  • 代价:不在堆内,GC 日志和堆 dump 都看不到,不好监控;用量超过上限抛 Direct buffer memory。
  • 上限:默认约等于 -Xmx,需要更明确时要显式设 -XX:MaxDirectMemorySize。
  • 释放:靠虚引用 + Cleaner,等对象被 GC 时才触发回收,所以释放时机不确定、不能依赖它兜底。

易错点:

  • 直接内存 OOM 时堆 dump 里查不到线索,排查要看堆外内存监控指标。
  • ByteBuf 必须严格按引用计数 release,漏释放会持续占用堆外内存。
  • 可用 Netty 自带的泄漏检测 -Dio.netty.leakDetection.level 在测试环境提前暴露泄漏。

13. 三色标记、对象进老年代条件、STW ★★☆ ​

三色标记是并发标记的基础,按扫描进度把对象分为三种状态:

  • 白色:尚未被扫描到。标记结束时仍是白色即判定为垃圾。
  • 灰色:自身已被扫描到,但其引用的子对象还没扫完。
  • 黑色:自身和所有子对象都扫描完毕。

并发标记时用户线程仍在修改引用,可能产生漏标:黑色对象新增了对白色对象的引用,同时灰色对象删除了对该白色对象的引用,结果这个白色对象被当成垃圾回收。两种补救方案:

  • CMS:增量更新(Incremental Update)——记录并发期间新增的引用,重新标记时以黑色对象为根重新扫描。
  • G1:SATB(Snapshot At The Beginning,原始快照)——靠写屏障记录引用被删除前的旧值,最后把这些旧引用当作存活对象重新扫描,防止对象被误回收。

对象进入老年代的条件:

  • 年龄达到阈值,默认 -XX:MaxTenuringThreshold=15;对象每次经历 Minor GC 后仍存活则年龄 +1。
  • 大对象直接进入:超过 -XX:PretenureSizeThreshold 的对象直接在老年代分配,避免在 Eden 与 Survivor 之间大量复制。
  • 动态年龄判断:Survivor 中相同年龄所有对象大小总和超过 Survivor 空间的一半时,年龄大于等于该年龄的对象直接晋升老年代。
  • 空间分配担保:Minor GC 前按历次晋升到老年代的平均大小估算,若老年代剩余空间不足则直接触发 Full GC。

STW(Stop The World):GC 过程中为保证对象引用更新的正确性(尤其涉及对象移动)而暂停所有用户线程。任何收集器都有 STW,只是长短不同;所谓低停顿就是压缩 STW 时间,ZGC 能把它压到毫秒级以下。


14. JVM 出现 OOM 会导致进程挂掉吗 ★★★ ​

不一定,取决于 OOM 发生在哪块区域、有没有被捕获。

  • 堆 OOM、元空间 OOM、直接内存 OOM 都是抛出 OutOfMemoryError。若没人捕获,默认只会终止抛出异常的那个线程,JVM 进程本身可能还活着,但内存状态已不可靠,容易出现连环 OOM,建议尽快重启。
  • OutOfMemoryError 是 Throwable 的子类,catch (Throwable) 能抓到;但抓到后进程状态通常已不可信,继续运行有风险。
  • 真正让进程「直接消失」的是操作系统层面的 OOM Killer:进程被内核直接 kill,Java 日志里看不到任何异常。

容器场景的判断方法:

  • JVM 堆或堆外内存总量超过容器的 memory limit 时,是内核的 OOM Killer 动手,pod 会被直接重启。
  • 排查应看 dmesg 或容器的 exit code 137(128 + SIGKILL 9),而不是翻 Java 日志。
  • 预防手段是给 JVM 显式设堆上限,并让容器 limit 比堆大出一截,给堆外内存(元空间、直接内存、线程栈)留出余量。

15. 堆和栈的区别 ★★★ ​

一句话:栈管「方法怎么跑」,堆管「对象放哪」,两者性质完全不同。

  • 栈(虚拟机栈):线程私有,每个线程一份;存的是「方法的执行过程」——每个方法对应一个栈帧,内含局部变量表、操作数栈、动态链接、返回地址;方法调用压栈、方法返回弹栈,用完即释放,不需要 GC;容量小(-Xss 默认约 1MB),递归太深抛 StackOverflowError。
  • 堆:线程共享,所有线程共用一块;存的是「对象实例」和数组,new 出来的对象都分配在这里;容量大(几百 M 到几十 G),是 GC 的主战场,分新生代和老年代;空间不足且回收不掉时抛 OutOfMemoryError: Java heap space。

补充:对象的引用(局部变量)在栈上,对象本体在堆里。调优时栈一般不动(除非递归很深),重点调堆的 -Xms / -Xmx 和堆上的 GC 行为。


16. JVM 的即时编译器(JIT)如何工作、热点代码与分层编译 ★★☆ ​

Java 采用「先解释、后编译」的执行方式:启动时字节码由解释器逐条执行;当某些代码执行频率足够高,JIT(Just-In-Time 编译器)把它们编译成本地机器码,之后直接执行机器码,速度大幅提升。这就是 Java「越跑越快」的原因。

热点代码判定靠两个计数器:

  • 方法调用计数器:统计方法被调用的次数。
  • 回边计数器:统计循环体往回跳转的次数,用于识别循环热点。
  • 两者统计次数超过阈值即触发即时编译,阈值可由 -XX:CompileThreshold 调整;方法调用计数器还会随时间衰减,避免长时间运行后「陈旧热点」一直占用编译资源。

分层编译(JDK 8 后默认开启)分阶段递进:

  • 第 0 层:纯解释执行,启动最快。
  • 第 1~3 层:C1 编译,编译快、优化较少,其中部分层级会采集运行时的类型与分支信息,供后续优化使用。
  • 第 4 层:C2 编译,编译慢但优化激进,逃逸分析、方法内联、循环优化等都在这一层。
  • 运行越久,热点代码越会从 C1 逐步升级到 C2,从而兼顾启动速度和峰值性能。

实践含义:预热很重要。压测和刚重启后的前几分钟延迟偏高是正常现象,说明热点方法还没编译完,不要急着据此调参数。


17. 什么是内存屏障,JVM 如何使用它保证可见性与有序性 ★★★ ​

内存屏障是 CPU 层面的一类指令,作用像「挡墙」:禁止屏障两边的内存操作重排,并约束多核之间的缓存同步。

按方向分两大类:

  • 读屏障(Load Barrier):屏障之后的读操作不能重排到屏障之前。
  • 写屏障(Store Barrier):屏障之前的写操作不能重排到屏障之后。

两者组合出四种屏障:

  • LoadLoad:保证前面的读先于后面的读完成。
  • StoreStore:保证前面的写先于后面的写刷出。
  • LoadStore:保证前面的读先于后面的写。
  • StoreLoad:同时具备上述三种效果,最强、代价最高,也是 volatile 写的关键屏障。

JVM 用它落地 volatile 和 happens-before:

  • 写 volatile 变量时,在写指令之后插入 StoreLoad 屏障,把工作内存中的写刷回主内存,并使其他核的缓存副本失效——这就是可见性。
  • 读 volatile 变量时,在读之前插入读屏障,保证读到的是主内存的最新值。
  • 屏障同时禁止编译器与 CPU 的指令重排——这就是有序性,典型场景是双重检查锁里防止 new 对象时的指令重排。

结论:volatile 的语义本质就是编译器在读写前后插入了内存屏障,happens-before 规则在硬件上也是靠这些屏障落地的。屏障由 JVM 自动插入,开发者无需手写。


18. 如何排查和解决 JVM 内存泄漏问题 ★★★ ​

内存泄漏指对象已经不再使用,但仍被引用着、GC 回收不掉,于是堆占用持续上涨,最终 OOM。

先判断是不是真泄漏:

  • 看 GC 日志:老年代占用只涨不降,Full GC 之后也回不去,基本可判定为泄漏。
  • 若 Full GC 后能明显回落,多半是容量不足或瞬时对象过多,不是泄漏。

定位步骤:

  1. 用 jmap 导 heap dump,或靠 -XX:+HeapDumpOnOutOfMemoryError 在 OOM 时自动导出。
  2. 用 MAT 打开,先看 Histogram 找实例数或占用最大的类,再看支配树(Dominator Tree)确认谁真正占着内存。
  3. 点开该对象的 GC Roots,顺引用链找到「是谁一直持有它不放」,定位到具体代码。
  4. 用 jstat 观察 GC 频率与各代占用变化,确认修复效果。

常见泄漏源:

  • static 集合一直 add 却从不 remove。
  • ThreadLocal 用完不 remove,尤其线程池复用线程时,Entry 里的值会跨请求一直挂着。
  • 监听器 / 回调注册了不注销。
  • 流、连接、ByteBuf 等资源不关闭。
  • 缓存无上限:静态 Map 当缓存用、key 无限增长,应改为有容量上限的 LRU。

修复手段:补上 remove、加容量上限、改用弱引用或引入过期淘汰机制。


19. 虚拟机栈和本地方法栈的区别与作用 ★★☆ ​

两者都是线程私有、都以栈帧为单位的栈结构,区别只在于「执行哪种方法」:

  • 虚拟机栈:执行 Java 方法(字节码方法)。
  • 本地方法栈:执行 native 方法(C/C++ 实现,如 Object.hashCode、System.arraycopy、NIO 的底层实现)。

HotSpot 把两者合并实现,所以调优只需要一个 -Xss 参数。

栈帧包含四部分:

  • 局部变量表:存放方法参数和方法内定义的局部变量,以 slot 为单位。
  • 操作数栈:字节码执行运算时压入、弹出的操作数。
  • 动态链接:指向运行时常量池中当前方法的引用,用于支持多态的方法绑定和符号引用解析。
  • 返回地址:方法返回后继续执行的位置。

两类异常:

  • 栈深度超过最大可用深度(递归太深):StackOverflowError。
  • 创建新线程时栈内存分配不出来:OutOfMemoryError: unable to create native thread。

实践含义:每个线程都要占用一块栈内存,线程不是越多越好。线程数应按 CPU 核数和 IO 特性确定,例如 Netty 默认 Worker 线程数为核数的 2 倍。


20. 什么是 JVM 的内存碎片,哪些垃圾收集器会产生/避免碎片 ★★☆ ​

内存碎片指堆里散落着许多小的空闲块,总量够用,但找不到一块连续的空间放下一个新对象。

根源是标记清除算法:只清死对象、不移动活对象,清完就留下碎片。

CMS 基于标记清除,因此有碎片问题,后果是:

  • 碎片积累后大对象分配失败,触发「并发模式失败」(Concurrent Mode Failure),提前 Full GC;
  • 极端情况下退化到 Serial Old,用单线程做压缩整理。

避免碎片靠「移动对象、压缩整理」:

  • 标记整理算法(Serial Old、Parallel Old):把存活对象向一端移动,不产生碎片。
  • 复制算法(新生代各收集器):把存活对象复制到另一块,不产生碎片。
  • G1:按 Region 做复制/整理,回收时把存活对象挪到其他 Region,同样不留碎片。

结论:CMS 有碎片、G1 没有。这也是从 CMS 迁移到 G1 的主要理由之一——CMS 追求低停顿却依赖标记清除,长期运行碎片必然积累,最终要靠 Full GC 甚至退化到 Serial 来收拾。


21. JVM 如何处理异常,try-catch-finally 的字节码实现 ★★★ ​

JVM 处理异常靠异常表(exception table):每个方法的 Code 属性里带一张表,每行记录一段字节码的起止范围(from / to)、对应的 handler 入口以及 catch 的异常类型(catch_type)。

执行过程:

  1. 某条指令抛出异常时,JVM 拿当前 pc 去异常表里查找落在范围内的条目。
  2. 找到且类型匹配,就跳到对应 handler 执行。
  3. 找不到匹配项,就弹出当前栈帧,把异常抛给上一层调用者;一直抛到最外层仍无匹配,线程终止。

finally 的实现:javac 在编译期把 finally 里的代码复制到每个出口——正常结束路径、每个 catch 分支末尾、以及未匹配上的异常路径各复制一份,所以 finally 一定执行。这不是靠特殊运行时机制,而是靠代码复制。

由此得出两个结论:

  • finally 里的 return 会覆盖 try 中已确定的返回值,也会吞掉 catch 分支中的异常,属于应避免的写法。
  • JDK 7 的 try-with-resources 本质是编译器自动生成 finally 来调用 close(),字节码层面仍是 finally 的代码复制。

实践约定:finally 里不做 return、不做可能抛异常的操作,否则会掩盖真实问题;资源释放(如 ByteBuf 的 release)应放在 finally 或 try-with-resources 中,不依赖 GC 兜底。


22. JVM 是什么,为什么需要 JVM(跨平台原理)★★★ ​

JVM 是 Java 虚拟机,一台用软件模拟出来的计算机,执行的是字节码而不是物理 CPU 的机器码。

执行流程:Java 源码 → javac 编译成 .class 字节码文件 → JVM 加载并执行,先是解释执行,热点代码再由 JIT 编译成本地机器码。

跨平台原理是「一次编译,到处运行」:字节码是平台无关的中间格式,同一份字节码在不同操作系统上由各自平台的 JVM 执行,JVM 屏蔽了指令长度、字节序、系统调用等底层硬件差异,不需要为每个平台单独重写或重新编译。

代价与补偿:

  • 相比 C/C++ 直接编译成机器码多了一层抽象,理论上性能有损耗。
  • 但 JIT 把热点代码编译成本地机器码后,损耗已经很小,长期运行可接近原生。

附加价值:

  • 自动内存管理(GC),开发者不必手动申请释放内存。
  • 运行时安全检查:字节码验证、数组越界检查、空指针检查等,比 C/C++ 更安全。
  • 统一的内存模型、GC 与线程语义,使并发程序在各 OS 上行为一致、可预期。

所以「为什么需要 JVM」可归纳为三点:跨平台 + 自动内存管理与运行时安全检查 + 统一的内存/并发模型。


附:原始题库(旧版逐题,与上文重叠,可折叠跳过) ​

展开查看原始题库

23. 什么时候会触发FullGC ​

除直接调用System.gc外,触发Full GC执行的情况有如下四种。 1. 旧生代空间不足旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不

足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

  1. Permanet Generation空间满 PermanetGeneration中存放的为一些class的信息等,当系统中 要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采 用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信

息: java.lang.OutOfMemoryError: PermGen space 为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

  1. CMS GC时出现promotion failed和concurrent mode failure 对于采用CMS进行旧生代GC的 程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当 这两种状况出现时可能会触发Full GC。 promotionfailed是在进行Minor GC时,survivor space放 不下、对象只能放入旧生代,而此时旧生代也放不下造成的; concurrent mode failure是在执行 CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。 应对措施为:增大

survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置- XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。

  1. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间 这是一个较为复杂的触发 情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩 余空间,那么就直接触发Full GC。 例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生

代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,

则执行Full GC。 当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。 除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过-java-

Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

24. 什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”? ​

Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节码文件。 Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。

25. 说说对象分配规则 ​

对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。

大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。

长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次 Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。

动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。

空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小, 如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查

HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。

26. 说说Java对象创建过程 ​

1.JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在后边讲)

2.为对象分配内存。 一种办法“指针碰撞”、一种办法“空闲列表”,最终常用的办法“本地线程缓冲分配(TLAB)”

3.将除对象头外的对象内存空间初始化为0

4.对对象头进行必要设置

27. 知道类的生命周期吗? ​

类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程, 如下图;

知道类的生命周期吗?

加载,查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象

连接,连接又包含三块内容:验证、准备、初始化。 1)验证,文件格式、元数据、字节码、 符号引用验证; 2)准备,为类的静态变量分配内存,并将其初始化为默认值; 3)解析,把类中的符号引用转换为直接引用

初始化,为类的静态变量赋予正确的初始值

使用, new出对象程序中使用

卸载,执行垃圾回收

28. 简述Java的对象结构 ​

Java对象由三个部分组成:对象头、实例数据、对齐填充。

对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、 GC分代年龄、锁标识状态、 线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型 (即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。

实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)

对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)

29. 如何判断对象可以被回收? ​

判断对象是否存活一般有两种方式:

引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。

30. JVM的永久代中会发生垃圾回收么? ​

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full

GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区 (注:Java8 中已经移除了永久代,新加了一个叫做元数据区的native内存区)

31. 你知道哪些垃圾收集算法 ​

GC最基础的算法有三种: 标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。

标记 -清除算法, “标记-清除”( Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

复制算法, “复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行

清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

分代收集算法, “分代收集”( Generational Collection)算法,把Java堆分为新生代和老年代, 这样就可以根据各个年代的特点采用最适当的收集算法。

32. Minor GC与Full GC分别在什么时候发生? ​

新生代内存不够用时候发生MGC也叫YGC,JVM内存不够的时候发生FGC

33. 对象一定分配在堆中吗?有没有了解逃逸分析技术? ​

「对象一定分配在堆中吗?」 不一定的,JVM通过「逃逸分析」,那些逃不出方法的对象会在栈上

分配。

「什么是逃逸分析?」

逃逸分析(Escape Analysis),是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。

逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。

「逃逸分析的好处」

栈上分配,可以降低垃圾收集器运行的频率。

同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。

标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈

上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并且GC频率也会减少。

34. 虚拟机为什么使用元空间替换了永久代? ​

「什么是元空间?什么是永久代?为什么用元空间代替永久代?」 我们先回顾一下 「方法区」 吧,看看虚拟机运行时数据内存图,如下:

虚拟机为什么使用元空间替换了永久代?

方法区和堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、 静态变量、即时编译后的代码等数据。

「什么是永久代?它和方法区有什么关系呢?」

如果在HotSpot虚拟机上开发、部署,很多程序员都把方法区称作永久代。可以说方法区是规范,永久代是Hotspot针对该规范进行的实现。在Java7及以前的版本,方法区都是永久代实现的。

「什么是元空间?它和方法区有什么关系呢?」

对于Java8,HotSpots取消了永久代,取而代之的是元空间(Metaspace)。换句话说,就是方法区还是在的,只是实现变了,从永久代变为元空间了。

「为什么使用元空间替换了永久代?」

永久代的方法区,和堆使用的物理内存是连续的。

虚拟机为什么使用元空间替换了永久代?

「永久代」 是通过以下这两个参数配置大小的~

-XX:PremSize:设置永久代的初始大小

-XX:MaxPermSize: 设置永久代的最大值,默认是64M

对于 「永久代」,如果动态生成很多class的话,就很可能出现「java.lang.OutOfMemoryError: PermGen space错误」,因为永久代空间配置有限嘛。最典型的场景是,在web开发比较多jsp页面的时候。

JDK8之后,方法区存在于元空间(Metaspace)。物理内存不再与堆连续,而是直接存在于本地内存中,理论上机器 「内存有多大,元空间就有多大」。

虚拟机为什么使用元空间替换了永久代?

可以通过以下的参数来设置元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC 会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间, 那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比, 减少为分配空间所导致的垃圾收集

-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比, 减少为释放空间所导致的垃圾收集

「所以,为什么使用元空间替换永久代?」

表面上看是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到

OOM错误。当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制啦。

35. 什么是Stop The World ? 什么是OopMap?什么是安全点? ​

进行垃圾回收的过程中,会涉及对象的移动。为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为「Stop The World」。也简称为STW。

在HotSpot中,有个数据结构(映射表)称为「OopMap」。一旦类加载动作完成的时候,

HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。在即时编译过程中,也会在「特定的位置」生成 OopMap,记录下栈上和寄存器里哪些位置是引用。

这些特定的位置主要在:

· 1.循环的末尾(非 counted 循环)

2.方法临返回前 / 调用方法的call指令后

3.可能抛异常的位置

这些位置就叫作「安全点(safepoint)。」 用户程序执行时并非在代码指令流的任意位置都能够在停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。

36. 说一下JVM 的主要组成部分及其作用? ​

说一下JVM 的主要组成部分及其作用?

JVM包含两个子系统和两个组件,分别为

Class loader(类装载子系统)

Execution engine(执行引擎子系统);

Runtime data area(运行时数据区组件)

Native Interface(本地接口组件)。

「Class loader(类装载):」 根据给定的全限定名类名(如:java.lang.Object)来装载class文件到运行时数据区的方法区中。

「Execution engine(执行引擎)」:执行class的指令。

「Native Interface(本地接口):」 与native lib交互,是其它编程语言交互的接口。 「Runtime data area(运行时数据区域)」:即我们常说的JVM的内存。

首先通过编译器把 Java源代码转换成字节码,Class loader(类装载)再把字节码加载到内存

中,将其放在运行时数据区的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

37. 什么是指针碰撞? ​

一般情况下,JVM的对象都放在堆内存中(发生逃逸分析除外)。当类加载检查通过后,Java虚拟机开始为新生对象分配内存。如果Java堆中内存是绝对规整的,所有被使用过的的内存都被放到一边,空闲的内存放到另外一边,中间放着一个指针作为分界点的指示器,所分配内存仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的实例,这种分配方式就是指针碰撞。

什么是指针碰撞?

38. 什么是空闲列表? ​

如果Java堆内存中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,不可以进行指针碰撞啦,虚拟机必须维护一个列表,记录哪些内存是可用的,在分配的时候从列表找到一块大的空间分配给对象实例,并更新列表上的记录,这种分配方式就是空闲列表。

39. 什么是TLAB? ​

可以把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配一小块

内存,这就是TLAB( Thread Local Allocation Buffer,本地线程分配缓存)。虚拟机通过 - XX:UseTLAB 设定它的。

40. 对象头具体都包含哪些内容? ​

在我们常用的Hotspot虚拟机中,对象在内存中布局实际包含3个部分:

  1. 对象头

  2. 实例数据

  3. 对齐填充

而对象头包含两部分内容,Mark Word中的内容会随着锁标志位而发生变化,所以只说存储结构就好了。

  1. 对象自身运行时所需的数据,也被称为Mark Word ,也就是用于轻量级锁和偏向锁的关键点。 具体的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向 锁线程ID、偏向锁时间戳。

  2. 存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实 例。

如果是数组的话,则还包含了数组的长度。

对象头具体都包含哪些内容?

附:一句话速记(可以跳过 ★☆☆) ​

  • Serial / ParNew / Parallel:老收集器,新项目基本不用了,知道名字和特点即可。
  • PermGen:JDK7 及以前的方法区实现,已废弃,现在一律说元空间。