【刨根问底】之JVMpart5(自定义类加载器实例、验证、准备、解析、初始化、类加载操作、动态类加载)

1.4.2.1.6自定义类加载器实例

//从ClassLoader继承新建类MyClassLoader
public class MyClassLoader extends ClassLoader {
	//该字段用来配置类加载器的目标根目录
    private String root;
      public String getRoot() {
        return root;
    }
    public void setRoot(String root) {
        this.root = root;
    }
	//重写的核心,findClass方法,在这里我们选择不破坏双亲委托机制
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    	//在findclass中,实际的核心还是获取类的数据,此处name是需要加载的类的全限定名
    	//在这里,我们把核心代码获取类的数据封装为独立的loadClassData方法
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
        	//在find成功加载了classData后,之后将数据交给defineClass方法,将其装载进JVM虚拟机之中
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        //由于参数为全限定名,这里对全限定名转换为具体的路径代码
        String fileName = root + File.separatorChar+className.replace('.',File.separatorChar) + ".class";
        System.out.println(fileName);
        try {
        	//生成输入流
            InputStream ins = new FileInputStream(fileName);
            //准备容器
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            //准备中转的容器,容量1024字节
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            
            while ((length = ins.read(buffer)) != -1) {
            //将指定 byte 数组中从偏移量  off 开始的    len 个字节写入此 byte 数组输出流
            //这一步也可以处理为一定程度的加解密,偏移固定的量后才能读取争取的数据等等
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
 public static void main(String[] args) throws InstantiationException, IllegalAccessException  {
        MyClassLoader classLoader = new MyClassLoader();
        //配置类加载器的根目录,我设置为了我自己的桌面
        classLoader.setRoot("C:\\Users\\Administrator\\Desktop");
        //定义读取类的类容器
        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("BaseGrammer.file1");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

这是一段我书写的自定义类加载器,即使读完我书写的注释或许还是会存在一定程度的疑惑,比如,全限定名是什么?

1.4.2.1.6.1全限定名

这是一个熟悉而又陌生的名字,举个例子,我们应该知道,常用的核心类String的全限定名为java.lang.String,根据jdk的包结构,我们应该可以推测处这实际上是路径的一部分,根类加载器将他配置的根目录和进行了转换的全限定名拼接起来就是.class文件的物理存储地址
在最开始的时候我意识到了全限定名的实际意义后,我自信满满的进行了如下配置:
【刨根问底】之JVMpart5(自定义类加载器实例、验证、准备、解析、初始化、类加载操作、动态类加载)_第1张图片
【刨根问底】之JVMpart5(自定义类加载器实例、验证、准备、解析、初始化、类加载操作、动态类加载)_第2张图片
我将想要加载的.class文件放在我的桌面上,然后将root设置为桌面,全限定名输入为.class文件的名字,却输出了如下错误
【刨根问底】之JVMpart5(自定义类加载器实例、验证、准备、解析、初始化、类加载操作、动态类加载)_第3张图片
编译器报错了,而且还提出我的名字取错了,应该是BaseGrammer.file1才对,看一看自己的项目浏览器,BaseGrammer是我们的入口方法所在的类所在的包名,而且我项目之下只分了这一级,难道说jdk希望全限定名必须要加上所在包名的前缀?于是我把名字换成了BaseGrammer.file1:
【刨根问底】之JVMpart5(自定义类加载器实例、验证、准备、解析、初始化、类加载操作、动态类加载)_第4张图片
果然,不报之前的错误了,变成了在这个路径下找不到类文件的错误,这是肯定的,因为我的类文件放在桌面上并没有隔着这样的一个文件夹,不过这个错误同时也再次验证了全限定名本质其实是路径的一部分的论证
那么,我们顺着它的意,继续使用这个名字,并把类放在符合这个名字的文件夹中呢?
【刨根问底】之JVMpart5(自定义类加载器实例、验证、准备、解析、初始化、类加载操作、动态类加载)_第5张图片
【刨根问底】之JVMpart5(自定义类加载器实例、验证、准备、解析、初始化、类加载操作、动态类加载)_第6张图片
果然,编译通过了,于是问题来了,是什么让编译器这么死脑筋,非要做这样看起来有点“多余”的限制呢?其实理由很简单,光是jdk中就可以看到java.util.Date和java.sql.Date两种类名完全一样的类,他们本质上都是Date.class文件,因此,这个强制性的全限定名其实是用于区分的,在我的这个实验中,因为方法处于BaseGrammer包中,于是为了区分,全限定名就被要求加上了这个前缀!

1.4.2.1.7加载知识小结

无意间就把加载这一节部分的知识扩充到了如此夸张的地步,总的来说:类的加载过程是:
把**.class文件使用类加载器加载进入jvm,变成元空间中的一个存放了类信息的c++的klass对象**,再在堆中创建一个基于klass信息创造的c++的oop对象,这个c++中的oop对象就是java里new出来的对象的具体实现,此处的包含了klass信息,且具有访问klass对象的接口的特殊对象名为class对象,且后续
new出来的该类型对象全部都是class对象的复制品

1.4.2.2连接

说完了加载阶段,我们继续向下,来到了类的连接阶段,该阶段包括验证、准备和解析三部分

1.4.2.2.1验证

该阶段对class文件的内容数据进行了验证,包括:

1.4.2.2.1.1文件格式验证

你可以理解为是验证这是否是一个正确的class文件,这个阶段将会验证class文件的一些公共特征

1.4.2.2.1.2元数据验证

实际上一个.class文件可以大体区分为两部分,其中一部分为存放数据、类的特征的元数据部分,该部分的代码内容被称为元数据
这个阶段将会根据类的元数据信息进行更深入的class文件是否正确的验证

1.4.2.2.1.3字节码验证

除去元数据外的另一部分用于存放类中的具体方法的底层实现代码,该部分的内容被称为字节码
这个阶段将会验证class文件中的方法是否正确,可行

1.4.2.2.1.4字符引用验证

在类被加载前,方法、类名等都因为为加载进入jvm之中,不能获取其直接地址,只能使用字符地址,此时将会验证这些字符引用是否有效

1.4.2.2.2准备

通过验证后,这个阶段所做的是:在方法区之中为类变量分配内存,并进行值的初始化
两个值得一讲的内容:

  • 1.类变量
    变量分为成员变量类变量两种类型,类变量顾名思义,是从属于类的变量,也就是被static修饰的那部分,static修饰的定义本来就是指:从属于类,与对象无关的部分
  • 2.值的初始化
    指的是为类变量赋上该类型的默认值,如String则赋为null,int则为0,但是也有例外的部分——当变量为常量,即被final static修饰,且在运行之前就确定的话,其值在此时就会被赋值上指定的数值,这是因为被命名为final的变量无法再次进行赋值

pass:被final static,且运行之前不确定的变量如:

public final static int a = math.random();

即此种随机数变量,直到运行时期才能知道具体的值,也因为这种原因,某种意义上我们所谓的顺序并不是绝对的

1.4.2.2.3解析

该阶段配合动态绑定和静态绑定理解有较好的效果,解析阶段所做的就是把符号引用转换为直接引用的过程,之前提的静态绑定就是类加载时期执行的解析,而动态绑定就是发生在运行时才运行的类

1.4.2.3初始化

该阶段简而言之,实际上就是将会执行类中包含的全部类初始化方法
类初始化方法:
总而言之,代码中全部有static修饰的代码片段都属于类初始化方法,如其名所示,这些代码将会执行类的初始化

public static int a = 10;
public static void say(){
	System.out.println("hai!");
}

其中在准备阶段,jvm为a变量准备了内存,并赋值为初始值0,然后在初始化阶段public static int a = 10;代码正式运行,其值被正式赋值为10,同时静态的其他方法也会在此时正式执行

对象初始化方法:
与类初始化方法相对,所有的非static修饰的方法都属于对象初始化方法,其意味着属于该类的对象被初始化时,将会被加载

public int b = 10;
public void shout(){
	System.out.println("hao!");
}

如以上的代码,在类被加载时并不会执行,当一个属于类的对象被加载时,这些代码片段将会启动,以执行对象的初始化

类加载的时机:
由于所有的类初始方法都会在此时执行,该阶段的时机显得格外重要,一般情况下,如下情况,类将会确切的进行初始化

  1. 使用某个类new一个对象,此时类将会初始化
  2. 使用、访问某个类的静态变量、静态方法,此时类将会初始化
  3. 使用放射对该类进行访问时,此时类将会初始化
  4. 当类作为启动类,被执行其中的main方法时,此时类将会初始化
  5. 该类的子类被加载时,此时类将会初始化

符合以上条件时,我们称其为类的主动使用,当类被首次主动使用时,才会触发类的初始化
不符合以上条件的初始化,我们称其为类的被动使用,比如this.class.getClassLoader().loadClass("file1.class"); ,此处的ClassA被加载,但是并没有触发上述的条件任何之一,因此并不会初始化

1.4.2.3类加载操作

类加载操作可以分为静态与动态两种不同的方式,再此,我会先说明定义,再用实际的操作实例,来深入讲解其性质
静态加载:
就是我们最常使用的new一个对象的方法,此时的类加载为静态加载

String str = new String();

动态加载
包括之前学到的使用类加载器

this.class.getClassLoader().loadClass("java.lang.Stirng");

该方法只是单纯的进行加载,未进行后续的操作

常见的通过反射加载类

Class.forName("java.lang.String");

该方法一直执行到初始化结束为止

值得讨论的细节:

  • 实际上反射的方法是类加载器的包装方法,其实际实现为:Class.forName(className,true, this.getClass().getClassLoader()),这之中的参数true为该类加载完毕后是否进行初始化
1.4.2.3动态类加载操作的实例应用

需求:要求方法根据不同的参数,加载对应的类,并命令其中的类执行某方法,以达成动态效果
使用两种类型的加载代码具体实现如下:

//---------------------定义实验用基类---------------------------------
abstract class People{
	public abstract void saySomeThing();
} 

class Student extends People{
	public void saySomeThing() {
		System.out.println("吃什么蝙蝠害的老子开不了学上网课!");
	}
	
}
class Boss extends People{
	public void saySomeThing() {
		System.out.println("吃什么蝙蝠害的老子工资发不下去公司亏到要哭!");
	}
}
class Me extends People{
	public void saySomeThing() {
		System.out.println("吃什么蝙蝠害的老子拿不到工资还要在家远程办公!");
	}
}
//---------------------定义实验用基类---------------------------------

public class t023 {
//---------------------传统的使用new来静态加载-------------------------
	public static void peopleSaySomethingStatic(String peopleKind) {
		switch (peopleKind) {
			case "Student":{
				Student stu = new Student();
				stu.saySomeThing();
				break;
			}
			case "Boss":{
				Boss bos = new Boss();
				bos.saySomeThing();
				break;
			}
			case "Me":{
				Me me = new Me();
				me.saySomeThing();
				break;
			}
			default:{
				System.out.println("闲的没事吃什么蝙蝠!!!");
				break;
			}
		}
		
	}
//---------------------动态加载-------------------------
	public static void peopleSaySomethingStaticDynamic(String peopleKind) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
		//Class cls = t023.class.getClassLoader().loadClass("BaseGrammer."+peopleKind);
		Class<?> cls = Class.forName("BaseGrammer."+peopleKind);
		People peo = (People)cls.newInstance();
		peo.saySomeThing();
		
	}
	public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
		t023.peopleSaySomethingStaticDynamic("Me");
		t023.peopleSaySomethingStatic("Me");
	}
}

可以看出使用动态加载将需要catch更多的可能出现的错误,但是与此相对应的将是会带来极其恐怖的代码质量提升,仅仅三行代码将可以替代几十行甚至当可能参数为成百上千时将可以替代不可计数数量的代码
且易于维护,静态加载将会面临每增加一种可能性,就需要修改源码,增加对应的代码的窘境,可维护性低的令人发指,而采取动态加载将完全不需要考虑这些,这带来的代码的提升是令人恐怖的!

你可能感兴趣的:(刨根问底系列)