Java的GC与内存分配策略

资料整理来源以及参考:java

深刻JAVA虚拟机git

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

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

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

可达性分析算法

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

能够做为GC Roots的对象包括:code

  • 虚拟机栈中引用的对象。
  • 方法区中静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI引用的对象。

图示以下:对象

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

然而,一个对象是否真正的死亡,至少须要两次的标记过程:若是对象再进行可达性分析时候没有对应引用链关联到,则被标记一次,而后进行一次筛选,筛选的条件是此对象是否有必要执行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。

收集的过程以下:

  1. 绝大多数刚刚被建立的对象会存放在伊甸园空间。
  2. 在伊甸园空间执行了第一次GC以后,存活的对象被移动到其中一个幸存者空间。
  3. 此后,在伊甸园空间执行GC以后,存活的对象会被堆积在同一个幸存者空间。
  4. 当一个幸存者空间饱和,还在存活的对象会被移动到另外一个幸存者空间。以后会清空已经饱和的那个幸存者空间。
  5. 在以上的步骤中重复几回依然存活的对象,就会被移动到老年代。

执行过程以下:

内存分配和回收策略

对象的内存分配主要分配在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),迁移至老年代。

相关文章
相关标签/搜索