垃圾回收机制和内存的动态分配

部分文字为本人总结,如有正确,纯属巧合

哪些内存需要回收?
什么时候回收?
如何回收?

哪些内存需要回收

用“二次标记”来确定对象是否真正可回收。即二次标记你可以回收了,就等着回收线程来收了自己就行了

第一次:利用“可达性分析算法”标记未被引用的对象,判断该对象是否调用过finalize方法,如果没有,则将对象放置到F-Queue队列中。
第二次:Finalizer线程调用队列中对象的finalize()方法并移除队列,再次利用“可达性分析算法”判断是否被引用。如果被引用,则暂时逃脱被标记;如果还未被引用,则等着被回收

注意:
1、第一次标记时,如果finalize已经调用过,说明是逃脱第二次标记的对象,该对象也等着被回收
2、finalize:虚拟机会触发该方法,但不会承诺等他运行结束,这样做的原因是,如果对象再finalize中执行缓慢或者死循环,则导致F-Queue中的对象不能得到及时的回收处理。

可达性分析算法:将一系列成为“GC Root”的对象作为起点向下搜索,对于搜索路径所覆盖到的对象即为被引用。即GC Root不可达则表示未被引用。

哪些对象可作为GC Root

【虚拟机栈中】引用的对象(栈帧中的本地变量表),是否可以理解为局部变量引用的对象?
【方法区中】类静态属性引用的对象
【方法区中】常量引用的对象
【本地方法栈中】JNI(一般说的Native方法)引用的对象

有些引用类型会特殊处理GC

特殊在哪儿:当内存空间充足时,你就活着。当内存空间垃圾回收后还是空间紧张,则可以抛弃。常用在缓存功能!!!
强引用(strong reference) 只要有引用存在就不会被回收
软引用(soft reference)GC一次后,发现没有可以内存空间,然后继续二次GC,释放软引用对象空间。可见GC并不是一锤子买卖!
弱引用(weak reference)GC后,无论当前内存是否可用,都回收
虚引用(phantom refrence)这个怪到无法理解??

方法区(1.8以前)

方法区中的垃圾回收的“性价比”一般比较低,在堆中,尤其是新生代,常规应用进行一次垃圾回收一般回收70%-95%的空间,而永久代的效率要远低于此。这可能也是1.8版本中替换为“元空间”的原因吧
回收两部分内容:废弃的常量和无用的类
废弃的常量:常量不被任何地方引用

收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

无用的类,类需要同时满足下面3个条件才能算是“无用的类”:

无类的实例:该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
无类的加载器:加载该类的ClassLoader已经被回收。
无类的Class对象:该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
无类的anything:……

如何回收

垃圾回收器常采用的垃圾回收算法(GC):
1、标记-清除算法

采用“二次标记法”,一次标记所有无用对象,直接回收其占用内存空间。
缺点(缺点1值得商榷):
1、效率低,标记和清除的效率都不高;
2、空间碎片,清除后产生不连续的内存空间,如果有较大对象产生,则不得不提前触发另一次GC
用途:分代回收中,新生代中的Eden区采用该算法,因为该区域的对象存活时间很短,不易产生碎片,且比复制算法节省空间

2、复制算法

将可用内存划分大小相等的两份,每次只使用其中一份。当其中一份用完了,触发GC操作,对该区域实行“标记-清除算法(清除发生在copy之后,此时只标记)”,然后将剩余的对象一次copy到另一份中
优点:效率高,避免空间随便,使用连续空间
缺点:可用内存缩小为原来一半

3、标记整理算法

大体上同“标记-清除算法”,只不过是清理后,将可用对象向一端移动,保证空间的连续性。
优点:保证空间连续性
缺点:效率低

4、分代垃圾回收
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

一个Eden区,两个Survivor区,一个年老区(Tenured)。前两个叫做新生代。
前提:
1、两个Survivor区中始终有一个是空闲的

详细说明:
Eden区随着对象的不断创建,总有满的时候,当其满时,触发GC操作(复制算法:Eden和Survivor会一起标记),此时将Eden和Survivor中还存活的对象一次性的copy到另外一块Survivor中,最后清理掉Eden和刚才用过的Survivor。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是说会有10%的内存会被“浪费”掉。当回收剩余大于10%时,就会将需要转移的对象全部移到老年代(Tenured),在转移之前,会先判断晋升的对象所需空间是否超出老年区剩余空间,如果超出,则Full GC,否则直接晋升。元数据区或者永久代只有在fullgc下才会回收
注意:之前我还以为是先从Eden到Survivor,再从Survivor到Tenured,其实不然,而是将Eden和Survivor会一起标记,一起copy
说明:对象优先在Eden分配,大对象直接进入老年代(可配置多大),长期存活的对象进入老年代(在Eden和survivor之间来回复制的次数,默认15,可配置),动态年龄判断(survivor中相同年龄占据该区域一半内存时,大于等于该年龄的晋升到老年代),当每次晋升的大小大于当前老年代的剩余空间时,将Full GC

什么时候回收?


由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。

Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。


Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。


参考:《深入理解Java虚拟机》

你可能感兴趣的:(虚拟机)