深入理解HotSpot JVM 基本原理

关于JAVA

Java®编程语言是一种通用的、并发的、面向对象的语言。它的语法类似于C和C++,但它省略了许多使C和C++复杂、混乱和不安全的特性。

Java 是几乎所有类型的网络应用程序的基础,也是开发和提供嵌入式和移动应用程序、游戏、基于 Web 的内容和企业软件的全球标准。.

从笔记本电脑到数据中心,从游戏控制台到科学超级计算机,从手机到互联网,Java 无处不在!

Java的技术体系主要有各种硬件平台上的JVM虚拟机、提供各开发领域接口支持的Java API、Java编程语言、三方Java框架(Spring等)构成。

Java程序设计语言、Java虚拟机、Java API类库这三部分统称为JDK(Java Development Kit),JDK是用于支持Java程序开发的最小环境。

可以把Java API类库中的Java SE API子集和Java虚拟机这两部分统称为JRE(Java Runtime Environment),JRE是支持Java程序运行的标准环境。

下图展示了Java技术体系所包含的内容,以及JDK和JRE所涵盖的范围。

关于JVM

Java虚拟机是Java平台的基石。负责其硬件和操作系统的独立性,为Java字节码的执行提供运行时环境。

JVM虚拟机在Java 虚拟机规范中没有规定具体实现,而是有各大厂商自己实现。

Implementation details that are not part of the Java Virtual Machine's specification would unnecessarily constrain the creativity of implementors. For example, the memory layout of run-time data areas, the garbage-collection algorithm used, and any internal optimization of the Java Virtual Machine instructions (for example, translating them into machine code) are left to the discretion of the implementor.

Classic VM 是“世界上第一款商用Java虚拟机”,在JDK 1.2之前是Sun JDK中唯一的虚拟机。

在JDK 1.2时,它与HotSpot VM并存,而在JDK 1.3时,HotSpot VM成为默认虚拟机,直到JDK 1.4的时候,Classic VM才完全退出商用虚拟机的历史舞台。

1999年4月27日,HotSpot虚拟机发布,HotSpot最初由一家名为“Longview Technologies”的小公司开发,因为HotSpot的优异表现,这家公司在1997年被Sun公司收购了。后来它成为了JDK 1.3及之后所有版本的Sun JDK的默认虚拟机。

在2008年和2009年,Oracle公司分别收购了BEA公司和Sun公司,这样Oracle就同时拥有了两款优秀的Java虚拟机:JRockit VM和HotSpot VM。

关于HostSpot

Java HotSpot虚拟机是Sun用于Java平台的VM。 它使用许多先进技术为Java应用程序提供最佳性能,包括最先进的内存模型,垃圾收集器和自适应优化器。

在SUN/Orace JDK中包括两种风格的VM

  • client mode
  • server mode

默认以client mode启动。

启动命令加- server,以server mode启动。

查看当前JVM mode:

两种mode的区别:

client mode

  • 短时间内启动,运行时,占用更少内存
  • C1轻量级编译器,优化较少
  • 适合轻量级程序和桌面程序

server mode

  • 启动慢,运行时,占用更大的内存
  • C2重量级编译器,更彻底的优化
  • 能提供更好的性能,适合生产部署

HotSpot JVM Architecture

HotSpot JVM 主要包括3个组件:

  • Class Loader Subsystem
  • Runtime Data Areas
  • Execution Engine

Class Loader Subsystem

Class Loader Subsystem是JVM必不可少的核心,用于读取/加载.class文件,并把字节码保存在JVM方法区。

加载过程

Java虚拟机中类加载的全过程:加载,验证,准备,解析,初始化。

Loading

Loading阶段,虚拟机完成三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流;
  2. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构;
  3. 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口。

加载的第一步是在Java虚拟机外部去实现的,有‘类加载器’来完成加载动作。

绝大部分Java程序都会用到以下3种系统提供的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)
    • 使用C++语言实现,是虚拟机自身的一部分。
    • 加载<JAVA_HOME>/lib中虚拟机识别的jar,如rt.jar。
    • 无法被Java程序直接引用
  2. 扩展类加载器(Extension ClassLoader)
    • 由sun.misc.Launcher$ExtClassLoader实现
    • 加载<JAVA_HOME>/lib/ext中类库
    • Java程序可以直接使用
  3. 应用程序类加载器(Application ClassLoader)
    • 由sun.misc.Launcher$AppClassLoader实现
    • 也称系统类加载器,加载应用程序classpath下的jar
    • 可以直接使用,程序中默认的类加载器

Linking

执行类或接口的链接。

  • Verification,验证。确保Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机的安全。
  • Preparation,准备。在方法区中为类变量(被static修饰的变量)分配内存并设置初始值。
  • Resolution,解析。虚拟机将常量池的符号引用替换为直接引用。

Initialization

执行类加载的最后阶段,为所有静态变量都分配了原始值,静态块从父类执行到子类。

加载原则

  1. Delegation,委派
  2. Visibility,可见性
  3. Uniqueness,唯一性

Delegation

双亲委派模型

双亲委派模型在JDK1.2中被引入,要求除了启动类加载器,其他类加载器都要有父类加载器。

加载过程:当一个类加载器收到加载请求的时候,先委派给父类加载器去完成,每个层次的类加载器都是如此,最终加载请求传到顶层启动类加载器。如果父类无法完成加载请求,子类才会去尝试加载。

破坏双亲委派模型

双亲委派解决了各类加载器的基础类的统一问题。

如果基础类需要反调用户代码,怎么办?

线程上下问类加载器(Thread Context ClassLoader),TCCL。解决基础类反调用户代码。例如JDK中实现SPI机制的JDBC、JNDI等。

Visibility

子类加载器能看到父类加载器加载的类。父类加载器看不到子类加载器中的类。

Uniqueness

父类加载器加载过的类不会被子类加载器加载。

Runtime Data Areas

Runtime Data Areas大致可以分为两部分:

  • 线程私有区,线程创建时分配内存。线程启动时初始化,并在线程完成后销毁,
  • 线程共享区,所有线程都可以访问。JVM启动时初始化,在关闭时销毁。

程序计数器

保存当前正在执行的JVM指令地址。每个线程都有自己的PC。

Java虚拟机栈

每个Java虚拟机线程都有一个私有Java虚拟机堆栈,与线程同时创建。

本地方法栈

供用非Java语言实现的本地方法的堆栈。换句话说,它是用来调用通过JNI(Java Native Interface Java本地接口)调用的C/C++代码。根据具体的语言,一个C堆栈或者C++堆栈会被创建。

Java堆

用来保存实例或者对象的空间,而且它是垃圾回收的主要目标。当讨论类似于JVM性能之类的问题时,它经常会被提及。JVM提供者可以决定怎么来配置堆空间,以及不对它进行垃圾回收。

方法区

方法区是所有线程共享的,它是在JVM启动的时候创建的。它保存所有被JVM加载的类和接口的运行时常量池,成员变量以及方法的信息,静态变量以及方法的字节码。JVM的提供者可以通过不同的方式来实现方法区。在Oracle 的HotSpot JVM里,方法区被称为永久区或者永久代(PermGen)。是否对方法区进行垃圾回收对JVM的实现是可选的。

JDK1.8以后,PermGen被永久移除,有Metaspace(元空间)来实现方法区。

与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

因此,默认情况下,元空间的大小仅受本地内存限制

运行时常量池

这个区域和class文件里的constant_pool是相对应的。这个区域是包含在方法区里的,不过,对于JVM的操作而言,它是一个核心的角色。因此在JVM规范里特别提到了它的重要性。除了包含每个类和接口的常量,它也包含了所有方法和变量的引用。简而言之,当一个方法或者变量被引用时,JVM通过运行时常量区来查找方法或者变量在内存里的实际地址。

Execution Engine

Interpreter

读取字节码指令并以顺序方式执行。

JIT

JIT (Just In Time) Compiler。

抵消了Interpreter执行速度慢的缺点并提高了性能。 JIT编译器同时编译字节码的类似部分,从而减少了编译所需的总时间。

Garbage Collection

通过收集和删除未引用的对象来释放内存。

对象是否存活

在堆里面存放着Java世界几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事就是要判断对象是否存活。

引用计数算法

引用计数算法(Reference Counting)实现简单,判定效率也很高。给对象中添加一个引用计数器,每当一个地方引用它时,计数器值就加1;引用失效时,计数器值就减1;计数器为0的对象就是不可能再被使用的。

Python语言、游戏脚本领域被广泛应用的Squire中都使用了引用计数算法进行内存管理。但是,java虚拟机里没有用,主要原因是很难解决对象之间相互循环引用的问题

可达性分析

可达性分析(Reachability Analysis),基本思路是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始往下搜索,搜索所走过的路程称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。

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

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

垃圾收集算法

常见的垃圾收集算法。

标记-清除算法

算法分为‘标记’、‘清除’两个阶段。

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

复制算法

复制(Copying),将可用内存按容量分为大小相等的两块,每次只使用其中的一块。 当这一块的内存用完了,将还存活的对象复制到另外一块上去。把已使用过的内存空间一次清理掉。

实现简单,运行高效,内存使用率不高,用于回收新生代。

IBM研究表明,新生代中98%的对象都是在第一次GC时被回收掉,不需要按照1:1分配空间。

HotSpot虚拟机默认Eden和Survivor比例是8:1,只有10%的内存会被浪费。

Survivor空间不够时,需要依赖老年代进行分配担保,新生代收集下来的存活对象直接进入老年代。

标记-整理算法

标记-整理(Mark-Compact)算法,标记所有存活对象,向一端移动,然后直接清理掉端边界意外的内存。

分代收集算法

根据对象存活周期不同,将Java堆分为新生代和老年代。

新生代,采用复制算法。

老年代采用‘标记-清理’或者‘标记-整理’算法。

垃圾收集器

如果说,收集算法是内存回收的方法论,那么,垃圾收集器就是内存回收的具体体现。

Java虚拟机规范没有规范如何实现垃圾收集器。不同的厂商、版本的虚拟机实现可能会有很大差别。

下面,基于JDK1.7 Update14之后的HotSpot虚拟机讨论收集器。

Serial 收集器

"Serial" is a stop-the-world, copying collector which uses a single GC thread.

ParNew 收集器

"ParNew" is a stop-the-world, copying collector which uses multiple GC threads.

Parallel Scavenge 收集器

"Parallel Scavenge" is a stop-the-world, copying collector which uses multiple GC threads.

Serial Old 收集器

"Serial Old" is a stop-the-world, mark-sweep-compact collector that uses a single GC thread.

Parallel Old 收集器

"Parallel Old" is a compacting collector that uses multiple GC threads. Using the -XX flags for our collectors for jdk6.

CMS收集器

"CMS"(Concurrent Mark Sweep) is a mostly concurrent, low-pause collector.

G1收集器

G1(Garbage First) straddles the young generation - tenured generation boundary because it is a generational collector only in the logical sense. G1 divides the heap into regions and during a GC can collect a subset of the regions.

JMM

Java内存模型(Java Memory Model),JMM,用来屏蔽掉各种硬件和操作系统之间内存访问差异,以实现在各种平台下一致的内存访问效果。

  1. JMM主要目标是定义程序中各个变量的访问规则。即在虚拟机中将变量存储到内存和从内存中取出变量。

  2. 所有变量都存储在主内存(Main Memory),每个线程还有自己的工作内存(Working Memory)。

  3. 线程对变量的读取、赋值等操作都必须在工作内存中进行。

  4. 线程间变量值的传递必须通过主内存来完成。

基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(CacheCoherence)。

除了增加高速缓存之外,为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件时的主内存名字一样,两者也可以互相类比,但此处仅是虚拟机内存的一部分)。

每条线程还有自己的工作内存(Working Memory,可与前面讲的处理器高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。

不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成

线程、主内存、工作内存三者的交互关系如图12-2所示。

这里所讲的主内存、工作内存与Java内存区域中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的。

如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。

从更低层次上说,主内存就直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。

volatile

关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制

当一个变量定义为volatile之后,它将具备两种特性:

  • 第一是保证此变量对所有线程的可见性
  • 第二个语义是禁止指令重排序优化

这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。

禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

参考

Our Collectors

了解 Java 技术

Java Virtual Machine from Sun

Java Garbage Collection Introduction

The Java® Virtual Machine Specification

Java发展史

The Java HotSpot Performance Engine Architecture

JVM Server vs Client Mode

JSR-133: JavaTM Memory Model and Thread Specification

JVM Architecture: JVM Class loader and Runtime Data Areas

JVM Architecture: Execution Engine in JVM

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章