此笔记来自于黑马程序员
基础篇
初识 JVM(Java Virtual Machine)
什么是 JVM
JVM 本质上是一个运行在计算机上的程序,他的职责是运行 Java 字节码文件
JVM 的功能
翻译成字节码
即时编译
- Java语言如果不做任何优化,性能不如C、C++等语言。
- Java 支持跨平台特性
- JVM 提供了**即时编译(Just-In-Time 简称JIT)**进行性能的优化,最终能达到接近C、C++语言的运行性能甚至在特定场景下实现超越。
常见的 JVM
JVM 的种类
Java 虚拟机规范
- 《Java虚拟机规范》由 Oracle 制定,内容主要包含了 Java 虚拟机在设计和实现时需要遵守的规范,主要包含 class 字节码文件的定义、类和接口的加载和初始化、指令集等内容。
- 《Java虚拟机规范》是对虚拟机设计的要求,而不是对 Java 设计的要求,也就是说虚拟机可以运行在其他的语言比如 Groovy、Scala 生成的 class 字节码 文件之上。
- 官网地址:https://docs.oracle.com/javase/specs/index.html
HotSpot 的发展历程
字节码文件详解
Java虚拟机 的组成
应用场景
- 解决工作中的实际问题-版本冲突
- 解决工作中的实际问题-系统升级
字节码文件的组成
以正确的姿势打开文件
- 字节码文件中保存了源代码编译之后的内容,以二进制的方式存储,无法直接用记事本打开阅读。
- 推荐使用 jclasslib 工具查看字节码文件.
- Github地址:https://github.com/ingokegel/jclasslib
字节码文件的组成
字节码文件的组成部分-Magic魔数
- 文件是无法通过文件扩展名来确定文件类型的,文件扩展名可以随意修改,不影响文件的内容。
- 软件使用文件的头几个字节(文件头)去校验文件的类型,如果软件不支持该种类型就会出错。
- Java字节码文件中,将文件头称为 magic 魔数,
字节码文件的组成部分-主副版本号
- 主副版本号指的是编译字节码文件的 JDK 版本号,主版本号用来标识大版本号,JDK1.0-1.1 使用了 45.0 - 45.3,JDK1.2 是 46 之后每升级一个大版本就加 1;副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号。
- 1.2之后大版本号计算方法就是:主版本号-44 比如主版本号 52 就是 JDK8
- 版本号的作用主要是判断当前字节码的版本和运行时的 JDK 是否兼容。
基础信息
字节码文件的组成部分-常量池
- 字节码文件中常量池的作用**:避免相同的内容重复定义,节省空间。**
- 常量池中的数据都有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速的找到对应的数据。
- 字节码指令中通过编号引引用到常量池的过程称之为符号引用。
字节码文件的组成部分-方法
- 操作数栈是临时存放数据的地方,局部变量表是存放方法中的局部变量的位置。
- iconst_x 将 常量x放入操作数栈
- istore_1 将 x sotre 到数组下标为1的地方
- iload_1 将数组下标为1 的值拷贝到操作数栈
- iadd 操作栈中的两数相加
- return 方法结束,返回
- iinc 1 by 1 在局部变量1号位置增加1
玩转字节码常用工具
Jclasslib github 既有 IDEA 插件也有 exe
Github地址:https://github.com/ingokegel/jclasslib
javap -v 命令
-
javap 是 JDK 自带的反编译工具,可以通过控制台查看字节码文件的内容。适合在服务器上查看字节码文件内容。
-
直接输入 javap 查看所有参数。
-
输入 javap -v 字节码文件名称 查看具体的字节码信息。(如果 jar 包需要先使用 jar-xvf 命令解压)
-
jclasslib 也有 ldea 插件版本,建议开发时使用 ldea 插件版本,可以在代码编译之后实时看到字节码文件内容。
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率。
- Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率。
- 官网: https://arthas.aliyun.com/doc/
- dump 类的全限定名:dump已加载类的字节码文件到特定目录。
- jad 类的全限定名:反编译已加载类的源码。
案例
总结
类的生命周期
类的生命周期描述了一个类加载、使用、、卸载的整个过程
生命周期概述
加载阶段
- 加载(Loading) 阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。程序员可以使用 Java 代码拓展的不同的渠道
- 类加载器在加载完类之后,Java虚拟机 会将字节码中的信息保存到方法区中
- 类加载器在加载完类之后,Java虚拟机 会将字节码中的信息保存到内存的方法区中生成一个 InstanceKlass 对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。
- 同时,Java虚拟机还会在堆中生成一份与方法区中数据类似的 java.lang.Class 对象。作用是在 Java 代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)。
- 对于开发者来说,只需要访问堆中的 Class对象 而不需要访问 方法区中所有信息这样 Java虚拟机 就能很好地控制开发者访问数据的范围
- 推荐使用 JDK 自带的 hsdb工具 查看Java虚拟机 内存信息。工具位于 JDK 安装目录下 lib 文件夹中的 sa-jdi.jar 中。
- 启动命令: java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
连接阶段
验证
验证内容是否满足《Java虚拟机规范》
- 连接(Linking)阶段的第一个环节是验证,验证的主要目的是检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束。这个阶段一般不需要程序员参与。
- 主要包含如下四部分,具体详见《Java虚拟机规范》:
文件格式验证,比如文件是否以 OxCAFEBABE 开头,主次版本号是否满足当前 Java虚拟机版本 要求。 - 元信息验证,例如类必须有父类**(super不能为空)**
- 验证程序执行指令的语义,比如方法内的指令执行到一半强行跳转到其他方法中去。
- 符号引用验证,例如是否访问了其他类中 private的 方法等。
- Hotspot JDK8 中虚拟机源码对版本号检测的代码如下,你能读懂它的含义吗:
准备
准备阶段为静态变量 (static) 分配内存并设置初始值。
- 注意:本章涉及到的内存结构只讨论 JDK8 及之后的版本,8 之前的版本后续章节详述。
- 准备阶段只会给静态变量赋初始值,而每一种基本数据类型和引用数据类型都有其初始值。
- final修饰的基本数据类型的静态变量**,准备阶段直接会将代码中的值进行赋值。**
解析
将常量池中的符号引用替换成指向内存的直接引用
- 符号引用就是在字节码文件中使用编号来访问常量池中的内容。
- 直接引用不在使用编号,而是使用内存中地址进行访问具体的数据。
初始化阶段(Most important)
初始化阶段会执行静态代码块中的代码并为静态变量赋值。 初始化阶段会执行字节码文件中 clinit 部分的字节码指令。(class init) 缩写为 clinit
- putstatic #2 <init/Demo1.value : I>
clinit 方法中的执行顺序与 Java 中编写的顺序是一致的
添加 -XX+TraceClassLoading 参数可以打印出加载井初始化的类
以下几种方式会导致类的初始化:
- 访问一个类的静态变量或者静态方法,注意变量是 **final修饰的并且等号右边是常量 **不会触发初始化。
- 调用 Class.forName(String className)。
- new 一个该类的对象时。
- 执行 Main 方法的当前类。
执行 main 方法先初始化 Test1 的初始化方法,输出结果 DA
clinit指令 在特定情况下不会出现,比如:如下几种情况是不会进行初始化指令执行的。
- 无静态代码块且无静态变量赋值语句。
- 有静态变量的声明,但是没有赋值语句。
- 静态变量的定义使用 final关键字,这类变量会在准备阶段直接进行初始化。
- 直接访问父类的静态变量,不会触发子类的初始化。
- 子类的初始化clinit 调用之前,会先调用父类的 clinit初始化方法。(类似于类的继承)
-
结论
-
final 修饰的变量如果赋值的内容需要执行指令才能得出结果,会执行 clinit方法 进行初始化
-
数组的创建不会导致数组中元素的类进行初始化。
类加载器
类加载器(ClassLoader)是 Java虚拟机 提供给应用程序去实现获取类和接口字节码数据的技术。类加载器只参与加载过程中的字节码获取并加载到内存这一部分。
类加载器的分类
类加载器的设计 JDK8 和 8 之后的版本差别较大,JDK8 及之前的版本中默认的类加载器有如下几种:
Arthas 中类加载器相关的功能
-
类加载器的详细信息可以通过 classloader 命令查看:
classloader -查看 classloader 的继承树,urls,类加载信息,使用 classloader 去 getResource
类加载器的分类-启动类加载器
- 启动 类加载器(Bootstrap ClassLoader)是由 Hotspot虚拟机 提供的、使用 C++编写的类 加载器。
- 默认加载 Java安装目录/jre/lib下 的类文件,比如 rt.jar,tools.jar, resources.jar 等。
类加载器的分类-Java 中的默认类加载器
- 扩展类加载器和应用程序类加载器都是 JDK 中提供的、使用 Java编写的类加载器。
- 它们的源码都位于 sun.misc.Launcher 中 ,是一个静态内部类。继承自 URLClassLoader。具备通过目录或者指定 jar包 将字节码文件加载到内存中
-
类加载器的加载路径可以通过 classloader -c hash值查看:
双亲委派机制
由于 Java虚拟机 中有多个类加载器,双亲委派机制的核心是解决一个类到底由谁加载的问题。
双亲委派机制指的是:当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过再由顶向下进行加载。
1.保证类加载的安全性
通过双亲委派机制避免恶意代码替换 JDK 中的核心类库,比如 java.lang.String,确保核心类库的完整性和安全性。
2.避免重复加载
双亲委派机制可以避免同一个类被多次加载。
向下委派加载起到了一个加载优先级的作用
- 每个 Java实现的类加载器 中保存了一个成员变量叫"父”(Parent)类加载器,可以理解为它的上级并不是继承关系。
打破双亲委派机制
- 一个 Tomcat程序 中是可以运行多个 Web应用 的,如果这两个应用中出现了相同限定名的类,比如 Servlet 类,Tomcat 要保证这两个类都能加载并且它们应该是不同的类。
- 如果不打破 双亲委派机制,当应用 类加载器加载Web应用中的 MyServlet 之后,Web应用2 中相同限定名 的MyServlet类 就无法被加载了。
- Tomcat 使用了自定义类加载器来实现应用之间类的隔离。每一个应用会有一个独立的类加载器加载对应的类。
打破双亲委派机制-自定义类加载器
- 先来分析 ClassLoader 的原理,,ClassLoader 中包含了 4个核心方法。双亲委派机制的核心代码就位于 loadClass 方法中。
-
阅读双亲委派机制的核心代码,分析如何通过自定义的类加载器打破双亲委派机制
-
打破双亲委派机制的核心就是将下边这一段代码重新实现
- 自定义类加载器默认的父类加载器
打破双亲委派机制的第二种方法:JDBC 案例
-
DriverManager 类位于 rt.jar包 中,由启动类加载器加载。
-
依赖中的 mysql驱动 对应的类,由应用程序类加载器来加载。
-
DriverManager 属于 rt.jar 是启动类加载器加载的。而 用户jar包 中的驱动需要由 应用类加载器 加载,这就违反了 双亲委派机制。
JDBC 案例之 SPI 机制
-
spi 全称为 (Service Provider Interface),是 JDK 内置的一种服务提供发现机制。
spi 的工作原理:
- 在 ClassPath路径下 的 META-INF/services文件夹 中,以接口的全限定名来命名文件名,对应的文件里面写该接口的实现。
- DriverManage 使用 SPi 机制,最终加载 jar包 中对应的驱动类。
流程总结
1、启动类加载器加载 DriverManager
2、在初始化 DriverManager 时,通过 SPI机制 加载 jar包 中的 myql 驱动。
3、SPI 中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。
4, 这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制。
打破双亲委派机制的第三种方法:OSGi 模块化
-
历史上,OSGi模块 化框架。它存在同级之间的类加载器的委托加载。OSGi 还使用类加载器实现了热部署的功能。
-
热部署指的是在服务不停止的情况下,动态地更新字节码文件到内存中。
注意事项:
1、程序重启之后,字节码文件会恢复,除非将 class文件 放入 jar包 中进行更新。
2、使用 retransform 不能添加方法或者字段,也不能更新正在执行中的方法。
JDK9 之后的类加载器
JDK8 及之前的版本中,扩展类加载器和应用程序类加载器的源码位于 rt.jar包 中的 sun.misc.Launcher.java。
-
由于 JDK9 引入了 module 的概念,类加载器在设计上发生了很多变化。
-
启动类加载器使用 Java编写,位于 jdk.internal.loader.ClassLoaders类 中。Java 中的 BootClassLoader 继承自 BuiltinClassLoader 实现从模
块中找到要加载的字节码资源文件。启动类加载器依然无法通过 java代码 获取到,返回的仍然 是null,保持了统一。
2、扩展类加载器被替换成了平台类加载器(Platform Class Loader)。
平台类加载器遵循模块化方式加载字节码文件,所以继承关系从 URLClassLoader 变成了BuiltinClassLoader,BuiltinClassLoader 实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,自身没有特殊的逻辑。
小总结
JVM 的内存区域
运行时数据区-总览
- Java虚拟机 在运行Java程序 过程中管理的内存区域,称之为运行时数据区。
- 《Java虚拟机规范》中规定了每一部分的作用。