JVM突击八股文

JVM学习指南


JVM 一般就垃圾回收 + 内存运行区域问的比较多,其他的问的很少,所以其实复习起来也很快,然后垃圾回收器的话,主要关注 G1 和 CMS 这两个就可以了,主要就是问这两个。

需要掌握的知识概括:

一、垃圾回收考点总结

1、掌握几种垃圾回收算法(标记-清除、标记-复制、标记-整理)

2、知道如何判断一个对象是否死亡(引用计数 + 可达性分析)

3、知道哪些可以作为 GCROOT

4、掌握几种常见垃圾回收器(重点是 CMS + G1)

5、掌握老年代 GC 与年轻代 GC,比如什么时候会发生

6、了解一两个工具,比如 jstack

7、掌握一些 GC 的配置参数

一、其他考点总结

1、JVM 内存空间分布(能够举例子)

2、JVM 进程与线程模型

3、JAVA类加载过程

4、能够说出几个常见的类加载器

5、栈和堆的区别,以及什么时候会栈溢出、堆内存溢出

6、知道常量池

7、双亲委派原则

8、了解对象头的一些属性

参考学习文章以及资料


我会在对应的面试题那里,补充对应的文章,专栏,视频和书籍,是一个持续补充的过程,大家有看到好的文章也可以发我,然后我也会给大家推荐对应的书籍 + 咱们训练营的专栏,作为一个进阶补充,有时间你就都看。

系统资料推荐:JVM 资料的话,两份即可,入门看 图解JVM学习指南,之后有时间看 《深入理解Java 虚拟机》阅读及其重点章节指南,基本足以应付 JVM。

正文

【JVM专题】垃圾回收基本问题🌟🌟🌟🌟🌟


PS:基本问题,侧重理解,一定要理解,否则很容易搞混淆。

1、为什么要有垃圾回收?

2、Java 垃圾回收中是如何判断一个对象死亡的?请简单介绍一下

3、刚才说到了引用计数法,引用计数法存在什么问题?

4、刚才说到了可达性分析,知道哪些可以作为 GCROOT 吗?

5、垃圾回收算法介绍一下

6、垃圾回收会发生在哪几个区域?

【参考文章以及资料补充】

5. JVM的垃圾回收机制是用来干嘛的?为什么要垃圾回收?

15. 大厂面试题:什么情况下JVM内存中的一个对象会被垃圾回收?

16. 大厂面试题:JVM中有哪些垃圾回收算法,每个算法各自的优劣?

参考回答:

1、为什么要有垃圾回收?

在传统的 C/C++ 语言中,开发人员需要手动分配和释放内存,这样就容易导致人为的忘记释放内存的情况,造成内存泄漏。Java 的垃圾回收机制可以把帮忙解决这个问题,它可以自动检测并回收不再使用的对象,也就是垃圾,可以减少内存泄漏的风险。

2、Java 垃圾回收中是如何判断一个对象死亡的?请简单介绍一下

常用的算法有两个,也就是 “引用计数算法” 和 “可达性分析法”。

引用计数法的思路很简单,当对象被引用时给计数器 + 1,当对象引用失效时计数器值 – 1。当计数器为零时,说明对象不再被使用,可以回收。

可达性分析法的思路就是从 GC Roots 开始向下搜索,当对象到 GC Roots 都没有任何引用相连时,说明对象是不可用的,可以被回收。其中 GC Roots 是一组必须活跃的引用,从 GC Roots 出发,程序通过直接引用或间接引用,能够找到可能正在被使用的对象。

3、刚才说到了引用计数法,引用计数法存在什么问题?

引用计数法的缺点是,如果对象存在循环依赖,那就无法定位该对象是否改被回收,比如 A 引用 B,B 引用 A 这种情况。

4、刚才说到了可达性分析,知道哪些可以作为 GC ROOT 吗?(需要记住几个)

比方说 JVM 内存结构中的虚拟机栈,虚拟机栈中有栈帧,栈帧里面存储着有指向堆的对象引用,那么位于虚拟机栈顶的栈帧就可以称作是 “活跃” 的栈帧,因为此刻它正在被线程调用。所以,当前活跃的栈帧指向堆里的对象引用就可以是 GC Roots。除此之外,类的静态变量,Java 本地方法所引用的对象都可以是 GC Roots。

5、垃圾回收算法介绍一下

垃圾回收的第一步是 “标记”,标记那些没有被 GC Roots 引用的对象,也就是垃圾。标记完之后,JVM 就可以选择直接清除那些垃圾,这个算法叫做 “标记清除算法”。

这个过程简单粗暴,但也存在内存碎片问题,导致可能我有 10M 的内存,但程序申请 9M 的空间却会失败,也就是说整个内存空间的 10M 是不连续的。解决的办法就是在标记完垃圾后,不直接清除,而是先把存活的对象都复制到另一块空间,复制完了之后,再把整个原空间给清除掉。这个过程叫做 “标记复制算法”。

这种算法也有缺点:内存利用率很低,得有一块新的内存留作复制。那就不如折中一下,可以在标记之后,把存活的对象移到当前空间的一边,把垃圾移到另一边,然后再清除,就没有内存碎片的问题,同时内存利用率也不会降低。这个过程叫做 “标记整理算法”。

6、垃圾回收会发生在哪几个区域?

垃圾回收主要是发生在堆上,尤其是在新生代中,一次垃圾回收通常可以回收 70% 到 99% 的内存空间。但其实也会对方法区进行垃圾回收。对方法区主要回收的是废弃的常量和不再使用的类,一般回收的空间比较少。

【JVM专题】年轻代与老年代连环炮🌟🌟🌟🌟🌟


PS:需要先理解为啥要分年轻代和老年代的本质,这样,再怎么问都可以应付,而且有些完全可以推测

1、为什么要区分年轻代和老年代?

2、哪些对像会进入老年代?

3、什么时候会进行年轻代GC?

4、什么时候会进行老年代GC?

【参考文章以及资料补充】

8. 聊聊JVM分代模型:年轻代、老年代、永久代

17. 大厂面试题:年轻代和老年代分别适合什么样的垃圾回收算法?

37. 大厂面试题:解释一下什么是Young GC和Full GC?

38. 大厂面试题:Young GC和Full GC分别在什么情况下会发生?

参考回答:

1、为什么要区分年轻代和老年代?

主要是有两个原因,一个是大部分对象的生命周期都很短,只有少部分对象可能会存活很长时间。另一个是垃圾回收会导致 stop the world,也就是应用会暂时停止访问。为了使 stop the world 持续的时间尽可能短以提高并发式 GC 所能应付的内存分配速率,有些垃圾收集器就将对象分成两类,存活时间短的对象所处的区域叫年轻代,存活时间长的对象叫老年代。JDK 8 及以前的垃圾收集器都是有这个分代概念的。

2、哪些对像会进入老年代?

一般有两种情况,第一种是先创建的对象太大了,就会直接进入老年代,另一种是对象的年龄太老了,每发生一次 Minor GC,存活的年龄就 +1,达到默认值 15 就会自动晋升成老年代。

3、什么时候会进行年轻代GC?

一般在年轻代的 Eden 区空间不足时,就会触发年轻代 GC,也就是 Minor GC。

4、什么时候会进行老年代GC?

大约有三种情况,第一次是 Young GC 发生之前,如果老年代的可用内存小于以往 Young GC 后升入老年代的对象的平均大小,此时就带先触发一次 Old GC,腾出更多空间;第二次是 Young GC 触发之后,如果需要晋升老年代的对象在老年代中空间不足,此时还带进行一次 Old GC;第三次是如果老年代的内存使用率超过了 92%,也会直接触发 Old GC。

总之就是如果老年代空间不足以放下更多对象了,那就带进行 Old GC 腾空间了。

【JVM专题】垃圾回收器常见问题🌟🌟🌟🌟🌟


PS:这块主要靠背了,一般就考察G1和CMS,特别容易忘,大家需要多看几遍,还有也要掌握 STW。

1、常用的垃圾回收器有哪些?(直接给出答案就行,等提问具体的回收器)
2、CMS垃圾回收器介绍一下

3、G1回收器了解吗?介绍一下

4、G1和CMS有啥区别?

5、STW了解吗?CMS什么时候会STW?为什么要ST W?

6、说一下垃圾回收?如果GC突然很慢怎么排查,比如原来GC完成只需要1秒,现在要5秒?

【参考文章以及资料补充】

18. 大厂面试题:JVM中都有哪些常见的垃圾回收器,各自的特点是什么?

19. “Stop the World”问题分析:JVM最让人无奈的痛点!

22. 一步一图:深入揭秘JVM的年轻代垃圾回收器ParNew是如何工作的!

23. 一步一图:那JVM老年代垃圾回收器CMS工作时,内部又干了些啥?

29. 大厂面试题:最新的G1垃圾回收器的工作原理,你能聊聊吗?

30. G1分代回收原理深度图解:为什么回收性能比传统GC更好?

参考回答:

1、常用的垃圾回收器有哪些?(直接给出答案就行,等提问具体的回收器)

常用的垃圾回收器有 CMS,G1,还有比较早的 Serial,Serial Old,Parallel New,Parallel Old 等等。

2、CMS垃圾回收器介绍一下

CMS 在 JDK 8 是比较新的垃圾回收器,它最大的特点是并发,也就是能尽量减少 STW 的时间,在某些场景下让用户线程和 GC 线程并发执行。

CMS 的工作流程简单来说分为五个步骤:初始标记,并发标记,并发预清理,重新标记以及并发清除。

初始标记会标记 GC Roots 直接关联的对象,这个过程会发生短暂的 STW。初始标记完之后会进入并发标记,在这个过程中不会发生 STW,用户线程与 GC 线程可以同时工作,这个阶段主要是从 GC Roots 向下追溯,标记所有可达的对象。之后会进入并发预处理阶段,这个阶段主要目标是减少下一个会发生 STW 的阶段的执行时间,通过扫描卡表或者遍历新生代来确认在并发标记中发生改变的对象。接着就是重新标记,这个阶段 STW 停顿的时间主要取决于上个阶段,接着标记还存活的老年代对象。最后就是并发清除阶段,这个阶段不会 STW,用户线程一遍执行,GC 线程一边回收刚标记过的垃圾。

不过 CMS 也有缺点,主要是存在内存碎片问题,因为它本质上还是按照 ”标记-清除“ 算法 去实现的。如果内存碎片太多的话会触发 Full GC,CMS 一般在 Full GC 这个过程中对碎片进行整理。整理的过程又包括 ”移动“,”标记“,也是会发生 STW 的;还有就是 CMS 回收过程中会产生浮动垃圾,主要是在并发清除阶段,由于 GC 的过程中用户线程也一直在执行,那就会一直产生垃圾,这些垃圾只能到下一次 GC 时才能清理。

3、G1回收器了解吗?介绍一下

使用 G1 回收器时,JVM 堆的划分不再是物理的形式,而是以逻辑的形式将整个堆划分成多个小的 Region,这样在垃圾回收时能比较容易的控制垃圾回收的时间,减少 STW。

G1 垃圾回收的过程主要分为 Minor GC 和 Mixed GC,某些特殊的场景会发生 Full GC。 对于 Minor GC 来说也是灯 Eden 区满了就触发 Minor GC。当整个堆空间的占用率到达一定阈值时才会触发 Mixed GC。

Minor GC 的过程可以简单分为三个步骤:根扫描,更新和处理 RSet,复制对象。根扫描的过程就和 CMS 的初始标记过程差不多,扫描与 GC Roots 直接关联的对象。第二步就是更新和处理 RSet,将老年代对象持有年轻代对象的引用都加入到 GC Roots 下,避免被回收。最后是复制对象,把扫描之后存活的对象往 ”空的 Survivor 区“ 或 ”老年代“ 存放,其它的 Eden 区清除。

RSet 是 G1 回收器中用来解决垮代引用问题的一块存储空间。每个 Region 都会有一小块区域作为 Rset,记录着其它 Region 引用了当前 Region 的对象关系。对于年轻代的 Region,它的 RSet 只保存了来自老年代的引用。对于老年代的 Region,它的 RSet 也只会保存老年代对它的引用。

除此之外,还有一个名词是 CSet,它保存了一次 GC 中,将执行垃圾回收的 Region。CSet 中的所有存活对象都会被转移到别的可用 Regin 上。在 Minor GC 的最后,会处理软引用,弱引用等等,结束收集。

Mixed GC 是一个混合的 GC,它不仅会回收年轻代,触发 Minor GC,也会回收部分老年代的 Region。回收过程大概是初始标记,并发标记,重新标记,清理。

首先初始标记的过程复用了 Minor GC 扫描 GC Roots 的操作,速度很快。然后进行并发标记,GC 线程与用户线程一起执行,GC 线程负责收集各个 Region 的存活对象信息,从 GC Roots 往下追随,查找整个堆存活的对象。然后是重新标记,用 STAB 算法标记那些在并发标记阶段发生变化的对象,带有 STW。最后是清理,这个阶段也会发生 STW,主要回收所有的年轻代 Region,部分回收价值高的老年代 Region。

某些情况下,如果 Mixed GC 中回收速度跟不上用户线程分配内存的速度,导致老年代填满无法继续进行 Mixed GC,就会降级到 Serial Old GC 对整个堆进行 GC。

4、G1和CMS有啥区别?

首先是 G1 回收器的内存结构完全区别于 CMS。G1 整体上是基于 ”标记-整理“ 算法的实现,不会出现内存碎片,而 CMS 本质上是基于 ”标记-清除“ 算法实现,会有内存碎片问题。除此之外,G1 回收器可以根据自定义停顿时间模型,来决定本次回收多少 Region。

5、STW了解吗?CMS什么时候会STW?为什么要STW?

STW 就是 Stop the World,在垃圾回收时除 GC 线程外,其它线程都要停止工作,导致应用暂时停止访问。

CMS 会在初始标记和重新标记阶段发生 STW,在初始阶段会标记与 GC Roots 相关的对象,这时是需要用户线程停止工作的。之后在重新标记阶段也一样,需要标记还存活着的老年代对象,这个过程也要 STW。

6、说一下垃圾回收?如果GC突然很慢怎么排查,比如原来GC完成只需要1秒,现在要5秒?

(不完善,遇到了再补充)

出现这种情况可以先去看下 GC 日志,看是哪个环节的时间很长,比如 root scraning,object copy 啥的。最好是用 gceasy 工具使 GC 日志可视化看看堆信息,交互式图表等等。

除此之外还可以 dump 线程进行分析。

<

h3>【JVM专题】内存模型相关<🌟🌟🌟🌟🌟/h3>


PS:这个靠的比较简单,一般就问那几个问题,也容易记忆,一般也问的多,另外就是,一定要理解 栈 和 堆的区别。

1、能说一下JVM运行时的内存区域划分吗?(大家需要把这个和JVM模型搞乱,最好问清楚点,有时候模型是指进程/线程模型)

2、那 JVM 进程与线程模型呢?介绍一下

3、堆和栈有啥区别?介绍一下

4、什么时候会出现堆栈溢出呢?如何排查?

5、对象一定是在堆在分配的吗?

6、常量池了解吗,有啥用?介绍一下

【参考文章以及资料补充】

2. 一探究竟:我们写的Java代码到底是如何运行起来的?

4. 大厂面试题:JVM中有哪些内存区域,分别都是用来干嘛的?

参考回答:

1、能说一下JVM运行时的内存区域划分吗?(大家容易把这个和JVM模型搞乱,最好问清楚点,有时候模型是指进程/线程模型)

简单来说 JVM 运行时的内存区域分为了五大块:程序计数器,虚拟机栈,本地方法栈,堆,方法区。

其中程序计数器用来记录各个线程执行的字节码地址,通常在线程上下文切换时用来保存当前线程的执行信息。

虚拟机栈用于保存方法的局部变量,操作树并参与方法的调用和返回。每个线程在创建时都会创建一个虚拟机栈,每次方法调用都会创建一个栈帧并压入虚拟机栈中。栈帧中保存的就是方法信息,如操作数栈,局部变量表等等。

本地方法栈就是用于管理 native 方法的调用,一般是由 C 语言实现的。

堆是线程共享的区域,几乎类的实例和数组分配的内存都来自于它。堆被划分为 “新生代” 和 “老年代”,新生代又被进一步划分为 Eden 和 Survivor 区,这些都主要跟垃圾回收机制有关。

首先方法区只是 JVM 的规范,具体的实现可能各个厂商不一样,在 HotSpot 虚拟机中,JDK 8 以前是用 “永久代” 实现的 “方法区”,在 JDK 8 中用 “元空间” 代替了 “永久代” 作为方法区的实现。那么方法区主要是用来存放虚拟机加载的 “类相关信息”,比如类信息,常量池等等。其中类信息包括类中的字段,方法,父类等等。常量池又包括静态常量池和动态常量池,静态常量池用来保存字面量以及符号引用等信息,动态常量池用来存储类加载时生成的 “直接引用” 等信息。

2、那 JVM 进程与线程模型呢?介绍一下

在操作系统层面,一个 JVM 实例被视为一个独立的进程,这个进程拥有自己的内存空间和执行环境。在单个 JVM 进程内,可以同时运行多个 Java 应用程序,它们之间相互独立。

JVM 的线程模型在 Java 规范中并没有要求用哪种方法实现,在 JDK 1.2 以前,是使用一种叫 “绿色线程” 的用户线程实现的。绿色线程是指由虚拟机调度,而不是本地的操作系统调度的线程,它可以在本来不支持多线程的操作系统上实现多线程。在 JDK 1.2 以后,采用的是内核线程来实现 Java 线程。内核线程是操作系统内核直接支持的线程,它由内核的线程调度器对内核线程进行控制和分配,程序一般不直接使用内核线程,而是使用它的高级接口:轻量级线程。每个轻量级线程都由一个内核线程与其对应,所以也叫做 1:1 的线程模型。

3、堆和栈有啥区别?介绍一下

主要有存储内容,内存分配和生命周期三个方面的不同。

存储内容方面,Java 堆主要用于存储对象实例和数组等动态分配的内容。同时堆中分配的内存由垃圾回收器自动回收,防止内存泄漏;虚拟机主要用于存储方法调用时的方法信息,方法调用上下文等等。这块内存的管理是自动进行的,当方法调用结束时,虚拟机栈上有关方法的内存会被自动清除。

内存分配方面,Java 堆主要由 JVM 动态分配和管理,对象实例一般通过 new 关键字来在堆中分配内存。虚拟机栈则是在方法调用时动态创建栈帧,压入栈中。

生命周期方面,对象在 Java 堆上分配内存后,其生命周期可以贯穿整个 Java 程序的运行,而虚拟机栈中的栈帧则取决于方法何时调用和返回。

4、什么时候会出现堆栈溢出呢?如何排查?

堆溢出也就是内存溢出 OOM,一般是由于创建的对象太多,导致超过了堆的最大容量。排查时可以通过性能检测工具如 jconsole,获取堆内存快照,然后观察溢出的对象是否是必要的,是的话就需要检查代码中对象的生命周期是否过长,或者优化算法,实在不行可以调整 JVM 的堆参数设置 -Xmx 和 -Xms,增加 JVM 最大内存和启动初始内存;如果溢出的对象不是必要的,就表明发生了内存泄漏,这时可以查看泄漏对象的 GC Roots 引用链,找到具体泄漏的位置。

如果是虚拟机栈溢出,很可能就是存在死循环,过多的递归调用导致把栈撑满了,此时可以直接看控制台的堆栈信息,比较容易定位。

5、对象一定是在堆在分配的吗?

不一定,JVM 会通过 “逃逸分析” 技术,对于逃不出方法的对象,会直接在栈空间上分配内存。这样可以直接在栈上快速创建和销毁对象,不用再将对象分配到堆中,减轻 JVM 垃圾回收的压力。

逃逸分析技术就是用来判断对象是否逃逸的技术。对象逃逸分为方法逃逸和线程逃逸。方法逃逸就是指,当一个对象在方法中被定义后,它被外部方法引用了,例如作为调用参数传递到其他方法中。线程逃逸是指一个对象被外部线程访问到了,比如赋值给可以在其他线程中访问的实例变量。

6、常量池了解吗,有啥用?介绍一下

Java 8 以前常量池存在于 JVM 方法区的永久代中,Java 8 及以后存在于元空间中,它主要用来存储编译期生成的各种字面量和符号引用等等。而这些字面量可以在程序运行时直接使用,不需要再创建,提高运行效率。对于符号引用,其中包括类的全限定名,方法名等等,主要用在类加载,动态绑定等阶段,来定位具体的类,方法等等。

【JVM专题】类加载相关🌟🌟🌟


PS:这块相对考的不多,最好可以举例子来说明。

1、JAVA类加载过程介绍一下?

2、双亲委派原则了解吗?介绍一下

3、为什么需要双亲委派?

4、怎么打破双亲委派模型?了解过吗?

5、类加载器有哪些?介绍几个常见的类加载器

【参考文章以及资料补充】

2. 一探究竟:我们写的Java代码到底是如何运行起来的?

3. 面试官对于 JVM 类加载机制的猛烈炮火,你能顶住吗?

参考回答:

1、JAVA类加载过程介绍一下?

Java 类加载分为三个阶段,第一个阶段是 “加载”,是类加载过程的起始点,在这个阶段,虚拟机通过类的全限定名找到对应的二进制字节码。这个过程主要是由类加载器来完成的;第二个阶段是 “连接“,它又包括三个子阶段:验证,准备和解析。验证阶段,虚拟机主要会验证加载的字节码是否符合 JVM 规范,包括类的结构,语义等方面。准备阶段,虚拟机会为类的静态字段分配内存并设置默认值。解析阶段,虚拟机会将符号引用转换为直接引用,这样虚拟机可以直接定位到目标类,方法等等;第三个阶段是 ”初始化“,这个阶段虚拟机主要会执行类的初始化代码,之后就完成了类加载过程。

2、双亲委派原则了解吗?介绍一下

双亲委派模型是一种层次化的类加载结构,从上往下依次由 BootStrapClassLoader,ExtClassLoader,AppClassLoader 和 自定义的 ClassLoader 构成。其中 BootStrapClassLoader,由 C++ 编写,用来加载核心库 Java.;ExtClassLoader 用来加载扩展库 javax.;AppClassLoader 用来加载用户类路径上的所有库;最后自定义的 ClassLoader 用来实现定制化加载。

它的工作原理为:当一个类加载器接收到类加载的请求时,它首先将这个请求委托给它的父加载器进行处理,直到顶层的 BootStrapClassLoader 被委托为止。如果父加载器可以找到并加载这个类,那么这个类加载的请求就结束了,否则,子加载器才会尝试加载该类,以此类推。

3、为什么需要双亲委派?

这种委派机制可以确保 Java 类的唯一性,避免类的重复加载。同时还保证了 Java 核心库的安全性和稳定性。因为Java核心库都是由 BootStrapClassLoader 加载的,任何用户自定义的类都无法覆盖核心类库中的类。

4、怎么打破双亲委派模型?了解过吗?

打破双亲委派模型的思路就是在加载类时,不按照依次往上匹配类加载器的方式加载。那么只需要自定义一个 ClassLoader,然后重写 loadClass 方法,自己定义一个其他的加载方式就行。

实际的应用中,Tomcat 就破坏了双亲委派模型。通常在用 Tomcat 部署 web 应用时,需要将 war 包放在 webapp 目录下,然后 Tomcat 就能运行这个 web 应用了。现在假设有两个 web 应用类,并且它们刚好都有一个全限定名都相同的 user 类,但具体的实现不一样。如果按照双亲委派模型的思路,最终只有一个 user 能被加载,也就只有一个 web 应用能部署成功。Tomcat 为了解决这个问题,它给每个 Web 应用都创建了一个 WebAppClassLoader,该类加载器重写了 loadClass 方法,优先加载当前应用目录下的类,如果当前目录找不到,才会一层一层往上找。这样的话,Tomcat 就做到了 Web 应用层级别的隔离。

还有比如 JDBC 也算是破坏了双亲委派模型,当我们使用 JDBC 时,是用 DriverManager.getConnection( ) 来获取连接的,但由于 DriverManager 本身是属于 Java 包下的,是由 BootStrapClassLoader 加载的,而通过 getConnection 得到的是其它厂商的实现类,显然是不能由 BootStrapClassLoader 加载。那么在这里就用到了 “线程上下文加载器”,DriverManager 在初始化时就会去得到线程上下文加载器,然后由它代替 BootStrapClassLoader 来加载具体的 Connection 类。

5、类加载器有哪些?介绍几个常见的类加载器

见 2 、

发表评论

后才能评论