乐趣区

一文了解JVM

一、什么是 JVM

JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

Java 语言的一个非常重要的特点就是平台无关性。而使用 Java 虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入 Java 语言虚拟机后,Java 语言在不同平台上运行时不需要重新编译。Java 语言使用 Java 虚拟机屏蔽了与具体平台相关的信息,使得 Java 语言编译程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java 虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是 Java 的能够“一次编译,到处运行”的原因。

二、JVM 总体概述

JVM 总体上是由类装载子系统(ClassLoader)、运行时数据区、执行引擎、垃圾收集这四个部分组成。其中我们最为关注的运行时数据区,也就是 JVM 的内存部分则是由方法区(Method Area)、JAVA 堆(Java Heap)、虚拟机栈(JVM Stack)、程序计数器、本地方法栈(Native Method Stack)这几部分组成。

三、JVM 体系结构

3.1 类装载子系统

Class Loader 类加载器负责加载.class 文件,class 文件在文件开头有特定的文件标示,并且 ClassLoader 负责 class 文件的加载等,至于它是否可以运行,则由 Execution Engine 决定。

3.2 运行时数据区

栈管运行,堆管存储。JVM 调优主要是优化 Java 堆和方法区。

3.2.1 方法区(Method Area)

方法区是各线程共享的内存区域,它用于存储已被 JVM 加载的类信息、常量、静态变量、运行时常量池等数据。

3.2.2 Java 堆(Java Heap)

Java 堆是各线程共享的内存区域,在 JVM 启动时创建,这块区域是 JVM 中最大的,用于存储应用的对象和数组,也是 GC 主要的回收区,一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:新生代、老年代、永久代。

说明:

  • Jdk1.6 及之前:常量池分配在永久代。
  • Jdk1.7:有,但已经逐步“去永久代”。
  • Jdk1.8 及之后:无永久代,改用元空间代替(java.lang.OutOfMemoryError: PermGen space, 这种错误将不会出现在 JDK1.8 中)。

3.2.3 Java 栈(JVM Stack)

1) 栈是什么

Java 栈是线程私有的,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就 Over,生命周期和线程一致。基本类型的变量和对象的引用变量都是在函数的栈内存中分配。

2) 栈存储什么

每个方法执行的时候都会创建一个栈帧,栈帧中主要存储 3 类数据:

  • 局部变量表:输入参数和输出参数以及方法内的变量;
  • 栈操作:记录出栈和入栈的操作;
  • 栈帧数据:包括类文件、方法等等。

3) 栈运行原理

栈中的数据都是以栈帧的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在栈中从入栈到出栈的过程。

4) 本地方法栈(Native Method Stack)

本地方法栈和 JVM 栈发挥的作用非常相似,也是线程私有的,区别是 JVM 栈为 JVM 执行 Java 方法(也就是字节码)服务,而本地方法栈为 JVM 使用到的 Native 方法服务。它的具体做法是在本地方法栈中登记 native 方法,在执行引擎执行时加载 Native Liberies. 有的虚拟机(比如 Sun Hotpot)直接把两者合二为一。

5) 程序计数器(Program Counter Register)

程序计数器是一块非常小的内存空间,几乎可以忽略不计,每个线程都有一个程序计算器,是线程私有的,可以看作是当前线程所执行的字节码的行号指示器,指向方法区中的方法字节码(下一个将要执行的指令代码),由执行引擎读取下一条指令。

6) 运行时常量池

运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。相较于 Class 文件常量池,运行时常量池更具动态性,在运行期间也可以将新的变量放入常量池中,而不是一定要在编译时确定的常量才能放入。最主要的运用便是 String 类的 intern()方法。

3.3 执行引擎(Execution Engine)

执行引擎执行包在装载类的方法中的指令,也就是方法。执行引擎以指令为单位读取 Java 字节码。它就像一个 CPU 一样,一条一条地执行机器指令。每个字节码指令都由一个 1 字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。

不过 Java 字节码是用一种人类可以读懂的语言编写的,而不是用机器可以直接执行的语言。因此,执行引擎必须把字节码转换成可以直接被 JVM 执行的语言。字节码可以通过以下两种方式转换成合适的语言:

  • 解释器:一条一条地读取,解释并执行字节码执行,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行语言的一个缺点。
  • 即时编译器:用来弥补解释器的缺点,执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行。执行本地代码比一条一条进行解释执行的速度快很多,编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。

3.4 垃圾收集(Garbage Collection, GC)

3.4.1 什么是垃圾收集

垃圾收集即垃圾回收,简单的说垃圾回收就是回收内存中不再使用的对象。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而未使用中的对象(未引用对象),则没有被任何指针给指向,因此占用的内存也可以被回收掉。

垃圾回收的基本步骤分两步:

  • 查找内存中不再使用的对象(GC 判断策略)
  • 释放这些对象占用的内存(GC 收集算法)

3.4.2 GC 判断策略

1) 引用计数算法

引用计数算法是给对象添加一个引用计数器,每当有一个引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器都为 0 的对象就是不可能再被使用的对象。缺点:很难解决对象之间相互循环引用的问题。

2) 根搜索算法

根搜索算法的基本思路就是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(也就是说从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。

在 Java 语言里,可作为 GC Roots 的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量应用的对象;
  • 本地方法栈中 JNI(Native 方法)引用的对象。

注:在根搜索算法中不可达的对象,也并非是“非死不可”的,因为要真正宣告一个对象死亡,至少要经历两次标记过程:第一次是标记没有与 GC Roots 相连接的引用链;第二次是 GC 对在 F -Queue 执行队列中的对象进行的小规模标记 (对象需要覆盖 finalize() 方法且没被调用过)。

3.4.3 GC 收集算法

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

标记 - 清楚算法采用从根集合(GC Roots)进行扫描,首先标记出所有需要回收的对象(根搜索算法),标记完成后统一回收掉所有被标记的对象。

该算法有两个问题:

  • 效率问题:标记和清除过程的效率都不高;
  • 空间问题:标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。

2) 复制算法(Copying)

复制算法是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将还存活的对象复制到另外一块上面, 然后把已使用过的内存空间一次清理掉。

3) 标记 - 整理算法(Mark-Compact)

标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动, 然后清理掉端边界以外的内存。

4) 分代收集算法(Generational Collection)

分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

新生代(Young Generation)的回收算法(以复制算法为主)

  • 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
  • 新生代内存按照 8:1:1 的比例分为一个 eden 区和两个 survivor(survivor0,survivor1)区。一个 Eden 区,两个 Survivor 区(一般而言)。大部分对象在 Eden 区中生成。回收时先将 eden 区存活对象复制到一个 survivor0 区,然后清空 eden 区,当这个 survivor0 区也存放满了时,则将 eden 区和 survivor0 区存活对象复制到另一个 survivor1 区,然后清空 eden 和这个 survivor0 区,此时 survivor0 区是空的,然后将 survivor0 区和 survivor1 区交换,即保持 survivor1 区为空,如此往复。
  • 当 survivor1 区不足以存放 eden 和 survivor0 的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次 Full GC(Major GC),也就是新生代、老年代都进行回收。
  • 新生代发生的 GC 也叫做 Minor GC,MinorGC 发生频率比较高(不一定等 Eden 区满了才触发)。

老年代(Tenured Generation)的回收算法(以标记 - 清除、标记 - 整理为主)

  • 在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
  • 内存比新生代也大很多(大概比例是 1:2),当老年代内存满时触发 Major GC 即 Full GC,Full GC 发生频率比较低,老年代对象存活时间比较长,存活率标记高。

永久代(Permanet Generation)的回收算法

用于存放静态文件,如 Java 类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class,例如 Hibernate 等,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。永久代也称方法区。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过根搜索算法来判断,但是对于无用的类则需要同时满足下面 3 个条件:

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例;
  • 加载该类的 ClassLoader 已经被回收;
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

3.4.4 垃圾收集器

1) Serial 收集器(复制算法)

新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是 client 级别默认的 GC 方式,可以通过 -XX:+UseSerialGC 来强制指定。

2) Serial Old 收集器(标记 - 整理算法)

老年代单线程收集器,Serial 收集器的老年代版本。

3) ParNew 收集器(停止 - 复制算法)

新生代多线程收集器,其实就是 Serial 收集器的多线程版本, 在多核 CPU 环境下有着比 Serial 更好的表现。

4) Parallel Scavenge 收集器(停止 - 复制算法)

新生代并行的多线程收集器,追求高吞吐量,高效利用 CPU。吞吐量一般为 99%,吞吐量 = 用户线程时间 /(用户线程时间 +GC 线程时间)。适合后台应用等对交互相应要求不高的场景。是 server 级别默认采用的 GC 方式,可用 -XX:+UseParallelGC 来强制指定,用 -XX:ParallelGCThreads= 4 来指定线程数。

5) Parallel Old 收集器(停止 - 复制算法)

老年代并行的多线程收集器,Parallel Scavenge 收集器的老年代版本,并行收集器,吞吐量优先。

6) CMS(Concurrent Mark Sweep)收集器(标记 - 清除算法)

CMS 收集器是一种以获取最短回收停顿时间为目标的收集器,CMS 收集器是基于“标记 – 清除”(Mark-Sweep)算法实现的,整个过程分为四个步骤:

  • 初始标记:标记 GC Roots 能直接关联到的对象,速度很快;
  • 并发标记:进行 GC Roots Tracing 的过程;
  • 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但比并发标记时间短;
  • 并发清除:整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

    • 优点:并发收集、低停顿
    • 缺点:对 CPU 资源非常敏感、无法处理浮动垃圾、产生大量空间碎片。

7) G1(Garbage First)收集器(标记 - 整理算法)

G1 是一款面向服务端应用的垃圾收集器,是基于“标记 - 整理”算法实现的,与其他 GC 收集器相比,G1 具备如下特点:

  • 并行与并发
  • 分代收集
  • 空间整合
  • 可预测性的停顿

G1 运作步骤:

  • 初始标记(stop the world 事件,CPU 停顿只处理垃圾)
  • 并发标记(与用户线程并发执行)
  • 最终标记(stop the world 事件,CPU 停顿处理垃圾)
  • 筛选回收(stop the world 事件,根据用户期望的 GC 停顿时间回收)

3.4.5 垃圾收集结构图

作者:郭晓利

来源:宜信技术学院

退出移动版