一、类加载器子系统
1.1 JVM体系结构
JVM被分为三个次要的子系统:
(1)类加载器子系统(2)运行时数据区(3)执行引擎
1.2 类加载器子系统作用
(1)类加载子系统负责从文件系统或者网络中加载class文件,class文件在文件开有特定的文件标识(0xCAFEBABE)。
(2)类加载器(Class Loader
)只负责class文件的加载,至于它是否能够运行,则由执行引擎(Execution Engine)决定。
(3)加载的类信息寄存于一块称为办法区的内存空间。除了类的信息外,办法区中还会寄存运行时常量池信息,可能还包含字符串字面量和数字常量(这部分常量信息是Class文件中常量池局部的内存映射)。
(4)Class对象是寄存在堆区的。
如果有一个Car.java
文件,编译后生成一个Car.class
字节码文件:
- class file存在于本地硬盘上,能够了解为一个模板。而这个模板在执行的时候是须要加载到JVM当中,JVM再依据这个模板实例化出N个截然不同的实例。
- class file加载到JVM中后,被称为DNA元数据模板,放在办法区。
- 在.class文件-->JVM-->最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),表演一个快递员的角色。
1.3 类的加载过程
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经验加载(Loading)、验证(Verification)、筹备(Preparation)、解析(Resolution)、初始化(Initialization)、应用(Using)和卸载(Unloading)七个阶段,其中验证、筹备、解析三个局部统称为连贯(Linking)。残缺的流程图如下所示:
加载、验证、筹备、初始化和卸载这五个阶段的程序是确定的。为了反对Java语言的运行时绑定,解析阶段也能够是在初始化之后进行的。(以上程序流程指的是程序开始的程序,在理论运行中,这些阶段通常都是相互穿插地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段)。
(1)加载阶段
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,JVM须要实现三件事:
- 通过一个类的全限定名获取定义此类的二进制字节流。
- 将这个字节流所代表的动态存储构造转化为办法区的运行时数据结构。
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为办法区这个类的各种数据的拜访入口。
加载class文件的形式
- 从本地零碎中间接加载。
- 通过网络获取,典型场景:Web Applet。
- 从zip压缩包中读取,成为日后jar、war格局的根底。
- 运行时计算生成,应用最多的是:动静代理技术。
- 由其余文件生成,典型场景:JSP利用从专有数据库中提取.class文件,比拟少见。
- 从加密文件中获取,典型的防Class文件被反编译的保护措施。
- ... ...
(2)链接阶段
- 验证 Verify
目标在于确保Class文件的字节流中蕴含信息合乎以后虚拟机要求,保障被加载类的正确性,不会危害虚拟机本身平安。
次要包含四种验证:文件格式验证,元数据验证,字节码验证,符号援用验证。
文件格式验证
第一阶段要验证字节流是否合乎Class文件格式的标准,并且能被以后版本的虚拟机解决。
- 是否以魔数0xCAFEBABE结尾。
- 主、次版本号是否在以后Java虚拟机承受范畴之内。
- 常量池的常量中是否有不被反对的常量类型(查看常量tag标记)。
- 指向常量的各种索引值中是否有指向不存在的常量或不合乎类型的常量。
- CONSTANT_Utf8_info型的常量中是否有不合乎UTF-8编码的数据。
- Class文件中各个局部及文件自身是否有被删除的或附加的其余信息。
元数据验证
第二阶段是对字节码形容的信息进行语义剖析。
- 这个类是否有父类(除了java.lang.Object之外,所有的类都该当有父类)。
- 这个类的父类是否继承了不容许被继承的类(被final润饰的类)。
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有办法。
- 类中的字段、办法是否与父类产生矛盾(例如笼罩了父类的final字段,或者呈现不合乎规定的办法重载,例如办法参数都统一,但返回值类型却不同等)。
字节码验证
通过数据流剖析和控制流剖析,确定程序语义是非法的、合乎逻辑的。
- 保障任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会呈现相似于“在操作栈搁置了一个int类型的数据,应用时却按long类型来加载入本地变量表中”这样的状况。
- 保障任何跳转指令都不会跳转到办法体以外的字节码指令上。
- 保障办法体中的类型转换总是无效的。
符号援用验证
对类本身以外(常量池中的各种符号援用)的各类信息进行匹配性校验,艰深来说就是,该类是否短少或者被禁止拜访它依赖的某些外部类、办法、字段等资源。
- 符号援用中通过字符串形容的全限定名是否能找到对应的类。
- 在指定类中是否存在合乎办法的字段描述符及简略名称所形容的办法和字段。
- 符号援用中的类、字段、办法的可拜访性(private、protected、public、<package>)是否可被以后类拜访。
咱们能够通过装置IDEA的插件——jclasslib Bytecode viewer
,来查看咱们的Class文件:
装置实现后,咱们编译完一个class文件后,点击View--> Show Bytecode With Jclasslib
即可显示咱们装置的插件来查看字节码。
- 筹备 Prepare
- 为类变量分配内存并且设置该类变量的默认初始值,即零值。(Boolean类型数据的零值为False)
例如上面这段代码:
public class Hello { private static int a = 1; // 筹备阶段为0,在下个阶段,也就是初始化的时候才是1。 public static void main(String[] args) { System.out.println(a); }}
- 这里不蕴含用final润饰的static,因为final在编译的时候就会调配了,筹备阶段会显式初始化;
- 这里不会为实例变量调配初始化,类变量会调配在办法区中,而实例变量是会随着对象一起调配到Java堆中。
- 解析 Resolve
- 将常量池内的符号援用转换为间接援用的过程。
- 事实上,解析操作往往会随同着JVM在执行完初始化之后再执行。
- 符号援用就是一组符号来形容所援用的指标。符号援用的字面量模式明确定义在《java虚拟机标准》的class文件格式中。间接援用就是间接指向指标的指针、绝对偏移量或一个间接定位到指标的句柄。
- 解析动作次要针对类或接口、字段、类办法、接口办法、办法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT_Methodref_info等。
(3)初始化阶段
初始化阶段就是执行类结构器法<clinit>()的过程。此办法不需定义,是javac
编译器主动收集类中的所有类变量的赋值动作和动态代码块(static{}块)中的语句合并而来,编译器收集的程序是由语句在源文件中呈现的程序决定的。
- 也就是说,当咱们代码中蕴含static变量的时候,就会有
<clinit>()
办法
<clinit>
()不同于类的结构器函数。(关联:结构器函数是虚拟机视角下的<init>
()办法。若该类具备父类,JVM会保障子类的<clinit>()
执行前,父类的<clinit>()
曾经执行结束。因而在Java虚拟机中第一个被执行的<clinit>()
办法的类型必定是java.lang.Object
。
- 任何一个类在申明后,都有生成一个结构器,默认是空参结构器
public class ClassInitTest { private static int num = 1; static { num = 2; number = 20; System.out.println(num); System.out.println(number); //报错,非法的前向援用 } private static int number = 10; // prepare:number = 0--> number-->initial: 20-->10 public static void main(String[] args) { System.out.println(ClassInitTest.num); // 2 System.out.println(ClassInitTest.number); // 10 }}
对于波及到父类时候的变量赋值过程:
public class ClinitTest { static class Father { public static int A = 1; static { A = 2; } } static class Son extends Father { public static int B = A; } public static void main(String[] args) { // 加载Father类,其次加载Son类 System.out.println(Son.B); }}
咱们输入后果为 2,也就是说首先加载ClinitTest的时候,会找到main办法,而后执行Son的初始化,然而Son继承了Father,因而还须要执行Father的初始化,同时将A赋值为2。咱们通过反编译失去Father的加载过程,首先咱们看到原来的值被赋值成1,而后又被复制成2,最初返回:
iconst_1putstatic #2 <com/kai/jvm/ClinitTest1$Father.A>iconst_2putstatic #2 <com/kai/jvm/ClinitTest1$Father.A>return
虚拟机必须保障一个类的<clinit>()
办法在多线程下被同步加锁。
public class DeadThreadTest { public static void main(String[] args) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 线程t1开始"); new DeadThread(); }, "t1").start(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 线程t2开始"); new DeadThread(); }, "t2").start(); }}class DeadThread { static { if (true) { System.out.println(Thread.currentThread().getName() + "\t 初始化以后类"); while(true) { } } }}
下面的代码,输入后果为:
线程t1开始线程t2开始线程t2 初始化以后类
从下面能够看出只可能执行一次初始化,其中一条线程始终在阻塞期待。
二、类加载器
2.1 类加载器的分类
在类加载阶段中,实现“通过一个类的全限定名来获取形容该类的二进制字节流”这个动作的代码就被称为“类加载器”(ClassLoader)。
JVM反对两种类型的类加载器 ,别离为启动类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
从概念上来讲,自定义类加载器个别指的是程序中由开发人员自定义的一类类加载器,然而Java虚拟机标准却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。
无论类加载器的类型如何划分,在程序中咱们最常见的类加载器次要有3类,如下所示:
Tips:各类加载器之间的关系不是传统意义上的继承关系。
咱们通过一个类,获取不同的加载器:
public class ClassLoaderTest { public static void main(String[] args) { // 获取零碎类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader); // 获取扩大类加载器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader); // 获取启动类加载器 ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println(bootstrapClassLoader); // 获取自定义加载器 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader); // 获取String类型的加载器 ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1); }}
失去的后果,从后果能够看出启动类加载器无奈通过代码间接获取,同时目前用户代码所应用的加载器为零碎类加载器。同时咱们通过获取String类型的加载器,发现是null,这间接阐明了String类型是通过启动类加载器进行加载的。(Java的外围类库都是应用启动类加载器进行加载的)
sun.misc.Launcher$AppClassLoader@18b4aac2sun.misc.Launcher$ExtClassLoader@4554617cnullsun.misc.Launcher$AppClassLoader@18b4aac2null
2.2 虚拟机自带的加载器
- 启动类加载器(疏导类加载器,Bootstrap ClassLoader)
- 这个类加载应用C/C++语言实现的,嵌套在JVM外部。
- 它用来加载Java的外围库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path门路下的内容),用于提供JVM本身须要的类。
- 并不继承自ava.lang.ClassLoader,没有父加载器。
- 加载扩大类和应用程序类加载器,并指定为他们的父类加载器。
- 出于平安思考,Bootstrap启动类加载器只加载包名为java、javax、sun等结尾的类。
咱们通过上面代码验证一下:
public class ClassLoaderTest { public static void main(String[] args) { System.out.println("*********启动类加载器************"); // 获取BootstrapClassLoader可能加载的API的门路 URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (URL url : urls) { System.out.println(url.toExternalForm()); } // 从下面门路中,随便抉择一个类,来看看他的类加载器是什么:失去的是null,则阐明是启动类加载器 ClassLoader classLoader = Provider.class.getClassLoader(); System.out.println(classLoader); }}
失去的后果(%20是空格):
*********启动类加载器************file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/resources.jarfile:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/rt.jarfile:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/sunrsasign.jarfile:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jsse.jarfile:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jce.jarfile:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/charsets.jarfile:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jfr.jarfile:/C:/Program%20Files/Java/jdk1.8.0_151/jre/classesnull
- 扩大类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
- 派生于ClassLoader类。
- 父类加载器为启动类加载器。
- 从java.ext.dirs零碎属性所指定的目录中加载类库,或从JDK的装置目录的jre/lib/ext子目录(扩大目录)下加载类库。如果用户创立的JAR放在此目录下,也会主动由扩大类加载器加载。
咱们通过上面代码验证一下:
public class ClassLoaderTest { public static void main(String[] args) { System.out.println("*********扩大类加载器************"); String extDirs = System.getProperty("java.ext.dirs"); for (String path : extDirs.split(";")) { System.out.println(path); } // Java\lib\ext目录下随便抉择一个类,查看他的类加载器是什么 ClassLoader classLoader = CurveDB.class.getClassLoader(); System.out.println(classLoader); }}
失去的后果:
*********扩大类加载器************C:\Program Files\Java\jdk1.8.0_151\jre\lib\extC:\WINDOWS\Sun\Java\lib\extsun.misc.Launcher$ExtClassLoader@7ea987ac
- 零碎类加载器(应用程序类加载器,AppClassLoader)
- Java语言编写,由sun.misc.Launcher¥AppClassLoader实现。
- 派生于ClassLoader类。
- 父类加载器为扩大类加载器。
- 它负责加载环境变量classpath或零碎属性java.class.path指定门路下的类库。
- 该类加载是程序中默认的类加载器,一般来说,Java利用的类都是由它来实现加载。
- 通过classLoader#getSystemclassLoader()办法能够获取到该类加载器。
2.3 用户自定义类加载器
在Java的日常利用程序开发中,类的加载简直是由上述3品种加载器相互配合执行的,在必要时,咱们还能够自定义类加载器,来定制类的加载形式。
为什么要自定义类加载器?
- 隔离加载类
- 批改类加载的形式
- 扩大加载源
- 避免源码透露
用户自定义类加载器实现步骤:
- 开发人员能够通过继承抽象类ava.lang.ClassLoader类的形式,实现本人的类加载器,以满足一些非凡的需要
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()办法,从而实现自定义的类加载类,然而在JDK1.2之后已不再倡议用户去笼罩1oadclass()办法,而是倡议把自定义的类加载逻辑写在findclass()办法中
- 在编写自定义类加载器时,如果没有太过于简单的需要,能够间接继承URIClassLoader类,这样就能够防止本人去编写findclass()办法及其获取字节码流的形式,使自定义类加载器编写更加简洁。
2.4 ClassLoader类
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包含启动类加载器)。
办法名称 | 形容 |
---|---|
getParent() | 返回该类加载器的超类加载器 |
loadClass(String name) | 加载名称为name的类,返回后果为java.lang.Class类的实例 |
findClass(String name) | 查找名称为name的类,返回后果为java.lang.Class类的实例 |
findLoadedClass(String name) | 查找名称为name的曾经被加载过的类,返回后果为java.lang.Class类的实例 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组b中的内容转换为一个Java类,返回后果为java.lang.Class类的实例 |
resolveClass(Class<?> c) | 连贯指定的一个Java类 |
获取ClassLoader的路径:
- 获取以后ClassLoader:
clazz.getClassLoader()
- 获取以后线程上下文的ClassLoader:
Thread.currentThread().getContextClassLoader()
- 获取零碎的ClassLoader:
ClassLoader.getSystemClassLoader()
- 获取调用者的ClassLoader:
DriverManager.getCallerClassLoader()
三、双亲委派机制
Java虚拟机对class文件采纳的是按需加载的形式,也就是说当须要应用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采纳的是双亲委派模式,即把优先将申请交由父类解决,它是一种工作委派模式。
3.1 工作原理
- 如果一个类加载器收到了类加载申请,它并不会本人先去加载,而是把这个申请委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,顺次递归,申请最终将达到顶层的启动类加载器;
- 如果父类加载器能够实现类加载工作,就胜利返回,假使父类加载器无奈实现此加载工作,子加载器才会尝试本人去加载,这就是双亲委派模式。(留神,此处的父类、子类不是继承中父类、子类的概念。)
上面用一个例子阐明:
public class StringTest { public static void main(String[] args) { String string = new String(); System.out.println("Hello World!"); }}
而后自定义一个java.lang.String类:
public class String { static { System.out.println("这是自定义的String类的动态代码块!"); }}
执行后果:Hello World!
3.2 双亲委派机制举例
当咱们加载jdbc.jar 用于实现数据库连贯的时候,首先咱们须要晓得的是 jdbc.jar是基于SPI接口进行实现的,所以在加载的时候,会进行双亲委派,最终从启动类加载器中加载 SPI外围类。而后再加载SPI接口实现类,就进行反向委派,通过线程上下文类加载器进行实现jdbc.jar
的加载。
3.3 双亲委派机制的劣势
- 防止类的反复加载
爱护程序平安,避免外围API被随便篡改
- 自定义类:java.lang.String
- 自定义类:java.lang.XXXX(报错:阻止创立 java.lang结尾的类)
3.4 沙箱平安机制
Java平安模型的外围就是Java沙箱(sandbox)。沙箱是一个限度程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范畴中,并且严格限度代码对本地系统资源拜访,通过这样的措施来保障对代码的无效隔离,避免对本地零碎造成毁坏。
组成Java沙箱的根本组件如下:
- 类加载体系结构
- class文件测验器
- 内置于Java虚拟机(及语言)的平安个性
- 平安管理器及Java API
Java平安模型的前三个局部——类加载体系结构、class文件测验器、Java虚拟机(及语言)的平安个性一起达到一个独特的目标:放弃Java虚构 机的实例和它正在运行的应用程序的外部完整性,使得它们不被下载的恶意代码或有破绽的代码进犯。相同,这个平安模型的第四个组成部分是平安管理器,它次要 用于爱护虚拟机的内部资源不被虚拟机内运行的歹意或有破绽的代码进犯。这个平安管理器是一个独自的对象,在运行的Java虚拟机中,它在对于内部资源的访 问管制起中枢作用。
例如,自定义一个java.lang.String类,然而在加载自定义String类的时候会率先应用启动类加载器加载,而启动类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包java.lang.中javalangString.class),报错信息说没有main办法,就是因为加载的是rt.jar包中的string类。这样能够保障对java外围源代码的爱护,这就是沙箱平安机制。
public class String { static { System.out.println("这是自定义的String类的动态代码块!"); } // 谬误 public static void main(String[] args) { System.out.println("Hello World!"); }}
四、补充
5.1 比拟class对象
在JVM中示意两个class对象是否为同一个类存在两个必要条件:
- 类的残缺类名必须统一,包含包名。
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须雷同。
换句话说,在JVM中,即便这两个类对象(class对象)起源同一个Class文件,被同一个虚拟机所加载,但只有加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
JVM必须晓得一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个援用作为类型信息的一部分保留在办法区中。当解析一个类型到另一个类型的援用的时候,JVM须要保障这两个类型的类加载器是雷同的。
5.2 类的被动应用和被动应用
Java程序对类的应用形式分为:被动应用和被动应用。
被动应用,又分为七种状况:
- 创立类的实例
- 拜访某个类或接口的动态变量,或者对该动态变量赋值
- 调用类的静态方法I
- 反射(比方:Class.forName("com.kai.Test"))
- 初始化一个类的子类
- Java虚拟机启动时被表明为启动类的类
- JDK7开始提供的动静语言反对:
- java.lang.invoke.MethodHandle实例的解析后果REF getStatic、REF putStatic、REF invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种状况,其余应用Java类的形式都被看作是对类的被动应用,都不会导致类的初始化。
参考
深刻了解Java虚拟机:JVM高级个性与最佳实际(第3版)
java中的平安模型(沙箱机制)