【Android面试】2023最新面试专题:Java虚拟机原理(二)

4 GC的流程是怎么样的?介绍下GC回收机制与分代回收策略

这道题想考察什么?

Java基础掌握情况,掌握对象回收过程以避免开发时出现内存问题

考察的知识点

GC机制

考生如何回答

说到GC垃圾回收,首先要知道什么是“垃圾”,垃圾就是没有用的对象,那么怎样判定一个对象是不是垃圾(能不能被回收)?Java 虚拟机中使用一种叫作可达性分析的算法来决定对象是否可以被回收。

可达性分析

可达性分析就通过一组名为”GC Root"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。

在这里插入图片描述

GC Root指的是:

  • Java 虚拟机栈(局部变量表)中的引用的对象。也就是正在运行的方法中的局部变量所引用的对象
  • 方法区中静态引用指向的对象。也就是类中的static修饰的变量所引用的对象
  • 方法区中常量引用的对象。
  • 仍处于存活状态中的线程对象。
  • Native 方法中 JNI 引用的对象。
优点

可达性分析可以解决引用计数器所不能解决的循环引用问题。即便对象a和b相互引用,只要从GC Roots出发无法到达a或者b,那么可达性分析便不会将它们加入存活对象合集之中。

缺点

在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为null)或者漏报(将引用设置为未被访问过的对象)。误报并没有什么伤害,Java虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存。 一旦从原引用访问已经被回收了的对象,则很有可能会直接导致Java虚拟机崩溃。

垃圾回收算法

在标记出对象是否可被回收后,接下来就需要对可回收对象进行回收。基本的回收算法有:标记-清理、标记-整理与复制算法。

标记清除算法

从”GC Roots”集合开始,将内存整个遍历一次,保留所有可以被 GC Roots 直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,过程分为 标记清除 两个步骤。

在这里插入图片描述

  • 优点:实现简单,不需要将对象进行移动。
  • 缺点:这个算法需要中断进程内其他组件的执行(stop the world),并且可能产生内存碎片,提高了垃圾回收的频率。
标记整理算法

与标记-清除不同的是它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。

  • 优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
  • 缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。
复制算法

将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

在这里插入图片描述

  • 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
  • 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

分代回收策略

不同的垃圾收集器实现采用不同的算法进行垃圾回收,除此之外现代虚拟机还会采用分代机制来进行垃圾回收,根据对象存活的周期不同,把堆内存划分为不同区域,不同区域采用不同算法进行垃圾回收。

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

代际划分

在这里插入图片描述

堆内存分为年轻代(Young Generation)和老年代(Old Generation)。而持久代使用非堆内存,主要用于存储一些类的元数据,常量池,java类,静态文件等信息。

垃圾回收

年轻代会划分出Eden区域与两个大小对等的Survivor区域。 其比例一般为8:1:1,这是因为根据统计95%的对象朝生夕死,存活时间极短。

  1. 新生成的对象优先存放在新生代中
  2. 存活率很低,回收效率很高
  3. 一般采用的 GC 回收算法是复制算法

当新对象生成,并且在Eden申请空间失败时,就会触发GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区,然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。所以一般在这里需要使用速度快、效率高的算法,使Eden区能尽快空闲出来。

在这里插入图片描述

minor gc

新对象的内存分配都是先在Eden区域中进行的,当Eden区域的空间不足于分配新对象时,就会触发年轻代上的垃圾回收,我们称之为"minor gc"。同时,每个对象都有一个“年龄”,这个年龄实际上指的就是该对象经历过的minor gc的次数。如图1所示,当对象刚分配到Eden区域时,对象的年龄为“0”,当minor gc被触发后,所有存活的对象(仍然可达对象)会被拷贝到其中一个Survivor区域,同时年龄增长为“1”。并清除整个Eden内存区域中的非可达对象。

当第二次minor gc被触发时,JVM会通过Mark算法找出所有在Eden内存区域和Survivor1内存区域存活的对象,并将他们拷贝到新的Survivor2内存区域(这也就是为什么需要两个大小一样的Survivor区域的原因),同时对象的年龄加1. 最后,清除所有在Eden内存区域和Survivor1内存区域的非可达对象。

当对象的年龄足够大(年龄可以通过JVM参数进行指定,默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同 ),当minor gc再次发生时,它会从Survivor内存区域中升级到年老代中,如图3所示。

major gc

当minor gc发生时,又有对象从Survivor区域升级到Tenured区域,但是Tenured区域已经没有空间容纳新的对象了,那么这个时候就会触发年老代上的垃圾回收,我们称之为"major gc"。而在年老代上选择的垃圾回收算法则取决于JVM上采用的是什么垃圾回收器。

总结

在JVM中一般采用可达性分析法进行是否可回收的判定,确定对象需要被回收后,对象在哪个代际将会采用不同的垃圾回收算法进行回收,这些算法包括:标记-清除,标记-整理与复制算法。

而之所以采用分代策略的原因是:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。 如果每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,而对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。

5 Java中对象如何晋升到老年代?

这道题想考察什么?

Java基础掌握情况,掌握对象回收过程以避免开发时出现内存问题

考察的知识点

GC机制

考生如何回答

新对象的内存分配都是先在Eden区域中进行的,当Eden区域的空间不足于分配新对象时,就会触发年轻代上的垃圾回收,我们称之为"minor gc"。同时,每个对象都有一个“年龄”,这个年龄实际上指的就是该对象经历过的minor gc的次数。当对象的年龄足够大,当minor gc再次发生时,它会从Survivor内存区域中升级到年老代中。

因此对象晋升老年代的条件之一为:若年龄超过一定限制(如15),则被晋升到老年态。**即长期存活的对象进入老年代。**除此之外,以下情况都会导致对象晋升老年代:

  • 大对象直接进入老年代

    ​ 多大由JVM参数-XX:PretenureSizeThreshold=x 决定;

  • 动态对象年龄判定

    ​ 当 Survivor 空间中相同年龄所有对象的大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而不需要达到默认的分代年龄。

除了以上提到的几种情况外,其实还有一种可能导致对象晋升老年代:分配担保机制。

空间分配担保

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,

如果大于,则此次Minor GC是安全的

如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。

为什么要进行空间担保?

新生代一般采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。

Minor Gc后的对象太多无法放入Survivor区怎么办?

假如在发生gc的时候,eden区里有150MB对象存活,而Survivor区只有100MB,无法全部放入,这时就必须把这些对象直接转移到老年代里。

如果Minor gc后新生代的对象都存活下来,然后需要全部转移到老年代,但是老年代空间不够,怎么办?

这时如果设置了 "-XX:-HandlePromotionFailure"的参数,就会尝试判断,看老年代内存大小是否大于之前每一次Minor gc后进入老年代的对象的平均大小。比如说,之前Minor gc 平均10M左右的对象进入老年代,此时老年代可用内存大于10MB,那么大概率老年代空间是足够的。

1、如果判断老年代空间不够,或者是根本没设置这个参数,那就直接触发"Full GC(对整个堆回收,包含:年轻代、老年代)",对老年代进行垃圾回收,腾出空间。

2、如果判断老年代空间足够,就冒风险尝试Minor gc。这时有以下几种可能。

  • Minor Gc 后,剩余的存活对象大小,小于Survivor区,那就直接进入Survivor区。
  • MInor Gc 后,剩余的存活对象大小,大于Survivor区,小于老年代可用内存,那就直接去老年代。
  • Minor Gc后,大于Survivor,老年代,很不幸,就会发生"Handle Promotion Failure"的情况 ,触发"Full GC"。

如果 Full gc后老年代还是没有足够的空间存放剩余的存活对象,那么就会导致**“OOM”** 内存溢出。

总结

实际上有四种情况可能会导致对象晋升老年代:

  • 大对象直接进入老年代
  • 年龄超过阈值
  • 动态对象年龄判定
  • 年轻代空间不足

6 判断对象是否被回收,有哪些GC算法,虚拟机使用最多的是什么算法?(美团)

这道题想考察什么?

是否掌握可达性分析法了解如何确定对象是否可被回收,从而避免程序内存泄露问题

考察的知识点

GC机制、可达性分析发、引用计数

考生如何回答

Java利用GC机制让开发者不必再像C/C++手动回收内存,但是并不是有了GC机制就万事皆可,在不了解GC机制算法的情况下,很容易出现代码问题导致该回收的对象无法被回收(内存泄漏),一直占用内存,导致程序可用内存越来越少,最终出现OOM。

首先GC会帮助我们自动回收内存,那么GC是如何确定对象是否可被回收的?在**《4 介绍下GC回收机制与分代回收策略》中介绍了可达性分析法**。而除了可达性分析法之外,还有被淘汰的引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

目前主流的java虚拟机都摒弃掉了这种算法,最主要的原因是它很难解决对象之间相互循环引用的问题,尽管该算法执行效率很高。比如:

class A{
    
    
    B b;
}
class B{
    
    
    A a;
}

A a = new A();
B b = new B();
//循环引用 导致无法释放
a.b = b;
b.a = a;

因此实际现在Java虚拟机都是采用的可达性分析法

最后

此面试题会持续更新,请大家多多关注!!!!

有需要以上面试题的朋友,可以扫描下方二维码免费领取~~

扫码还可以享受ChatGPT机器人的服务,可不要错过!!!

图片

猜你喜欢

转载自blog.csdn.net/datian1234/article/details/131710087