【4_JVM虚拟机】

《深入理解编程之美》

Java文件是如何被运行的

在这里插入图片描述

Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释,在运行 三个步骤。

1、Java 程序 先经过JDK中的javac 编译,生成字节码.class 文件;

2、再通过类加载器把字节码文件加载进JVM虚拟机内存,然后通过解释器逐行解释执行;

3、最后转化成机器可执行的二进制机器码。

JVM架构

  • JVM架构图分析

JVM被分为三个主要的子系统:

(1)类加载器子系统

(2)运行时数据区

(3)执行引擎

在这里插入图片描述

一、类加载器子系统

一个类的完整生命周期:

在这里插入图片描述

类加载:当需要使用某个类时,虚拟机将会加载它的字节码.class文件,并创建对应的class对象,将class文件加载到虚拟机的内存中。

1、加载

类加载过程的第一步,主要完成下面 3 件事情:

  1. 从 classpath 路径,通过 全类名 获取定义此类的 二进制字节流(.class 的二进制文件)
  2. 将字节流所代表的 静态存储结构 转换为 方法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

获取Class对象的三种方式

1、Class.forName(“全类名”)
2、类名.class
3、对象.getClass()
4、类加载器 ClassLoader.loadClass(“全类名”);

通过类加载器获取 Class 对象不会进行初始化,意味着不进行 初始化等一些列步骤,静态块和静态对象不会得到执行

类加载器&双亲委派机制

通过 启动类加载器 (BootStrap class Loader)、扩展类加载器(Extension class Loader)和应用程序类加载器(Application class Loader) 这三种类加载器帮助完成类的加载。

1、BootstrapClassLoader(启动类加载器)

​ 最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar包 和类 或者被 -Xbootclasspath参数指定的路径中的所有类。这个加载器会被赋予最高优先级。

2、ExtensionClassLoader(扩展类加载器)

​ 主要负责加载目录 %JRE_HOME%/lib/ext 目录下的 jar包 和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。

3、AppClassLoader(应用程序类加载器)

​ 面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。

在必要时,我们还可以自定义类加载器。

上述的类加载器会遵循 委托层次算法(Delegation Hierarchy Algorithm)加载类文件。

过程:

在这里插入图片描述

启动类加载器,由C++实现,没有父类。

拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null

应用类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader

自定义类加载器,父类加载器肯定为AppClassLoader。

过程:

1、类加载器 收到类加载的请求

2、将这个请求向上委托给父类加载器去执行,一 直向上委托,直到启动类加载器

3、启动加载器检查是否能够加载当前这个类,能加载就结束, 使用当前的加载器;否则, 抛出异常,通知子加载器进行加载

4、重复步骤3

都找不到,报Class Not Found 异常

双亲委派模式优势:

1、Java类随着它的类加载器一起具备了 一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

2、其次是考虑到安全因素,起隔离的作用避免了我们的代码影响了JDK的代码。保证了 Java 的核心 API 不被篡改。

沙箱安全机制

Java安全模型的核心就是Java沙箱(sandbox) 。

沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。

在Java中将执行程序分成 本地代码远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。

2、连接

校验

字节码校验器会校验生成的字节码是否正确,如果校验失败,我们会得到校验错误。

主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

准备

正式为 类变量分配内存 ,并设置类变量(static修饰)初始值。

初始值"通常情况下"是数据类型默认的零值(如 0、0L、null、false 等)。比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是 111(初始化阶段才会赋值)。

特殊情况:比如给 value 变量加上了 final 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111

解析

虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。

3、初始化

执行初始化方法 <clinit> ()方法。这里所有的静态变量会被赋初始值,并且静态块将被执行。

执行顺序:

静态代码块(类加载时的初始化阶段) --> 非静态代码块(new创建对象实例前) --> 构造方法(new创建对象实例)

说明: <clinit> ()方法是编译之后自动生成的。对于<clinit> () 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 <clinit> () 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个进程阻塞,并且这种阻塞很难被发现。

4、使用

5、卸载

卸载类 即该类的 Class 对象被 GC。

卸载类需要满足 3 个要求:

  1. 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  2. 该类没有在其他任何地方被引用
  3. 该类的类加载器的实例已被 GC

只要想通一点就好了,jdk 自带的 BootstrapClassLoader, ExtClassLoader, AppClassLoader 负责加载 jdk 提供的类,所以它们(类加载器的实例)肯定不会被回收。而我们自定义的类加载器的实例是可以被回收的,所以使用我们自定义加载器加载的类是可以被卸载掉的。

HotSpot 虚拟机对象探秘

HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程。

美团:类里面成员为什么设置为static属性,从而引出类加载知识,然后又引出new一个对象的背后发生了什么对于一个非static字段,它的加载是如何一个过程呢?

对象的创建

未整理

在这里插入图片描述

Java 对象的创建过程。

在这里插入图片描述

1、类加载检查

虚拟机遇到一条 new 指令时,首先检查能否在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

2、分配内存

类加载检查通过后,接下来虚拟机将为新生对象分配内存。(在堆内存中划分一块确定大小的内存)

分配方式有 “指针碰撞”“空闲列表” 两种。选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

压缩整理功能:GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩")。

注:复制算法内存是规整的。

在这里插入图片描述

内存分配并发问题(补充内容,需要掌握)

在创建对象的时候还有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
3、初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4、设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

5、执行 init 方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

对象的内存布局

在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头实例数据对齐填充

1、Hotspot 虚拟机的对象头包括两部分信息:

​ 第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),

​ 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例

2、实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

3、对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位

建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有① 使用句柄② 直接指针两种:

1、句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了 对象实例数据 与类型数据 各自的具体地址信息;

在这里插入图片描述

2、直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址

在这里插入图片描述

这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。

使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

二、运行时数据区

在这里插入图片描述

成员变量:随着对象的创建而存在,随着对象的消失而消失。所以存在堆中

局部变量:随着方法的调用而存在,随着方法的调用完毕而消失 。所以存在栈中

  1. JDK1.7 之前 运行时常量池 逻辑包含 字符串常量池 存放在 方法区
  2. JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代 。
  3. JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 字符串常量池 在堆中,运行时常量池在元空间中
  4. 常量池、字符串常量池、运行时常量池区别 只是文中没有考虑JDK版本问题

在这里插入图片描述

在这里插入图片描述

1、方法区

方法区 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

方法区也被称为永久代。永久代是 HotSpot 虚拟机 的概念,方法区是 Java 虚拟机规范中的定义。

hotspot 虚拟机对方法区的实现为永久代。

JDK 1.8 之前:方法区 ====> JDK 1.8 之后:元空间(元空间使用的是直接内存)

本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

JDK 1.8 之前:

在这里插入图片描述

JDK 1.8 :

在这里插入图片描述

常用参数
  • JDK 1.8 之前方法区(永久代)通过下面这些参数来调节 方法区大小
-XX:PermSize=N      //方法区 (永久代) 初始大小
-XX:MaxPermSize=N   //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen

相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。

  • JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。通过下面这些参数来调节 元空间大小:
-XX:MetaspaceSize=N     //初始化元空间大小,控制发生GC
-XX:MaxMetaspaceSize=N  //限制元空间大小上限,防止占用过多物理内存。

与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的 系统内存。

为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace)

下图来自《深入理解 Java 虚拟机》第 3 版 2.2.5

在这里插入图片描述

  1. 整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

    当元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace

你可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

  1. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
  2. 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。

移除的原因可以大致了解一下:融合HotSpot JVM和JRockit VM而做出的改变,因为JRockit是没有永久代的,不过这也间接性地解决了永久代的OOM问题。

3、栈区

  • 栈:先进后出,后进先出 (桶)

  • 栈内存,主管程序的运行,生命周期和线程同步; 线程结束,栈内存也就是释放。对于栈来说,不存在垃圾回收问题 ,一旦线程结束,栈就Over!

  • 栈内存中存储:

    ​ 1、8种基本类型的变量(boolean、byte、char、short、int、float、long、double)

    ​ 2、对象的引用变量(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)

    ​ 3、实例方法

  • 栈帧:

栈中的数据都是以栈帧的格式存在,它是一个关于方法和运行期数据的数据集。

说白了在JVM中叫栈帧,放到Java中其实就是方法 [ test()、main()就相当于两个栈帧 ]

而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。

栈底部 子帧指向上一个栈帧的方法,上一个栈帧的父帧指向栈底部方法

在这里插入图片描述

Java 虚拟机栈会出现两种错误:StackOverFlowError 栈溢出错误OutOfMemoryError 内存溢出错误

  • StackOverFlowError 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError Java 虚拟机栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

扩展:那么方法/函数如何调用?

Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。

Java 方法有两种返回方式:

  1. return 语句。
  2. 抛出异常。

不管哪种返回方式都会导致栈帧被弹出。

4、PC寄存器

程序计数器主要有两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

注意,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

5、本地方法栈

本地方法栈保存本地方法信息。对每一个线程,将创建一个单独的本地方法栈。

比如Thread类开启线程的start()方法,里面的start0方法带有一个native关键字修饰,而且不存在方法体,这种用native修饰的方法就是本地方法,这是使用C来实现的,一般这些方法都会放到一个叫做本地方法栈的区域。

和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

虚拟机栈和本地方法栈为什么是线程私有的?
  • 虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

2、堆区

堆内存中存放的是新创建的对象,垃圾收集就是收集这些new关键字创建的对象然后交给GC算法进行回收。

JDK 1.8 之前:

JVM内存会划分为堆内存和非堆内存,堆内存中也会划分为年轻代老年代,而非堆内存则为永久代

  1. 年轻代(Young Generation) 。年轻代又会分为EdenSurvivor区。Survivor也会分为FromPlaceToPlace
  2. 老年代(Old Generation)
  3. 永久代(Permanent Generation)

Eden,FromPlace和ToPlace的默认占比为 8:1:1

可以通过一个 -XX:+UsePSAdaptiveSurvivorSizePolicy 参数来根据生成对象的速率动态调整。

在这里插入图片描述

JDK 1.8 之后:

JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。

在这里插入图片描述

三、执行引擎

分配给运行时数据区的字节码将由执行引擎执行。执行引擎读取字节码并逐段执行。

1、解释器

解释器能快速的解释字节码,但执行却很慢。 解释器的缺点就是,当一个方法被调用多次,每次都需要重新解释。

2、编译器

JIT编译器消除了解释器的缺点。执行引擎利用解释器转换字节码,但如果是重复的代码则使用JIT编译器将全部字节码编译成本机代码。本机代码将直接用于重复的方法调用,这提高了系统的性能。

a. 中间代码生成器 – 生成中间代码

b. 代码优化器 – 负责优化上面生成的中间代码

c. 目标代码生成器 – 负责生成机器代码或本机代码

d. 探测器(Profiler) – 一个特殊的组件,负责寻找被多次调用的方法。

3、垃圾回收器

收集并删除未引用的对象。可以通过调用**System.gc()来触发垃圾回收,但并不保证会确实进行垃圾回收。JVM的垃圾回收只收集哪些由new关键字创建的对象**。所以,如果不是用new创建的对象,你可以使用**finalize函数**来执行清理。

GC垃圾回收机制

在这里插入图片描述

1、JVM 内存分配与回收

轻GC和重GC分别在什么时候发生?

问新生代和老年代的区别也可以这么答。(先堆划分为…

**1、**当我们new一个对象后,一般会先放到年轻代的Eden区域。

  • 例外

大对象直接进入老年代。[大对象就是需要大量连续内存空间的对象(比如:字符串、数组)]

目的是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

分配担保机制:就是当在新生代无法分配内存的时候,把新生代的对象转移到老生代,然后把新对象放入腾空的新生代。(标记-复制算法)

**2、**当Eden空间满了之后,会触发 年轻代的GC(Minor GC)操作,存活下来的对象移动到Survivor0区或者Survivor1区。并且对象的年龄还会加 1(Eden 区–>Survivor 区后对象的初始年龄增加 1)。

经过这次 GC 后,Eden 区和"From"区已经被清空。这个时候,“From"和"To"会交换他们的角色,也就是新的"To"就是上次 GC 前的“From”,新的"From"就是上次 GC 前的"To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的。

**3、**经过多次的 Minor GC后,当它的年龄增加到一定程度,就会被晋升到老年代中。老年代是存储长期存活的对象的。

  • 例外:

有可能当次Minor GC后,存活下来的对象移动到From,但From区域空间不够用,即使放不下的对象还达不到进入老年代的条件,也会提前进入老年代。

  • 进入老年区的条件:

当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。

对象晋升到老年代的年龄阈值,通过参数 -XX:MaxTenuringThreshold 来设置。

其实也不一定是要满足-XX:MaxTenuringThreshold 为15 时才会移动到老年代。

Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值。

可以举个例子:

如对象年龄5的占30%,年龄6的占36%,年龄7的占34%,加入某个年龄段(如例子中的年龄6)后,总占用超过Survivor空间 TargetSurvivorRatio的时候,从该年龄段开始及大于的年龄对象就要进入老年代(即例子中的年龄6对象,就是年龄6和年龄7晋升到老年代),这时候无需等到MaxTenuringThreshold中要求的15。

-XX:MaxTenuringThreshold  //晋升年龄最大阈值,默认15。

-XX:TargetSurvivorRatio  //设定survivor区的目标使用率。默认50,即survivor区对象目标使用率为50%。

**4、**当老年区占满时就会触发 重GC (Full GC),期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。

**5、**当老年区执行了 Full GC 之后仍然无法进行对象保存的操作,就会产生OOM,

这时候就是虚拟机中的堆内存不足,原因可能会是堆内存设置的大小过小,这个可以通过参数-Xms、-Xmx来调整。也可能是代码中创建的对象大且多,而且它们一直在被引用从而长时间垃圾收集无法收集它们。

在这里插入图片描述

Minor GC和Full GC的区别:

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多的存活率不高,所以Minor GC非常频繁,一般回收速度也比较快。
  • 老年代GC(Major GC / Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

参数调优

更多

JVM三大性能调优参数:-Xms –Xmx –Xss

在实际应用中,可以直接将堆的初始值与堆的最大可用值相等,这样可以减少程序运行时垃圾回收的次数,从而提高效率。

-Xms        //JAVA堆内存的初始,默认为物理内存的1/64
            //一般最好和-Xmx一样大,避免在程序运行过程中动态的申请调整堆内存。
-Xmx        //JAVA堆的最大值,默认为物理内存的1/4
-Xss	    //每个线程的堆栈大小
    
-Xmn        //JAVA堆年轻区大小
-XX:+PrintGCDetails
-XX:TargetSurvivorRatio=60
-XX:+PrintTenuringDistribution
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:MaxTenuringThreshold=3
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
//GC日志

-XX:+PrintGC	 	 	
/*
输出形式:
[GC 118250K->113543K(130112K), 0.0094143 secs]
[Full GC 121376K->10414K(130112K), 0.0650971 secs]
*/
    
-XX:+PrintGCDetails	
/*    
输出形式:
[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
[GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]
*/

堆溢出的解决方案:
java.lang.OutOfMemoryError:Java heap space 堆内存溢出

解决办法:
设置堆内存大小 :
-Xms1m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError(打印内存溢出错误信息)

在实际开发中,可以在Tomcat的catalina.sh文件中设置JVM的堆内存大小
JAVA_OPTS=“-Server -Xms800m -Xmx800m -XX:PermSize=256m -XX:MaxPermSize=512m -XX:MaxNewSize=512m”

栈溢出:产生于无限递归调用,循环遍历是不会的,但是循环方法里面产生递归调用,也会发生栈溢出。

解决办法:设置线程最大调用深度
设置 -Xss的大小。例如-Xss5m

2 、对象已经死亡?

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。

在这里插入图片描述

引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;

当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。

public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;

    }
}
/*
最后面两句将objA 和objB 赋值为null,也就是说objA和objB指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数都不为0,那么垃圾收集器就永远不会回收它们。

可达性分析算法

这个算法的基本思想就是通过一系列的 “GC Roots” 对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。(见下)

在这里插入图片描述

可作为 GC Roots 的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

不可达的对象并非 “非死不可”

更详细见

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,

要真正宣告一个对象死亡,至少要经历两次标记过程

1、可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。

2、 如果这个对象被判定为有必要执行finalize()方法,那么此对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

JVM之判断对象是否存活(引用计数算法、可达性分析算法,最终判定)

finalize()方法最终判定对象是否存活:

再谈引用(强 软 弱 虚)

参考1参考2参考3

无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。

JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。

JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)

1.强引用(StrongReference)

强引用:指创建一个对象并把这个对象赋给一个引用变量。

//强引用声明格式
//object是对 java对象new Object() 的强引用
Object object = new Object();
String str="abc";

只要某个对象与强引用关联,那么JVM在内存不足的情况下,宁愿抛出outOfMemoryError错误,也不会回收此类对象。

如果我们想要JVM回收此类被强引用关联的对象,我们可以显示地将 该对象 置为null,那么JVM就会在合适的时间回收此对象。

object = null;
str = null;

2.软引用(SoftReference)

java中使用 SoftRefence 来表示 软引用,如果某个对象与软引用关联,那么如果内存空间足够,垃圾回收器就不会回收它,JVM只会在内存不足的情况下回收该对象。

那么利用这个特性,软引用可用来实现内存敏感的高速缓存(比如网页缓存、图片缓存等)。使用软引用能防止内存泄露,增强程序的健壮性。

软引用适合做缓存,在内存足够时,直接通过软引用取值,无需从真实来源中查询数据,可以显著地提升网站性能。当内存不足时,能让JVM进行内存回收,从而删除缓存,这时候只能从真实来源查询数据。

//软引用声明格式:
//strSoft 是对java对象new String("abc") 的软引用 
public static void main(String args[]) {
    //SoftReference的实例strSoft 保存了对一个Java对象new String("abc") 的软引用    
    SoftReference<String> strSoft = new SoftReference<String>(new String("abc"));   
    // SoftReference类所提供的get()方法返回Java对象的强引用
    System.out.println(strSoft.get());  //abc
    //通知JVM进行内存回收
    System.gc();         
    System.out.println(strSoft.get());  //abc
    // 可以看得出来,此时JVM内存充足,暂时还没有回收被软引用关联的对象。
}

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。

即:使用ReferenceQueue清除失去了软引用对象的SoftReference

ReferenceQueue queue = new  ReferenceQueue();
SoftReference<String> strSoft = new SoftReference<String>(new String("abc"), queue);   
//....软引用不能立马看到回收。

System.out.println(queue.poll());  // 删除即将出队的元素
/*意思就是
如果软引用所引用的对象没有被垃圾回收,则打印null
如果软引用所引用的对象被垃圾回收,则打印 java.lang.ref.WeakReference@2e817b38

3.弱引用(WeakReference)

java中使用 WeakReference 来表示弱引用。如果某个对象与弱引用关联,那么当JVM在进行垃圾回收时,无论内存是否充足,都会回收此类对象。

不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

//弱引用声明格式:
//strWeak 是对java对象new String("abc") 的弱引用
public static void main(String args[]) {
    //弱引用
	WeakReference<String> strWeak = new WeakReference<String>(new String("abc"));   
    System.out.println(strWeak.get());  //abc
    //通知JVM进行内存回收
    System.gc();
    System.out.println(strWeak.get());  // null
}
System.out.println(strSoft.get());  //abc
System.gc(); 
Thread.sleep(100); // 最好停100ms,确保垃圾回收已经执行
//gc线程是一个优先级很低的守护线程,还来不及扫描该该对象所在的区域,即来不及对该对象的回收,就已经执行下一个sout了。
System.out.println(strSoft.get());  //null

注意:这里说的被 弱引用 关联的 java对象 是指只有弱引用与之关联,如果存在 强引用 同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。

public static void main(String args[]) {
    String str = new String("abc")                                   //强引用
	WeakReference<String> strWeak = new WeakReference<String>(str);  //弱引用
    System.out.println(strWeak.get());  //abc
    //通知JVM进行内存回收
    System.gc();
    System.out.println(strWeak.get());  // abc
}

扩展:

弱引用可以在回调函数在防止内存泄露。因为回调函数往往是匿名内部类,一个非静态的内部类会隐式地持有外部类的一个强引用,当JVM在回收外部类的时候,此时回调函数在某个线程里面被回调的时候,JVM就无法回收外部类,造成内存泄漏。在安卓activity内声明一个非静态的内部类时,如果考虑防止内存泄露的话,应当显示地声明此内部类持有外部类的一个弱引用。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

ReferenceQueue queue = new  ReferenceQueue();
WeakReference<String> strSoft = new WeakReference<String>(new String("abc"), queue); 

4.虚引用(PhantomReference)

java中使用PhantomReference来表示虚引用。虚引用,引用就像形同虚设一样。如果一个对象仅持有虚引用,就像某个对象没有引用与之关联一样。若某个对象与虚引用关联,那么在任何时候都可能被JVM回收掉。虚引用不能单独使用,必须配合引用队列一起使用。

//虚引用声明格式:
public static void main(String args[]) {
        ReferenceQueue<String> queue = new ReferenceQueue<>();
        PhantomReference<String> str = new PhantomReference<String>("abc", queue);
        System.out.println(str.get()); //null
    }

虚引用主要用来跟踪对象被垃圾回收的活动

当垃圾回收器准备回收一个对象时,如果发现它与虚引用关联,就会在回收它之前,将这个虚引用加入到引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被回收,如果确实要被回收,就可以做一些回收之前的收尾工作。

特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生

如何利用软引用和弱引用解决OOM问题

下面举个例子,假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。

设计思路是:用一个HashMap来保存图片的路径 和 相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。在Android开发中对于大量图片下载会经常用到。

如何判断一个常量是废弃常量?

运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?

  1. JDK1.7 之前 运行时常量池 逻辑包含 字符串常量池 存放在 方法区
  2. JDK1.7 字符串常量池被从方法区拿到了中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是 hotspot 中的永久代 。
  3. JDK1.8 hotspot 移除了永久代用元空间(Metaspace)取而代之, 字符串常量池 在堆中,运行时常量池在元空间中
  4. 常量池、字符串常量池、运行时常量池区别 只是文中没有考虑JDK版本问题

假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池了。

如何判断一个类是无用的类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

类需要同时满足下面 3 个条件才能算是 “无用的类”

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

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

3、垃圾收集算法

在这里插入图片描述

标记-清除算法

首先标记出所有不需要回收的对象(可用对象),在标记完成后统一回收清掉所有没有被标记的对象

它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:

  1. 效率问题(两次扫描,严重浪费时间)
  2. 空间问题(标记清除后会产生大量不连续的碎片)

产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。

在这里插入图片描述

标记-复制算法

理解: 标记–>复制–>清除

为了解决效率问题,“标记-复制”收集算法出现了。

它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

在这里插入图片描述

标记-整理(标记-压缩)

理解: 标记–>整理–>清除

根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存

在这里插入图片描述

分代收集算法

延伸面试问题: HotSpot 为什么要分为新生代和老年代?

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

分代收集算法:根据各个年代的特点选择合适的垃圾收集算法

新生代采用标记-复制算法,老年代采用标记-整理算法。

4 、垃圾收集器

参考1参考2

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

虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器

第一点:“Stop The World”——即当进行垃圾收集的时候,必须暂停其它所有的工作线程。

第二点:Java的HotSpot虚拟机有两种工作模式,**Client模式(轻量级)**和 Server模式(重量级)。[可以通过在cmd中输入命令java -version进行查看]

在这里插入图片描述

1、它们所处区域,则表明其是属于新生代收集器还是老年代收集器:

​ 新生代收集器(标记-复制):Serial、ParNew、Parallel Scavenge;

​ 老年代收集器(标记-整理):Serial Old、Parallel Old、CMS(例外,标记-清除);

​ 整堆收集器:G1(分代收集算法)

两个收集器间有连线,表明它们可以搭配使用

其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案(后面介绍);

2、GC中的并行和并发:

  • 并行(Parallel) :多个线程同时执行GC任务,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。

串行:Serial、Serial Old

并行:ParNew、Parallel Scavenge、Parallel Old;

并发:CMS

并发+并行:G1

1、Serial(串行) 收集器

Serial(串行)收集器(Serial Garbage Collector)是最基本、历史最悠久的垃圾收集器了。 JDK1.3.1前是HotSpot新生代收集的唯一选择;

1、特点

针对新生代的垃圾收集器,新生代采用 标记-复制算法。

单线程收集器。使用一条垃圾收集线程去完成垃圾收集工作,它在进行垃圾收集时,必须暂停其他所有工作线程( “Stop The World” ),直到它收集结束。

在这里插入图片描述

2、应用场景

​ HotSpot在 Client模式 下默认的 新生代收集器;

3、Serial 收集器优点:

简单而高效(与其他收集器的单线程相比)

​ 对于限定单个CPU的环境来说,Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。

4、设置参数

​ 通过JVM参数-XX:+UseSerialGC可以使用 Serial(串行)垃圾回收器。

5、Stop TheWorld说明

JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿;会带给用户不良的体验;

从JDK1.3到现在,从Serial收集器 --> Parallel收集器 --> CMS --> G1,用户线程停顿时间不断缩短,但仍然无法完全消除;

4、Serial Old 收集器

Serial Old是 Serial收集器的老年代版本;采用"标记-整理"算法。它同样是一个单线程收集器。

  • 应用场景

    主要用于Client模式;

    而在Server模式有两大用途:

A、在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);

B、作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用(后面详解);

Serial/Serial Old收集器运行示意图如下:

在这里插入图片描述

2、ParNew 收集器

1、特点

针对新生代,新生代采用 标记-复制算法。

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。

在这里插入图片描述

2、应用场景

​ 在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

3、设置参数

-XX:+UseConcMarkSweepGC  指定使用CMS后,会默认使用ParNew作为新生代收集器;
-XX:+UseParNewGC         强制指定使用ParNew;  
-XX:ParallelGCThreads    指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

4、为什么只有ParNew能与CMS收集器配合

​ CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;

​ CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;

3、Parallel Scavenge 并行收集器

Parallel Scavenge 收集器也是使用标记-复制算法的多线程收集器,它看上去几乎和 ParNew 都一样。 那么它有什么特别之处呢?

-XX:+UseParallelGC

    使用 Parallel 收集器+ 老年代串行

-XX:+UseParallelOldGC

    使用 Parallel 收集器+ 老年代并行

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。

1、特点

Parallel Scavenge 收集器 与ParNew收集器相似,也是新生代收集器,使用标记-复制算法的多线程收集器。

主要特点是:它的关注点与其他收集器不同。

Parallel Scavenge 收集器关注点是 吞吐量(高效率的利用 CPU)。

而CMS 等垃圾收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间(提高用户体验)。

在这里插入图片描述

2、应用场景

高吞吐量为目标,(高效率的利用 CPU,减少垃圾收集时间),尽快的完成程序的运算任务等。

主要适合在后台运算而不需要与用户进行太多交互。(其不适合需要与用户交互的程序,良好的响应速度能提升用户的体验,此种场景CMS效果更好)。

例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序;

吞吐量:是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。

(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))

3、设置参数

参数详细见参考1参考2

-XX:MaxGCPauseMillis       控制最大垃圾收集停顿时间
-XX:GCTimeRatio            直接设置吞吐量大小的   
-XX:+UseAdptiveSizePolicy  开关参数。开启这个参数后,就不用手工指定一些细节参数,如:
新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等;

Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略(GC Ergonomiscs),把内存管理优化交给虚拟机去完成。

这也是Parallel Scavenge收集器与ParNew收集器一个重要区别。

-XX:+UseParallelGC     使用Parallel Scavenge(年轻代)+Serial Old(老年代)的组合进行GC
-XX:+UseParallelOldGC  使用Parallel Scavenge(年轻代)+Parallel Old(老年代)的组合进行GC

5、Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。使用“标记-整理”算法。它同样是一个多线程收集器。

Parallel Scavenge/Parallel Old收集器运行示意图如下:

在这里插入图片描述

2、应用场景

​ JDK1.6及之后用来代替老年代的Serial Old收集器;

特别是在Server模式,多CPU的情况下;

在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

3、设置参数

-XX:+UseParallelOldGC:指定使用Parallel Old收集器;

6、CMS 收集器

1、特点

是HotSpot在JDK1.5推出的第一款真正意义上的并发收集器(垃圾收集线程与用户线程同时工作)

针对老年代。基于"标记-清除"算法;

获取最短回收停顿时间为目标;它非常符合在注重用户体验的应用上使用。

优点:并发收集、低停顿;

需要更多的内存(看后面的缺点);

2、应用场景

与用户交互较多的场景;

希望系统停顿时间最短,注重服务的响应速度;

以给用户带来较好的体验;

如常见WEB、B/S系统的服务器上的应用;

3、设置参数

-XX:+UseConcMarkSweepGC:指定使用CMS收集器;

4、CMS收集器运作过程

比前面几种收集器更复杂,可以分为4个步骤:

  • 初始标记:

    仅标记GC Roots能直接关联到的对象。

    需要暂停所有的其他线程(Stop The World),但速度很快 ;

  • 并发标记:

    进行GC Roots Tracing的过程。同时开启 GC 和用户线程,

    GC 线程用一个闭包结构去记录可达对象。

    因为用户线程可能会不断的更新引用域,所以这个闭包结构并不能保证包含当前所有的可达对象。

    所以这个算法里会跟踪记录这些发生引用更新的地方。

  • 重新标记:

    为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,

    需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;

    采用多线程并行执行来提升效率;

  • 并发清除:

    开启用户线程,同时 GC 线程开始清除未标记的对象。

整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行的;

在这里插入图片描述

优点:并发收集、低停顿

三个明显的缺点:

  • 对 CPU 资源敏感;

    并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。

  • 无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败

    在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;

​ 这使得 并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;

如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;

​ 这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生,这样的代价是很大的。

  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

    解决:设置-XX:+UseCMSCompactAtFullCollection参数,使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程。但合并整理过程无法并发,停顿时间会变长;

7、G1 收集器

G1 (Garbage-First) 是一款面向服务端的垃圾收集器,针对具有大内存、多处理器的机器。具备高吞吐量 和极低的GC 停顿 特征。

1、被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:

  • 并行与并发

    能充分利用多CPU、多核环境下的硬件优势;

    并行,使用多个 CPU(或CPU 核心)来缩短 Stop-The-World 停顿时间;

    并发,让垃圾收集与用户程序同时进行。

  • 分代收集

    能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;

    能够采用不同方式处理不同时期的对象;

    虽然保留分代概念,但Java堆的内存布局有很大差别;

    将整个堆划分为多个大小相等的独立区域(Region);

    新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;

  • 空间整合

    从整体看,是基于标记-整理算法;从局部(两个Region间)看,是基于标记-复制算法;

    都不会产生内存碎片,有利于长时间运行;

  • 可预测的停顿

    这是 G1 相对于 CMS 的另一个大优势。 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

为什么G1收集器可以实现可预测的停顿

G1可以建立可预测的停顿时间模型,是因为:

可以有计划地避免在Java堆的进行全区域的垃圾收集;

G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;

每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);

这就保证了在有限的时间内可以获取尽可能高的收集效率;

在这里插入图片描述

2、G1收集器运作过程

不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。

  • 初始标记(Initial Marking)

  • 并发标记(Concurrent Marking)

  • 最终标记(Final Marking)

  • 筛选回收(Live Data Counting and Evacuation)

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值