n1cef1sh's Blog

前言

了解了基本的反射机制以后,对于动态加载还是看不太懂,再学习一下类的加载机制,分为Java篇和Android篇。个人对于原理机制理解能力不强,内容大都参考网上博文,整合了自己的理解,也算积累一些追究原理的方法和经验。

类的生命周期

加载——验证——准备——解析——初始化——使用——卸载

其中验证、准备、解析三个阶段统称为链接。

而加载、链接、初始化则是类的加载过程。

分阶段大致了解一下流程。

加载

又称“装载”。主要完成如下几个工作:

我们可以通过系统提供的类加载器ClassLoader来加载,也可以通过自定义的类加载器去加载,自己控制获取二进制字节流的方式。

验证

验证第一步加载的类的正确性,检测Class文件的字节流是否符合虚拟机的要求。分为四部分:

准备

为类的静态变量分配内存,并将起初始化为默认值。

解析

解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。

初始化

初始化是使用前的最后一个阶段。在准备阶段有一个给变量赋初始值的操作,而初始化阶段则根据用户的自定义计划对变量进行赋值或者初始化其他资源。例如上面提到的public static int age = 20;在这里就对age赋值了20.

默认的ClassLoader

Java里默认的类加载器有三种,它们实质上也是类(除一个例外)。

写个简单的例子看一下。

package com.fish.test2;

public class Demo {
	public static void main(String[] args) {
		ClassLoader c1 = Demo.class.getClassLoader();
		System.out.println("ClassLoader is :" + c1.toString());
		
		ClassLoader c2 = int.class.getClassLoader();
		System.out.println("ClassLoader is :" + c2.toString());
	}
}

第一个输出是ClassLoader is :sun.misc.Launcher$AppClassLoader@2a139a55,说明我们自己定义的这个Demo类是由AppClassLoader加载的。

但是第二个输出的部分报错了。

Exception in thread "main" java.lang.NullPointerException
	at com.fish.test2.Demo.main(Demo.java:9)

错误是空指针,其实int.Class和String.Class是由Bootstrp ClassLoader加载的,这里需要理解另一个知识点,关于双亲委托。

双亲委托

这是一种很重要的委托机制。

首先除了最顶端的BootstrapClassLoader意外,每个类加载器都有一个父加载器,简单描述三个默认加载器的话

AppClassLoader–>ExtensionClassLoader–>BootstrapClassLoader

从右向左,依次是前者的父加载器。但如果我们测试一下。

	ClassLoader c1 = Demo.class.getClassLoader();
	System.out.println("ClassLoader is :" + c1.toString());
	System.out.println("ClassLoader's parent is :" + c1.getParent());
	System.out.println("ClassLoader's parent's parent is :" + c1.getParent().getParent());

输出为

ClassLoader is :sun.misc.Launcher$AppClassLoader@2a139a55
ClassLoader's parent is :sun.misc.Launcher$ExtClassLoader@7852e922
ClassLoader's parent's parent is :null 我们验证了AppClassLoader的父加载器是ExtensionClassLoader,但是ExtensionClassLoader的父加载器却是null。

这里面涉及到另一个URLClassLoder,详细的原理参考这里

我们这里就简单点理解,BootstrapClassLoader因为是由C/C++编写的,本身是虚拟机的一部分,所以它并不是一个JAVA类,我们无法获得它的引用。它也没有父加载器,但是可以作为一个加载器的父加载器,这里它就相当于ExtensionClassLoader的父加载器。所以getparent返回null。

最后我们来看所谓的双亲委托机制。

关于这个委托机制网上有很多示意图,个人感觉反倒看起来更不好理解了。用语言来描述可能更顺畅一些。

我个人觉得这样描述一遍,流程还是挺清晰的。就好比有个任务交给某个人,他没有权利自作主张(除非他以前处理过这个任务),他需要把这个任务转达给爸爸,爸爸也要按照家规,向爷爷传达,直到最后家里的祖宗(BootstrapClassLoader)收到这个任务,他一查自己的能力,能做就做,做不了,就告诉刚刚那个子代“你来吧”。

那么这个机制的意义在于什么呢?主要有两点

1、防止重复

几个类加载器都要加载同一个类A,如果各自直接加载各自的,那么内存里就会有多份类A的字节码文件,重复浪费了很多资源。而采取委托机制的话,先让父类尝试加载,一旦父类加载了,子类就不必重复加载该类了。

2、加强安全

假设不采取委托机制,比如一段恶意代码里有一些假的“java.lang.Integer”代码,这个并不是我们原本的那个Integer,而是别有用心的人伪装出来的有破坏性的恶意代码,然后我们加载了这些代码,GG。

而采取了委托机制的话,加载请求传到了顶端的启动类加载器BootstrapClassLoader,他在自己的范围里一查,在核心JavaAPI里找到了这个名字的类,发现该类已经加载,就直接返回已加载过的Integer.class,不会理会恶意代码了。这样就可以防止一些核心的API库被篡改或者伪装了。

自定义类加载器

虽然Java提供了几个默认的类加载器,但是他们的搜索范围也是有局限的,而且不够灵活,如果我们要加载一些特殊目录的资源,就要自己动手写自己的类加载器了。

这里首先插播一个网上看到的面试题目。

能否自己写个类叫java.lang.System?

一般来说不可行,因为Java类加载采取了委托机制,爸爸能找到的类,儿子就没有机会加载,而System这个类是由祖宗BootstrapClassLoader加载的,即使我写了这个类也没有机会被加载,最后使用的也是Java系统提供的System。

但是呢,这个委托机制并不是强制的,所以我们可以自定义一个类加载器,把这个加载器放在特殊的目录下,避开那三个默认加载器的特定目录,这样我们就可以避开系统提供的加载器以及委托机制了,就可以用自己的类加载器加载自己的System类了,美滋滋。

编写步骤

可以看出,当我们理解了这个委托机制以后,这个题目也就变得很容易解答了,而且不仅回答了能否,还给出了解决的方法。这也体现了自定义类加载器的用处。下面就学习一下编写简单的类加载器步骤。

ClassLoader中loadclass()方法的源码如下(体现了委托机制):

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {      
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
              
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
   	 }

这里面就提到了findclass()

protected Class<?> findClass(String name) throws ClassNotFoundException {
   		 throw new ClassNotFoundException(name);
}

源码只是个空方法抛出了个异常,也就是留给我们自定义的方法。

实例

首先随便写个类(需要被加载的类)

   package com.fish.test2;

	public class Demo {
	
	public void demo() {
		System.out.print("This is a demo for test!");
	}
}

把生成的字节码文件Demo.class移动到自定义的目录下,比如桌面。我的桌面目录是C:\Users\75723\Desktop

然后编写我的类加载器

package com.fish.test2;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {

private String myClasspath;  //class文件所在目录

public MyClassLoader(String myClspath) {
	myClasspath = myClspath;
}

protected Class<?> findClass(String name) throws ClassNotFoundException{
	
	File file = new File(myClasspath, getClassName(name));
	FileInputStream fis = null;
	ByteArrayOutputStream baos = new ByteArrayOutputStream();
	
	int data = 0;
	byte[] b = null;
	try {
		fis = new FileInputStream(file);
		

		//这里最初写错了,如果把data = fis.read()单拿出来,然后while(data!=-1)就会读取错误。
		while((data = fis.read()) != -1) {
			baos.write(data);
		}
		
		b = baos.toByteArray();
		fis.close();
		baos.close();
		
		return defineClass(name, b, 0, b.length);//把class二进制内容转换成class对象
			
	}catch (IOException e) {
		e.printStackTrace();
	}
	return super.findClass(name);
}


private String getClassName(String name) {
	// TODO Auto-generated method stub
	String clsName = null;
	
	if(name != null) {
		int lastIndexOf = name.lastIndexOf('.'); //.最后出现的位置
		if(lastIndexOf != -1) {
			clsName =  name.substring(lastIndexOf+1) + ".class";//substring(int a)从位置a开始截取字符串
		}
		else {
			clsName =  name + ".class";
		}
	}
	
	return clsName;
}
}

最后写个test测试效果。

package com.fish.test2;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Test {
public static void main(String[] args) throws NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
	
	MyClassLoader clsLoader = new MyClassLoader("C:\\Users\\75723\\Desktop");		
	Class<?> cls = clsLoader.loadClass("com.fish.test2.Demo");
	
	Object obj = cls.newInstance();
	Method method = cls.getDeclaredMethod("demo",null);
	
	method.invoke(obj,null);
}
}

获得输出

参考来源

https://blog.csdn.net/tubby_ting/article/details/54093475

https://blog.csdn.net/briblue/article/details/54973413

https://www.jianshu.com/p/125d82b9849d

https://www.cnblogs.com/lanxuezaipiao/p/4138511.html