类加载和类加载器
1. 类加载过程
1.加载
1.1 何为加载
加载类的二进制数据
类的加载指的是将类的.class文件中的二进制数据读到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个Class对象,此Class对象用来封装类在方法区内的数据结构。
(jvm规范并未说明Class对象位于哪里,HosSpot将其放在方法区中)
1.2 加载的方式
- 从本地磁盘中加载class二进制文件
- 通过网络加载class二进制文件
- 从zip,jar等归档文件中加载
- 从专有数据库中提取class文件
- 源代码动态编译为class文件
1.3 加载的注意事项
- jvm允许类加载器在预料某个类将要被使用时就预先加载他,如果预先加载过程中遇到了class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误。
- 如果这个类一直没有被程序使用,那么类加载器就不会报告错误。
2.连接
1. 验证:确保类加载的正确性
- 类文件的结构检查
- 语义检查
- 字节码验证
- 二进制兼容性验证
2. 准备:为类的静态变量分配内存(也是分配默认值的过程)
3. 解析:将类中的符号引用转为直接引用
3.初始化
为类的静态变量赋正确的初始值
3.1 初始化时机
所有的Java虚拟机实现 必须在每个类或接口 被Java程序首次主动使用时 才初始化他们
1 主动使用
主动使用包含七种情况
- 创建类的实例
- 访问某个类或接口的静态变量,或对该静态变量赋值。
- 调用类的静态方法
- 反射
- 初始化一个类的子类(子类初始化父类也会初始化)
- Java虚拟机启动时被标明为启动类的类(如包含main方法)
- jdk1.7开始提供的动态语言支持
2.被动使用
不是主动使用的都为被动使用
3.2初始化步骤
- 类没有加载和连接,先进行加载和连接。
- 类存在直接父类,并且父类没有初始化,那么先初始化直接父类,
- 类中存在初始化语句,那就依次执行初始化语句。
初始化代码示例
- 子父类
package jvmstu.classloader;
public class ClassLoader01 {
public static void main(String[] args) {
//当打印str1时,根据主动使用时才会初始化得,Child.str1 使用的是父类的静态变量,不会初始化子类,子类静态代码块不会执行。
//System.out.println(Child.str1);
//当打印str1时,根据主动使用时才会初始化得,Child.str2 使用的是子类的静态变量,子类初始化父类也会初始化,子类和父类的静态代码块都会执行。
System.out.println(Child.str2);
}
}
class Papa{
public static String str1="im Papa";
static {
System.out.println("Papa 初始化");
}
}
class Child extends Papa {
public static String str2="im Child";
static {
System.out.println("Child 初始化");
}
}
- 常量
package jvmstu.classloader;
public class ClassLoader02 {
public static void main(String[] args) {
System.out.println(Papa1.str1);
}
}
class Papa1{
//常量在 编译阶段 会存入 调用这个常量的方法所在的类的常量池中。
//运行时,Papa1.str1 并没有引用到Papa1类,因此Papa1没有初始化。
//甚至可以将编译好的Child1的class文件删除,也不会影响程序运行。
public static final String str1="im Papa";
static {
System.out.println("Papa 初始化");
}
}
- 编译期常量和运行期常量
package jvmstu.classloader;
import java.util.UUID;
public class ClassLoader03 {
public static void main(String[] args) {
System.out.println(Papa2.str1);
}
}
class Papa2{
//UUID.randomUUID().toString() 运行时才会知道是什么,不是编译器常量。
//运行时常量在运行时会使用类的静态常量。
//运行时,Papa2.str1 引用到Papa2类,因此Papa2初始化。
public static final String str1= UUID.randomUUID().toString();
static {
System.out.println("Papa 初始化");
}
}
- 创建类的实例 new对象
package jvmstu.classloader;
import java.util.UUID;
public class ClassLoader04 {
public static void main(String[] args) {
//创建类的实例,会首次主动使用类。
Papa3 papa=new Papa3();
System.out.println("====");
Papa3 papa2=new Papa3();
}
}
class Papa3{
public static final String str1= UUID.randomUUID().toString();
static {
System.out.println("Papa 初始化");
}
}
- 接口的初始化
package jvmstu.classloader;
import java.util.UUID;
public class ClassLoader05 {
public static void main(String[] args) {
//此处删掉编译好的class(不管是Papa5还是Child5)并不会出错
//接口在初始化时,不一定要求父接口全部完成初始化
//当真正使用父接口时,父接口才会初始化。
System.out.println(Child5.str2);
}
}
interface Papa5{
public static final String str1= UUID.randomUUID().toString();
}
interface Child5 extends Papa5{
public static String str2= "papa5 初始化";
}
- 初始化前准备阶段
package jvmstu.classloader;
public class ClassLoader06 {
public static void main(String[] args) {
//当调用getInstance(静态方法)会进行类的初始化
//初始化前准备阶段会为静态变量分配内存,同时赋予默认值。
//初始化阶段,会从上到下执行初始化赋值。
//new 对象会调用构造方法。
Singleton singleton = Singleton.getInstance();
System.out.println("con1: "+Singleton.con1);
System.out.println("con2: "+Singleton.con2);
}
}
class Singleton{
public static int con1;
//public static int con2=0;
private static Singleton singleton=new Singleton();
public static int con2=0;
private Singleton(){
con1++;
con2++;
System.out.println(con1);
System.out.println(con2);
}
public static Singleton getInstance(){
return singleton;
}
}
- 加载方式
package jvmstu.classloader;
/**
* classLoader.loadClass 不会初始化类
* Class.forName 会初始化化类
*/
public class ClassLoader08 {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader=ClassLoader.getSystemClassLoader();
Class<?> loadClass = classLoader.loadClass("jvmstu.classloader.Papa8");
System.out.println(loadClass);
System.out.println("==================");
Class<?> aClass = Class.forName("jvmstu.classloader.Papa8");
System.out.println(aClass);
}
}
class Papa8{
static int a=2;
static {
System.out.println("papa load init");
}
}
2.类加载器
类加载器有三种
在虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)
1.加载器类型
1.1 根加载器
启动(Bootstrap)类加载器
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
1.2 扩展(Extension)类加载器
扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
1.3 系统(System)类加载器
也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
2.加载器工作模式
双亲委派模式
双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
双亲委派模式的好处
- 可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
- 其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
3 自定义加载器
用户可以编写自定义加载器,用于个性化的加载。
3.1 如何编写一个自定义加载器
- 继承ClassLoader类
- 重写findClass方法和loadClassData方法
- findClass 调用 loadClassData 返回class
- loadClassData,加载二进制文件,是自定义的io操作,可以在网络或者各种介质中加载。实际上如何将二进制文件转换为类,是由native方法,也即jvm完成的。
自定义加载器代码示例
注意
因为类的双亲委托机制,在classpath下的类都会被父加载器加载,因此还是不会调用自定义的加载器加载。
将classpath下的ClassLoader01.class移动到自定义的path下,此时父加载器不能加载ClassLoader01,自定义加载器会开始加载。
package jvmstu.classloader;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
/**
* 自定义加载器
*/
public class ClassLoader09 extends ClassLoader{
private String classLoaderName;
private String path;
private final String fileExtension=".class";
//构造方法
//系统类加载器作为父构造器
public ClassLoader09(String classLoaderName){
super();
this.classLoaderName=classLoaderName;
}
//指定父构造器
public ClassLoader09(ClassLoader parent,String classLoaderName){
super(parent);
this.classLoaderName=classLoaderName;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString() {
return "ClassLoader09{" +
"classLoaderName='" + classLoaderName + '\'' +
", fileExtension='" + fileExtension + '\'' +
'}';
}
//重写findClass 方法
protected Class<?> findClass(String className){
byte [] data=loadClassData(className);
return this.defineClass(className,data,0,data.length);
}
//实现loadClassData 读取二进制文件。
private byte [] loadClassData(String name){
byte [] data=null;
InputStream inputStream=null;
ByteArrayOutputStream baos=null;
try{
String replace = name.replace(".", "/");
System.out.println(replace);
inputStream=new FileInputStream(path+replace+this.fileExtension);
baos=new ByteArrayOutputStream();
int ch=0;
while(-1!=(ch=inputStream.read())){
baos.write(ch);
}
data=baos.toByteArray();
}catch (Exception ex){
ex.printStackTrace();
}finally {
try{
inputStream.close();
baos.close();
}catch (Exception ex){
ex.printStackTrace();
}
}
return data;
}
public static void main(String[] args) throws Exception {
ClassLoader09 loader = new ClassLoader09("loader");
loader.setPath("C:/Users/13166/Desktop/test/");
Class<?> aClass = loader.loadClass("jvmstu.classloader.ClassLoader01");
Object o = aClass.newInstance();
System.out.println(o.getClass().getClassLoader());
}
}
4.自定义系统类加载器
jvm允许将自定义的类加载器定义为系统(System)类加载器,只需添加jvm参数。(查看源码可见逻辑)
5.线程上下文类加载器
双亲委托模型下,类数据是由下到上加载的,对于spi(服务提供接口),java的核心库是由启动类加载器加载的,而这些接口的实现是由厂商实现的,由系统加载器加载。这样双亲加载模型就无法满足spi要求。
因此线程上下文类加载器就是解决这个问题的。父加载器加载的类,可以访问当前线程上下文类加载器加载的类,解决了父加载器加载的类不能访问子加载器加载的类的问题。
6.类加载器的命名空间
6.1 何为命名空间
- 每个类加载器都有自己的命名空间,命名空间由该加载器和该加载器的父加载器所加载的类组成
- 同一命名空间中,不会出现类的完整名字相同的两个类。
- 不同命名空间中,可以出现类的完整名字相同的两个类。
注意事项
- 子加载器所加载的类可以通过“引用”加载访问父加载器加载的类。(双亲委托)
- 父加载器所加载的类不可以通过“引用”加载本该由子加载器加载的类
- 没有关系加载器所加载的类是互相不可见的
6.2 类加载器和命名空间的关系
在双亲委托模型下
- 同一个命名空间的类是相互可见的
- 子加载器的命名空间包含所有父加载器的命名空间,因此子加载器加载的类是可以看见父加载器加载的类,父加载器加载的类则不可看见子加载器加载的类。
- 如果两个加载器之间没有直接或间接的关系,则他们各自加载的类相互不可见。
3.类的使用和卸载
使用 加载连接和初始化的过程
卸载 内存中销毁类
一个类在何时结束生命周期,取决于类的class文件何时被销毁
java自带的类加载器会始终引用他们加载的类,因此不会卸载。自定义的加载器加载的类可以被卸载。