类加载器
在Java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的
加载:
查找并加载类的二进制数据加载到内存中,
将二进制文件加载到数据区的方法区内
并在内存中创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
class文件通过 本地系统、网络下载、归档文件中、专有数据库中、源文件动态编译 获得
连接:
- 验证: 加载类的正确性
- 准备:为类的静态变量分配内存,初始化为默认值
- 解析:把类中的符号引用转换为直接引用
初始化:
为类的静态变量赋予正确的初始值
初始化是存在顺序的, 从上到下初始化。
编译器为它编译的每个类都至少生成一个实例初始化方法,java的class文件中,这个实例初始化方法被称为<.init>
类的卸载
由JVM自带的类加载器所加载的类(根类、扩展类、系统类), 在VM的生命周期中,★始终不会被卸载。
JVM始终会引用这些自带加载器, 这些加载器始终引用他们所加载的类的Class对象。这些Class不会被卸载
★★★(用户自定义的类加载器,加载的类, 是能被卸载的)
类的使用
类或者是接口被Java虚拟机首次主动使用才初始化
主动使用
-
创建类的实例
定义类的数组,只是开辟空间(运行期创建新类型[L),并没有初始化(会加载类)
-
访问某个类或接口的静态变量,或者对该静态变量赋值
对于静态字段,只有直接定义了该字段的类(不管引用)才会被初始化。
但是会加载定义类到虚拟机中
定义为final设置常量,在编译阶段就赋值到常量池(助记符)中,并不会加载类
如果常量的值并非编译器确定的,不会放到常量池中,那么在运行期间就会主动使用
-
调用类的静态方法
-
反射
-
子类被初始化
子类的初始化会先调用父类的初始化
初始化一个类时,不会初始化它所实现的接口
初始化一个接口时,并不会初始化它的父接口
-
标记为启动类(含有main方法的)
-
JDK7的动态语言支持,解析句柄初始化
被动使用
除了主动其他都是被动, 不会主动的初始化类
类加载器
JVM规范可以预先加载将要使用的类(可能没有使用)。
如果在预先加载的过程中存在class文件丢失,加载器必须在程序首次主动使用该类时才报告错误(LinkageError), 如果类没有被主动使用,那么类加载器将不会报告错误
1 |
clazz.getClassLoader(); // 当前类的ClassLoader |
数组的类型是在运行时动态生成的,使用的类加载器是元素的类加载器, 原生类型没类加载器
-
JVM自带加载器
-
根类加载器(Bootstrap)
加载lang包的核心类库,从sub.boot.class.path所指定的目录中加载类库,依赖底层属于JVM实现
由JVM启动启动类加载器
-
扩展类加载器(Extension)
父加载器为根加载器,从java.ext.dirs、jre\lib\ext指定的目录加载类库
纯Java实现,是ClassLoader的子类
-
系统(应用)类加载器(System)
应用类加载器,父类为扩展加载器
从classpath或者java.class.path指定的目录中加载类
用户定义类加载器的父加载器
通过修改启动参数 java.system.class.loader来修改系统加载器(上加载器还是VM系统类加载器).
-
-
用户自定义加载器
- Java.lang.ClassLoader的子类
- 用户自定义
1
2
3
4
5loadClass
|
findClass
|
defineClass 使用父类定义类
当前类加载器
每个类都会使用自己的类加载器(加载自身的加载器)去加载其它类(依赖的类)
线程上下文类加载器
如果线程没有通过setContextClassLoader设置类加载器的话, 线程将继承其父线程的上下文类加载器
Java应用运行时初始线程的上下文类加载器是System(App)系统加载器
父ClassLoader可以使用当前线程currentThread的上下文getContextLoader所指定的classloader来加载类
这样可以解决不同命名空间下的可见性(摆脱双亲委托模型)
双亲委托机制
可以确保Java核心库的类型安全(核心类都是由启动类来完成)
不同的类加载加载的类相互隔离在一些框架中存在应用场景
- 自底向上检查类是否已经加载
- 自顶向下尝试加载类
子加载器有父加载器的实例(装饰模式)
每个加载器实例有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成
★ 子加载器所加载的类可以访问父加载器所加载的类
★ 父加载器无法加载由子加载器加载过的类
★ 同一个命名空间内的类是相互可见的被某个加载器加载的类, 如果类内部需要加载其它类, 它将使用该类的加载器类作为其加载器
破坏双亲委派机制
-
为了向前兼容ClassLoader
loadClass中实现双亲委派机制
加入findClass,重写findClass实现自己的加载方式
-
实现SPI
例如 JNDI、JDBC、JCE、JAXB、JBI等实现SPI
通过Thread上下文加载器实现SPI
父加载器通过使用上下文中存储的子类加载器去加载类
-
实现热替换(HotSwap)、热部署(HotDeployment)
线程上下文加载器
线程上下文加载器就是为了破坏Java双亲上下文加载委托机制
高层提供统一的接口让底层实现,为了能让高层能实例化底层的类,需要通过线程上下文加载器来帮助高层的ClassLoader找到并加载该类
// 常量在编译阶段就会存入到这个常量的方法所在类的常量池中,
// 本质上调用常量没有直接引用到定义常量到类,并不触发常量的初始化
助记符
每个助记符在java底层都有相应的实现类
ldc
将int,float,string类型的常量从常量池中推送到栈顶
bipush
将单字节(-128~127)的常量值推送到栈顶
sipush
将短整形常量(-32768~32767)推送到栈顶
iconst_{N}
{N}: m1,0<=N<=5
将int(-1~5)类型推送到栈顶
anewarray
创建一个引用类型的(类、接口、数组)数组,并将其引用值压入栈顶
newarray
创建一个指定的原始类型(如int、float、char等)的数组,并将其压入栈顶
invokevirtual
执行虚方法, 进行多态查找流程