写点什么

相信自己,这次一把搞定 JVM 面试

  • 2022 年 7 月 02 日
  • 本文字数:2629 字

    阅读完需:约 9 分钟

JVM 是 Java 工程师面试的必问基础知识,一般会在第一轮技术基础面试中会被问到。因此这部分知识的掌握以及应用是非常重要的基本功。本文分为上下两篇,力求最大程度通过面试问题厘清 JVM 的知识体系脉络。希望在大家准备面试的时候可以有所帮助。话不多说,我们直接上正菜。


一、请说说 JVM 的内存布局是怎样的?

分析:JVM 加载类的每个阶段都需要有对应的物理介质进行承接。因此 JVM 的每个区域划分都有其特定的作用。对内存区域烂熟于心,才能在遇到异常分析问题对时候有迹可循,才能不自乱阵脚。


内存区域的划分大致如下图所示:



堆区域:该区域属于线程共享区域,是非常重要都内存区域。主要存放我们代码中创建出来的对象信息。


元数据空间:主要是存放“.class”⽂件⾥加载进来的类信息,还包括⼀些类似常量池信息。都存放在这个区域中。


程序计数器:在 JVM 规范中,每个线程都有属于它们自己的程序计数器,所以这块区域是线程私有的。这块区域内存空间较小,主要用于存储当前线程正在执行的 Java 方法的 JVM 指令地址,即字节码的行号。这样在程序执行完成某个分支之后,可以根据该信息,回到原来程序执行处,继续后续的逻辑处理。如果正在执行 Native 方法,则这个计数器为空。


本地方法栈:本地方法栈是在调用本地方法(Native)时使用的栈,每个线程都有一个本地方法栈。


虚拟机栈:该区域也属于线程私有区域,每个线程在创建的时候都会创建一个虚拟机栈,生命周期与线程一致,线程退出时,线程的虚拟机栈也回收。线程内部方法调用时,都会创建对应的栈帧进行压栈,方法调用结束时会进行出栈操作。栈帧中主要包含了局部变量表、操作数栈等信息。


我们将类加载与 JVM 内存区域进行了联动,这样通过图形展示,大家可以更好的理解这部分内容。


二、JVM 的分代模型是什么?为什么设计分代模型?


 分析:面试官主要考察面试者对于 JVM 分代模型掌握程度以及背后的设计思想。


 不同的编码方式导致实际代码中创建对象的方式也不尽相同,因此对象的存活周期也是不同的。根据对象在 JVM 存活时间的不同,我们将 JVM 内存区域进行划分,区域划分后的分代模型主要为年轻代、老年代以及永久代。


其实大部分的对象都是朝生夕死,所以这部分对象在年轻代中,还有一些对象需要长时间存活,因此在老年代中。永久代或者说当下的元空间,存放一下类信息。


JVM 的分代模型其实就是一种分而治之代思想的实践。目的主要是根据对象的不同生存条件,采用不同的垃圾回收算法进行垃圾回收。


年轻代采用代是复制算法进行垃圾回收,老年代采用代是标记整理算法进行垃圾回收。



三、Survivor 区域为什么设计 S0、S1 两个区域?一个不行吗?


分析:这个问题不太好回答,因为可能很多人没有思考过这个问题。所以我们在平时学习的时候,要多问下自己设计者为什么这么设计?他的意图是什么?这样我们自己也会多收获一点。


其实 Survivor 区就是年轻代和老年代的缓冲。如果没有这个缓冲区的话,那么年轻代每进行一次 Minor GC 都会将存活的对象复制到老年代,那么老年代就会很快被填满,从而触发 Full GC。


 那么只有一个 Survivor 区行不行呢?假设我们就设计一个 Survivor 区,看看垃圾回收的过程是怎样的。当进行垃圾回收的时候,存活对象被复制到 Survivor 区,如果 Survivor 区本身就有存活对象,也需要进行垃圾回收。那么应该采用什么垃圾回收算法呢?如果再复制,就会转移到老年代中去。那么如果采用标记清除算法,就会产生内存碎片的问题。综上,设计两个 Survivor 区的原因主要有两点,一方面是想让短暂存活的对象尽量在年轻代进行活动,减少其进入老年代的概率。另一方面使用复制算法,避免产生内存碎片。


四、JVM 的垃圾回收算法有哪些?分析下各自的优劣。


分析:垃圾回收算法以及各自的优缺点有助于理解各个区域在垃圾回收算法选择上的考虑。


1、标记-清除算法首先标记出需要回收的垃圾对象,在标记完成之后统一回收所有被标记的对象。但是在清除垃圾对象后,我们可以发现造成了内存区域不连续的问题,导致产生了很多内存碎片,降低了内存空间使用率。



2、复制算法为了解决内存碎片的问题,复制算法应运而生。复制算法将内存分为大小相等的两块区域,每次使用其中的一块。在使用的这半块空间中,先将不能进行垃圾回收的对象进行标记(注意和标记清除算法的区别),再将存活的对象拷贝到另外一块内存区域中,最后将这块内存中的垃圾对象一把全部清除掉。这样两块内存区域就可以这样来回使用。从这个过程可以看出来,内存碎片的问题不存在了,每次使用的都是连续的内存空间。但是复制算法又引入了新的问题,就是每次只能使用一半的空间,实在有点浪费。


因此,需要对复制算法进行进一步的优化。我们知道在大多数情况下,每次 GC 之后,大部分的对象都会被回收掉,只有少部分很小比例的对象存活。那么是不是可以调整下内存区域的使用比例来达到优化的目的呢?于是有了问题 2 中的年轻代模型。通过 Eden、S0、S1 区域的划分,最大化使用空间。为什么使用两个 S 区,可以参看上文内容。



3、标记-整理算法对于老年代来说,复制算法显然不合适。因为老年代中的对象都是大对象或者存活率高的对象,频繁的复制对性能的影响比较大。因此老年代采用标记-整理垃圾回收算法。首先标记所有的存活对象,将所有存活的对象都向一端移动,这样存活的对象就可以紧密的靠在一起。然后清理另一端内存空间,避免产生内存碎片。但是值得注意的是,老年代的垃圾回收算法效率较低。如果频繁出现老年代的 GC,对系统运行稳定性有影响,这也是我们进行 GC 优化的一个方向。


五、什么时候出发 minor GC?什么时候触发 full GC?


1、minor GC


当一个新的对象来申请内存空间的时候,新生代的 Eden 区域以及 S 区域没有足够空间再进行分配时,触发 minor GC。


2、full GC

在讨论 full GC 之前,我们先看下哪些场景下,对象会进入老年代。


(1)大对象

  JVM 的参数设置中,可以设置大对象的阈值,比如超过 1MB 的对象可以直接升入老年代。避免这类对象躲过 GC 之后,在年轻代中来回复制,耗费资源。


(2)S 区域空间不足

在进行一次 minor GC 之后,如果 S 区域没有足够的连续空间承载存活对象,那么这部分对象也会升入老年代。


(3)空间担保机制

判断 minor GC 之后,进入老年代对象的平均大小。如果大于平均大小则进行一次回收,保证有足够空间可以进行承载。


(4)动态年龄判断

 通过设置对应的参数,判断对象躲过垃圾回收的次数,如果达到一定阈值,则将该对象升入老年代。基于以上分析,每次达到升入老年代的条件后,但是老年代又没有足够的连续空间可以装载存活的对象时,则进行 full GC 来空出足够的空间。


发布于: 刚刚阅读数: 3
用户头像

真正的大师永远怀着一颗学徒的心 2018.09.18 加入

InfoQ签约作者、阿里云专家博主,一线大厂高级开发工程师,专注Java后端以及分布式架构,在通往CTO的道路上不断前行。关注公众号:慕枫技术笔记。

评论

发布
暂无评论
相信自己,这次一把搞定JVM面试_JVM_慕枫技术笔记_InfoQ写作社区