本博客内容是博主学习时自己整理的,如有错误,欢迎大家积极指出
参考博客地址:JVM 类加载机制
类的生命周期
-
加载
- 通过包名+类名来获取此类的二进制字节流(即将class字节码加载到内存中)
- 将字节流中的静态数据结构转化为方法区的运行时数据结构(静态变量,静态代码块,常量池等)
- 在内存中(一般是堆)生成一个代表这个类的java.lang.Class对象,作为方法区这个类访问的入口
补充: 第一步中的获取类方法仅是一种方式,还可以通过zip、网络、数据库等方式获取
-
连接
-
验证
- 目的:确保Class字节流信息不会威胁到虚拟机
- 四种验证:
- 文件格式验证:验证字节流是否符合Class规范、主次版本号是否在当前虚拟机范围内,常量池中常量是否有不被支持的
- 元数据验证:对字节码描述信息进行分析,如是否存在父类、是否继承了不能被继承的类等
- 字节码验证:整个过程最复杂的阶段;通过数据流和控制流的分析,确定程序语义是否正确。主要针对方法体的验证(类型转换是否正确、跳转指定是否正确)
- 符号引用验证:在后面的解析过程中发生,确保解析动作能正常执行
-
准备
- 目的:为static修饰得变量分配内存地址并设置类变量得初始值,这些变量使用的内存都在方法区进行分配
补充:
- 这里进行分配的仅包括类变量(static修饰的变量),不包括实例变量。实例变量将会在对象实例化时,随着对象一起分配在Java堆中。
- 初始化值通常为0值,赋值是在初始化中进行的
- 基本数据类型初始化0值表
- 目的:为static修饰得变量分配内存地址并设置类变量得初始值,这些变量使用的内存都在方法区进行分配
-
解析
- 将JVM常量池内的符号引用替换为直接引用的过程(这一步可能在初始化之前,也可能在初始化之后)
- 解析大体分为:
- 类或接口解析
- 字段解析
- 类方法解析
- 接口方法解析
补充:
- 解释符号引用:java类在编译成class文件时,虚拟机不知道其确切地址,便用符号代替,解析就是将符号替换为真正地址
- 常出现在这个阶段的异常:
- NoSuchFieldError:根据继承关系从下往上,找不到相关字段时的报错。(字段解析异常)
- IllegalAccessError:字段或者方法,访问权限不具备时的错误。(类或接口的解析异常)
- NoSuchMethodError:找不到相关方法时的错误。(类方法解析、接口方法解析时发生的异常)
-
-
初始化:
- 主要是对类中的static{}进行操作,对应字节码()方法。(对于类或接口不是必须)
- 虚拟机规定必须对类进行初始化的6种情况
- 遇到new、getstatic、putstatic、invokestatic字节码指令,如果类没有进行初始化,则初始化该类。会出现这些指令的情况
- new 实例化对象时
- 读取或者设置一个静态字段时(被final修饰,在编译器已经进入常量池的字段除外)
- 调用一个类的静态方法时
- 通过java.lang.reflect包进行反射调用时,如果类没有初始化,则需要对其进行初始化
- 当初始化一个类时,其父类还未初始化时,则需要对其父类进行初始化
- 当虚拟机启动时,需要指定主类(含mian()方法),虚拟机将初始化此类
- JDK1.7使用动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic、REF_putsattic、REF_invokestatic的方法句柄,并且这个方法句柄所对应的类没有初始化,则需要对其进行初始化
- 如果接口中定义了jdk1.8加入的默认方法(接口被default修饰),如果实现这个接口的类被初始化,那么接口应该在其之前被初始化
- 补充:
- 动态语言:
- 句柄:
- 遇到new、getstatic、putstatic、invokestatic字节码指令,如果类没有进行初始化,则初始化该类。会出现这些指令的情况
-
使用
-
卸载
类加载器
-
作用
- 执行使用之前的整个过程(加验准解出)
-
种类
-
启动类加载器(Bootstrap ClassLoader)
- 加载器的核心类库,rt.jar、resources.jar、charsets.jar,由C++编写,随JVM启动
-
扩展类加载器(Extension ClassLoader)
- 用于加载lib/ext目录下的的jar和.class文件
- 是一个Java类,继承自URLClassLoader
- 可通过系统变量java.ext.dirs指定目录
-
应用程序类加载器(Application ClassLoader)
- 我们写的Java类的默认加载器,也叫System ClassLoader
- 一般用来记载classpath下的其他jar和.class文件
- 我们写的代码一般会首先尝试使用这个类加载器进行加载
-
自定义类加载器(User ClassLoader)
- 个性化的定义一些功能扩展
-
双亲委派模式
-
除了Bootstrap CLassloader之外,其他的类加载器都应该有自己的父类。这里的父类不是继承和是一种叫组合的方式
-
java.lang.ClassLoader中实现双亲委派模式代码
-
流程:
- 在已经加载的类中查找是否存在,存在则返回
- 不存在,委托父类加载器进行加载(第二步的递归),如果父类没有加载,就委托父类的父类加载,直到根类加载器
- 如果根加载器都无法加载则抛出ClassNotFoundException异常,然后再调用findClass进行加载
-
通过上面的流程我们可以发现,如果要自定义类加载器,只需要重写findClass方法即可,不需要重写loadClass方法
-
双亲委派模式的优点
- 安全性,避免Java核心api被篡改;比如用户通过自定义类加载器加载了一个自己写的java.lang.String类到JVM中,那么就可能造成病毒代码植入
- 双亲委派模式从最顶端的加载器开始执行,则始终不会加载自定义的String
- 避免重复加载,即避免同样的字节码多次加载
- 补充:
- 上述避免重复加载只是针对同一个类加载器来说,要比较两个类是否相等,只有在这两个类是同一个类加载器加载的前提下才有意义,否则,即使是同一个类被不同类加载器加载那么他们也一定是不相等的
- 这里的相等包括Class对象的equal()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况
- 自定义类加载器
- 为什么需要自定义类加载器
- 加密;因为Java代码可以很轻松的进行反编译,所以你不想让别人看你的源码,你就可以将编译后的代码进行某种加密,这时ClassLoader是无法进行加载的,所以需要自定义类加载器先解密然后进行加载
- 非标准来源的代码;比如字节码是放在云、数据库等地方的
- 不破坏双亲委托模式的自定义类加载器,只需要重写findClass()方法即可
- 为什么需要自定义类加载器
-
-
破坏双亲委派模式
- Java SPI(service provider interface) 和 tomcat 就是破坏双亲委派模式的例子
- SPI模式讲解(以JDBC为例子):jdk提供了一套由三方实现或扩展的API,三方厂商可以通过这些API对应用进行补充或扩展
-
以前我们连接数据库的步骤,其中我们会通过Class.forName()方法去加载驱动类到内存;那么具体在这过程中做了些什么事呢?
-
8.0之前手动加载驱动的流程
-
Mysql8.0后我们可以使用SPI自动注册,不用手动Class.forName()去加载
-
SPI是怎么自动注册的
-
加载驱动过程【因为博主用的JDK17,和上面博客链接中的流程有所不同,17中加了些加载时候对权限的判定】
-
从加载驱动的源码中可以看到,多处的地方都用到了Class.forName(),那么Class.forName()是怎么破坏双亲委托模式的呢?
-
在
ensureDriversInitialized()
方法中有一段代码ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
-
点进其中的load方法
ClassLoader cl=Thread.currentThread().getContextClassLoader();
,可以看到 -
接下来的步骤
-
可以从上面的步骤看到,最后通过指定Class.forName()的类加载器,实现破坏双亲委托模式【这只是自动注册实现破坏双亲委托模式,其实CLass.forName()中就是使用Caller的加载器进行加载的,可以debug看看】
-
-
- 总结:
- 实现双亲委派模式
- 在 jar 包的 META-INF/services 目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名
- 接口实现类所在的 jar 包在 classpath 下
- 主程序通过 java.util.ServiceLoader 动态状态实现模块,它通过扫描 META-INF/services 目录下的配置文件找到实现类的全限定名,把类加载到 JVM
- SPI 的实现类必须带一个无参构造方法
- 技术实现:主要是靠接编程+策略模式+配置文件组合实现的动态加载机制,使用java.util.ServiceLoader类进行的动态加载。流程如下:
- 实现双亲委派模式
- SPI模式讲解(以JDBC为例子):jdk提供了一套由三方实现或扩展的API,三方厂商可以通过这些API对应用进行补充或扩展
- Java SPI(service provider interface) 和 tomcat 就是破坏双亲委派模式的例子
结束语
博主才开始看源码,确实看得有点头大,里面有很多东西还是一脸懵逼,不过看一遍下来还是收益良多!还有一点要说的就是jdk版本不一样,很多逻辑有出入,不过好像原理都差不多!