您的位置:首页 > 其它

JVM-垃圾收集机制与内存分配策略

2016-10-21 17:50 323 查看
JVM-垃圾收集机制与内存分配策略
 

JAVA与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

 

1、垃圾收集(Garbage Collection,GC)需要考虑的问题

A:哪些内存需要回收?

B:什么时候回收?

C:如何回收?

 

2、哪些内存需要回收

JVM中JAVA栈,PC寄存器,本地方法栈随线程而生,随线程而灭。一般会考虑垃圾收集的区域是堆和方法区。

 

(1)回收堆区

A:引用计数算法

算法思路:给每个对象一个引用计数器,当对象被引用的时候的计数器的值加一,当引用失效的时候计数器得值减一。所以在任何时候只要计数器的值为零就代表该对象是没有被引用的,以至于可以说是能被回收。

算法的利弊:使用该算法实现简单,判定效率也很高,但是很难解决对象之间的相互循环引用问题。因为循环引用的话计数器的值永远不可能为零,也就是不可能被正常回收。

B:根搜索算法

算法思路:通过一系列的名为”GC Roots”的对象为起点,通过这些节点开始向下搜索,搜索走过的路径成为引用链。当一个对象到GC Roots没有任何引用链时,则证明此对象是不可用的,以至于可以说是能被回收。

在JAVA中,可作为GC Roots对象包括下面几种:

a:JAVA栈中的对象

b:方法区中类的静态属性应用的对象

c:方法区中常量引用的对象

d:本地方法栈中Native方法的引用的对象


 

C:生存还是死亡

在根搜索算法中的不可达的对象,也并非是”非死不可”的,这时候他们暂时处于”缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。

第一次标记:根搜索后发现没有与GC Roots相连接的引用链,进行第一次标记并筛选。筛选的条件是对象是否有必要执行finalize()方法。当对象没有覆盖或该方法已经被虚拟机调用过,虚拟机将这两种情况视为”没有必要执行”。如果是没有必要执行的就直接被处死了。

第二次标记:对于对象被判定为有必要执行finalize()方法,那么这个对象就会被放置在一个名为F-Queue的队列中,并在稍后由一条由虚拟机自动建立的,低优先级的Finalizer线程去执行。Finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue进行第二次小规模的标记,如果对象要在finalize()方法中成功拯救自己———只要重新与引用链上任何一个对象建立关联即可。那在第二次标记时将被移出”即将移出”集合。如果对象这次机会没有抓住就只能等死了。

 

2、回收方法区

方法区可以实现垃圾收集,但是方法区收集的”性价比”一般比较低。主要回收两部分的内容:废弃常量和无用的类。

A:废弃常量的回收与堆区对象的回收类似。

B:无用的类的判定

a:该类所有的实例都已经被回收

b:加载该类的ClassLoader已经被回收

c:该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法

 

3、什么时候回收

那么对于 Minor GC
的触发条件: 大多数情况下,直接在Eden
区中进行分配。如果Eden区域没有足够的空间,那么就会发起一次Minor GC;对于Full
GC(Major GC)的触发条件:也是如果老年代没有足够空间的话,那么就会进行一次Full GC。

注:上面所说的只是一般情况下, 实际上,需要考虑一个空间分配担保的问题 :

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于则进行Minor GC,如果小于则看HandlePromotionFailure设置是否允许担保失败(不允许则直接Full
GC)。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试Minor GC(如果尝试失败也会触发Full GC),如果小于则进行Full
GC。

但是,具体到什么时刻执行,这个是由系统来进行决定,是无法预测的。

 

4、如何回收

A:标记—清除算法

算法思路:分为标记和清除两个阶段。首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。

缺点:一是效率问题,因为标记和清除过程的效率不高。二是空间问题,标记清除后会产生大量的不连续的内存碎片,这样会使程序在以后的运行过程中需要分配比较大的对象的时候无法找到足够的连续内存空间而不得不提前触发另一次垃圾收集动作。

回收前如下图所示:

 


回收后如下图所示:

 


 

B:复制算法

算法思路:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存空间用完后,将所有存活的对象复制另一块上面,并清除此块中的所有已使用过的对象。使用于堆中的新生代

优缺点:实现简单,运行高效,对标记清除算法的缺点有很好补充。不足的是将内存缩小为原来的一半。

回收前如图所示:

 


回收后如图所示:

 


C:标记—整理算法

算法思路:对于存活对象较多的情况下使用复制算法效率将会变低。该算法与标记—清除算法类似,只不过该算法标记过可回收对象后,将所有存活对象都向一端移动,然后直接清除掉端边界以外的内存。

回收前如图所示:

 


回收后如图所示:

 


 

D:分代收集算法

算法思想:根据对象存活周期的不同将内存划分为几块,一般分为新生代和老年代,这样就可以根据各个年代的特点选择合适的收集算法。新生代每次垃圾回收都有很多对象死去,少量存活,选用复制算法。老年代中对象存活率高,没有额外空间对它进行分配担保,就必须使用标记—清除或标记—整理算法。

分代结构如下图所示:

 


执行过程:首先会在Eden空间进行垃圾回收,然后将回收后还存活的对象(利用复制算法)复制到From Survivor空间。如果From Survivor空间也被使用完了就触发一次垃圾回收,将From
Survivor空间里存活的对象复制到To Survivor空间中。最后To Survivor空间也满了就再触发一次垃圾回收,将To
Survivor空间存活的对象复制到转到老年代。老年代对象的存活率比较高,要是满了就不能像新生代那样向后传递存活的对象,此时就需要利用(利用标记—整理算法)算法对其进行垃圾回收。

 

5、Finalize()方法

他是垃圾回收器回收一个对象的时候第一个要调用的方法。不过由于Java的垃圾回收机制能自动为我们做这些事情,所以我们在一般情况下是不需要自己来手工释放的。

finalize的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存.所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作。

Finalize()在什么时候被调用?

A:所有对象被Garbage Collection时自动调用,比如运行System.gc()的时候.

B:程序退出时为每个对象调用一次finalize方法。

C:显式的调用finalize方法

除此以外,正常情况下,当某个对象被系统收集为无用信息的时候,finalize()将被自动调用。但是jvm不保证finalize()一定被调用,也就是说,finalize()的调用是不确定的,这也就是为什么sun不提倡使用finalize()的原因。

 

6、引用的类型

A 强引用:指程序代码中普遍存在的这类引用,如强引用任在就不会被回收。

B 软引用:指还有用但非必需的对象,在系统将要发生内存溢出异常之前,将会把这类对象列进回收范围之内进行第二次回收,如果回收后还没有足够的内存才会抛出内存溢出异常。提供SoftReference类实现软引用。

C 弱引用:指非必需的对象,强度比软引用弱,只能生存到下一次垃圾回收之前。提供WeakReference类实现弱引用。

D虚引用:最弱的引用关系,不会对生存时间造成影响,存在的以为目的是当这个对象被回收时受到一个系统通知。提供PhantomReference类实现虚引用。

 

7、内存分配策略

A:通常情况下,对象在eden中分配。当eden无法分配时,触发一次Minor
GC。

B:配置了PretenureSizeThreshold的情况下,对象大于设置值将直接在老年代分配。 

C:在eden经过GC后存活,并且survivor能容纳的对象,将移动到survivor空间内,如果对象在survivor中继续熬过若干次回收(默认为15次)将会被移动到老年代中。回收次数由MaxTenuringThreshold设置。 

D:如果在survivor空间中相同年龄所有对象大小的累计值大于survivor空间的一半,大于或等于个年龄的对象就可以直接进入老年代,无需达到MaxTenuringThreshold中要求的年龄。 

E:在Minor GC触发时,会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间,如果大于,改为直接进行一次Full GC,如果小于则查看HandlePromotionFailure设置看看是否允许担保失败,如果允许,那仍然进行Minor
GC,如果不允许,则也要改为进行一次Full GC。 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息