JVM

376 阅读13分钟

Java发展重大事件

image.png

image.png

背景介绍

image.png

跨语言平台

image.png

基本概念

JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、 一个垃圾回收,堆和一个存储方法域。 JVM 是运行在操作系统之上的,它与硬件没有直接的交互。

运行过程

我们都知道Java源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件,而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码。

也就是如下:

① Java 源文件—->编译器—->字节码文件

② 字节码文件—->JVM—->机器码

每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是Java为什么能够跨平台的原因了,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会 存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。

JVM生命周期

  • 虚拟机的启动 Java虚拟机的启动是通过引导类加载器(Bootstrap Class Loader)创建一个初始类来完成的,这个类由虚拟机的具体实现指定。

  • 虚拟机的执行

    • 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序
    • 程序开始执行时它才运行,程序结束时他就停止
    • 执行一个所谓的Java程序的时候,真正执行的是一个Java虚拟机的进程

虚拟机分类

  • Oracle HotSpot image.png
  • Oracle JRockit
  • IBM J9

类加载器

image.png

image.png

image.png

TODO image.png

字节码文件

0000

8421

Class文件是一组以8位(一个字节)字节为基础单位(0xFF)的二进制流、半个字节4位用一个0xF表示

4位2进制可用1位16进制数表示一个字节8位可用两个个16进制代替0xFF

  • 无符号数:基本类型数据,一 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数。用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以_info结尾,用于描述有层次关系的复合结构的数据。

每个Class文件的头4个字节0xCAFEBABE称为魔数(Magic Number),用来确定这个文件是否为能被虚拟机接受的Class文件格式。

image.png

加载过程

image.png

image.png

分类

image.png

  • Bootstrap ClassLoader

image.png

  • Extension ClassLoader / Platform ClassLoader

image.png

  • AppClassLoader

image.png

image.png

双亲委派模型

image.png

Tomcat类加载器

image.png

JVM内存分区

image.png

运行时数据区

image.png

程序计数器

一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的 程序计数器,这类内存也称为“线程私有”的内存。

正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。

这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError 情况的区域。

image.png

image.png

字节码指令 >> 机器指令

image.png

image.png

虚拟机栈

是描述 java 方法执行的内存模型

每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异 常)都算作方法结束。

image.png

局部变量表

  • 局部变量表也成为局部变量数组或本地变量表
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 这些数据类型包括各种基本数据类型、对象引用(reference),以及returnAddress类型
  • 由于局部变量表示建立在线程栈上,是线程的私有化数据,因此不存在数据安全问题
  • 局部变量表所需要的容量大小是在编译期确定下来的,并保存在方法的Code属性maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
Slot

image.png

image.png

image.png

image.png

image.png

操作数栈

image.png

image.png

image.png

image.png

动态链接

指向运行时常量池的方法引用

image.png

image.png

方法的调用

image.png

image.png

动态语言和静态语言

image.png

image.png

虚方法表

image.png

返回地址

image.png

虚拟机栈大小调整

  • -Xss226k

image.png

栈工作原理

image.png

image.png

image.png

image.png

本地方法栈

本地方法区和 Java Stack(虚拟机栈) 作用类似,区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则 Native 方法服务, 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个 C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。

image.png

堆-Heap

一个进程 >> JVM实例 >> 运行时数据区 >> 方法区、堆区

是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。

由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。

image.png

堆空间参数

image.png

堆优化技术

逃逸分析 >> 栈上分配 >> 标量替换

image.png

逃逸分析并不成熟

  • 关于逃逸分析的论文在1999年就己经发表了,但直到JDK 1.6才有实现,而且这项技 术到如今也并不是十分成熟的。
  • **其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分 析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复 杂的分析的,这其实也是一个相对耗时的过程。
  • 一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃 逸分桥的近程就白省浪费掉了。
  • 虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。
  • 注意到有一些观点,认为通过逃逸分析,JVN会在栈上分配那些不会逃逸的对象,这 在理论上是可行的, 但是取决于JVy设计者的选择。据我所知, Oracle Hotspot JV4中并未这么做,这一点在逃逸分析相关的文档里己经说明,所以可以明确所有的 对象实例都是创建在堆上。
  • 目前很名书籍还是基于JDK 7以前的版本,JDK己经发生了很大变化, intern字符串的缓存和静态变量曾经都被分配在永久代上, 而永久代己经被元数据区取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区,市 是直接在堆上分配,所以这一点同样待合前面一点的结论:对象实例都是分配在堆上

image.png

image.png

标量替换

image.png

image.png

堆空间大小设置

image.png

image.png

image.png

image.png

image.png

Young / Old Generation

image.png

image.png

新生代

image.png

老年代

image.png

OOM

image.png

直接内存

image.png

image.png

image.png

image.png

image.png

image.png

方法区、永久代

image.png

image.png

image.png

image.png 但是1.7及以后,静态变量和对象实例一起放到堆当中!

image.png

永久代为什么被元空间替代

image.png

image.png

image.png

栈、堆、方法区交互关系

image.png

常量池

image.png

image.png

1. 全局字符串池(string pool也有叫做string literal pool)

全局字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。

在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

2. class文件常量池(class constant pool)

class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。 字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。 符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:

类和接口的全限定名

字段的名称和描述符

方法的名称和描述符

常量池的每一项常量都是一个表,一共有如下表所示的11种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(取值1-12),代表当前这个常量属于哪种常量类型。

image.png

3.运行时常量池(runtime constant pool)

当java文件被编译成class文件之后,也就是会生成我上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?

jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。

举个实例来说明一下:

public class HelloWorld {
    public static void main(String []args) {
        String str1 = "abc"; 
        String str2 = new String("def"); 
        String str3 = "abc"; 
        String str4 = str2.intern(); 
        String str5 = "def"; 
        System.out.println(str1 == str3);//true 
        System.out.println(str2 == str4);//false 
        System.out.println(str4 == str5);//true
    }
}
  • 首先,在堆中会有一个”abc”实例,全局StringTable中存放着”abc”的一个引用值

  • 第二句的时候会生成两个实例,一个是”def”的实例对象,并且StringTable中存储一个”def”的引用值,还有一个是new出来的一个”def”的实例对象,与上面那个是不同的实例都存在堆当中。

  • 当在解析str3的时候查找StringTable,里面有”abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同。

  • str4是在运行的时候调用intern()函数,返回StringTable中”def”的引用值,如果没有就将str2的引用值添加进去,在这里,StringTable中已经有了”def”的引用值了,所以返回上面在new str2的时候添加到StringTable中的 “def”引用值。

  • str5在解析的时候就也是指向存在于StringTable中的”def”的引用值。

Summary:

上面程序的首先经过编译之后,在该类的class常量池中存放一些符号引用,然后类加载之后,将class常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的”abc”实例对象),然后将这个对象的引用存到全局String Pool中,也就是StringTable中,最后在解析阶段,要把运行时常量池中的符号引用替换成直接引用,那么就直接查询StringTable,保证StringTable里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。

  1. 全局常量池在每个VM中只有一份,存放的是字符串常量的引用值。

  2. class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。

  3. 运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

对象实例化

image.png

深拷贝 浅拷贝

拷贝,顾名思义就是为了获得一个相同的对象,而不需要我们再人为的创建和赋值。java中的拷贝需要实现java.lang.Cloneable接口,然后重写clone()方法,这个无论深、浅拷贝都需要这样做。

而深拷贝和浅拷贝的区别,则在于java对象中的基本数据属性和引用数据类型属性。

浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。

深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

  • 浅拷贝

image.png

image.png

  • 深拷贝

image.png

image.png

  • 利用序列化实现深拷贝

image.png

对象创建过程

image.png

  • 字节码

image.png

  • 对象内存布局

image.png

image.png

  • 对象访问定位
    • 句柄访问

image.png

- 直接指针(Hotspot)

image.png

执行引擎

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

GC

垃圾:运行程序中没有任何指针指向的对象

image.png

垃圾标记算法

image.png

1.引用计数算法

image.png

image.png

2.可达性分析算法

image.png

image.png

GC Roots

image.png

finalization

image.png

image.png

image.png

MAT

image.png

image.png

image.png

image.png

image.png

JProfiler

image.png

GC 算法

标记-清除 (Mark-Sweep)

image.png

复制 (Copying)

image.png

标记-压缩 (Mark-Compact)

image.png

分代收集算法

image.png

增量收集算法

image.png

image.png

分区算法

image.png

System.gc()

image.png

内存泄漏

image.png

image.png

Stop The World

image.png

并发 并行

image.png

垃圾回收的并发与并行

image.png

image.png

安全点

image.png

引用

image.png

  • 软引用 image.png

image.png

  • 弱引用

image.png

Minor GC / Magor GC / Full GC

image.png

image.png

image.png

image.png

TLAB

image.png

image.png

评估GC性能指标

image.png

image.png

image.png

image.png

垃圾回收器

垃圾回收器发展史

image.png

七款经典垃圾回收器与分代之间的关系

image.png

image.png

image.png

image.png

分类

image.png

Serial / Serial Old(标记整理)

image.png

ParNew (并行GC)

image.png image.png

Parallel Old

多线程标记整理

Parallel Scavenge

多线程复制算法,更加高效

image.png

CMS (标记清除!!!)

多线程标记清除 Concurrent Mark Sweep

image.png

G1 (复制 + 标记整理)

image.png

image.png

ZGC

Java11

为大堆提供了非常短的停顿时间

image.png

Summary

image.png

JVM调优

  • 项目搭建之前的预设置
  • 监控调优
  • OOM调优

JVM参数

image.png

指标

image.png

三部曲

监控

image.png

分析

image.png

调优