转载

JVM 面试题集

傻瓜源码-内容简介

【职场经验】(持续更新)

如何日常学习、如何书写简历、引导面试官、系统准备面试、选择offer、提高绩效、晋升TeamLeader.....

【源码解读】(持续更新)

1. 源码选材:Java架构师必须掌握的所有框架和类库源码

2. 内容大纲:按照“企业应用Demo”讲解执行源码:总纲“阅读指南”、第一章“源码基础”、第二章“相关Java基础”、第三章“白话讲源码”、第四章“代码解读”、第五章“设计模式”、第六章“附录-面试习题、相关JDK方法、中文注释可运行源码项目”

3. 读后问题:粉丝群答疑解惑

已收录: HashMap 、 ReentrantLock 、 ThreadPoolExecutor 、 《Spring源码解读》 、 《Dubbo源码解读》 .....

【面试题集】(持续更新)

1. 面试题选材:Java面试常问的所有面试题和必会知识点

2. 内容大纲:第一部分”注意事项“、第二部分“面试题解读”(包括:”面试题“、”答案“、”答案详解“、“实际开发解说”)

3. 深度/广度:面试题集中的答案和答案详解,都是对齐一般面试要求的深度和广度

4. 读后问题:粉丝群答疑解惑

已收录: Java基础面试题集 、 Java并发面试题集 、 JVM面试题集 、 数据库(Mysql)面试题集 、 缓存(Redis)面试题集 .....
【粉丝群】(持续更新) 收录:阿里、字节跳动、京东、小米、美团、哔哩哔哩等大厂内推

:stuck_out_tongue: 作者介绍:Spring系源码贡献者、世界五百强互联网公司、TeamLeader、Github开源产品作者

:stuck_out_tongue: 作者微信:wowangle03 (企业内推联系我)

加入我的粉丝社群,阅读更多内容。从学习到面试,从面试到工作,从 coder 到 TeamLeader,每天给你答疑解惑,还能有第二份收入!

JVM 面试题集

注意事项

  1. 阅读本文前,建议先阅读“如何成为值钱的 Java 开发-指南”一文,文中写有面试题集推荐学习顺序。
  2. 建议按照文章顺序阅读面试题(内容前后顺序存在依赖关系)。
  3. 本文中的 JVM 指代的都是 Java 虚拟机 HotSpot。

一. JVM

1. JVM 是什么?

答案:

JVM 虚拟机就是执行字节码(Class)的一个虚拟计算机。( Java 中使用的 JVM 都是 HotSpot 虚拟机)

解释:

JVM 执行字节码(Class)大致分为两步:( JDK 的 Javac 编译器(独立存在 JVM 外部)会先将 java 文件编译成字节码(Class)文件)

  1. **第一步:**类加载器(ClassLoader)先把 Class 文件中描述类的信息(比如:类的全局限定名、访问修饰符、字段描述、方法描述等)转换成二进制字节流,加载到由 JVM 管理的内存空间(也叫运行时数据区)里去,然后 JVM 开始对数据进行校验、转换解析和初始化,最终完成类信息的加载;

  2. 第二步:JVM 的字节码执行器负责解析执行字节码,并把生成的中间数据存入由 JVM 管理的内存空间相应数据区域内。

以上步骤,涉及到了 4 个角色:编译器、类加载器、运行时数据区、字节码执行引擎。面试中,一般只考核类加载器和运行时数据区的相关知识,也是本面试题集所讲的核心内容。

扩展:

现在 JDK 分为 Sun Jdk(Oracle JDK)和 Open Jdk;现在企业一般都是用 Open Jdk,因为免费,并且功能够用。而 Sun Jdk 和Open Jdk 都是由 sun 公司发布,后被 Oracle 收购,主要不同之处在于 Open Jdk 可以免费使用,并且源码相较 Sun Jdk 不是非常完整。

2. JDK、JRE、JVM 分别是什么关系?

  • JDK 主要包含 JRE 和 开发工具(比如:运行 Java 程序的 Java 工具、编译 Java 程序的 Javac 工具);
  • JRE 主要包含 JVM 和 Java 类库(比如:java.math.*、java.util. *);
  • JVM 就是执行字节码(Class)的一个虚拟计算机。

二. 运行时数据区

1. JVM 管理的内存空间都有哪几个数据区域?

答案:

一共有 5 个。分别是:

  1. 程序计数器
  2. 虚拟机栈
  3. 本地方法栈
  4. 方法区

解释:

1. 程序计数器

每条线程都有一个程序计数器,可以看成线程执行字节码的行号指示器。

2. 虚拟机栈

用于存储栈帧,每个方法在执行的同时,都会创建一个栈帧,用于存储局部变量表、方法出入口等信息;每个方法从执行开始到结束,都对应着一个栈帧在虚拟机栈中的入栈和出栈过程。

3. 本地方法栈

与虚拟机栈的区别,只是本地栈是为 native 修饰的方法服务。native 修饰的方法,说明不是用 Java 实现的,而是用 C 或者 C++ 等其它语言实现的。

4. 堆

用于存储实例化的对象。为了更高效的垃圾回收,设计者还把堆分为青年代和老年代;大多数情况下,新创建的对象都优先放入青年代,如果是需要占用很大连续内存的 Java 对象,JVM 会直接放入老年代。如果青年代中经历多次垃圾回收,仍然存活的对象也会被移到老年代里。

5. 方法区

方法区被描述为堆的“逻辑”部分,为了区分堆,还有个别名,叫“非堆”。用于存储描述类(Class)的信息、常量池(主要用于保存编译期和运行期生成的字面量、字符串、常量等)、静态变量等。

JDK 7 开始,常量池、静态变量由方法区挪到了堆中。

JDK 8 之前,在 JVM 内存空间中使用永久代实现方法区;JDK 8 开始,改为由计算机本地内存实现的元空间实现方法区。改为元空间后,有利于减小方法区内存溢出的概率。(因为不受 JVM 内存空间的限制,而是受整个计算机可用内存的限制)

2. JVM 内存空间中哪些数据区域线程私有,哪些共有?

  • 私有:程序计数器、虚拟机栈、本地方法栈;

  • 共有:堆、方法区。

3. JVM 中哪些数据区域会被垃圾收集器(GC)回收?

答案:

堆和方法区。

解释:

数据区域的垃圾回收是 JVM 自动管理。除了堆和方法区以外的其它内存区域的大部分数据达到回收条件,也都会被回收销毁,只不过不是垃圾回收器负责而已。

4. 堆什么时候会触发GC?

答案:

青年代没有足够空间装载对象时,垃圾收集器将会自动触发 Minor GC(也叫 Young GC);

老年代在没有足够空间装载对象时,垃圾收集器将会自动触发 Major GC(也叫 Old GC)。

解释:

青年代垃圾回收(Minor GC/Young GC)发生得非常频繁,每次都有大批对象死去。鉴于青年代每次垃圾回收都会有大批对象死去,设计者还把青年代分为 Eden 空间、From Survivor 空间、To Survivor 空间。

要存入青年代的对象,都会优先放入 Eden 空间和其中一个 Survivor 空间;在每次执行 Minor GC 时,垃圾回收器就会把存活的对象复制到另一个没用到的空 Survivor 空间里。如果多次回收后,青年代中仍然有存活的对象,那么这些存活对象将会被放到老年代里。

老年代发生的垃圾回收(Major GC/Old GC),发生次数要比青年代少得多,每次回收的对象数量也较少。

青年代中的对象经历多次垃圾回收后仍然存活,就会进入到老年代?

每个对象都有一个年龄计数器,每经历一次 Minor Gc,仍然存活,并且成功复制到另一个 Survivor 空间后,年龄就会增加 1 岁:

  • 默认年龄达到 15 岁,就会进入老年代;
  • 当年龄未达到 15 岁,但是 Survivor 空间中年龄相同对象的内存总和大于 Survivor 空间的一半时,那么大于等于该年龄的对象也都会进入老年代。

5. 虚拟机如何判断堆中对象是否应该被回收?

答案:

JVM 虚拟机使用”可达性分析算法“判断对象是否存活。

解释:

1. 可达性分析算法

可达性分析算法就是以“GC Roots”为起点,通过引用关系搜索所有直接或间接关联的对象,没有搜索到的对象,就视为不可达,即死亡。

当 JVM 在可达性分析算法下,判定为不可达的对象,仍有机会再次和 GC Roots 建立引用关系,重新存活。如下:

  1. 当 JVM 在可达性分析算法下,判定为不可达的对象,会被标记一次记号,放到即将回收的集合里暂存;
  2. 随后再在标记记号的对象里筛选出是否有重写 finalize() 方法并未执行的,再把这些对象放到一个队列里,开辟新线程去执行队列里对象的 finalize() 方法;
  3. 执行 finalize() 方法后,如果对象又和 GC Roots 建立了引用关系(就是把对象作为值,set 到和 GC Roots 有引用关系的对象属性里),在第二次标记的时候,就会把这种对象从即将回收的集合里排除掉。

2. GC Roots

包括但不限于以下几种:

  1. 栈帧里局部变量表保存的对象;比如:调用方法的参数、局部变量等。
  2. 在本地方法栈中 native 方法里引用的对象。
  3. 静态变量。
  4. sychronized 持有的锁对象。
  5. 比如空指针等常驻异常对象。

扩展:

还有一种判断对象是否存活的算法,称之为“引用计数算法”。就是在对象里存储一个引用计数器,每当其它对象引用本对象时,计数器值就加一;当引用失效时,计数器值就减一;当计数器值为零时,就视为对象死亡。

在 HotSpot 中并没有被使用,因为它有很多特殊场景需要考虑,比如:当两个对象互相引用,两个对象的计数器都是 1,都被视为可用对象。但是实际上两个对象除了对方,就没有地方被引用了,导致引用计数算法无法回收它们。

6. GC Roots 都有哪些?列举一下?

包括但不限于以下几种:

  1. 栈帧里局部变量表保存的对象;比如:调用方法的参数、局部变量等;
  2. 在本地方法栈中 native 方法里引用的对象;
  3. 静态变量;
  4. sychronized 持有的锁对象;
  5. 比如空指针等常驻异常对象。

7. 如何判断常量池中的常量和方法区中类信息是否可回收?

在常量池中,没有任何地方被引用的字面量,则视为可回收。

在方法区中的类信息,回收条件苛刻,只有符合条件的对象才允许被回收(这里注意是允许,不代表一定会被回收),有如下三个条件:

  1. 在堆中不存在任何该实例和子类实例。
  2. 没有任何地方通过反射获得该实例。
  3. 加载该类的类加载器也被回收。

8. 垃圾回收算法(GC)有几种算法?

答案:

一共有 3 种算法;标记-清除算法、标记-整理算法、标记-复制算法。

解释:

分代收集理论

分代收集就是指堆划分成多个不同的区域,然后将对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。这样做的好处就是针对不同数据的特性,使用不同回收算法,增加空间的利用率和垃圾回收效率。

HotSpot 的设计团队为了让堆满足“分代收集”的设计原则,所以就把堆细分为了新生代和老年代,不同的数据区域使用不同的垃圾回收算法。

新生代每次回收大量对象,有少量存活对象,适用“标记-复制”算法;老年代对象存活率高,适用”标记-清理“算法或者“标记-整理”算法。

停顿时间

停顿时间就是指 JVM 在执行垃圾回收过程中,可能需要暂停用户线程;这个暂停的时间就是停顿时间。这个停顿的过程叫 Stop The World。

吞吐量

本文中的吞吐量是指 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量= CPU 运行用户代码时间/(CPU 运行用户代码时间 + CPU 垃圾收集时间)。

1. 标记-清理算法

“标记-清理”算法大致分为两步:第一步,标记所有需要回收的对象;第二步,直接清理标记的对象。

但是使用这种算法,有两点不足:

1)如果内存区域内大部分都是可回收对象,还需要逐个标记和清理,执行效率低;

2)容易产生不连续的内存碎片,导致内存分配和访问困难。

综上所述,“标记-清理”算法更适用于大部分装载对象都是不可回收的内存区域,即老年代。

2. 标记-整理算法

“标记-整理”算法大致分为三步:第一步,标记所有需要回收的对象;第二步,让没有标记的存活对象向一边移动;第三步,直接清理掉另一边的对象。

标记-整理算法和标记-清除算法第一步相同;标记-整理算法在标记后,不直接清理,而是选择移动后,再清理掉另一边的对象。这样做,相比“标记-清理”算法,不会产生不连续的内存碎片,但是却产生了新的问题,就是移动成本高,并且移动期间必须暂停用户应用程序线程才能进行。

但是从整体上分析,停顿用户线程并移动对象的方案,要比产生不连续内存碎片的方案,吞吐量更好;所以如果要求高吞吐的垃圾回收,可以选择实现“标记-整理”算法的回收器;如果想要更短的停顿时间,就可以选择实现“标记-清理”算法的回收器。

3. 标记-复制算法

想要实施”标记-复制“算法,首先需要内存区域满足一定的空间结构,比如:将内存区域分为大小相等的两块内存,每次只使用一块,当一块的内存空间不够了,触发垃圾回收时,就把存活对象复制到另一块上,然后清理掉之前的内存空间中的对象。

也就是说“标记-复制”算法大致分为三步:

  1. 第一步:标记所有需要回收的对象;
  2. 第二步:将存活的对象复制到另一块空内存空间里;
  3. 第三步:清除其它内存空间的死亡对象。

采用这种算法,有三点不足:

1)当需要复制的存活对象多时,性能低。

2)需要划分出多个独立空间,空间占用大;

3)垃圾回收时,当另一块内存不够装载复制的对象时,就需要使用其它内存空间来帮忙装载复制的对象(这个机制称之为担保机制)。

综上所述,“标记-复制”算法适用于复制的存活对象少,有其它空间可做担保的内存区域;即青年代,老年代可做担保。

3.1 HotSpot 虚拟机

在 HotSpot 虚拟机中,因为青年代对象经历垃圾回收后,存活的对象占比较少,所以不需要 1:1 等分内存,而是将内存分为一块较大的 Eden 和两块较小的 Survivor 空间,每次只使用 Eden 和一块 Survivor。当垃圾回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。( 三块内存空间的默认比例是 Eden:From Survivor:To Survivor = 8:1:1)

为了避免出现 Survivor 空间内存不够装载复制过来的存活对象的问题,JVM 采用担保机制(当 Survivor 内存不够装载被复制过来的对象时,直接放入老年代中)来解决。

但是担保机制可行的前提是老年代必须有足够的地方装载被复制过来的存活对象,所以在发生 Minor GC ,可能触发担保机制之前,JVM 就会先检查老年代最大可用连续内存是否大于青年代历次晋升到老年代存活对象的平均大小;如果大于,则可以进行 Minor GC 垃圾回收;如果小于或者配置了不开启担保机制,则直接进行 Full Gc,清理整堆和方法区的死亡对象。

注意:因为担保机制是通过比较青年代存活对象历次晋升到老年代的平均大小;所以在极端场景下,Minor GC 后,老年代仍有可能无法装载新晋升到老年代的对象,最终还是要执行 Full GC,来在堆空间中腾出地方装载存活对象;如果仍然装不下,那只有抛出 OutOfMemoryError 错误了。

加入我的粉丝社群,阅读剩余全部内容

从学习到面试,从面试到工作,从 coder 到 TeamLeader,每天给你答疑解惑,还能有第二份收入,这样的知识星球,难道你还要犹豫!

JVM 面试题集
原文  https://juejin.im/post/5eeff943e51d457406480826
正文到此结束
Loading...