Fari

jvm学习笔记

基于栈和基于寄存器的指令集架构

Jvm前端编译器架构=都是= 基于栈 的指令集架构,与之对应的还有 基于寄存器 的指令集架构。### 基于栈的指令集架构

* 跨平台性好、指令集小、指令多、性能相较于寄存器更差
* 例:

file

基于寄存器的指令集架构

* 直接使用cpu的指令集,故执行幸能更好,但是移植性较差

Hotspot/JRocket/J9

JRocket:号称最快的虚拟机,专注于服务端,牺牲程序启动速度,因此其内部不包含解析器的实现,全部代码都即时编译器编译后执行

J9:运行IBM自己的软件时速度较快,J9最厉害的地方是它高度模块化,不但可以部署在桌面或服务器上,还可以部署到嵌入式环境中,例如CLDC级别的环境;这些环境用的是同一个J9核心VM,搭配上适用于具体环境的GC和JIT编译器。

hotspot:运用最广泛的虚拟机

类加载子系统

graph LR
加载 ---> 验证
subgraph 链接
验证 ---> 准备 ---> 解析
end
解析 ---> 初始化

类的加载过程

  1. 加载 加载二进制流并产生对应的Class对象。
  2. 链接 2.1 验证
    • 确保class文件的字节流满足当前虚拟机的要求,保证被加载类的正确性
    • 主要包含四种验证:文件格式验证(例如是否以魔数开头),元数据验证,字节码验证,符号引用验证 2.2 准备
    • 为类变量分配内存并且设置该类变量(static修饰的变量)的默认初始值,即零值。如 int i = 3,则在此阶段将i赋值为0。
    • 注:这里不包含被final修饰的类变量,它在编译的时候就已经赋零值了,在准备阶段会显示初始化,即 i 赋值为3。
    • 准备阶段不会为实例变量初始化,类变量会分配到方法区中,而示例变量则会随着对象一起分配到java堆中。 2.3 解析
    • 将常量池内的符号引用转换为直接引用的过程。
  3. 初始化
    • 初始化阶段就是执行类构造器方法()的过程。
    • 该方法不需要定义,由javac编译器自动生成(如果没有类变量或静态代码块就不会生成该方法)。不同于类的构造器,即()
    • 构造器方法中的指令语句按照源文件出现的顺序执行。
    • 若该类有父类,则一定要保证父类的()已经执行完毕了
    • 虚拟机必须保证一个类的()方法在多线程下被同步加锁。

file

file

类加载器

file

  1. 虚拟机规范中规定类加载器只有两种,启动类加载器(bootstrap class loader)和用户自定义类加载器(凡是直接或间接继承自ClassLoader类的均为用户自定义类加载器)

  2. Bootstrap class loader使用的是c/c++编写,其他类加载器使用java编写。

  3. 启动类加载器只加载核心类库(JAVAHOME/jre/lib/rt.jar resources.jar sun.boot.class.path路径下的类),一般使用getclassloader()方法获取不到启动类加载器(返回null) file

  4. 出于安全考虑,bootstrap只加载包名为java、javax、sun等开头的类

注:JVM必须知道一个类是由启动类加载器加载的还是由用户自定类加载器加载的。如果是由用户自定义类加载器加载,则JVM #会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中#,当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。(关键字:动态链接)

双亲委派机制

file

例如,如果在项目中创建了一个 java.lang.String 类,当new String()时,返回的仍然是核心api中的String

类的主动使用和被动使用

file

注:详见:10.类的生命周期,11.类加载器

运行时数据区

graph 
subgraph Runtime
程序计数器
虚拟机栈
本地方法栈
堆
元空间
end
  1. StackOverflowError vs OutOfMemoryError

    • Java虚拟机规范允许栈的大小是动态的或者固定不变的。
    • 如果是固定不变的,当线程请求分配栈的容量超过指定的最大容量,则会抛出 StackOverflowError 异常
    • 如果是动态扩展的,并且在尝试扩展时无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,则抛出 OutOfMemoryError 异常
  2. 设置栈的固定大小

    • -Xss file
  3. 栈帧的内部结构

    • 局部变量表(Local Variables)
    • 定义为一个数字数组【图1】,主要用于存储方法参数和定义在方法体内部的局部变量,这些数据类型包括各类的基本数据类型,对象引用(reference),以及returnAddress类型。
    • 局部变量表所需的容量大小是在编译器确定下来的,并保存在方法的Code属性的 maximum local variables 数据项中。方法运行期间不会改变其大小。
    • 线程安全
    • 一开始创建时候是一个定长的空数组,执行字节码指令时遇到局部变量才将其放入该表,并在表中存如该值(对象存引用)。其中表的Slot会重复利用
    • 基本类型放的是值,引用类型放的是索引。使用表的index获取 file

【图1】 file

3.1. 关于Slot的理解

3.2. 操作数栈(jvm解释引擎是基于栈的执行引擎的原因)

3.3. 栈顶缓存技术 基于栈的架构的虚拟机使用零地址指令更加紧凑,但完成一项操作需要更多的入栈和出栈指令,也就意味着更多次数的内存读写,Hotspot提出了,将栈顶元素全部缓存在物理cpu的寄存器中,以此降低对内存的读写次数,提升引擎的执行效率

  1. 动态链接

    • 每一个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用地址
  2. 方法的调用 JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。 5.1. 静态链接(对应早期绑定,绑定范围更大,包括属性方法等,链接只表示方法) 当一个字节码文件被装载进jvm内部时,如果被调用的目标方法在编译时可知,且运行期间长期保持不变,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接 5.2. 动态链接(对应晚期绑定) 如果被调用的方法在编译期间无法确定(多态),只能在程序运行期间将调用方法的符号引用转换为直接引用的过程。

5.3. 动态类型语言vs静态类型语言 区别在于对类型的检查是在编译时期(静态)还是运行期(动态),通俗解释为,静态类型语言是判断变量自身类型信息,动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有。(java原本署于静态类型语言,但在引入了 invokedynamic 指令之后(java8中的lambda表达式使用该指令)就具有了动态类型语言的特性,js为动态类型)

5.4. returnAddress 存储的该方法的pc寄存器的值(即下一条该执行指令的地址值) 无论方法通过哪种方式退出(正常执行完成、异常退出),在方法退出后都会返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令地址。而异常退出时,返回地址时通过异常表来确定的,栈帧中一般不保存这部分的信息。异常退出的时候不会给调用者返回任何返回值。

如果某一变量生命周期只在方法内有效则线程安全

  1. 本地方法栈 本地方法主要用于:语言扩展、与操作系统交互
    • 栈、堆、方法区 关系 file
  1. 方法区(元空间)
    • JDK7之前为永久代,之后修改为元空间(直接使用本地物理内存)。可以理解为永久代和元空间分别是方法区的两种实现。
    • java虚拟机规范申明其在逻辑上署于堆空间,但一般在具体实现上将其和堆分开,故其也成为非堆
graph
subgraph Java栈
subgraph 栈帧1
subgraph 本地变量表1
int1[int]
short1[short]
reference1[reference]
end
end

subgraph 栈帧2
subgraph 本地变量表2
int2[int]
short2[short]
reference2[reference]
end
end
end

subgraph Java堆
subgraph 对象实例1
classPoint1[到对象类型的指针]
instance1[对象实例数据]
end

subgraph 对象实例2
classPoint2[到对象类型的指针]
instance2[对象实例数据]
end
end

subgraph 方法区
classData[对象类型数据class]
end

reference1 ---> instance1
reference2 ---> instance2
classPoint1 & classPoint2 ---> classData

图示: file file file

执行引擎

file

StringTable

垃圾回收

Object obj = new Object();  //声明强引用
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  //销毁强引用
- 弱引用(Weak Reference)
被弱引用关联的对象只能生存到下一次垃圾收集之前,当垃圾收集器工作时,无论内存空间是否足够,都会回收。一般用于缓存。 WeakHashMap<k,v>使用了弱引用
Object obj = new Object();  //声明强引用
WeakReference<Object> sf = new WeakReference<Object>(obj);
obj = null;  //销毁强引用
- 虚引用(Phantom Reference)
一个对象是否存在虚引用,完全不会对其生存时间构成影响,也无法通过虚引用获得一个对象实例。唯一目的就是能在该对象被回收时受到一个系统通知。一般用于对象回收过程追踪。创建虚引用后,试图使用get()获取时总是返回null

file

file

file

Class 文件结构

Class文件格式采用一种类似于C语言结构体的方式及进行存储,这种结构只有两种数据类型:无符号数和表(可以理解为数组) file

- 无符号数署于基本数据类型,以 u1\u2\u4\u8(类比 int、long...)分别表示1、2、4、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF8编码构成的字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有的表都习惯性以“_info”结尾。表用于描述有层次关系的符合结构数据,整个Class文件本质就是一张表。由于表没有固定长度,所以通常在其前面加上个数说明

字节码指令集


类的生命周期

graph LR
loading[加载]--->verification[验证]
subgraph linking
verification--->preparation[准备]
preparation--->resolution[解析]
end
resolution--->initialization[初始化]
initialization--->using[使用]
using--->unloading[卸载]

其中,验证、准备、解析三个部分统称为链接

loading
Linking
Verification
Preparation
Resolution
Initialization
Using
Unloading

类加载器

ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个于目标类对应的java.lang.Class对象实例。然后交给jvm进行链接、初始化等操作,因此ClassLoader在整个装载阶段,只能影响到类的加载阶段。

graph TD
UC1[User ClassLoader1] ---> AC[Application ClassLoader]
UC2[User ClassLoader2] ---> AC[Application ClassLoader]
AC ---> EC[Extension ClassLoader] ---> BC[Bootstrap ClassLoader]

除了引导类加载器,其他的也成为自定义加载器。上图并非继承关系,只是包含关系,即下层加载器(图中的上面)中包含对上层加载器的引用。

file

引导类加载器
扩展类加载器
应用程序加载器(系统类加载器)
用户自定义类加载器

数组类型的加载,数组类Class不需要加载器加载,只是在使用时才创建。使用的类加载器与数组元素的类加载器相同。如果是基本数据类型数组或void 如int[ ]则获取其class返回null,此时该null并不表示启动类加载器,而表示不需要类加载器。因为基本数据类型不需要加载,在jvm启动时已经预设了。

ClassLoader
SecureClassLoader 与 URLClassLoader
Class.forName() 与 ClassLoader.loadClass()
双亲委派机制

从jdk1.2开始使用。如果一个类加载器在接到加载类的请求时,首先将该请求委托给父类加载器完成,依次递归。只有父类加载器无法完成时,自己才去加载。jvm规范只是建议使用该方式

优势
- 避免类的重复加载,确保一个类全局唯一。
- 保护核心类库的安全,防止被篡改。例如自己实现一个java.lang.String,加载时就会报错
代码实现

file

弊端
破坏机制的三次行为
沙箱安全机制

自行百度

java9新特性

因为模块化的引入,导致类加载器有了较大变化。但为了保证兼容性,其并没有从根本上改变三层类加载器架构和双亲委派模型

性能监控与调优

性能调优三步:发现问题(性能监控)、排查问题(性能分析)、解决问题(性能调优)

  1. 性能监控 出现的问题可能有:GC频繁、cpu load过高、OOM、内存泄漏、死锁、程序响应时间较长等

  2. 性能分析 实施方式可能有:打印GC日志,通过GCViewer等工具分析查看、使用命令行工具 jstack/jmap/jinfo等、dump堆文件,使用内存分析工具分析、使用阿里的Arthas或jconsole/JVisualVM实时查看jvm状态

  3. 性能调优 方式:适当增加内存、选择合适的垃圾回收器、优化代码、增加机器、合理设置线程池线程数量、使用中间件如缓存,消息队列等

性能测试指标

  1. 停顿时间(响应时间) 提交请求与返回该请求之间使用的时间或执行垃圾收集时程序的工作线程被暂停的时间
  2. 吞吐量
  3. 并发数
  4. 内存占用

监控工具

命令行工具

file

jps:查看正在运行的Java进程

file

参数: -l 命令显示主程序全类名 -m 输出传递给main函数的参数 -v 显示启动虚拟机手动指定的参数 如-Xms20m

注:如果java进程使用了参数 -XX:-UsePerfData,那么jps和jstat命令将无法获得该进程 可以连接远程主机

jstat:查看jvm的统计信息

可以显示本地或远程jvm进程中的类装载、内存、垃圾收集、JIT编译等运行数据 file

例如:查看进程18996的堆相关(包括Eden、两个S区、老年代、永久代等)信息并每秒打印一次,共打印十次

jstat -gc 18996 1000 10 file

或者若只关心各个区域已使用内存占比的情况

jstat -gcutil 18996 1000 10 file

使用 -t 参数可以计算出GC时间占比

jinfo:查看虚拟机配置参数信息,也可以用于调整参数

只能修改部分被标记为manageable的参数,并立即生效

查看层级赋值的参数

jinfo -flags PID file

jmap:导出内存映像文件和内存使用情况

-dump:生成堆快照(可手动导出,也可以使用参数自动触发导出) file 一般使用 -dump:live 只生成存活对象的快照

-heap:输出整个堆的详细信息,包括GC的使用、堆配置信息、内存使用信息等 file

-histo:输出堆中对象的统计信息,包括类、实例数量和合计容量

注:jmap只会在安全点才会执行,否则阻塞等待 dump文件可以使用jhat命令查看(jdk9之后移除,使用virtualVM代替),会启动一个httpserver在浏览器中查看信息。一般不在生产环境直接使用,会占用较高的CPU

jstack:打印JVM线程快照(虚拟机堆栈跟踪)

线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈集合。作用:用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致长十间等待等问题。 file

jcmd:可以实现除了jstat之外所有命令的功能

可以使用 jcmd PID help 查看针对该进程可执行的所有指令

jstatd:远程主机信息的收集
图形工具
jconsole(自带)
Visual VM(自带):可用于取代 jconsole

最大的特点是支持插件扩展。

JMC(自带)

优点:取样分析,而不是传统的代码植入,对项目性能影响小

MAT(Eclipse插件)

memory analisy tool内存分析工具,也可以单独下载使用。主要用于分析dump文件(hprof文件或phd文件) 最大特点是可以生产内存泄漏报表

JProfiler

收费,更强大,使用方便,界面友好

Arthas(阿尔萨斯)

阿里巴巴开发,在线排查,无需重启,动态追踪代码,实时监控jvm状态

Btrace

浅堆与深堆(对应浅拷贝深拷贝)
浅堆(Shallow Heap)

指一个对象所消耗的内存。在32位系统中,一个对象引用会占据4字节,一个int类型会占据4字节,long类型占据8字节,每个对象头占据8字节。根据堆快照格式笔筒,对象的大小可能会向8字节对齐。 以String类型为例,其内部有三个属性,int hash32,int hash,ref value:2个int值共占8字节,ref对象引用占用4字节,String对象头占用8字节,合计20字节,向8字节对齐,占24字节(jdk7).这24字节位String对象的浅堆大小,它与String的value实际值无关,无论字符串长度如何,浅堆大小始终是24字节

深堆(Retained Heap)

注:浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到(直接或间接)的所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。 因为对象的层层引用最终都归结于基本数据类型,所以只计算浅堆之和即可 引入概念:支配树

内存泄漏
与内存溢出的区别

严格来说jvm误以为某个对象还在引用,无法回收(占着茅坑不拉屎) 内存溢出:申请内存时,没有足够的内存可用 内存泄漏会导致内存溢出

内存泄漏的8种情况
  1. 静态集合类 file

  2. 单例模式 file

  3. 内部类持有外部类 file

  4. 各种连接,如数据库连接、网络连接等 file

  5. 变量不合理的作用域 file

  6. 改变hash值 file

  7. 缓存泄漏 file

  8. 监听器和回调 file

jvm运行时参数

GC日志

Tags: