详细的讲一遍Java GC

本文从Java的垃圾定义、如何回收垃圾、垃圾回收策略等方面详细讲一遍Java的垃圾回收机制

什么是垃圾回收

垃圾回收(Garbage Collection,GC),就是释放垃圾占用的存储空间,对内存中(主要是堆)已经死亡或长时间未被使用的对象进行清除和回收,防止内存泄漏。

内存模型

先看下jdk 1.6、1.7、1.8的内存模型

jdk1.6

jdk1.7

jdk1.8

可以看到,jdk从1.6到1.8的演进中,将常量池放到了堆里面,将永久代移出了JVM内存,放到了机器内存中(元空间-metaspace)。初衷在于将不可回收或很难回收的放到了堆外内存(永久代的回收有非常严格的要求),可控的收拢在堆内。

如何定义垃圾

GC前,我们得直到哪些内存是可以回收的。这里就涉及到垃圾回收的相关算法:引用计数算法和可达性分析算法

引用计数法

引用计数法是在对象中分配一块空间来记录该对象被引用的次数。如果该对象被其他对象引用一次则+1,如果删除对该对象的引用,则引用次数-1。当该对象的引用次数为0时,那么该对象就可被回收。

String str = new String("west");
str = null;

创建一个字符串,则字符串"west"有1个引用,即 str;将 str 置为 null,则 "west" 的引用次数为0。在引用计数算法中,"west"所占用的内存即可被回收了(写代码中,不用的对象显示置为null即是这个道理)。

从上面也可以看到,引用计数法是将垃圾判断逻辑放到了应用的运行当中,而不是在垃圾回收时,停止整个应用的运行(stop the world),直到对堆中的垃圾回收处理完毕。但引用计数法被淘汰也有它的弊端,当我们定义2个对象(计数+1),且这两个对象相互引用(计数+1),再置空2个对象的声明引用(计数-1)时,实际这2个对象已经不可能在被访问了,但因为计数器不为0(1+1-1=1),GC回收器永远无法回收他们。

可达性分析算法

可达性分析算法是通过 GC ROOT 作为起点,从这些节点向下搜索的路径,当一个对象在这个路径上都没有被任何引用时,则证明该对象是未被使用(不可达)可回收的。

可以看到,可达性算法解决了引用计数算法里的“循环依赖”问题,只要对象和GC ROOT没有关联(直接或间接),则可被回收。

上文说的不用的对象显示置为 null 即没有被引用,即可被及时清理。

什么是GC ROOT

看上面内存模型图,GC ROOT 的对象包含:虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中的Native方法引用的对象。

如何回收垃圾

在确定了哪些是垃圾可以回收后,GC要做的就是如何回收这些垃圾。不同的虚拟机有不同的方式来实现GC,常见的有标记清除算法、标记整理算法、复制算法、分代收集算法,下面我们详细介绍下各个算法的实现原理。

标记清除算法

上图可以很明显的看出来,标记清除算法分为2步:标记出要回收的区域,然后清除这些空间内的垃圾。被清理出来的区域就变为未使用的内存区域。

不足:标记清除会产生大量的内存碎片,可用的内存空间会被切割成多块,而创建对象做内存分配时,需要的是连续的内存空间。如上图所示,如果现在需要一块4M的内存区域,其实是无法开辟出来的,但实际我们可用的内存空间总量是大于4M的。也就是我虽然有足够的内存空间,实际却不可用。

标记整理算法

如上图,标记整理算法将所有存活的对象向一端移动,再清理掉边界外的内存区域。这样避免了内存碎片问题,未使用的内存空间都是持续的空间。

不足:标记整理算法对内存做了频繁的变动,需要整理所有存活对象的引用地址并指向新的内存地址,性能和效率差很多。

复制算法

如上图,复制算法是将内存区域分为两块同等大小的区域,每次只使用其中一块。当这一块内存用完了,将存活着的对象迁移到另一块未使用的区域,再将原有的区域的内存空间全部清理掉。复制算法解决了内存碎片问题,逻辑简单,性能高效。

不足:总共14M的内存空间,实际可用的只有7M

分代收集算法

分代收集算法针对对象存活周期的不同,将堆内存分为新生代和老年代,并针对这两块区域的特点采用最合适的收集算法。可以认为分代收集算法是上述几种算法的组合。

在新生代中,每次GC时都有绝大部分对象死亡,只有极少量存活,则可以使用复制算法,只需要小量的复制存活对象成本就可完成垃圾收集。老年代中,因为存活的对象较多较大,没有多余的空间来做复制,一般使用标记整理算法来回收。

先看下分代收集的内存模型:

新生代

所有new出来的对象都会放在 Eden 区,当 Eden 区空间不足时,JVM发起 Minor GC,Eden 区中未被回收的存活对象移入 S0,如 S0 空间不足时,则再次 Minor GC,将存活的对象移入 S1,将S0的对象清除(复制算法),并记录移入对象被GC的次数。下一次 Minor GC 时,S0 和 S1的职责互换,以此反复。直到存活对象被GC的次数超过15时,才会被移入老年代。

可以认为,S0,S1就是老年代的一个缓存,且在缓存中的对象有极大可能在被GC15次之前被回收。而两个 Survivor区(S0,S1),又可以保证总有一个区域是空闲的,而另一个是没有碎片可使用的。据说不多分几个 Survivor,是经过权衡后最佳的选择。过多可能导致单个 Survivor 区域过小,容易被塞满。

老年代

老年代占据了堆内存中大部分的空间,只有在Major GC的时候才会进行清理,但每次GC时会"stop-the-world",即其他线程都会被暂停。

需要注意,大对象需要连续的大量内存空间,因此被创建出来时就会直接进到老年代。避免在新生代中做大量的内存复制操作。

常用垃圾收集器

一张图总结:Serial、Parallel Old、CMS、G1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值