最近写SDK代码遇到SPI,这其中涉及到类加载器的概念,又重新温习了一下,
考虑到类加载器一直没有系统地梳理一边,趁热总结一下。
1.引言
我们开发的java代码被javac编译为class文件后,需要加载到jvm内存里,才可以发挥作用。
这个加载有点像读的概念:虚拟机是个运行的程序,不断地读取class文件,然后输出对应的操作。
说到底,虚拟机就是一个由c/c++写出来的软件,在不同的平台上有不同的实现,但功能都相同–符合jvm规范。
通过jvm约定规范了class文件的结构,即不同的虚拟机实现都可以读懂同一份class文件,
从这个角度也能说明java跨平台的本质实现。
这个过程中有个很重要的过程类加载,这个“读”操作除了进行读,还进行了增强;
在读之前先进行了一系列校验,在读之后,又对读取的内容进行按部就班地加工。
未接触到虚拟机关于类加载器部分规范之前,我们可以猜想这样的设计:
方案1:将类加载器放在程序运行开始的时候,程序运行之初就加载所有的类,运行期间不允许加载;
方案2:在运行期间动态加载
方案1在启动时,做了大量的类加载操作,任务太重,占据大量内存和cpu资源;
但是运行时相对较快;
方案2在启动时很慢,保留了大量资源,在需要时才加载,
运行时因为类加载过程会消耗系统资源;
且方案二有个天生的有点,动态加载;如果不限定加载方式,那么可以从任何地方读取class文件(或者说类描述信息的载体)。
事实上jvm规范就是采用的第二种。
2.定义
“JVM把描述类的数据从class文件加载到内存,并对这些数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这个过程被称为jvm的类加载机制” ——from 《JVM规范》。
java天生的动态扩展属性就来源于运行期间对类的动态加载。因此,编写一个面向接口的程序,可以等运行时才确定这个接口的实现类,且这个实现类可以从任何地方加载进Jvm内存。
3.类加载整个过程
类的整个生命周期包括以下几个部分,JVM规范规定了这些步骤大开始时间,但是没有规定必须串行执行,因此在实际的类加载过程中,步骤执行时段可以交叉。
1)通过类的全限定名来获取定义此类的二进制字节流;
2)将这个二进制流代表的类数据转化为方法区的运行时数据结构;
3)在内存中生成一个代码此类的Class类型对象,作为这个类的访问入口;
对于第一步,虚拟机规范并没有规定必须去哪里加载类,只要求要得到二进制字节流,
因此加载路径可以多样化:
1)从zip包中获取:jar包,war包;
2)从网络上获取;
3)运行时生成:动态代理
4)从其他文件生成:每个JS文件最终都会生成一个Servlet类的class文件
5)从数据库读取
6)从加密文件中获取:通过编译时加密+加载时解密,实现对class文件的反编译保护。
(2) 验证
1)class文件文本格式校验
魔数cafebabe校验,根据主次版本号确定该虚拟机是否可以处理这个class,常量池是否有不被支持的类型等
2)元数据校验
进行的是语义分析,主要涉及类的继承关系:是否继承了不允许被继承的类,
如果该类不是抽象类,是否实现了父类所有的抽象方法;
3)字节码验证
进行数据流分析和控制流分析。
4)符号引用验证
校验符号引用能否正确转化为直接引用,例如符号引用能否通过字符串的全限定名找到对应的类等。
(3)准备
给静态字段分配内存,设初始零值
(4) 解析1
将可以确定的符号引用解析为直接引用的过程,即确定方法调用的版本,是个静态分派。
(5)初始化
执行编译器生成的类构造器方法()
收集了所有static变量的赋值动作和static代码块(按照在java中的顺序)
注意:静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的只能赋值,而不能访问;
这是代码规范中建议把static块放在文件底部的根本原因。
(6)解析2:
直到运行时才能确定方法的调用版本,运行时解析,是个动态分派。
(7)使用:
类型使用的入口是方法区中指向类信息的class对象
(8)卸载:
类的卸载的条件比较苛刻;
1)类型A所有的对象已经被gc,
2)加载类型A的类加载器已经被gc,
因此大部分类被加载进内存后,在整个jvm进程结束前都不会被GC,因为引导、扩展、系统类加载器在整个生命周期中都不会被卸载。
4.类的初始化时机
JVM对于什么时候开始“加载”或者“连接”没有规定,但是严格规定了必须立刻进行类的"初始化"的六种情况:
1)遇到 new,getstatic,putstatic,invokestatic指令时:即创建对象时,读写静态变量或者调用静态方法时。
2)使用反射时
3)初始化类时,如果父类没有初始化,先初始化父类
4)虚拟机启动的main方法所在的类(主类)
5)jdk7提供的动态语言中:MethodHandle实例解析出的方法句柄涉及静态方法时,需要先初始化这个方法句柄对应的类。
6) jdk8中接口默认方法(被default修饰的接口方法),如果这个接口的实现类要初始化时,这个接口要先初始化。
5.类加载器介绍
JVM启动过程中涉及类加载器类的加载和初始化过程,以windows下的JVM为例:
(1) java.exe启动,找到JRE,加载jvm.dll,启动JVM并进行初始化;
(2) jvm初始化Bootstrap类加载器,加载核心类
(3) Bootstrap类加载器加载Extension类加载器,Extension类加载器加载扩展类。
(4) Extension类加载器加载System类加载器,System类加载器在classpath中加载主类App.class
(5) 执行App.class的main方法,整个程序启动成功;
(6) 在运行的过程中使用System类加载器加载类;也可使用System类加载器加载自定义类+通过自定义加载器加载类进JVM;
在上述流程中涉及到3个JVM提供的类加载器,
(1)引导类加载器(Bootstrap ClassLoader)
负责加载JDK中的核心类库,
<JAVA_HOME>/lib 以及 -Xbootclasspath参数指定的路径中存放的jar包;
且必须时可识别的:按照文件名识别,不能识别的不会被加载。
如:rt.jar等,String、Integer等类型都是有引导类加载器加载的。
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
(2)扩展类加载器(Extension ClassLoader)
主要负责加载Java的扩展类库,
默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包
由java.ext.dirs系统属性指定的jar包.
允许用户将具有通用性的类库放到ext目录下,以扩展Java SE的功能;
(3)系统类加载器(App ClassLoader)
主要负责加载用户类路径classpath
或者java.class.path系统属性
或者CLAsspATH操作系统属性所指定的JAR类包和类路径.
如果开发者没有使用自定义加载器,自己开发的类都由这个类加载器加载;
6.双亲委派机制
双亲委派机制时在jdk1.2以后引入的。
6.1 父子关系
其中引导类加载器是jvm提供的,使用c++语言实现;
其他类加载器都是继承于java.lang.classloader,因此都有一个parent属性,用来指向父加载器(组合关系而不是继承关系)
其中:
启动类加载器,由C++实现,没有父类。
拓展类加载器,父类加载器为null,表示父加载器是引导类;
系统类加载器,父类加载器为ExtClassLoader
自定义类加载器,父类加载器为系统类加载器。
6.2 双亲委派模型
(1) 当一个类加载器A收到一个加载请求时,自己先不加载,直接委托父类B去加载,B也会委托自己的父类C,一层层上传,直到引导类加载器。
(2) 如果父类可以加载,替代子类加载,返回成功;如果父类不可以加载,子类自己加载。
涉及的代码如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 检查这个classsh是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果有父类的加载器委托父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空,则当前加载器是bootStrap
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
//如果bootstrap没有加载过,则递归回来,尝试自己去加载
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClasstime().addelapsedtimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
线程上下文类加载器,命名空间,自定义类加载器,tomcat对类加载器的使用与热部署等内容放在JVM-3 类加载机制(下)中整理。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。