Java 虚拟机 ( 简读版 )

1. 背景

本文聊聊Java 虚拟机的一些知识。

2.知识

Java Virtual Machine(Java虚拟机,简称JVM)是一个抽象的计算机器。像真正的计算机器一样,它有一个指令集并在运行时操纵各种内存区域。

拆分几个过程来理解概貌

JVM 使得 Java程序 与 操作系统(及硬件)保持独立性。JVM 并不依赖 Java 编程语言 ,它只知道一种特定的二进制格式,即class文件格式。一个class文件包含着Java虚拟机指令(或字节码)和符号表,以及其它辅助信息。

拆分几个过程来理解:

  • 1、Java 语言写的 Java 文件,比如 xxx.java 文件 经过编译后 变成 class 文件格式的 “字节码” 文件。
  • 2、字节码文件经过压缩后被打包成一个 jar 文件。
  • 3、Jar 文件可被分发到 安装了JVM的 Windows 或者 Linux 的计算机上,Jar包内的 class字节码 被传入 JVM 。JVM负责 将 字节码 解释成具体的机器指令来执行。

JVM 具体可以由 ”JVM 规范“ 定义,有很多厂商提供了JVM的实现。比如:Oracle 的 JVM 之一名为 HotSpot,另一个继承自BEA Systems 的是JRockit。Android 使用了 Dalvik虚拟机,ART虚拟机。

3. JVM 核心组成部分(三大项)

也就是说 JVM 有这些组成部分:

  • 1、它有指令集 (即:字节码 )
  • 2、它有 指令解释器 来 解析指令。 (即:字节码执行器和JIT)
  • 3、它可以操作 内存区域,以装载和执行。(即:操作内存 )

重点这三项,我们慢慢讲。

3.1、字节码指令

JVM 具有针对以下任务组的字节码指令规范:

  • 加载和存储
  • 算术
  • 类型转换
  • 对象创建和操作
  • 操作数栈管理(push/pop)
  • 控制转移(分支)
  • 方法调用和返回
  • 抛出异常
  • 基于监视器的并发

这些指令是一些操作任务,被加载到JVM后可以被执行。

3.2、字节码解释器和即时编译器

(1) 字节码解释器 ( bytecode interpreter )
字节码解释器用于将字节码解析成计算机能执行的语言,一台计算机有了 Java 字节码解释器后,它就可以运行任何 Java 字节码程序。同样的 Java 程序就可以在具有了这种解释器的硬件架构的计算机上运行,实现了“跨平台”。

(2) 即时编译器 ( just-in-time compiler,JIT )
JIT 编译器可以在执行程序时将 Java 字节码翻译成本地机器语言

一般来讲,Java 字节码经过 字节码解释器执行时,执行速度总是比编译成本地机器语言的同一程序的执行速度慢。而 即时编译器 在执行程序时将 Java 字节码翻译成本地机器语言,以显著加快整体执行时间。

3.3、虚拟机架构

JVM 操作内存有这些:

  • JVM 有一个堆( heap )用于存储对象和数组。垃圾回收器要在这里工作。

  • 代码、常量和其他类数据存储在方法区( method area )中。

  • 每个 JVM 线程也有自己的调用栈( JVM stack ),用于存储 “帧”。每次调用方法时都会创建一个新的 帧(放到栈里),并在该方法退出时销毁该帧。

  • 每个提供一个操作数堆栈 ( operand stack)和一个局部变量数组 ( local variables )。操作数栈用于计算操作数和接收被调用方法的 "返回值",而局部变量数据用于传递“方法参数”。

为了兼容性。每个特定的主机操作系统都需要自己的 JVM 和运行时实现。虽然实际实现可能不同,但是 在语义上都以相同的方式解释字节码。

还与重要的垃圾回收机制,我们后面再讲。

3.4 类加载器 ( Class loader )

JVM 字节码的基本单位是。类加载器 ( Class loader ) 用于识别和加载符合 Java的 Class 文件格式的内容。

类加载器按顺序执行下面三个活动:

  • 1)加载( Loading ):查找和导入二进制数据内容
  • 2)链接( Linking ):执行下面三个子步骤
    -- 2.1) 验证(Verification):确保导入内容的正确性
    -- 2.2 )准备(Preparation):为类变量分配内存,并将内存初始化为默认值
    -- 2.3) 解析(Resolution):将符号引用(symbolic references)转换为直接引用(direct references)。
  • 3)初始化( Initialization ):调用Java 代码 ,将类变量初始化为其正确起始值 。

一般来说,有两种类型的类加载器:

  • 1、引导类加载器
  • 2、用户定义的类加载器。

每个 Java 虚拟机实现都必须有一个引导类加载器 ( bootstrap class loader ),能够加载受信任的类,以及一个扩展类加载器或应用程序类加载器 ( application class loader)。

3.5 JVM 语言

有很多种 JVM 语言可以选择,比如 Groovy、Scala和 Kotlin 等,这些语言编写的代码都可以被编译成 字节码后 在JVM 上运行。

4. 垃圾回收器( Garbage Collection, GC )

垃圾收集( GC ) 是一种自动内存管理形式。作用是将内存中不再被使用的对象进行回收,由垃圾收集器执行“垃圾收集策略”执行回收工作。

也就是有这两点:

  • 垃圾回收策略
  • 按“垃圾回收策略”实现的“垃圾回收器”

下面再分别描述这两点。

4.1 常见的垃圾回收策略

(1) 什么样的对象需要被回收?
判断对象是否存活一般有两种方式:

  • 引用计数( reference-counting ):每个对象都有一个引用计数器,每次被引用时计数加1,引用失效一次则计数减1,计数为0时意味着可以被回收。此方法简单,无法解决对象相互循环引用的问题。

  • 可达性分析( GC roots tracing ):从GC Roots开始向下搜索,搜索所走过的路径中能连通到达的对象是可用对象,无法到达的为不可用对象,即可回收。

(2) 垃圾回收的算法/策略
1、“标记-清除" 算法 (mark-sweep)
分 “标记” 和 “清除” 两个阶段:首先标记处所需要回收的对象,在标记完成后统一回收所有被标记的对象。

标记-清除

可以看到,产生了大量不连续的内存碎片。

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


复制算法

代价是可用区域缩小为原来的一半。

3、“标记-压缩" 算法 (mark-compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存


标记-压缩

4、分代收集策略
HotSpot 是 Oracle 一个JVM ,为了加速代码执行,它使用了分代堆 ( generational heap )。

它根据对象存活周期的不同将 '内存堆 (Heap)' 分为:

  • 新生代 :新产生的对象优先进去
  • 老年代:新生代存活久的放到这里,大对象则直接进入老年代。
  • 永久代 : 方法区称之为永久代,它基本不会变。

这样就可以根据各个年代的特点采用不同的收集算法,比如 针对新生代的区域使用标记-清除算法,针对老年代区域使用 标记-压缩算法。实际不同的JVM实现也有多种方式组合。

(在新的版本中已经将永久代废弃,引入了元空间的概念,永久代使用的是JVM内存而元空间直接使用物理内存)。

新生代中的对象在每次GC时都会有大量对象被回收,少量存活。老年代中的对象存活率较高。

新生代又分为三个区:

  • Eden区
  • Survivor区(Survivor from、Survivor to)

在分配时:

  • 新产生的对象优先进去Eden区,当Eden区满了之后再使用 Survivor from,当Survivor from 也满了之后 则触发进行Minor GC(新生代GC)。
  • Minor GC 会将 Eden 和Survivor from中存活的对象 copy进入Survivor to,然后清空 Eden和Survivor from,这个时候原来的Survivor from成了新的Survivor to,原来的Survivor to成了新的Survivor from。
  • 复制的时候,如果Survivor to 无法容纳全部存活的对象,则将对象copy进去老年代,如果老年代也无法容纳,则进行 Full GC(老年代GC)。

4.2 垃圾回收器

垃圾回收器是垃圾回收策略的具体实现,有下面这些:

  • Serial ( 串行收集器 )
  • Parallel (并行收集器)
  • CMS ( Concurrent mark sweep collector ,并发收集器)
  • G1

(1) Serial ( 串行收集器 )

串行收集器 是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。垃圾收集的过程中会Stop The World(服务暂停)。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩算法。

Serial收集器的多线程版本 叫做 ParNew 收集器,它支持并行。它的新生代采用并行方式,老年代串行;新生代复制算法、老年代标记-压缩

(2) Parallel (并行收集器)

Parallel收集器更关注系统的吞吐量。它会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩。

(3) CMS ( Concurrent mark sweep collector ,并发收集器)

当前 Java Web 应用重视服务的响应,希望停顿时间短。CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

CMS收集器的内存回收过程是与用户线程一起并发地执行的,它会先执行一次标记过程,后在标记扫描一次因用户程序继续运作而导致标记产生变动的那一部分。

整个过程分为4个步骤,包括:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录

(4) G1收集器

G1收集器有以下特点:

  • 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
  • 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离了。

5.参考:

Oracle 的 Java 虚拟机规范
https://docs.oracle.com/javase/specs/jvms/se8/html/
维基百科
https://en.m.wikipedia.org/wiki/Java_virtual_machine

https://www.cnblogs.com/haojile/p/12578470.html
https://blog.csdn.net/qq_38163244/article/details/109551205
https://mp.weixin.qq.com/s?__biz=MzI4NDY5Mjc1Mg==&mid=2247483952&idx=1&sn=ea12792a9b7c67baddfaf425d8272d33&chksm=ebf6da4fdc815359869107a4acd15538b3596ba006b4005b216688b69372650dbd18c0184643&scene=21#wechat_redirect
https://zhuanlan.zhihu.com/p/34426768
https://blog.csdn.net/qq_41701956/article/details/81664921

END

你可能感兴趣的:(Java 虚拟机 ( 简读版 ))