深入理解GC垃圾回收机制
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解GC垃圾回收机制相关的知识,希望对你有一定的参考价值。
参考技术A 在我们程序运行中会不断创建新的对象,这些对象会存储在内存中,如果没有一套机制来回收这些内存,那么被占用的内存会越来越多,可用内存会越来越少,直至内存被消耗完。于是就有了一套垃圾回收机制来做这件维持系统平衡的任务。1.确保被引用对象的内存不被错误的回收
2.回收不再被引用的对象的内存空间
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时, 计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:引用计数收集器可以很快地执行,交织在程序的运行之中。
缺点:很难处理循环引用,比如上图中相互引用的两个对象,计数器不为0,则无法释放,但是这样的对象存在是没有意义的,空占内存了。
引用计数法处理不了的相互引用的问题,那么就有了可达性分析来解决了这个问题。
从GC Roots作为起点,向下搜索它们引用的对象,可以生成一棵引用树,树的节点视为可达对象,反之最终不能与GC Roots有引用关系的视为不可达,不可达对象即为垃圾回收对象。
我自己的理解是,皇室家族每过一段时间,会进行皇室成员排查,从皇室第一代开始往下找血缘关系的后代,如果你跟第一代皇室没有关系,那么你就会被剔除皇室家族。
1.虚拟机栈中引用的对象(正在运行的方法使用到的变量、参数等)
2.方法区中类静态属性引用的对象(static关键字声明的字段)
3.方法区中常量引用的对象,(也就是final关键字声明的字段)
4.本地方法栈中引用的对象(native方法)
1.显示地赋予某个对象为null,切断可达性
在main方法中创建objectA、objectB两个局部变量,而且相互引用。相互引用直接调System.gc()是回收不了的。而将两者都置为null,切断相互引用,切断了可达性,与GCRoots无引用,那么这两个对象就会被回收调。
2.将对象的引用指向另一个对象
这里将one的引用也指向了two引用指向的对象,那么one原本指向的对象就失去了GCRoots引用,这里就判断该对象可被回收。
3.局部对象的使用
当方法执行完,局部变量object对象会被判定为可回收对象。
4.只有软、弱、虚引用与之关联
new出来的对象被强引用了,就需要去掉强引用,改为弱引用。被弱引用之后,需要置空来干掉强引用,达到随时可回收的效果。
只被软引用的对象在内存不足的情况,可能会被GC回收掉。
只被弱引用持有的对象,随时都可能被GC回收,该对象就为可回收对象。
是不是被判定为了可回收对象,就一定会被回收了呢。其实Ojbect类中还有一个finalize方法。这个方法是对象在被GC回收之前会被触发的方法。
该方法翻译过来就是:当垃圾回收确定不再有对该对象的引用时,由垃圾回收器在对象上调用。子类重写finalize方法以处置系统资源或执行其他清除。说人话就是对象死前会给你一个回光返照,让你清醒一下,想干什么就干什么,甚至可以把自己救活。我们可以通过重写finalize方法,来让对象复活一下。
示例:
执行的结果:
这里我们重写FinalizeGC类的finalize方法, 使用FinalizeGC.instance = this语句,让对象又有了引用,不再被判定为可回收对象,这里就活了。然后再置空再回收一下,这个对象就死了,没有再被救活了。所以finalize方法只能被执行一次,没有再次被救活的机会。
在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。
元空间有注意有两个参数:
MetaspaceSize :初始化元空间大小,控制发生GC阈值
MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存
为什么移除永久代?
移除永久代原因:为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。
有了元空间就不再会出现永久代OOM问题了!
1.Generational Collection(分代收集)算法
分代收集算法是GC垃圾回收算法的总纲领。现在主流的Java虚拟机的垃圾收集器都采用分代收集算法。Java 堆区基于分代的概念,分为新生代(Young Generation)和老年代(Tenured Generation),其中新生代再细分为Eden空间、From Survivor空间和To Survivor空间。 (Survivor:幸存者)
分代收集算法会结合不同的收集算法来处理不同的空间,因此在学习分代收集算法之前我们首先要了解Java堆区的空间划分。Java堆区的空间划分在Java虚拟机中,各种对象的生命周期会有着较大的差别。因此,应该对不同生命周期的对象采取不同的收集策略,根据生命周期长短将它们分别放到不同的区域,并在不同的区域采用不同的收集算法,这就是分代的概念。
当执行一次GC Collection时,Eden空间的存活对象会被复制到To Survivor空间,并且之前经过一次GC Collection并在From Survivor空间存活的仍年轻的对象也会复制到To Survivor空间。
对象进入到From和To区之后,对象的GC分代年龄ege的属性,每经过GC回收存活下来,ege就会+1,当ege达到15了,对象就会晋级到老年代。
2.Mark-Sweep(标记-清除)算法
标记清除:标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。标记-清除算法主要是运用在Eden区,该区对象很容易被回收掉,回收率很高。
3.Copying(复制)算法
复制算法的使用在From区和To区,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
缺点:可使用内存缩减为一半大小。
那么复制算法使可使用内存大小会减半,设计上是怎么解决这个问题的呢。就是给From和To区划分尽可能小的区域。经过大数据统计之后,对象在第一次使用过后,绝大多数都会被回收,所以能进入第一次复制算法的对象只占10%。那么设计上,Eden、From、To区的比例是8:1:1,绝大多数对象会分配到Eden区,这样就解决了复制算法缩减可用内存带来的问题。
4.Mark-Compact (标记—整理)算法
在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记—清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记—整理算法,与标记—清除算法不同的是,在标记可回收的对象后将所有存活的对象压缩到内存的一端,使它们紧凑地排列在一起,然后对边界以外的内存进行回收,回收后,已用和未用的内存都各自一边。
垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现:
Serial 收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,
优点是简单高效;
Serial Old 收集器 (标记-整理算法): 老年代单线程收集器,Serial 收集器
的老年代版本;
ParNew 收集器 (复制算法): 新生代收并行集器,实际上是 Serial 收集器
的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现;
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行
收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿
的特点,追求最短 GC 回收停顿时间。
深入理解java虚拟机 - 垃圾回收机制(GC)
垃圾回收机制(GC)是java常重要特性之一。它让开发者无需关注内存的创建和释放,而是通过GC自动回收垃圾(无用对象)。
哪些内存需要回收
java堆和方法区是垃圾回收的主要内存区域,程序计数器、虚拟机栈、本地方法栈这几个内存区域是现成私有的,线程结束时内存自然也就回收了。
如何判断对象可回收?
在java堆里存放着几乎所有的对象实例,垃圾收集器在进行垃圾回收之前第一件事情就是判断哪些对象还"活着",哪些对象已"死去"(不会再被使用的对象);
引数记数法
引数记数法就是给对象添加一个引数计数器,每当有其他地方引用对象时,计数器就加1;当引用失效时,计数器就减1,如果计数器任何时刻都为0,那么对象不可能在被使用,垃圾收集器就可以进行回收了。引数记数法简单高效,微软公司的COM技术,python语言,游戏脚本语言Squirrel都是使用了引数记数法进行内存管理。但是主流java虚拟机没有使用引述记数法进行内存管理,原因主要是他很难解决对象之间互相引用的的问题。
可达性分析算法
java虚拟机是通过可达性分析算法来判定对象是否存活的。可达性分析算法的主要思想是通过一系列的称为"GC Roots"的对象作为起始点从这些起点开始向下搜索,搜索走过的路径称为引用链,当一个对象到"GC Roots"没有任何引用链相连时("GC Roots"到此对象不可达),则证明此对象是不可用,可回收的。
在java语言中,可作为"GC Roots"的对象包括以下几种
- 虚拟机栈()中引用的对象
- 方法区中类静态变量引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
垃圾收集算法
简单介绍下几种垃圾收集算法
标记-清除算法
最简单的垃圾收集算法是“标记-清除”算法,算法分为“标记”和“清除”两个阶段:首先要标记出需要回收的对象,在完成标记后统一回收所有被标记的对象。标记过程就是判断哪些对象需要回收。
标记-清除算法有两个不足之处:
一个是效率问题,标记-清除的效率都不高;
另一个空间问题,标记-清除之后会产生大量不连续的内存碎片,内存空间碎片太多可能会导致java虚拟机在分配较大对象时,无法找到足够的连续内存而触发一次垃圾收集动作。
复制算法
复制算法是为了解决标记-清除算法的效率问题而产生的。它是如何解决效率问题的呢?
复制算法将内存分为两块大小相等的两块,每次只使用其中一块。当这一块的内存用完了,将存活的对象复制到另一块上,然后将使用过的一块内存全部清理掉。这样每次都是对整个半区进行垃圾回收,内存分配时就不需要担心内存碎片的问题了。复制算法实现简单,运行高效。代价就是内存缩小为原来的一半。
复制算法主要用来回收新生代,IBM的专家研究过,新生代的中的对象98%都是“朝生夕死”的,并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次只使用Eden和其中一块Survivor空间。当进行垃圾回收时将Eden和Survivor中还存活的对象复制到另一块Survivor空间上,最后清理掉Eden和使用过的Survivor空间。Eden空间和两块Survivor空间之间的比例时8:1:1,新生代可用空间为90%(80% + 10%),只浪费了10%内存空间。当存活对象多于10%时,Survivor空间不够用,这些对象将通过分配担保机制直接进入老年代。
标记整理算法
老年代是通过标记-整理算法来进行垃圾回收的。标记过程与标记-清除算法一样,清除过程不是直接清理可回收对象,而是让所有的对象都向一端移动,然后直接清理掉边界以外的内存。
分代收集算法
现代商业虚拟机的垃圾收集都采用“分代收集”算法,分代收集算法根据对象存活周期的不同将java堆内存分为新生代和老年代。新生代每次垃圾收集时大量对象死去,只有少量对象存活,那就使用复制算法。老年代中的对象存活率高,没有额外内存空间进行担保,那就使用标记清除算法或者标记整理算法。
垃圾收集器
未完。。。
参考资料
《深入理解java虚拟机》
以上是关于深入理解GC垃圾回收机制的主要内容,如果未能解决你的问题,请参考以下文章