# 内存区域

# Java 内存区域划分

Java 虚拟机规范规定的区域分为以下 5 个部分:

  • 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
  • Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
  • 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
  • Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
  • 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

# 本地内存

Java 虚拟机在执行的时候会把管理的内存分配成不同的区域,这些区域被称为虚拟机内存,同时,对于虚拟机没有直接管理的物理内存,也有一定的利用,这些被利用却不在虚拟机内存数据区的内存,我们称它为本地内存。
本地内存不受虚拟机内存参数的限制,只受物理内存容量的限制。
虽然不受参数的限制,但是如果内存的占用超出物理内存的大小,同样也会报 OOM。

元空间
在 Java1.8 中,HotSpot 虚拟机已经将方法区(永久带)移除,取而代之的就是元空间。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

直接内存

  • 直接内存不是虚拟机运行时数据区的一部分,也不是《Java 虚拟机规范》中定义的内存区域。
  • 直接内存是在 Java 堆外的、直接向系统申请的内存空间。
  • 来源于 NIO ,通过存在堆中的 DirectByteBuffer 操作 Native 内存。
  • 通常,访问直接内存的速度会优于 Java 堆,即读写性能高。因此处于性能考虑,读写频繁的场合可能会考虑使用直接内存。Java 的 NIO 库允许 Java 程序使用直接内存,用于数据缓冲区。

# 主内存和本地内存的关系

JMM 规定的主内存和本地内存的关系

  • 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中所有的变量内容都是主内存的拷贝。
  • 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,再同步到主内存中。即在修改变量之前,要先执行第一步规定的,把数据拷贝到工作内存中再修改,再同步。
  • 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成。

总结:所有的共享变量,是存在于主内存中的,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,正是由于有交换的过程,并且这个交换不是实时的,所以才导致了可见性的问题。volatile 变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为 volatile 类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。

# 对象内存布局

Java 对象保存在堆中时,由以下三部分组成:

  • 对象头(object header):包括了关于堆对象的布局、类型、GC 状态、同步状态和标识哈希码的基本信息。Java 对象和 vm 内部对象都有一个共同的对象头格式。
  • 实例数据(Instance Data):主要是存放类的数据信息,父类的信息,对象字段属性信息。
  • 对齐填充(Padding):为了字节对齐,填充的数据,不是必须的。

对象头包括两部分:Mark Word, Klass pointer

# Mark Word

用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。

Mark Word 在不同的锁状态下存储的内容不同,在 64 位 JVM 中是这么存的

锁标志位(lock):区分锁状态,11 时表示对象待 GC 回收状态,只有最后 2 位锁标识 (11) 有效。
是否偏向(biased_lock):是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
分代年龄(age):表示对象被 GC 的次数,当该次数到达阈值的时候,对象就会转移到老年代。
对象的 hashcode(hash):运行期间调用 System.identityHashCode () 来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果 31 位不够表示,在偏向锁,轻量锁,重量锁,hashcode 会被转移到 Monitor 中。
偏向锁的线程 ID(JavaThread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的 ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
epoch:偏向锁的时间戳。
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。锁记录存储的是所之前锁对象的 Mark Word 的拷贝。
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器 Monitor 的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到 Monitor 以管理等待的线程。在重量级锁定的情况下,JVM 在对象的 ptr_to_heavyweight_monitor 设置指向 Monitor 的指针。

# 数据对齐

字段内存对齐的其中一个原因,是让字段只出现在同一 CPU 的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。其实对其填充的最终目的是为了计算机高效寻址。

# 偏向锁

# 锁撤销

撤销偏向锁就是将锁对象 oop 的对象头恢复成无锁状态或者膨胀成轻量级锁状态,执行撤销动作的前提是锁对象 oop 的对象头处于偏向锁状态。
锁撤销过程
尝试获取某个偏向锁,如果该偏向锁被某个线程占用了,但是没有关联的 BasicObjectLock ,即实际占用该偏向锁的方法已经退出了,则会将其恢复成无锁状态,然后膨胀成轻量级锁,但是在撤销一定次数后触发批量重偏向(rebasic)的情形下也可能重新获取该偏向锁。如果该偏向锁正在被某个方法所使用,即存在对应的 BasicObjectLock ,则直接将该偏向锁膨胀成轻量级锁。

在已偏向后调用 Object.hashCode () 方法时,锁定状态下,锁膨胀为重量级锁;非锁定状态下,锁会恢复为无锁状态。

# 批量重偏向

避免短时间内大量偏向锁的撤销。例如一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。当执行批量重偏向后,如果原偏向锁持有者线程不再执行同步块,则锁可以偏向于新线程。
以 class 为单位,为每个 class 维护一个偏向锁撤销计数器,每一次该 class 的对象发生偏向撤销操作时,该计数器 +1,当这个值达到重偏向阈值(默认 20 )时, JVM 就认为该 class 的偏向锁有问题,因此会进行批量重偏向。每个 class 对象会有一个对应的 epoch 字段,每个处于偏向锁状态对象的 mark word 中也有该字段,其初始值为创建该对象时,class 中的 epoch 的值。每次发生批量重偏向时,就将该值 +1,同时遍历 JVM 中所有线程的栈,找到该 class 所有正处于加锁状态的偏向锁,将其 epoch 字段改为新值。下次获得锁时,发现当前对象的 epoch 值和 class 的 epoch 不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过 CAS 操作将其 mark word 的 Thread Id 改成当前线程 Id。

# 批量锁撤销

当一个偏向锁如果撤销次数到达 40 的时候就认为这个对象设计的有问题;那么 JVM 会把这个对象所对应的类所有的对象都撤销偏向锁;并且新实例化的对象也是不可偏向的。

# 对象计算过哈希值之后还能进入偏向锁状态吗

  • 当一个对象已经计算过 identity hashcode,它就无法进入偏向锁状态;
  • 当一个对象当前正处于偏向锁状态,并且需要计算其 identity hashcode 的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;
  • 重量锁的实现中,ObjectMonitor 类里有字段可以记录非加锁状态下的 mark word ,其中可以存储 identity hash code 的值。或者简单说就是重量锁可以存下 identity hash code。

Identity hash code 是未被覆写的 java.lang.Object.hashCode ()

# 类文件结构

class 文件格式

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attribute_count 1
attribute_info attributes attributes_count

# 类加载机制

# 类的生命周期

类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。
类的生命周期

# 类加载时期

在什么情况下需要开始类加载过程的第一个阶段「加载」,《Java 虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。
类初始化时期
对于初始化阶段,《Java 虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行「初始化」(而加载、验证、准备自然需要在此之前开始):

  1. 遇到 new(用 new 实例对象),getStatic(读取一个静态字段),putstatic(设置一个静态字段),invokeStatic(调用一个类的静态方法)这四条指令字节码命令时。
  2. 使用 java.lang.reflect 包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
  3. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main () 方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用 JDK 7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  6. 当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

下面这几种情况就不会触发类初始化:

  • 通过子类调用父类的静态字段。此时父类符合情况一,而子类不符合任何情况。所以只有父类被初始化。
  • 通过数组来引用类,不会触发类的初始化。因为 new 的是数组,而不是类。
  • 调用类的静态常量不会触发类的初始化,因为静态常量在编译阶段就会被存入调用类的常量池中,不会引用到定义常量的类。

# 类加载器

在 JDK1.8 及之前的版本中中,类加载器主要有下面四种:
BootstrapClassLoader:启动类加载器,使用 C++ 实现;
ExtClassLoader:扩展类加载器,使用 Java 实现;
AppClassLoader:应用程序类加载器,加载当前应用的 classpath 的所有类;
UserDefinedClassLoader:用户自定义类加载器;

# 双亲委派

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        return c;
}

# 为什么要双亲委派

  • 避免重复加载:使用双亲委派模型也可以避免一个类被重复加载,当一个类被加载之后,因为使用的双亲委派模型,这样不会出现多个类加载器都将同一个类重复加载的情况了。
  • 安全:当使用双亲委派模型时,用户就不能伪造一些不安全的系统类了,比如 jre 里面已经提供了 String 类在启动类加载时加载,那么用户自定义再自定义一个不安全的 String 类时,按照双亲委派模型就不会再加载用户定义的那个不安全的 String 类了,这样就可以避免非安全问题的发生了。

# 破坏双亲模型

双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者们的类加载器实现方式。这个委派和加载顺序完全是可以被破坏的。
如果想自定义类加载器,就需要继承 ClassLoader ,并重写 findClass ,如果想不遵循双亲委派的类加载顺序,还需要重写 loadClass 。
JDBC 打破双亲模型
在双亲委托模型下,类加载器是由下至上的,即下层的类加载器会委托上层进行加载。但是对于 SPI 来说,有些接口是 JAVA 核心库提供的,而 Java 核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的 jar 包(厂商提供), Java 的启动类加载器是不会加载其他来源的 jar 包,这样传统的双亲委托模型就无法满足 SPI 的要求。而通过给当前线程设置上下文类加载器,就可以通过设置的上下文类加载器来实现对于接口实现类的加载。
线程上下文类加载器是从 jdk1.2 开始引入的,类 Thread 中的 getContextClassLoader () 与 setContextClassLoader (ClassLoader c1),分别用来获取和设置类加载器。

# 自动内存管理吗

# 对象何时死亡

# 引用计数算法

算法会在每一个对象上记录这个对象被引用的次数,只要有任何一个对象引用了次对象,这个对象的计数器就 +1 ,取消对这个对象的引用时,计数器就 -1 。任何一个时刻,如果该对象的计数器为 0 ,那么这个对象就是可以回收的。
缺点:1. 无法处理循环引用的情况;2. 堆内对象的每一次引用赋值和每一次引用清除,都伴随着加减法的操作,会带来一定的性能开销。

# 可达性分析算法

现代虚拟机基本都是采用可达性分析算法来判断对象是否存活,可达性算法的原理是以一系列叫做 GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点。这样通过 GC Root 串成的一条线就叫引用链),直到所有的结点都遍历完毕,如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为垃圾对象,会被 GC 回收。
可达性分析算法

# 垂死挣扎

对象可回收,就一定会被回收吗
对象的 finalize 方法给了对象一次垂死挣扎的机会,当对象不可达(可回收)时,当发生 GC 时,会先判断对象是否执行了 finalize 方法,如果未执行,则会先执行 finalize 方法,我们可以在此方法里将当前对象与 GC Roots 关联,这样执行 finalize 方法之后,GC 会再次判断对象是否可达,如果不可达,则会被回收,如果可达,则不回收!
finalize 方法只会被执行一次,如果第一次执行 finalize 方法此对象变成了可达确实不会回收,但如果对象再次被 GC,则会忽略 finalize 方法,对象会被回收!这一点切记!

# GC Roots

可以作为 GC Roots 的对象

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
  2. 方法区中类静态属性引用的对象;java 类的引用类型静态变量。
  3. 方法区中常量引用的对象;比如:字符串常量池里的引用。
  4. 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
  5. JVM 的内部引用(class 对象、异常对象 NullPointException、OutofMemoryError,系统类加载器)。(非重点)
  6. 所有被同步锁 (synchronized 关键)持有的对象。(非重点)
  7. JVM 内部的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。(非重点)
  8. JVM 实现中的「临时性」对象,跨代引用的对象(在使用分代模型回收只回收部分代的对象)。(非重点)

# 垃圾收集算法

# 分代收集理论

弱分代假说:绝大多数对象,在第一次垃圾收集时就会被回收。
强分代假说:熬过越多次收集过程的对象越难以消亡。
这两种假说给我们提供了一种设计思想就是将 java 堆划分出不同的区域,别急我们慢慢来说明一下:根据弱分代理论呢,如果有专门一块区域的绝大多数对象都是「脆弱」的,那每次回收时就只关注少量存活就好了,就可以以较少的代价回收到大量空间。那根据强理论呢,如果有专门一块区域的绝大多数对象都是「顽固」的,那虚拟机就可以以较低的频率进行垃圾回收,这就提高了内存的利用率。

跨代引用假说:跨代引用相对于同代引用来说仅占极少数。
该假说认为只会存在很少的跨代引用。因为只要经过一些次数的垃圾收集,即使还存在跨代引用,新生代会变成老年代,跨代引用也就自然消失了,所以跨代引用的数量不会多。在对新生代对象进行收集时,由于可能存在老年代对象引用了该对象,那么,需要找到这些老年代对象。根据跨代引用假说,这些跨代引用的数量不会太多,相比于对老年代进行扫描,在新生代建立一个全局数据结构,记录哪一块老年代内存会存在跨代引用,虽然维护这个数据结构,也需要少量的开销。但仍然显得更加合算。

基于这个分代假设,一般的垃圾回收器把内存分成三类:Eden (E), Suvivor (S) 和 Old (O), 其中 Eden 和 Survivor 都属于年轻代, Old 属于老年代,新对象始终分配在 Eden 里面,熬过一次垃圾回收的对象就被移动到 Survivor 区了,经过数次垃圾回收之后还活着的对象会被移到 Old 区。

收集方式
Java 堆分为新生代和老年代,针对收集对象处于哪一代,一共有以下四种收集方式。

  • 部分收集
    • 新生代收集 (Minor GC / Young GC),只收集新生代垃圾对象
    • 老年代收集 (Major GC / Old GC),只收集老年代垃圾对象,目前只有 CMS 收集器会单独收集老年代对象。需要注意的是,Major GC,目前这个说法有点混淆,有时候专指老年代收集,有时候指的是整堆收集(Full GC)。
    • 混合收集 (Mixed GC),收集来自整个新生代以及部分老年代中的垃圾对象。目前只有 G1 会有这种行为。
  • 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。

# 标记 - 清除算法


原理:分为标记和清除两个阶段:标记阶段和清除阶段。标记过程中,将活动对象的标记位设置为有效状态,表示这些对象是可达的,不会被回收。将未被标记的对象视为垃圾对象,将其所占用的内存空间释放,以便下次分配给新的对象使用。
缺点

  1. 会产生大量内存碎片,空间不连续后面有大对象时无法使用,可能会引起一次垃圾回收
  2. 标记和清除两个过程都比较耗时,效率不高

# 标记 - 复制算法


原理:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
缺点

  1. 解决了标记 - 清除算法中清除慢的问题,但是空间利用率低。
  2. 在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是如果不想浪费 50% 的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都有 100% 存活的极端情况。

# 标记 - 整理算法


原理:该算法标记阶段和 Mark-Sweep 一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
缺点:同标记复制算法,由于整理牵扯到了对象的移动,效率会变得较低。

# 并发的可达性分析

  • 白色:
    • 表示对象尚未被垃圾收集器访问过。
    • 可达性分析开始前,所有对象都是白色的,如果分析结束后,依然是白色的对象,意味着它是不可达的,将会被回收。
  • 黑色:
    • 表示对象已经被垃圾收集器访问过,而且对象的引用链已经遍历完成。
    • 黑色的对象,意味着它是可达的,不会被回收。
    • 如果被其他对象引用,不需要重新遍历一遍。
    • 黑色的对象不可能直接(不经过灰色的对象)指向某个白色的对象。
  • 灰色:
    • 表示对象已经被垃圾回收器访问过,但是对象的引用链没有遍历完成。
    • 灰色的对象在黑色的对象和白色的对象之间。
# 浮动垃圾

并发标记的过程中,若一个已经被标记成黑色或者灰色的对象,突然变成了垃圾,由于不会再对黑色标记过的对象重新扫描,所以不会被发现,那么这个对象不是白色的但是不会被清除,重新标记也不能从 GC Root 中去找到,所以成为了浮动垃圾,浮动垃圾对系统的影响不大,留给下一次 GC 进行处理即可。

# 对象丢失

对象消失需要满足的条件

  • 插入一条或多条从黑色对象到白色对象的新引用
  • 删除全部从灰色对象到该白色对象的直接或间接引用

对象消失示意图

解决方案
解决并发扫描时的对象消失问题,只需要破化以上两个条件的其中一个即可。因此产生两种解决方案:

  • 增量更新:破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
  • 原始快照:破化的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。

以上无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的,且以上两种方案在垃圾收集器中都有实际使用如:CMS 是基于增量更新来做并发标记;G1、Shenandoah 则是使用原始快照来实现的。

# 经典垃圾收集器

# CMS

Concurrent Mark Sweep,采用标记 - 清除算法,用于老年代,常与 ParNew 协同工作。优点在于并发收集与低停顿。

# 步骤

初始标记:这个过程十分快速,需要 stop the world,遍历所有的对象并且标记初始的 GC Roots。
并发标记:这个过程可以和用户线程一起并发完成,所以对于系统进程的影响较小,主要的工作为在系统线程运行的时候通过 GC Roots 对于对象进行根节点枚举的操作,标记对象是否存活,注意这里的标记也是较为迅速和简单的,因为下一步还需要重新标记。
重新标记:需要 stop the world,用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象(使用增量更新方式),时间短。
并发清理:和用户线程一起并发,清除那些没有标记的对象并且回收空间。
CMS 使用 增量更新的方式解决「对象消失」的问题。

# 缺点
  • 对 CPU 资源太敏感,虽然在并发标记阶段用户线程没有暂停,但是由于收集器占用了一部分 CPU 资源,导致程序的响应速度变慢。
  • 无法处理浮动垃圾。原因在于 CMS 是以获取最短停顿时间为目标的,自然不可能在一次垃圾处理过程中花费太多时间,只好在下一次 GC 的时候处理。有可能出现「Con-current Mode Failure」失败进而导致另一次完全「Stop The World」的 Full GC 的产生。
  • 由于 CMS 收集器是基于「标记 - 清除」算法的,前面说过这个算法会导致大量的空间碎片的产生,一旦空间碎片过多,大对象就没办法给其分配内存,那么即使内存还有剩余空间容纳这个大对象,但是却没有连续的足够大的空间放下这个对象,所以虚拟机就会触发一次 Full GC。Full GC 会进行一次碎片整理。

# G1 (Garbage First)

通过把 Java 堆分成大小相等的多个独立区域,回收时计算出每个区域回收所获得的空间以及所需时间的经验值,根据记录两个值来判断哪个区域最具有回收价值,所以叫 Garbage First(垃圾优先)。G1 收集器与之前的收集器最大的不同就在于堆内存的划分,之前的收集器只区分新生代与老年代,而 G1 收集器则是把堆内存划分成多个独立的 Region 。

# 步骤

初始标记:这个阶段是 STW (Stop the World) 的,所有应用线程会被暂停,标记出从 GC Root 开始直接可达的对象。

并发标记:从 GC Roots 开始,对堆中对象进行可达性分析,找出存活对象,耗时较长。
最终标记:标记那些在并发标记阶段发生变化的对象(使用原始快照方式),将被回收。
筛选回收:首先,对各个 Regin 的回收价值和成本进行排序,根据用户所期待的 GC 停顿时间,来指定回收计划,回收一部分 Region 。

G1 中提供了 Young GC、Mixed GC 两种垃圾回收模式,这两种垃圾回收模式,都是 Stop The World (STW) 的。

# 回收模式

Young GC
在分配一般对象(非巨型对象)时,当所有 eden region 使用达到最大阀值、并且无法申请足够内存时,会触发一次 YoungGC 。
每次 young gc 会回收所有 Eden 、以及 Survivor 区,并且将存活对象复制到 Old 区以及另一部分的 Survivor 区。

Mixed GC
当越来越多的对象晋升到老年代 old region 时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集,即 mixed gc ,该算法并不是一个 old gc ,除了回收整个 young region ,还会回收一部分的 old region 。

巨型对象
G1 垃圾收集器还增加了一种新的内存区域,叫做 Humongous 内存区域,主要用于存储大对象,如果超过 1.5 个 region , 就放到 Humongous 区域。

# 特点

G1 的一个显著特点他能够让用户设置应用的暂停时间,G1 回收的第 4 步,它是「选择一些内存块」,而不是整代内存来回收,这是 G1 跟其它 GC 非常不同的一点,其它 GC 每次回收都会回收整个 Generation 的内存 (Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而 G1 每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。