JVM——垃圾回收机制与内存分配模型

JVM——垃圾回收机制与内存分配模型

  什么内存区域的对象需要使用垃圾回收机制回收?

前言:

  线程私有的内存区域的生命周期随线程而生,随线程而灭。分配与回收具有确定性,当方法或线程结束时就会被回收,因此,JVM的垃圾回收器及内存分配模型都是针对线程共享的内存区域(Java堆+方法区)

  回收对象前,如何判断对象已死?

一、对象是否已死?

  垃圾回收器在对堆进行垃圾回收前,要先判断这些对象哪些还活着,哪些已死?
  1、引用计数法:给每个对象创建时附加一个引用计数器并初始化为0,被引用+1,引用失效-1,在任意时刻,若计数器值为0,说明对象已死。
   实现简单,判定效率高,但无法解决循环引用(只有a引用b,同时b引用a,这两个计数器都不为0,但JVM把两个当做垃圾回收了)
  2、可达性分析算法(JVM就使用该算法):通过一系列成为“GC Roots”的对象作为起始点,从这些结点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连时(从GC Roots到该对象不可达),证明此对象是不可用的(是不可用,并没有直接判断死)。

  可以做GC Roots的有哪些对象?

   能做GC Roots的对象:

   虚拟机栈(栈帧中的局部变量表)中引用的对象
   方法区中类静态属性引用的对象
   方法区中常量引用的对象
   本地方法栈中JNI(本地方法)引用的对象

  引用的具体分类(JDK1.2后)?

   引用可分为强引用、软引用、弱引用、虚引用四种,强度依次降低。
    ①强引用:代码中直接明示(Object obj = new Object()),只要强引用还在,垃圾回收器永远不会回收此对象(即便内存不够)
    ②软引用:有用但不必须的对象,若内存不够时,垃圾回收器对软引用对象进行回收,如果内存足够,则会保留软引用对象
    ③弱引用:非必须的对象,垃圾回收机制开启,无论内存够不够用,都会将弱引用对象进行回收
    ④虚引用:一个对象的虚引用完全不会对该对象的生存周期有影响。

  上面的可达性算法宣布对象不可用,并没有宣布对象死亡,对象到底是否死亡?

   即使是不可达的对象,也并没有"非死不可",要宣告一个对象真正的死亡,需要经历两次标记过程:第一次标记就是可达性算法,如果不可达,标记一次并判断该对象是否有必要执行finalize()方法,若当前对象没有覆写finalize()方法(继承Object默认的)或finalize()方法已被JVM调用过,该对象被二次标记,则该对象被判定真正的死了。
   如果覆写了finalize()方法且没被JVM调用过,该对象被放在F-Queue队列中,由JVM自动创建一个Finalizer线程执行,若对象覆写的finalize()方法中重新与任意一个GC Roots建立引用链,则该对象死里逃生,但若在覆写的方法中没有建立引用链,则依旧被判死亡。
   任何一个对象的finalize()都只会被系统自动调用一次(最多死里逃生一次)

  方法区上的内容如何被回收?

   方法区主要回收两个部分:废弃常量与无用类。
   废弃常量:没有任何引用指向该常量
   无用类:满足3个条件
    ①该类所有实例已被回收
    ②该类的类加载器ClassLoader被回收
    ③该类的Class对象没被引用,无法通过反射访问该类的内容
   JVM只是有可能会对该类进行回收,要求太严格,几乎不会被回收(方法区叫做"永久代"),JVM会不定期的对方法区进行回收来防止永久代溢出。

  Java堆分为新生代与老年代,分别使用什么垃圾回收算法及其原理?

二、垃圾回收算法(GC)

  1、垃圾算法的基础:标记-清除算法
   算法分为标记,清除两个阶段,首先标记出所有需要回收的对象(宣告对象彻底死亡的那两次标记),在标记完成后统一回收所有被标记的对象。
   缺点:
    ①效率问题:标记和清除两个阶段的效率都不高
    ②会产生大量的不连续碎片,若程序运行中需要分配较大对象时,无法找到足够大的连续空间。
  2、新生代垃圾回收算法(Minor GC):复制算法
   新生代对象具有“朝生夕灭”的特点(新生代对象存活率比较低)

  复制算法具体如何实现?

   将新生代内存分为一块Eden区和两块大小相等的Survivor区,Eden区与两块Survivor区(From区和To区)的比例为8:1:1,每次使用Eden区和其中一块Survivor(From),当需要回收时,将Eden区和Survivor中还存活的对象一次性复制到另一块Survivor空间(To)上,最后清理掉Eden区和刚才用过的那块Survivor区(From)。下次再回收,又使用Eden去和其中一块Survivor(To),重复上面步骤即可。

  新生代为何如此设计?

   新生代内存的利用率为90%(Eden+1/2*Survivor),剩下的10%用来存放回收后存活的对象。由于新生代朝生夕灭的特性,一个对象创建后不久就死了,还存活的对象不多(这才敢把Survivor区设置的那么小)。

  新生代对象怎么进入老年代?

   ①经过反复复制,有的对象可能在Survivor的两块区间中复制来复制去,如此交换15次,该对象就不太符合新生代的特性了(还没死),就将该对象存入老年代。
   ②由于复制算法需要将Eden区+1/2Survivor区中还存活的对象放到1/2Survivor区中,如果这1/2*Survivor区放不下还存活的对象,那么这些对象直接通过分配担保机制进入老年代。

  复制算法的优点有哪些?

   实现简单,运行高效,无空间碎片问题
  3、老年代垃圾回收算法(Major GC/Full GC):标记-整理算法
   老年代的对象存活率都比较高,因此不能使用复制算法(大量对象复制操作)。

  标记-整理算法的流程是什么?

   标记阶段与标记-清除算法是相同的(判断对象已死的那两次标记)
   整理阶段:把存活的对象向一端移动,而后一次性将存活对象边界以外的内存全部清理掉。

  什么是分代算法?

  4、分代收集算法-存活对象的划分
   一般将Java堆分为新生代和老年代。
   新生代(Minor GC)中,每次垃圾回收都会有大量的对象死去,只有少量存活(朝生夕灭),采用复制算法。
   老年代(Full GC/Major GC)中,对象存活率高,没有额外空间对它进行分配担保,采用标记-整理算法。

三、垃圾收集器

  垃圾收集器的分类?<没有最优的收集器,只有最适合的收集器>

   新生代GC收集器:Serial,ParNew,Parallel Scavenge
   老年代GC收集器:Serial Old,Parallel Old,CMS
   全区域GC收集器:G1

  前言:并行,并发,吞吐量到底什么

   并行:多条垃圾收集线程并行工作,用户线程仍处于等待状态
   并发:用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序继续执行,而垃圾收集程序在另一个CPU上。
   吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))

  不同的垃圾收集器具体是怎样的?

  1、Serial收集器:串行GC,单线程收集器,在Serial进行垃圾收集时,必须暂停其他所有工作线程,直到Serial收集结束(Stop The World)
   应用于:JVM运行在Client模式下默认的新生代收集器
   优点:简单而高效,对于单核CPU,没有线程交互开销,可获得最高的单线程收集效率
  2、ParNew收集器:Serial收集器的多线程版本
   应用于:JVM运行在Server模式下默认的新生代收集器
   优点:随着CPU数量的增加,提高GC对系统资源的利用率
  3、Parallel Scavenge收集器:吞吐量优先收集器,具有高吞吐量。
   应用于:高吞吐量场景
   优点:高吞吐量,GC自适应调节策略
  4、Serial Old收集器:Serial收集器的老年代版本,单线程收集器,使用标记-整理算法
   应用于:
    Client模式:Serial Old主要用于Client模式下的虚拟机
    Server模式:CMS收集器的备用
  5、Parallel Old收集器:Parallel Scavenge的老年代版本
  6、CMS收集器:Concurrent Mark Sweep,以获取最短系统停顿时间为目标的垃圾收集器。使用标记-清除算法。
   流程分为4个步骤:
    ①初始标记:需要Stop the World标记一下GC Roots能够直接关联到的对象,速度很快。
    ②并发标记:耗时较长,并发
    ③重新标记:需要Stop the World,速度较快
    ④并发清除:耗时较长,并发
   整个过程中耗时最长的并发标记和并发清除过程收集器线程和用户线程是并发执行的,所以总体上CMS收集器与用户线程是并发执行的。

在这里插入图片描述

   应用于:B/S系统(浏览器-服务器)服务端(重视响应速度)
   优缺点:并发收集,低停顿,但对CPU资源敏感,产生大量不连续空间碎片(标记-清除算法导致)
  7、G1收集器:全区域垃圾回收器,同样追求低停顿。

  GC日志的查看?

  如何查看GC日志即各参数的意义
  初始设置参数:
   -XX:+PrintGCDetails:打印详细GC日志
   -XX:+UseSerialGC:使用Serial回收器
   -Xms:初始堆大小
   -Xmx:最大堆大小
   -Xmn:新生代大小
   -XX:SurvivorRatio:新生代中Eden区域与一块Survivor区域容量比值,默认为8(注意:是Eden区与1块Survivor的比值,但新生代一共有2块Survivor,要算Survivor总大小,需要再将该Survivor*2
  GC日志:
   出现Full GC:说明这次GC发生了Stop-The-World(在日志中不是区分新/老生代的,但其他地方都指老年代)
   方括号内:GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存总容量)
   方括号外:GC前Java堆已使用容量->GC后Java堆已使用容量

四、内存分配模型

  JVM到底是如何给一个对象分配内存的?

   1、对象优先在Eden区分配:若Eden区没有足够空间进行对象分配时,JVM会发生Minor GC
   2、大对象直接进入老年代:需要大量连续空间的对象,何以为大,通过-XX:PretenureSizeThreshold = 字节大小设置,凡是比该字节大的就叫做大对象。
   3、长期存活的对象将进入老年代:JVM为每个对象定义了一个对象年龄计数器(Age)。如果对象在Eden区出生并经历一次Minor GC后仍然存活并且能被Survivor空间容纳的话,把此对象年龄+1.何为长期存活,通过 -XX:MaxTenuringThreshold(设置晋升老年代年龄阈值):若年龄大于阈值,则晋升老年代(默认为15)
   4、动态对象年龄判定:Survivor空间中相同年龄(age)的所有对象大小的总和>=1/2*Survivor,则年龄>=age的对象直接进入老年代,无须等到MaxTenuringThreshold的阈值
   5、空间分配担保:Eden快满时需要进行Minor GC,JVM会检查老年代最大可用的连续空间是否大于新生代所有对象空间。
    若大于,此次Minor GC是安全的(有老年代给它担保,大不了就把所有对象都通过分配担保机制进入老年代)
    ①若小于,JVM检查HandlePromotionFailure是否允许担保失败,若HandlePromotionFailure==true,则继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。大于,则尝试Minor GC
    ②若HandlePromotionFailure==false或①中尝试Minor GC失败,则直接改为Full GC,先使用Full GC清理老年代,再进行Minor GC。
    大部分情况下都是可以担保成功的,避免频繁执行Full GC
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值