2024年最新JVM(二):垃圾收集器与内存分配策略,廖师兄springcloud视频

最后

在面试前我整理归纳了一些面试学习资料,文中结合我的朋友同学面试美团滴滴这类大厂的资料及案例

MyBatis答案解析
由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

大家看完有什么不懂的可以在下方留言讨论也可以关注。

觉得文章对你有帮助的话记得关注我点个赞支持一下!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

GCFinalizeDo.gcFinalizeDo = this;

System.out.println(“==尝试自救”);

}

public static void main(String[] args) throws InterruptedException {

GCFinalizeDo.gcFinalizeDo = new GCFinalizeDo();

GCFinalizeDo.gcFinalizeDo = null;

System.gc();

if(gcFinalizeDo != null){

System.out.println(“=自救成功”);

}else{

System.out.println(“=自救失败”);

}

}

}

结果如下

在这里插入图片描述

有点诡异,先输出自救失败,又进行尝试自救,这是因为Finalizer线程执行finalize方法的优先级比较低,前面提到过Finalizer是一个自动建立的、低调度优先级的线程

改动一下

public class GCFinalizeDo {

public static GCFinalizeDo gcFinalizeDo;

/**

  • 重写finalize方法进行自救

  • @throws Throwable

*/

@Override

protected void finalize() throws Throwable {

super.finalize();

//与静态变量进行关联,自救

GCFinalizeDo.gcFinalizeDo = this;

System.out.println(“==尝试自救”);

}

public static void main(String[] args) throws InterruptedException {

GCFinalizeDo.gcFinalizeDo = new GCFinalizeDo();

GCFinalizeDo.gcFinalizeDo = null;

System.gc();

//先睡5S

Thread.sleep(500);

if(gcFinalizeDo != null){

System.out.println(“=自救成功”);

}else{

System.out.println(“=自救失败”);

}

}

}

在这里插入图片描述

整个过程如下

  • 修改静态变量的赋值,让其指向一个对象地址

  • 将静态变量指向的地址为null,此时原先指向的对象就需要发生GC

  • 为了避免该对象发生GC,在finalize方法里面对该对象重新进行引用

  • 最后自救成功

下面进行两次GC,看结果会怎样

public static void main(String[] args) throws InterruptedException {

GCFinalizeDo.gcFinalizeDo = new GCFinalizeDo();

GCFinalizeDo.gcFinalizeDo = null;

System.gc();

//先睡5S

Thread.sleep(500);

if(gcFinalizeDo != null){

System.out.println(“=自救成功”);

}else{

System.out.println(“=自救失败”);

}

//第二次GC测试

GCFinalizeDo.gcFinalizeDo = null;

System.gc();

//先睡5S

Thread.sleep(500);

if(gcFinalizeDo != null){

System.out.println(“=自救成功”);

}else{

System.out.println(“=自救失败”);

}

}

}

在这里插入图片描述

可以看到,尝试自救只输出了一次,这也证明了每个对象的finalize方法仅仅只会执行一次,也就是自救的机会只有一次

对于finalize方法,并不鼓励使用,因为finalize运行代价高昂,而且具有不确定性,无法保证各个对象的调用顺序,如果说再GC后要进行处理而调用这个方法,那还不如使用finally去完成,所以说这个方法真的除了自救之外没啥用途了,而且自救还会发生不确定性

回收方法区

方法区被称为HotSpot虚拟机中的元空间或者永久代(元空间就是元数据的空间,而元数据其实就是类对象)

方法区一般是没有垃圾收集行为的,但还是存在着一些收集器支持对方法区进行回收,这是因为方法区进行垃圾收集的性价比相对于Java堆来说通常也是比较低的,在Java堆的新生代中,对常规应用进行一次垃圾收集通常可以回收70%至99%的内存空间,而方法区由于存放的是元数据和常量,判定条件比较苛刻,所以其区域垃圾收集的回收成果往往会远低于此

方法区的垃圾回收主要关于两部分内容

  • 废弃的常量

  • 不再使用的c类、符号和字段

对于常量来说还比较容易判断,只要判断虚拟机中没有地方引用这个常量即可,但对于类的判断就比较复杂了

对于类的判断,需要判断三个方面

  • 该类所有的实例是不是都已经被回收,也就是Java堆中不存在该类以及任何派生子类的实例

  • 加载该类的类加载器是不是已经被回收

  • 该类的class是不是已经没有地方进行引用,即没有地方通过反射来访问该类

只有满足上面三个条件,Java虚拟机才允许对该无用类进行回收,这里还只是允许而已,还要涉及到垃圾收集器是否支持回收无用类

垃圾收集算法


经过前面的判断,我们已经可以决定出哪些对象可以进行回收了,下面就来看看如何进行垃圾收集

从判断对象消亡的角度出发、垃圾收集算法还可以划分为引用计数式垃圾收集和追踪式垃圾收集,这两类又通常被称为直接垃圾收集和间接垃圾收集,在Java中主要采用追踪式垃圾收集

分代收集理论

分代收集,顾名思义就是按照年龄、年代来进行收集

分代收集又建立在两个分代假说之上

  • 弱分代假说:绝大部分对象都是朝生夕灭的,发生垃圾回收就被回收掉

  • 强分代假说:熬过越多次垃圾收集过程的对象就越难消灭

根据这两个假说,JVM收集器对于Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄用经过的垃圾回收次数来表示)分配到不同的区域之中去存储。

分代分区域的优点就在于,可以将朝生夕灭的对象集中起来,因为这些对象都很难熬过垃圾回收,那么每次对这块区域进行垃圾回收时,只要考虑如何保留少量的存活即可,不需要去标记那些大量将要被回收的对象,这样就能以较低代价回收到大量的空间;相反,如果剩下那些难消灭的对象,那么也可以把这些难以消灭的对象集中起来,只要考虑标记少量被回收的对象,根据年龄来进行这样的区域划分,虚拟机便可以用较低的频率来回收这个区域,同时兼顾乐垃圾收集的时间开销和内存的空间有效利用

因为进行区域划分,让垃圾收集器有了工作范围这一性质,所以根据工作范围,就有了各种的收集器,如下几种

  • 部分收集:PartialGc,目标不是整个Java堆,而是部分

  • MinorGc/YoungGc:新生代收集

  • MajorGc/OldGc:老年代收集,目前只有CMS收集器会单独收集老年代

  • MixedGc:混合收集,目标是整个新生代和部分老年代,目前只有GI收集器支持

  • 整堆收集:FullGc,目标是整个Java堆和方法区

同时,针对不同的区域安排与里面存储对象的存亡特征,需要采用相匹配的垃圾收集算法(如何标记、如何清除,垃圾收集器采用的算法)

  • 标记——复制算法

  • 标记——清除算法

  • 标记——整理算法

Java虚拟机一般将Java堆划分成新生代和老年代两个区域

  • 新生代:对应的就是弱分代,刚来的,朝生夕灭,没熬过垃圾回收,每次垃圾回收都会出现大量的新生代对象死亡

  • 老年代:对应的就是强分代,熬过的垃圾回收多,每次垃圾回收都只有少量的对象死亡

  • 新生代每次存活后的对象都会晋升到老年代中存放

分代收集不仅仅只是划分区域来收集这么简单,因为对象不是孤立的,是存在引用关系的,甚至会出现跨代引用的,比如新生代引用了老年代

举个栗子

比如现在要进行一次仅限于新生代区域内的收集,也就是MinocGC,但新生代中的对象完全有可能会被老年代所引用,那么这里就要再加多一层判断,判断老年代是否引用了新生代,那么此时新生代是没有意识到老年代引用了它,新生代不仅要固定的GC Roots看是否有标记,还有额外去遍历老年代中所有对象从而确保可达性分析结果的正确性,也就是说还要去考虑老年代的情况

举个栗子

在这里插入图片描述

老年代引入了新生代,新生代进行回收时,无法通过老年代最终到达GC Roots,所以也会被回收,所以回收新生代的时候,要遍历老年代,看有没有老年代用到该新生代对象

此时,就需要为分代收集理论添加第三条原则

  • 跨代引用假说:跨代引用相对于同代引用来说仅仅占极少数

这条假说之所以成立,是因为存在互相引用关系的两个对象是应该倾向于同时生存或者同时消亡的,比如一个新生代引用了老年代,老年代会称为GC Roots,那么此时新生代不能被GC清除,那么新生代在熬过了一轮GC之后就会变成老年代,此时就不存在跨代引用了

现在分代理论就变成了三条原则

  • 弱分代:对象都是朝生夕灭的

  • 强分代:熬过越多次的垃圾回收,对象就越难消灭

  • 跨分代引用:跨代引用相对于同代引用仅仅占少数

根据第三条原则,我们就可以不再为了少量的跨代引用去扫描整个老年代了,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(记忆集),这个结构把老年代划分成若干个小块,并且表示出老年代哪一块内存存在跨代引用的问题,那么就不需要对所有的老年代进行遍历了,只需要将包含了跨代引用关系的小块内存里的对象才会被加入到GC Roots进行扫描

这种模式虽然会增加去维护新生代记忆集的开销,但相比于遍历老年代仍然是划算的

下面就学习一下三种清除算法

标记——清除算法

标记清除算法跟其名字一样,分为两步进行

  • 标记:标记出所有需要回收的对象

  • 清除:清除回收所有已经标记过的对象

标记清除算法是基础的算法,后面的算法都是基于标记清除算法来实现的

标记的过程前面已经分析过了,标记算法的缺点在于

  • 执行效率不稳定:假如Java堆中包含大量的对象需要进行回收,这时就要进行大量的标记清除动作,即每个对象都要进行对应的标记和清除,随着对象越来越多,标记和清除的效率就越来越低(这里效率越来越低是指做的清除标记动作越来越多,完成整体的标记和清除效率降低)

  • 内存空间碎片化:对象在被标记清除之后,会产生大量不连续的内存碎片,当内存碎片太多时,后面如果要存放大对象的时候,会导致无法找到足够的空间给大对象进行分配

标记——复制算法

针对标记——清除算法的执行效率不稳定、内存空间碎片化的问题,提出了复制算法(标记——复制算法)

  • 标记:标记出仍然存活的对象

  • 复制:将仍然存活的对象复制到另外一块内存

整体的过程如下

  • 将内存按容量划分为大小相等的两块,一块用于存放对象,另一块空置(这里讨论的是半区标记复制方法)

  • 当存放对象的区域用完了,就会将还存活着的对象转移到空置的区域,并且是通过堆顶的指针去进行连续地去放置(解决了内存空间碎片化问题)

  • 对存放对象的区域进行一次清理(解决了执行效率不稳定问题)

这种算法的优点在于对于大多数对象是可回收的情况,算法需要复制的就是少数的存活对象,而且每次都是针对整个半区进行清理,减少了清理的动作,并且复制转移后,存活对象使用的内存变回连续,避免了碎片化的问题;但缺点也很明显,对于大多数对象是存活的情况,算法需要耗费较大的时间成本去进行复制转移,并且对于程序来说,可以使用的内存只有一半了,GC次数更加频繁了

Java虚拟机一般优先采用标记——复制算法去回收新生代,不过对于新生代来说,大部分的对象都要熬不过第一轮收集(大概98%),也就是说所存活的新生代很少,那么就可以去调整一下内存的分配,不需要1:1的比例去划分新生代的内存空间

先在大概已经懂了标记——复制算法大概是怎么一回事了,下面介绍一下HotSpot虚拟机采用的什么内存分配策略的标记——复制算法

Appel式回收

HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局

Apple式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor空间,当发生垃圾搜集时,会将Eden和其中一块Survivor中仍然存活的对象一次性复制到另外一块Survival空间上,然后直接清理掉Eden和Survivor空间(不包括复制转移的那一块)

HotSpot默认Eden和Survivor的比例是8:1(有两块Survivor,加起来就为1了),即新生代的对象可以使用新生代内存空间的90%,还有10%用于复制转移(默认情况下),普通环境下大概98%的新生代都要被回收,仅有2%存活,所以留下10%已经足够了,但是这只是在一般环境下,并不是针对所有环境,同时也会出现其他情况下,存活的新生代超过了10%,那怎么办呢?

发生上述情况就要依赖于其他内存区域(大多数情况是老年代)进行分配担保了,这是Apple式提供的一个逃生门功能

当Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保机制直接进入老年代

标记——整理算法

对于标记——复制算法,缺点就是需要使用一块固定大小的内存来进行复制转移存活的对象,效率会降低,如果不想浪费50%的内存空间,就需要提供担保机制,所以一般对于老年代来说不会采用标记——复制算法,因为老年代绝大多数都是存活的

针对老年代的存活特征,出现了标记——整理算法

总结

这份面试题几乎包含了他在一年内遇到的所有面试题以及答案,甚至包括面试中的细节对话以及语录,可谓是细节到极致,甚至简历优化和怎么投简历更容易得到面试机会也包括在内!也包括教你怎么去获得一些大厂,比如阿里,腾讯的内推名额!

某位名人说过成功是靠99%的汗水和1%的机遇得到的,而你想获得那1%的机遇你首先就得付出99%的汗水!你只有朝着你的目标一步一步坚持不懈的走下去你才能有机会获得成功!

成功只会留给那些有准备的人!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

对老年代的存活特征,出现了标记——整理算法

总结

这份面试题几乎包含了他在一年内遇到的所有面试题以及答案,甚至包括面试中的细节对话以及语录,可谓是细节到极致,甚至简历优化和怎么投简历更容易得到面试机会也包括在内!也包括教你怎么去获得一些大厂,比如阿里,腾讯的内推名额!

某位名人说过成功是靠99%的汗水和1%的机遇得到的,而你想获得那1%的机遇你首先就得付出99%的汗水!你只有朝着你的目标一步一步坚持不懈的走下去你才能有机会获得成功!

成功只会留给那些有准备的人!

[外链图片转存中…(img-S8Pm33SF-1715151078379)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值