JVM GC

JVM GC

虽然内存分配和GC都已经“自动化”了,但内存回收和GC在很多时候都是影响系统性能、并发能力的主要因素之一,并且当我们需要排查各种内存溢出和内存泄露时,你就需要深入底层去了解JVM GC的原理了,以实施对它的监控和调节。本次主要讲解JVM GC的相关内容。

理解JVM GC的方式只需要知道三个问题就行了:

  • 那些内存需要回收?
  • 怎么回收?
  • 什么时候回收?

1 哪些内存需要回收?

先回忆一下Java内存区域的划分,共有:虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、程序计数器(Program Counter Register)、Java堆(Heap)、方法区(Method Area)。

我们知道虚拟机栈、本地方法栈、程序计数器①所占用的内存基本上在类结构确定下来的时候就明确了,②且都是线程私有的,生命周期与线程相同。所以它们的内存分配和内存回收都是确定的,不需要我们过多的去考虑。

而Java堆、方法区则不一样,他们通常只有在程序处于运行期间才能知道到底要创建哪些对象,所以对他们的内存分配和内存回收都是动态的,所以GC主要关注的就是这两个区域。

2 如何回收?

2.1 “判决”

“判决”的目的自然是:确定哪些对象“活着”、哪些对象“死了”。

过去会采用引用计数算法(给对象添加一个引用计数器,每被引用一次就+1;引用失效则-1。当引用为0时,说明对象“死了”,则GC进行回收),但它却解决不了对象间相互循环引用的问题,而容易导致内存泄露。所以很少使用了。

现在主流的算法是——可达性分析算法,思想非常简单:①我们可以将对象看做“节点”,将对象间的引用看做“路径”,那么我们便可以依次画出连通图(图论);②紧接着只要我们找到了一系列必然存活着的“节点”作为根节点(GC Root),再向下进行搜索,就能够将可达和不可达的节点区分出来;这样就解决了对象间相互循环引用的问题,由此我们就确定出来了哪些对象“活着”,哪些对象“死了”(并不一定会马上被回收掉,进行”缓刑“)。

那么哪些对象可作为GC Root呢?

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

2.2 ”缓刑“

在可达性分析算法中被确定不可达的对象并不会马上就被回收掉,而是先执行“缓刑”,即它们还需经过至少两次标记才会被GC回收掉。

大致流程:确定没有与GC Root相连的引用链时,此对象会执行第一次标记;筛选此对象是否需要执行finalize()方法;如果是,则进入F-Quene队列,等待执行finalize()方法;否则,执行第二次标记,被回收。执行finalize()方法时,如果此对象重新与引用链上的对象建立起关联,则将其移除“即将回收”集合,存活;否则回收。

**注:**如果对象已执行过一次finalize()方法,并成功“逃脱”回收,那么第二次再执行“缓刑”的时候,则不会再执行finalize()方法了。即在这个过程中每个对象只有一次执行finalize()方法的机会。

附上流程图,以方便理解:
在这里插入图片描述

2.3 常见垃圾收集算法

说道算法了,可能好些同学又有些害怕了,其实完全没有必要,因为垃圾收集算法的思想都比较简单,很容易理解!(如果你学过操作系统这门课的话,这对你来说一定是小菜一碟)

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

顾名思义,分为两个阶段:标记、清除。先标记出来需要回收的内存对象,再进行统一回收。

在这里插入图片描述

(用excel画图,颜色选了好长一阵…)

作为最基础的收集算法,自然是跑不了有很明显的缺点了:

  • 效率问题:标记和清除两个过程效率都不高。
  • 空间问题:清除之后,会产大量不连续的碎片,下次要是来个大一点的对象不方便分配内存空间。
2.3.2 复制算法(Copying)

复制算法是为了解决标记-清除算法中的效率、空间问题出现了。它将内存空间分为大小相等的A、B两块,每次只使用其中的一块,当A块内存用完后,就将还存活的对象复制到B块,然后把A块已使用的内存空间一次清理掉。

在这里插入图片描述

这样便完美的解决了标记-清除算法中的效率和空间问题。但最大的缺陷是,内存空间缩小成了以前的一半大小…

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

针对复制收集算法中存活率较高,复制次数增多导致效率降低的问题,提出了标记-整理算法。

与标记-清除算法类似,同样先标记出可回收的对象,但后续是将所有存活的对象向一端移动,然后清理掉端边界以外的内存空间。

在这里插入图片描述

2.3.4 分代收集算法(Generational Collection)

当前商业虚拟机的垃圾收集都采用“分代收集“算法,主要是根据对象存活的周期,将Java堆划分为新生代与老年代,再以它们各自的特点采用最适用的算法。算法同上,并没有任何实质改变。

生存周期特征采用算法
新生代对象存活率低复制算法
老年代对象存活率高标记-清理算法、标记-整理算法

其中对新生代的复制算法有所优化,不再按照1:1的比例来划分内存空间,而是采用Eden(伊甸)区和两个Survivor区,内存空间大小比例为8:1:1的方式来划分。如下图:

在这里插入图片描述
①内存分配时,每次使用Eden和其中一块Survivor区;

②当回收时,将Eden和Survivor中还存活的对象,复制到另一块Survivor区中去。当另一块Survivor区域空间不足时,将会分配到老年代中去;

③清理掉Eden和刚才使用过的Survivor区。

3 什么时候回收?

把这个问题放在最后的原因是,弄清楚如何回收是前提。

GC分为Minor GC与Full GC,只要清楚了它们的触发条件,也就清楚了什么时候进行GC了!

  • Minor GC:
    • 当Eden区中没有足够空间时。
  • Full GC:Full GC一般都会有一次Minor GC
    • 调用System.gc时,不一定执行;
    • 老年代空间不足;
    • 方法区空间不足;
    • 通过Minor GC后进入老年代的平均大小 > 老年代的可用内存。
    • 由Eden区、From Space区向To Space区复制时,对象大小 > To Space可用内存,则把该对象转存到老年代,且老年代的可用内存 < 该对象大小。

(以上内容均整理自《深入理解Java虚拟机:JVM高级特性与最佳实践 第二版》)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值