> 文档中心 > 深入浅出 JVM 详解

深入浅出 JVM 详解

JVM

  • 1 基本介绍
    • 1.1 什么是 JVM
    • 1.2 JVM 与其他虚拟机的区别
  • 2 JVM 执行流程
  • 3 JVM 内存区域划分
    • 3.1 内存区域中的各个概念解释
    • 3.2 内存区域中的相关异常
    • 3.3 确定某个变量在哪个内存区域
    • 3.4 Java 引用类型的理解
  • 4 JVM 类加载
    • 4.1 类加载的基本流程
      • 4.1.1 加载(Loading)
      • 4.1.2 验证(Verification)
      • 4.1.3 准备(Preparation)
      • 4.1.4 解析(Resolution)
      • 4.1.5 初始化(Initialization)
      • 4.1.6 使用(Using)
    • 4.2 示例
    • 4.3 双亲委派模型
      • 4.3.1 概述
      • 4.3.2 三种类加载器
      • 4.3.3 三种类加载器执行说明
      • 4.3.4 双亲委派模型的优点
  • 5 垃圾回收(GC)
    • 5.1 什么是垃圾回收(GC)
    • 5.2 为什么要进行垃圾回收
    • 5.3 垃圾回收的优点/缺点是什么
    • 5.4 垃圾回收要回收的内存有哪些
    • 5.5 垃圾回收到底是怎么回收的
    • 5.6 垃圾对象的判断算法
      • 5.6.1 引用计数算法
      • 5.6.2 可达性分析
        • 5.6.2.1 核心思想
        • 5.6.2.2 有关四种引用
    • 5.7 垃圾回收算法, 具体是怎么回收的
      • 5.7.1 标记 - 清除算法
      • 5.7.2 复制算法
      • 5.7.3 标记 - 整理算法
      • 5.7.4 分代算法
      • 5.7.4.1 分代算法概述及核心思想
      • 5.7.4.2 分代算法的过程
      • 5.7.4.3 Minor GC 与 Full GC 的区别

1 基本介绍

1.1 什么是 JVM

 本篇文章主要介绍 **JVM 是什么, JVM 的内存布局是什么样的, JVM 的类加载机制是怎么一回事,以及垃圾回收的概念,**这些概念都是以后升学或者是找工作面试常考/常问的问题,因此需要我们经常回顾,本篇文章便可以当做常回顾的工具.
 我们在上学期间可能已经接触过虚拟机这个概念,虚拟机就是一个完整的计算机系统,其具有完整的硬件功能,并且运行在一个完全隔离的环境中, 例如在上 Linux 课的时候老师会让我们在电脑上装一个 VMware 这样的软件, VMware 就是一个可以在 Windows 上很容易运行的虚拟机,而 JVM 就是一个虚拟机,只不过此虚拟机和 VMware 有着本质的区别.

1.2 JVM 与其他虚拟机的区别

  Java Virtual Machine 简称 JVM, 也就是 Java 虚拟机.

  • VMwave 是通过软件来模拟物理 CPU 的指令集, 物理系统中会有很多的寄存器;
  • JVM 则是通过软件模拟 Java 字节码的指令集, JVM 中只是主要保留了 PC 寄存器,其他的寄存器都进行了删减, 也就是说 JVM 是一台被定制过的现实当中不存在的计算机.
  • 也可以这样说 JVM 只是对硬件设备进行了简单的抽象封装,从而达到跨平台的效果, 与 VMware 等虚拟机根本不是一回事, VMware 等虚拟机是 100% 使用软件模拟出的硬件.

2 JVM 执行流程

 如下图所示,便是 JVM 的整个执行流程.
在这里插入图片描述

3 JVM 内存区域划分

JVM 其实本质上是一个 Java 进程,用来管理硬件资源(例如内存), JVM 启动后就会从操作系统这里申请到一大块内存区域.

可以这样比喻, 一个操作系统就类似于一个中学, 而一个进程呢就相当于这所中学里面的初一年级, 初一年级从这所中学申请了一块区域,例如蕴美楼这块区域就是初一年级所申请的区域,这就相当于 JVM 从系统中申请到了内存, 然后初一年级的老师又将蕴美楼分为多个班级,这就相当于 Java 进程对内存空间划分了多个区域, 每个区域都有着不同的功能.

3.1 内存区域中的各个概念解释

  • 堆: 里面放的便是 new 的对象;
  • 方法区: 里面放的是 类对象, 类的 static 成员作为类的属性, 同样也是在类对象中, 也就是在方法区里;

那么类对象里面都有什么呢?
1. 包含了这个类的各种属性的名字,类型以及访问权限;
2. 包含了这个类的各种方法的名字,参数类型,返回值类型,访问权限以及方法实现的二进制代码;
3. 包含了这个类的 static 成员

  • 程序计数器: 是内存区域中最小的一部分, 里面只是放了一个内存地址,这个地址的含义便是接下来要执行的指令地址;例如 .class 文件(二进制字节码)中就是一些指令,在内存中每个指令都有自己的地址,CPU 执行指令就需要从内存中取地址,然后再在 CPU 上执行.
  • 栈: 其里面放的是局部变量;

本地方法栈和虚拟机栈(Java 栈)的区别: 虚拟机栈是给 JVM 使用的, 而本地方法栈则是给本地方法使用的.

3.2 内存区域中的相关异常

 内存区域中的异常主要有两种,一种是堆溢出,另一种是栈溢出.简单来说:

  • 堆溢出: 堆空间耗尽,出现这样的情况大多是一直 new 对象但不去释放对象;
  • 栈溢出: 典型的场景就是无限递归, 栈里面除了要放局部变量之外,还要放方法的调用关系.

堆和栈搞多大的空间是可以通过 JVM 来显示配置的,例如可以设置 JVM 参数

  • -Xms: 设置堆最小值;
  • -XMX: 设置堆最大值.
  • 内存泄漏: 泄漏对象无法被 GC(垃圾回收);
  • 内存溢出: 内存对象确实还应该存活, 此时要根据 JVM 堆参数与物理内存相比较检查是否还应该把 JVM 堆内存调大,或者是检查对象的生命周期是否过长.

3.3 确定某个变量在哪个内存区域

 咱们以例子的形式来讲解一下我们的代码中某个变量到底在哪个内存区域中.
示例1:

class Test {    public int value = 0;}@WebServlet("/JVM")public class TestJVM extends HttpServlet {    @Override    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Test test = new Test();    }}

那么问题来了, test 变量是在哪个内存区域呢?
在这里插入图片描述
示例2:

class Test {    public int value = 0;}@WebServlet("/JVM")public class TestJVM extends HttpServlet {    Test test = new Test();    @Override    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {    }}

此时的 test 在哪个内存区域?
在这里插入图片描述

示例3: 如果 Test 前面加上 static,此时的 test 在哪个内存区域呢?
在这里插入图片描述

3.4 Java 引用类型的理解

  学过 C 语言的都知道, C 指针是一个变量,这个变量里面存了一个整数,并且这个整数的含义就是内存的地址; C 指针的功能非常强大,支持各种的比较操作以及+ / - 等; 那么 Java 的引用也可以看做是一个"低配的指针",其引用相当于对 C 指针功能的缩减版,也就是说只能用来解引用(.的时候就是在解引用)和比较(== / !=).

4 JVM 类加载

  Java 中的类加载是 JVM 中的一个非常核心的流程,其做的事情就是将 .class 文件转换成 JVM 中的对象, 例如我们发明了一个编程语言,那么肯定是想让这个编程语言跑起来, 那么这就需要把源代码编译成可执行程序,然后再去执行代码逻辑. 要想完成类加载,必须要明确的知道 .class 文件中都有啥,按照规则来进行解析,因此编译器和 JVM 类加载器必须要商量好 .class 文件的格式, 这个 .class 文件的格式是 Java 虚拟机规范文档里面约定的,其实就是一种"协议".关于 Java 虚拟机规范文档地址为: Java虚拟机规范文档.
在这里插入图片描述

4.1 类加载的基本流程

  关于类加载的基本流程主要是从 .class 文件 => 内存类对象的过程,主要步骤如下:
在这里插入图片描述

4.1.1 加载(Loading)

 这里的加载阶段指的是整个类加载过程中的一个阶段, 这里千万不要与类加载混淆,一个是加载Loading,另一个是类加载 Class Loading; 首先将 .class 文件先找到, 代码中需要加载某个类,就需要在某个特定的目录中找到这个 .class 文件, 找到后打开这个文件并进行读取,此时就把这些数据读到了内存里面;总之,在加载 Loading 阶段, Java 虚拟机需要完成以下三件事情:
1) 通过一个类的全限定名来获取定义此类的二进制字节流;
2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
3) 在内存中生成一个代表这个类的 java.lang.Class 对象, 作为方法区这个类的各种数据的访问入口.

4.1.2 验证(Verification)

验证就是把刚才读到内存里的东西进行一个校验, 主要是验证一下刚才读到的这个内容是不是一个合法的 .class 文件, 必须是编译器生成的 .class 文件才能通过验证, 如果我们随便创建一个后缀名为 .class 文件是不能通过验证的; 并且这里除了验证文件格式之外,也会验证一下文件里面的一些字节码指令(方法里面具体要执行的指令)是否正确. 总之, 验证选项可以是文件格式验证, 字节码验证, 符号引用验证等.

4.1.3 准备(Preparation)

这里的准备阶段其实就是为了类对象中的一些成员分配内存空间, 并且进行一个初步的初始化操作, 也就是把初始空间设为全 0; 例如:

class Test {    public static int value = 111;}

类似于这句代码初始化 value 的 int 值为 0, 而不是111.

4.1.4 解析(Resolution)

  解析操作主要是针对字符串进行的处理, .class 文件中会涉及到一些字符串常量, 在这个类加载的过程中, 就需要把这些字符串常量给替换成当前 JVM 内部已经持有的字符串常量的地址. 也可以说解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程, 也就是初始化常量的过程. 这里需要注意的是: 并不是程序一启动, 就立即把所有的类都给加载了, 而是用到哪个类就加载哪个类, 但是呢字符串常量池是最初启动 JVM 就有的, 存在于堆中.

4.1.5 初始化(Initialization)

  这里的初始化才是对静态变量进行初始化, 同时也会执行 上面代码中 static 代码块. 也就是说 Java 虚拟机真正开始执行类中编写的 Java 程序代码, 将主导权移交给应用程序, 初始化阶段就是执行类构造器方法的过程.

4.1.6 使用(Using)

到这里其实已经加载完成了!!!

4.2 示例

class A {    public A() { System.out.println("A 构造方法");    }    static { System.out.println("A static");    }}class B extends A {    public B() { System.out.println("B 构造方法");    } static { System.out.println("B static");    }}public class TestJVM{    public static void main(String[] args) { B b = new B();    }}

运行结果:
深入浅出 JVM 详解
代码解读:
当 new B() 的时候,就先尝试加载 B 这个类. 然后加载 B 的时候发现 B 继承自 A, 于是又得先去加载 A, 两个类都加载完了再进行实例化操作, 构造方法是 new 对象的时候才调用, 此时已经初始化完成了; 口诀: 由父及子, 静态先行.

4.3 双亲委派模型

4.3.1 概述

  • 站在 Java 虚拟机的角度来看, 只存在两种不同的类加载器,: 一种是启动类加载器,这个类加载器由 C++ 来实现, 是虚拟机自身的一部分; 另外一种就是其他所有的类加载器, 这些类加载器都是由 Java 语言实现的, 独立存在于虚拟机外部, 并且全都继承自抽象类 java.lang.ClassLoader. 站在我们程序猿的角度, 类加载器就应该划分的更细致一些,自 JDK1.2以来, Java 一直保持这三层类加载器以及双亲委派的类加载器. 那么什么是双亲委派模型呢?
  • 如果一个类加载器收到了类加载的请求, 它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成, 每一个层次的类加载器都是如此, 因为所有的加载请求最终都应该传送到最顶层的启动类加载器中, 只有当父加载器反馈自己无法完成这个加载请求时,也就是在搜索范围内没有找到所需的类时,子加载器才会尝试自己去完成加载.
  • 在加载(Loading) 阶段, JVM 去哪个目录中来找 .class 这样的一个细节属于整个类加载过程中非常不起眼的一个环节, 但是这个环节却比较重要, 这是比较难以置信的, 可能是双亲委派模型这个名字比较霸气吧. 进行类加载过程中,一个比较重要的环节就是根据这个类的名字"java.lang.String"找到对应的 .class 文件, 在 JVM 中,有三个类加载器来负责进行这里找文件的操作, 这三个类加载器各有自己负责的区域.

4.3.2 三种类加载器

在这里插入图片描述

4.3.3 三种类加载器执行说明

执行步骤说明:

  • 这三个类加载器之间存在父子关系, 但是并不是继承里面的父类子类这样的关系, 而是像链表一样, 每个类里面都有一个 parent 字段, 指向了父类加载器.
  • 当在代码中使用到某个类的时候, 就会触发类加载器, 也就是说先从 AppClassLoader 开始, 但是 AppClassLoader 并不会真的开始去扫描自己负责的目录, 而是先去找其父类 ExtClassLoader, 但是 ExtClassLoader 也不会立即去扫描自己负责的目录, 而是也去找其父类加载器 BootStrap.
  • 到达 BootStrap 后, 也不会立即去扫描自己负责的目录, 也是先去找其父类加载器, 但是呢 BootStrap 上面已经没有了父类, 因此就去扫描自己负责的目录.
  • 如果在 BootStrap 中找到了需要的类, 就进行加载, 就没有其他类加载器的事情了, 但是如果没有匹配到合适的类, 就会告诉子加载器 ExtClassLoader, 再从 ExtClassLoader 所负责的目录中查找所需的类, 如果找到就加载, 如果未找到就去 AppClassLoader 所负责的目录区域查找类, 如果找到了就加载, 如果到这里还没有找到的话, 那就芭比Q了, 直接抛出 ClassNotFoundExcept 异常.

4.3.4 双亲委派模型的优点

1) 避免重复加载类: 例如 A 类和 B 类都有一个父类 C 类, 那么当启动 A 的时候就会将 C 类加载起来, 那么在 B 类进行加载时就不需要再重复加载 C 类了.
2) 安全性而言: 使用双亲委派模型也可以保证 Java 的核心 API 不会被篡改, 如果没有使用这种模型, 而是每个类加载器加载自己的话就会出现一些问题, 例如在编写一个成为 java.lang.Object 类的时候, 程序运行起来后系统就会出现多个不同的 Object 类, 而有些 Object 类又是用户自己提供的, 因此安全性就得不到保证了.
其实我们自己写的类加载器就不一定非得遵守双亲委派模型, 自己写的类加载器就是为了告诉程序去一些目录中找 .class, 例如 Tomcat webapps 里面就有很多类, 这些就是由 Tomcat 内部自己实现的类加载器来完成的, Tomcat 就没有遵循双亲委派模型.

5 垃圾回收(GC)

5.1 什么是垃圾回收(GC)

 举个例子来说明: 例如积攒了一天的各种各样的垃圾我放到了一个袋子里面, 这个时候我把这些垃圾分类好放到对应的垃圾桶里面, 这就是相当于 C 语言的手动回收内存; 但是呢有时候我比较懒, 我只是把所有垃圾直接扔到楼下的垃圾桶旁边, 当我走后, 保洁员在收拾小区卫生的时候就会对垃圾桶旁边的这些垃圾进行归类, 分类扔进不同的垃圾桶里面, 这就相当于垃圾回收机制, 也就是说谁扔垃圾都可以, 但是呢会有个统一负责的人来进行归类回收. 对于 Java 来说, 代码中的任何地方都可以申请内存, 然后由 JVM 统一进行释放, 具体来说 JVM 内部的一组专门负责垃圾回收的线程来进行这样的工作.简言之,垃圾回收就是回收内存的过程, 我们都知道其实 JVM 就是个 Java 进程, 但是一个进程会持有很多的硬件资源, 如 CPU, 内存, 硬盘以及带宽资源; 但是系统的内存总量是一定的, 并且内存还要同时给很多的进程来使用, 例如我们电脑是16G内存,它是一定的,程序在使用内存的时候必须先申请才能使用, 用完之后还要记得释放掉.
在这里插入图片描述

5.2 为什么要进行垃圾回收

 从代码编写的角度来看, 内存的申请时机是非常明确的, 但是内存的释放时机有些时候却是不太明确的, 这个时候就给内存的释放带来了一些困难, 典型的问题就是: 这个内存我是否还要继续使用? 关于内存泄露这件事一旦出现是比较难调试的, 我们很难找到是哪个地方出现了内存泄露; 当出现在生产环境中时, 如果不能第一时间暴露出问题, 而是逐渐积累达到一定程度后, 这时候爆发出来将会造成毁灭性的问题, 因此有效的垃圾回收是非常有必要的!

5.3 垃圾回收的优点/缺点是什么

优点: 可以更大程度的保证不出现内存泄露的情况, 但是需要注意的是这里不是 100% 保证, 当程序猿作死的时候 JVM 是救不了的!!!
缺点:
1) 需要消耗额外的系统资源;
2) 内存的释放可能存在延迟, 也就是说某个内存不用了可能不会第一时间去释放掉内存, 而是稍后释放;
3) 可能会出现 STW 问题.

5.4 垃圾回收要回收的内存有哪些

  1. 堆: 垃圾回收要释放的内容主要是在堆区;
  2. 方法区: 方法区里面大部分存的是"类对象", 通过类加载生成的, 对方法区进行垃圾回收就相当于是"类卸载";
  3. 栈 / 程序计数器: 内存都是和具体的线程绑定在一起的, 这两部分的内容都是自动释放的, 当代码块结束/线程结束的时候, 内存就自动释放了. 我们日常所说的垃圾回收主要是指堆上内存的回收,因为堆所占据的内存空间是最大的, 本身就占据了一个程序绝大部分的内存.

那么关于回收堆上的内容, 具体回收的是什么呢?
在这里插入图片描述
由上图可以看出, 针对堆上的对象主要分为三种情况:
(1) 完全使用的; 这里的肯定不是我们回收的对象.
(2) 完全不使用的; 这才是我们要回收的内存.
(3) 一半要使用, 一半不使用的: 这种情况下不能进行回收, 因为如果回收起来的话成本会比较大, Java 中的垃圾回收是以"对象"为基本单位, 一个对象要么被回收, 要么不被回收, 不会出现回收一般的情况.

5.5 垃圾回收到底是怎么回收的

垃圾回收的基本思想: 先找出垃圾, 然后再回收垃圾; 应遵循宁可放过, 也不能错杀. 上面我们也说过了回收的是再也不会被使用的对象, 如果正在使用的对象也回收了的话, 那程序猿还怎么"玩". 例如去看牙医, 医生说有两颗蛀牙, 但是呢有一颗蛀牙不是特别的确定要不要拔, 那么这时候肯定是先把确定的那颗蛀牙给拔了, 不确定的先留着. 相比于回收少了这样的问题而言, 回收多了显然是更严重的问题.
在这里插入图片描述

5.6 垃圾对象的判断算法

  对于 GC, 判断垃圾主要有下面这两种典型方案.

5.6.1 引用计数算法

 ==算法基本思想: 给对象增加一个引用计数器, 每当有一个地方引用它时, 计数器就 + 1; 当引用时效时, 计数器就 - 1; 任何时刻计数器为 0 的对象就是不能再被使用的对象.==举例如下:
在这里插入图片描述
引用计数的优缺点:
优点: 规则简单, 实现方便, 程序运行效率高效;
缺点:

  • 空间利用率比较低,如果一个对象很大, 在程序中对象数目也不多,此时引用计数完全可以; 如果一个很大的对象在里面加多个计数器也没有什么负担; 但是如果一个对象很小, 在程序中对象数据也很多, 此时引用计数就会带来不可忽视的空间开销.
  • 存在循环引用的问题, 在某些特殊的代码下, 循环引用会导致代码的引用计数判断出现问题, 从而无法回收.

下面我们举个例子来解释一下这个致命的缺点是怎么回事:
在这里插入图片描述
由于以上错误, 我们在 Java 中并没有使用引用计数这种方式来判定垃圾, 而是使用了下面的可达性分析.

5.6.2 可达性分析

5.6.2.1 核心思想

核心思想: 通过一系列称为 " GC Roots" 的对象作为起始点, 从这些节点开始向下搜索, 搜索走过的路径称之为 " 引用链", 当一个对象到 GC Roots 没有任何的引用链时, 则证明是不可用的.如下图所示:
在这里插入图片描述
JVM 中采取的方案就是: 在 JVM 中存在一个 / 一组线程来周期性的进行上述的遍历过程, 不断地找出不可达的对象, 然后由 JVM 进行回收.
关于这些对象, 包含下面这三种:

  1. 栈上的局部变量表中的引用;
  2. 常量池里面的引用指向的对象;
  3. 方法区中引用类型的静态成员变量.

基于上述过程, 便可以完成垃圾对象的标记, 和引用计数相比较, 可达性分析确实是复杂了很多, 同时实现可达性分析的遍历过程的开销也是比较大的; 但是带来的好处就是解决了引用计数的两个缺点: 内存上不需要消耗额外的空间, 也没有产生循环引用的问题.

5.6.2.2 有关四种引用

  不管是引用计数还是可达性分析, 其判定规则都是看当前对象是否有引用来指向, 也就是说都是通过引用来进行判定对象的生死的; 引用的诞生之前只是为了用来访问对象, 但是随着时代的发展, 引用也是可以用来判定对象的生死的, 但是也有的不可以判定, 主要有如下四种引用.

  1. **强引用: 这是我们日常使用的引用, 既能够访问对象, 也能决定对象的生死;**只要强引用还存在, 垃圾回收器永远不会回收掉引用的对象实例.
  2. 软引用: 能够访问对象, 但是只能一定程度的决定对象的生死, 也就是说 JVM 会根据内存是否富裕来自行决定; 对于软引用关联着的对象, 在系统将要发生内存溢出之前, 会把这些对象列入回收范围之中进行第二次回收, 如果这次回收还是没有足够的内存, 才会抛出内存溢出异常.
  3. 弱引用: 能够访问对象, 用来描述非必须对象的, 但是其强度要弱于软引用, 被弱引用关联的对象只能生存到下一次垃圾回收发生之前, 当垃圾回收器开始进行工作的时候, 无论当前内容是否够用, 都会回收掉只被弱引用关联的对象.
  4. 虚引用: 既不能找到对象, 也不能决定对象的生死, 只能在对象临被回收前进行一些善后的工作; 其也可以称为幽灵引用或者幻影引用, 也是最弱的一种引用关系, 一个对象是否有虚引用的存在, 完全不会对其生存时间构成影响, 也无法通过虚引用来取得一个对象实例, 为一个对象设置虚引用的目的就是能在这个对象被收集器回收时收到一个系统通知.

5.7 垃圾回收算法, 具体是怎么回收的

5.7.1 标记 - 清除算法

标记 - 清除算法是最基础的收集算法, 也就是说算法分为"标记"和"清除"两个阶段, 首先标记出所有要回收的对象, 在标记完成后统一回收所有被标记的对象, 后续的收集算法其实都是基于这种算法的思路.
在这里插入图片描述

  • 如上图所示, 灰色就是正在使用的对象, 黑色是要释放的对象, 白色是已经释放了的对象;
  • 虽然说以上的过程可以释放掉不用的内存空间, 但是却引入了额外的问题: 内存碎片; 也就是说空间的内存和正在使用的内存是交替出现的, 是无规律的, 因此如果再想要去申请一个小块内存还可以, 如果是要申请一个大块地连续内存, 此时就可能会分配失败; 毕竟大多数我们是要申请一块连续的内存空间;
  • 内存碎片问题如果一直累积下去, 就会出现系统看起来内存挺多的, 但是就是申请不了, 尤其是频繁申请释放的场景中尤为严重, 为了解决内存碎片这个问题, 复制算法就是一个很好的方案.

5.7.2 复制算法

复制算法将可用内存按容量划分为大小不等的两块, 每次只使用其中的一块; 当这块内存需要进行垃圾回收时, 会将此区域还存活的对象复制到另一块上面, 然后再把已经使用过的内存区域一次清理掉. 这样做的好处就是每次都是对整个半区进行内存回收, 内存分配的时候也就不需要考虑内存碎片等情况, 只需要移动堆顶指针, 按顺序分配即可.
其中: 黑色: 可回收; 白色: 未使用; 灰色: 存活对象
在这里插入图片描述
复制算法的缺点:
1) 可用的内存空间只有一半;
2) 如果要回收的对象比较少, 剩下的对象比较多, 那么复制的开销就会很大;

因此复制算法适用于对象会被快速回收, 并且整体内存不会很大的场景下.

5.7.3 标记 - 整理算法

  复制收集算法在对象存活率较高时会进行比较多的复制操作, 效率会变低; 而标记 - 整理算法可以解决内存空间利用率的问题;标记过程仍与" 标记 - 清除 "过程一致. 但后续步骤不是直接对可回收对象进行清理, 而是让所有存活对象都向一端移动, 然后直接清理掉端边界以外的内存. 其类似于 " 顺序表删除元素, 搬运 ", 这样的操作可以有效避免内存碎片, 同时提高内存的利用率.
在这里插入图片描述
缺点: 在这个搬运过程中, 也是一个很大的开销, 这个开销比复制算法里面复制对象的开销甚至更大.
实际实现垃圾回收算法, 要能够结合以上三种方式, 取长补短.

5.7.4 分代算法

5.7.4.1 分代算法概述及核心思想

  分代算法和上面所写的三种算法都有所不同, 分代算法是通过区域划分, 实现不同区域和不同的垃圾回收策略, 从而更好实现垃圾回收; 如果中国的一国两制方针, 对于不同的情况和地域设置更符合当地的规则, 从而实现更好的管理, 这就是分代算法的核心思想.
如何分代?
根据对象的" 年龄 ", 来对整个内存进行分类; 把年龄短的对象放在一起, 年龄长的对象放在一起; 不同年龄的对象可以采取不同的垃圾回收算法来进行处理. 在 JVM 中, 进行垃圾回收扫描也是周期性的, 这个对象每次经历了一个扫描周期, 就认为是年龄增长了一岁, 也就是说根据年龄的长短来对整个内存进行分类, 根据不同年龄的对象, 采取不同的垃圾回收算法来进行处理.
在这里插入图片描述

5.7.4.2 分代算法的过程

在这里插入图片描述

5.7.4.3 Minor GC 与 Full GC 的区别

  • Minor GC 又称之为新生代 GC, 指的是发生在新生代的垃圾收集; 由于 Java 对象大多都具备朝生夕灭的特性, 因此 Minor GC (采用的是复制算法)非常频繁, 一般回收速度也比较快.
  • Full GC 又称为老年代 GC 或者 Major GC, 指的是发生在老年代的垃圾收集, 出现了 Major GC, 经常会伴随至少一次的 Major GC, 其速度一般会比 Minor GC 慢上很多倍.
    在这里插入图片描述

开发者涨薪指南 深入浅出 JVM 详解 48位大咖的思考法则、工作方式、逻辑体系