您的位置:首页 > 编程语言 > Java开发

Java虚拟机-垃圾收集器和内存分配机制

2015-03-10 11:38 218 查看
在了解垃圾收集器之前,我们需要知道什么样的对象才会被GC收集

1.判断对象已死

堆内存中存放着Java世界中几乎所有的对象实例,垃圾收集器对堆内存回收时,第一件事就是要确定那些对象还存活/死去。

判断方法:

(1)引用计数法

即给每个对象添加一个引用计数器,每当有地方引用它,则计数器+1,引用失效时计数器-1,为0时就不可能再被引用。这么看引用计数法实现简单,判定效率也高,但是一些主流的Java虚拟机里并没有使用它,因为它很难解决对象之间的循环引用(例:ObjA.objb=objb,ObjB.objA=objA);

(2)可达性分析算法

主要思想:提供一个类似于GC Root这样的起始节点,从这个节点开始往下搜索,搜索走过的路径代表一个引用链,当一个对象到Root节点没有任何一条引用链时,则证明此对象不可用。如下图所示



在java中一般可以作为GC Root的对象有以下几种(可以思考他们的生命周期来选择):

1.虚拟机栈中引用的对象

2.方法区中类静态属性引用的对象

3.方法区中常量引用的对象

4.本地方法栈中Native方法引用的对象

Java中就是采用可达性分析法来对对象进行GC的

(3)引用的类别

部分开发人员也许对引用的分类不大清楚,因为不会去关心Java的内存回收机制,为了更好的分析,我们需要分析一下GC日志:

下面介绍一下引用的分类:

1.强引用(Strong Reference):只要强引用存在,GC就不会回收被引用的对象,例:Object obj = new Object();

2.软引用(Soft Reference):在系统发生内存溢出前,会把软引用对象列入回收范围之内进行2次回收

强引用,导致内存溢出,可以看到最后一次进行fullGC时,由于obj是强引用无法进行垃圾回收,已经无法释放需要给arr分配的内存空间了



软引用,跟上图的强引用对比发现,最后一次GC时将软引用的对象obj从GC中回收,所以未发生内存溢出



3. 弱引用(Weak Reference)是用来描述非必须的对象的,强度比软引用更弱一些,被引用的对象只能生存到下一次垃圾回收之前,当GC时,无论当前内存是否充足,都会回收掉弱引用引用的对象,如下,weak引用的对象就已经被回收了。



4.虚引用,一个对象存在虚引用,完全不会影响到它的生存时间,也无法通过虚引用来获得一个对象实例。

(4)生存与死亡

在可达性分析算法中不可达的对象也并非非死不可,这时候他们处于缓刑阶段,要真正回收,至少需要两次标记的过程。对象进行可达性分析后没有发现与GC Root相连,那么他会被进行第一标记并且进行一次筛选,筛选是否有必要执行finalize。当对象的finalize方法被执行过或者未覆盖finalize方法时,这种情况的对象都被视为没有必要执行。

如果一个对象被判定为有必要执行finalize方法,则会被加入一个F-Queue中,并在稍后由虚拟机自动创建一个低优先级的Finalizer线程去执行它。这里要注意的是虚拟机只会去出发这个方法,但不一定会等待它执行结束(防止存在死循环等情况)。如果一个对象在finalize方法中也没有与GC Root关联上,那它就会被GC了。

(5)方法区的垃圾回收

永久带的垃圾回收主要是两种:常量和无用的类;无用的常量指的是例如:常量池中存在一个“string”常量,但是没有任何String 对象去引用它。

无用的类判别条件:

该类所有的实例都已被回收,即堆中不存在任何该类的实例。

加载该类的ClassLoader已经被回收。

该类的Class对象没有在任何地方被引用。

符合以上标准的虚拟机可以对这个类进行回收,是否对类进行回收,HotSpot提供了-Xnoclassgc参数进行控制(命令行java -version可以看到当前虚拟机的信息)

2. 垃圾回收算法

(1)标记-清除

标记-清除是最基本的垃圾回收算法(后面的算法都是基于此算法改进的),如同他的算法名称一样,可以分为标记和清除两个步骤

首先,标记所有要被回收的对象

其次,标记完成后对所有标记的对象进行回收

主要问题:

效率问题,标记和清除两个阶段的效率都不高

空间问题,标记-清除后会存在大量不连续的内存碎片,空间碎片会导致如果需要分配一个较大的内存空间时,无法找到连续的内存空间。



(2)复制算法

为了解决效率问题,这种复制算法应运而生,它的主要思想是把一个内存分为两块,每次只使用其中一块,当这一块的内存用完了酒吧还存活的对象复制到另外一块内存上,然后再把已使用的那块内存完全清空掉。这样使得每次只对半块内存进行回收,同时也只用移动堆顶指针,而不用关心内存碎片了,实现简单,运行效率高。但是每次只能使用内存的一般,代价太高。

所以这种算法一般被用来回收新生代的内存,据研究新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的空间来划分内存,而是将新生代内存划分为一块较大的Eden控件和两块Survivor控件。当回收时,将Eden和Survior中还存活的对象复制到另外一块Survior中,并清理Eden和刚才使用过Survivor。HotSpot虚拟机默认Eden和Survivor的内存比例是8:1。每次新生代中可用内存空间为新生代内存容量的90%。但是不可能保证每次都只有不到10%的内存存活,当Survivor不够时,需要依赖其他的内存(老年代)进行分配担保,存入老年代中。



(3)标记-整理算法

复制整理算法如果用在存活率较高的内存中就会因为要进行过多的复制操作,导致效率降低。由于老年代中大部分对象都是存活的,所以不能使用此种算法进行老年代的垃圾回收。根据老年代的特点,标记-整理算法应运而生,标记-整理算法首先也是要进行标记操作,但是标记完后不直接对对象进行清理,而是将所有存活的对象都向一端移动,然后直接清理掉所有边界以外的内存(从而保证了内存空间的连续性)



(4)分代收集算法

当前商用虚拟机都采取分代收集算法,这种算法的思想就是把内存划分为几块永久带,老年代,新生代对不同的区域采用不同的方法,譬如:新生代采用复制算法,老年代采用标记-清除/标记-整理算法。

3.HotSpot的算法实现

HotSpot是常用的商用虚拟机之一,我们来了解一下HotSpot虚拟机的相关工作

(1)枚举根节点

我们知道Java使用的时可达性分析算法来查看一个对象是否存在一个与GC Root的链,但是可以想象一个方法区都可能有数百M的大小,如果要逐个去查看对象的引用会消耗很多时间。

可达性分析对时间的敏感还体现在GC的停顿上,为了保持一致性,在GC时必须停止Java执行线程的工作(STW,STOP THE WORLD),目前主流的JVM都采用准确式GC(即虚拟机可以准确的知道内存中某个位置的数据具体是什么类型的),所以当系统停顿下来后,并不需要一个不漏的检查完所有的所有对象是否被引用。HotSpot中使用一组OopMap的数据结构来实现,在类加载完成的时候,HotSpot会把对象上什么偏移量上的什么类型计算出来,在JIT编译的时候也会在特定的位置记录下栈和寄存器是哪些位置引用的。

(2)安全点

安全点指的是程序不是在任何位置都可以停下来进行GC,只能在特定的位置停下来GC(OopMap只在特定位置记录了引用信息)。安全点既不能太少,以至于让gc等待时间太长,也不能太过于平凡增加运行时的负荷。GC是主要使用主要式线程中断,主要思想是不直接操作线程,设置一个标志,让线程自己主动去轮询这个标识,发现中断标识时挂起自己。

4.内存分配与回收策略

(1)对象优先在Eden中分配

大多数情况下,对象在Eden区中分配,如果没有足够连续的空间时,会发起一次Monitor GC(新生代GC,Full GC-老年代和持久区GC),如果还是没有足够的内存,将使用分配担保机制直接进入老年代。

(2)大对象直接进入老年代

所谓的大对象是指需要连续内存空间较大的对象,最典型的就是大数组和大集合,在写程序时应尽量避免朝生夕灭的大对象,大对象会导致内存中还存在不少空间就得触发GC来安置他们。我们可以使用:-XX:PretenureSizeThreshold参数来指定一个大小,当对象大于此参数设定时,将直接在老年代中分配,这样的做法是防止在Eden区和两个Survivor中发生大量的内存复制。

(3)长期存活对象直接进入老年代

虚拟机采用分代收集的算法,那么虚拟机就的区分哪些对象应该放在新生代,那些对象应该放到老年代中,为了做到这点,虚拟机给每个对象定了一个Age,如果他能熬过一次Monitor GC并且能被Survivor容纳,则它的年龄加1,当它的年龄达到一定程序(默认参数为15)则会被放入到老年代(年龄的阀值:-XX:MaxTenuringThrehold设置)

(4)动态年龄判断

并不是所有年龄达到-XX:MaxTenuringThrehold设置的对象才被放入老年代,当Survivor中所有相同年龄所有对象大小为当前Survivor的大小的一半,所有大于或等于该年龄的对象直接进入老年区。

(5)空间分配担保

在发生Monitor GC之前虚拟机会检查老年代最大可用内存是否大于新生代所有内存总控件,如果大于则代表Monitor GC是安全的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: