JVM_2_GC相关

程序计数器,虚拟机栈,本地方法栈随线程而生,随线程而灭,他们的回收就不用过多考虑,但是方法区和堆的内存分配和回收都是动态的,GC关注的就是这部分内存.

jvm的基本概念

堆被分成新生代和老年代,新生代被分成一个Eden区和两个Survivor区(From Survivor区和 To Survivor区)

新生代GC(又叫Minor GC):指的是新生代的垃圾收集动作,因为java对象大都具备朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快.

老年代GC(又叫Major GC/Full GC): 指发生在老年代的GC,老年代GC比新生代GC慢很多.

进入老年代的几种情况

大对象直接进入老年代:大对象即需要大量连续内存空间的java对象,最典型的就是那种很长的字符串或数组,大对象堆虚拟机来说是一个坏消息,尤其是那些朝生夕灭的大对象,经常出现大对象容易导致还有不少空间就要出发垃圾收集器去找空间安置他们

*虚拟机提供了一个-XX:PertenureSizeThreshold参数,令大于这个值的对象直接在老年代分配

长期存活的对象直接进入老年代:虚拟机给每个对象都设置了一个年龄计数器,如果对象在Eden区出生并经过第一次Minor GC后仍然存在,并且能被别Survivor区容纳的话,就将被移动到Survivor中(具体怎么移动看下面的复制算法),并且对象年龄加1,当它的年龄增加到一定程度(默认15岁)之后,就将会晋升到老年代中,这个可以通过参数-XX:MaxTenuringThreshold设置.

  • 当然为了能很好的适应不同程序的内存情况,虚拟机不是永远地要求对象的年龄达到-XX:MaxTenuringThreshold参数值才可以晋升到老年代,如果survivor空间中相同年龄所有对象大小的总和大于survivor的一半,那么年龄大于等于该年龄的对象就可以直接进入老年代.

空间分配担保进入老年代:

  • 在进行MinorGC前, 如果老年代最大可用的连续空间大于新生代对象总大小,那么虚拟机就会认为Minor GC是安全的,那么就会进行Minor GC .反之会进行一次Full GC.
  • 如果上面的条件不满足,那么虚拟机就会去查看HandlePromotionFailure的设置值是否是允许冒险,
  • 如果允许冒险,那么虚拟机就会去检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小(平均大小也就是每次`新生代对象要移过来的对象的大小的平均值),
  • 如果大于,虚拟机将尝试进行一次MinorGC,不过这样子是有风险的哈!!!,

  • 如果小于,那么虚拟机将进行一次Full GC.

  • 如果HandlePromotionFailure的设置值为不允许冒险,那么虚拟机将进行一次Full GC.

如何判断对象是否已经死了

在堆里面存放着所有对象的实例,在GC对其回收之前就要确定哪些对象还活着不能回收,哪些对象已经嗝屁了可以回收,有以下几种算法:

引用计数算法

概念:

  • 引用计数算法是指给对象添加一个引用计数器,当有一个地方引用它的时候,计数器+1,当引用失效时,计数器-1,当计数器为0就表示该对象不能被使用,可以被回收.

这样的话会存在一个问题:如果两个对象的某个字段互相引用,例如对象ojbA和对象objB都有字段instance,赋值令objA.instance = objB和objB.instance = objA,那么它应该不能被回收,但是实际上它是被回收了的,那么就说明虚拟机不是通过引用计数算法回收垃圾的.

可达性分析算法

java就是通过这种算法来判定对象是否存活的.

这种算法的基本思想:是通过一系列的称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的时候,则证明对象不可用

如图中的object5,object6,object7,它们虽然互相有关联,但是到GC Roots没有任何引用链,所以它们被判定为可回收的对象.
在这里插入图片描述
可作为GC Roots的对象(全局性引用对象执行上下文对象,比如栈帧中的局部变量表),主要包括下面几种:

  • 虚拟机栈中引用的对象.

  • 方法区中类的静态属性代表的对象,也就是类的静态成员变量代表的对象.

  • 方法区中常量引用的对象,就是final修饰的变量,字符串常量等.

  • 本地方法栈中引用的对象,也就是native方法中的对象.

判断对象死亡的过程

即使在可达性分析算法中不可达的对象,也不是非死不可的,这时候他们暂时会处于缓刑阶段,要真正宣告一个对象的死亡,至少要经历两次标记过程:

  • 如果对象在可达性分析后发现没有和GC Roots相连的引用链,那它将会被第一次标记并且进行一次筛选:筛选的条件是此对象是否有必要执行finalize()方法.
  • 当对象没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过了,那么这个对象将会被当做没有必要执行,直接被虚拟机回收.

  • 如果这个对象被判定为有必要执行finalize()方法,那么这个对象会被放在一个名为F-Queue的队列之中,稍后再对该对象执行finalize()方法,注意,此时虚拟机会创建一个低优先级的Finalizer线程去执行它(也就是去触发这个方法,不会等方法执行结束).

  • 如果对象想要拯救自己,那么对象就要在finalize方法执行结束前将自己与一个仍然存活的对象所在引用链中的任意一个对象相连,比如把自己(this)赋值给某个类变量(也就是static成员变量)或者普通成员变量,那么它在第二次标记时他就会被移出即将回收的集合,如果这个对象在第二次标记后还没有成功拯救自己,那它就只能被回收了.

注意:这种自救方式只有一次,因为finalize()方法最多被系统自动调用一次,也就是说你这次把你自己救活了,下次可达性算法分析后被判定为可回收对象后就会在第一次标记的时候就被回收了.

垃圾收集算法

标记-清除算法(最基础的算法)

  • 该算法分为标记清除两个阶段:
  • 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,标记过程就是上面写的判断对象死亡的过程.
  • 它的主要不足,有两个:
  • 一个效率比较低: 这个标记和清除的效率本来都不高,我也不知道具体是为啥,可能太分散了啊,要两次标记啊之类的吧.

  • 一个空间问题: 标记清除后会产生大量的碎片空间,空间碎片太多可能会导致以后在程序运行过程中需要分配大量对象时,无法找到足够的连续空间而不得不出发一次垃圾收集动作

复制算法(在标记-清除算法基础上解决了效率问题)

  • 它将新生代可用的内存容量划分成大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,它就将存活的对象复制到另外一块上去,然后再把已经使用过的空间一次全部清理掉,同时将存活对象复制到另一块去的时候,只需要移动堆顶指针,按照顺序放就可以了,这样既没有碎片化的空间,操作也很简单,效率就高一些,如图:
    在这里插入图片描述
    理论是那样子,不过虚拟机一般没有五五分,因为新生代的对象基本都是朝生夕死的,所以一般都是将新生代的内存分为一块较大的Eden区和两块较小的Survivor区(From Survivor区和To Survivor区),比例一般是8:1:1
  • 每次只使用Eden区From Survivor区(对象在Eden区产生,当Eden区满了之后再往From Survivor区中放,都满了才进行GC),当回收的时候,将存活的对象都复制到To Survivor区,然后将其他两个区域直接全部清理,回收结束后将现在的From Survivor区To Survivor区的身份互换,即让From Survivor区作为To Survivor区去存放对象.
  • 同时,当Survivor中的区域不够用的时候,就会将对象通过分配担保机制(就相当于贷款时候的担保人)放到老年代中去哈哈哈.

标记整理算法(在复制算法基础上继续优化)
当复制算法在对象存活率比较高的时候就要进行较多的复制操作,效率就会变低,而且,如果不想五五开这样浪费50%的空间的话,就需要额外空间进行担保,所以在老年代不选用复制算法

根据老年代的特点,有人提出一种标记-整理算法,过程和标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,如图:
在这里插入图片描述
分代收集算法(JVM虚拟机采用的GC算法)

  • 就是一般把java堆划分成新生代老年代,新生代一般采用复制算法,因为新生代一般垃圾回收时都有大量对象死去,只有少量存活,而老年代中因为对象存活率高,没有额外空间对他进行担保,所以采用标记-整理算法标记-清理算法来进行回收.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值