JVM 八股二十问,你能扛到第几问?

作者:微信小助手

发布时间:2022-01-25T14:38:55

大家好,我是飘渺。
马上过完年又到了面试的高峰期,而一般情况下JVM在面试中是跑不掉的,所以特意给大家整理了一份JVM的常见面试题,刚好趁着过年的时候卷起来。话不多说,咱们直接开卷。

1.说说 JVM 内存区域

JVM 内存区域

这张图就是一个 JVM 运行时数据图, 紫色区域代表是线程共享的区域 ,JAVA 程序在运行的过程中会把他管理的内存划分为若干个不同的数据区域, 每一块儿的数据区域所负责的功能都是不同的,他们也有不同的创建时间和销毁时间

  • 1.程序计数器

    • 程序计数器是 程序控制流的指示器,循环,跳转,异常处理,线程的恢复等工作都需要依赖程序计数器去完成 。程序计数器是 线程私有 的,它的 生命周期是和线程保持一致 的,我们知道,N 个核心数的 CPU 在同一时刻,最多有  N个线程同时运行,在我们真实的使用过程中可能会创建很多线程,JVM 的多线程其实是通过线程轮流切换,分配处理器执行时间来实现的。既然涉及的线程切换,所以每条线程必须有一个独立的程序计数器。
  • 2.虚拟机栈

    • 虚拟机栈,其描述的就是线程内存模型, 也可以称作线程栈 ,也是每个 线程私有 的, 生命周期与线程保持一致 。在每个方法执行的时候,jvm 都会同步创建一个栈帧去存储局部变量表,操作数栈,动态连接,方法出口等信息。一个方法的生命周期就贯彻了一个栈帧从入栈到出栈的全部过程。
  • 3.本地方法栈 本地方法栈的概念很好理解,我们知道,java底层用了很多c的代码去实现,而其调用c端的方法上都会有native,代表本地方法服务,而本地方法栈就是为其服务的。

  • 4.堆 堆可以说是jvm中最大的一块儿内存区域了,它是所有线程共享的,不管你是初学者还是资深开发,多少都会听说过堆,毕竟几乎所有的对象都会在堆中分配。

  • 5.方法区

    • 方法区也是所有 线程共享 的区域,它 存储 了被 jvm 加载的 类型信息、常量、静态变量等数据 。运行时常量池就是方法区的一部分,编译期生成的各种字面量与符号引用就存储在其中。
  • 6.直接内存

    • 这部分数据并 不是 jvm 运行时数据区的一部分 ,nio 就会使用到直接内存,也可以说 堆外内存 ,通常会 配合虚引用一起去使用 ,就是为了资源释放,会将堆外内存开辟空间的信息存储到一个队列中,然后GC会去清理这部分空间。堆外内存优势在 IO 操作上,对于网络 IO,使用 Socket 发送数据时,能够节省堆内存到堆外内存的数据拷贝,所以性能更高。看过 Netty 源码的同学应该了解,Netty 使用堆外内存池来实现零拷贝技术。对于磁盘 IO 时,也可以使用内存映射,来提升性能。另外,更重要的几乎不用考虑堆内存烦人的 GC 问题。但是既然是内存。也会受到本机总内存的限制,

2.垃圾对象是怎么找到的?

  • 1.引用计数算法

就是给对象添加一个计数器

  • 每当有一个地方引用它的时候,计数器就加1
  • 每当有一个引用失效的时候,计数器就减1

当计数器的值为0的时候,那么该对象就是垃圾了 这种方案的原理很简单,而且判定的效率也非常高,但是却可能会有其他的额外情况需要考虑。

相互引用

比如两个对象循环引用 ,a 对象引用了 b 对象,b 对象也引用了 a 对象,a、b 对象却没有再被其他对象所引用了,其实正常来说这两个对象已经是垃圾了,因为没有其他对象在使用了,但是计数器内的数值却不是 0,所以引用计数算法就无法回收它们。这种算法是比较直接的找到垃圾 ,然后去回收,也被称为"直接垃圾收集"。

  • 2.根可达算法

这也是 JVM 默认使用 的寻找垃圾算法它的原理就是定义了一系列的根,我们把它称为 "GC Roots" ,从 "GC Roots" 开始往下进行搜索,走过的路径我们把它称为 "引用链" ,当一个对象到 "GC Roots" 之间没有任何引用链相连时,那么这个对象就可以被当做垃圾回收了。

root search

如图, 根可达算法 就可以 避免 计数器算法不好解决的 循环引用问题 ,Object 6、Object 7、Object 8彼此之前有引用关系,但是 没有与"GC Roots" 相连,那么就会被当做垃圾所回收

3.GC Roots 有哪些?

在java中,有固定的GC Roots 对象不固定的临时GC Roots对象 :

固定的GC Roots:

  • 1.在 虚拟机栈(栈帧的本地变量表)中所引用的对象 ,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如 Java 类的 引用静态变量
  • 在方法区中 常量引用的对象 ,譬如字符串常量池中的引用。
  • 在方法区栈中   JNI (譬如 Native 方法)引用的对象
  • Java   虚拟机内部的引用 ,如基本数据类型对应的 Class 对象,一些常驻的异常对象(空指针异常、OOM等),还有类加载器。
  • 所有 被 Synchronized 持有的对象
  • 反应 Java 虚拟机内部情况的   JMXBean、JVMTI 中注册的回调本地代码缓存等

临时GC Roots:

  • 为什么会有临时的 GC Roots ? :目前的垃圾回收大部分都是 分代收集和局部回收 ,如果只针对某一部分区域进行局部回收,那么就必须要考虑的 当前区域的对象有可能正被其他区域的对象所引用 ,这时候就要将这部分关联的对象也添加到 GC Roots 中去来确保根可达算法的准确性。这种算法是利用了 逆向思维 ,找到使用的对象,剩下的就是垃圾,也被称为"间接垃圾收集"。

4.java 有哪四种引用类型?

  • 1.强引用

"Object o = new Object()" 就是一种强引用关系,这也是我们在代码中最常用的一种引用关系。无论任何情况下,只要强引用关系还存在,垃圾回收器就不会回收掉被引用的对象。

  • 2.软引用

当内存空间不足时,就会回收软引用对象。

// 软引用  
SoftReference<String> softRef = new SoftReference<String>(str);

软引用用来描述那些有用但是没必要的对象。

  • 3.弱引用

弱引用要比软引用更弱一点,它 只能够存活到下次垃圾回收之前 。也就是说,垃圾回收器开始工作,会回收掉所有只被弱引用关联的对象。

WeakReference<String> weakRef = new WeakReference<String>(str);

ThreadLocal 中就使用了弱引用来防止内存泄漏。

  • 4.虚引用

虚引用是最弱的一种引用关系,它的唯一作用是用来作为一种通知。如零拷贝(Zero Copy),开辟了堆外内存,虚引用在这里使用,会将这部分信息存储到一个队列中,以便于后续对堆外内存的回收管理。

5.说一说分代收集理论

大多数的垃圾回收器都遵循了分代收集的理论进行设计,它建立在两个分代假说之上:

  • 弱分代假说 :绝大多数对象都是朝生夕灭的。
  • 强分代假说 :熬过越多次数垃圾回收过程的对象就越难消亡。

这两种假说的设计原则都是相同的:垃圾收集器 应该将jvm划分出不同的区域 ,把那些较难回收的对象放在一起(一般指老年代),这个区域的垃圾回收频率就可以降低,减少垃圾回收的开销。剩下的区域(一般指新生代)可以用较高的频率去回收,并且只需要去关心那些存活的对象,也不用标记出需要回收的垃圾,这样就能够以较低的代价去完成垃圾回收。

  • 跨代引用假说 :如果某个新生代的对象存在了跨代引用,但是老年代的对象是很难消亡的,那么随着时间的推移,这个新生代对象也会慢慢晋升为老年代对象,那么这种跨代引用也就被消除了。

由于跨代引用是很少的,所以我们不应该为了少量的跨代引用去扫描整个老年代的数据,只需要在新生代对象建立一个 记忆集 来记录引用信息。记忆集: 将老年代分为若干个小块,每块区域中有 N 个对象 ,在对象引用信息发生变动的时候来维护记忆集数据的准确性,这样每次发生了  "Minor GC"  的时候只需要将记忆集中的对象添加到  "GC Roots"  中就可以了。

6.垃圾收集算法有哪些?

总共有三种

  • 1.标记清除算法

这种算法的实现是很简单的,有两种方式

  • 1.标记出垃圾,然后清理掉
  • 2.标记出存货的对象,回收其他空间
标记清除算法

这种算法有两个缺点

  • 1.随着对象越来越多,那么所需要消耗的时间就会越来越多

  • 2.标记清除后会导致碎片化,如果有大对象分配很有可能分配不下而出发另一次的垃圾收集动作

  • 2.标记复制算法

这种算法解决了第一种算法碎片化的问题。就是 开辟两块完全相同的区域 ,对象只在其中一篇区域内分配,然后 标记 出那些 存活的对象,按顺序整体移到另外一个空间 ,如下图,可以看到回收后的对象是排列有序的,这种操作只需要移动指针就可以完成,效率很高, 之后就回收移除前的空间

标记复制算法

这种算法的缺点也是很明显的

  • 浪费过多的内存,使现有的 可用空间变为 原先的 一半

  • 3.标记整理算法