【Java劝退师】JVM 知识脑图 - Java 虚拟机

1,937 阅读11分钟

JVM

JVM

Java 虚拟机

一、名词解释

  • JVM - Java Virtual Machine - Java 虚拟机 - 虚构出来的计算机,在真实计算机上仿真出各种计算机功能

  • JRE - Java Runtime Environment - Java 运行环境 - 提供运行 Java 程序需要的基本类库,比如 操作文档、连接网络

  • JDK - Java Development Kit - Java 开发工具包 - 提供 Java 开发时需要的小工具,比如 javac、jConsole

    JVM、JRE、JDK 为包含关系

    Java 进程(.java) → 使用 JDK 编译(javac) → Java 字节码(.class、.jar) → JVM 将 字节码 翻译成对应 操作系统(Windows、Linux、Mac) 的 机器代码

二、JVM 架构

1. 类装载子系统

  • 功能

    • 获取 .class 文档,将 类信息、常量池、静态变量 放入方法区,作为实例化对象时的模板
  • 类加载过程

    1. 加载

      • JVM 只有在第一次主动使用类的时候才会加载该类,不是一次把所有类都加载到内存中

      • 加载器分类

        • 启动类加载器 Bootstrap Class Loader
          • 加载 Java 核心类库,用于提供 JVM 自身需要的类 - rt.jar、resource.jar
          • 属于 引导类加载器,由 C/C++ 实现,其他加载器皆为 自定义加载器,由 Java 实现
        • 扩展类加载器 Extension Class Loader
          • 加载 jre/lib/ext 目录下的扩展包
        • 系统类加载器 System Class Loader
          • 进程中默认的类加载器
      • 类加载采用 双亲委派模型

        • 当类加载器收到类加载请求,不会自己先尝试加载这个类,而会把这个请求委派给父类加载器完成,当父类在自己搜索范围内找不到指定类时,子类加载器才会尝试去加载
        • 【目的】为了避免黑客串改关键类的代码
    2. 验证

      • 确保 .class 文档符合 JVM 要求,不会危害 JVM 安全
    3. 准备

      • 为【静态变量】设置初始值 0
    4. 解析

      • 将常量池内的 符号引用(对类、变量、方法的描述) 替换为 直接引用(直接指向目标的指针)
    5. 初始化

      • 运行 静态代码块

      • 为 静态变量 赋值

        静态代码块 与 静态变量赋值 的运行顺序,是依照语句的撰写顺序决定的

2. 运行时数据区

1. 进程计数器

  • 纪录当前线程需要运行的下一条指令的行号,负责控制 分支、循环、跳转、异常处理
  • 当运行的是 Native 方法时,存储的值是 undefined
  • 唯一一个没有规定任何 OutOfMemoryError 的区域

2. 虚拟机栈

  • 用于保存栈侦

    • 每个方法运行时都会创造一个栈侦

    • 栈侦内部保存 局部变量(表)、操作数栈、动态链接、方法返回地址

    • 局部变量(表)

      • 包含 8 种基本数据类型、对象引用、returnAddress
      • long、double 占用 2 个 局部变量空间(Slot),其余占用 1 个
    • 操作数栈

      • 提供 数值计算的中间空间

      数值计算流程 : 从 局部变量表 中,将常量或变量到入栈到 操作数栈,计算完成后,再将栈中元素出栈到 局部变量表 或 方法调用者

    • 动态链接

      • 指向 运行时常量池 该栈所属方法的 符号引用,方便调用实际方法
    • 方法返回地址

      • 存放调用该方法的 进程计数器 的值,用于返回给 进程计数器,继续运行后续程序
  • 使用 连续的内存空间

  • 配置参数

    • 每个线程的虚拟机栈大小 - -Xss - 【默认】1M

3. 本地方法栈

  • 与 虚拟机栈 类似,但保存的是 Native 方法的栈侦

以上皆为线程私有,线程间互不影响,与线程生命周期相同

4. 堆

  • 保存 对象实例 与 数组

  • 线程间共享

  • JVM 管理的内存中最大的一块

  • JVM 会尽可能将 堆空间 保持在尽量低的程度

  • 可以不使用连续的内存空间

  • 垃圾收集器主要管理的区域

  • 配置参数

    • 堆空间最小值 - -Xms
    • 堆空间最大值 - -Xmx
  • 分类

    • 年轻代

      • 回收频繁
      • 分为 伊甸园、幸存者0区、幸存者1区 - 默认比例 8 : 1 : 1
      • 当【伊甸园】区满时,将触发 Minor GC,并造成 Stop The World(STW),暂停所有用户线程
      • 每次只使用其中 1 个幸存者区,所以年轻代实际可用空间为 90%
      • 参数配置
        • 伊甸园 较 幸存者区 倍数 - -XX:SurvivorRatio=8
        • 放入老年代的累计回收存活次数 - -XX:MaxTenuringThreshold=15
    • 老年代

      • 回收频率较低
      • 年轻代中的对象,经过几次回收后仍存在,将放入老年代 -【默认】15 次
      • 老年代 与 青年代 默认比例 2 : 1
      • 内存不足将触发 Major GC(速度慢 Minor GC 10倍以上),如垃圾清理后空间仍不足,将报 OOM 异常
      • 参数配置
        • 老年代 较 青年代 倍数 - -XX:NewRatio=2

      【对象分配过程】

      ○ 新对象首先放在 伊甸园,当 伊甸园 满时进行垃圾回收(Minor GC),将 伊甸园 剩余对象放入 幸存者0区

      ○ 当再次触发回收,幸存者0区没被回收的对象,将被放入 幸存者1区

      ○ 再次触发回收,幸存者1区 没被回收的对象将放入 幸存者0区,以此类推

      ○ 直到累计次数 15 次,将对象放入 老年代

5. 方法区(元空间)

  • 保存 类信息、运行时常量池、静态变量

  • 类信息

    • Class 信息

    • Field 信息

    • Method 信息

      都会放在运行时常量池中

  • 运行时常量池

    • 常量池在运行时的表现形式

      字节码文档(.class)内部包含常量池,用于存放编译期间生成的 字面量 与 符号引用

  • 存在于本地内存(改善垃圾回收效率、避免拷贝损耗),空间可以不连续

  • 参数配置

    • 元空间初始大小 - -XX:MetaspaceSize=n - 【默认】 21M
    • 最大元空间大小 - -XX:MaxMetaspaceSize=n - 【默认】没有限制

三、异常

1. OutOfMemoryError

  • 原因
    • 加载数据过多 - 一次从数据库取出太多数据
    • 集合中对象引用过多,且使用完后没有清空
    • 发生 死循环 或 循环中产生过多对象
    • 堆内存分配不合理

2. StackOverFlowError

  • 原因
    • 超出线程允许请求的栈深度
      • 调用方法次数过多
      • 通常栈深度 1000 ~ 2000 都没问题

四、垃圾回收

  • 优点
    • 开发人员不用考虑内存管理
    • 有效防止内存泄漏
  • 缺点
    • 过度依赖垃圾回收,将降低开发人员解决 内存溢出 问题的能力

1. 回收算法

1. 判断对象以死

  • 引用计数算法

    • 当对象被引用时,计数值+1,引用失效时,计数值-1,数值为0表示对象已死
    • Java 中并没有使用此算法
    • 优点
      • 实现简单
      • 运行效率高
    • 缺点
      • 无法检测出循环引用
  • 可达性分析算法

    • 将 GC Roots 作为起点,搜索所有关联到的对象;搜索经过的路径被称为引用链,当一个对象不含有从 GC Roots 出发的引用链,将被视为不可达,认为该对象已死

    • GC Roots 类型

      • 局部变量表中的对象
      • static 对象
      • final 对象
      • Native 对象
      • 被 同步锁(synchronized) 持有的对象
    • JVM 判断流程

      1. 发现对象没有与 GC Roots 连接的引用链,会被第一次标记

      2. 若被标记对象是否覆写了 finalize() 方法,且没有被调用过,则将该对象放入 F-Queue 队列中,否则则直接回收

        finalize() 方法为 Object 中的一个垃圾回收方法,相当于一个免死金牌,但只能免死一次

      3. 对 F-Queue 中的对象进行第二次小规模标记,对象可以在 finalize() 方法中与引用链上的对象创建关联,避免被回收

2. 垃圾收集算法

  • 理论

    • 分代收集 - 根据对象的生命周期作划分,并进行分区管理
    • 弱分代假说 - 绝大多数对象都是朝生夕灭的
    • 强分代假说 - 熬过越多次垃圾收集的对象,越难以消灭
  • 标记-清除算法

    • 标记出所有需要回收的对象,然后统一进行回收
    • 缺点 - 产生大量的内存碎片
  • 标记-复制算法

    • 将存活对象复制到另一块内存,并将原使用的内存空间清理掉
    • 年轻代大多采用此算法,透过 幸存者0区 与 幸存者1区 的设立,可将空间浪费降为总年轻代的10%
    • 缺点
      • 需要预留一半的内存,浪费空间,导致GC频繁
      • 当存活对象较多时,复制成本增加,效率降低
      • 当大部分对象都是存活的(老年代),将无法使用这种算法
  • 标记-整理算法

    • 让存活对象向内存的一端移动,然后清理掉边界外的内存

2. 垃圾收集器

垃圾回收算法的具体实现

  • 性能指针
    • 吞吐量 : 用户代码时间 / (用户代码时间 + 垃圾收集时间)
    • 暂停时间 : 运行垃圾回收时,工作线程被暂停的时间
    • 内存占用 : Java 堆所占的内存大小
    • 收集频率 : 垃圾收集的频次

1. Serial / Serial Old 收集器

  • 单线程收集器,垃圾收集时必须暂停其他线程
  • 使用方式 : -XX:+UseSerialGC

2. ParNew 收集器

  • 多线程并行 的 年轻代 收集器

  • 使用方式

    • -XX:+UseParNewGC

    • -XX:ParallelGCThreads=线程数 - 【默认】CPU 核心数

3. Parallel Old 收集器

  • 多线程并行 的 老年代 收集器
  • 基于 标记-整理算法 实现
  • 适合 注重吞吐量、CPU资源敏感 的场景
  • 使用方式
    • -XX:+UseParallelOldGC

4. Parallel Scavenge 收集器

  • 吞吐量优先 的 年轻代 收集器

  • 并行多线程回收

  • Java 1.8 默认使用

  • 可自动调节年轻代中的 伊甸园、幸存者 比例

  • 适合 后台运算、交互不多的场景 - 批次任务、科学计算

  • 使用方式

    • -XX:+UseParallelGC

    • -XX:MaxGCPauseMillis=毫秒数 - 设置最大垃圾收集停顿时间

    • -XX:GCTimeRatio=n

      • 设置吞吐量,默认为 99
      • n 值范围为 1~99
    • -XX:ParllGCThreads=n

      • 设置年轻代线程数
      • 【默认】CPU核心数小于等于 8,与核心数相同;CPU 核心数大于 8,n值为 3 + (5 * CPU_COUNT ) / 8
    • -XX:+UseAdaptiveSizePolicy - 开启年轻代比例自适应调节

5. CMS 收集器

  • 以 最短垃圾收集停顿时间 为目标的 老年代 收集器

  • 基于 标记-清除算法 实现

  • 适合 互联网服务 场景

  • GC 流程

  1. 初始标记
    • 标记 GC Roots 直接关联到的对象,速度快
    • 将造成 Stop the World
  2. 并发标记
    • 从 GC Roots 直接关联到的对象遍历所有关联对象
    • 过程耗时,但不需暂停用户线程
  3. 重新标记
    • 修正并发标记期间,因用户线程继续运行,导致标记错误的纪录
    • 将造成 Stop the World
    • 运行时间稍长,但小于并发标记时间
  4. 并发清除
    • 删除判断已经死亡的对象
  • 缺点

    • 在 并发标记 阶段,不会因占用用户线程导致停顿,但会导致进程变慢,导致总吞吐量降低 - 在 CPU 核心数不足 4 个时,影响大
    • 无法处理 浮动垃圾,可能导致 Full GC 发生
    • 基于 标记-清除算法,将产生 内存碎片
  • 使用方式

    • -XX:+UseConcMarkSweepGC
    • -XX:CMSFullGCsBeforeCompaction=n
      • 运行 Full GC 若干次就进行碎片整理

      • 【默认】0次

6. G1 收集器

  • 年轻代、老年代 皆可使用的垃圾收集器

  • 满足低 GC 停顿时间 与 高吞吐量 特征

  • 针对多核 CPU 与 大容量内存 机器

  • 将 内存 划分成约 2000 个独立的 Region,依旧保留分代思想,但不再是物理隔离

  • Region 大小为 2 的 N 次幂,1MB、2MB、4MB、8MB、16MB、32MB

  • 增加 Humongous 内存区域,用于保存超过 1.5 个 Region 的大对象,视为老年代

  • 整体采 标记-整理算法;局部采 标记-复制算法 - 不会产生内存碎片

  • 跟踪 Region 中垃圾的价值大小,维护一个优先列表,优先回收价值高的区域

  • GC 流程

    1. 初始标记
      • 只标记与 GC Roots 直接关联的对象
      • 造成 Stop the World
    2. 并发标记 : 从 GC Roots 直接关联到的对象遍历所有关联对象
    3. 最终标记
      • 修正并发标记期间,因用户继续运行,导致标记错误的纪录
      • 造成 Stop the World
    4. 筛选回收
      • 根据时间进行价值最大化收集
      • 造成 Stop the World
  • 使用方式

    • -XX:+UseG1GC
    • -XX:MaxGCPauseMillis=200 - 期望最大 GC 停顿时间
    • -XX:InitiatingHeapOccupancyPercent=n
      • 当老年代占整堆大小百分比达到域值n,则进行 Mixed GC

      • 【默认】45