06-JVM

1、JVM的含义与历史

什么是JVM?

JVM是字节码文件与计算机操作系统两者之间的翻译官,是java能实现一次编写多平台运行的关键。

JVM的历史

  • 最早的Sun Classic:由Sun开发,只能使用纯解释器的方式来执行 Java 代码,如果要使用 JIT 编译器那就必须使用外挂的 。到jdk1.3之前。

  • 没有被真正大规模使用过的Sun Exact VM:由Sun开发,解决了 Classic VM 存在的解释器和编译器无法同时工作的问题,还具备了一些现代高性能处理器的特性

  • Longview Technologies 公司开发的HotSpot VM:不仅仅有前面说到两款虚拟机的优点(如:准确式内存管理),也有许多自己的新技术,Sun1997 年收购了 Longview Technologies 公司,从而获得了 HotSpot VM。从1.3一直作为JDK默认JVM到现在。

  • BEA 公司的 BEA JRockit 和 IBM 公司的 J9 VM

最后的赢家:Oracle
看了这么些历史,似乎都是在说 Sun公司发布的虚拟机,与 Oracle 似乎没有什么关系。但在 2010 年,Oracle 公司收购了 Sun 公司,这样 Oracle 就拥有了 HotSpot VM。再加上其在 2008 年收购 BEA 公司获得的 JRocket VM,Oracle 公司就拥有了地球上最优秀的两款虚拟机。

对于虚拟机未来的规划,Oracle 宣布会将 JRockit 的优秀特性整合到 HotSpot VM 中,例如移植 JRockit 的垃圾回收器和 MissionControl 服务。

2、从源代码到机器码

2.1源代码到字节码:前端编译器

我们运行 javac 命令的过程,其实就是 javac 编译器解析 Java 源代码,并生成字节码文件的过程。说白了,其实就是使用 javac 编译器把 Java 语言规范转化为字节码语言规范。

字节码文件大致结构:

img

2.2字节码到机器码,编译与解释共存:解释运行与JIT编译器

1、概念理解

在部分商用虚拟机中(如HotSpot),Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler)。

​ --------深入理解Java虚拟机

2、解释运行与JIT编译器执行过程

  • 当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率;
  • 但JIT编译器选择的所有提升运行速度的优化手段并不一定都正确,当激进优化的假设不成立时(加载了新类后,类型继承结构发生了变化),会通过逆优化退回到解释状态执行。所以在整个java虚拟机的执行架构里,解释器与编译器经常是相辅相成地配合工作

img

3、解释与编译各有优势

当程序运行环境中***内存资源限制较大***(如部分嵌入式系统中),可以使用*解释器执行节约内存*,反之可以使用***编译执行来提升效率***。此外,如果编译后出现“罕见陷阱”,可以通过逆优化退回到解释执行。

4、JIT编译器的两种模式:client-model和server-model

HotSpot虚拟机中内置了两个即时编译器:Client Complier和Server Complier,简称为C1、C2编译器,分别用在客户端和服务端。目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。程序使用哪个编译器,取决于虚拟机运行的模式。HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式。

用Client Complier获取更高的编译速度,用Server Complier 来获取更好的编译质量。为什么提供多个即时编译器与为什么提供多个垃圾收集器类似,都是为了适应不同的应用场景。

​ —————深入理解Java虚拟机

理解完上面的话大概就知道了为什么两种模式的名字叫客户端和服务器了!

  • 对Client Compiler来说,它是一个简单快速的编译器,主要关注点在于****局部优化****,而放弃许多耗时较长的全局优化手段。
  • 而Server Compiler则是专门面向服务器端的,并为服务端的性能配置特别调整过的编译器,是一个****充分优化****过的高级编译器。

5、热点代码的判断

在HotSpot虚拟机中使用的是基于计数器的热点探测方法,因此它为每个方法准备了两个计数器:方法调用计数器***和***回边计数器。在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就成为所谓的*“热点代码”*,就会触发JIT编译。

3、类加载机制

3.1先了解java虚拟机运行时数据区

主要分为三个部分:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kWG7fS2Y-1617940006240)(C:\Users\91051\AppData\Roaming\Typora\typora-user-images\image-20210322104847947.png)]

堆区

  • jvm只有一个heap区,被所有线程共享,不存放基本类型和对象引用,只存放对象本身。
  • 堆的优劣势:堆的优势是可以动态的分配内存大小,生存期也不必事先告诉编译器,java的垃圾收集器会自动收取这些不在使用的数据,但缺点是,由于要在运行时动态分配内存,存取速度慢。

栈区

  • 每一个线程包含一个stack区,只保存基本数据类型的对象和自定义对象的引用(不是对象),对象都存放在共享heap中;
  • 每个栈中的数据(基本数据类型和对象引用)都是私有的,其他栈不能访问;
  • 栈分为3部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)
  • 栈的优势劣势:存取速度比堆要快,仅次于直接位于CPU的寄存器,但必须确定的是存在stack中的数据大小与生存期必须是确定 的,缺乏灵活性。单个stack的数据可以共享。,

方法区(HotSpot的元空间)

1、又叫静态区,跟堆一样,被所有的线程共享。存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。

3.2类加载机制

1.什么是类的加载机制

先进行类的加载,再对类的数据进行校验、转化解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称为虚拟机的类加载机制

​ --------深入理解Java虚拟机

2.类加载的过程

类的生命周期:加载、验证、准备、解析、初始化、使用、卸载。

前面所说的类加载是指泛指整个过程,这里是单指第一个阶段“加载”

1、加载

  • 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class对象, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
  • 类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
  • 既可以使用系统提供的类加载器完成,也可以由用户自定义的类加载器完成

加载.class文件的方式

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件

2、验证

3、准备

准备阶段就是正式为类变量(静态变量)分配内存地址并设置变量初始值的阶段。

注意:

  • 仅包括类变量,不包括实例变量
  • 设置初始值都向“ 0 ”看齐,引用类型就为null
  • 如果是静态常量则会直接初始化为指定值

4、解析

5、初始化

初始化阶段就是执行类构造器()方法的过程。

()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。收集的顺序与源文件中出现的顺序一样。

注意:

  • 执行子类的()方法前必须先执行父类的()方法,因此Java虚拟机中第一个被执行()方法的肯定是Object的。
  • 前面的static语句块可以对定义在它之后的变量赋值,但不能对其访问。
  • 执行接口的()方法不是必须执行父接口的()方法,只有当父接口中的变量被使用时父接口才会进行初始化。即执行()方法。

初始化触发条件:

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

6、使用

当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码

7、卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。

3.利用类加载机制分析程序语句执行

补充:

对象初始化:

  1. 有父类再初始化父类对象(默认调用父类无参构造函数)

  2. 本类实例变量的初始化(没赋值的为0,赋值的直接赋值)和实例代码块

  3. 执行自己的构造方法

  4. 判断是否有满足类的初始化条件的类。

  5. 先分析该类的准备阶段做的事。

  6. 再分析类的初始化阶段做的事

例题:

public class Book {
    public static void main(String[] args)
    {
        staticFunction();
    }

    static Book book = new Book();

    static
    {
        System.out.println("书的静态代码块");
    }

    {
        System.out.println("书的普通代码块");
    }

    Book()
    {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }

    public static void staticFunction(){
        System.out.println("书的静态方法");
    }

    int price = 110;
    static int amount = 112;
}

开始分析:

1、确定初始化条件:初始化 main 方法所在的整个类

2、从准备阶段开始分析

为类变量分批内存地址并赋初始值:

static Book book = null;
static int amount = 0;

3、分析初始化阶段

执行()方法,即执行类变量的赋值语句和static代码块

static Book book = new Book();
/**
该语句块会执行对象初始化操作:

 1. 没有需要考虑的父类初始化
 2. 本类实例变量的初始化(没赋值的为0,赋值的直接赋值)和 实例代码块

 
 {
    System.out.println("书的普通代码块");
 }
 int price = 110;

 3、最后执行自己的构造方法

 
 Book()
     {
         System.out.println("书的构造方法");
         System.out.println("price=" + price +",amount=" + amount);
     }
 
*/

static
    {
        System.out.println("书的静态代码块");
    }
static int amount = 112;

到这就全部分析完了,最后来一张总的执行顺序:

//准备阶段
static Book book = null;
static int amount = 0;
//初始化阶段
static Book book = new Book();
/**
该语句块会执行对象初始化操作:

 1. 没有需要考虑的父类初始化
 2. 本类实例变量的初始化(没赋值的为0,赋值的直接赋值)和 实例代码块
*/
 
 {
    System.out.println("书的普通代码块");
 }
 int price = 110;

 //3、最后执行自己的构造方法

 
 Book()
     {
         System.out.println("书的构造方法");
         System.out.println("price=" + price +",amount=" + amount);
     }
 
*/

static
    {
        System.out.println("书的静态代码块");
    }
static int amount = 112;

最后输出结果就很顺理成章了:

书的普通代码块
书的构造方法
price=110,amount=0
书的静态代码块
书的静态方法

3.3类加载器与双亲委派模型

类加载器

从Java开发人员角度看,JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader

  1. BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。
  2. ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。
  3. AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载用户路径(ClassPath)上的所有类库,如果没有其它用户自定义类加载器,程序中默认就是使用该加载器

在Java虚拟机角度来看只有两种加载器:

  1. 启动类加载器:顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。
  2. 其它所有加载器:由java语言实现,独立存在于虚拟机外,且全部继承自java.lang.ClassLoader

双亲委派模型

每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型

1、即在类加载的时候,系统会首先判断当前类是否被加载过

  • 已经被加载的类会直接返回
  • 否则才会尝试加载,进入下面的过程

2、加载的时候,判断父类加载器是否为null

  • 不为null首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader
  • 当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器

3、当父类加载器无法处理时,才由自己来处理。

源码

private final ClassLoader parent; 
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查请求的类是否已经被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
                        c = parent.loadClass(name, false);
                    } else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //抛出异常说明父类加载器无法完成加载请求
                }
                
                if (c == null) {
                    long t1 = System.nanoTime();
                    //自己尝试加载
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

双亲委派模型的好处

java中的类随着它的加载器一起具备了一种带有优先级的层次关系。

亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

破坏双亲委派模型

从前面的源码可以看出java自己对双亲委派模型的实现代码就在loadClass方法中,如果我们自定义类加载器时重写了该方法就破坏了双亲委派模型,但大多数我们不会破坏,这时我们自定义类加载器一般不会重写该方法,而是去重写findClass方法。

4、GC——垃圾回收机制

4.1谁是垃圾?

判断垃圾对象一般有两种方式:

  1. 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。A引用B,B引用C,C引用A,它们只是在自身引用,并没有其他变量引用它们,按照垃圾的的逻辑来说它们应该是垃圾,但计数器上它们都是1.
  2. 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

在Java语言中,GC Roots包括:

  • 虚拟机栈中引用的对象。
  • 方法区中类静态属性实体引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI引用的对象。

4.2怎么收垃圾

垃圾收集算法有三种:

1、“标记-清除”(Mark-Sweep)算法

  • 如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。
  • 它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

图片

2、标记-复制算法

  • “复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
  • 这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

图片

3、标记-压缩算法

  • 复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
  • 根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

图片

三种收垃圾的算法各有优缺点,显然我们实际不会单独只用某一个算法来回收垃圾,而是结合使用,而结合使用就会产生新的问题:我们怎样实现在不同区域用适合对应区域的回收算法?这时分代思想就出来了!

分代思想

所谓分代算法,就是根据 JVM 内存的不同内存区域,采用不同的垃圾回收算法。例如对于存活对象少的新生代区域,比较适合采用复制算法。这样只需要复制少量对象,便可完成垃圾回收,并且还不会有内存碎片。而对于老年代这种存活对象多的区域,比较适合采用标记压缩算法或标记清除算法,这样不需要移动太多的内存对象。

试想一下,如果没有采用分代算法,而在老年代中使用复制算法。在极端情况下,老年代对象的存活率可以达到100%,那么我们就需要复制这么多个对象到另外一个内存区域,这个工作量是非常庞大的。

  1. 新生代垃圾回收

如我们上面所说,新生代的特点是存活对象少,适合采用复制算法。而复制算法的一种最简单实现便是折半内存使用,另一半备用。但实际上我们知道,在实际的 JVM 新生代划分中,却不是采用等分为两块内存的形式。而是分为:Eden 区域、from 区域、to 区域 这三个区域。那么为什么 JVM 最终要采用这种形式,而不用 50% 等分为两个内存块的方式?

要解答这个问题,我们就需要先深入了解新生代对象的特点。根据IBM公司的研究表明,在新生代中的对象 98% 是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间。所以在HotSpot虚拟机中,JVM 将内存划分为一块较大的Eden空间和两块较小的Survivor空间,其大小占比是8:1:1。当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Eden空间。

4.3垃圾收集器

前面说完收集垃圾的理论,现在来看理论的实践者——垃圾收集器

image.png

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。

CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

  • 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
  • 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
  • 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

CMS 垃圾收集器

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

  • 对 CPU 资源敏感;
  • 无法处理浮动垃圾;
  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

G1 收集器

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

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

  • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
  • 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
  • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

G1 收集器的运作大致分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

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

4.4垃圾回收类型

Minor GC

从年轻代空间回收内存被称为 Minor GC,有时候也称之为 Young GC。对于 Minor GC,你需要知道的一些点:

  • 当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以 Eden 区越小,越频繁执行 Minor GC。
  • 当年轻代中的 Eden 区分配满的时候,年轻代中的部分对象会晋升到老年代,所以 Minor GC 后老年代的占用量通常会有所升高。
  • 质疑常规的认知,所有的 Minor GC 都会触发 Stop-The-World,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的,因为大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果情况相反,即 Eden 区大部分新生对象不符合 GC 条件(即他们不被垃圾回收器收集),那么 Minor GC 执行时暂停的时间将会长很多(因为他们要JVM要将他们复制到 Survivor 区或老年代)。

Major GC

从老年代空间回收内存被称为 Major GC,有时候也称之为 Old GC。

  • 许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。
  • Minor GC 作用于年轻代,Major GC 作用于老年代。 分配对象内存时发现内存不够,触发 Minor GC。Minor GC 会将对象移到老年代中,如果此时老年代空间不够,那么触发 Major GC。因此才会说,许多 Major GC 是由 Minor GC 引起的。

Full GC

Full GC 是清理整个堆空间 —— 包括年轻代、老年代和永久代(如果有的话)。因此 Full GC 可以说是 Minor GC 和 Major GC 的结合。

  • 当准备要触发一次 Minor GC 时,如果发现年轻代的剩余空间比以往晋升的空间小,则不会触发 Minor GC 而是转为触发 Full GC。因为JVM此时认为:之前这么大空间的时候已经发生对象晋升了,那现在剩余空间更小了,那么很大概率上也会发生对象晋升。既然如此,那么我就直接帮你把事情给做了吧,直接来一次 Full GC,整理一下老年代和年轻代的空间。

  • 另外,即在永久代分配空间但已经没有足够空间时,也会触发 Full GC。

Stop-The-World

Stop-The-World,中文一般翻译为全世界暂停,是指在进行垃圾回收时因为标记或清理的需要,必须让所有执行任务的线程停止执行任务,从而让垃圾回收线程回收垃圾的时间间隔。

  • 在 Stop-The-World 这段时间里,所有非垃圾回收线程都无法工作,都暂停下来。只有等到垃圾回收线程工作完成才可以继续工作。可以看出,Stop-The-World 时间的长短将关系到应用程序的响应时间,因此在 GC 过程中,Stop-The-World 的时间是一个非常重要的指标。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值