类加载机制
类加载过程
stateDiagram-v2 direction LR state 链接 { direction LR 验证 --> 准备 准备 --> 解析 } 加载 --> 链接 链接 --> 初始化 初始化 --> 使用 使用 --> 卸载
将.class字节流实例化成Class对象并进行相关初始化的过程
加载
- 通过类的完全限定名称获取该类的二进制字节流
- 可从zip包读取,如jar、war
- 可从网络获取
- 运行时动态生成
- 将字节流表示的静态存储结构转换为方法区的运行时存储结构
- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口
对于数组类的创建:数组类本身不通过类加载器创建,由Java虚拟机直接在内存中动态构造出来的
验证
目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,验证阶段的工作量在虚拟机的类加载过程中占了相当大的比重
文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内
元数据验证
对字节码描述的信息进行语义分析,主要目的是对类的元数据信息进行语义校验,保证不存在与《Java语言规范》定义相悖的元数据信息
字节码验证
对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为
符号引用验证
这个阶段检查该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源
准备
准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存
基本数据类型的初始值:
数据类型 | 零值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | '\u0000' |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
解析
确保类与类之间的相互引用正确性
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标
- 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄
解析阶段是将常量池的符号引用替换为直接引用的过程
类或接口的解析
在解析类的时候,如果当前处于类D,则加载其他类的职责会交给类D的类加载器,在加载类时,会检查类D是否有访问权限(修饰符以及模块访问权限)
字段解析
字段解析的过程中会按照继承链从下往上进行查找,当查找到引用时,也会进行检查权限
类方法解析
方法解析首先会判断是否为接口,如果是接口 直接抛出异常
否则跟字段解析一样递归从下往上查找,递归查找不到就会查找父接口等,再找不到就抛出异常,如果找到了同样也会进行权限检查
接口方法解析
如果发现不是接口 抛出异常
递归查找父接口 直到Object 否则就查找失败
同样如果查找到会进行权限检查
初始化
初始化阶段是虚拟机执行类构造器 <clinit>()
方法的过程
<clinit>()
是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的
- 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
static { i=1; // 可以赋值 System.out.println(i); // 无法通过编译}static int i =0;
- Java虚拟机会保证在子类的`
()`方法执行前,父类的` ()`方法已经执行完毕
static class Father { static int a= 1; static { a=2; }}static class Son extends Father{ static int b = a;}public static void main(String[] args) { System.out.println(Son.a); // 2}
- `
()`方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成` ()`方法 - 执行接口的`
()`方法不需要先执行父接口的` ()`方法 - JVM会保证只有一个线程能进入clinit方法
类加载时机
主动引用
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化
- new关键字实例化对象
- 读取或设置一个类型的静态字段
- 调用一个类型的静态方法
- 对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化
- 始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类
- 如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStaticREF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
- 一个接口中定义了默认方法,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
被动引用
除此主动引用外,所有引用类的方式都不会触发初始化,称为被动引用
- 通过子类引用父类的静态字段,不会导致子类初始化
static class Father { static { System.out.println("father init"); } static int value = 123;}static class Son extends Father{ static { System.out.println("son init"); }}public static void main(String[] args) { System.out.println(Son.value); // fatcher init ...}
- 定义一个该类的数组不会导致该类的初始化
static class Ref{ static { System.out.println("ref init"); }}public static void main(String[] args) { //Ref[]这个类由虚拟机自动生成,包装了对真正数组的访问 Ref[] refs = new Ref[10]; // print nothing}
- 引用该类的常量不会导致该类初始化
static class Ref { static { System.out.println("ref init"); } public static final int value = 123;}public static void main(String[] args) { // 编译期优化掉了,这个123存放在常量池里面 System.out.println(Ref.value); // only print 123}
类加载器
两个类相等,需要类本身相等,并且使用同一个类加载器进行加载
ClassLoader myLoader = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf(".") + 1)+".class"; InputStream is = getClass().getResourceAsStream(fileName); if (is == null) { return super.loadClass(name); } byte[] b = new byte[is.available()]; is.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException(name); } }};Object obj = myLoader.loadClass("wang.ismy.jvm.classload.ClassLoaderTest").newInstance();System.out.println(obj.getClass()); // obj是使用自定义加载器加载的System.out.println(obj instanceof wang.ismy.jvm.classload.ClassLoaderTest); // false
分类
- 按虚拟机角度
- 启动类加载器(bootstrap classloader),使用 C++ 实现,是虚拟机自身的一部分 用来加载核心类库 如rt.jar
- 其他类加载器,都是JAVA中继承自java.lang.ClassLoader的类加载器
- 按JAVA开发人员角度
- 启动类加载器 负责`
\lib目录` - 扩展类加载器 负责`
\lib\ext` - 应用程序类加载器 程序自定义 负责加载用户类路径(ClassPath)上所有的类库
- 启动类加载器 负责`
双亲委派模型
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ synchronized (getClassLoadingLock(name)) { // 首先,检查请求的类是否已经被加载过了 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 如果有父加载器就从从父加载器加载 c = parent.loadClass(name, false); } else { // 否则从启动类加载器加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果一个类找不到就抛出ClassNotFoundException // 说明父类和启动类加载器都无法满足需求 } if (c == null) { // 依旧找不到,就调用自身的findClass 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; }}
stateDiagram-v2 direction BT state optional { UserDefineClassLoader2 --> UserDefineClassLoader1 } UserDefineClassLoader1 --> ApplicationClassLoader ApplicationClassLoader --> ExtensionClassLoader ExtensionClassLoader --> BootstrapClassLoader
- 首先判断是否已经加载
- 若无,找父加载器加载
- 若再无,由当前加载器加载
- 上级类加载器所加载的类,无法访问下级类加载器所加载的类
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载
这样就使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一
所以系统中的String类加载优先级就会比在classpath或者用户自定义的String类优先级更高
自定义类加载路径
- 使用URLClassLoader
URL url = new URL("file:~/mysql-connector-java-5.1.44-bin.jar");URLClassLoader loader = new URLClassLoader(new URL[]{url});Class<?> klass = loader.loadClass("com.mysql.jdbc.Driver");System.out.println(klass);
- 获取当前线程类加载器
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
- 热部署
自定义类加载器
- 模板方法模式
findInCache -> parent.loadClass -> findClass()
class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { if ("wang.ismy.Main".equals(name)){ byte[] bytes = new byte[0]; try { bytes = new FileInputStream("path").readAllBytes(); } catch (IOException e) { e.printStackTrace(); } return defineClass(name,bytes,0,bytes.length); }else { return super.findClass(name); } }}
自定义类加载器的场景:
- 隔离加载类 确保中间件应用的jar不会影响到中间件的jar
- 修改加载方式 从其他地方获取class字节流
- 字节码加解密
打破双亲委派模型
- JDK1.2之前 自定义ClassLoader必须重写loadClass
- ThreadContextClassLoader可以实现基础类调用实现类代码,通过thread.setContextClassLoader指定
- OSGi 的类查找模型也不遵守双亲委派
- Tomcat的类加载也不遵守
实例
Tomcat 的正统类加载机制
- 放置在/common目录中。类库可被Tomcat和所有的Web应用程序共同使用。
- 放置在/server目录中。类库可被Tomcat使用,对所有的Web应用程序都不可见。
- 放置在/shared目录中。类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
- 放置在/WebApp/WEB-INF目录中。类库仅仅可以被该Web应用程序使用
Tomcat6之后 用户可以通过修改配置文件指定server.loader和share.loader的方式重新启用原来完整的加载器架构
stateDiagram-v2 direction BT JasperLoader --> WebappClasssLoader WebappClasssLoader --> SharedClassLoader SharedClassLoader --> CommonClassLoader CatalinaClassLoader --> CommonClassLoader CommonClassLoader --> ApplicationClassLoader ApplicationClassLoader --> ExtensionClassLoader ExtensionClassLoader --> BootstrapClassLoader
OSGi 灵活的类加载机制
OSGi(Open Service Gateway Initiative)是OSGi联盟(OSGi Alliance)制订的一个基于Java语言的动态模块化规范
stateDiagram-v2 direction BT BundleClassLoader --> BundleAlassLoader BundleBlassLoader --> BundleAlassLoader BundleBlassLoader --> BundleClassLoader BundleBlassLoader --> ParentClassLoader BundleAlassLoader --> ParentClassLoader
在OSGi中,加载器之间的关系不再是双亲委派模型的树形结构,而是已经进一步发展成一种更为复杂的、运行时才能确定的网状结构