第一章-前言
1.什么是JVM
- 什么是JVM
Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境) - 好处
-
- 一次编译,处处执行
- 自动的内存管理,垃圾回收机制
- 数组下标越界检查
- JVM、JRE、JDK 的关系
2.学习 JVM 有什么用
- 面试必备
- 中高级程序员必备
- 想走的长远,就需要懂原理
3.常见的 JVM
https://en.wikipedia.org/wiki/Comparison_of_Java_virtual_machines
我们主要学习的是 HotSpot 版本的虚拟机
OracleJDK和OpenJDK的区别:
我来告诉你 Oracle JDK 与 OpenJDK 之间到底有什么区别 - 死磕 Java
4.JVM组成
- ClassLoader:Java 代码编译成二进制后,会经过类加载器,这样才能加载到 JVM 中运行。
- Method Area:类是放在方法区中。
- Heap:类的实例对象。
- 当类调用方法时,会用到 JVM Stack、PC Register、本地方法栈。
- 方法执行时的每行代码是有执行引擎中的解释器逐行执行,方法中的热点代码频繁调用的方法,由 JIT 编译器优化后执行,GC 会对堆中不用的对象进行回收。需要和操作系统打交道就需要使用到本地方法接口。
5.小结
Q1:什么是JVM?
Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境)
Q2:JVM的组成部分?
- 类加载器
- 运行时数据区(JVM内存结构)
- 执行引擎
- 本地方法接口/本地方法库
第二章-JVM加载机制
1.类装载子系统
1.1介绍
- 类加载子系统负责从文件系统或是网络中加载.class文件(字节码文件),class文件在文件开头有特定的文件标识。
- 把加载后的class类信息存放于方法区,除了类信息之外,方法区还会存放运行时常量池信息
- ClassLoader只负责.class文件的加载,至于它是否可以运行,则由Execution Engine决定;
- 如果调用构造器实例化对象,则该对象存放在堆区;
1.2类加载的过程
1.2.1加载
将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
- _java_mirror: 这是 Java 对象的一个引用,指向一个代表该类的 java.lang.Class 实例。这个实例通常被称为“类对象”或“类镜像”。每个加载到 JVM 中的类都会有一个对应的 Class 对象,它允许 Java 程序在运行时访问类的信息。例如对 String 来说,就是 String.class的引用。
- _fields 即成员变量, 存储了类的所有字段信息,包括字段名、类型、访问标志等。这些信息对于反射操作非常重要。
- methods 即方法, 存储了类的所有方法信息,包括方法名、返回类型、参数列表、字节码等。这也用于支持反射调用
- _constants 即常量池 。常量池包含了类文件中的所有常量值,如字符串字面量、final 变量的值等,以及符号引用(比如类名、方法名和方法描述符),这些都是解析和链接阶段需要用到的数据。
- class_loader 即类加载器, 是一个指向加载此类的类加载器的引用。类加载器负责从文件系统、网络或其他来源加载类定义,并创建相应的
Class
对象。 - vtable(虚方法表): 这是用于实现动态绑定(多态性)的数据结构。每个类都有一个虚方法表,其中包含了该类及其超类中所有可被重写的方法的入口点。
- itable(接口方法表): 当一个类实现了某个接口或者继承了一个实现了接口的类时,itable 会记录下这些接口的方法与实际方法之间的映射关系,以便于快速查找和调用。
- _super: 这是一个指向直接父类的
Klass
对象的指针。如果类没有显式地声明继承自另一个类,则默认继承自Object
类
加载后的结构:
- instanceKlass 是存储在方法区(1.8 后的元空间内,本地内存),但 _java_mirror是存储在堆中
- instanceKlass和.class (JAVA镜像类)互相保存了对方的地址
- 每个 Java 对象的头中包含一个类型指针,指向该对象所属类的 InstanceKlass 对象。通过这个类型指针,对象可以找到方法区中的 InstanceKlass 对象。InstanceKlass 对象中有一个 _java_mirror 字段,指向一个存储在 Java 堆中的 java.lang.Class 实例。这样,对象可以通过类型指针找到方法区中的 InstanceKlass,再通过 _java_mirror 字段获取 java.lang.Class 实例,从而获取类的各种信息。
注意:
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
1.2.2链接
验证
验证类是否符合 JVM规范,安全性检查等
准备
准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配
- 这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中
- 这个阶段赋初始值的变量指的是那些不被final修饰的static变量
-
- 比如"public static int value = 123",value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行
- 比如"public static final int value =123", final修饰的类常量, 就不一样了,在准备阶段,虚拟机就会给value赋值为123
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程 。
- 符号引用
符号引用是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。
- 直接引用
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经存在在内存中了 。 - 例子: 符号引用
-
- 符号引用
代码 Class<?> c = classloader.loadClass("com.lcuyp.jvm.f_load.C");
- 符号引用
package com.lcuyp.jvm.a_load;
/**
* @Description: 符号引用测试: 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程 。
* @author: yp
* 步骤:
* 1.jps 查出进程ID
* 2.使用HSDB工具, 进入JDK的安装目录执行:
* java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
*
*/
public class Load01 {
public static void main(String[] args) throws Exception {
ClassLoader classloader = Load01.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
Class<?> c = classloader.loadClass("com.lcuyp.jvm.a_load.C");
//C c = new C();
System.in.read();
}
}
//符号引用: com.lcuyp.jvm.a_load.D 但不是D的具体的地址
//直接引用: 具体的D的地址
class C {
D d = new D();
}
class D {
}
-
- 查出进程ID
jps
-
- 使用HSDB工具: 进入jdk的安装目录 D:\devsoft\java\java8\jdk, 执行
java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
- 例子: 直接引用
代码: C c = new C();
package com.lcuyp.jvm.a_load;
/**
* @Description: 符号引用测试: 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程 。
* @author: yp
* 步骤:
* 1.jps 查出进程ID
* 2.使用HSDB工具, 进入JDK的安装目录执行:
* java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
*
*/
public class Load01 {
public static void main(String[] args) throws Exception {
ClassLoader classloader = Load01.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
//Class<?> c = classloader.loadClass("com.lcuyp.jvm.a_load.C");
C c = new C();
System.in.read();
}
}
//符号引用: com.lcuyp.jvm.a_load.D 但不是D的具体的地址
//直接引用: 具体的D的地址
class C {
D d = new D();
}
class D {
}
参考符号引用的例子, 使用HSDB再操作一遍, 此时D是直接引用了
注: HSDB(Hotspot Debugger),JDK自带的工具,用于查看JVM运行时的状态。位于JDK安装目录 …/jdk/lib/sa-jdi.jar 包中,是sa-jdi.jar包中的一个函数。
1.2.3初始化
- 初始化阶段就是执行<cinit>方法的过程。<cinit>并不是程序员在Java代码中直接编写的方法, 它是Javac编译器的自动生成物。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块) 中的 语句合并产生的, 编译器收集的顺序是由语句在源文件中出现的顺序决定的, 静态语句块中只能访问到定义在静态语句块之前的变量, 定义在它之后的变量, 在前面的静态语句块可以赋值, 但是不能访问
package com.lcuyp.jvm.a_load;
/**
* 静态语句块中只能访问到定义在静态语句块之前的变量, 定义在它之后的变量, 在前面的静态语句块可以赋值, 但是不能访问
*/
public class TestClinit02 {
static int j = 2;
static {
i = 0; // 给变量赋值可以正常编译通过
//System.out.print(i); // 这句编译器会提示“非法向前引用”
j=0; //赋值
System.out.println(j); //访问了j
}
static int i = 1;
}
- 发生的时机:概括得说,类初始化是【懒惰的】
-
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
- 不会导致类初始化的情况
-
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象.class 不会触发初始化
- 创建该类的数组不会触发初始化
- ()方法与类的构造函数(即在虚拟机视角中的实例构造器()方法) 不同, 它不需要显 式地调用父类构造器, Java虚拟机会保证在子类的()方法执行前, 父类的()方法已经执行 完毕。 因此在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object。 由于父类的()方法先执行, 也就意味着父类中定义的静态语句块要优先于子类的变量赋值 操作
- ()方法对于类或接口来说并不是必需的, 如果一个类中没有静态语句块, 也没有对类变量的赋值操作, 那么编译器可以不为这个类生成()方法。 接口中不能使用静态语句块, 但仍然有变量初始化的赋值操作, 因此接口与类一样都会生成 ()方法。 但接口与类不同的是, 执行接口的()方法不需要先执行父接口的()方法, 因为只有当父接口中定义的变量被使用时, 父接口才会被初始化。 此外, 接口的实现类在执行()初始化时也 一样不会执行接口的()方法 。
package com.lcuyp.jvm.f_load;
/**
* @Description:
* @author: yp
*/
public interface InterfaceA {
public static int a = 10;
}
1.3<cinit>
与<init>
- 例子
package com.lcuyp.jvm.a_load;
public class ParentA {
static {
System.out.println("1");
}
public ParentA() {
System.out.println("2");
}
}
class SonB extends ParentA {
static {
System.out.println("a");
}
public SonB() {
System.out.println("b");
}
public static void main(String[] args) {
ParentA ab = new SonB();
ab = new SonB();
}
}
运行结果:
1.4小结
Q1: 类装载子系统的作用?
类加载子系统负责从文件系统或是网络中加载.class文件(字节码文件),把加载后的class类信息存放于方法区
Q2:类加载的过程是怎么样的?
- 加载
- 链接
-
- 验证
- 准备(类常量赋值, 类变量赋初始值)
- 解析
- 初始化(类变量赋值)
Q3:<cinit>与<init>的区别?
- <cinit>是一个类构造函数,是Javac编译器的自动生成物; <init>是一个实例构造函数,
- <cinit>在类加载时被调用,只会执行一次; <init>在创建对象时被调用
2.类加载器
2.1作用
类加载器的作用是将类的.class文件中的二进制数据读入到内存中,将其放在方法区内,然后再创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
注意:
JVM主要在程序第一次主动使用类的时候,才会去加载该类,也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次
2.2类加载器分类
名称 | 加载哪的类 | 说明 |
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
- 引导类加载器
-
- 这个类加载器使用c/c++实现,嵌套在jvm内部
- 它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的内容),
- 并不继承自java.lang.ClassLoader,没有父加载器
- 扩展类加载器
-
- java语言编写,由sun.misc.Launcher$ExtClassLoader实现
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的JAR 放在此目录下,也会自动由扩展类加载器加载;
- 父类加载器为启动类加载器
- 应用加载器
-
- java语言编写,由 sun.misc.Lanucher$AppClassLoader 实现
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载的,它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库
- 父类加载器为扩展类加载器
- 通过 ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器
2.3小结
Q1: 类加载器有什么用?
类加载器的作用是将类的.class文件中的二进制数据读入到内存中,将其放在方法区内,然后在创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
Q2: 类加载器有哪几种?
一共有3种:
- 引导类加载器
- 扩展类加载器
- 应用类加载器
3.双亲委派模型
3.1介绍
双亲委派模型是Java类加载机制中的一种机制,它是一种层次化的类加载体系结构。在该模型中,类加载请求会依次向上委派给父类加载器进行处理,直到达到顶层的启动类加载器。只有当父类加载器无法找到对应的类时,子类加载器才会尝试加载该类。
3.2为什么要设计双亲委派机制
- 避免类的重复加载: 当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
- 沙箱安全机制: 比如jdk里面的定义的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
package java.lang;
/**
* @Description:
* @author: yp
*/
public class String {
public static void main(String[] args) {
System.out.println("我的StringClass");
}
}
原因: jdk是通过启动类加载器加载的String, JDK里面的String是没有main()方法的。
3.3小结
Q1: 什么是双亲委派模型?
双亲委派模型是一种层次化的类加载体系结构,类加载请求会依次向上委派给父类加载器进行处理,直到达到顶层的启动类加载器。只有当父类加载器无法找到对应的类时,子类加载器才会尝试加载该类。
Q2:双亲委派模型有什么作用?
- 避免类的重复加载
- 沙箱安全机制