深入理解jvm之类加载器

jvm默认提供了三种类的加载器,BootstrapClassloader(启动类加载),ExtensionClassloader(扩展类加载器)以及AppClassloader(应用类加载器),他们之间是具有层次关系(父子关系),具体如下图所示

  • BootstrapClassloader(启动类加载)

启动加载器采用c语言来编写的, 主要负责加载%JAVA_HOME%/lib下面的核心类库或者是xbootclasspath指定的jar包等虚拟机所识别的类库。启动类加载器由于涉及到虚拟机的本地实现细节,开发人员无法直接过去到它引用。

  • ExtensionClassloader(扩展类加载器)

扩展类加载器负责加载%JAVA_HOME%/lib/ext或者是由系统变量指定的-Djava.ext.dir路径下的类库。开发人员可直接使用扩展类加载器的引用。

  • AppClassloader(应用类加载器)

应用类加载器主要负责加载用户类路径(java -classpath或者是-Djava.java.class,也就是当前类以及当前类所引用的第三方类库所在路径)下的类库,一般情况,如果没有指定类加载器,开发人员自己所编写的类都由应用类加载器来加载。开发人员也可以直接使用应用类加载器的引用。下面通过一段代码来看下这三个类加载器的层次关系。

public class ClassloadTest {

	public static void main(String[] args) {
		ClassLoader loader1 = ClassLoader.getSystemClassLoader();
		ClassLoader loader2 = loader1.getParent();
		ClassLoader loader3 = loader2.getParent();
		System.out.println(loader1);
		System.out.println(loader2);
		System.out.println(loader3);

	}

}


打印结果如下:

sun.misc.Launcher$AppClassLoader@37b90b39
sun.misc.Launcher$ExtClassLoader@558fe7c3
null

loader3之所以为null这是因为启动类加载器采用c语言编写的,我们无法直接获取到它的引用。

但是有些时候,我们需要自定义类加载器。

加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。

从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。

以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。

在实现自己的classLoader之前,先了解下类的加载机制-双亲委派机制,classloader部门源码如下

 protected synchronized Class<?> loadClass(String name, boolean resolve)
	throws ClassNotFoundException
    {
	// First, check if the class has already been loaded
	Class c = findLoadedClass(name);
	if (c == null) {
	    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.
	        c = findClass(name);
	    }
	}
	if (resolve) {
	    resolveClass(c);
	}
	return c;
    }

双亲委派模型的过程如下

当接到加载类的任务时,当前类加载器会把加载任务委托给父类加载器,父类加载器会采用同样策略委托给其父类加载器直到启动类加载器。此时启动类加载器会尝试加载这个类,如果加载失败就会让扩展类加载器去加载当前类,如果扩展类加载器也加载失败就会让应用类加载器去加载,如果应用类加载器加载失败(没有定义自定义类)就会抛出ClassNotFoundException异常。

使用双亲委派模型的好处

主要是为了安全性,比如说现在自己编写了一个String类,只是对其中的equal方法稍作修改,如果说没有双亲委派机制,jvm会将这个String类当成系统的String类去加载,导致java核心类的api被替换。有了双亲委派机制,启动类加载器会优先加载系统的String类,这样自己定义的String类就不会被加载,但是你可能说我们可以自己定义类加载器来强制加载自定义的类,不去调用父类加载器。这样确实可行,但是有一个问题,同一个类被不同的加载器加载之后是两个不同的Class对象了。

除此之外使用双亲委派机制可以避免一个类被重复加载。

自定义一个类加载器用来加载网络上的文件

从上面源码可以看出,调用loadClass时,根据委托机制父类加载器会优先加载,如果都加载失败,再调用当前类加载器的findClass方法来完成加载,因此我们需要继承ClassLoader类并重写findClass方法即可。

package com.linewell.test.classload;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;

public class MyClassLoader extends ClassLoader{
	private String rootUrl;
	
	public MyClassLoader(String rootUrl){
		this.rootUrl=rootUrl;
	}
	@Override
	protected Class<?> findClass(String classname) throws ClassNotFoundException {
		Class clazz=null;
		byte[] classData = getClassData(classname);
		if(classData==null){
			throw new ClassNotFoundException();
		}
		clazz = defineClass(classname, classData, 0, classData.length);
		return clazz;
	}
	public byte[] getClassData(String classname){
		InputStream is=null;
		String path=classNameToPath(classname);
		try {
			URL url = new URL(path);
			byte[] buff=new byte[1024*4];
			int len=0;
			is = url.openStream();
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			while ((len=is.read(buff))!=-1) {
				out.write(buff, 0, len);
			}
			return out.toByteArray();
			
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			if(is!=null){
				try {
					is.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		return null;
	}
	private String classNameToPath(String classname){
		return rootUrl+"/"+classname.replace(".","/" )+".class";
	}

}

自定义一个类,在tomcat的webapps/ROOT目录下,建立好类的包结构,然后把类编译后的class放到其中,如下图所示

 

编写一个测试类进行测试

package com.linewell.test.classload;


public class ClassloaderTest {

	public static void main(String[] args) throws Exception {
		String rootUrl="http://localhost:8081";
		MyClassLoader myClassLoader = new MyClassLoader(rootUrl);
		String classname="test.HelloWorld";
		try {
			Class clazz = myClassLoader.loadClass(classname);
			ClassLoader classLoader = clazz.getClassLoader();
			System.out.println(classLoader);
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}

	}

}

 

最终打印结果如下

引用

https://blog.csdn.net/seu_calvin/article/details/51404589?tdsourcetag=s_pctim_aiomsg

https://blog.csdn.net/justloveyou_/article/details/72466105?tdsourcetag=s_pctim_aiomsg

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值