JVM 结构与垃圾回收算法原理

JVM 概述

JVM 结构

JVM(Java Virtual Machine)是用于运行 Java 字节码的虚拟机,由 类加载器子系统(Class Loader Subsystem)、运行时数据区(Runtime Data Area)、执行引擎和本地接口库(Navite Interface Library)。本地接口库调用本地方法库(Navicat Method Library)与操作系统进行交互。

JVM 结构.png

JVM 运行机制

Java 源文件被编译器编译成字节码文件(.Class 文件),JVM 将字节码文件编译成相应操作系统的机器码,机器码调用相应操作系统的本地方法库执行相应的方法。

JVM 内存区域

JVM 内存区域分为线程私有区域(程序计数器、虚拟机栈、本地方法区)、线程共享区域(堆、方法区)和直接内存。

直接内存

也叫做堆外内存,在并发编程中经常使用,可以避免在 Java 堆和 Native 堆中来回复制数据带来的资源浪费

如:JDK 的 NIO 模块通过调用本地方法 Native 函数库直接在操作系统上分配堆外内存,然后直接使用 DirectByteBuffer 对象作为这块内存的引用对内存进行操作。在一些高并发框架(Netty、Flink、HBase、Hadoop)都有用到堆外内存。

程序计数器:线程私有,无内存溢出问题

一块很小的内存区域,用于存储当前运行的线程和所执行的字节码的行号指示器。

虚拟机栈:线程私有,描述 Java 方法的执行过程

描述 Java 方法的执行过程的内存模型,它在当前栈帧中存储了局部变量表、操作数栈、动态链接、方法出口、部分运行时数据、处理动态链接(Dynamic Linking)方法的返回值和异常分派(Dispatch Exception)。

栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机中的入栈和出栈。

本地方法栈:线程私有

与虚拟机栈类似,本地方法区栈为 Native 方法服务。

堆:线程共享

也叫运行时数据区,JVM 创建对象和产生数据都被存储在堆中,堆是被线程共享的内存区域。是垃圾回收的主要区域,从 GC 角度还可以细分为:新生代、老年代和永久代。

方法区:线程共享

也叫永久代,用于存储常量、静态变量、类信息、即时编译其编译后的机器码、运行时常量池。

在类信息中包含了类的版本、字段、方法、接口、常量信息等。

JVM 的运行时内存(JVM 堆)

从 GC 角度可以将 JVM 堆分为新生代、老年代和永久代。其中新生代默认占 1/3,老年代默认占 2/3,永久代占非常少的堆内存空间。

堆内存结构.png

新生代

JVM 新创建的对象都会先保存在新生代中,因此新生代会频繁的触发 MinorGC,新生代又分为 Eden 区、SurvivorFrom 区和 SurvivorTo 区。

MinorGC 采用复制算法实现,具体过程如下:

  1. 将 Eden 区和 SurivivorFrom 区中存活的对象复制到 SurvivorTo,将符合条件的对象复制到老年区(条件:年龄达到 XX:MaxTenuringThreshold 设置,或者属于大对象)
  2. 清空 Eden 区和 SurivivorFrom 区中的对象。
  3. 将 SurvivorTo 区和 SurivivorFrom 互换,原来的 SurvivorTo 成为下一次 GC 时的 SurivivorFrom。

老年代

老年代存放长生命周期的对象和大对象。老年代的 GC 过程叫做 MajorGC。在 MinorGC 后出现老年代对象且老年代空间不足没有连续的空间分配给大对象,会触发 MajorGC。

MajorGC 采用标记清除算法,该算法首先会标记所有存活的对象,然后回收未被标记的对象。

在没有内存空间可以分配给老年代时会抛出 Out Of Memory 异常。

永久代

永久代指内存永久保存的区域,主要存放 Class 和 Meta(元数据)的信息。GC 不会对永久代进行垃圾回收,当加载的类过多时会抛出 Out of Memory 异常。

注意:Java8 以后永久代已经被数据区(元空间)取代,元空间与永久代的区别在于元空间没有使用虚拟机的内存,而是直接使用操作系统的本地内存。因此元空间的大小不受 JVM 内存限制。

垃圾回收与算法

确定可回收对象

GC Roots 是指一组必须活跃的引用,例如:前所有正在被调用的方法的引用类型的参数/局部变量/临时值。

常用的垃圾回收算法

分为标记和清除两个步骤,在标记阶段标记所有需要回收的对象;在清除阶段清除所有可回收的对象并释放其所占用的内存空间。存在内存碎片化问题

标记清除算法.png

为了解决内存碎片化问题而设计的,首先将内存区域划分为两块大小相等的内存区域,新生成的对象都被存放在区域 1,当区域 1 存储满后对区域 1 进行一次标记,并将区域 1 仍存活的对象全部复制到区域 2,最后将区域一全部清除即可。存在内存浪费问题,对象在两个区域来回复制还会影响 系统运行效率

复制算法.png

结合前两种算法的优点,先标记对象,标记完成后将存活对象移动到内存的另一端,然后清除该端对象并释放内存

标记整理.png

前三种算法都堆所有类型的对象都进行回收,因此针对不同类型的对象 JVM 采用了不同的垃圾回收算法,该算法被称为分代收集算法。该算法根据对象的不同类型将内存划分为不同区域,堆内存分为新生代和老年代。

目前,大部分 JVM 在新生代采用复制算法,老年代采用标记整理算法

引用类型与垃圾回收

垃圾收集器

JVM 针对新生代和老年代分别提供了多种不同的垃圾收集器。针对新生代的有:Serial、ParNew、Parallel Scavenge;针对老年代有:Serial Old、Parallel Old、CMS;还有针对不同区域的 G1。

垃圾回收器 分区 线程数 算法
Serial 新生代 单线程 复制算法
ParNew 新生代 多线程 复制算法
Parallel Scavenge 新生代 多线程 复制算法
Serial Old 老年代 单线程 标记整理算法
Parallel Old 老年代 多线程 标记整理算法
CMS 老年代 多线程 标记清除算法
G1 针对不同区域 多线程 标记整理算法

拓展一些问题

Q:为什么 GC 要分代?

java 的 gc 为什么要分代? - 知乎 (zhihu.com)

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    2830 引用 • 8051 回帖 • 740 关注
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    154 引用 • 115 回帖 • 1 关注
1 操作
Tooi6 在 2020-11-23 17:03:46 更新了该帖

赞助商 我要投放

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...