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

Java的GC与内存分配策略

2018-07-03 16:37 162 查看
资料整理来源以及参考:

深入JAVA虚拟机

https://www.zhihu.com/question/21539353 (关于Java为啥关于引用计数以及可达性问题,查看gityuan的回答)

https://www.cubrid.org/blog/understanding-java-garbage-collection (这篇讲的也不错)

Java的GC机制主要针对于 堆以及方法区 而言,对于程序计数器,虚拟机栈,本地方法栈三个区域是随着线程而生,随线程而灭的,栈中的栈帧随着方法的进入和退出有条不紊的执行出栈和入栈的操作,每个栈帧分配的内存在编译期就是可知的。

可达性分析算法

Java中通过可达性算法来管理对象的引用,算法的基本思路是通过一系列的"GC Roots"的对象作为起始点,从节点向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则说明对象不可用。

可以作为GC Roots的对象包括:

虚拟机栈中引用的对象。

方法区中静态属性引用的对象。

方法区中常量引用的对象。

本地方法栈中JNI引用的对象。

图示如下:



上图可以看出,对象实例3和对象实例5没有在GC Roots的路径下,所以会标记为不可达的。

然而,一个对象是否真正的死亡,至少需要两次的标记过程:如果对象再进行可达性分析时候没有对应引用链关联到,则被标记一次,然后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()或者finalize()已经被虚拟机调用过,则没必要执行。如果这个对象被判定有必要执行finalize()方法,则会放置在一个队列中,稍后GC会对这个队列进行第二次的小规模标记,如果还是没有对应引用,则该对象会被回收。

回收方法区

回收方法区主要回收两部分内容:废弃常量和无用的类。

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

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:

该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。

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

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

垃圾收集算法

垃圾收集算法主要有三种:
标记-清除算法(mark-sweep)
复制算法(copying)
标记-整理算法(mark-compact)


标记-清除算法(mark-sweep)

该算法分两阶段进行,一是标记,二是清除。首先标记出需要回收的对象,在标记完成后统一回收所有被标记的对象。



图片来源

使用该算法有两点不足:

效率问题:标记和清除两个过程的效率都不高。

空间问题:标记清除之后产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

复制算法的出现是为了解决上述的效率问题,他将内存按容量划分为大小相等的两块,每次使用其中的一块。一块内存如果用完了,将这块内存还存活的对象复制到第二块内存当中,然后把已使用过的内存空间一次清理掉。



优点:每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点:算法的代价是将内存缩小为了原来的一半。

现在JVM都采用该算法进行新生代内存回收,主流的是并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Suvivor空间,每次使用Eden和其中一块Suvivor。当回收时,将Suvivor和Eden中还存活的对象一次性复制到另一块Suvivor空间中,最后清理用过的Suvivor和Eden空间。HotSpot虚拟机默认Suvivor和Eden的比例为1:8,也就是每次新生代中可用的内存空间为整个新生代容量的90%,(80Eden+10Suvivor),只有10%内存会被浪费掉。如果Suvivor空间不够用了,需要依赖其它内存(老年代)来进行分配党报

如果另一块Suvivor空间没有足够的空间去存放上一次新生代收集下来的存活对象,则这些对象将直接通过分配担保机制进入老年代。

标记-整理算法

复制算法在存活的对象多的情况下就要进行较多的复制操作,效率将会降低。因此提出了标记-整理算法,该算法适用于老年的内存的回收。过程跟标记-清除一样,但是后续步骤不是直接对可回收对象进行清理,而是把所有存活的对象向一端移动,然后清理掉边界外的内存。



图片来源

分代收集

分代收集算法根据对象的存活周期将内存划分为几块。一般是分为老年代以及新生代。

新生代(Young generation): 绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可到达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为”minor GC“。

老年代(Old generation): 对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中消失的过程,我们称之为”major GC“(或者”full GC“)。



上图中的持久代( permanent generation )就是方法区(method area)。他用来保存类常量以及字符串常量。因此,这个区域不是用来永久的存储那些从老年代存活下来的对象。这个区域也可能发生GC。并且发生在这个区域上的GC事件也会被算为major GC。

收集的过程如下:

绝大多数刚刚被创建的对象会存放在伊甸园空间。

在伊甸园空间执行了第一次GC之后,存活的对象被移动到其中一个幸存者空间。

此后,在伊甸园空间执行GC之后,存活的对象会被堆积在同一个幸存者空间。

当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。之后会清空已经饱和的那个幸存者空间。

在以上的步骤中重复几次依然存活的对象,就会被移动到老年代。

执行过程如下:



内存分配和回收策略

对象的内存分配主要分配在Eden区上,如果启动了本地线程分配缓冲,按现在优先在TLAB上分配。内存分配优先集如下:

JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
也就是说,Java中每个线程都会有自己的缓冲区称作TLAB(Thread-local allocation buffer),每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可

对象优先在Eden分配:大多情况下,对象在新生代Eden区分配,当Eden没有足够空间时候会进行一次minor GC。

大对象直接进入老年代:大对象指的是大量连续内存空间的Java对象。直接进入老年代的目的是避免在Eden以及两个Survivor之间发生大量的内存复制。

长期存活的对象进入老年代:虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过一次minor GC仍然存活,并且被Survivor容纳,将被移动到Survivor控件,年龄置为1,。对象每熬过一场minor GC,年龄加1,当年龄到达一定程度时候(默认15),迁移至老年代。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Java Sweep Mark