热门八股-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飙高、死锁和常用工具

相关推荐
ly76891 小时前
XML 从入门到实践:语法、命名空间、XPath、XSD 与 Java 安全解析
xml·java·python
studyForMokey1 小时前
【Android 内存管理】JVM 内存模型与 GC 原理
android·jvm
长谷深风1111 小时前
好的 Tool Schema,不是字段越全越好
java·大数据·人工智能·ai agent·agent工作流·智能体设计·ai产品设计
IT 行者1 小时前
用 HiddenHttpMethodFilter 解决浏览器不支持 PUT/DELETE/PATCH 的问题
java·spring
晚安code1 小时前
Redis 内存淘汰策略实战:什么时候淘汰、怎么选、怎么防缓存击穿
java·redis·缓存
goyeer1 小时前
Liunx日志管理与journalctl
java·linux·运维·服务器·运维开发·信息化·信息化企业管理
tianyu2343 小时前
Java 正则表达式终极指南:从 Pattern 到 Matcher,从双重转义到性能优化,一篇全搞定
java·开发语言·正则表达式·group·find·pattern·matcher
Emily156853598705 小时前
Emerson 1C31129G03 Analog Input Module
java·开发语言·前端·plc·emerson·1c31129g03·input module
就叫_这个吧10 小时前
Java递归方法实现面包屑导航
java·开发语言