JVM:垃圾收集器与内存分配策略(2)

堆和方法区的垃圾回收

  • 由于程序计数器、虚拟机栈、本地方栈为线程私有。方法结束或者线程结束时,内存自然回收。
  • 堆和方法区上的内存分配只有在运行期间才能确定,这部分内存的分配和回收都是动态的。

所以垃圾回收指的是Java堆和方法区上的内存回收,后面用GC表示垃圾回收

 

那何时触发GC???

当程序要创建对象时,内存区不能划分出足够的内存,这时候会触发一次GC

在开发中常遇到是:频繁创建对象,由于内存区一直分配内存,当剩余内存不足时就会触发一次GC。当GC后仍然不足以分配出去就会抛出OOM异常

 

触发GC时,如何判断对象是否存活(可回收)?

先介绍判断对象存活的几种方式

  • 引用计数器法(未使用)

**原理:**每当有一个地方引用它时,计数器加1,引用消失则计数器减1,当计数器为0时,对象即可回收

**缺点:**两个对象互相引用时导致计数器不为0,无法回收

  • 可达性分析算法(当前使用)

当一个对象到GC Root没有任何引用,则该对象可回收。

        GC Root                 GC Root
           |                       |
        object1                 object1
        /     \                 /     
    object2  object4        object2   object4
    /           |           /            |
object3      object5     object3      object5

如图:从左到右,object4到GC Root的引用链断开,object4、object5会被判定为可回收。

可以视为GC Root的有4种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象

  2. 方法区中类静态属性引用的对象(静态变量)

  3. 方法区中常量引用的对象(常量)

  4. 本地方法栈中JNI(Native方法)引用的对象

 

引用概念的扩展

当对象可回收(不可达),而内存空间足够时,希望能保存这部分对象,于是出现强引用、软引用、弱引用、虚引用4种引用的概念,使可回收对象可以继续保存一段时间。

  • 强引用:通过new关键字创建的对象为强引用对象,不会被垃圾收集器回收

  • 软引用:发生内存溢出之前回收该类对象。多用于内存敏感的环境。软引用可以加快JVM垃圾回收的速度,防止内存溢出。

  • 弱引用:垃圾收集器工作时,只要发现有弱引用的对象就会回收

  • 虚引用:用于监测对象的是否已经回收。使用场景:NIO操作时使用直接内存->通过一个储存在堆中的DirectByteBuffer对象作为这块内存的引用进行操作->通过虚引用可以知道对象被回收时机->于是回收直接内存

 

既然对象已经可以回收了,我们来看下它是如何回收的

垃圾回收算法

先来看下每个算法的执行过程,白色为未分配的内存、灰色为已使用的内存、深灰色为已标记(可回收)内存

使用已使用已标记

 

标记-清除算法 (Mark-Sweep)

标记-清除算法是最基本的算法

 
回收前:通过可达性分析标记内存块

 
回收后:清除了带标记的内存块

 
缺点:

  1. 标记和清除过程效率不高
  2. 由上图可看到:因为回收后出现了大量内存碎片,导致无法分配大块连续的内存(比如:数组),导致触发下一次GC

 

复制算法(Copying)

为了弥补标记-清除算法的两个缺点,出现了复制算法,复制算法不在需要关注内存碎片且提高了回收效率

 
回收前:每次使用容量的一半

 
回收后:先复制存活的到另一块内存,然后清理一整块4x3的内存

 
优点:

  1. 非常适合用在对象存活率低的场景(新生代多的场景)–原理接下来介绍
  2. 效率高(只需要移动堆顶指针,按顺序分配即可

缺点:

  1. 内存缩小为原来的一半

 

标记-整理算法(Mark-Compact)

相对于复制算法,标记-整理算法适用于对象存活率高的情况,它的关键在于整理

 
回收前:将存活对象移向一端,清理以外的内存

 
回收后:存活的对象聚集在一端

 
优点:

  1. 适合用在对象存活率高的场景(老年代多的场景)

 

垃圾收集算法的演变和用途

 

         ->复制算法  
标记-清除            -> 分代收集算法
         ->标记-整理 

标记-清除算法是最基本的算法,而分代收集算法是结合复制算法和标记-整理算法的优点产生

复制算法的优化

有研究表明在 新生代 对象中,只有不到10%对象存活,于是将复制算法中 50% : 50%的内存划分方式变为 90% :10%。故内存空间分为:Survivor区 和 Eden区

 
内存区 = Survivor + Survivor + Eden

 
每次使用 Survivor + Eden (占 90%的内存),内存回收后 旧Survivor区 的存活对象复制到新 Survivor 区,即使用另一块 Survivor 和 同一块Eden。

 
如果某些条件下,存活的对象大于10%,Survivor区放不下这时需要将这些对象放入老年代(分代算法),这就是常说的担保分配(至于担保逻辑自行了解)

 

分代收集算法

分代算法是将堆内存中 新生代和老年代,根据复制算法和标记-整理算法的优势,回收不同的对象

 

总结

Created with Raphaël 2.2.0 当剩余内存不足以分配给对象,触发一次GC 可达性分析,知道可回收的对象 1.如果将要发生内存溢出,回收软引用对象 2.回收所有弱引用和虚引用对象 通过垃圾回收算法处理 完成垃圾回收

 

附录

本地变量表:也称 局部变量表 是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。

新生代: 刚创建出来的对象

老年代: 新生代的基础上,经历过多次GC还没有被回收的对象

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值