GC垃圾回收——总结

GC垃圾回收

JVM的垃圾回收机制,在内存充足的状况下,除非你显式调用System.gc(),不然它不会进行垃圾回收;在内存不足的状况下,垃圾回收将自动运行html

判断对象是否要回收的方法

引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器减1。当计数器为0的时候,对象就能够被回收。java

缺点:存在循环引用的状况,致使2个循环引用对象的内存得不到释放。目前没有一个JVM的垃圾回收实现是使用这个算法的。程序员

主流的Java虚拟机没有使用引用计数算法来管理内存,由于它很难解决循环引用的问题。算法

可达性分析算法

思路是:经过一系列“GC Roots”对象做为起点,从这些节点开始向下进行搜索,搜索所走过的路径被称为“引用链”。当一个对象到GC Roots没有任何引用链相连,也就是说从GC Roots到这个对象不可达,则证实此对象是不可用的。如图object五、object六、object7虽然互相关联,可是他们到GC Roots是不可达的,因此他们将被判断为可回收的对象。数据库

image-20191026100934969

(把一些对象当作root对象,JVM认为root对象是不可回收的,而且root对象引用的对象也是不可回收的)数组

在Java语言中,可做为GC Roots的对象包含下面几种:缓存

  • 虚拟机栈(栈帧中本地变量表)中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中Native方法引用的对象

即时在可达性分析法中不可达的对象,也并不是是非死不可,要真正宣告一个对象的死亡,只要要经历2次标记的过程安全

  • 若是对象在进行可达性分析以后,发现没有与GC Roots相连的引用链,那它将会被第一次标记。
  • 判断该对象是否有必要执行finalize(),若是对象没有覆盖finalize方法或者finalize已经被覆盖过了,虚拟机将这两种状况视为”没有必要执行“。服务器

    • 若是这个对象被判断为有必要执行finalize()方法,那么这个对象将会被放置在一个F-Queue的队列中,并在稍后由虚拟机创建的、低优先级的Finalizer线程去执行。这里的”执行“指的是虚拟机会触发这个方法,可是不承诺等待到该方法执行完毕。这样作的缘由是:数据结构

      • 若是一个对象的finalize()方法执行缓慢,甚至发生了死循环,那么将致使F-Queue队列中其余对象永久等待下去,甚至致使整个内存回收系统奔溃,由于在F-Queue中的对象没法进行垃圾回收。
    • finalize()方法是对象最后一次逃脱死亡命运的机会,若是对象在finalize()方法中成功拯救本身——和引用链上任何一个对象关联起来,好比把本身(this)赋值给某个类变量或者成员变量,那么在第二次标记时,它将被移除”即将回收“的集合。
    • 若是对象没有成功逃脱,那么基本上它就真的被回收了(第二次标记)。

任何一个对象的finalize方法只会被系统自动调用一次,若是对象面临下一次回收,它的finalize方法不会被再次执行。尽可能避免使用finalize方法,由于它只是为了使C/C++程序员更容易接收Java所做出的一个妥协,它的运行代价高昂,不肯定性达,没法保证各个对象的调用顺序。

public class HYFinalize {


    public static void main(String[] args) {
        Book book = new Book(true);

        book.checkIn();

        // 每一本书都应该进行checkIn操做,从而释放内存。
        // 这本书没有进行 checkIn操做,所以,没有执行清理操做(没有输出finalize execute)。也就是利用finalize方法进行终结验证,从而找出没有释放对象的内存。
        new Book(true);

        // 手动调用垃圾回收
        System.gc();
    }

}

class Book {
    boolean checkOut;

    public Book(boolean checkOut) {
        this.checkOut = checkOut;
    }

    void checkIn() {
        checkOut = false;
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();

        if (checkOut) {
            System.out.println("finalize execute");
        }
    }
}

方法区中的垃圾回收

Java虚拟机规范确实说过能够不在方法区中实现垃圾收集,方法区的垃圾收集效率也很是低,由于条件苛刻。

方法区(在HotSpot虚拟机中称为永久代)主要回收的内容有:废弃常量和无用的类

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

对于无用的类则须要同时知足下面3个条件:

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

这里解释下为何须要回收该类的ClassLoader?

public Class<?> getDeclaringClass() throws SecurityException {
        final Class<?> candidate = getDeclaringClass0();
  /*
  * 反射里面使用到ClassLoader,所以要把ClassLoader干掉,才能保证没有地方能够经过反射调用到Class类。
  * 而后当类的实例都会被回收了,而且该类没有在任何地方被引用到了,那么这个类就能够被回收了
  */
  if (candidate != null)
  candidate.checkPackageAccess(
  ClassLoader.getClassLoader(Reflection.getCallerClass()), true);
  return candidate;
}

能够经过虚拟机参数控制类是否被回收 -Xnoclassgc。

在大量使用反射、动态代理、GCLib等ByteCode框架、动态生成JSP 这类频繁自定义ClassLoader的场景,都须要虚拟机具有类卸载功能,以保证永久代不会溢出。

常见的垃圾回收算法

标记-清除算法

思想

算法分为标记、清除两个阶段:首先标记处全部须要回收的对象,在标记完成后,统一回收全部被标记的对象。

它的标记过程,使用的是可达性分析算法。

它是最基础的算法,由于后面的垃圾回收算法都是基于标记-清除算法进行改进。标记-清除也是最简单的算法。

image-20191026104917493

优势

实现简单

缺点

  • 一个是效率问题,标记和清除过程,两个效率都不高
  • 另一个是空间问题,标记-清除以后,会产生大量不连续的内存碎片。空间内存碎片太多,那么须要给较大的对象分配内存空间的时候,没法找到足够内存空间,而不得不提早触发一次垃圾回收。

复制收集算法

思想

将可用内存划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还活着的对象复制到另一块上,而后再把已使用过的内存空间一次性清理掉。

优势

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

缺点

只是这种算法的代价将内存缩小为原来的一半,代价过高了。

image-20191026105459849

如今商业虚拟机都采用这种方法来回收新生代,IBM公司研究代表,新生代中的对象98%都是朝生暮死的,因此并不须要按照1:1来划份内存空间,而是将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。

当回收时,将Eden和Survivor中还存活的对象一次性复制到另一个Survivor空间上,最后清理Eden和刚才使用过的Survivor空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存为整个新生代的90%,只有10%的内存会被浪费。

固然,98%的对象可回收只是通常场景下的数据,咱们没有办法保证每次回收都只有很少于10%内存大小的对象存活,当Survivor空间不够时,须要依赖其余内存进行分配担保

分配担保:若是另一块Survivor空间没有足够空间存放上一次新生代回收存活下来的对象时,这些对象将直接进入老年代。

Mark-Compact(标记-整理算法)

思想

复制收集算法在对象存活率较高时,就要进行较多的复制操做,致使效率变低。因为老年代存活率较高,因此通常不采用这种算法。

根据老年代的特色,有人提出了”标记-整理“算法,标记过程仍然使用”可达性分析算法“,而后让全部的存活对象向一端移动,而后直接清理掉端边界之外的内存。

image-20191026112718120

优势

  • 不容易产生内存碎片
  • 内存利用率高

缺点

  • 存活对象多而且分散的时候,移动次数多,效率低下
  • 程序暂停

分代收集算法

思想:

只是根据对象的存活周期的不一样把堆分红新生代和老年代(永久代指的是方法区),这样就能够根据各个年代的特色采用最适当的收集算法。

“分代收集”是目前大部分JVM的垃圾收集器所采用的算法。

在新生代中,每次垃圾收集都有大量对象死去,只有少了存活,那就采用复制算法,只需付出少许对象的复制成本就能够完成收集。

而老年代中对象存活率高,而且没有其余空间对它进行分配担保,就必须使用标记-清理 或者 标记-整理算法进行回收。

新生代
  • 在新生代里面存放的是存活时间比较短的对象,如某一个方法的局域变量、循环内的临时变量等等
  • 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。
  • 新生代里面分红一份较大的Eden空间和两份较小的Survivor(存活)空间。每次只使用Eden和其中一块Survivor空间,而后垃圾回收的时候,把存活对象放到未使用的Survivor空间中,清空Eden和刚才使用过的Survivor空间。
  • 一块Eden和一块Survivor区,比值为8:1。这样子的设置是有缘由的。新生代采用复制算法,若是单纯的把内存分为2块,因为存活对象不多,那么存放存活对象的那块堆内存,会有不少内存浪费。所以,使用两块10%的内存做为空闲和活动区间(两块Survivor区),而另外80%的内存(Eden区),则是用来给新建对象分配内存的。一旦发生GC,将10%与另外80%的活动区间 中存活的对象转移到10%的空闲区间,接下来,将以前90%的内存所有释放。
  • 绝大多数刚建立的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,所以在其上分配内存极快

20160730141640502

新生代垃圾回收流程

  • 当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的)
  • 此后,每次Eden区满了,就执行一次Minor GC,并将Eden剩余的存活对象都添加到Survivor0
  • 当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,而后清理掉Survivor0区。以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。重复上述步骤,只不过此次是Eden区和Survivor1区配合。

Eden区是连续的空间,且Survivor总有一个为空。通过一次GC和复制,一个Survivor中保存着当前还活 着的对象,而Eden区和另外一个Survivor区的内容都再也不须要了,能够直接清空,到下一次GC时,两个Survivor的角色再互换。所以,这种方 式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“中止-复制(Stop-and-copy)”法),这不表明着中止复制清理法很高效,其实,它也只在这种状况下高效,若是在老年代采用中止复制,则挺悲剧的。

老年代(tenured)
  • 存放的是存活时间比较长的对象,如缓存对象、数据库链接对象、单例对象等等
  • 老年代中由于对象存活率高、没有额外空间对它进行分配担保,就只能使用“标记-清除”或“标记-整理”算法来进行回收。
  • 在新生代里的每个对象,都会有一个年龄,当这些对象的年龄到达必定程度时(年龄就是熬过的GC次数,每次GC若是对象存活下来,则年龄加1),则会被转到年老代,而这个转入年老代的年龄值,通常在JVM中是能够设置的。
永久代
  • 在堆区外有一个永久代,
  • 对永久代的回收主要是无效的类和常量,而且回收方法同老年代

HotSpot的GC算法实现

枚举根节点

可做为GC Roots的节点主要在全局性的引用(例如常量或者静态属性)、执行上下文中(例如栈帧中的本地变量表),如今不少应用仅仅方法区就有数百兆,若是要逐个检查这里面的引用,找出GC Roots节点,那么必然会消耗不少的时间,

另外,可达性分析对执行时间的敏感还提如今GC停顿上,由于这项分析工做必须在一个能确保一致性的快照中进行——这里”一致性“的意思是整个系统看起来像被冻结在某个时间点上,不能够出现分析过程当中引用关系还在变化的状况,该点不知足的话,分析结果的准确性将没法保证。这点是致使GC进行时必须停顿全部Java执行线程的其中一个重要缘由。即时是在号称几乎不会停顿的CMS收集器中,枚举根节点时也是要停顿的。

准确式内存管理

准确式内存管理,又称为“准确式GC”。

虚拟机能够知道内存中某个位置的数据具体是什么类型。好比内存中有一个32位的整数123456,它究竟是一个引用类型,指向123456的地址,仍是一个数值为123456的整数,虚拟机将由能力辨别出来,这样子才能在GC的时候,准确判断堆上的数据是否还可能被使用。

因为使用准确式内存管理,Exact VM抛弃了基于handler的对象查找方式(缘由是GC后对象可能被移动位置,好比对象的地址本来为123456,而后该对象被移动到654321的地址,在没有明确信息代表内存中的哪些数据是引用的前提下,虚拟机是不敢把内存中全部123456的值改成654321的,由于不知道这个值是整数仍是指向另一块内存的地址,所以有些虚拟机使用句柄来保持引用的稳定),经过准确式内存管理,能快速判断该数据是否引用,就能够避免使用句柄,从而减小一次查找地址的开销,提升执行性能。

因为目前主流的Java虚拟机都是采用准确式GC,因此当执行系统停顿下来后,并不须要一个不漏的检查完执行上下文和全局的引用位置,虚拟机应当有办法直接指导哪些地方存放着对象引用。

HotSpot怎么快速找到可达对象

在HotSpot实现中,使用一组称为OopMap的数据结构来达到这个目的。

在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程当中,也会在特定位置记录下栈和寄存器中哪些位置是引用。这样子,在GC扫描的时候,就能够直接知道哪些是可达对象了。

安全点

在OopMap的协助下,HotSpot能够快速准确完成GC Roots枚举。可能致使OopMap内容变化的指令很是多,若是为每一条指令都生成对应的OopMap,那么将会须要大量的额外空间,这样GC的成本将会变得很高。

实际上,HotSpot也没有为全部指令生成OopMap,只有在特定位置生成这些信息,这个位置称为“安全点”。

程序在执行过程当中,并不是在全部地方均可以停顿下来进行GC,只有在到达安全点时才能暂停。安全点的选定既不能太以致于让GC等待太长的时间,也不能过多以致于增大运行时的负荷。因此安全点的选定是以“是否让程序长时间运行”为标准进行选定的。长时间运行最明显的特征是指令复用,好比说方法调用、循环跳转、异常跳转等,因此具备这些功能的指令才会产生安全点。

对于安全点,另一个须要考虑的问题,是如何让全部线程跑到最近的安全点再停顿下来,这里有2种方案可供选择:抢占式中断、主动式中断。

抢占式中断

抢占式中断不须要线程的执行代码主动去配合。

在GC发生时,首先把全部线程中断,若是发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。

如今几乎没有虚拟机采用抢占式中断来暂停线程。我的以为是太粗暴了,好比直接中断线程。

主动式中断

主动中断的思想是:当GC须要中断线程时,不直接对线程操做,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,当发现中断标记为真就本身中断挂起。轮询标记的位置就是安全点的位置。

安全区域

使用安全点是否已经完美解决何时进入GC的问题。可是假如程序不执行呢?因此的程序不执行就是没有分配CPU时间片,最典型的就是线程处于sleep或者阻塞状态,这时候线程没法执行到安全点,而且响应中断挂起。JVM也不太可能等待线程从新得到CPU时间片,这时候就须要安全区域来解决。

安全区域指在一段代码片断中,引用关系不会发生变化。这个区域任务地方开始GC都是安全的。咱们能够把安全区域看作是扩展的安全点。

在线程执行到安全区域时,首先标识本身已经进入安全区域了,那样,当这段时间内发生GC时,就不用管那些标识为安全区域状态的线程了。

在线程要离开安全区域时,它首先检查系统是否已经完成根节点枚举,若是完成,线程就继续执行,不然,它就继续等待直到收到能够离开安全区域的信号。

垃圾收集器

若是说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

image-20191026130140758

上图展现了不一样分代的垃圾收集器,若是两个收集器之间存在连线,那么说明它们能够搭配使用。

垃圾收集器所处的区域,则代表它是新生代收集器,仍是老年代收集器。

Serial收集器

Serial收集器是最基本,发展历史最悠久的收集器。Serial是一个单线程收集器。是新生代收集器。

Serial收集器在进行垃圾收集的时候,必须暂停其余全部的线程,直到它收集结束。“Stop the World”暂停线程 这个工做是后台自动发起和完成的,在用户不可见的状况下把用户正常工做的线程停掉,这对于不少应用来讲是很难接受的。假如你的计算机每运行1个小时就要停顿5分钟,你会有怎样的心情?下图展现了Serial收集器的运行过程:

image-20191026131046698

“Stop the World”是没有办法避免的,举个简单例子:你妈妈在打扫房间的时候,你还一遍扔垃圾,这怎么打扫的完?。目前之间尽可能减小停顿线程的时间。

serial收集器仍然是虚拟机运行在client模式下的默认新生代垃圾收集器。它也有因为其余收集器的地方:简单而高效。对于单CPU的环境来讲,Serial收集器因为线程交互的开销,专心作垃圾收集,天然能够得到最好的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存通常不会很大,收集几十兆甚至一两百兆的新生代,停顿时间能够控制在几十毫秒甚至一百毫秒之内,只要不是频繁发生,仍是能够接受的。

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本。除了多线程进行垃圾收集以外,其余都和Serial同样。

是新生代收集器。

ParNew收集器的工做过程如图:

image-20191026135929901

ParNew收集器是许多运行在Server模式下的虚拟机首选的新生代收集器。由于除了Serial收集器外,只有它能和CMS收集器配合工做。

Parallel Scavenge收集器

Parallel Scavenge是新生代收集器。它也是使用复制算法的收集器,又是并行的多线程收集器。看上去了ParNew同样,那么它有什么特别之处呢?

Parallel Scavenge是为了达到一个可控制的吞吐量。吞吐量=运行用户代码的时间 / (运行用户代码的时间 + 垃圾收集的时间)。高吞吐量代表CPU时间被有效的利用,尽快完成程序的运算任务。

Parallel Scavenge收集器提供了参数控制最大垃圾收集停顿时间,虚拟机将尽量保证垃圾回收的时间不超过该值。不过你们不要任务把这个参数的值设小一点就可使垃圾收集速度加快,GC停顿时间缩短,是以牺牲吞吐量和新生代空间来换取的,系统会把新生代调小一些,收集300MB的新生代确定比收集500MB的快,但这也致使垃圾收集更频繁一些。原来10秒收集一次,每次停顿100毫秒,如今变成5秒收集一次,每次停顿70毫秒。停顿时间是降低了,可是系统吞吐量下来了。

因为和吞吐量关系密切,Parallel Scavenge也被称为吞吐量优先收集器

Parallel Scavenge还有一个参数,这个参数打开之后,就不须要手工指定新生代大小、Eden和Survivor比例等参数,虚拟机会根据运行状况,动态调整这些参数,已提供最适合的停顿时间,这种调节方式成为GC自适应调节策略。自适应调节策略也是Parallel Scavenge收集器和ParNew收集器的一个重要区别。

Parallel Scavenge没法和CMS配合工做。

Serial Old收集器

Serial Old是Serial收集器的老年代版本。它一样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义是给Client模式下的虚拟机器使用,工做过程以下:

image-20191026142719613

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。

这个收集器是JDK1.6以后才开始提供的,在此以前,Parallel Scavenge收集器一直处于比较尴尬的位置,由于若是新生代选择了Parallel Scanvenge收集器,老年代除了Serial Old收集器以外别无选择。老年代Serial Old收集器在服务端的拖累,使用Parallel Scavenge收集器未必能在总体应用上得到吞吐量最大化的效果。因为单线程的老年代收集,没法充分利用服务端多CPU的能力,在老年代很大并且硬件比较高级的环境,这种组合的吞吐量甚至还不如ParNew + CMS组合给力。

直到Parallel Old收集器出现后,Parallel Scavenge才有了比较名副其实的应用组合。在注重吞吐量与多CPU的场景,能够优先考虑Parallel Scavenge 和 Parallel Old收集器。Parall Old工做状态如图:

image-20191026143854952

CMS收集器

概念

CMS是一种以获取最短停顿时间为目标的收集器。互联网应用就很是注重服务器的响应速度,但愿系统停顿时间最短,已给用户带来最好的体验。CMS收集器就很是符合这类应用的需求。

CMS收集器基于标记-清除算法实现的。它的运做过程分为4个步骤:初始标记、并发标记、从新标记、并发清除

其中,初始标记、从新标记两个步骤仍然须要暂停用户线程。

  • 初始标记仅仅是标记一下GC Roots可以直接关联的对象,速度很快。
  • 并发标记就是进行GC Roots 向下查找过程,也就是从GC Roots开始,对堆中对象进行可达性分析。这时候用户线程还能够继续执行。
  • 从新标记阶段是为了修正并发标记期间因用户线程继续运做而致使标记产生变更的那一部分对象标记记录

    • 这个阶段的标记时间通常比初始标记稍长一点,但远比并发标记时间短。
  • 并发清除是GC垃圾收集线程 和 用户线程并行的,清理被回收的对象。

因为整个过程当中耗时最长的并发标记并发清除的阶段收集器均可以和用户线程并行工做,因此整体上来讲,CMS收集器的内存回收是与用户线程一块儿并发执行的。

image-20191026145340936

优势

减小了GC停顿时间

缺点

  • 对CPU资源敏感,在并发阶段,由于占用一部分CPU资源,所以会致使程序变慢。当CPU个数比较少的时候,对用户影响可能很大。

    • 为了因对这种状况,虚拟机提供了一种增量式并发收集器,是CMS收集器的变种。在并发标记、并发清除阶段,让GC线程、用户线程交替运行,尽可能减小GC线程独占资源的时间,这样一来,整个垃圾收集的时间更长,但对用户的影响就少一些。实践证实,增量式并发收集器的效果很通常,已经不提倡用户使用了。
  • 没法处理浮动垃圾。在并发清除阶段,用户线程还在运行着,还会产生新的垃圾。这一部分垃圾出如今标记过程以后,CMS没法在当次收集中处理掉它们,只好留到下一次GC的时候再清理掉。这一部分垃圾就称为浮动垃圾

    • 因为垃圾收集阶段,用户线程还在运行,所以须要预留足够的内存空间给用户使用。 所以CMS收集器不能像其余收集器同样,等到老年代几乎被填满了再进行收集,须要预留一部份内存空间提供用户线程使用。在JDK1.6中,CMS的启动阈值为92%,也就是老年代使用了92%以后,CMS收集器就会进行垃圾回收。
    • 若是CMS运行期间预留的内存不够用户线程使用,就会出现一次“Concurent Mode Fail”失败,这时虚拟机临时启用Serial Old收集器来从新进行老年代的垃圾回收,这样的停顿时间就长了。所以,若是启动阈值设置得过高,容易致使“Concurrent Mode Fail”,性能反而下降。
  • CMS是一块基于标记-清除实现的垃圾收集器,那么在收集结束时会有大量内存碎片产生。当内存碎片过多的时候,若是要对大对象进行内存分配,可是没法找到足够大的连续内存空间进行分配,就会触发一次Full GC。

    • 为了解决这个问题,CMS提供一个开关,默认是开启的,表示CMS要进行Full GC的时候,开启内存碎片的整理合并过程,并该过程是没法并发的,所以停顿时间就变长了。

G1收集器

G1收集器是当前收集器发展最前沿的成果之一。G1收集器是一款面向服务端应用的垃圾收集器。Hotspot团队但愿G1收集器将来能替换掉CMS收集器。

在G1以前的其余收集器进行收集的范围都是整个新生代或者老年代,而G1再也不是这样。G1将堆分红许多大小相同的区域单元,每一个单元称为Region。Region是一块地址连续的内存空间,G1模块的组成以下图所示:

image-20191026170627820

G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分Region(不须要连续)的集合。Region的大小是一致的,数值是在1M到32M字节之间的一个2的幂值数,JVM会尽可能划分2048个左右、同等大小的Region。

G1收集器具有以下特色

  • 并发与并行:G1能充分利用CPU、多核环境的硬件优点。使用多个CPU缩短Stop the world,也就是工做线程的暂停时间。在执行GC动做的同时,G1收集器仍然可以经过并发的方式让Java程序执行。
  • 分代收集:与其余收集器同样,分代概念在G1收集器中仍然得以保留。虽然G1收集器不用其余收集器配合技能管理整个GC堆,但他依然可以采用不一样方式去处理新建立的对象和已经存活了一段时间、熬过屡次GC的旧对象 以取得更好的收集效果。
  • 空间整合:G1总体上看是基于标记-整理算法实现的收集器,从局部看(两个Region之间),是基于复制算法实现的。使用这两种算法,在G1运行期间不会产生内存碎片,垃圾收集后,能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会由于没法找到连续内存空间而提早触发下一次GC。
  • 可预测的停顿:G1除了追求减低停顿之外,还创建了可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间片断内,消耗在垃圾收集上的时间不得超过N毫秒。

停顿时间模型

G1收集器之因此能创建可预测的停顿时间模型,是由于它能够有计划的避免对整个堆进行垃圾收集。G1维护了一份优先列表,每次根据容许的收集时间,优先回收价值最大的region。(怎样才是价值大?region采用复制算法,那么若是一个region中垃圾不少,存活对象不多,那么这个迁移存活对象的工做就不多,而且收集完以后,可以获得的内存空间不少,这种就是价值大的region)。

这种使用region划份内存空间,而且有优先级的区域回收方式,保证G1收集器在有限的时间内,能够获取尽量高的效率。

避免全堆扫描

一个对象分配在某个Region中,可是它能够与整个Java堆中任意对象发生引用关系。那么作可达性分析法判断对象是否存活的时候,岂不是扫码怎么java堆才能确保准确性?这个问题其实并不是G1才有,只是G1更加突出而已。若是回收新生代不得不扫描老年代的话,那么Minor GC的 效率可能降低很多。

在G1收集器中,Region之间的引用 和 其余收集器中新生代和老年代之间的引用,虚拟机都是使用Remembered Set来避免全堆扫描。在G1中,每个Region都有一个Remembered Set。当对引用进行写操做的时候,G1检查该引用的对象是否在别的region中,是的话,则经过CardTable把相关引用信息存到被引用对象的Remembered Set中。当进行内存回收时,把RememberSet加入到GC Roots根节点的枚举范围。这样就能够保证不全堆扫描也不会有遗漏。

工做流程

若是不计算Remembered Set的操做,G1收集器的运做大体分为以下操做:初始标记、并发标记、最终标记、筛选回收。

  • 初始标记阶段:仅仅只是标记一下GC Roots直接关联的对象。而且修改TMAS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中建立对象。这阶段须要停顿线程,但耗时很短。
  • 并发标记阶段:从GC Roots开始,对堆中对象进行可达性分析,找出存活对象。这阶段耗时较长,但可与用户程序并发执行
  • 最终标记阶段:修复在并发标记阶段因用户程序运行致使标记发生变化的那一步部分标记记录。虚拟机将这段时间对象变化记录到Remembered Set Log中,在最终标记阶段把Remembered Set Log合并到Rmembered Set中。这阶段须要停顿线程。
  • 筛选回收阶段:首先对各个Region的回收价值和成本进行排序,根据用户所指望的GC停顿时间来指定回收计划。这个阶段其实也能够作到和用户线程一块儿并发执行,但由于只回收一部分Region,时间是用户可控制的,并且停顿线程能大幅提升收集效率。所以没有实现为和用户线程并发执行。

image-20191026182758933

内存分配与回收策略

对象的分配,主要在新生代的Eden区上,若是启动了本地线程分配缓存,那么将优先在TLAB上分配。少数状况下,也可能直接分配在老年代中。

对象优先在Eden区分配

大多数状况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

大对象直接进入老年代

大对象是指须要大量连续内存空间的对象。最典型的就是很长的数组或者字符串。常常出现大对象,就容易致使内存还有很多空间的时候,就触发GC来获取足够的连续内存空间来放置这些大对象。

长期存活对象将进入老年代

虚拟机怎么识别哪些对象应该存放到新生代,哪些对象应该存放到老年代?为了作到这点,虚拟机给每一个对象定义了一个对象年龄计数器(放在对象头的Mark World中)。

若是对象在Eden区出生,经历了第一次Minor GC后仍然存活,而且能被Survivor容纳的话,将被移动到Survivor空间,而后年龄计数器设置为1。

对象在Survivor中每熬过一次Minor GC,年龄就增长1岁。当它的年龄到达必定程度(默认是15岁),就会被移动到老年代。

动态年龄判断

若是Survivor中相同年龄的对象大小总和 大于 Survivor空间的一半,年龄大于或等于该年龄的对象就能够进入老年代。无需等到阈值(好比15岁)。

空间分配担保

新生代使用复制算法,可是为了保证内存利用率,所以只用其中一块Survivor区保存存活对象。所以当发生大量对象在Minor GC后仍然存活的状况,就须要老年代进行分配担保,就是指把Survivor没法容纳的对象直接进入老年代。老年代要进行这样的担保,前提是老年代自己还有容纳这些对象的剩余空间,一共有多少对象可以存活下来,在实际内存回收完成以前是没法知道的,只好取以前成功晋升老年代的对象容量大小的平均值,与老年代剩余空间进行比较。决定是否进行Full GC以腾出更多的空间。

具体操做为:

在进行Minor GC以前,虚拟机将会检查老年代最大可用的连续空间是否大于新生代全部对象总空间。若是这个条件成立,则认为Minor GC是安全的(老年代能够担保成功)。

若是不成立,则虚拟机会查看HandlePromotionFailure设置值是否容许担保失败。若是容许,将检查老年代最大可用连续空间是否 大于 以前成功晋升老年代的对象容量大小的平均值。若是大于,则尝试进行一次Minor GC。若是小于,或者HandlePromotionFailure设置为不容许担保失败,则进行一次Full GC。

HandlePromotionFailure通常打开,避免频繁Full GC。

在JDK6以后,已经再也不使用HandlePromotionFailure这个参数了,JDK6以后的规则变为:

  • 只要老年代的连续内存空间 大于新生代对象总大小 或者 大于历次晋升的对象平均大小,就进行minor GC,不然进行Full GC。

几种不一样的垃圾回收类型

Minor GC

Minor GC又称为新生代GC。指发生在新生代的垃圾收集动做。由于Java对象大多很快死亡,因此Minor GC很是频繁,通常回收速度也比较快。

Major GC/Full GC

又称为老年代GC。指发生在老年代的GC。出现了Major GC,常常伴随着一次Minor GC。Major GC通常速度比Minor GC慢10倍以上。

其余内存

除了Java堆和永久代以外,还有一些区域会占用比较多的内存,这里全部内存总和受到操做系统进程最大内存的限制。

  • Direct Memory:可用过-XX: MaxDirectMemorySize调整大小,内存不足时,会抛出OutOfMemoryError 或者 OutOfMemoryError : Direct buffer memory
  • 线程堆栈:可经过-Xss调整大小。内存不足时抛出StackOverflowError(即没法分配新的栈帧)或者OutOfMemoryError: unable to create new native thread(没法创建新的线程)
  • Socket缓冲区:每一个Socket链接都有Send和Receive两个缓冲区,分别占大约37KB和25KB内存,链接多的话,这两块内存占用也比较可观。若是没法分配,则可能抛出 IOException : Too many open files异常。
  • JNI代码:若是代码中使用JNI调用本地库,那本地库使用的内存也不在堆中
  • 虚拟机和GC:虚拟机、GC的代码执行也要消耗必定的内存

问题

为何要划分红年轻代和老年代?

为了针对不一样的内存区域采用不一样垃圾收集算法,从而提升效率

年轻代为何被划分红eden、survivor区域?

经过划分eden、survivor区,可以提升年轻代的内存使用率。由于年轻代的大部分对象都会很快死去,所以只须要使用少部分的内存来保留存活对象。

参考

https://blog.csdn.net/mccand1...

重点: https://www.cnblogs.com/aspir...、https://www.cnblogs.com/aspirant/category/1195271.html

https://www.cnblogs.com/1024Community/p/honery.html#25-%E6%96%B9%E6%B3%95%E5%8C%BA%E5%A6%82%E4%BD%95%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E9%9C%80%E8%A6%81%E5%9B%9E%E6%94%B6

[https://www.cnblogs.com/heyon...