JVM
JVM 入门篇
JVM特点
- 一次编译,到处运行
- 自动内存管理
- 自动垃圾回收
JVM整体架构
类装载子系统
加载阶段(loading)
- 类加载器
- 引导类加载器 BootstrapClassLoader
- 扩展类加载器
- 系统类加载器 NetworkClassLoader、URLClassLoader
- 从何处加载字节码文件
- 本地
- 网络
- 压缩包 zip、jar、war
- 加密文件 安卓.des文件
- 数据库
链接阶段(linking)
- 验证
- 校验字节码文件格式
- 包括文件头里的魔数(CA FE BA BE)
- jdk版本
- 元数据验证
- 字节码验证
- 符号引用验证
- 校验字节码文件格式
- 准备
- 为类变量分配内存,并设置初始值(int 0,boolean false等等)
- 不包括static final变量,final在编译时已分配内存
- 不包括实例变量初始化,类变量分配在方法区,实例变量分配在堆区
- 解析
- 常量池中的符号引用转换为直接引用(指向目标的指针、相对偏移量等)
- 解析操作往往在JVM执行完初始化之后再执行
- 解析主要针对类、接口、字段、类方法、接口方法、方法类型等,对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等
初始化阶段(initial)
- 初始化阶段就是执行类构造器方法 clinit() 的过程
- 这个方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
- 静态变量赋值
- 静态代码块执行
- clinit() 不同于类的构造器方法,clinit是类的构造器,init是类实例的构造器
- 由于初始化阶段是在链接阶段之后执行,因此我们对静态变量的赋值操作可以在静态变量声明之前,比如下图的场景:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cq6zpDLO-1692419330856)(image/24加载顺序例子.png “静态代码块给变量赋值可以写在变量声明之前”)] - 如果有父类,则先进行父类初始化,即先对父类静态变量赋值、执行父类静态代码块,然后再子类
类加载器分类
引导类加载器(BootstrapClassLoader)
- 加载Java核心类库,jre/lib/rt.jar、resources.jar、sun.boot.class.path路径下的类
- 不继承自ClassLoader,而是由C/C++实现的,所以java代码中看不到
- 加载其他类加载器
- 之加载包名为java、javax、sun等开头的类
自定义类加载器(User-Defined ClassLoader)
- JVM规范定义:所有派生于抽象类ClassLoader的类加载器都属于自定义类加载器(Java 语言实现的类加载器都属于)
- ExtClassLoader
- AppClassLoader
- 其他用户自定义ClassLoader(URLClassLoader等)
- 自定义类加载器实现
- 继承ClassLoader类,重写findClass()方法
- 继承URLClassLoader(实际应用中采用这种方式,更便捷)
- 自定义类加载器的优势
- 隔离加载类
- 修改类的加载方式
- 扩展加载源
- 加载加密源码
双亲委派机制
- 类的加载逐层向父加载器委托,最终由BootstrapClassLoader优先加载,如果父加载器无法加载,则交由子加载器处理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xuFHAs34-1692419330857)(image/35双亲委派机制.png “双亲委派机制”)] - 优点
- 避免类的重复加载
- 防止核心类库被篡改
- 双亲委派机制可以被借鉴
- 例子:我们自定义java.lang.String类,然后定义一个静态代码块,输出“我是自定义String类”,然后再新建一个Test类,去创建一个String对象,观察String对象属于哪个类(这个又叫沙箱安全机制)(md编译都过不了,幸亏我自己试了)
/Users/yuanshen/IdeaProjects/javase/src/main/java/java/lang/String.java
java: 程序包已存在于另一个模块中: java.base
- 相同类的判断条件
- 类的全限定名一致
- 加载这个类的加载器相同
类的主动使用和被动使用
- 区别
- 被动使用不会触发类的初始化
- Class.forName 与 ClassLoader.loadClass 区别
- Class.forName 会触发类的初始化
- ClassLoader.loadClass 不会触发类的初始化
- 由此推断 ClassLoader.loadClass 属于被动使用类
- 主动使用
- 创建类的实例
- 访问某个类或接口的静态变量,或对静态变量赋值
- 调用类的静态方法
- 反射(比如:Class.forName(“com.github.Test”))
- 初始化一个类的子类(初始化子类之前会先初始化父类,初始化父类的静态变量、静态代码块)
- java.lang.invoke.MethodHandle
- 除主动使用的场景外,都是被动使用
- JVM必须知道一个类型是由启动类加载器加载还是用户类加载器加载,用户类加载器加载的,JVM会将类加载器的一个引用作为类型信息的一部分保存到方法区
运行时数据区
方法区(元空间)
- 类的结构
- 运行时常量池(包括字符串常量池、类常量池等)
- 字段
- 方法和构造函数,包括类和接口初始化方法
- 方法区在逻辑上是堆的一部分,但不强制要求方法区的位置,在HotSpot VM中是单独的一块区域
- 可能抛出OOM
运行时常量池
- 从方法区分配,可以理解为方法区的一部分
- 包含多种常量,比如编译时已知的数字、运行时解析的方法和字段引用都有可能
- 类或接口的运行时常量池是在Java虚拟机创建类或接口时构造的
- 由于属于方法区的一部分,所以也会抛出OOM
堆区
- 包括类实例、数组
- 可能抛出OOM
- 年轻代
- from
- to
- suvivor
- 老年代
虚拟机栈
- 出现的背景
- JVM的指令集时根据栈设计的
- 优点
- 跨平台
- 指令集小,编译器容易实现
- 缺点
- 性能下降,同样的功能需要更多指令集
- 基于寄存器设计的指令集实现复杂,但性能更好
- 什么是Java虚拟机栈
- JVM中每个线程创建时都会创建一个虚拟机栈,内部保存的是一个个栈桢,对应着一次次Java方法调用
- 生命周期和线程一致
- 作用:主管Java程序的运行,保存方法的局部变量、部分结果,参与方法的调用和返回
- 栈的优点
- 栈是一种快速有效的分配存储方式,访问速度仅次于PC寄存器
- 只有入栈、出栈两个操作
- 不存在垃圾回收问题
- 面试题:开发中遇到的异常有哪些?
- Error:StackOverflowError、OutOfMemoryError、NoClassDefFoundError、NoSuchFieldError
- Checked Exception: IOException
- Unchecked Exception: RuntimeException、NPE、ValidationException
- 栈可能出现的异常
- JVM规范允许Java栈的大小是动态或者固定不变的
- StackOverflowError
- OutOfMemoryError
- 设置虚拟机栈大小
- -Xss1m | -Xss1024k
- 默认大小
- Linux 1024KB
- MacOS 1024KB
- Oracle Solaris 1024KB
- Windows 默认值基于虚拟内存
- 运行原理
- 压栈和出栈,先进后出
- 一个活跃线程中,只会有一个活动的栈桢,称为当前栈桢,对应的方法叫当前方法,对应的类叫当前类
- 执行引擎执行的所有字节码指令只针对当前栈桢操作
- 不同线程的栈桢不允许相互调用
- 当前方法返回结果时,当前栈桢会将执行结果返回给前一个栈桢,然后虚拟机丢弃当前栈桢
- 返回结果的方式有两种
- return正常返回
- 抛异常
- 栈内部结构
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)
- 动态链接(Dynamic Linking)(指向运行时常量池的方法引用)
- 方法返回地址(Return Address)
- 一些附加信息
- 局部变量表
- 它是一个数字数组,存储编译期可知类型的变量,包括方法参数、方法体内部的变量
- 每个方法都会有一个局部变量表
- 编译时就已确认局部变量表的大小,保存在方法的Code属性中
- 查看方式
javap -v target/classes/co/spraybot/Test.class
- idea -> 选中类 -> view -> show Bytecode with Jclasslib
- 字节码中方法内部结构(idea插件: jclasslib Bytecode Viewer)
- 变量槽的理解
- 局部变量表的基本存储单元是Slot(变量槽)
- 小于32位的类型只占一个slot(包括基本数据类型、引用类型、returnAddress),大于32位的类型占两个slot(long和double,注意Long和Double属于引用类型,只占一个槽)
- byte|short|char|boolean 存储前被转为int
本地方法栈
- 也俗称"C堆栈"
- 可以由Java虚拟机指令集的解释器实现使用
- 可能抛出StackOverflowError和OutOfMemoryError
程序计数器(PC寄存器)
- 引申一点线程相关知识:线程是CPU调用的基本单元,JVM中的线程和物理线程一对一映射
- 程序计数器的作用
- 记录当前线程下一条指令的偏移地址
- CPU先从寄存器读取下一条指令偏移地址,然后找到对应的指令,并执行
- 为什么要用PC寄存器,CPU直接执行不好吗?
- 线程的执行是分片式的,只有当线程分配到CPU时间片才开始执行,可能执行了一部分,时间片用完了,这个时候CPU跑去执行另外一个线程,当再次分配到时间片时,需要有东西告诉CPU执行到哪了,所以就需要PC寄存器保存当前线程的状态,告诉CPU下一步该执行哪条指令
- 为什么PC寄存器要每个线程都拥有一个?
- 在多个线程并发的情况下,线程A执行了一半,然后线程C开始执行,还未执行C,又轮到A执行,这种情况下最好的办法就是每个线程保存各自的状态
执行引擎
- JIT即时编译器
- 解释器
- 垃圾回收器
- Profiler
- Interpreter
本地方法接口
Java代码执行流程
Java源代码 ——> 编译器 ——> 字节码 ——>
类加载器 ——> 字节码校验器 ——> 解释器 | JIT编译器
–> 操作系统
JVM指令集架构
- 基于栈的指令集(JVM)
- 实现简单
- 与硬件无关,方便跨平台
- 基于寄存器的指令集(Android)
- 执行效率更高,性能更好
- 实现复杂
- 与硬件耦合
可以通过javap命令将java文件反编译,查看汇编指令
javap -v -l -c -p EnumI18nUtils.class
JVM生命周期
- 虚拟机的启动
- 通过引导类加载器(BootStrap ClassLoader)创建一个初始类(Intitial Class)来完成启动
- 虚拟机的执行
- 由Java虚拟机进程执行Java程序
- 虚拟机的退出
- 程序正常执行结束
- 遇到Exception或Error终止
- 调用Runtime.exit | System.exit | Runtime.halt
- JNI 卸载 Java 虚拟机
JVM发展历程
- Classic JVM
- 只有解释器,第一款商用java虚拟机
- jdk 1.4时被完全淘汰
- Exact VM
- jdk 1.2时,sun提供了此虚拟机
- 准确式内存管理
- 可以知道内存中某个位置的数据具体是什么类型
- 编译器与解释器混合工作模式
- HotSpot VM
- jdk 1.3开始作为默认的jvm
- 编译器与解释器协同工作
- 通过计数器找到热点点吗,触发即时编译
- JRockit VM
- 世界上最快的JVM
- 完全由即时编译器编译为机器码,启动速度慢,但执行速度飞快
- 用于服务器
- IBM J9 VM
- IBM公司研发,号称当前世界上执行最快的java虚拟机
- 用于服务器、客户端、嵌入式平台