JAVA-DIY-14-WEEK

Do you like technology?

Posted by Hyuga on July 25, 2019

第14次讨论主题:Classloader的秘密?

话题

类加载的过程是怎么样的?

JavaClassJVM中的完整生命周期有七个阶段:

  • 加载 Load
  • 校验 Verify
  • 准备 Prepare
  • 解析 Resolve
  • 初始化 Initialize
  • 使用 Use
  • 卸载 Unload
加载阶段
  • 类加载器通过Class的全限定名称获取Class的二进制节流
  • 将Class的二进制内容加载到虚拟机的方法区
  • 在内存中生成一个java.lang.Class对象表示这个Class
校验阶段
  • 文件格式验证,确保文件格式符合Class文件格式的规范。如:验证魔数、版本号等。
  • 元数据验证,确保Class的语义描述符合Java的Class规范。如:该Class是否有父类、是否错误继承了final类、是否一个合法的抽象类等。
  • 字节码验证,通过分析数据流和控制流,确保程序语义符合逻辑。如:验证类型转换是合法的。
  • 符号引用验证,发生于符号引用转换为直接引用的时候(转换发生在解析阶段)。如:验证引用的类、成员变量、方法的是否可以被访问(IllegalAccessError),当前类是否存在相应的方法、成员等(NoSuchMethodError、NoSuchFieldError)。
准备阶段
  • 虚拟机会在方法区中为Class分配内存,并设置static成员变量的初始值为默认值。
解析阶段
  • 虚拟机会将常量池中的符号引用替换为直接引用,解析主要针对的是类、接口、方法、成员变量等符号引用。
初始化阶段

开始在内存中构造一个Class对象来表示该类,即执行类构造器。

  • 构造方法方法中执行的是对static变量进行赋值的操作,以及static语句块中的操作。
  • 虚拟机会确保先执行父类的构造方法。
  • 如果一个类中没有static的语句块,也没有对static变量的赋值操作,那么虚拟机不会为这个类生成构造方法。
  • 虚拟机会保证构造方法的执行过程是线程安全的。
使用阶段

调用过程。

卸载阶段

类不在使用,交由gc回收。

两个不同的类加载器加载同一个类,如何进行区分和隔离?

根据双亲委派模式机制,每个类加载器在加载类时,都会先交由上一级类加载器去加载,所以不会出现一个类被两个类加载器所加载。

但是可以通过自定义类加载来实现同一个类被多个不同的类加载器所加载。

一个类在JVM中是用其全限定类名和其类加载器命名作为其唯一标识,如果一个类被不同的类加载器所加载,在JVM中生成的class对象是不同的。 这种方式破坏了双亲委派模式,使得同一个类可以重复加载。


以下文章内容转载自

  • 作者:mChenys
  • 来源:CSDN
  • 原文:https://blog.csdn.net/mchenys/article/details/81289163

类加载器

JVM怎么加载类?

JAVA类加载器的作用是寻找类文件进行加载,将class字节码加载到JVM内存中,链接(验证、准备、解析)并初始化,最终形成可以被虚拟机直接使用的Java类型。

类加载器种类

启动类加载器(Bootstrap ClassLoader)

由C++语言实现(针对HotSpot VM),负责将存放在%JAVA_HOME%\jre\lib目录或-Xbootclasspath参数指定的路径中的类库加载到JVM内存中,像java.lang.、java.util.、java.io.*等等。可以通过vm参数“-XX:+TraceClassLoading”来获取类加载信息。我们无法直接使用该类加载器。

其他类加载器(Java语言实现)

扩展类加载器(Extension ClassLoader)

负责加载%JAVA_HOME%\jre\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。我们可以直接使用这个类加载器。

应用程序类加载器(Application ClassLoader),或者叫系统类加载器

负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

自定义类加载器

通过继承ClassLoader类实现,主要重写findClass方法,这个地方有个坑,在eclipse中重写了findClass方法,发现该方法并没有执行,解决办法就是把loadClass方法也重写了,然后在该方法中优先使用重写的findClass方法查找目标类,如果查询不到再使用父类的loadClass方法查询

类加载器使用顺序

在JVM虚拟机中,如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

也就是说,对于每个类加载器,只有父类(依次递归)找不到时,才自己加载 。这就是双亲委派模型

为什么需要双亲委派模型呢?

可以提高Java的安全性,以及防止程序混乱。

提高安全性方面:

假设我们使用一个第三方Jar包,该Jar包中自定义了一个String类,它的功能和系统String类的功能相同,但是加入了恶意代码。那么,JVM会加载这个自定义的String类,从而在我们所有用到String类的地方都会执行该恶意代码。

如果有双亲委派模型,自定义的String类是不会被加载的,因为最顶层的类加载器会首先加载系统的java.lang.String类,而不会加载自定义的String类,防止了恶意代码的注入。

防止程序混乱:

假设用户编写了一个java.lang.String的同名类,如果每个类加载器都自己加载的话,那么会出现多个String类,导致混乱。如果本加载器加载了,父加载器则不加载,那么以哪个加载的为准又不能确定了,也增加了复杂度。

JVM眼中的相同的类

在JVM中,不可能存在一个类被加载两次的事情!一个类如果已经被加载了,当再次试图加载这个类时,类加载器会先去查找这个类是否已经被加载过了,如果已经被加载过了,就不会再去加载了。

但是,如果一个类使用不同的类加载器去加载是可以出现多次加载的情况的!也就是说,在JVM眼中,相同的类需要有相同的class文件,以及相同的类加载器。当一个class文件,被不同的类加载器加载了,JVM会认识这是两个不同的类,这会在JVM中出现两个相同的Class对象!甚至会出现类型转换异常!

自定义类加载器

我们可以自定义类加载器,只需继承ClassLoader抽象类,并重写findClass方法(如果要打破双亲委派模型,需要重写loadClass方法)。原因可以查看ClassLoader的源码:

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;
        }
    }

这个是ClassLoader中的loadClass方法,大致流程如下:

  • 检查类是否已加载,如果是则不用再重新加载了;

  • 如果未加载,则通过父类加载(依次递归)或者启动类加载器(bootstrap)加载;

  • 如果还未找到,则调用本加载器的findClass方法;

以上可知,类加载器先通过父类加载,父类未找到时,才有本加载器加载。

因为自定义类加载器是继承ClassLoader,而我们再看findClass方法:

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

可以看出,它直接返回ClassNotFoundException。

因此,自定义类加载器必须重写findClass方法。

自定义类加载器示例代码:

package blog.csdn.net;
 
import java.io.FileInputStream;
 
/**
 * 自定义的类加载器,可以加载指定目录下的class文件
 * 
 * @author mChenys
 *
 */
class MyClassLoader extends ClassLoader {
	
	//被加载的class文件存放目录
	private String classPath;
 
	public MyClassLoader(String classPath) {
		this.classPath = classPath;
	}
 
	@Override
	public Class<?> loadClass(String name) throws ClassNotFoundException {
		// 先去加载器里面看看已经加载到的类中是否有这个类
		Class<?> c = findLoadedClass(name);
		if (c == null) {
			//先用本类加载器查询
			try {
				c = this.findClass(name);
			} catch (Exception e) {
			}
			if(c==null) {
				//本类加载不到再用父类的方法进行双亲委派机制查
				c =super.loadClass(name);
			}
		}
 
		return c;
	}
 
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		try {
			byte[] data = loadByte(name);
			return defineClass(name, data, 0, data.length);
		} catch (Exception e) {
			throw new ClassNotFoundException();
		}
 
	}
 
	/**
	 * 获取加载的class文件的字节流数据
	 *
	 * @param name
	 * @return
	 * @throws Exception
	 */
	private byte[] loadByte(String name) throws Exception {
                //通过类的全路径名称生成路径
		name = name.replaceAll("\\.", "/");
		FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
		int len = fis.available();
		byte[] data = new byte[len];
		fis.read(data);
		fis.close();
 
		return data;
	}
}

Person类:

package blog.csdn.net;
 
public class Person {
	public Person() {
		System.out.println("Person本类加载器:" + getClass().getClassLoader());
		System.out.println("Person父类加载器:" + getClass().getClassLoader().getParent());
	}
 
	public String print() {
		System.out.println("Person:print()执行了...");
		return "PersonPrint";
	}
 
}

测试类:

package blog.csdn.net;
 
import java.lang.reflect.Method;
 
public class _Main {
 
	public static void main(String[] args) throws Exception {
		// 创建自定义加载器,通过构造方法传入class文件所在根目录
		MyClassLoader myClassLoader = new MyClassLoader("E:/Workspaces/eclipse/classloader/bin");
		// 开始加载类,注意:Person.class文件必须放在E:/Workspaces/eclipse/classloader/bin/目录下
		Class<?> clazz = myClassLoader.loadClass("blog.csdn.net.Person");
		// 下面是反射的操作...
		Object o = clazz.newInstance();
		Method print = clazz.getDeclaredMethod("print");
		String result = (String) print.invoke(o);
		System.out.println("print方法执行结果:" + result);
	}
}

执行结果:

通过结果可以验证Person.class文件是通过自定义的类加载器加载的.