Java 虚拟机

概述

JDK 包含了 Java 语言、Java 虚拟机和 Java API 类库三部分,是 Java 程序开发的最小环境。
JRE 包含了 Java API 中的 Java SE API 子集和 Java 虚拟机两部分,是 Java 程序运行的标准环境。

Java 虚拟机家族

包括 HotSpot VM、J9 VM 和 Zing VM 等。

Java 虚拟机执行流程

分为两大部分,编译时环境和运行时环境,当一个 Java 文件经过 Java 编译器编译后生成 Class 文件,这个文件由 Java 虚拟机处理。
Java 虚拟机与 Java 语言没有什么必然的联系,它只与特定的二进制文件:Class 文件有关。


Java 虚拟机结构

抽象的 JVM 结构包括:

  • 运行时数据区域:
  • 方法区:线程共享
  • Java 堆:线程共享
  • Java 虚拟机栈:线程私有
  • 本地方法栈:线程私有
  • 程序计数器:线程私有
  • 执行引擎:
  • 即时编译器:线程共享
  • 垃圾回收器:线程共享
  • 本地库接口:线程共享
  • 本地方法库:线程私有
  • 其中类加载子系统并不属于 JVM 的内部结构:线程私有

Class 文件格式

Java 文件被编译后生成 Class 文件,这种二进制格式文件不依赖于特定的硬件和操作系统。每一个 Class 文件中都对应着唯一的类或者接口的定义信息,但是类或接口并不一定定义在文件中,比如它们可通过类加载器来直接生成。

类的生命周期

一个 Java 文件被加载到 Java 虚拟机内存中到从内存中卸载的过程,被称为类的生命周期。

类的生命周期包括:加载、链接、初始化、使用和卸载。

广义上类的加载包括:

  • 加载:查找并加载 Class 文件。
  • 根据特定名称查找类或接口类型的二进制字节流。(由类加载子系统完成)
  • 将这个二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
  • 链接:
  • 验证:确保被导入类型的正确性。
  • 准备:为类的静态字段分配字段,并用默认值初始化这些字段。
  • 解析:虚拟机将常量池内的符号引用替换为直接引用。
  • 初始化:将类变量初始化为正确初始值。

类加载子系统

类加载子系统通过多种类加载器来查找和加载 Class 文件到 Java 虚拟机中,Java 虚拟机有两种类加载器:

  • 系统加载器:包括三种
  • Bootstrap ClassLoader(引导类加载器):用 C++ 代码实现,用于加载指定 JDK 核心类库。
  • Extensions ClassLoader(拓展类加载器):用于加载 Java 扩展类,提供除了系统类之外的额外功能。
  • Application ClassLoader(应用程序类加载器):又称作 System ClassLoader(系统类加载器),因为它可以通过 ClassLoader 的 getSystemClassLoader 方法获取到。
  • 自定义加载器:通过继承 java.lang.ClassLoader 类的方式来实现自己的类加载器。

运行时数据区域

  • 程序计数器:又称 PC 寄存器,用以确保线程切换后能恢复到正确的执行位置。
  • Java 虚拟机栈:生命周期与线程相同,其内存储线程中 Java 方法调用的状态,并且包含多个栈帧。
  • 本地方法栈:与 Java 虚拟机栈类似,用来支持 Native 方法的。
  • Java 堆:用以存储对象实例并被垃圾收集器管理。
  • 方法区:用以存储已经被 jvm 加载的类的结构信息。
  • 运行时常量池:可理解为类或接口的常量池的运行时表现形式。(常量池指用来存放编译时期生成的字面量和符号引用)

对象的创建

当虚拟机接收到一个 new 指令时,会有如下操作:

  • 判断对象对应的类是否加载、链接和初始化
  • 为对象分配内存
  • 处理并发安全问题
  • 初始化分配到的内存空间
  • 设置对象的对象头
  • 执行 init 方法进行初始化

当虚拟机收到一个 new 指令时,首先会判断对象对应的类是否加载,链接和初始化,然后为对象分配内存并处理并发安全问题,然后初始化分配到的内存空间,为对象设置对象头,最后执行 init 方法进行初始化。

类的加载过程,Person p = new Person(); 为例进行说明。

  • 因为new用到了Person.class,所以会先找到Person.class文件,并加载到内存中;

  • 执行该类中的static代码块,如果有的话,给Person.class类进行初始化;

  • 在堆内存中开辟空间分配内存地址;

  • 在堆内存中建立对象的特有属性,并进行默认初始化;

  • 对属性进行显示初始化;

  • 对对象进行构造代码块初始化;

  • 对对象进行与之对应的构造函数进行初始化;

  • 将内存地址付给栈内存中的 p 变量

首先查找并加载 person.class 文件,然后执行该类中的静态代码块,并初始化 person.class 类。然后在堆内存中开辟空间分配内存地址,再经过一系列的初始化过程(特有属性,构造代码块,构造函数),最后将内存地址付给栈内存中的 p 变量。


对象的堆内存布局

以 HotSpot 虚拟机为例,分三个区域:

  • 对象头(Header):包括两部分信息。
  • Mark World:用于存储对象运行时的数据。
  • 元数据指针:用于指向方法区中目标类的元数据,通过它可以确定对象的具体类型。
  • 实例数据(Instance Data):用于存储对象中的各种类型的字段信息(包括从父类继承来的)。
  • 对齐填充(padding):不一定存在,起到了占位符的作用,没有特别的含义。

以 HotSpot 为例,分三个区域。对象头,实例数据和对其填充。其中对象头包括两部分信息,Mark World 和元数据指针,Mark World 用来存储其运行时数据,元数据指针用来确定其具体类型,实例数据用来存储对象各种类型的字段信息,对其填充不一定存在,起到占位符的作用,没有实际意义。


oop-klass 模型

用来描述 Java 对象实例的一种模型,分为两个部分:

  • OOP:指的是普通对象指针,用来表示对象的实例信息。
  • klass:用来描述元数据。

垃圾标记算法

垃圾收集器,通常被称作 GC。其主要做了两个工作:

  • 内存的划分和分配
  • 对垃圾进行回收

以我的理解简单的解释下,GC就是垃圾回收机制,分为检测垃圾和回收垃圾两步。

检测垃圾两种方法:引用计数法(无法处理循环引用的问题)。可达性分析算法(重点看这个),可以想象为一个有向图,以根集对象为起始点,如果对象不可达,则是垃圾对象。(接下来为个人理解,不知是否正确)比如,java以main为入口,其中创建了两个对象A和B,则在堆内存中,给A对象实例和B对象实例分配内存,在栈内存中持有他们的引用变量。这时有向图为:mian->A(堆)->A(栈),mian->B(堆)->B(栈)。将对象A赋值给对象B,即0bjectB=objectA,则B的引用变量也指向了A(堆),不再指向B(堆)。这时B(堆)则是自己孤零零的呆在堆内存中,没有引用变量指向他,也就意味着B(堆)和main是不通的(也就是不存在main入口到他的持续指向箭头),B(堆)就叫做对象不可达,可以被回收。

为什么说长生命周期持有短生命周期的对象,会导致短生命周期对象无法释放。GC回收是通过对象是否可达来判断,然后长生命周期对象持有短生命周期的对象,就会导致对象一直是可达的。。以上面对象A和对象B为例,引用变量A和引用变量B都指向堆内存中的A对象实例,这时就有两个连通的线路;mian->A(堆)->A(栈),mian->B(堆)->A(栈)。所以,即使A对象本身的生命周期走完了,可B对象的生命周期还没走完,依然还会保有对A对象实例的引用。即使第一条线路断开了,第二条还是连通的,则A对象还是可达的,无法被GC回收。

Java 中的引用

  • 强引用:当新建一个对象时就创建了一个具有强引用的对象,即使 OOM 它也不会被 GC 回收。
  • 软引用:内存不够时,会回收这些对象的内存。
  • 弱引用:不管当前内存是否足够,只要被发现就会回收它的内存。
  • 虚引用:与没有任何引用一样,任何时候都可能被 GC 回收,但被回收时会收到一个系统通知。

引用计数法

其基本思想就是每个对象都有一个引用计数器,当对象在某处被引用时,它的引用计数器就加 1,引用失效就减 1。当值为 0,则该对象就不能被使用,变成了垃圾。

它没有解决对象之间相互循环引用的问题。

根搜索算法

其基本思想就是选定一些对象作为 GC Roots,并组成根对象集合,然后以这些 GC Roots 的对象作为起始点,向下搜索,如果目标对象到 GC Roots 是连接着的,就称该目标对象是可达的,如果目标对象不可达则说明目标对象是可以被回收的对象。

垃圾标记算法有引用计数法和根搜索算法。引用计数法指每个对象都有一个引用计数器,引用成功加一,引用失败减一,当为零时,则成为了垃圾。但此算法不能解决对象循环引用的问题。所以可使用根搜索算法解决,又叫可达性分析算法,指可选用一些对象作为根节点,从根节点到引用对象如果是连接着的,就代表是可达状态,否则为不可达状态可被回收。


Java 对象在虚拟机中的生命周期

在 Java 对象被类加载器加载到虚拟机中后,Java 对象在 JVM 中有 7 个阶段:

  1. 创建阶段
  2. 应用阶段
  3. 不可见阶段
  4. 不可达阶段
  5. 收集阶段
  6. 终结阶段
  7. 对象空间重新分配阶段

垃圾收集算法

标记-清除算法

它将垃圾收集分两个阶段:

  • 标记阶段:标记出可以回收的对象。
  • 清除阶段:回收被标记的对象所占用的空间。

它主要有两个缺点:

  • 标记和清除的效率都不高
  • 容易产生大量不连续的内存碎片

复制算法

为了解决标记-清除算法的效率不高的问题。其广泛应用于新生代中。

它把内存空间划分为两个相等的区域,每次只使用其中一个区域。在垃圾收集时,遍历当前使用的区域,把存活对象复制到另一个区域中,最后将当前使用的区域的可回收的对象进行回收。

标记-压缩算法

它被广发应用于老年代中,在标记可回收对象后将所有存活的对象压缩到内存的一端,使它们紧凑地排列在一起,然后对边界以外的内存进行回收,回收后,已用和未用的内存都各自一边。

分代收集算法

Java 堆区基于分代的概念,分为:

  • 新生代:又细分为

    • Eden 空间

    • From Survivor 空间

    • To Survivor 空间

  • 老年代

分代收集
根据 Java 堆区的空间划分,垃圾收集的类型分为两种:

  • Minor Collection:新生代垃圾收集
  • Full Collection(Major Collection):老年代垃圾收集,通常情况下会伴随至少一次的 Minor Collection,它的收集频率较低,耗时较长。

垃圾清除算法有标记清除,复制,标记压缩和分代收集算法。标记清除算法会将垃圾回收分为标记阶段和清除阶段,标记阶段会将可回收对象进行标记,而清除阶段则将被标记的对象所占空间进行回收。它的缺点是效率不高,并且会造成大量不连续的内存碎片。复制算法是将内存空间分为相等两份,每次使用其中的一份区域,垃圾收集时会将存活对象复制到另外一份空间中,并对此空间可回收对象进行回收,它被广泛应用于新生代中。标记压缩是将存活对象压缩到内存的一角,然后对边界外空间进行垃圾回收,它被广泛应用于老年代中。分代收集算法是指根据 java 堆的分代空间划分,然后对相应空间采取相应的垃圾清除算法。


备注

参考资料:
Android 进阶解密