JVM
阅读提示
建议先看题目目录,再按“概念 -> 原理 -> 场景 -> 优化”顺序复习。
每题先讲结论,再补关键机制和项目实践,回答会更稳。
1、JAVA 四种引用类型(了解)
强引用:
描述:强引用是我们最常见的引用类型,只要一个对象具有强引用,它就不会被垃圾回收器回收。
例子:
Object obj = new Object();在这个例子中,
obj是一个强引用,指向Object的一个实例。只要obj还在使用,这个Object实例就不会被回收。软引用:
描述:软引用是一种内存敏感的引用,如果一个对象只具有软引用,那么在内存空间不足时,垃圾回收器会考虑回收它。
例子:
SoftReference<Object> softRef = new SoftReference<>(new Object());在这个例子中,
softRef是一个软引用,指向一个Object实例。如果内存不足,这个实例可能会被回收。弱引用:
描述:弱引用比软引用更加弱,它不足以阻止对象的垃圾回收。无论当前内存空间足够与否,只要垃圾回收器发现了只具有弱引用的对象,就会回收它。
例子:
WeakReference<Object> weakRef = new WeakReference<>(new Object());在这个例子中,
weakRef是一个弱引用,指向一个Object实例。无论内存空间如何,这个实例都可能被回收。虚引用:
描述:虚引用是所有引用类型中最弱的一个。它无法阻止垃圾回收器的回收过程,也无法通过虚引用来获取对象的实例。它的主要作用是跟踪对象被垃圾回收的状态。
例子:
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>());- 在这个例子中,
phantomRef是一个虚引用,指向一个Object实例,并且关联了一个引用队列。这个实例随时可能被回收,而且回收时,phantomRef会被加入到关联的引用队列中。
2、类加载过程(按图顺序向面试官介绍,细节了解)

- 加载:加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
- 验证 :这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 准备:创建类或接口的静态字段(包括
static final),并赋予类型默认值,如int为0、引用为null;不执行 Java 初始化表达式。这里描述的是 JVMS 的逻辑阶段,不承诺字段的物理位置。
class InitDemo {
static int age = 26;
static final int LIMIT = 6 * 7;
static final String NAME = "IT枫斗者";
static final Integer BOXED = 42;
static final int RUNTIME = Integer.parseInt("42");
}final 只限制再次赋值。按 JLS,只有以常量表达式初始化的基本类型或 String 类型的 final 变量才是常量变量;上例的 LIMIT、NAME 是,BOXED、RUNTIME 不是。Java 编译器通常为前两者生成 ConstantValue 属性,而后两者的赋值要执行 <clinit> 字节码。编译期记录常量值不等于运行时字段已在编译期分配和赋值。
规范顺序可简记为:准备:静态字段默认值 → 初始化过程:ConstantValue 赋值 → 执行 <clinit> 中的赋值与 static 块。JVMS 8 和 21 都将 ConstantValue 的赋值规定为初始化的一部分,且早于调用 <clinit>;不要把“所有 final 都在准备阶段赋最终值”当作规范。HotSpot 可在创建类镜像等较早的内部步骤提前设置常量字段,这是实现细节,不能据此改变规范的阶段定义(可对照 OpenJDK 21 javaClasses.cpp 的 initialize_static_field)。直接读取 InitDemo.LIMIT 通常使用编译进调用方的常量值,不触发 InitDemo 初始化;读取 BOXED 则会触发。
依据:JVMS 8 §5.4.2 / §5.5、JVMS 21 §4.7.2 ConstantValue、JLS 21 §4.12.4。
解析:JVM会在此阶段把类的二进制数据中的符号引用替换为直接引用。
符号引用:符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有 了直接引用,那引用的目标必定已经在内存中存在。
初始化 :按初始化协议处理常量字段后,执行编译器汇集静态字段初始化表达式和静态块生成的
<clinit>()(若存在),例如将age从0赋为26。它不是实例构造器<init>;没有相应初始化代码的类可以没有<clinit>。解析也可以按需延迟,不必全部完成后才初始化。使用:对象都出来了,业务系统直接调用阶段。
卸载 :类的生命周期不等于单个对象的生命周期;只有定义它的类加载器可被回收时,该类才可能卸载,并非业务用完就立即卸载。
3、类加载器种类以及加载范围

参考回答:
上图按 JDK 8 常见实现理解;JDK 9 模块化以后,应区分以下版本边界,不能直接套用扩展目录和 rt.jar。
| 类型 | JDK 8(Oracle/OpenJDK 常见实现) | JDK 9+,包括 JDK 21 |
|---|---|---|
| 启动类加载器 | 加载 rt.jar 等核心运行库;sun.boot.class.path 是当时的实现属性 | 加载 java.base 等关键模块;运行时镜像使用模块化布局,不再有 rt.jar |
| 扩展/平台类加载器 | 扩展加载器通过 java.ext.dirs、jre/lib/ext 加载扩展库 | 扩展机制移除,改为平台类加载器;用 ClassLoader.getPlatformClassLoader() 获取,不再向 lib/ext 放 JAR |
| 应用/系统类加载器 | 通常加载 classpath,父级为扩展加载器;常见实现继承 URLClassLoader | 内建应用加载器父级为平台加载器,不再继承 URLClassLoader;同时参与应用模块的加载 |
| 自定义类加载器 | 继承 ClassLoader,用于容器隔离等场景 | 仍可自定义;需要可见完整平台 API 时,父级通常选平台加载器,不能假定 null 父级可见所有平台类 |
启动加载器在 ClassLoader API 中以 null 表示,所以 Object.class.getClassLoader() 返回 null 不代表未加载,也不能对这个值调用 getParent()。哪些平台类由哪个加载器定义属于实现细节,不能硬编码。自定义系统加载器、模块层和容器也可能改变常见结构。
版本依据:JDK 21 迁移指南 与 ClassLoader API。
4、双亲委派是什么?

参考回答:
ClassLoader.loadClass的默认流程先查已加载的类,再委派父加载器;父级找不到时再调用本加载器的findClass。父级为null时交给启动加载器。这是默认委派流程,不意味着所有自定义加载器、容器和模块加载过程都严格逐层走到启动加载器。- 采用父级委派有助于避免重复定义核心类型。例如
java.lang.Object在 JDK 8 来自rt.jar,在 JDK 9+ 来自java.base模块,通常均由启动加载器定义;这里复用的是同一个Class类型定义,不是所有实例都是同一个 Object 对象。
5、如何破坏双亲委派模型 (了解)
参考回答:
重写loadClass方法
背景:在JDK 1.2之前,Java就已经存在类加载器和ClassLoader抽象类,但双亲委派模型是在JDK 1.2中引入的。为了兼容旧代码,JDK 1.2中的ClassLoader类允许用户重写loadClass方法来实现自定义的类加载逻辑,这导致了双亲委派模型的第一次“被破坏”。
实现方式:通过继承ClassLoader抽象类并重写其loadClass方法,可以在类加载过程中自定义委派逻辑,从而实现打破双亲委派模型。但需要注意的是,JDK官方并不推荐这种方式,因为它可能会破坏Java程序的稳定性和安全性。
使用线程上下文类加载器(Thread Context ClassLoader)
背景:在某些情况下,顶层类加载器(如启动类加载器)加载的类可能需要调用由用户自定义类加载器加载的类。例如,JDK 8 常见实现中的 JDBC 接口
java.sql.Driver由启动加载器定义;JDK 9+ 的java.sql通常由平台加载器定义。数据库厂商提供的驱动则常由应用或容器加载器加载,不能把接口的定义加载器写成所有版本都固定为启动加载器。为了解决这种跨类加载器的调用问题,Java引入了线程上下文类加载器。实现方式:通过java.lang.Thread类的setContextClassLoader()方法可以设置当前线程的上下文类加载器。当需要加载某些特定类时,可以使用线程上下文类加载器来加载,而不是遵循双亲委派模型。这种方式在Java SPI(服务提供接口)机制中得到了广泛应用。
其他框架和中间件的实现
在一些特定的框架和中间件中(如Tomcat、OSGi等),为了实现特定的功能(如热部署、模块化等),它们也采用了打破双亲委派模型的方式。这些框架和中间件通常会对类加载器进行扩展和定制,以满足其特殊需求。
6、JVM内存结构


参考回答:
程序计数器(线程私有)
一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。
虚拟机栈(线程私有)

是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
本地方法区(线程私有)
本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务, 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。
堆(Heap-线程共享)-运行时数据区
是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。堆的分代布局取决于收集器;以下是 HotSpot 常见分代收集器的简化模型,并非 JVMS 要求: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。
方法区/永久代(线程共享)
方法区是 JVMS 的逻辑区域,用于类结构、方法数据和运行时常量池等;永久代只是旧版 HotSpot 的一种实现,不能与方法区画等号。HotSpot JDK 8/21 使用本地内存中的 Metaspace 保存类元数据,静态字段存储随堆中的类镜像对象,字符串对象也在堆中,JIT 机器码有独立 Code Cache。
运行时常量
运行时常量池由每个类或接口的 Class 文件常量池构建,包含字面量相关条目、符号引用等,并不是“所有 static final 字段或字符串对象的存放区”。方法区及其常量池的逻辑定义与物理布局应分开理解,见 JVMS 21 §2.5 与 JDK 21 类元数据说明。
7、判断对象是否能被回收的算法
参考回答:
引用计数法
给对象添加一个引用计数器,每当有一个地方引用他的时候该计数器的值就+1,当引用失效的时候该计数器的值就-1;当计数器的值为0的时候,jvm判定此对象为垃圾对象。存在内存泄漏的bug,比如循环引用的时候,所以jvm虚拟机采取的是可达性分析法。
可达性分析法
有一些根节点GC Roots作为对象起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的时候,则证明此对象为垃圾对象。
补充:哪些可作为GC Roots?
虚拟机栈中的引用的对象
方法区中的类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(native方法)引用的对象
8、JVM 运行时内存
参考回答:

以下 Eden/双 Survivor 模型适用于 HotSpot JDK 8/21 的 Serial、Parallel 等传统分代收集器;G1 使用动态分代的 Region,不能照搬固定连续分区。传统模型可细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。
其中新生代(Young) 被分为 Eden和S0(from)和S1(to)。 常见示意比例为 Eden : from : to = 8 : 1 : 1,
-XX:SurvivorRatio=8表示 Eden 与单个 Survivor 的比值;自适应调整会改变实际大小,不能视为所有收集器固定比例。新生代
是用来存放新生的对象。大小由收集器、参数及自适应策略决定,并不固定占堆的 1/3。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。
Eden 区:Java 新对象的出生地(大对象的分配策略依收集器而定)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
ServivorFrom:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
ServivorTo:保留了一次 MinorGC 过程中的幸存者。
老年代
主要存放应用程序中生命周期长的内存对象。
老年代回收的触发条件和算法取决于收集器;不能把 Major GC、Full GC 混用,也不能认为每次老年代回收前都必有 Minor GC。CMS 主要标记清除,Parallel Old 使用压缩,G1 通过 Mixed GC 回收部分老年代 Region,必要时才回退到 Full GC。具体以所用版本和 GC 日志的事件名称为准。
垃圾回收后仍无法满足分配需求才可能抛出 OOM;“老年代已满”并不等于立刻 OOM。
元空间
Metaspace 是 HotSpot 自 JDK 8 起替代永久代的类元数据存储实现,使用本地内存;它仍可能因本地内存不足、类加载器泄漏或容量上限而 OOM。类卸载后才有机会回收相应元数据。
-XX:MetaspaceSize是触发元数据 GC 的初始高水位,后续可调整,并非预分配的元空间初始容量;-XX:MaxMetaspaceSize限制最大容量。见 JDK 21 GC 指南:类元数据。
9、MinorGC 的过程(复制->清空->互换,新生代)
参考回答:
- 下述是传统分代收集器年轻代复制回收的简化流程;并非所有 GC 的统一过程。G1 同样复制疏散对象,但以 Region 为单位。
- *eden、servicorFrom 复制到 ServicorTo,年龄+1:*首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);
- 清空 eden、servicorFrom :*然后,清空 Eden 和 ServicorFrom 中的对象;*
- *ServicorTo 和 ServicorFrom 互换:*最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom区。
10、为什么把堆栈分成两个?(了解)
参考回答:
- 栈代表了处理逻辑,堆代表了存储数据,分开后逻辑更清晰,面向对象模块化思想。栈是线程私有,堆是线程共享区,这样分开也节省了空间,比如多个栈中的地址指向同一块堆内存中的对象。
- 栈是运行时的需要,比如方法执行到结束,栈帧按调用关系入栈和出栈,而堆对象可以跨越创建它的方法调用存活;栈的地址增长方向由实现决定,对象本身的大小也不会任意动态增长。
11、简述下对象的分配规则(了解)
参考回答:
- 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次YGC。并将还活着的对象放到from/to区,若本次YGC后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
- 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
- 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次YGC那么对象会进入Survivor区,之后每经过一次YGC那么对象的年龄加1,直到达到阀值对象进入老年区。年龄阈值依收集器和自适应策略而定,不能统一写成默认 15。可以通过 -XX:MaxTenuringThreshold 参数来设置。
- 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一 半,年龄大于或等于该年龄的对象可以直接进入老年代。无需等到 -XX:MaxTenuringThreshold 参数要求的年龄。
- 空间分配担保。年轻代疏散需要足够的 Survivor 或老年代空间;空间不足可能引发晋升失败或后续 Full GC,取决于收集器。
HandlePromotionFailure是早期 HotSpot 的历史参数,不应作为 JDK 8/21 的通用调优开关。对象年龄、大小及存活量都是决策因素,不能仅用一个固定阈值预测晋升。参见 JDK 8 分代说明。
12、CMS 收集器(多线程标记清除算法,老年代)
参考回答:
版本范围:本节描述 HotSpot JDK 8 中的历史 CMS;CMS 在 JDK 9 被弃用、JDK 14 被移除,JDK 21 不能使用 -XX:+UseConcMarkSweepGC。见 JDK 8 CMS 指南、JEP 291 和 JDK 21 迁移指南:JDK 14 移除 CMS。

- CMS 工作机制相比其他的垃圾收集器来说更复杂,可用以下 4 个主要阶段概括(实际日志还可能包含预清理、并发重置等阶段):
- 初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
- 并发标记 :进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
- 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
- 并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
所以CMS的优点是:
- 并发高
- 停顿低
- STW时间短。
缺点:
- 对cpu资源非常敏感(并发阶段不要求全程暂停用户线程,但会一起占用CPU资源,竞争激烈的话会导致程序变慢)。
- 无法处理浮动垃圾,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,失败后而导致另一次Full GC的产生,由于CMS并发清除阶段用户线程还在运行,伴随程序的运行自然会有新的垃圾产生,这一部分垃圾是出现在标记过程之后的,CMS无法在本次去处理他们,所以只好留在下一次GC时候将其清理掉。
- 内存碎片问题(因为是标记清除算法)。当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
13、三色标记算法(了解)
这个算法基于图论中的图染色问题,通过给对象标记三种不同的颜色来表示其垃圾回收的状态。
这三种颜色是:
- 白色: 表示对象还没有被访问过,即未被扫描和标记。
- 灰色: 表示对象已被发现,但尚未完成其出边(引用字段)的扫描。
- 黑色: 表示对象及其引用对象都已经被扫描和标记。
三色是标记状态的抽象模型,不要求 JVM 给对象实际保存三种颜色。基本过程是把根可达对象加入灰色工作集,取出灰色对象扫描其引用,将尚未访问的目标变灰,扫描完当前对象后将它变黑;工作集处理完且并发修改得到正确处理后,剩余白色对象才可判为本轮未标记对象。
并发标记期间,用户线程会改变引用;若只扫描一次,可能漏掉仍然存活的对象。因此收集器用写屏障及后续处理维护正确性。例如 HotSpot JDK 8/21 的 G1 使用 SATB 保存被覆盖的旧引用,Remark 阶段处理剩余标记工作,而不是简单“重新扫描全部存活对象”。具体阶段依收集器而异,三色模型本身不规定必须并发或必须有重新标记阶段。见 JDK 8 G1 指南。
14、G1垃圾回收器(了解)
G1(Garbage First)在 JDK 7u4 达到正式支持状态(更早有实验实现);JDK 9 起成为服务器配置下的默认收集器,JDK 21 常见 HotSpot 配置也默认使用 G1。JDK 8 可用 -XX:+UseG1GC 开启。默认选择仍应结合实际 VM 与启动参数确认。历史版本见 Oracle JDK 7 G1 说明。
在G1之前的垃圾回收器,如Parallel Scavenge、Parallel Old、CMS等,主要针对Java堆内存中的特定部分(新生代或老年代)进行操作。然而,G1将Java堆内存划分为多个大小相等的区域(Region),并根据每个区域中垃圾对象的数量和大小来优先进行垃圾回收;G1这种基于Region回收的方式,可以预测停顿时间。
G1会根据每个Region里面垃圾“价值”的大小,在后台维护一个优先级列表,每次根据用户设定的允许收集停顿的时间 (-XX:MaxGCPauseMillis,G1 默认目标为 200 毫秒)优先处理价值收益最大的Region。
这是软目标,既不是定时触发间隔,也不是最大停顿的硬保证。G1 的 Young GC 回收年轻代,Mixed GC 同时选择部分老年代 Region,并不是每次都任意挑选全堆最“划算”的 Region。见 JDK 21 G1 指南。
15、CMS垃圾回收器和G1垃圾回收器对比(了解)
本节比较历史 HotSpot CMS(如 JDK 8)与 JDK 8/21 的 G1;JDK 21 已无 CMS。
CMS 主要采用标记清除,容易产生碎片;G1 在疏散阶段复制存活对象到其他 Region,能够整理所回收区域,但并非完全没有碎片。特别是巨型对象需要连续 Region,仍可能因连续空间不足触发回收,甚至 Full GC。
G1 使用 Remembered Set 等结构追踪跨 Region 引用,并通过写前屏障支持 SATB 并发标记、写后屏障维护跨区域引用信息。这些机制需要 CPU 和内存;实际开销应按工作负载测量,不能无条件断言比 CMS 更高。见 JDK 21 G1 内部机制。
16、大型项目如何进行性能瓶颈调优(了解)
参考回答:
- 数据库与SQL优化:一般dba负责数据库优化,比如集群主从等。研发负责SQL优化,比如索引、分库分表等。
- 集群优化:一般OP负责,让整个集群可以很容易的水平扩容,再比如tomcat/nginx的一些配置优化等。
- 硬件升级:选择最合适的硬件,充分利用资源。
- 代码优化:很多细节,可以参照阿里巴巴规范手册和安装sonar插件这种检测代码质量的工具。也可以适当的运用并行,比如CountDownLatch等工具。
- jvm优化:内存区域大小设置、对象年龄达到次数晋升老年代参数的调整、选择合适的垃圾收集器以及合适的垃圾收集器参数、打印详细的GC日志和oom的时候自动生成dump。
- 操作系统优化
17、系统内存飙高,如何查找问题?
参考回答:
- 找出哪个进程内存占用高(top命令)
- 查看jvm进程号(jps命令)
- 导出堆内存 (jmap命令生成dump文件,注意:线上系统,内存特别大,jmap执行期间会对进程产生很大影响,甚至卡顿,所以操作前最好先从负载均衡里摘掉。)
- 分析dump文件 (比如mat软件)
18、常用的调优工具有哪些?(了解)
参考回答:
- JDK内置的命令行:jps(查看jvm进程信息)、jstat(监视jvm运行状态的,比如gc情况、jvm内存情况、类加载情况等)、jinfo(查看jvm参数的,也可动态调整)、jmap(生成dump文件的,在dump的时候会影响线上服务)、jhat(仅旧版 JDK,JDK 9 已移除;分析 dump 可用 MAT)、jstack(查看线程的)。
- 可视化工具:JConsole;VisualVM 在 JDK 8 的部分发行版随附,JDK 9+ 不再随 JDK 分发,需要单独安装。工具版本变化见 Oracle 迁移指南。
- 阿里巴巴开源的arthas:神器,线上调优很方便,安装和显示效果都很友好。
19、GC常用参数(了解)
参考回答:
堆设置:
-Xms:设置堆的初始大小。
-Xmx:设置堆的最大大小。
栈设置:
-Xss:设置每个线程的栈大小。
垃圾回收器设置:
-XX:+UseG1GC:使用 G1 垃圾回收器。
-XX:+UseParallelGC:使用并行垃圾回收器。
性能调优:
-XX:PermSize 和 -XX:MaxPermSize:在 Java 8 之前设置永久代的初始大小和最大大小。
-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize:在 Java 8 及以上版本设置元数据 GC 的初始高水位和 Metaspace 最大容量。
JDK 8:
-XX:+PrintGCDetails打印详细 GC 日志;JDK 9+(含 21)使用统一日志,如-Xlog:gc*。调试和分析:
-verbose:gc:输出 GC 概要;详细程度不能等同于-Xlog:gc*。参数语义见 JDK 21 java 命令。-XX:+HeapDumpOnOutOfMemoryError:在内存溢出时生成堆转储。
20、YoungGC和FullGC的触发条件是什么?(了解)
参考回答:
Young GC、Mixed GC、并发标记周期和 Full GC 应按收集器区分,以下针对 HotSpot JDK 8/21 常见实现:
- Young GC:常因 Eden 分配空间不足或达到收集器的年轻代分配目标而启动。
-XX:MaxGCPauseMillis是停顿目标,不是 GC 时间间隔;频繁 Young GC 本身不是固定的 Full GC 触发条件。 - 老年代回收:CMS 的并发周期、G1 的并发标记和 Mixed GC 都不等于 Full GC。G1 的启动阈值还可自适应,不存在通用“老年代达到 92% 就 Full GC”的规则。
- Full GC:可能由分配/晋升或疏散失败后的回退、元数据分配压力、显式 GC 请求等引起;具体行为受收集器和参数影响。
System.gc()也只提出请求,不能保证立即 Full GC。 - 版本边界:PermGen 空间压力属于 JDK 7 及以前的 HotSpot;JDK 8/21 应检查 Metaspace、类加载器和类卸载。回收后仍不能满足分配时才可能 OOM。
依据:JDK 8 CMS 的并发失败与周期触发、JDK 21 G1 回收周期、显式 GC 与类元数据。
21、Java中的类什么时候会被加载?(了解)
参考回答:
加载与初始化不同。JDK 8/21 都允许 JVM 提前加载类;以下主动使用要求相关类型在使用前完成初始化:创建实例、调用其声明的静态方法、读写其声明的非常量静态字段等。初始化类前还须初始化父类及规范要求的、声明默认方法的超接口。
Class.forName(name) 通常会初始化,Class.forName(name, false, loader) 和 ClassLoader.loadClass(name) 不要求初始化。读取编译期常量、获取 SomeType.class 或创建 SomeType[] 不会仅因此初始化 SomeType。静态块在初始化时执行,不能写成“一加载就执行”;通过子类名访问继承的静态字段只初始化实际声明该字段的类。见 JLS 21 §12.4.1。
22、为什么基本类型不放在堆里(了解)
参考回答:
基本类型并非都放在栈上。方法中的基本类型局部变量属于栈帧局部变量表;对象的基本类型实例字段和基本类型数组元素随对象在堆上;静态字段属于类状态。JIT 还可能将值放入寄存器或消除分配,因此不能用“大小固定”解释存储位置。见 JVMS 21 §2.5 / §2.6。
23、JVM调优(先了解,后面需要结合项目)
参考回答:
JVM的调优是为了优化应用程序的性能、资源利用率以及响应时间。调优的目标包括减小垃圾回收停顿时间、提高吞吐量、降低内存消耗等。实际中,要根据业务的实际情况去调整和优化。
- 使用JVM监控工具,如VisualVM、JConsole、Mission Control等,对应用程序进行性能分析。通过查看内存使用情况、垃圾回收统计、线程活动等信息,找到性能瓶颈和潜在问题。
- 启用GC日志并分析日志文件。GC日志包含了垃圾回收的详细信息,可以了解各种垃圾回收事件的发生时间、停顿时间、频率等。通过分析GC日志,可以确定是否需要调整垃圾回收器的类型、内存大小等参数。
- 在应用程序出现内存泄漏或者内存占用过高时,获取Heap Dump并分析。Heap Dump是一个内存快照,可以帮助你查找对象的引用链,找到内存泄漏的根本原因。
- 调整堆大小: 根据性能分析的结果,调整堆内存的大小。可以通过调整
-Xms和-Xmx参数来设置堆的初始大小和最大大小,以及通过-Xmn参数来设置新生代大小。 - 选择垃圾回收器: 根据应用程序的特性,选择合适的垃圾回收器。例如,对于响应时间敏感的应用,可以选择G1垃圾回收器。
- 调整垃圾回收器参数: 根据GC日志的分析,调整垃圾回收器的相关参数。例如,可以通过
-XX:MaxGCPauseMillis设置垃圾回收停顿时间的软目标,通过-XX:GCTimeRatio设置垃圾回收时间与应用执行时间的比率等。 - 代码优化: 通过代码审查和性能测试,对应用程序进行优化,避免不必要的资源消耗和垃圾回收压力。特别注意避免创建过多的临时对象。
版本迁移练习
按 JDK 8 到 21 类加载器排查 运行探针,区分 null、平台加载器、jrt: 与旧代码强转问题;到 Java 面试资料 保存自己的版本对照。
