1.内存结构概述
2.类加载子系统作用
- 负责从文件系统或者网络中加载Class文件(该文件在文件开头有特定的标识)
- 只负责Class文件的加载,至于能否运行由
Execution Engine
决定- 加载的类信息被存放在称为方法区的内存空间中
tips:
- 除了类的信息,方法区还会存放运行时常量池信息(可能包括字符串常量和数字常量,这部分信息是Class文件中常量池部分的内存映射)
- 将Class文件进行反编译会发现结构中有常量池,将该信息加载到内存中就是运行时常量池
3.类的加载过程
3.1 加载
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将该字节流代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的
java.lang.Class
对象作为方法区中该类的数据访问入口
3.2 链接
验证:确保Class文件的字节流中包含的信息符合当前JVM要求,确保加载类的准确性
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备:为**类变量(静态变量)**分配内存并且设置该类变量的默认初始值
- 不包含分配内存给
final
修饰的静态变量(视为常量),因为final
在编译时就会分配,在准备阶段会显示初始化- 不会为实例变量分配内存并初始化,因为类变量分配在方法区,而实例变量随着对象分配到堆中
解析:将常量池内的符号引用转换为直接引用的过程(实际上解析是在JVM执行完初始化后再执行)
- 符号引用:一组符号用于描述所引用的目标
// 像#1就是使用到的类的符号引用 Constant pool: #1 = Methodref #8.#30 // java/lang/Object."<init>":()V #2 = Class #31 // com/atguigu/SellTicket02 #3 = Methodref #2.#30 // com/atguigu/SellTicket02."<init>":()V #4 = Class #32 // com/atguigu/SellTicket03 #5 = Methodref #4.#30 // com/atguigu/SellTicket03."<init>":()V #6 = Methodref #4.#33 // com/atguigu/SellTicket03.start:()V #7 = Class #34 // com/atguigu/test
- 直接引用:直接指向目标的指针、相对偏移量或间接定位到目标的句柄
3.3 初始化
初始化过程就是执行类构造器方法的过程
- 该方法不需要定义,是
javac
编译器自动收集类中的所有类变量赋值和静态代码块中的语句合并而来- 方法中的指令是按语句在源文件中出现顺序执行
- 如果类中没有静态代码块或类变量就不会生成该方法
- 若该类有父类,JVM会保证父类的先执行后再执行子类的
- JVM必须保证一个类的方法在多线程下被同步加锁
Runnable r = () -> { System.out.println(Thread.currentThread().getName() + "开始"); DeadThread deadThread = new DeadThread(); System.out.println(Thread.currentThread().getName() + "结束"); }; Thread thread1 = new Thread(r, "线程1"); Thread thread2 = new Thread(r, "线程2"); thread1.start(); thread2.start(); // 定义类 class DeadThread{ // 用于产生<cinit>方法,死循环用于让该方法一直执行。如果该方法不是同步加锁的,则另一个线程也会执行该方法,最终结果会输出两个线程都在进行初始化 static { if (true){ System.out.println(Thread.currentThread().getName() + "进行初始化"); while (true){} } } } // 输出结果: //线程1开始 //线程2开始 //线程1进行初始化(此时线程2因为同步加锁的原因无法执行<cinit>方法)
类构造器方法不同于平日提到的类的构造器(在JVM视角下构造器是方法)
tips:
- 如下面代码所示,为什么可以先赋值再声明?在链接中的准备阶段,已经对类变量number分配内存并设置默认值0,然后在初始化阶段执行
clinit
方法按语句顺序先完成静态代码块的赋值(即number=5),再初始化为10static { // 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问 number = 5; // 赋值不报错 System.out.println(number); // 编译器会报错 } public void m(){ // 不会报错,因为执行该方法时,在堆中的对象中已经有了一个名为number的引用并初始化指向了 System.out.println(number); } private static int number = 10; // 测试 System.out.println(test2.number); // 打印10
4.加载器的分类
- 自定义类加载器:所有派生于抽象类
ClassLoader
的类加载器(即使用Java语言编写)都划分为自定义加载器,图中的Extension Class Loader
和System Class Loader
因为间接继承了ClassLoader
,所以也属于该类加载器
- 扩展类加载器
Extension Class Loader
:
- 父类加载器为引导类加载器
- 如果用户创建的jar包放在JDK的
jre/lib/ext
目录下,会自动由扩展类加载器加载- 系统类加载器(应用程序加载器)
System Class Loader
:
- 父类加载器为扩展类加载器
- 程序中默认的类加载器,一般来说Java应用的类都是由它完成加载
- 用户自定义类加载器:
- 为什么要自定义类加载器:
- 隔离加载类:使用多个框架时可能出现类的冲突
- 修改类加载的方式
- 扩展加载源
- 防止源码泄露:相当于自定义编译和反编译方式
- 引导类(启动类)加载器(
Bootstrap ClassLoader
)
- 使用
C/C++
实现,嵌套在JVM内部- 用于加载Java核心库(如
JAVA_HOME/jre/lib/rt.jar
),提供JVM自身需要的类- 不继承
java.lang.ClassLoader
,没有父类加载器- 加载扩展类和应用程序类加载器,并指定为它们的父类加载器
- 处于安全考虑,只记载包名为
java、javax、sun
等开头的类
// 获取系统类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); // JDK11:jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc // JDK8:sun.misc.Launcher$AppClassLoader@... System.out.println(systemClassLoader); // 获取系统类加载器的父类加载器 --> 扩展类加载器 // JDK11:jdk.internal.loader.ClassLoaders$PlatformClassLoader@7c30a502 // JDK8:sun.misc.Launcher$ExtClassLoader@... ClassLoader parent = systemClassLoader.getParent(); System.out.println(parent); // 获取扩展类加载器的父类加载器 --> 引导类加载器(获取不到) ClassLoader parent1 = parent.getParent(); System.out.println(parent1); // null // 获取当前用户自定义类的加载器(默认使用系统类加载器) ClassLoader curLoader = ClassLoaderTest.class.getClassLoader(); // JDK11:jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc // JDK8:sun.misc.Launcher$AppClassLoader@... System.out.println(curLoader); // 获取系统核心类库的加载器(获取不到,即使用引导类加载器) ClassLoader sysloader = String.class.getClassLoader(); System.out.println(sysloader); // null
tips:
- 上述提到的父类并不是指继承关系,而是层级上的关系
- JVM为什么需要那么多类加载器?一方面允许在一个JVM里运行不同的应用程序,另一方面方便用户独立的对不同类库进行运行时增强
5.双亲委派机制
概念:JVM对Class文件采取的按需加载(即需要使用某个类才将它的Class文件加载到内存生成对象),而加载该类的Class文件时采用双亲委派模式(即把请求交给父类处理,它是一种任务委派模式)
工作原理:
- 如果一个类加载器收到了类加载的请求,它并不会自己先去加载,而是把该请求委托给父类加载器去执行
- 如果父类加载器还存在父类加载器,则继续向上委托,直到请求到达引导类加载器
- 如果父类加载器可以完成类的加载,就成功返回;如果无法完成,子类加载器才去尝试自己加载
- 场景:创建一个
java.lang
包,且在该包下写一个String
类// src/com/psj/test.java public static void main(String[] args) { String s = new String(); System.out.println("程序正常执行"); } // src/java/lang/String public class String { // 如果加载的是该类就会执行该静态代码块 static { System.out.println("执行自定义的String类"); } public static void main(String[] args) { System.out.println("程序正常执行"); } } // 执行test.java最终结果还是执行系统类库的Strig类 // 执行Strng.java会报错,因为在核心库中的String类是没有定义main方法的 // 原因:自定义String类的类加载器默认为系统类加载器,此时由于双亲委派机制,系统类就向上一直找,最终找到引导类加载器。引导类加载器判断该类处在Java开头的包下,可以进行加载,但是加载的是核心库中的String,完成加载后就成功返回不会再向下执行
优点:
- 避免了类的重复加载
- 保护程序的安全,防止核心API被随意修改
- 保护引导类加载器:
// src/java/lang/Psj.java public class Psj { public static void main(String[] args) { System.out.println("程序正常运行"); } } // 最终程序会报错,即使类名不同,使用的包名为核心库下的包名依旧不行,防止破坏引导类加载器
tips:
- 加载器能否加载类要看该类是否在指定的包路径下,比如引导类加载器就会去加载Java开头的包下的类
- 上述场景对Java核心源代码以及引导类加载器的保护称为沙箱安全机制
6.其他
在JVM中表示两个Class对象是否为同一个类有两个必要条件:
- 类的完整类名必须一致(包括包名)
- 加载该类的
ClassLoader
必须相同方法区中会保存该类是由什么加载器进行加载的
类的使用分为主动使用和被动使用:
主动使用:
- 创建类的实例
- 访问访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射
- 初始化某类的子类
- JVM启动时被标记为启动类的类
被动使用:除了以上情况,其他使用Java类的方式都是对类的被动使用,不会导致类的初始化