快来看!2023超真实的JVM相关面试题!!

2.1、介绍下Java内存区域(运⾏时数据区)。

1684114672480.jpg

Java虚拟机在执⾏Java程序的过程中会把它所管理的内存划分为以下6个运⾏时数据区域。

1)程序计数器(Program Counter Register)

⼀块较⼩的内存空间,可以看作当前线程所执⾏的字节码的⾏号指⽰器。如果线程正在执⾏的是⼀个Java⽅法,这个计数器记录的是正在执⾏的虚拟机字节码指令的地址;如果正在执⾏的是Native⽅法,这个计数器值则为空。

2)Java虚拟机栈(Java Virtual Machine Stacks)

与程序计数器⼀样,Java虚拟机栈也是线程私有的,它的⽣命周期与线程相同。虚拟机栈描述的是Java⽅法执⾏的内存模型:每个⽅法在执⾏的同时都会创建⼀个栈帧⽤于存储局部变量表、操作数栈、动态链接、⽅法出⼝等信息。每⼀个⽅法从调⽤直⾄执⾏完成的过程,就对应着⼀个栈帧在虚拟机栈中⼊栈到出栈的过程。

3)本地⽅法栈(Native Method Stack)

本地⽅法栈与虚拟机栈所发挥的作⽤是⾮常相似的,它们之间的区别不过是虚拟机栈为虚拟机执⾏Java⽅法(也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的Native⽅法服务。

4)Java堆(Java Heap)

对⼤多数应⽤来说,Java堆是Java虚拟机所管理的内存中最⼤的⼀块。Java堆是被所有线程共享的

⼀块内存区域,在虚拟机启动时创建。此内存区域的唯⼀⽬的就是存放对象实例,⼏乎所有的对象实例都在这⾥分配内存。

5)元数据区(Method Area

在JDK1.7的时候,有⼀个JVM内存区域中有⼀块⽅法区,主要存放虚拟机加载的类信息,静态变量,常量等。JDK1.8时,移除了⽅法区的概念,⽤⼀个元数据区代替。与Java堆⼀样,是各个线程共享的内存区域,它⽤于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。⽅法区是JVM规范中定义的⼀个概念,具体放在哪⾥,不同的实现可以放在不同的地⽅。

6)运⾏时常量池(Runtime Constant Pool)

运⾏时常量池是⽅法区的⼀部分。Class⽂件中除了有类的版本、字段、⽅法、接⼝等描述信息外,还有⼀项信息是常量池,⽤于存放编译期⽣成的各种字⾯量和符号引⽤,这部分内容将在类加载后进⼊⽅法区的运⾏时常量池中存放。

2.2、怎么判定对象已经“死去”?

常⻅的判定⽅法有两种:引⽤计数法和可达性分析算法,HotSpot中采⽤的是可达性分析算法。

引⽤计数法

给对象中添加⼀个引⽤计数器,每当有⼀个地⽅引⽤它时,计数器值就加1;当引⽤失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使⽤的。客观地说,引⽤计数算法的实现简单,判定效率也很⾼,在⼤部分情况下它都是⼀个不错的算法,但是主流的Java虚拟机⾥⾯没有选⽤引⽤计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引⽤的问题。

可达性分析算法

这个算法的基本思路就是通过⼀系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所⾛过的路径称为引⽤链,当⼀个对象到GC Roots没有任何引⽤链相连(⽤图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可⽤的。如下图所⽰,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

1684114988406.jpg

1684115053611.jpg

2.3、介绍下四种引⽤(强引用、软引用、弱引用、虚引用)?

强引在程序代码之中普遍存在的,类似“Object obj=new Object()”这类的引⽤,只要强引⽤还存在,垃圾收集器永远不会回收掉被引⽤的对象。

软引⽤:⽤来描述⼀些还有⽤但并⾮必需的对象,使⽤SoftReference类来实现软引⽤,在系统将要发⽣内存溢出异常之前,将会把这些对象列进回收范围之中进⾏第⼆次回收。

弱引⽤:⽤来描述⾮必需对象的,使⽤WeakReference类来实现弱引⽤,被弱引⽤关联的对象只能⽣存到下⼀次垃圾收集发⽣之前。

虚引⽤:是最弱的⼀种引⽤关系,使⽤PhantomReference类来实现虚引⽤,⼀个对象是否有虚引⽤的存在,完全不会对其⽣存时间构成影响,也⽆法通过虚引⽤来取得⼀个对象实例。为⼀个对象设置

虚引⽤关联的唯⼀⽬的就是能在这个对象被收集器回收时收到⼀个系统通知。

2.4、垃圾收集有哪些算法,各⾃的特点?

清除算法

⾸先标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象。它的主要不⾜有两个:⼀个是效率问题,标记和清除两个过程的效率都不⾼;另⼀个是空间问题,标记清除之后会产⽣⼤量不连续的内存碎⽚,空间碎⽚太多可能会导致以后在程序运⾏过程中需要分配较⼤对象时,⽆法找到⾜够的连续内存⽽不得不提前触发另⼀次垃圾收集动作。

复制算法

为了解决效率问题,⼀种称为“复制”(Copying)的收集算法出现了,它将可⽤内存按容量划分为⼤⼩相等的两块,每次只使⽤其中的⼀块。当这⼀块的内存⽤完了,就将还存活着的对象复制到另外⼀块上⾯,然后再把已使⽤过的内存空间⼀次清理掉。这样使得每次都是对整个半区进⾏内存回收,内存分配时也就不⽤考虑内存碎⽚等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运⾏⾼效。只是这种算法的代价是将内存缩⼩为了原来的⼀半,未免太⾼了⼀点。

标记 - 整理算法

根据⽼年代的特点,有⼈提出了另外⼀种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法⼀样,但后续步骤不是直接对可回收对象进⾏清理,⽽是让所有存活的对象都向⼀端移动,然后直接清理掉端边界以外的内存。

分代收集算法

当前商业虚拟机的垃圾收集都采⽤“分代收集”算法,这种算法并⽆新的⽅法,只是根据对象的存活周期的不同将内存划分为⼏块,⼀般是把Java堆分为新⽣代和⽼年代,这样就可以根据各个年代的特点采⽤最适当的收集算法。在新⽣代中,每次垃圾收集时都发现有⼤批对象死去,只有少量存活,那就选⽤复制算法,只需要付出少量存活对象的复制成本就可以完成收集。⽽⽼年代中因为对象存活率⾼、没有额外空间对它进⾏分配担保,就必须使⽤“标记-清理”或“标记-整理”算法来进⾏回收。

2.5、HotSpot为什么要分为新⽣代和⽼年代?

HotSpot根据对象存活周期的不同将内存划分为⼏块,⼀般是把Java堆分为新⽣代和⽼年代,这样就可以根据各个年代的特点采⽤最适当的收集算法。在新⽣代中,每次垃圾收集时都发现有⼤批对象死去,只有少量存活,那就选⽤复制算法,只需要付出少量存活对象的复制成本就可以完成收集。⽽⽼年代中因为对象存活率⾼、没有额外空间对它进⾏分配担保,就必须使⽤“标记—清理”或者“标记—整理”算法来进⾏回收。

其中新⽣代⼜分为1个Eden区和2个Survivor区,通常称为From Survivor和To Survivor区。1684117076146.jpg 2.6、新⽣代中Eden区和Survivor区的默认⽐例?

在HotSpot虚拟机中,Eden区和Survivor区的默认⽐例为8:1:1,即-XX:SurvivorRatio=8,其中Survivor分为From Survivor和ToSurvivor,因此Eden此时占新⽣代空间的80%。

2.7、HotSpot GC的分类?

针对HotSpot VM的实现,它⾥⾯的GC其实准确分类只有两⼤种:

1)Partial GC:并不收集整个GC堆的模式,具体如下:

  • 1. Young GC/Minor GC:只收集新⽣代的GC。

  • 2. Old GC:只收集⽼年代的GC。只有CMS的concurrent collection是这个模式。

  • 3. Mixed GC:收集整个新⽣代以及部分⽼年代的GC,只有G1有这个模式。

2)Full GC/Major GC:收集整个GC堆的模式,包括新⽣代、⽼年代、永久代(如果存在的话)等所有部分的模式。FullGC是对整个堆来说的,出现Full GC的时候经常伴随⾄少⼀次的Minor GC,但⾮绝对的。MajorGC的速度⼀般会⽐Minor GC慢10倍以上。

2.8、HotSpot GC的触发条件?

这⾥只说常⻅的Young GC和Full GC。

Young GC:当新⽣代中的Eden区没有⾜够空间进⾏分配时会触发Young GC。

Full GC:

当准备要触发⼀次Young GC时,如果发现统计数据说之前Young GC的平均晋升⼤⼩⽐⽬前⽼年代

剩余的空间⼤,则不会触发Young GC⽽是转为触发Full GC。(通常情况)

  • 1.当准备要触发⼀次Young GC时,如果发现统计数据说之前Young GC的平均晋升⼤⼩⽐⽬前⽼年代剩余的空间⼤,则不会触发Young GC⽽是转为触发Full GC。(通常情况)

  • 2. 如果有永久代的话,在永久代需要分配空间但已经没有⾜够空间时,也要触发⼀次Full GC。

  • 3. System.gc()默认也是触发Full GC。

  • 4. heap dump带GC默认也是触发Full GC。

  • 5. CMS GC时出现Concurrent Mode Failure会导致⼀次Full GC的产⽣。

2.9、Full GC后⽼年代的空间反⽽变⼩?

HotSpot的Full GC实现中,默认新⽣代⾥所有活的对象都要晋升到⽼年代,实在晋升不了才会留在新

⽣代。假如做Full GC的时候,⽼年代⾥的对象⼏乎没有死掉的,⽽新⽣代⼜要晋升活对象上来,那么

Full GC结束后⽼年代的使⽤量⾃然就上升了。

2.10、什么情况下新⽣代对象会晋升到⽼年代?

如果新⽣代的垃圾收集器为Serial和ParNew,并且设置了-XX:PretenureSizeThreshold参数,当

对象⼤于这个参数值时,会被认为是⼤对象,直接进⼊⽼年代。

  • 1.如果新⽣代的垃圾收集器为Serial和ParNew,并且设置了-XX:PretenureSizeThreshold参数,当对象⼤于这个参数值时,会被认为是⼤对象,直接进⼊⽼年代

  • 2. Young GC后,如果对象太⼤⽆法进⼊Survivor区,则会通过分配担保机制进⼊⽼年代。对象每在Survivor区中“熬过”⼀次Young GC,年龄就增加1岁,当它的年龄增加到⼀定程度(默认为15岁,可以通过-XX:MaxTenuringThreshold设置),就将会被晋升到⽼年代中。

  • 3.如果在Survivor区中相同年龄所有对象⼤⼩的总和⼤于Survivor空间的⼀半,年龄⼤于或等于该年龄的对象就可以直接进⼊⽼年代,⽆须等到MaxTenuringThreshold中要求的年龄。

2.11、新⽣代垃圾回收器和⽼年代垃圾回收器都有哪些?有什么区别?

• 新⽣代回收器:Serial、ParNew、Parallel Scavenge

• ⽼年代回收器:Serial Old、Parallel Old、CMS

• 整堆回收器:G1

新⽣代垃圾回收器⼀般采⽤的是复制算法,复制算法的优点是效率⾼,缺点是内存利⽤率低;⽼年代回收器⼀般采⽤的是标记-整理的算法进⾏垃圾回收。

  • 1)Serial收集器(复制算法): 新⽣代单线程收集器,标记和清理都是单线程,优点是简单⾼效;

  • 2)ParNew收集器 (复制算法): 新⽣代收并⾏集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着⽐Serial更好的表现;

  • 3)Parallel Scavenge收集器 (复制算法): 新⽣代并⾏收集器,追求⾼吞吐量,⾼效利⽤ CPU。吞吐量 = ⽤⼾线程时间/(⽤⼾线程时间+GC线程时间),吞吐量可以⾼效率的利⽤CPU时间,尽快完成程序的运算任务,适合后台应⽤等对交互相应要求不⾼的场景;

  • 4)Serial Old收集器 (标记-整理算法): ⽼年代单线程收集器,Serial收集器的⽼年代版本;

  • 5)Parallel Old收集器 (标记-整理算法): ⽼年代并⾏收集器,吞吐量优先,Parallel Scavenge收集器的⽼年代版本;

  • 6)CMS(Concurrent Mark Sweep)收集器(标记-清除算法): ⽼年代并⾏收集器,以获取最短回收停顿时间为⽬标的收集器,具有⾼并发、低停顿的特点,追求最短GC回收停顿时间。对于要求服务器响应速度的应⽤上,这种垃圾回收器⾮常适合。在启动 JVM 的参数上“XX:+UseConcMarkSweepGC”来指定使⽤ CMS 垃圾回收器。CMS 使⽤的是标记-清除的算法实现的,所以在 gc 的时候回产⽣⼤量的内存碎⽚,当剩余内存不能满⾜程序运⾏要求时,系统将会出现Concurrent Mode Failure,临时 CMS 会采⽤ Serial Old 回收器进⾏垃圾清除,此时的性能将会被降低。

  • 7)G1(Garbage First)收集器 (标记-整理算法): Java堆并⾏收集器,G1收集器是JDK1.7开始提供的⼀个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产⽣内存碎⽚。此外,G1收集器不同于之前的收集器的⼀个重要特点是:G1回收的范围是整个Java堆(包括新⽣代,⽼年代),⽽前六种收集器回收的范围仅限于新⽣代或⽼年代。