Java虚拟机(JVM)详解——看完直接入门(内存结构、GC算法)

Java发展史

JDK1.7之前时间轴

上图是Sun公司在被Oracle(甲骨文)收购前的几个重大时间节点。

       其中1995年更名原因是因为Oak注册登记时,发现该名已被其他公司占用,遂更名为Java。
       在Sun被收购后Oracle陆续发布了 JDK1.7JDK1.8 ,也是当今被使用的 最为广泛 的两个版本。
       在 JDK1.7及其以前 我们所使用的都是Sun公司的 HotSpot JVM ,但由于Sun公司和BEA公司都被Oracle收购,jdk1.8将采用Sun公司的 HotSpot 和BEA公司的 JRockit 两个JVM中精华形成 jdk1.8的JVM
       有过一定了解的同学肯定知道在JDK1.8移除了JVM中的 永久代 改为 元空间 。官方解释是:为 融合 HotSpot JVM 与 JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代

有了元空间就不再会出现永久代OOM问题了!(但是元空间依然会出现内存溢出)

Java虚拟机(JVM)

       Java虚拟机(Java Virtual Machine 简称JVM )是运行所有Java程序的 抽象计算机 ,是Java语言的 运行环境 ,它是Java 最具吸引力的特性之一。Java虚拟机有自己完善的硬体架构,如 处理器堆栈寄存器 等,还具有相应的 指令系统
       JVM在它的生存周期中有一个明确的任务,那就是 运行Java程序 ,因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。多个程序启动就会存在多个虚拟机实例,多个虚拟机实例之间数据不能共享。
       Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

       开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件),再然后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时编译器有选择的转换成机器码执行。简化下就是下面这个流程:
Java源文件编译器字节码文件JVM机器码
注:接下来的内容基本基于JDK1.8来讲

JVM内存结构

看过上图,应该对jvm有了一个大概的了解,如果没看懂也不要紧,慢慢听我来一一解释图中的这些组件

类装载子系统

       也可以称为 类加载器 ,是将字节码文件(.class)文件加载进JVM中运行的组件,加载过程如下:

  1. 加载:到指定或者默认的路径下查找和导入.class文件;
  2. 校验和解析:
    2.1) 检查加载进来的class的正确性;
    2.2) 给类的静态变量分配存储空间;
    2.3) 将符号引用转化成直接引用;
  3. 初始化:对静态变量,静态代码根据其数据类型块执行初始化操作;

具体更多的细节这里就不一一展开,文章末尾会有扩展知识传送门

字节码执行引擎

        执行引擎 是Java虚拟机 最核心 的组成部分之一,执行引擎在执行Java代码的时候可能会有 解释执行(通过解释器执行)和 编译器执行(通过即时编译器产生本地代码执行)两种选择,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

运行时数据区

        运行时数据区又分为 线程私有区域 (上图中紫色底色区域)和 线程公有区域(上图中蓝色底色区域)。

线程私有区域

        线程私有区域随每一个线程产生和消亡,因此基本不需要考虑内存回收的问题。主要包含三块:程序计数器虚拟机栈本地方法栈 。又因为操作系统会限制线程数量,所以一般我们不会去过多的修改栈的大小设置。

程序计数器

        是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。

虚拟机栈

        有些人也喜欢称其为 线程栈 或是 Java栈。当一个线程开始运行JVM虚拟机马上会在 虚拟机栈空间 中划一小块栈内存空间分配给该线程。虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。栈中的数据都是以 栈帧(Stack Frame)的格式存在。如下图:

栈帧

        线程每一个方法都会在该线程的内存中分配一小块栈帧内存空间给该方法,也就是 栈帧栈帧 的大小在编译期确定,不受运行期数据影响。方法执行完成后局部内存空间释放(出栈)。遵循 先进后出 原则。
        那么按照上图解释就是我们执行了main方法→main方法调用了test方法 如果test方法没有调用更多的其它方法则此刻线程栈中只有两个栈帧,当test方法执行完后,test方法栈帧也就会出栈,那么就只剩下一个main方法栈帧了。

局部变量表

        部变量表是一组变量值的一片连续的内存空间,用于存放方法参数和方法内部定义的局部变量。(创建的对象则会存放对象在堆中的地址)

操作栈

        也可以叫做 操作数栈 ,当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。
例我有如下代码:

public static void main(String[] args) {
        int num = 1;
        int sum = num + 2;
        System.out.println(sum);
    }

栈中的执行过程如下:

  1. iconst_1 将int型 1 推送至栈顶
  2. istore_1 将栈顶int型数值存入第二个本地变量(大家考虑下为啥是第二个不是第一个?)
  3. iload_1 将第二个int型本地变量推送至栈顶
  4. iconst_2 将int型 2 推送至栈顶
  5. iadd 将栈顶两int型数值相加并将结果压入栈顶
  6. istore_2 将栈顶int型数值存入第三个本地变量
  7. getstatic 获取指定类的静态域,并将其值压入栈顶(java/lang/System.out)
  8. iload_2 将第三个int型本地变量推送至栈顶
  9. invokevirtual 调用实例方法(也就是println 打印出栈顶的这个值 也就是我们的sum)
  10. return 从当前方法返回void

大家也可以自己通过 javap -c 文件名.class 来看代码具体在堆栈中执行的信息

动态链接

        符号引用和直接引用在运行时进行 解析链接过程 ,叫 动态链接 (Dynamic Linking)。一个方法调用另一个方法,或者一个类使用另一个类的成员变量时,需要知道其名字,符号引用 就相当于名字,这些被调用者的名字就存放在Java字节码文件里(.class 文件)。

方法返回地址

        也可以叫 方法出口 ,当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法 返回的字节码指令 ;一种是遇见 异常 ,并且这个异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法 被调用的位置 ,程序才能继续执行。方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈、把返回值(如果有的话)压入调用者栈帧的操作数栈中、调整PC计数器的值以指向方法调用指令后面的一条指令等。

本地方法栈

        本地方法栈虚拟机栈 的作用和原理都非常相似。区别只不过是Java栈是为了执行 Java方法 服务的,而本地方法栈则是为了执行 本地方法(Native Method)服务的,现在用的少了,在JAVA才问世的时候,经常需要去调用C语言所编写的一些本地方法。

线程公有区域

方法区(元空间)

        它与 一样,是被线程共享的区域。用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

        堆是是一个完全二叉树实现的,堆要求孩子节点要小于等于父亲节点。
        堆是 Java虚拟机 管理内存 最大 的一块内存区域,Java中的 内存是用来存储对象本身以及数组,堆是被所有线程 共享 的,在JVM中只有一个堆,也是 GC所主要针对的区域 。new 出来的对象 一般存放在在堆里面, 有可能会在栈上面。
        逃逸分析 (Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。当一个对象在方法中被定义后判断其是否有可能被外部方法所引用。如果判断不会被外部方法引用,则可能会分配到 上。

喝口水然后我们来好好了解一下堆,看我一枪 … … 呃 不好意思,咱们看图看图
堆介绍图
       JDK1.8中将堆内存分为两个大区 Young area 年轻代Old area 老年代 。其中年轻代默认占有堆内存的 三分之一 ,老年代默认占有堆内存的 三分之二

Young area 年轻代

       年轻代又分为 Eden区Survivor区 ,Survivor区由 From Survivor(S0)To Survivor(S1) 组成,默认比例是:8(Eden) : 1(S0) : 1(S1) ,设置这个比例是为了充分利用内存空间,减少浪费。
       年轻代对象朝生夕死,存活率很低,在年轻代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。新生成的对象 一般 先放到年轻代 Eden区 (之前有所过 逃逸分析 会分配到栈上,如果 对象过大 也有可能直接分配到老年代)。当Eden区没有足够的空间进行分配时,触发Minor GC
       GC开始时,对象只会存在于 Eden区S0/S1(S0和S1其中一个作为保留区域,后文中假定S1为保留区)。 GC进行时 ,Eden区中所有存活的对象都会被复制到 S1,而 S0 中仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮GC,年龄值就加1,GC分代年龄存储在对象头中)的对象会被 移到老年代 中。没有达到阀值的对象会被复制到S1,接着清空Eden区和S0区。然后开始等待下一轮垃圾回收,此时S0成为新的保留区。

Minor GC对年轻代进行回收,不会影响到老年代。因为年轻代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。

这里有个问题:当Eden区存活的对象大小超过了S1区的大小怎么办?

空间分配担保机制
       当出现Minor GC后大部分对象仍然存活的话,就需要老年代进行分配担保,把 Survior区无法容纳的对象直接晋升到老年代
       在发生 Minor GC之前 ,虚拟机会检查 老年代最大可用的连续空间 是否 大于新生代所有对象的总空间 。如果大于,则此次Minor GC是安全的,如果小于,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果HandlePromotionFailure = true,那么会继续检查老年代最大可用连续空间是否大于 历次晋升到老年代的对象的平均大小 ,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure = false,则改为进行一次Full GC。

Old area 老年代

       老年代里存放的都是存活时间较久的,大小较大的对象,当年老代容量不足以继续分配的时候,会触发一次Full GC,回收老年代和年轻代中不再被使用的对象资源。

Full GC:对整个堆进行回收,包括年轻代和老年代以及元空间。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满、元空间被写满和System.gc()被显式调用等。

课外知识:对象升入老年代的几种方式
  • 部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
  • 如果对象的大小大于Eden的二分之一会直接分配在old,如果old也分配不下,会做一次majorGC,如果小于eden的一半但是没有足够的空间,就进行minorgc也就是新生代GC。
  • minor gc后,survivor仍然放不下,则放到老年代
  • 动态年龄判断 ,大于等于某个年龄的对象超过了survivor空间一半 ,大于等于某个年龄的对象直接进入老年代

GC

垃圾判断算法

引用计数法(基本不用)

       在这种算法中,假设堆中每个对象,都有一个引用计数器。当一个对象被创建并且初始化赋值后,该对象的计数器的值就设置为 1,每当有一个地方引用它时,计数器的值就加 1。反之,当引用失效时,例如一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,则之前被引用的对象的计数器的值就减 1。而那些引用计数为 0 的对象,就可以称之为垃圾,可以被收集。当一个对象被当做垃圾收集时,它引用的任何对象的计数器的值都减 1。
优点:引用计数法实现起来比较简单,对程序不被长时间打断的实时环境比较有利。
缺点:需要额外的空间来存储计数器,难以检测出对象之间的循环引用。

可达性分析法(普遍使用)

可达性分析法
       可达性分析法的基本思路是:将一系列的 根对象( GC Roots) 作为 起始点 ,从这些节点开始向下搜索,搜索所走过的路径称为 引用链 ,如果一个对象到根对象没有任何引用链相连,那么这个对象就不是可达的,也称之为 不可达对象 。更准确的说,一个对象只要满足下述两个条件之一,就会被判断为可达的:对象是属于根集中的对象对象被一个可达的对象引用
GC Roots 有哪些:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中的常量引用的对象
  • 方法区中的静态属性引用的对象
  • 本地方法栈中JNI(即一般说的native方法)中引用的对象

       开始进行标记前,需要先在 安全点暂停 应用线程,否则如果对象图一直在变化的话是无法真正去遍历它的。对于安全点,另一个需要考虑的问题就是如何在 GC 发生时让所有线程(这里不包括执行 JNI 调用的线程)都“跑”到最近的安全点上再停顿下来。两种解决方案:

  • 抢先式中断(Preemptive Suspension):抢先式中断不需要线程的执行代码主动去配合,在 GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应 GC 事件。
  • 主动式中断(Voluntary Suspension):主动式中断的思想是当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志地地方和安全点是重合的。

小知识:暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。

       在根搜索算法中,要真正宣告一个对象死亡,至少要经历 两次标记过程:
       如果对象在进行根搜索后发现没有与根对象相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。
       如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为 F-Queue 的队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后 GC 将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。
GC 判断对象是否可达看的是强引用

  • 强引用(Strong Reference):如Object obj = new Object(),这类引用是 Java 程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用(Soft Reference):它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2 之后提供了SoftReference类来实现软引用。
  • 弱引用(Weak Reference):它也是用来描述非必须对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 JDK1.2 之后,提供了WeakReference类来实现弱引用。
  • 虚引用(Phantom Reference):也称为幻引用,最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2 之后提供了PhantomReference类来实现虚引用。

优点:可以解决循环引用的问题,不需要占用额外的空间
缺点:多线程场景下,其他线程可能会更新已经访问过的对象的引用

垃圾回收算法

标记-清除算法

       标记-清除(Tracing Collector)算法是最基础的收集算法,为了解决引用计数法的问题而提出。它使用了根集的概念,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析法中判定垃圾对象的标记过程。

优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
缺点:这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小,对空闲列表的管理会增加分配对象时的工作量;标记清除后会产生大量不连续的内存碎片,虽然空闲区域的大小是足够的,但却可能没有一个单一区域能够满足这次分配所需的大小,因此本次分配还是会失败,不得不触发另一次垃圾收集动作。

标记-整理算法

       标记-整理(Compacting Collector)算法标记的过程与“标记-清除”算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。

优点:经过整理之后,新对象的分配只需要通过指针碰撞便能完成,比较简单;使用这种方法,空闲区域的位置是始终可知的,也不会再有碎片的问题了。
缺点:GC 暂停的时间相比标记-清除会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。

标记-复制算法

       复制(Copying Collector)算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。 复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如“标记-整理”算法。一种典型的基于复制算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执行。

优点:标记阶段和复制阶段可以同时进行;每次只对一块内存进行回收,运行高效;只需移动栈顶指针,按顺序分配内存即可,实现简单;内存回收时不用考虑内存碎片的出现。
缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。

分代收集算法(目前JVM所选用的策略)

       分代收集(Generational Collector)算法的将堆内存划分为新生代、老年代。分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收,以便提高回收效率。

  • 新生代 GC(Minor GC / Scavenge GC):发生在新生代的垃圾收集动作。因为 Java 对象大多都具有朝生夕灭的特性,因此 Minor GC 非常频繁(不一定等 Eden 区满了才触发),一般回收速度也比较快。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用 标记-复制算法 来完成收集。
  • 老年代 GC(Major GC / Full GC):发生在老年代的垃圾回收动作。由于老年代中的对象生命周期比较长,因此 Major GC 并不频繁,一般都是等待老年代满了后才进行 Full GC,而且其速度一般会比 Minor GC 慢10倍以上。另外,在老年代中进行 Full GC 时,会顺便清理掉元空间中的废弃对象。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 标记-清除算法标记-整理算法 来进行回收。

垃圾收集器

  • Serial Collector:串行收集器
  • Paraller Collector:即并行收集器
  • STW:Stop-the-world 停止所有用户线程,全心全意去做垃圾回收。

Serial 收集器

       单线程方式进行收集,且在 GC 线程工作时,系统不允许应用线程打扰。此时,应用程序进入暂停状态(STW),Serial 是针对年轻代的垃圾回收器,采用 标记-复制算法

ParNew 收集器

       充分利用了多 处理器 ( CPU )的优势,多个 GC 线程并行收集,相比Serial 收集器极大地缩短STW时间,ParNew 是针对年轻代的垃圾回收器,采用 标记-复制算法

Parallel Scavenge 收集器

       和 ParNew 类似,但更注重吞吐率。在 ParNew 的基础上演化而来的 Parallel Scanvenge 收集器被誉为 吞吐量优先 收集器。 吞吐量 就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即 吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间) 。如虚拟机总运行了 100 分钟,其中垃圾收集花掉 30 秒,那吞吐量就是99.5%。
       Parallel Scanvenge 收集器在 ParNew 的基础上提供了一组参数,用于配置期望的收集时间或吞吐量,然后以此为目标进行收集。通过 VM 选项可以控制吞吐量的大致范围:

  • -XX:MaxGCPauseMills:期望收集时间上限,用来控制收集对应用程序停顿的影响。
  • -XX:GCTimeRatio:期望的 GC 时间占总时间的比例,用来控制吞吐量。
  • -XX:UseAdaptiveSizePolicy:自动分代大小调节策略。

       但要注意停顿时间与吞吐量这两个目标是相悖的,降低停顿时间的同时也会引起吞吐的降低。因此需要将目标控制在一个合理的范围中。

Serial Old 收集器

       Serial Old 是 Serial 收集器的老年代版本,单线程收集器,采用 标记-整理算法 。这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。

Parallel Old 收集器

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

CMS收集器

       CMS(Concurrent Mark Swee)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS 收集器仅作用于老年代的收集,采用 标记-清除算法 ,它的运作过程分为 4 个步骤:

  1. 初始标记(CMS initial mark):初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。需要STW
  2. 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,可以和用户线程并行。无需STW
  3. 重新标记(CMS remark):是为了 修正并发标记期间 因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始阶段稍长一些,但远比并发标记的时间短。需要STW
  4. 并发清除(CMS concurrent sweep):采用 标记-清除算法 。无需STW

       CMS 以流水线方式拆分了收集周期,将耗时长的操作单元保持与应用线程并发执行。只将那些必需 STW 才能执行的操作单元单独拎出来,控制这些单元在恰当的时机运行,并能保证仅需短暂的时间就可以完成。这样,在整个收集周期内,只有两次短暂的暂停(初始标记和重新标记),达到了近似并发的目的。CMS 收集器之所以能够做到并发,根本原因在于采用基于 标记-清除算法 并对算法过程进行了细粒度的 分解 。前面已经介绍过 标记-清除算法 将产生大量的内存碎片这对新生代来说是难以接受的,因此新生代的收集器并未提供 CMS 版本。

G1 收集器

       G1(Garbage First)重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成,即 G1 提供了接近实时的收集特性。
G1 具备如下特点:

  • 并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU 来缩短 STW 停顿的时间,部分其他收集器原来需要停顿 Java 线程执行的 GC 操作,G1 收集器仍然可以通过并发的方式让 Java 程序继续运行。
  • 分代收集:打破了原有的分代模型,将堆划分为一个个区域。
  • 空间整合:采用 标记-整理算法 实现收集器,
  • 可预测的停顿:这是 G1 相对于 CMS 的一个优势,通过期望停顿时间来确定回收计划。

它的运作过程分为 4 个步骤:

  1. 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,耗时很短。需要STW
  2. 并发标记(Concurrent Marking):是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。无需STW
  3. 最终标记(Final Marking):是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。需要STW
  4. 筛选回收(Live Data Counting and Evacuation):首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。因为只回收一部分 Region,时间是用户可控制的,停顿用户线程将大幅提高收集效率。需要STW

查看 JVM 使用的默认垃圾收集器
CMD 执行命令: java -XX:+PrintCommandLineFlags -version
查看 JVM 使用的默认垃圾收集器
JDK1.8默认打开 -XX:+UseParallelGC

参数垃圾收集器详解
UseSerialGC虚拟机运行在 Client 模式下的默认值,使用 Serial + Serial Old 组合进行内存回收
UseParNewGC使用 ParNew + Serial Old 组合进行内存回收
UseConcMarkSweepGC使用 ParNew + CMS + Serial Old 组合进行内存回收,Serial Old 收集器将作为 CMS 收集器出现Concurrent Mode Failure失败后的备用收集器使用
UseParallelGC虚拟机运行在 Server 模式下的默认值,使用 Parallel Scavenge + Serial Old (PS MarkSweep) 组合进行内存回收
UseParallelOldGC使用 Parallel Scavenge + Parallel Old 组合进行内存回收

韭菜课堂:内存溢出与内存泄漏的区别
内存泄漏(memory leak) : 是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
内存溢出( out of memory) : 指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。


参考资料
扩展知识传送门

每一次阅读、点赞、收藏、评论都是我坚持下去最大的动力谢谢大家

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhibo_lv

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值