小言_互联网的博客

(Java实习生)每日10道面试题打卡——JVM篇2

299人阅读  评论(0)
  • 临近秋招,备战暑期实习,祝大家每天进步亿点点!Day07
  • 有粉丝大佬要求更新有难度的,所以本篇总结的是 JVM 相关的面试题,后续会每日更新~
  • :JVM 比较枯燥,直接刷题前,最好先去串一遍 JVM 课程,这里推荐传智播客的 JVM 教程:黑马程序员JVM教程笔记完整目录


1、请你说一说java 中的 五种引用?Java 中都有哪些引用类型?

① 强引用

  • 只要沿着 GC Root 的引用链能够找到该对象,就不会被垃圾回收;只有当 GC Root 都不引用该对象时,才会回收强引用对象。
  • 换句话说就是,只要强引用存在,JVM 垃圾回收器就永远都不会回收被引用的对象,即使内存不足,JVM 会抛出 OutOfMemoryError

比如,new 一个对象 Student ,将对象 Student 通过=(赋值运算符),赋值给变量 stu,则变量 stu 就强引用了对象 Student

// 只要 stu 指向 Student 对象,那它就是强引用,永远都不会被 JVM 回收
Student stu = new Student();
// 如果将 stu 置为 null,可以切断 GC Root 引用链,这样 stu 就会被 JVM 回收
stu = null;

② 软引用

  • 如果仅有软引用引用某个对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象。即,在内存足够时,JVM 不会回收软引用对象,但当内存不足时,软引用对象就会被回收,所以软引用对象通常用来描述一些非必要但仍有用的对象。
// 不直接通过 list 引用 byte[]
// list -----> SoftReference -----> byte[] 添加了一层软引用:
List<SoftReference<byte[]>> list = new ArrayList<>();

③ 弱引用

  • 弱引用是较软引用更低一级的引用,如果仅有弱引用引用某个对象,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象。
List<WeakReference<byte[]>> list = new ArrayList<>();

④ 虚引用

  • 虚引用必须配合引用队列使用,主要配合 ByteBuffer 使用,引用对象被回收时,会将虚引用入队,然后调用虚引用相关方法(Unsafe.freeMemory())释放直接内存。

⑤ 终结器引用

  • 所有的类都继承自Object 类,Object 类有一个finalize()方法。当某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中(被引用对象暂时没有被回收),然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize()方法。调用以后,该对象就可以被垃圾回收了。

引用队列

在回收软引用、弱引用所指向的对象时,软引用本身不会被清理。如果想要清理引用,需要使借助引用队列

ReferenceQueue,当一个引用(软引用、弱引用)关联到了一个引用队列后,当这个引用所引用的对象要被垃圾回收时,就会将它加入到所关联的引用队列中,所以判断一个引用对象是否已经被回收的一个现象就是,这个对象的引用是否被加入到了它所关联的引用队列。

ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);

说到底,引用队列就是一个对引用的回收机制,当软引用或弱引用所包装的对象为 null 或被回收时,这个引用也就不在具有价值,引用队列就是清除掉这部分引用的一种回收机制。

  • 软引用和弱引用可以配合引用队列(也可以不配合):在弱引用虚引用所引用的对象被回收以后,会将这些引用放入引用队列中,方便一起回收这些软/弱引用对象。
  • 虚引用和终结器引用必须配合引用队列虚引用终结器引用在使用时会关联一个引用队列。

2、JVM 是如何判断对象可以被回收的?判断对象是否可以回收的两种算法?

① 引用计数法(java不采用这种方法)

  • 如果一个对象被其他变量所引用,则让该对象的引用计数+1,如果该对象被引用 2 次则其引用计数为 2,依次类推。
  • 某个变量不再引用该对象,则让该对象的引用计数-1,当该对象的引用计数变为0 时,则表示该对象没用被其他变量所引用,这时候该对象就可以被作为垃圾进行回收。

引用计数法弊端:循环引用时,两个对象的引用计数都为 1 ,导致两个对象都无法被释放回收。最终就会造成内存泄漏!

② 可达性分析算法

可达性分析算法就是JVM中判断对象是否是垃圾的算法:该算法首先要确定 GC Root (根对象,就是肯定不会被当成垃圾回收的对象)。

在垃圾回收之前,JVM会先对堆中的所有对象进行扫描,判断每一个对象是否能被 GC Root 直接或者间接的引用,如果能被根对象直接或间接引用则表示该对象不能被垃圾回收,反之则表示该对象可以被回收

  • JVM中的垃圾回收器通过可达性分析来探索所有存活的对象。
  • 扫描堆中的对象,看能否沿着 GC Root 为起点的引用链找到该对象,如果找不到,则表示可以回收,否则就可以回收。
  • 可以作为GC Root的对象
    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中 Native 方法引用的对象。

3、JVM 垃圾回收算法有哪些?

① 标记-清除算法

  • 标记清除算法:顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识,清除相应的内容,给堆内存腾出相应的空间。
    • 这里的腾出内存空间并不是将内存空间的字节清 0,而是记录下这段内存的起始结束地址,下次分配内存的时候,会直接覆盖这段内存。
  • 它的主要不足有两个:
    • 一个是效率问题,标记和清除两个过程的效率都不高;
    • 另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

② 标记-整理算法

  • 标记-整理算法:会将不被 GC Root 引用的对象回收,清除其占用的内存空间。然后整理剩余的对象内存空间,可以有效避免因内存碎片而导致的问题,但是牵扯到对象的整理移动,需要消耗一定的时间,所以效率较低。

③ 复制算法

  • 复制算法:将内存分为等大小的两个区域,FROMTO(TO中为空)。先将被 GC Root 引用的对象从 FROM 放入 TO 中,再回收不被 GC Root 引用的对象。然后交换 FROMTO

这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。流程如下:

  • 当需要回收对象时,先将 GC Root 直接引用的的对象(不需要回收的对象)从 FROM 放入 TO 中:

  • 然后清除FROM中的需要回收的对象:

  • 最后交换 FROMTO 的位置:(FROM 换成 TO,TO 换成 FROM )

④ 分代收集算法

  • 分代收集算法 :这种算法是把 Java 堆分为新生代老年代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。根据不同年代的特点采用最适当的收集算法。
    • 新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
      • 新生代里有 3 个分区:伊甸园、To 幸存区、From 幸存区,它们的默认占比是 8:1:1
    • 老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清理或者标记—整理算法来进行回收。

下面来逐步介绍一下分代收集算法的流程:

  • 长时间使用的对象放在老年代中(长时间回收一次,回收花费时间久),用完即可丢弃的对象放在新生代中(频繁需要回收,回收速度相对较快):

  • 新创建的对象都被放在了新生代的伊甸园中:

  • 当伊甸园中的内存不足时,就会进行一次垃圾回收,这时的回收叫做 Minor GC (Young GC):

Minor GC 会将伊甸园和幸存区FROM仍需要存活的对象复制到 幸存区 TO中, 并让其寿命加1,再交换FROM和TO

  • 伊甸园中不需要存活的对象将其清除:

  • 交换FROM和TO

  • 同理,继续向伊甸园新增对象,如果满了,则进行第二次 Minor GC

流程相同,仍需要存活的对象寿命+1:(下图中 FROM 中寿命为1的对象是新从伊甸园复制过来的,而不是原来幸存区 FROM 中的寿命为1的对象,这里只是静态图片不好展示,只能用文字描述了)

再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(会触发 Stop The World, 暂停其他用户线程,只让垃圾回收线程工作),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1

  • 如果幸存区中的对象的寿命超过某个阈值(最大为 154 bit),就会被放入老年代中:

  • 如果新生代老年代中的内存都满了,就会先触发 Minor Gc,再触发 Full GC,扫描新生代和老年代中所有不再使用的对象并回收:

分代收集算法流程小结:

  • 新创建的对象首先会被分配在伊甸园区域。
  • 新生代空间不足时,触发 Minor GC,伊甸园和 FROM 幸存区需要存活的对象会被 COPY 到 TO 幸存区中,存活的对象寿命+1,并且交换 FROMTO
  • Young GC 会引发 Stop The World:暂停其他用户的线程,等待垃圾回收结束后,用户线程才可以恢复执行。
  • 当对象寿命超过阈值15时,会晋升至老年代。
  • 如果新生代、老年代中的内存都满了,就会先触发 Minor GC,再触发 Full GC,扫描新生代和老年代中所有不再使用的对象并回收。

4、JVM 垃圾回收机制有哪些?

在触发 GC 的时候,具体如下,这里只说常见的 Young GC (Minor GC) 和 Full GC

① Young GC (Minor GC)

  • 当新生代中的 Eden(伊甸园) 区没有足够空间进行分配时会触发 Young GC
  • Young GC 其实就是一次 复制垃圾回收算法伊甸园和幸存区FROM仍需要存活的对象复制到 幸存区 TO中, 并让其寿命加1,再交换 FROM和 TO。这时候伊甸园中不需要存活的对象就将其清除。

② Full GC

  • 如果新生代老年代中的内存都满了,就会先触发 Young GC,再触发 Full GC,扫描新生代和老年代中所有不再使用的对象并回收。
  • System.gc() 默认也是触发 Full GC
  • Full FC 使用 标记—清理 或者 标记—整理 算法来进行回收。

5、说一下JVM 有哪些垃圾回收器?

相关概念:

  • 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
  • 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
  • 吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间)),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%

垃圾回收器的分类:

  • 串行:单线程垃圾收集。
  • 吞吐量优先:多线程垃圾收集,单位时间内,让STW(stop the world,停掉其他所有工作线程)时间最短。
  • 响应时间优先:多线程垃圾收集,尽可能单次STW时间变短(尽量不影响其他线程运行)。

7种垃圾回收器:

如图:(图片参考自:https://thinkwon.blog.csdn.net/article/details/104390752

主要来了解一下下面两个回收器(7个回收器,就算背也难记住,先掌握2个再说!)

  • CMS 收集器老年代并行收集器
    • 以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
    • CMS 收集器基于标记-清除算法实现,会产生内存碎片。
  • G1 收集器Java堆并行收集器
    • G1收集器是 JDK1.7 提供的一个新收集器,G1 收集器基于标记-整理算法实现,也就是说不会产生内存碎片。
    • 此外,G1 收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而其他六种收集器回收的范围仅限于新生代或老年代。

CMS 和 G1 都是属于响应优先的垃圾回收器:尽可能让**单次 **STW 时间变短(尽量不影响其他线程运行)。

:STW ,即 Stop The World, 暂停其他用户线程,只让垃圾回收线程工作。


6、请你说一下 GC Root 有哪些?

在Java语言中,可作为GC Roots的对象包括下面 4 种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 Native 方法引用的对象

7、分析题:a++ + ++a

案例代码如下:

/**
* 从字节码角度分析 a++ 相关题目
*/
public class Demo3_2 {
   
    public static void main(String[] args) {
   
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);// 11
        System.out.println(b);// 34
    }
}

上面a、b的结果是怎样得来的呢?

分析

  • iinc 指令是直接在局部变量桶位(slot)上进行运算。
  • iload指令是用于读取变量
  • a++++a 的区别是先执行 iload还是 先执行iinca++是先 iloadiinc++a相反。

对虚拟机指令不清楚的去看一下这篇文章:JVM_07 类加载与字节码技术(字节码指令)

bipush 10 操作是把a = 10 放入操作数栈:

istore 1 操作,把操作数栈中的10弹出,放入到局部变量表的槽位1中:

③ 接下来执行a++操作,我们上边提前说明了,a++是先执行iload读取,再执行iinc 加 1

  • iload 1 将 变量a=10,读取到操作数栈stack中:

  • 执行iinc指令,在局部变量表上对a进行 +1 操作,这时候 a 为11:

④ 下面执行++a操作,先iinciload

  • 执行iinc指令,在局部变量表上对a进行+1操作,这时候a为12:

  • iload 1 将局部变量表中a=12,读取到操作数栈stack中:

⑤ 下面进行 a++ + ++a 操作,在操作数栈中进行相加,得到结果22,这时候第1个加法完成:

⑥ 下面执行第二个加法(a++ + ++a)+ a--操作:

  • a-- 先执行 iload 命令,在执行 inc 1,-1命令,如下,先将局部变量表中的12读取到操作数栈:

  • 接下来执行 inc 1,-1命令,在局部变量表中进行-1操作,此时局部变量表中的值由12减为11:

  • 在操作数栈中,执行第二次加法运算,得到结果为34:

⑦ 最后将操作数栈中的数据弹出到局部变量表中,赋值2号槽位b=34:

因此程序运行结果得到:a为11,b为34


8、HotSpot为什么要分为新生代和老年代?

HotSpot根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

  • 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
  • 而在老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

其中新生代又分为1个伊甸园区(Eden)和2个幸存区(Survivor),通常称为From Survivor和To Survivor区。


9、说一下 JVM 的几个主要组成部分?

如图:(图片参考自https://blog.csdn.net/weixin_43591980/article/details/116903332

主要由 4 个部分组成:

  • 运行时数据区域:就是我们常说的JVM的内存。
  • 执行引擎:执行 class 字节码文件中的指令。
  • 类加载系统:根据给定的全限定名类名(如:java.lang.Object)来装载 .class 文件到运行时数据区中的方法区中。
  • 本地接口:与本地方法库交互,是其它编程语言交互的接口。

10、Java会存在内存泄漏吗?请简单描述

答案:Java中会存在内存泄漏。

Java中虽然存在 GC垃圾回收机制,及时回收不再被使用的对象。但是依然存在内存泄露的情况!

Java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露的发生场景。


参考文章:

总结的面试题也挺费时间的,文章会不定时更新,有时候一天多更新几篇,如果帮助您复习巩固了知识点,还请三连支持一下,后续会亿点点的更新!


为了帮助更多小白从零进阶 Java 工程师,从CSDN官方那边搞来了一套 《Java 工程师学习成长知识图谱》,尺寸 870mm x 560mm,展开后有一张办公桌大小,也可以折叠成一本书的尺寸,有兴趣的小伙伴可以了解一下,当然,不管怎样博主的文章一直都是免费的~


转载:https://blog.csdn.net/weixin_43591980/article/details/117348434
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场