类加载子系统
-
类加载子系统负责从文件系统或网络中加载class文件,所有的class文件在文件开头会有特定魔术因子 ClassLoader只负责加载class文件,至于他只有运行由Execution Engine决定
-
加载的类信息存在在方法区中,除了类的信息,方法区还存放运行时常量池信息,可能还包括字符串字面量和数字常量
-
class文件加载到JVM中 被称为DNS元数据模板,放在方法区
-
执行流程
- Loading-> 【Verification -> Preparation -> Resolution】Linkking -> Initialization
-
加载阶段 Loading
- 通过类的全限定名获取此类的二进制字节流
- 将这个字节流锁代表的静态存储结构转化为方法区中的运行时数据结构
- 内存中生成一个代表在这个类的Class对象,作为方法区这个类的各数据访问入口
- 加载Class文件的方式
- 从本地系统直接加载
- 从网络获取
- 从zip压缩包读取 后演变成jar war
- 运行时计算生成 动态代理技术
- 由其他文件生成
- 从加密文件获取,防止class文件被反编译的保护措施
-
链接阶段 Linkking
-
验证Verify
- 用于确保class文件中字节流信息符合当前虚拟机要求,保证被加载类的正确定,不会危及虚拟机自身安全
- 主要包括四种验证 文件格式验证 元数据验证 字节码验证 符号引用验证
-
准备Prepare
- 为类变量分配内存并且设置该类变量的默认初始值
public class ClassLoaderTest1 { public static int a = 1; // prepare 为 0 在 initialization 之后为1 public static void main(String[] args) { System.out.println(a); } }
-
解析 Resolve
- 将常量池内的符号引用转换为直接引用的过程
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后执行
- 符号引用就是一组符号来描述引用的目标,符号引用的字面量形式明确定义在java虚拟机规范中,直接引用就是直接指向目标的指针,相对偏移量或者一个间接定位到目标的句柄
- 解析动作主要针对类 接口 字段 类方法 接口方法 方法类型 对应常量池中的CONSTANT Class info CONSTANT FieldRef info CONSTANCT methodRef info
-
-
初始化阶段 initialization
-
初始化阶段就是执行类的初始化方法
-
此方法不需要定义,是java编译器clinit方法本身由C和C++实现自动收集类中的所有类变量和赋值工作和静态代码中的逻辑
- 代码中包含static变量就会有clinit
-
clinit构造器方法中指令按语句在源文件中出现的顺序执行
- 若存在父类 JVM会保证先执行父类的构造方法再执行子类的
- 任何一个类在声明之后都会默认有一个无参构造器
public class ClassLoaderTest2 { static { a = 1; System.out.println(a);// illegal forward reference 非法前项引用 } public static int a = 5; public static void main(String[] args) { System.out.println(a); } }
-
JVM必须保证一个类的clinit构造方法在多线程中被同步加锁
public class ClassLoaderTest3 { public static void main(String[] args) { new Thread(){ @Override public void run() { System.out.println(Thread.currentThread().getName() + "\t 线程t1开始"); new DeadClass(); } }.start(); new Thread(){ @Override public void run() { System.out.println(Thread.currentThread().getName() + "\t 线程t2开始"); new DeadClass(); } }.start(); } } class DeadClass{ static { if (true) { System.out.println(Thread.currentThread().getName() + "\t 初始化当前类"); while(true) { // 任意一个线程抢占到创建对象实现的时候初始化会死循环 另外一个线程永远创建不了会卡死 } } } }
-
-
类加载器的分类
- JVM支持两种类加载器,分别为引导类加载器 Bootstrap ClassLoader 和自定义类加载器User-Defined ClassLoader
- 自定义加载器一般是指开发人员自定义的类加载器,但在java虚拟机规范中定义所有派生于抽象类ClassLoader的类加载器都属于自定义类加载器 意思是除了BootstrapClassLoader是由C和C++实现其他包括Extension Class Loader 扩展类加载器 和System Class Loader 系统类加载器都属于自定义加载器
- 加载器之间是属于包含关系,不存在继承关系
public class ClassLoaderTest4 { public static void main(String[] args) { ClassLoader appClassLoader = ClassLoaderTest4.class.getClassLoader(); System.out.println(appClassLoader); ClassLoader extensionClassLoader = appClassLoader.getParent(); System.out.println(extensionClassLoader); ClassLoader bootstrapClassLoader = extensionClassLoader.getParent(); System.out.println(bootstrapClassLoader); ClassLoader btClassLoader = String.class.getClassLoader(); System.out.println(btClassLoader); /** * sun.misc.Launcher$AppClassLoader@18b4aac2 * sun.misc.Launcher$ExtClassLoader@4554617c * null * null */ } }
-
虚拟机自带的加载器
-
引导类加载器 Bootstrap ClassLoader
- 基于C/C++实现嵌套在JVM中
- 用于加载java核心库(JAVA_HOME/jre/lib/rt.jar等…)
- 不继承java.lang.ClassLoader
- 加载扩展类和应用程序加载器并指定为他们的父类加载器
- 处于安全考虑,Boostrap启动类加载类只加载包名java javax sun开头的类
-
扩展类加载器 Extension ClassLoader
- Java 语言编写,由sun.misc.Launcher$ExtClassLoader实现
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
-
应用程序类加载 AppClassLoader
- java语言编写,由sun.misc.LaunchersAppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过classLoader#getSystemclassLoader 方法可以获取到该类加载器
-
用户自定义加载器
- 一般情况上面三种类加载器已经可以满足我们需求,一般在下面几种情况下需要我们自定义类加载器
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄露
- 一般情况下直接继承URIClassLoader类实现loadClass即可
// Bootstrap ClassLoader加载的类路径 for (URL url : Launcher.getBootstrapClassPath().getURLs()) { System.out.println(url); } // file:/C:/Program%20Files/Java/jdk1.8.0_101/jre/lib/resources.jar // file:/C:/Program%20Files/Java/jdk1.8.0_101/jre/lib/rt.jar // file:/C:/Program%20Files/Java/jdk1.8.0_101/jre/lib/sunrsasign.jar // file:/C:/Program%20Files/Java/jdk1.8.0_101/jre/lib/jsse.jar // file:/C:/Program%20Files/Java/jdk1.8.0_101/jre/lib/jce.jar // file:/C:/Program%20Files/Java/jdk1.8.0_101/jre/lib/charsets.jar // file:/C:/Program%20Files/Java/jdk1.8.0_101/jre/lib/jfr.jar // file:/C:/Program%20Files/Java/jdk1.8.0_101/jre/classes
- 一般情况上面三种类加载器已经可以满足我们需求,一般在下面几种情况下需要我们自定义类加载器
-
获取ClassLoader的方法
- 获取当前类的classLoader clazz.getClassLoader()
- 获取当前线程上下文的classLoader Thread.currentThread().getClassLoader()
- 获取系统的ClassLoade ClassLoader.getSytemClassLoader()
- 获取调用者的ClassLoader DriverManager.getCallerClassLoader()
-
-
双亲委派机制
- java虚拟机对class文件采用按需加载的方式,也就是说当需要使用该类时才会将他的class文件加载到内存中生成class对象,而且加载某一个类的时候,JVM采用双亲委派模式,即把请求先交给上层classloader处理,如果处理不了再往下委派,他是一种任务委派模式
- 工作原理
- 如果一个类加载器收到类加载请求,他不是立刻加载,而是先找他的上级加载器执行
- 如果上级加载器还存在上级,则递归继续往上委托,最终达到顶级引导类加载器
- 如果上级加载器可以完成类加载任务,就成功返回,否则下级类加载器尝试加载
- 沙箱安全机制
- 如自定义一个java.lang.String类,在使用的过程中,java.lang.String会优先被最顶级的Bootstrap ClassLoader 优先加载,即rt.jar包中的String.class,不会加载我们自定义的String,这样有效保护了java核心库源码的安全,防止恶意修改核心API
- 双亲委派的优点
- 避免类的重复加载
- 安全性 防止核心API被篡改
- 自定义的 java.lang.String不会被加载
- 自定义的 java.lang.XXXX不会加载 直接报错阻止java.lang开头的包加载
-
判断两个class对象是否相同
- 全限定名一致
- 加载这个类的class loader一致
- JVM必须知道一个类是由引导类加载器加载还是用户自定义加载器加载。如果是用户自定义加载器加载,JVM会将类加载器的一个引用作为类型信息保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类的类加载类相同
-
类的主动使用和被动使用
- java程序对类的使用分别 主动使用和被动使用 主动使用分为以下几种情况
- 创建类实例
- 访问某个类或者接口的静态变量 或者对静态变量赋值
- 调用类的静态方法
- 反射 Class.forName等
- 初始化一个类的子类
- Java虚拟机启动时被表明的启动类的类
- JDK开始提供动态语言支持
- java.lang.invoke.MethodHandler实例的解析结果REF getStatic、REF putStatic、REF invokeStatic句柄对应的类没有初始化,则初始化
- 除了以上7种情况,其他java类的使用方式都是对类的被动使用
- java程序对类的使用分别 主动使用和被动使用 主动使用分为以下几种情况