热门八股-JVM

内存结构

1.介绍一下JVM内存模型

JVM内存模型通常指JVM运行时数据区

主要包括:

  • 程序计数器:记录当前线程执行到哪条字节码指令
  • Java虚拟机栈:每个线程私有,方法调用时创建栈帧
  • 本地方法栈:给native方法使用
  • 堆:线程共享,主要存放对象实例,是GC管理的重点区域
  • 方法区:线程共享,存放类元信息、常量、静态变量、JIT编译后的代码
  • 运行时常量池:方法区的一部分,存放字面量和符合引用
  • 直接内存:不属于JVM运行时数据区,但经常被NIO使用,可能OOM。

2.String s = new String("abc")执行过程中分别对应哪些内存区域?

分布在3处内存区域:堆、字符串常量池、虚拟机栈

  1. "abc"是字符串字面量,会放入字符串常量池
  2. new String("abc"):执行new关键字,在堆中创建一个新的String对象
  3. s局部变量,存在当前方法的虚拟机栈栈帧里
  4. s最终指向堆里新建的String对象

创建几个对象:

如果常量池之前没有"abc",可能会创建两个对象:常量池里的"abc"和堆里的new String对象

如果常量池之前有"abc",只会在堆里新建一个String对象

3.堆为什么分新生代和老年代?比例默认是多少

堆分代是因为大多数对象都有"朝生夕死"的特点。很多对象创建后很快就不用了,少数对象会存活很久。

如果把所有对象混在一起回收,每次GC都扫描整个堆,成本很高。

  • 新生代存放新创建、生命周期短的对象
  • 老年代存放存活时间长、体积较大的对象

常见默认内存比例:

新生代:老年代=1:2

新生代内部划分:Eden区:Survivor0:Survivor1=8:1:1

4.Eden、From、To区作用?为什么会有两个Survivor

新生代一般分为Eden、From Survivor、ToSurvivor。

大多数对象先在Eden区分配。Minor GC时,Eden和当前From区中存活的对象会被复制到To区,然后From和To角色互换

两个Survivor的作用:

  • 配合复制算法使用
  • 让存活对象在两个Survivor之间来回复制
  • 记录对象年龄,达到阈值后晋升老年代

为什么不是一个Survivor

复制算法需要一个空区域作为目标空间。如果只有一个Survivor,清理和复制会混在一起,不好保证空间连续和回收效率

为什么不是多个Survivor

两个已经足够完成复制和年龄判断,更多会浪费空间

5.方法区/元空间作用?存放什么?

方法区是JVM规范中的概念,用来存放类相关的信息。

通常存放:

  • 类的元信息,比如类名、父类、接口、字段、方法
  • 运行时常量池
  • 静态变量
  • JIT编译后的代码缓存

JDK 8 以后,HotSpot用元空间Metaspace实现方法区。元空间使用的是本地内存,不再使用永久代

6.永久代和元空间的区别?JDK 8为什么废除永久代改用元空间

永久代PermGen是JDK 8之前HotSpot对方法区的实现,使用JVM堆内存的一部分

元空间Metaspace是JDK 8之后的方法区实现,使用本地内存

为什么废除永久代:

  • 永久代大小不好设置,容易出现OutOfMemoryError:PermGen space
  • 类元信息放在本地内存中更加灵活,受限更少
  • 方便HotSpot与其他JVM实现统一方法区概念
  • 字符串常量池等内容逐步移到堆中,永久代的职责变的不合适。

主要元空间不是无限的,也可能OOM,可以通过-XX:MaxMetaspace限制大小

7.直接内存是什么?会不会OOM?

直接内存Direct Memory也叫堆外内存,不属于JVM运行时数据区,是操作系统本地内存

Java NIO的ByteBuffer,allocateDirect()会使用直接内存,它减少了Java堆和native内存之间的数据拷贝,提升IO读写性能,适合IO场景。

直接内存也会OOM

OutOfMemoryError:Direct buffer memory

  • 大量direct buffer没有被释放
  • Netty等框架使用直接内存过多
  • -XX:MaxDirectMemorySize设置太小或没有规范

类加载机制

1.类加载的五步

类加载通常分为五个阶段:

  1. 加载 Loading:通过类全限定名获取字节码,生成Class对象
  2. 验证 Verification:检查字节码是否合法,防止破坏JVM
  3. 准备 Preparation:静态变量分配内存,赋默认零值
  4. 解析 Resolution:把符号引用替换成直接引用
  5. 初始化 Initialization:执行类构造器<clinit>(),给静态变量赋真实值,执行静态代码块

2.什么是双亲委派机制?

双亲委派机制是类加载器加载类时的一种委派规则

当一个类加载器收到类加载请求时,不会自己先加载,而是先把请求交给父类加载器。父类加载器还会继续向上委派。只有父类加载器加载不了,子类加载器才会尝试自己加载

它的核心思想是:先让上层加载器加载基础类,避免核心类被随意替换

双亲委派机制的优点:

  • 保证类的唯一性:确保所有请求都能传到启动类加载器,避免了不同类加载器重复加载相同类的情况
  • 保证了安全性:由于Java的核心类库在启动类加载器中加载,而启动类加载器只加载信任路径中的类,这样可以防止不可信的类假冒核心类
  • 实现了类的复用:核心类只需要加载一次,所有子加载器复用,减少内存消耗

3.双亲委派机制的缺点是什么?

主要缺点:

  • 不够灵活,父加载器无法使用子加载器加载的类
  • 不适合某些插件化、热部署、模块隔离场景
  • SPI场景下,Java核心类需要加载第三方实现类,双亲委派机制会受限制

比如JDBC中DriverManager是核心类库里的类,但具体数据库驱动由应用提供,所以需要线程上下文类加载器来解决

4.如何打破双亲委派机制?

打破双亲委派机制通常是自定义类加载器,重写loadClass()或调整加载顺序

默认情况下,loadClass()实现了双亲委派。如果只重写findClass(),一般仍然遵循双亲委派。

要打破它,通常会在loadClass()中改成自己先加载,加载不了再交给父加载器。

典型场景:

  • Tomcat Web应用隔离
  • OSGi模块化
  • 插件系统
  • 热部署框架

不要随意打破双亲委派机制,可能导致类冲突、安全问题和类型转换异常。

5.如何实现自定义类加载器?

常见做法是继承ClassLoader,重写findClass()。

垃圾回收机制

1.如何判断对象是否存活?可达性分析?引用计数?

判断对象是否存活主要有两种思路:引用计数法和可达性分析法

  • 引用计数法:每个对象维护一个计数器,有引用指向它就+1,引用失效就-1,计数为0就可以回收。

缺点是无法解决循环引用。比如对象A引用B,B引用A,但它们都不被外部访问,计数仍然不为0.

Java主流JVM使用可达性分析法

  • 可达性分析法:从GC Roots出发,沿引用链向下搜索。可以被GC Roots直接或间接到达的对象是存活对象,无法到达的对象可以被回收。

2.GC Roots有哪些?

常见的包括:

  • 虚拟机栈中引用的对象。比如局部变量
  • 本地机栈中JNI引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 被synchronized持有的对象
  • JVM内部引用,比如系统类加载器、基本数据类型Class对象。

3.三大垃圾回收算法以及各自的优缺点

  • 标记-清除:先标记存活对象,再清楚未标记对象

优点:实现简单,不需要移动对象。缺点:会产生内存碎片,回收效率不稳定

  • 标记-复制:标记存活对象,将所有存活对象复制到另一块空闲内存区域,原来整块区域全部清空

优点:没有内存碎片,分配块。缺点:浪费一部分空间,存活对象多时复制成本高

  • 标记-整理:标记存活对象,再把存活对象向一端移动,然后清理边界外空间

优点:没有碎片,适合老年代。缺点:移动对象成本较高

4.为什么新生代使用复制算法,而老年代使用标记整理

新生代对象大多数朝生夕死,存活对象少。使用复制算法只需要复制少量存活对象,效率很高。

老年代对象存活率高,如果用复制算法,要复制大量对象,成本很高而且还要额外保留一大块空闲区域

5.新生代中的对象符合哪些条件之后会晋升到老年代

常见情况:

  • 对象年龄达到阈值,默认最大常见值是15,因为对象头中年龄字段通常是4bit
  • 大对象直接进入老年代,比如很大的数组或字符串
  • Survivor空间放不下,存活对象会提前晋升
  • 动态年龄判断:某个年龄及以上对象总大小超过Survivor一半时,这些对象可能直接晋升

6.什么是Minor GC、Major GC、Full GC?触发条件?

Minor GC:回收新生代。通常在Eden区空间不足时触发

Major GC:通常指回收老年代

Full GC:回收整个Java堆和方法区相关区域,停顿时间通常更长

常见Full GC触发条件:

  • 老年代空间不足
  • 方法区或元空间不足
  • 调用System.gc(),不一定立刻执行,但可能触发
  • Minor GC前判断老年代担保空间不足
  • 大对象分配失败

面试重点说:Minor GC比较频繁,Full GC成本高,线上尽量减少频繁Full GC。

7.什么是STW?

STW是Stop The World,意思是垃圾回收时暂停用户线程

GC过程中,为了保证对象引用关系不再乱变,JVM可能会暂停所有业务线程,只让GC线程工作

STW影响接口响应变慢、系统短暂停顿。停顿时间越长,用户感知越明显

现代垃圾收集器如G1,ZGC,Shenandoah都在尽量降低STW时间

8.常见的垃圾收集器有哪些?

  • Serial:单线程收集器,适合客户端或小内存场景
  • ParNew:Serial的多线程版本,常与CMS搭配
  • Parallel Scavenage:关注吞吐量,适合后台计算机任务
  • Serial Old:老年代单线程收集器
  • Parallel Old:Parallel Scavenage的老年代版本
  • CMS:低停顿老年代收集器,使用标记-清楚,可能产生碎片
  • G1:面向服务端应用,按Region管理堆,兼顾吞吐和低停顿
  • ZGC:超低延迟收集器。适合大内存低停顿场景
  • Shenandoah:低停顿收集器,目标类似ZGC

9.什么是三色标记法?

三色标记法是并发标记中最常用的对象标记思想

它把对象分为三种颜色:

  • 白色:还没被访问,最终仍是白色可能被回收
  • 灰色:已经被访问,但它引用的对象还没被扫描完
  • 黑色:已经被访问,并且它引用的对象也扫描完了

GC从GC Root开始,把对象从白变灰,再从灰变黑,逐步完成标记

并发标记时,用户线程还在运行,引用关系可能变化,所以需要写屏障和读屏障等机制保证标记正确。

JVM实战调优

1.常见的OOM有哪几种?分别什么原因?

常见的OOM包括:

  • 堆内存溢出:java.lang.OutOfMemoryError:Java heap space

原因通常是对象太多、缓存无界、集合持续增长,内存泄漏

  • 元空间溢出:java.lang.OutOfMemoryError:Metaspace

原因通常是动态生成类太多,比如CGLIB、反射代理、热部署类加载器泄漏

  • 直接内存溢出::java.lang.OutOfMemoryError:Direct buffer memory

原因通常是NIO、Netty直接内存使用过多或释放不及时

  • 栈溢出::java.lang.StackOverflowError:

原因通常是递归太深、方法调用链太长、栈帧过大

2.常见JVM参数:-Xms、-Xmx、-Xss、-XX:MetaspaceSize含义?

  • -Xms:堆初始大小
  • -Xmx:堆最大大小

生产中常把-Xms、-Xmx设置成一样,避免运行时频繁扩缩容

  • -Xss:每个线程的栈大小。设置太大,能创建的线程数变少;设置太小,容易栈溢出
  • -XX:MetaspaceSize:元空间初始触发GC的阈值,不是最大值
  • -XX:MaxMetaspaceSize:元空间最大值
  • -XX:MaxDirectMemorySize:直接内存最大值
  • -XX:+HeapDumpOnOutOfMemoryError:OOM时自动导出堆dump,线上排查很有用。

3.如何线上排查OOM?完整排查流程?

  1. 先看错误类型,是heap、metaspace、direct memory还是stack
  2. 查看应用日志,确认OOM发生时间和业务操作
  3. 确认JVM参数,比如堆大小,元空间大小,GC日志配置
  4. 如果是堆OOM,获取heap dump
  5. 用MAT、VisualVM、JProfiler等工具分析dump
  6. 查看大对象、对象数量、GC Roots引用链
  7. 判断是内存泄漏还是瞬时流量导致内存不够
  8. 修复代码或调整参数,再压测验证

注意:生产dump可能很大,抓取和拷贝要注意磁盘空间和业务影响

4.如何排查频繁Full GC

频繁Full GC常见原因:

  • 老年代空间不足
  • 大对象频繁进入老年代
  • 内存泄漏,对象回收不掉
  • 元空间不足
  • 显式调用System.gc()
  • 新生代太小,导致对象过早晋升

排查流程:

  1. 查看GC日志,确认Full GC频率、耗时、回收前后内存变化
  2. 如果Full GC后老年代下降明显,可能是内存压力大或参数不合理
  3. 如果Full GC后老年代几乎不下降,重点怀疑内存泄漏
  4. 用jstat观察各区域变化
  5. 用jmap导出dump分析大对象和引用链
  6. 检查是否有大缓存。无界队列、大集合、ThreadLocal未清理

调优方向通常是先定位对象来源,再考虑调整堆、新生代比例或更换GC

5.什么的内存泄漏?和内存溢出的区别?

内存泄漏是指对象已经不在被业务需要,但仍然被引用,导致GC无法回收

内存溢出是指程序申请内存时,JVM没有足够内存可以分配,于是抛出OOM

内存泄漏持续发生可能导致内存溢出

常见内存泄漏场景:

  • 静态集合一直添加不清理
  • ThreadLocal用完不remove
  • 监听器、回调未注销
  • 连接、文本句柄没关闭
  • 本地缓存没有容量和过期限制

6.线上CPU飙高、线程死锁怎么排查?

CPU飙高排查流程:

  1. 用top找到CPU高的Java进程pid
  2. 用top -Hp pid找到CPU高的线程tid
  3. 把tid转成16进制
  4. 用jstack pid导出线程栈
  5. 在栈里搜索16进制线程id,定位具体代码

线程死锁排查:

  • 使用jstack pid,它通常会直接显示deadlock信息
  • 查看哪些线程持有哪些锁、等待哪些锁
  • 根据堆栈定位代码中的锁顺序问题

常见修复方式:

  • 统一加锁顺序
  • 减少锁粒度
  • 避免嵌套锁
  • 使用tryLock超时退出

7.jps、jstack、jmap、jhat、jstat常用命令作用

  • jps:查看当前机器上的Java进程
  • jstack:查看线程栈,排查死锁、线程阻塞、CPU飙高
  • jmap:查看堆信息,导出heap dump
  • jhat:分析heap dump的老工具,现在不太推荐,更多用MAT、VisualVM.
  • jstat:查看GC、类加载、JIT等运行时统计

面试重点:CPU高看jstack,堆问题看jmap,GC趋势看jstat

8.堆dump怎么抓取,怎么分析?

常见heap dump抓取方式:

OOM自动抓取:

复制代码
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump

手动抓:

复制代码
jmap -dump:format=b,file=heap.hprof <pid>

也可以

复制代码
jcmd <pid> GC.heap_dump heap.hprof

分析思路:

  1. 用MAT或VisualVM打开dump
  2. 看占用内存最大的对象
  3. 看Dominator Tree
  4. 找GC Roots引用链
  5. 判断对象为什么没有被回收
  6. 回到代码里定位集合、缓存、ThreadLocal、类加载器等来源

注意:线上抓取dump可能导致短暂停顿,文件也可能很大,操作前要确认磁盘空间和业务影响

总结

  • JVM运行时数据区包括程序计数器、虚拟机栈、本地方法栈、堆、方法区,直接内存也常被问到
  • 堆分代是因为大多数对象生命周期短,新生代用复制算法,老年代更适合标记整理
  • 两个Survivor是为了配合复制算法,一个作为来源,一个作为目标
  • JDK 8 后永久代被元空间替代,元空间使用本地内存
  • 类加载的五步是加载、验证、准备、解析、初始化
  • 双亲委派是先交给父加载器加载,父加载不了子加载器再加载
  • Java判断对象存活注意靠可达性分析,而不是引用计数
  • GC Roots常见有栈引用、静态变量、常量、JNI引用、锁持有对象
  • STW是暂停用户线程,现代GC都在减少STW时间
  • OOM排查先看类型,再抓日志。GC、dump,最后分析引用链

内存结构:知道每块区域存什么,是否线程共享、会不会OOM

类加载:掌握五步流程、双亲委派、自定义类加载器

垃圾回收:理解对象存活判断、GC Roots、回收算法、分代收集、STW

实战调优:会看OOM、Full GC、CPU飙高、死锁和常用工具

相关推荐
金銀銅鐵5 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
福建佰胜张工5 小时前
SMC 薄型气爪 MHF2-16D1 深度解析:平行开闭、低剖面结构在自动化装配中的选型与落地
java·服务器·安全·自动化
catoop5 小时前
API Key 设计与安全生成方案:结构拆解、数据库设计与 Java 实现
java·数据库·安全
system_error_x6 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
驭渊的小故事6 小时前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客6 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1887 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn7 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su8 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
我命由我123459 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法