深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) 周志明 著

有疑问的地方标记※

第一部分 走进Java

第1章 走近Java

第二部分 自动内存管理

第2章 Java内存区域与内存溢出异常

2.1 概述

C、C++程序开发需要内存管理,内存的分配及回收。
Java开发只需要内存分配,内存回收是jvm的工作
一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那排查错误、修正问题将会成为一项异常艰难的工作。

2.2 运行时数据区域

在这里插入图片描述

2.2.1 程序计数器

线程私有
记录执行到哪一行,为了线程切换后能恢复到正确的执行位置
如果是本地方法则计数器值为空
不会出现OOM

2.2.2 Java虚拟机栈

线程私有
调用一个方法时,jvm创建一个栈帧(Stack Frame)存储局部变量表、操作数栈、动态连接、方法出口等信息
方法被调用直至执行完毕,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
如果栈深度大于虚拟机所允许的深度StackOverflowError
如果栈容量可以动态扩展无法申请到足够的内存会抛出OutOfMemoryError

2.2.3 本地方法栈

线程私有
与虚拟机栈作用类似,区别是执行本地方法

2.2.4 Java堆

线程共享
虚拟机启动时创建,存放对象实例
垃圾收集器管理的内存区域,也被称作“GC堆”
新生代:Eden空间、From Survivor空间、To Survivor空间
老年代

通过参数-Xmx和-Xms设定
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常

2.2.5 方法区

线程共享,别名叫作“非堆”(Non-Heap)
存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

2.2.6 运行时常量池

是方法区的一部分。
存放Class文件的常量池表
一般都是编译期,除非动态编译
方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常

之前叫永久代,有-XX:MaxPermSize的上限,且有默认大小,能够像堆内存一样进行分代回收,但永久代一般不需要这些特点(除非动态编译,需要GC)
所以JDK 7的HotSpot,把原本放在永久代的字符串常量池、静态变量等移到元空间
到了jdk8完全废弃了永久代的概念,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。

2.2.7 直接内存

不属于虚拟机运行时数据区的一部分
※NIO,基于通道(Channel)与缓冲区 (Buffer)的I/O方式,使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。避免了在Java堆和Native堆中来回复制数据。
可能导致OutOfMemoryError异常出现

2.3 HotSpot虚拟机对象探秘

2.3.1 对象的创建
  1. 类加载检查:
    能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。
    如果没有,那必须先执行相应的类加载过程
  2. 分配内存:
    假设堆内存是规整的,使用的内存放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,分配内存就是把指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump The Pointer)
    但如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,就须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)
    选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。因此,当使用Serial、ParNew等带压缩 整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;而当使用CMS这种基于清除 (Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。
    对象创建在虚拟机中是非常频繁的行为,修改一个指针所指向的位置,在并发情况下非线程安全,可能出现正在给对象 A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。两种解决方案:一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定
    初始化内存空间:
    将分配到的内存空间(但不包括对象头)都初始化为零值,如果用TLAB,这一项工作也可以提前至TLAB分配时顺便进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值
    初始化对象头:
    是哪个类的实例、如何才能找到类的元数据信息、哈希码(实际上调用Object::hashCode()方法时才计算)、GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。
    Class文件中的init()方法
2.3.2 对象的内存布局

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:
对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

  1. 对象头
    HotSpot虚拟机对象的对象头部分包括两类信息。
    第一类(Mark Word):
    Mark Word在32位和64位的虚拟机(未开启压缩指针)大小分别为32bit和64bit
    用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
    在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,1个比特固定为0,在其他状态(轻量级锁定、重量级锁定、GC标 记、可偏向)下对象的存储内容如表2-1所示
    在这里插入图片描述
    第二类:
    类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。
    类型指针不一定存在
    如果是数组的话,会存数组的长度,否则就没
  2. 实例数据:
    类的属性,字段
    HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)
    -XX:FieldsAllocationStyle参数=1时引用类型在基本数据类型后面,=0时引用在原始类型前面
    相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
    如果HotSpot虚拟机的 +XX:CompactFields参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。
  3. 对齐填充:
    就是实例占用的内存空间必须是8字节的整数倍,不足就补齐,主要就是实例数据部分
    思考
    jdk1.8在64位虚拟机默认开启指针压缩。原始64bit,压缩后32bit
    那么(64位机器)一个对象,没有属性,实例大小=对象头=Mark Word+类型指针(默认压缩)=8+4=12字节,最后对齐填充为16字节
    有一个(long/double)属性=12+8=20字节,最后对齐填充为24字节
    有一个(int)属性=12+4=16字节,不需要对齐
    有两个(int)属性=12+8=20字节,最后对齐填充为24字节
    有一个引用类型(引用类型存的也是指针,同类型指针大小)=12+4=16字节
2.3.3 对象的访问定位

通过栈上的reference,定位堆上的具体对象。
访问方式主要有两种,句柄和直接指针:

  1. 使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference本身不需要被修改
    在这里插入图片描述
  2. 直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销(HotSpot就是使用直接指针)
    在这里插入图片描述

2.4 实战:OutOfMemoryError异常

《Java虚拟机规范》规定,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(下文称OOM)异常的可能

2.4.1 Java堆溢出

年轻代老年代全满了,并且fullGC清理不掉,jvm就挂了

java.lang.OutOfMemoryError: Java heap space

设置堆内存最大值与最小值为20M
-Xms20m -Xmx20m
在出现内存溢出异常的时候Dump出当前的内存堆转储快照会把整个堆内存dump下来,不必担心性能,因为这时jvm已经挂了
-XX:+HeapDumpOnOutOfMemoryError

package com.baomidou.mybatisplus.samples.assembly;

import java.util.ArrayList;
import java.util.List;

/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOM {

    public static void main(String[] args) {
        List list = new ArrayList();
        while (true) {
            list.add(new Object());
        }
    }
}

结果

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid46776.hprof ...
Heap dump file created [28353060 bytes in 0.058 secs]

生成堆内存快走文件
在这里插入图片描述
分析快照文件结果
使用jdk自带工具jvisualvm.exe(工具在jdk\bin目录下)分析
在这里插入图片描述
在这里插入图片描述
点击线程
在这里插入图片描述
可以定位到代码具体位置
在这里插入图片描述
点击类,可以看到实例个数
在这里插入图片描述
堆内存溢出:分为内存泄漏(Memory Leak)、内存溢出(Memory Overflow)

  1. 内存溢出,对象必须存活。
    解决方案:设置堆的最大值-Xmx即可
  2. 内存泄漏:对象非必须存活的,却没有被GC
    ※如何查看引用链解决方案:查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎 样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息 以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
2.4.2 虚拟机栈和本地方法栈溢出

HotSpot虚拟机中不区分虚拟机栈和本地方法栈
-Xoss参数设置本地方法栈大小(实际没有效果)
栈容量由-Xss参数来设定
《Java虚拟机规范》中描述了两种异常:

  1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

  2. 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。

《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,HotSpot虚拟机的选择是不支持扩展,所以创建线程申请内存时,内存不足就会OutOfMemoryError。线程运行时不会出现OutOfMemoryError,只会因为无法容纳新的栈帧出现StackOverflowError
总结
不要一直创建线程,使用线程池,可以避免OutOfMemoryError
尽量避免使用递归,递归过深就会StackOverflowError,如果避免不了就设置-Xss,但设置后线程数就会减少,因为每个线程占用的内存多了

2.4.3 方法区和运行时常量池溢出

方法区的主要职责是用于存放类型的相关信息,如类 名、访问修饰符、常量池、字段描述、方法描述等

  1. 方法区出现溢出的情况:动态产生类
    反射时的 GeneratedConstructorAccessor和动态代理
    运行于Java虚拟机上的动态语言(例如Groovy等)
  2. 为了预防实际开发中,大量动态产生类造成OOM,HotSpot提供了一些参数作为元空间的防御措施
    -XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存 大小。
    -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集 进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放 了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该
    值。
    -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可 减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最 大的元空间剩余容量的百分比。
  3. jdk6字符串常量池在永久代,jdk7以上字符串常量池在堆
/**
 * JDK 6中运行,会得到两个false
 * 在JDK 6中 ,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储
 * intern()方法返回是永久代里面这个字符串实例的引用
 * StringBuilder创建的字符串对象实例在Java堆上,所以必然不可能是同一个引用,结果将返回false。
 * JDK8:true、false
 * JDK8常量池在元空间,但字符串常量池在堆
 * 就不需要再拷贝字符串的实例从堆到字符串常量池,因为字符串常量池在堆里面
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        String str1 = new StringBuilder("计算机").append("软件").toString();
        System.out.println(str1.intern() == str1);
        //“java”字符是加载sun.misc.Version这个类的时候已经进入常量池
        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);
    }
}
2.4.4 本机直接内存溢出

-XX:MaxDirectMemorySize指定
默认与Java堆最大值(由-Xmx指定)一致
jdk8总结

  1. 堆溢出
    -Xms20m -Xmx20m
    -XX:+HeapDumpOnOutOfMemoryError发生堆溢出时保存快照
    重启,并调大最大值
  2. 栈溢出
    不要一直创建线程,使用线程池,可以避免OutOfMemoryError
    尽量避免使用递归,递归过深就会StackOverflowError,如果避免不了就设置-Xss,但设置后线程数就会减少,因为每个线程占用的内存多了
  3. 方法区(元空间)溢出
    大量使用动态类才会发生,一般不考虑
    -XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存 大小
  4. 本机直接内存溢出
    -XX:MaxDirectMemorySize
    默认与Java堆最大值(由-Xmx指定)一致

第3章 垃圾收集器与内存分配策略

3.1 概述

垃圾收集(Garbage Collection,GC)
·哪些内存需要回收?
·什么时候回收?
·如何回收?

3.2 对象已死?

在堆里面存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对象)了。

3.2.1 引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是垃圾。
优点
原理简单,判定效率也很高
缺点
占用了一些额外的内存空间来进行计数
循环引用时无法回收

3.2.2 可达性分析算法

在这里插入图片描述

3.2.3 再谈引用

在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软 引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强 度依次逐渐减弱。

  1. 强引用不会被GC。
Object obj=new Object()
  1. 软引用,在OOM之前GC
Object obj=new Object();
SoftReference reference = new SoftReference(obj);
//通过get()方法获取引用的指针,如果被回收返回的就是null
Object o = reference.get();
  1. 弱引用,在GC时就被回收
Object obj=new Object();
WeakReference reference = new WeakReference(obj);
//通过get()方法获取引用的指针,如果被回收返回的就是null
Object o = reference.get();
  1. 虚引用也称为“幽灵引用”或者“幻影引用”,创建时就被GC
    唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        PhantomReference reference = getReference();
        //执行gc
        System.gc();
        //get()方法返回null
        System.out.println(reference.get());
        //被回收true,如果不执行gc返回false
        System.out.println(reference.isEnqueued());
        //被排队false,如果不执行gc返回true
        System.out.println(reference.enqueue());
    }

    static PhantomReference getReference() {
        Object obj = new Object();
        ReferenceQueue queue = new ReferenceQueue();
        PhantomReference reference = new PhantomReference(obj, queue);
        return reference;
    }
}
3.2.4 生存还是死亡?

经过2次标记

  1. 可达性分析后发现没有与GC Roots相连接的引用链,将会被第一次标记
    对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。
    如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize() 方法。
  2. 稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果在finalize()方法中拯救自己了,就会被移出“即将回收”的集合,否则就回收
/**
 * 仅仅为演示使用,实际不要这么做
 * 此代码演示了两点:
 * 1.对象可以在被GC时自我拯救。
 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 */
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("yes, i am still alive :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
        // 下面这段代码与上面的完全相同,但是这次自救却失败了
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}

简单来讲,判断对象不可达进行第一次标记,如果覆盖了finalize()方法就放入F-Queue队列,执行finalize()时,如果被拯救变成可达,就移出。否则就标记为垃圾

3.2.5 回收方法区

个人认为没必要
性价比低
回收不可达的常量,不再被使用的类
不再使用的类要三个条件:

  1. 类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  2. 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP的重加载等,否则通常是很难达成的。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载 器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

3.3 垃圾收集算法

3.3.1 分代收集理论

建立在两个分代假说之上:

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
  3. 跨代引用: 假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整 个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。遍历整个老年代所有对象 的方案虽然理论上可行,但无疑会为内存回收带来很大的性能负担。为了解决这个问题,就需要对分代收集理论添加第三条经验法则:
    跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数
    ※跨代引用为何要扫描整个老年代?下文也提到年轻代在年龄增长之后晋升到老年代中,跨代引用也随即被消除了,还有必要考虑跨代?
    名词
    ■部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
    ■新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
    ■老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单 独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
    ■混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 集器会有这种行为。
    ■整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
3.3.2 标记-清除算法

“标记-清除”(Mark-Sweep)算法
首先标记出需要回收的对象,后回收掉被标记对象,也可以反过来,标记存活对象,统一回收未标记对象。
缺点有两个
第一个:执行效率不稳定。当堆中包含大量需要GC的对象,须大量标记和清除,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
第二个:内存空间的碎片化问题。标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
在这里插入图片描述

3.3.3 标记-复制算法

标记复制(半区复制Semispace Copying)算法
为解决标记-清除算法面对大量可回收对象时执行效率低的问题
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:
当多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
实现简单,运行高效
缺点:
将可用内存缩小为了原来的一半,空间浪费。
如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销
在这里插入图片描述

3.3.4 标记-整理算法

标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
※存活的对象都向内存空间一端移动,可回收对象占着位置,能移动吗?,有可能移动就是覆盖
缺点:
如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新 所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行“Stop The World”
在这里插入图片描述

3.4 HotSpot的算法细节实现

3.4.1 根节点枚举

3.4.2 安全点
3.4.3 安全区域
3.4.4 记忆集与卡表
3.4.5 写屏障
3.4.6 并发的可达性分析

3.5 经典垃圾收集器

在这里插入图片描述
图3-6展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用
中间横线上面是年轻代垃圾收集器,下面是老年代
横线上的比较特殊,但也遵循分代理论

3.5.1 Serial收集器
  1. 是最基础、历史最悠久的收集器
  2. 是HotSpot虚拟机新生代收集器的唯一选择。
  3. 单线程
  4. 主要意义是供客户端模式下的HotSpot虚拟机使用
    在这里插入图片描述
3.5.2 ParNew收集器
  1. Serial收集器的多线程版本
    在这里插入图片描述
  2. 只有它能与CMS收集器配合工作
3.5.3 Parallel Scavenge收集器(jdk8默认)

标记-复制

  1. 和ParNew非常相似
  2. CMS等收集器的关注点是尽可能 地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐 量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值, 即:
    在这里插入图片描述
    如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%
    Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
    -XX:MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的 时间不超过用户设定值。
    -XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的 比率,相当于吞吐量的倒数。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5% (即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。
  3. -XX:+UseAdaptiveSizePolicy这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)
3.5.4 Serial Old收集器

基于标记-整理

  1. 是Serial收集器的老年代版本
  2. 主要意义也是供客户端模式下的HotSpot虚拟机使用
    在这里插入图片描述
3.5.5 Parallel Old收集器

基于标记-整理

  1. 是Parallel Scavenge收集器的老年代版本
    在这里插入图片描述
3.5.6 CMS收集器
  1. 基于标记-清除
  2. 获取最短回收停顿时间为目标的收集器
  3. 整个过程分为四个步骤
    1)初始标记(CMS initial mark) :标记GC Roots直接关联到的对象
    2)并发标记(CMS concurrent mark):标记GC Roots关联到的所有对象
    3)重新标记(CMS remark) :为修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
    4)并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象
    在这里插入图片描述
    缺点:
  4. CMS默认启动的回收线程数是(处理器核心数量 +3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的 处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时, CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能 力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。
  5. CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。在CMS的并发标记和并发清理阶 段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分 垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集 时再清理掉。这一部分垃圾就称为“浮动垃圾”。
  6. CMS是一款基于“标记-清除”算法实现的收集器,会有大量空间碎片产生
3.5.7 Garbage First收集器(简称G1)

整体来看基于标记-整理,局部(两个Region 之间)基于标记-复制

  1. 把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

  2. Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设 定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象, 将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代 的一部分来进行看待,如图3-12所示
    在这里插入图片描述

  3. 在后台维护一 个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默 认值是200毫秒),优先处理回收价值收益最大的那些Region

  4. 如果我们不去计算用户线程运行过程中的动作(如使用写屏障维护记忆集的操作),G1收集器的 运作过程大致可划分为以下四个步骤:
    ·初始标记(Initial Marking):只标记GC Roots直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
    ·并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。
    ·最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
    ·筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的
在这里插入图片描述

3.6 低延迟垃圾收集器

  1. HotSpot的垃圾收集器从Serial发展到CMS再到G1
  2. 衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟 (Latency)
  3. 一款优秀的收集器通 常最多可以同时达成其中的两项。
  4. 一般注重低延迟。多占用内存,空间换时间
  5. 图3-14中浅色阶段表示必须挂起用户线程,深色表示收集器线程与用户线程是并发工作的。由图 3-14可见,在CMS和G1之前的全部收集器,其工作的所有步骤都会产生“Stop The World”式的停顿; CMS和G1分别使用增量更新和原始快照(见3.4.6节)技术,实现了标记阶段的并发,不会因管理的堆 内存变大,要标记的对象变多而导致停顿时间随之增长。但是对于标记阶段之后的处理,仍未得到妥 善解决。CMS使用标记-清除算法,虽然避免了整理阶段收集器带来的停顿,但是清除算法不论如何优 化改进,在设计原理上避免不了空间碎片的产生,随着空间碎片不断淤积最终依然逃不过“Stop The World”的命运。G1虽然可以按更小的粒度进行回收,从而抑制整理阶段出现时间过长的停顿,但毕竟 也还是要暂停的
    在这里插入图片描述
    Shenandoah和ZGC,几乎整个工作过程全 部都是并发的,只有初始标记、最终标记这些阶段有短暂的停顿,这部分停顿的时间基本上是固定 的,与堆的容量、堆中对象的数量没有正比例关系
3.6.1 Shenandoah收集器
3.6.2 ZGC收集器
  1. ZGC收集器是一款基于Region内存布局的,(暂时) 不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低 延迟为首要目标的一款垃圾收集器。
  2. 与G1相似,但至少有三个不同
    1)支持并发的整理算法,G1的回收阶段是可以多线程并行的,但却不能与用户线程并发
    2)Shenandoah(目前)是默认不使用分代收集的,没有专门的新生代Region或者老年代Region的存在,没有实现分代
    3)摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题
    在这里插入图片描述

3.7 选择合适的垃圾收集器

第三部分 虚拟机执行子系统

第四部分 程序编译与代码优化

第五部分 高效并发

附录

总结

书本提到过的算法

  1. 对象创建时在堆上分配内存:空闲列表(Free List),指针碰撞(Bump The Pointer)
  2. 判断对象是否可回收:引用计数算法(Reference Counting),可达性分析算法(Reachability Analysis)
  3. 垃圾收集算法:标记清除算法,标记复制算法,标记整理算法
    会Stop The World

除了 CMS收集器,其他都不存在只针对老年代的收集。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值