部分内容来自以下博客:
https://www.cnblogs.com/jhxxb/p/10900405.html
https://www.cnblogs.com/lzq210288246/p/13067904.html
1 类加载器
1.1 是什么
类加载器在Java虚拟机中所处的位置如图:
类加载器是一个通过类的全类名来加载类的二进制字节流的代码模块,其主要作用是将class文件二进制数据放入方法区内,然后在堆内创建一个Class类型的对象,Class对象封装了类在方法区内的数据结构,并且向开发者提供了访问方法区内数据结构的接口。
1.2 类的唯一性
对于任意一个类,都需要由类的类加载器和类本身一同确立其在Java虚拟机中的唯一性。
即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。
这里所指的相等,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。
2 分类
2.1 启动类
BootstrapClassLoader,负责加载存放在JDK安装目录下lib目录中的类库,或被-Xbootclasspath参数指定路径中的类库(如rt.jar,所有的java开头的类均被BootstrapClassLoader加载)。
启动类加载器是无法被程序直接引用的,也就是说是无法直接获取的。
2.2 扩展类
ExtensionClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载JDK安装目录下ext目录中的类库,或者由java.ext.dirs系统变量指定路径中的类库(如javax开头的类)。
开发者可以直接使用扩展类加载器。
2.3 应用类
ApplicationClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类。
开发者可以直接使用应用类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
3 获取
3.1 原理
获取系统类加载器(可以获取):
1 ClassLoader.getSystemClassLoader(); 2 Thread.currentThread().getContextClassLoader();
获取扩展类加载器(可以获取):
1 ClassLoader.getSystemClassLoader().getParent();
获取引导类加载器(不可以获取):
1 ClassLoader.getSystemClassLoader().getParent().getParent();
3.2 测试
测试代码如下:
1 public static void main(String[] args) { 2 // 获取系统类加载器 3 ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); 4 System.out.println(appClassLoader); 5 // 获取扩展类加载器 6 ClassLoader extClassLoader = appClassLoader.getParent(); 7 System.out.println(extClassLoader); 8 // 获取引导类加载器 9 ClassLoader bootClassLoader = extClassLoader.getParent(); 10 System.out.println(bootClassLoader); 11 // 自定义类使用的是系统类加载器 12 System.out.println(ClassLoaderTest.class.getClassLoader()); 13 // JDK提供的类使用的是引导类加载器 14 System.out.println(String.class.getClassLoader()); 15 }
运行结果如下:
1 sun.misc.Launcher$AppClassLoader@18b4aac2 2 sun.misc.Launcher$ExtClassLoader@6d9c638 3 null 4 sun.misc.Launcher$AppClassLoader@18b4aac2 5 null
3.3 关系
三个类加载器的关系如图:
这里类加载器之间的父子关系一般不会以继承(Inheritance)关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
4 加载机制
4.1 全盘负责
当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
4.2 双亲委派
先让父类加载器试图加载该类,如果父类加载器之上还有加载器,则进一步向上委托,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
4.3 缓存机制
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。
这就是为什么修改了Class后,必须重启虚拟机,程序的修改才会生效。
5 双亲委派机制
5.1 意义
避免类的重复加载,确保一个类的全局唯一性。
保证程序安全稳定运行,防止核心类被随意篡改。
5.2 破坏
双亲委派主要出现过三个较大规模被破坏的情况。
5.2.1 双亲委派引入前的破坏代码
类加载器和抽象类ClassLoader则在JDK1.0时代就已经存在,双亲委派在JDK1.2之后才被引入,添加了一个新的方法findClass()。
在此之前,用户去继承ClassLoader类的唯一目的就是为了重写loadClass()方法,而双亲委派的具体逻辑就实现在这个方法之中。
JDK1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,这样就可以保证新写出来的类加载器是符合双亲委派规则的。
5.2.2 基础类无法加载用户提供的代码
双亲委派很好地解决了各个类加载器的基础类的统一问题,越基础的类由越上层的加载器进行加载,但如果基础类需要调用用户的代码,这就产生了新的问题。
例如JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器只能加载基础类,无法加载用户类。
为此Java引入了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过Thread.setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
如此,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派的层次结构来逆向使用类加载器,实际上已经违背了双亲委派的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
5.2.3 用户对程序动态性的追求
代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。
OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再是双亲委派中的树状结构,而是进一步发展为更加复杂的网状结构。
6 沙箱安全机制
6.1 是什么
Java安全模型的核心就是Java沙箱。
沙箱是一个限制程序运行的环境,沙箱机制就是将Java代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
沙箱主要限制系统资源访问,不同级别的沙箱对系统资源访问的限制也可以不一样,系统资源包括CPU、内存、文件系统、网络等等。
6.2 安全模型
组成Java沙箱的基本组件:
1)类的加载,特别是双亲委派。
2)类文件的验证,即class文件检验器。
3)内置于Java虚拟机(及语言)的安全特性。
4)安全管理器及API。
Java安全模型的前三个部分的安全特性一起达到一个共同的目的,保持Java虚拟机的实例和它正在运行的应用程序的内部完整性,使得它们不被下载的恶意代码侵犯。
相反,安全模型的第四个组成部分是安全管理器,它主要用于保护虚拟机的外部资源不被虚拟机内运行的恶意或有漏洞的代码侵犯。这个安全管理器是一个单独的对象,在运行的Java虚拟机中,它在对于外部资源的访问控制起中枢作用。
6.3 举例
自定义了一个String类,但在加载String类的时候会使用引导类加载器进行加载,而引导类加载器在加载过程中加载的是JDK自带的String类。
这样可以保证对Java核心源代码的保护,在一定程度上可以保护程序安全,保护原生的JDK代码。
7 自定义类加载器
7.1 需求
通常情况下,直接使用系统类加载器即可满足大部分需求,但有时也需要自定义类加载器。比如应用是通过网络来传输Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。
7.2 步骤
1)继承ClassLoader抽象类
AppClassLoader和ExtClassLoader都是Launcher的静态内部类,其访问权限是缺省的包访问权限。
2)重写父类的findClass()方法
JDK的loadCalss()方法在所有父类加载器无法加载的时候,会调用本身的findClass()方法来进行类加载。
7.3 代码
7.3.1 模拟被加载类
模拟一个被加载的简单类:
1 //存放于D盘根目录 2 public class DemoBusiness { 3 4 public static void business() { 5 ClassLoader classLoader = DemoBusiness.class.getClassLoader(); 6 System.out.println("ClassLoader >>> " + classLoader); 7 System.out.println("ClassLoader.parent >>> " + classLoader.getParent()); 8 } 9 }
为了不被应用程序类加载器加载,将简单类放在D盘根目录。
7.3.2 编译被加载类
编译简单类的Java代码,生成class字节码文件:
1 D:\>javac -encoding utf8 DemoBusiness.java
7.3.3 自定义类加载器
代码如下:
1 public class DemoClassLoader extends ClassLoader { 2 protected Class<?> findClass(String name) throws ClassNotFoundException { 3 // 加载指定类名的Class 4 String classDir = "D:\\" + name.replace('.', File.separatorChar) + ".class"; 5 byte[] classData = loadClassData(classDir); 6 if (classData == null) { 7 throw new ClassNotFoundException(); 8 } else { 9 return defineClass(name, classData, 0, classData.length); 10 } 11 } 12 private byte[] loadClassData(String path) { 13 try (InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream()) { 14 int bufferSize = 4096; 15 byte[] buffer = new byte[bufferSize]; 16 int length = 0; 17 while ((length = ins.read(buffer)) != -1) { 18 baos.write(buffer, 0, length); 19 } 20 return baos.toByteArray(); 21 } catch (IOException e) { 22 e.printStackTrace(); 23 } 24 return null; 25 } 26 }
7.3.4 测试类
代码:
1 public class DemoTest { 2 public static void main(String[] args) throws Exception { 3 // 指定类加载器加载调用 4 DemoClassLoader classLoader = new DemoClassLoader(); 5 classLoader.loadClass("DemoBusiness").getMethod("business").invoke(null); 6 } 7 }
7.3.5 运行结果
结果如下:
1 ClassLoader >>> com.demo.classloader.DemoClassLoader@1ee0005 2 ClassLoader.parent >>> sun.misc.Launcher$AppClassLoader@18b4aac2
8 类的生命周期
8.1 简介
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括加载、连接、初始化、使用和卸载五个阶段。
如图:
其中,连接包括验证、准备、解析三个阶段。解析阶段在某些情况下可以在初始化后再开始,这是为了支持Java语言的运行时绑定。
另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
8.2 加载阶段
8.2.1 是什么
类的加载就是将class文件中的二进制数据读取到运行时内存中,将class文件中类的信息放在方法区中,然后在堆中创建一个Class对象,用于封装方法区中的数据结构。
8.2.2 做什么
在进行加载的时候,虚拟机需要完成三件事:
1)通过类的全类名获取该类的二进制字节流。
2)将二进制字节流所代表的静态结构转化为方法区的运行时数据结构。
3)在内存中创建一个代表该类的Class对象,作为方法区这个类的各种数据的访问入口。
8.2.3 何时做
虚拟机规范允许某个类在预料被使用的时候预先执行类的加载,不需要等到某个类首次被使用的时候才进行类的加载。
如果在进行类的加载时遇到class文件缺失,只有当使用到了该类的时候,类加载器才会报告错误,如果该类并没有被使用,那么类加载器是不会报告错误的。
8.2.4 文件的来源
1)从本地硬盘直接加载。
2)通过网络下载class文件加载。
3)从压缩文件中提取class文件加载。
4)从数据库中提取加载。
5)将源文件编译成class文件加载。
8.2.5 补充说明
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义的类加载器来完成加载。
8.3 验证阶段
8.3.1 做什么
确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成四个阶段的检验动作:
1)文件格式验证:验证字节流是否符合class文件格式的规范。例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
2)元数据验证:对字节码描述的信息进行分析,以保证其描述的信息符合Java语言规范的要求。例如:这个类是否有除了Object之外的超类。
3)字节码验证:通过数据流和控制流分析,确定程序语义是安全法的、符合逻辑的,不会导致虚拟机崩溃。
4)符号引用验证:确保解析动作能正确执行。
8.3.2 补充说明
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
8.4 准备阶段
8.4.1 做什么
为类变量(也就是静态成员变量,不包括实例变量)分配内存并设置初始值的阶段,这些变量所使用的内存都在方法区中进行分配。
注意,准备阶段只针对静态成员变量,一般情况下,这个阶段只是设置初始值,并不是赋值,静态成员变量的赋值是在初始化阶段的cinit方法中完成的。
在某些情况下,如果静态成员变量同时是常量,并且赋值不涉及方法调用(包括构造方法调用),这个阶段会进行赋值。
8.4.2 补充说明
非static类型的变量,赋值是在初始化阶段的init方法中完成的。
static类型的变量,但不是final类型的变量,设置初始值是在准备阶段完成的,赋值是在初始化阶段的cinit方法中完成的。
static类型的变量,并且是final类型的变量,并且赋值不涉及方法调用,赋值是在准备阶段完成的。
举例:
1 // 在准备阶段设置初始值为0,在初始化阶段clinit方法中赋值 2 public static int i = 1; 3 // 在准备阶段赋值 4 public static final int INT_CONSTANT = 1; 5 // 在准备阶段设置初始值为null,在初始化阶段clinit方法中赋值 6 public static final Integer INTEGER_CONSTANT=Integer.valueOf(1); 7 // 在准备阶段赋值 8 public static final String STR = "hello"; 9 // 在准备阶段设置初始值为null,在初始化阶段clinit方法中赋值 10 public static final String STR_CONSTANT = new String("hello");
8.4.3 接口说明
接口中的属性都是static类型和final类型共同修饰的,所以在准备阶段就已经完成了赋值。
8.5 解析阶段
8.5.1 做什么
虚拟机将常量池内的符号引用替换为直接引用,会把该类所引用的其他类全部加载进来,类中的引用方式包括继承、实现接口、域变量、方法定义、方法中定义的本地变量等等。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这七类符号引用进行。
8.5.2 符号引用
在编译Java文件时,Java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。
8.5.3 直接引用
直接指向目标的指针(指向方法区,Class对象)、指向相对偏移量(指向堆区,Class实例对象)或指向能间接定位到目标的句柄。
8.6 初始化
8.6.1 做什么
为类的静态变量赋予正确的初始值,虚拟机负责对类进行初始化,主要对类变量进行初始化。
在Java中对类变量进行初始值设定有两种方式:
1)声明类变量时指定初始值。
2)使用静态代码块为类变量指定初始值。
换句话说,初始化阶段是执行类构造器cinit方法的过程。
如果要初始化的类具有父类,在执行子类的cinit方法前,虚拟机会保证父类的cinit方法已经执行完成。
8.6.2 线程安全
在执行cinit方法时,虚拟机会保证在多线程环境中正确加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit方法。
如果一个类的cinit方法中有耗时的操作,可能会造成多线程阻塞,从而导致产生死锁,并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息。
8.6.3 init方法和cinit方法
init是对象构造器方法,也就是说在new一个对象,并调用该类的构造方法时才会执行的方法,用于对非静态变量进行赋值。
clinit是类构造器方法,也就是在虚拟机中类的生命周期里,初始化阶段调用的方法,用于对静态变量进行赋值。
举例说明:
1 class X { 2 static Log log = LogFactory.getLog(); // <clinit> 3 private int x = 1; // <init> 4 X () { 5 // <init> 6 } 7 static { 8 // <clinit> 9 } 10 }
8.6.4 何时做
只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
1)创建类的实例,包括使用new的方式,也包括使用反射、克隆、序列化的方式,会触发初始化。
2)访问某个类或接口的静态变量,或者对该静态变量赋值,会触发初始化。
3)调用类的静态方法,会触发初始化。
4)作为父类,初始化子类时,会触发初始化。
5)类中包含main()方法作为主类,虚拟机在启动时,会触发初始化
6)JDK1.7开始提供的动态语言支持,涉及解析相关方法句柄对应的类,虚拟机在启动时,会触发初始化。
除此之外,其它所有引用类的方式都不会触发初始化,称为被动引用:
1)作为子类,引用父类静态属性
通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义这个字段的类才会被初始化。
代码:
1 class SuperClass { 2 static { 3 System.out.println("SuperClass init!"); 4 } 5 public static int value = 123; 6 } 7 class SubClass extends SuperClass { 8 static { 9 System.out.println("SubClass init!"); 10 } 11 } 12 public class NotInitialization { 13 public static void main(String[] args) { 14 System.out.println(SubClass.value); 15 // SuperClass init! 16 } 17 }
2)通过数组引用类
通过数组定义来引用类,不会触发此类的初始化。
代码:
1 class SuperClass { 2 static { 3 System.out.println("SuperClass init!"); 4 } 5 public static int value = 123; 6 } 7 public class NotInitialization { 8 public static void main(String[] args) { 9 SuperClass2[] superClasses = new SuperClass2[10]; 10 } 11 }
3)引用类的常量
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
代码:
1 class ConstClass { 2 static { 3 System.out.println("ConstClass init!"); 4 } 5 public static final String HELLO_BINGO = "Hello Bingo"; 6 } 7 public class NotInitialization { 8 public static void main(String[] args) { 9 System.out.println(ConstClass.HELLO_BINGO); 10 } 11 }
8.6.5 接口说明
接口中不能使用静态代码块,但是允许有静态变量初始化的赋值操作,因此接口和类一样也会生成类构造方法。
但是接口在执行类构造方法时,不需要加载父接口,也就不需要在准备阶段执行父接口的类构造方法。
只有当使用到了父接口的静态变量的时候,才会加载父接口,并且在准备阶段执行父接口的类构造方法。
8.7 使用阶段
8.7.1 做什么
调用成员变量或者成员方法执行业务逻辑。
8.8 卸载阶段
8.8.1 做什么
虚拟机在进行垃圾收集的时候卸载类。
8.8.2 满足条件
该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收。
该类对应的Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
9 类的加载顺序
9.1 规律
当加载一个类时:
1)加载同时被static和final修饰的基本类型(包括String类型)的常量,并赋值。在准备阶段完成。
2)加载父类的静态代码,包括静态代码块和静态变量,优先加载写在前面的代码。在初始化阶段完成。
3)加载类的静态代码,包括静态代码块和静态变量,优先加载写在前面的代码。在初始化阶段完成。
4)加载父类的成员属性,包括构造代码块和成员变量,优先加载写在前面的代码。在初始化阶段完成。
5)加载父类的构造器方法。在初始化阶段完成。
6)加载类的成员属性,包括构造代码块和成员变量,优先加载写在前面的代码。在初始化阶段完成。
7)加载类的构造器方法。这一步是在在初始化阶段完成。
9.2 代码
测试代码如下:
1 public class Demo { 2 public static void main(String[] args) throws Exception { 3 new Person(); 4 } 5 } 6 class Person extends Human { 7 public PersonOther personOther = new PersonOther(); 8 public static PersonOtherStatic personOtherStatic = new PersonOtherStatic(); 9 public Person() { 10 super(); 11 System.out.println("Person 构造器 ..."); 12 } 13 { 14 System.out.println("Person 普通代码块 ..."); 15 } 16 static { 17 System.out.println("Person 静态代码块 ..."); 18 } 19 } 20 class Human { 21 public static HumanOtherStatic humanOtherStatic = new HumanOtherStatic(); 22 public Human() { 23 super(); 24 System.out.println("Human 构造器 ..."); 25 } 26 { 27 System.out.println("Human 普通代码块 ..."); 28 } 29 static { 30 System.out.println("Human 静态代码块 ..."); 31 } 32 } 33 class PersonOther { 34 public PersonOther() { 35 super(); 36 System.out.println("PersonOther 构造器 ..."); 37 } 38 { 39 System.out.println("PersonOther 普通代码块 ..."); 40 } 41 static { 42 System.out.println("PersonOther 静态代码块 ..."); 43 } 44 } 45 class PersonOtherStatic { 46 public PersonOtherStatic() { 47 super(); 48 System.out.println("PersonOtherStatic 构造器 ..."); 49 } 50 { 51 System.out.println("PersonOtherStatic 普通代码块 ..."); 52 } 53 static { 54 System.out.println("PersonOtherStatic 静态代码块 ..."); 55 } 56 } 57 class HumanOtherStatic { 58 public HumanOtherStatic() { 59 super(); 60 System.out.println("HumanOtherStatic 构造器 ..."); 61 } 62 { 63 System.out.println("HumanOtherStatic 普通代码块 ..."); 64 } 65 static { 66 System.out.println("HumanOtherStatic 静态代码块 ..."); 67 } 68 }
执行结果如下:
1 HumanOtherStatic 静态代码块 ... 2 HumanOtherStatic 普通代码块 ... 3 HumanOtherStatic 构造器 ... 4 Human 静态代码块 ... 5 PersonOtherStatic 静态代码块 ... 6 PersonOtherStatic 普通代码块 ... 7 PersonOtherStatic 构造器 ... 8 Person 静态代码块 ... 9 Human 普通代码块 ... 10 Human 构造器 ... 11 PersonOther 静态代码块 ... 12 PersonOther 普通代码块 ... 13 PersonOther 构造器 ... 14 Person 普通代码块 ... 15 Person 构造器 ...
10 forName()方法和loadClass()方法有什么区别
10.1 forName()方法
会导致类的主动加载。
Class.forName()是一个静态方法,根据传入的全类名返回一个Class对象。
该方法在将Class文件加载到内存的同时,会执行类的初始化。
10.2 loadClass()方法
不会导致类的主动加载。
ClassLoader.loadClass()是一个实例方法,需要一个ClassLoader对象来调用该方法,根据传入的全类名加载类到内存。
该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。