- 类加载器子系统整体结构
- 类加载过程
- 加载
- 加载过程
- 类加载器
- 引导类加载器
- 扩展类加载器
- 应用类加载器
- 自定义类加载器
- 线程上下文类加载器
- JVM类加载双亲委托机制
- JVM类加载双亲委托机制+线程上下文类加载器
- Tomcat类加载机制
- 链接
- 验证
- 准备
- 解析
- 初始化
- 初始化过程
- 类的初始化时机
- class对象同类判断
- 同类同类加载器
- 作者声明
- 输入为.class文件
- 输出到Runtime-运行时数据区中
- 对.class文件进行加载》链接(验证-准备-解析)》初始化
- 类加载子系统只负责加载.class文件到运行时数据区中,不负责编译检查和运行检查
- 编译时检查有javac(前端编译器)决定;运行时检查执行引擎中解释器和JIT(即时编译器)决定
- 双亲委托机制
- 其它机制
过程:
- 判断:加载前先判断是否已经加载过这个类了
- 获取.class文件:通过类的全限定名获取此类的二进制字节流
- 生成Class对象:在运行时数据区下的方法区中生成java.lang.Class对象,作为类信息的访问入口
具体方法:输入为类的全限定名,输出为Class对象
//Class> loadClass(String name)
public Class> loadClass(String name) throws ClassNotFoundException {
}
//Class> findClass(String name)
protected Class> findClass(String name) throws ClassNotFoundException {
}
//Class> defineClass(String name)
protected final Class> defineClass(String name, java.nio.ByteBuffer b,ProtectionDomain protectionDomain) throws ClassFormatError{
}
类加载方式-.class文件加载方式
- 本机获取加载
- 网络获取加载
- zip包中获取加载(jar和war格式是zip格式)
- 加密文件中获取加载(防止.class文件被反编译)
- 动态生成加载(动态代理)
- 其它文件或数据库中获取加载(JSP应用)
JVM类加载器
- BootStrap ClassLoader(引导类加载器)
- Extension ClassLoader(扩展类加载器)
- Application ClassLoader(应用类加载器)
线程上下文类加载器 - 一般是Application ClassLoader(应用类加载器)
BootStrap ClassLoader(引导类加载器)
- 用来加载JVM自身所需要的类
- 加载扩展类加载器和应用类加载器,并指定为它们的父类加载器
- C/C++语言编写实现
- 不继承java.lang.ClassLoader
- 用来加载java核心库,提供了JVM自身所需要的类
- JAVA_HOME/jre/lib/rt.jar
- resource.jar
- sun.boot.class.path路径下的内容
- 为了安全,它只加载包名为java, javax, sun等开头的类
Extension ClassLoader(扩展类加载器)
- 用来加载扩展库
- 向上委托BootStrap ClassLoader(引导类加载器)
- java语言编写实现
- 继承java.lang.ClassLoader
- 加载位置
- jre/lib/ext下加载类库
- java.ext.dirs系统属性指定路径下的类
Application ClassLoader(应用类加载器)
- 用来加载应用程序
- 程序中默认的类加载器
- 向上委托Extension ClassLoader(扩展类加载器)
- java语言编写实现
- 继承java.lang.ClassLoader
- 加载位置
- 加载环境变量classpath路径下的类
- java.class.path系统属性指定路径下的类
原因
- 隔离加载类:引入多个框架依赖的类,路径相同类名也相同,类的内容不同
- 修改类加载方式:启动类加载器是一定会调用的,JVM用户自定义类加载器可以调整
- 扩展加载源:网络中*.class文件、DB中*.class文件
- 防止源码泄露:加密后的字节码文件,自定义累加器解密后运行
步骤
- extends Classloader》重写findClass()方法 (findClass()方法中需要有获取字节码流的过程)
- 或extends URLClassloader类,不需要自己编写findClass()方法
方法
- 输入为类的全限定名,输出为Class对象
//Class> loadClass(String name)
public Class> loadClass(String name) throws ClassNotFoundException {
}
//Class> findClass(String name)
protected Class> findClass(String name) throws ClassNotFoundException {
}
//Class> defineClass(String name)
protected final Class> defineClass(String name, java.nio.ByteBuffer b,ProtectionDomain protectionDomain) throws ClassFormatError{
}
线程上下文类加载器
- 出现原因
- 如果基础类要调用用户代码
- 需要父类加载器请求子类加载器去完成类加载的动作
- JDBC标准:JDBC接口由java团队实现;JDBC实现由不同厂商负责
- 设置
- 这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,
- 如果创建线程时还未设置,它将会从父线程中继承一个,
- 如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
- 应用场景
- Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等
- 结果
- 在一定程度上打破了双亲委托机制
过程
- 类加载器收到类加载请求,先交由直接双亲加载器加载
- 双亲加载器收到委托类加载请求,先交由直接双亲加载器加载,最终到达顶层的BootStrap ClassLoader(引导类加载器)
- 如果双亲类加载器能够完成类加载任务则成功返回,不能完成类加载任务则返还子加载器尝试加载,子加载器也不能加载则抛出异常
注意
- 双亲委托不是继承
- 扩展类加载器没有继承引导类加载器而是继承ClassLoader
- 应用类加载器没有继承扩展类加载器而是继承ClassLoader
- 引导类加载器使用C语言/C++语言编写没有继承ClassLoader
- SPI接口由引导类加载器加载
- SPI接口实现由线程上下文类加载器(默认是App Class Loader)加载
- SPI接口调用SPI接口实现
- 举例:
- jdbc接口是SPI接口,jdbc.jar是SPI接口实现
- Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等
知识基础
- jsp文件本质是servlet,servlet就是一个java类
- jvm类加载器通过类的全类名判断是否已经加载过该类
- 导致问题1:相同类库不同版本无法区分
- 导致问题2:jsp文件修改后由于全类名未变不会重新加载jsp文件
Tomcat(web容器)需要解决的问题
- 自身依赖问题:
- web容器本身有依赖的类库,不能与web应用程序依赖的类库混淆。
- 版本问题:
- 相同版本依赖相互共享:不同web应用程序依赖同一个第三方类库的相同同版本,保证相同同版本依赖相互共享。
- 不同版本依赖相互隔离:不同web应用程序依赖同一个第三方类库的不同版本,保证不同版本依赖相互隔离。
- jsp热修改问题:
- web容器(tomcat)jsp修改后不用重启web容器(tomcat)问题
Tomcat(web容器)解决问题的类加载器
自身依赖问题
- 通过Catalina类加载器(Catalina ClassLoader)加载Tomcat需要的依赖类库
相同版本依赖相互共享: - 通过Shared类加载器(Shared ClassLoader)加载web应用共享的依赖类库
不同版本依赖相互隔离: - 每个web应用都有自己的WebApp类加载器(WebApp ClassLoader)
jsp热修改问题 - jsp文件本质是servlet,servlet就是一个java类
- 一个jsp文件对应一个唯一的jsp类加载器
- 问题:修改jsp文件内容,但jsp生成的*.class文件全类名不变
- 解决:直接卸载掉这jsp文件的类加载器
- 过程:Tomcat(web容器)检测到jsp文件被修改》卸载对应的jsp类加载器》重新创建jsp类加载器》重新加载这个jsp文件
其它详细内容请查看Tomcat类加载机制
链接 验证介绍
- 确保.class文件数据符合JVM规范
- 验证:文件格式,元数据,字节码,符号引用
例子
- 符合JVM规范的字节码文件开头必须是CA FE BA BE
- static变量(static无final修饰):在方法区中分配内存与默认初始化(JVM给定值)
- static final变量:进行显示初始化(用户值),final变量编译时在常量池中分配内存
介绍
- 解析就是将常量池中符号引用转化为直接引用的过程
- 事实上,解析操作伴随着初始化过程
- 符号引用:JVM规范中定义的符号,这些符号用来描述所引用的目标
- 直接引用:是直接指向目标的指针、相对偏移量或句柄
- 解析类,接口、类方法、接口方法、方法类型、字段等,对应常量池中的CONSTANT_Class_info、CONSTANT_Methodref_info、CONSTANT_Fieldref_info
例子
- 上图是,经过jclasslib反编译后的常量池(静态常量池)
- 静态常量池:位于.class文件中的常量池,静态文件中的常量池
- 运行时常量池:.class文件中的常量池被装载到运行时数据区就变成了运行时常量池
介绍
- 初始化阶段就是执行类构造方法
()的阶段
() - 来历
- 此方法不是用户自定义的
- 此方法由javac前端编译器收集类中所有“类变量赋值操作”和“静态代码块中的语句”合并而来
- “类变量赋值语句”和“静态代码块中语句” 在
()方法中的顺序与在源文件中出现的顺序一致
- 父子类
() - 若该类有父类,JVM会保证先执行父类的
()方法,再执行子类的 ()方法
- 若该类有父类,JVM会保证先执行父类的
- 接口
(): - 接口不能使用静态代码块,但可以有类变量的初始化赋值操作,因此可以生成
()方法。 - 与类不同的是,执行接口的
()方法不需要先执行父接口的 ()方法,只有父类接口定义的变量被使用时父类接口才会初始化。 - 实现接口的类在初始化时不会执行接口的
()方法。
- 接口不能使用静态代码块,但可以有类变量的初始化赋值操作,因此可以生成
- 多线程
() :- JVM保证
()只会有1个线程执行1次 ()是多线程安全的方法,JVM会保证类构造方法 ()在多线程下被同步加锁 - 如果多线程同时初始化一个类,那么只会有一个线程去执行这个类的
()方法 ,其它线程都需要阻塞等待,直到活动线程的()方法执行完毕,其它线程不会再次初始化。
- JVM保证
()执行时机,见后面 类的初始化时机 - 类的首次主动使用时才会进行静态变量的初始化,才会执行
()方法 - 只有在类的第1次主动使用时,才会执行
()方法1次。
例子
注意
- 类的首次主动使用时才会进行静态变量的初始化,才会执行
()方法不一定会出现,如果一个类中没有“类变量赋值语句”或“静态代码块”,那么编译器就不会为这个类生成 ()方法。 ()方法如果出现,必然有对“类变量赋值语句”或“静态代码块” ()是对象构造方法 - .class文件经过jclasslib反编译之后才会出现
()方法
类的初始化时机-
- 类的首次主动使用时才会进行静态变量的初始化,才会执行
()方法 - 只有在类的第1次主动使用时,才会执行
()方法1次。
问题:什么叫做类的首次主动使用呢
解答:创建本对象;创建子类对象;使用类的静态方法或属性
- 创建对象
- 创建本类对象(Apple apple=new Apple())
- 创建子类对象(FuShiApple fsApple=new FuShiApple ())
- 反射创建对象;通过Class文件反射创建对象(Class.forname(“cn.xxx.Test”)).newInstance();
- Java虚拟机启动时被标记为启动类的类(含有main方法)
- 使用静态属性/方法
- 使用类的静态属性:调用类的静态属性或者给静态属性赋值
- 调动类的静态方法
- 动态语言支持(java.lang.invoke.MethodHandle实例解析的结果等)
- 创建类引用数组不会进行类的初始化,A[] as=A{10}
两个class对象所属类是否为同一个类-两个必要条件
- 全类名一致
- 类加载器实例对象一致
类加载器实例的引用存储
- 同样一个类应该被同一个类加载器加载
- 类信息(Class实例)中保存类加载器引用信息
- 如果一个类是由用户自定义类加载器加载的,JVM会将这个类加载器实例的引用作为类型信息的一部分放到方法区
- 文章如有问题,欢迎指正!!!



