Java中类的加载机制

#Java 共 7,464 字 约 24 分钟

前言

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

类的生命周期

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

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

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

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

加载

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

  • 1、通过类的全名来获取该类的二进制字节流。
  • 2、把字节流所代表的静态储存结构转化成方法区的运行时数据结构。
  • 3、在Java的堆里生成一个代表该类的java.lang.Class对象,作为这些数据在方法区的访问入口。

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

验证

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

  • 1、文件格式验证:验证Class文件格式规范。
  • 2、元数据验证:对字节码描述的信息进行语义分析,看是否符合java语言规范要求。
  • 3、字节验证码:对类的方法进行分析,确保这些方法的执行不会危害虚拟机。
  • 4、符号引用验证:符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否可被当前类访问。

准备

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

  • 进行内存分配的只包括类变量(static修饰的),而不包括实例变量。
  • 初始值是数据类型的零值,例如public static int age = 20; 经过准备阶段后,age的初始值是0,而非20。把age赋值为20的操作在初始化阶段。

解析

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

  • 符号引用::符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

  • 直接引用: (1)直接指向目标的指针 (2)相对偏移量 (3)一个能间接定位到目标的句柄

    直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

初始化

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

默认的ClassLoader

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

  • 1、Bootstrp ClassLoader:启动类加载器,加载JDK核心类库。它是由C/C++编写的,本身是虚拟机的一部分,并不是一个JAVA类,也就是无法在java代码中获取它的引用。JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件。
  • 2、Extension ClassLoader:扩展类加载器,加载Java拓展类库,默认加载JAVA_HOME/jre/lib/ext/的所有jar
  • 3、App Classloader:系统类加载器,加载应用程序classpath目录下的所有jar和class文件

写个简单的例子看一下。

plaintext
UTF-8|11 Lines|
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加载的。

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

plaintext
UTF-8|2 Lines|
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

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

plaintext
UTF-8|4 Lines|
	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());

输出为

plaintext
UTF-8|3 Lines|
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(也就是所有的类加载请求都会传到这里,如果前边缓存里没有记录的话)。

  • 依次查找——由上及下
    当请求到达顶端的BootstrapClassLoader后,它在自己的搜索范围内去查找这个类或者资源,如果找到了就加载。否则它会告诉刚刚传给自己请求的子加载器“我找不到该类”,子加载器再在自己的搜索范围内查找该类,尝试自己去加载,依次向下。如果一旦加载了,就会在缓存里留下记录,等以后再收到请求的时候先查询缓存,提高了效率。

我个人觉得这样描述一遍,流程还是挺清晰的。就好比有个任务交给某个人,他没有权利自作主张(除非他以前处理过这个任务),他需要把这个任务转达给爸爸,爸爸也要按照家规,向爷爷传达,直到最后家里的祖宗(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类了,美滋滋。

编写步骤

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

  • 1、自定义一个类,继承抽象类ClassLoader
  • 2、重写它里面的findClass()方法 (如果想保持双亲委派模型,就应该重写findClass()方法,否则可以重写loadClass()方法。)
  • 3、在刚刚重写的findClass()方法里调用defineClass(),这个方法可以把class二进制内容转换成class对象。
  • p.s.:因为“家规”是除了顶端的祖宗加载器以外,其他类加载器都要有父加载器parent,所以如果自定义的时候未指定具体加载器,那么默认的parent是AppClassLoader。

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

plaintext
UTF-8|38 Lines|
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()

plaintext
UTF-8|3 Lines|
protected Class<?> findClass(String name) throws ClassNotFoundException {
	 throw new ClassNotFoundException(name);
}

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

实例

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

plaintext
UTF-8|8 Lines|
   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

然后编写我的类加载器

plaintext
UTF-8|62 Lines|
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测试效果。

plaintext
UTF-8|17 Lines|
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