深入理解JVM——垃圾回收机制

如果要了解java的垃圾回收,我们首先要知道三个方面

  1. 何为垃圾–可达性算法
  2. 怎么回收–回收算法
  3. 用什么回收–垃圾回收器

何为垃圾

首先介绍一下几个概念

1.引用计数算法

像Python语言、游戏脚本、微软COM技术,都用引用计数算法来进行内存管理。原理就是给每一个对象增加一个引用计数器,如果引用了就+1,如果引用失效就-1,引用为0的就是垃圾。

比如Object o = new Object();

那么Object()对象就会+1,如果o=null;那么Object()对象就会-1。
当然在o被赋值为null之前,可能有a=o赋值,那么Object()对象的计数器也会+1。如果没有其他对象的引用指向这个对象,如a=null,计数器为0,那么这个对象就是垃圾。

很多java开发者认为早起的java就是用的引用计数器,实际上java并未用过引用计数器来管理内存,因为他存在一个很明显的问题。

public class A {
    private Object obj=null;

    public static void main(String[] args) {
        A objA=new A();//引用计数器objA+1 结果为1
        A objB=new A();//引用计数器objB+1 结果为1
        objA.obj=objB;//引用计数器objB+1 结果为2
        objB.obj=objA;//引用计数器objA+1 结果为2
        objA=null;//引用计数器objA-1 结果为1
        objB=null;//引用计数器objB-1 结果为1
    }
}

在objA、objB都在堆空间中开辟了一个内存空间,因为存在一个相互引用,objA等objB被回收后引用计数器就为0,而objB也要等objA被回收后引用计数器才能为0,因此循环引用导致堆空间的对象都无法被视为垃圾而回收。

实际上java使用可达性算法来实现内存管理。

2.可达性算法

该算法又叫根搜索算法,基本思路就是通过一个叫GC Roots的对象作为起始点,从这个对象往下搜索,走过的路径称为引用链,当一个对象没有与任何引用链相连时就视为对象不可用。
而GC Roots对象包括下面四个

  1. 虚拟机栈中的引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即一般的Native方法)引用的对象

可达性算法图解

2.1 关于引用

如果对GC Roots还是不太理解的话,我们可以举例看一下,
Object obj=new Object();
关于这行代码,new Object()会在堆空间中开辟 一块内存存放对象实例,而对象的引用obj 就会放在虚拟机栈的该栈帧的局部变量表中。那么,这个引用的对象就是GC Roots节点。如果这行代码是全局属性,前面加了一个static或者final修饰,那么该引用的对象就是方法区中类静态属性引用的对象或方法区中常量引用的对象。引用类型的数据中存放着该对象的内存地址,如果没有任何引用指向堆中的一个对象时,该对象包括他互相引用的对象都会视为不可以对象,最终将会被垃圾回收期回收,同时也说明了可达性算法完全解决了引用计数算法存在循环引用而造成的问题。

jdk1.2之后java将引用分为强引用、软引用、弱引用、虚引用。

  1. 强引用: Object obj=new Object();这就是一个强引用,只要引用还存在,垃圾回收机制就不会被回收。
  2. 软引用:一些可用的但是非必须的对象可用软引用定义,SoftReference类可实现软引用,在系统内存溢出之前,先清理不可达对象和软引用对象,如果内存还不够再报异常。
  3. 弱引用:WeakReference类可实现软引用,该对象只能存活在下一次垃圾回收之前。
  4. 虚引用:PhantomReference类可实现虚引用,为对象设置虚引用的目的完全是为了对象被回收之前得到一个系统通知。

怎么回收

首先了解三个回收算法

1.标记清理法

通过可达性算法标记出所有需要回收的对象,然后在下次垃圾回收机制触发后回收掉。
标记清理法图解
该算法存在两个很典型的问题

  1. 效率问题:标记和清理过程的效率都不高
  2. 空间问题:每次被回收后,就会存在大量的内存碎片,如图所在,可用内存都是一个个碎片,如果下次要分配一个连续的大对象,比如说较大的数组,就无法找到连续的内存空间。

2.标记整理法

和标记清理法一样,但是后续并不是对可回收内存直接清理,而是把所有存活对象都向一端移动,最后把另一端全部清理掉,这样就解决了标记清理法的内存碎片问题。标记整理法图解

3.复制算法

复制算法的思路是将内存划分为两块,用其中一块来存放对象实例,垃圾回收触发后,把存活的对象都放在另一块内存,然后把剩下的一块内存空间清理掉。这样就完全不用考虑内存碎片问题,只需要一定堆顶指针,按顺序分配即可。该算法很明显的问题就是内存缩小为原来的一半。
复制算法图解

4.分代回收算法

结合以前的不足,java最终采用分代回收算法来管理内存,所谓分代回收,并不是一种新的算法,而是利用之前的三种算法对不同区域进行回收。

java将堆内存分为新生代老年代。新生代用于存放绝大多数新对象实例,也就是大部分new 出来8:1:1的比例分为Eden、survivor1、survivor2。新创建的对象都会首先放在Eden中,第一次垃圾回收触发的时候,通过复制算法先将Eden中存活的对象放在survivor1中,然后清空Eden中的对象。下次垃圾回收机制触发后,通过复制算法将Eden和survivor1中存活的对象放在survivor2中,然后清空Eden和survivor1,依次循环,survivor1和survivor2每次垃圾回收之后都会有其中一块为空,以备下次垃圾回收时存放存活的对象。

因为往往很多对象存活的时间都很短,Eden里大部分对象都会被清理,所以只分配了10%的空闲空间以备存放存活对象,这样不仅避免了Eden存放新对象的内存碎片(标记清理法的不足),也没有将内存缩小很多(复制算法的不足),如果对象经过多次垃圾回收依旧活,到达一定次数就会通过复制算法存放在老年代。

即便Eden里大部分对象都会被清理,也很有可能出现垃圾回收时survivor里装不下存活的对象,确保万无一失,装不下的对象也会通过复制算法放在老年代。

同时由此可知新生代都是通过复制算法管理内存。老年代的内存远比新生代的内存大,根据各公司的垃圾回收器不同,算法也不同,但是都通过标记整理法、标记清理法的其中一种算法管理内存。

  • 超过一定大小的对象直接分配给老年代,
  • 在新生代垃圾回收后survivor内存已经装不下存活的对象,装不下的也会分配给老年代
  • 在新生代进行垃圾回收后一直存活的对象,达到一定年龄(默认15岁)会分配给老年代

怎么回收

java虚拟机对垃圾回收的实现并没有任何规定,所有个厂家进行垃圾回收的算法也各部相同,但是大致思路还是一样的。目前为止并没有一套适用于任何场景的垃圾回收器,同时也没有绝对高效的垃圾回收器,都是通过多种垃圾回收器结合起来使用。

1.Serial收集器

Serial收集器是历史最悠久的收集器,曾经在jdk1.3.1之前是虚拟机新生代垃圾回收的唯一选择。这个收集器是一个单线程收集器,所谓单线程,并不仅仅是单CPU完成垃圾收集工作,让人无法容忍的是在垃圾回收的时候它会停止所有的线程,直到收集结束,就好像妈妈给孩子们打扫房间的时候,所有孩子停止手上的事。

Serial收集器图解
在用户不可见的情况下停止所有线程进行垃圾回收,这让人难以接受,就像每次使用电脑1小时都要卡5分钟。但是目前为止,也没有办法完全消除这个问题,只是尽量缩短了因为垃圾回收而停顿的时间。

2.ParNew收集器

这是一个多线程收集器,收集算法和分配策略和Serial收集器完全一样,依旧是要停止所有的线程进行垃圾回收,parNew收集器公用了Serial收集器的大部分代码,唯一的区别就是用多线程进行垃圾回收。
parNew收集器图解
由于多线程频繁切换的开销,单核中的多线程效率远远低于单线程,所以parNew收集器只有在多线程中才能体现出效果,甚至可以说2个CPU的环境下,parNew收集器的效率也不能保证100%大于Serial收集器。

3.Parallel Scavenge收集器

Parallel Scavenge收集器和parNew收集器一样都是新生代收集器,也是并行多线程采用复制算法,那它有什么特点呢?

原来垃圾回收的时候尽量缩短用户的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,

吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收)

比如用户代码运行时间99分钟,然后回收1分钟垃圾,那么吞吐量就是1%,Parallel Scavenge收集器默认提供部分参数来控制吞吐量,程序员也能修改参数来维护,自适应调节策略是Parallel Scavenge收集器最显著的特点。

可能有人会想把新生代调小一点效率会不会更高?答案是不会。虽然新生代变小了,每次垃圾回收的停顿时间会很短,但是回收的频率会变高,那么吞吐量也会降下来。就好比你学校的垃圾桶调小了一样,倒垃圾的次数会变多。

4.CMS收集器

cms收集器以最短回收时间为目的的收集器,非常适用于B/S的服务器上,因为这些服务器尤其重视用户响应时间。CMS采用标记清理算法,适用于老年代,整个过程分为四个部分。

  1. 初步标记:标记GC Roots直接关联的对象,速度很快。
  2. 并发标记:通过直接子节点往下寻找节点,和用户线程一起工作
  3. 重新标记:重新标记并发标记过程中产生的新垃圾,停顿时间比初步标记长一点,但是远比并发标记短。
  4. 并发清除:清除垃圾,和用户线程一起工作
    CMS收集器图解
    CMS是多线程收集器,自然对CPU特别敏感,在并发阶段因为占用线程,可能会导致应用程序变慢。4个CPU以上,并发阶段可能只会占用25%,但是低于4个,占用比例可能会更大,导致用户线程执行速度降到50%。

5.G1回收器

G1回收器采用标记整理法,避免了CMS垃圾回收出现的内存碎片问题。能够非常精确的控制停顿,指定在M毫秒内垃圾回收只占用N毫秒。G1回收器将java堆(包括新生代、老年代)划分为多个大小固定的独立区域。根据每块垃圾堆积程度加入一个队列,优先收集垃圾最多的片段。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值