JVM内存模型最新面试题(持续更新)

发布于:2024-05-19 ⋅ 阅读:(198) ⋅ 点赞:(0)

问题:java中创建的对象一般放在哪里?

回答:

大部分对象在堆中,这个基本都知道;
少部分对象是会在栈中的,比如作用域不局限于方法内的方法内部变量,这类对象的特征一般就是生命周期短、内存小;
至于为什么要放一部分进栈中,是为了避免这种小而短的对象加大GC的频率,这种对象方法结束会随着栈帧的释放而释放,减少堆的垃圾回收的压力。
在jdk1.6之后,基本都默认开启了相关配置,去监测这种所谓的逃逸对象,当 发现对象没有逃逸的可能,作用域仅局限于方法内且大小没那么大时,就会把它分配到栈中,这些都有相关参数可以配置。


问题:讲了栈中的对象,那你讲讲放入堆中的对象一般是怎么处理的?

回答:

在这里插入图片描述

常规情况下,我们创建的对象,都默认是生命周期比较短的,优先分配在年轻代的Eden区。
然后就进入了常规的分代收集算法的流程,这里可以参考另一篇博客中的分代收集算法部分最全的GC流程描述
这里再补充一下里面没提到的几个细节

  1. 为什么默认年龄是15? 在程序编程中,15是个很敏锐的数字,因为是2^4-1,也就是4个比特位所能表达的对象大小。事实上,java对象头上存储分代年龄的长度确实就是4bit,详见多线程原理之synchronized锁的原理
  2. 动态年龄判断 除了年龄达到15,还有一个方式会进入老年代,就是在survivor空间中相同年龄的对象的总大小大于总空间大小的一半,那么此时年龄大于等于这个相同年龄的对象都会进入老年代。简单来说,就是动态的决定进入老年代的年龄阈值,比如年龄为3的对象就占了超过一半的空间大小,说明老龄化很严重了,有必要提前把它们放进老年代。
  3. 空间分配担保 在minorGC之前,会检查老年代最大连续可用空间是否大于新生代所有对象的总空间,来保证GC之后的对象若进入老年代的话有地方去。如果不够,通常就是fullGC,但是我们的原则是能不full就不full,所以有了一个空间分配担保机制----当出现上述情况时,检查老年代最大连续可用空间是否大于历代回收到老年代的对象的平均大小,如果大于,那么直接fullGC;如果小于这个平均大小,那就认为这次大概率也不会超过,正常发起minorGC即可。

问题:那上面有提到minorGC,为什么要避免fullGC呢?fullGC一般发生在什么时候?一般分为几种GC呢?

回答:

根据垃圾收集回收的区域不同,垃圾收集主要分为:
Young GC
Old GC
Full GC
Mixed GC
Young GC

新生代内存的垃圾收集事件称为 Young GC(又称 Minor GC),当 JVM 无法为新对象分配在新生代内存空间时总会触发 Young GC。
比如 Eden 区占满时,新对象分配频率越高,Young GC 的频率就越高。
Young GC 每次都会引起全线停顿(Stop-The-World),简称STW,暂停所有的应用线程,停顿时间相对老年代 GC 造成的停顿,几乎可以忽略不计。而且触发频繁,需要一种高效的回收算法。
Old GC 、Full GC、Mixed GC
Old GC:只清理老年代空间的 GC 事件,只有 CMS 的并发收集是这个模式。
Full GC:清理整个堆的 GC 事件,包括新生代、老年代、元空间等 。当老年代或者持久代(元空间)满了,或者System.gc被显式的调用都会触发Full GC。fullGC每次调用也会STW,比Ygc的时间要久多了,所以要尽量避免。
Mixed GC:清理整个新生代以及部分老年代的 GC,只有 G1 有这个模式


问题:那如果fullGC完空间还是不够会发生什么?

回答:

会抛错,也就是我们熟知的OOM内存溢出
OOM的排查思路


问题:讲一讲垃圾收集器吧,大概有几种

回答:

收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现
在这里插入图片描述
serial、parNew、Parallel Scavenage是年轻代收集器,使用的都是复制算法,因为复制的存活对象一般比较少;
下面的so、cms、po(简称)是老年代收集器,使用的都是标记-清除或者标记整理算法,因为需要标记清除的对象比较少;
当然是可以组合使用的
比如JDK1.8用的就是PS+PO,这是一个吞吐量优先的组合
pn+cms则是响应时间优先的组合
G1是包含了年轻代和老年代的收集器
ZGC是一个jdk11的试验品
Epsilon是一个调试工具

其中,serial和so都是单线程串行的,加上回收的时候要stop-the-world,基本被淘汰了
所以,为了减少垃圾回收的时候,jdk1.8才使用了并行的垃圾回收器组合,PS和P0,他们在回收的时候,是有多个GC线程并行的,当然在这个过程中用户线程还是等待。
所以,为了再次减少STW等待时间,才有了CMS以及后面的G1,不断的优化嘛


问题:那你说说CMS收集器是怎么减少STW时间的

回答:

我们首先确认一个前提,那就是垃圾回收和用户线程肯定不能做到百分百异步的,因为他们要做的事情本身就存在一定的重合和冲突;
为了优化STW,减少等待时间,我们能做的无非就是细化拆分垃圾回收的过程,看哪些过程是可以和用户线程并行的,而哪些是雀氏不能的。
之前的PSPO或者串行收集器,都是一股脑的STW
CMS就把这个过程细分为了4个阶段

  1. 初始标记(STW),这个过程主要是把GCROOT能够直接关联的根对象标记出来,因为不用遍历,所以比较快,STW时间会短一些。
  2. 并发标记,这个过程就是从根对象开始遍历了,比较耗时
  3. 修正标记(STW),理想状况下,上面两步结束之后就可以清除了;但是并发标记阶段难免会有一些新的变动,所以这一步主要是修正,由于数量较少,所以也比较快
  4. 清除阶段,这一步就是把标记好的垃圾进行清除了,比较耗时,但是也可以和用户线程并行。
    通过这么拆分,CMS达到了并发收集低延迟的效果

问题:细说修正标记的过程,了解过浮动垃圾么

回答:

修正标记的过程无非会遇到两种情况

  1. 新的可用对象,这种是在并发遍历标记的过程中新创建的,很容易就能找到,而且数量一般较少,所以不耗时,而且必须要标记,不然会把要用的对象当作垃圾清除掉。
  2. 新的垃圾,也就是浮动垃圾。这种是在并发遍历标记的过程中被弃用的,不重新标记的话,本次清理会成为漏网之鱼。但因为要把一个可达变为不可达的对象找出来,需要重新遍历,相当于重新进行一次第一步的过程,代价太大了,所以CMS不处理。而且无非就是少清理一点垃圾而已,算是一个小缺点,但是还能接受。

问题:那除了浮动垃圾之外,CMS还有什么缺点?

回答:

有失必有得,正因为它在垃圾回收阶段要和用户线程并行,所以不能像之前的全程STW一样,堆区满了才进行;要给用户线程留一定的执行空间,所以垃圾回收也就是FullGc的阈值会低一些。否则在CMS运行期间,用户线程所需的空间不够,也会报错的。这个时候只能启动后备预案,planB,临时用串行收集器来重新进行垃圾收集,这样的耗时就会很长了。


问题:CMS为什么要使用标记清除,不使用标记整理?

回答:

还是一样的原因,因为垃圾回收的过程中用户线程也要用内存,所以只能动要清除的内存。所带来的代价就是,标记清除相比标记整理,会产生大量的空间碎片,变相的提升了fullGC的频率。
空间和时间上总要有所取舍的,没办法,为了低延迟只能降低一些吞吐量。


问题:说了这么多CMS,那谈谈G1吧,对G1有什么了解

回答:

CMS作为一个老年代收集器,具有低延迟的有点,但是吞吐量相对较低;为了更加灵活的控制延迟和吞吐量的关系,最大化利用日益剧增的硬件条件,在此基础上推出了G1.
在这里插入图片描述

G1是一个包含有老年代和年轻代的收集器,它最大的特点就是把内存划分为了一个个的region,也就是会有多个Eden和Survivor以及Old,此外还新增了一个H区来存放大对象。
它的回收阶段类似CMS,也是四个阶段,主要区别在于第四阶段。
在第四阶段中,得益于Region的划分,G1会先把各个Region的回收价值(回收空间综合回收时间来计算)做个排序,然后可以根据用户的预期参数来决定回收多少Region,来灵活的控制延迟时间。这个过程可以不STW,但是即便STW了也很快。
除了可预测的停顿时间外,这种Region的划分也极大的减少了内存的碎片化(因为已经提前划分成了一个个相对较大,能够接受的大碎片,每次回收完,这个大碎片内是空的完整的,使用了标记压缩和复制算法)


网站公告

今日签到

点亮在社区的每一天
去签到