ClassLoader的作用就是将class文件加载到jvm虚拟机中去,JVM就可以正确运行了。但是,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。

JVM自带有三个类加载器

Bootstrap ClassLoader

最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。

Extention ClassLoader

扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。

Appclass Loader

加载当前应用的classpath的所有类。 也就是我们自己定义的class。

三个类加载器的加载顺序?

我们可以看下sun.misc.Launcher源码,它是一个java虚拟机的入口应用。

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // 先初始化 ExtClassLoader 
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // 初始化 AppClassLoader , 传递 ExtClassLoader 
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }
        //设置AppClassLoader为线程上下文类加载器
        Thread.currentThread().setContextClassLoader(loader);
    }

    static class ExtClassLoader extends URLClassLoader {}

    static class AppClassLoader extends URLClassLoader {}

可以看到Launcher 这个类的构造方法中初始化了拓展类加载器和应用类加载器,在创建应用类加载器的时候,将拓展类加载器作为参数传到应用类加载器的方法中。

ExtClassLoader的getExtClassLoader()源码

private static volatile Launcher.ExtClassLoader instance;// 单例模式实例对象
//保证 ExtClassLoader 是单例
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
    if (instance == null) {
	synchronized(Launcher.ExtClassLoader.class) {
	    if (instance == null) {
		instance = createExtClassLoader();
	    }
	}
    }

    return instance;
}

// 创建ExtClassloader
private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
    try {
	return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
	    public Launcher.ExtClassLoader run() throws IOException {
		// 获取需要加载的类的文件目录信息
		File[] var1 = Launcher.ExtClassLoader.getExtDirs();
		int var2 = var1.length;
		// 分别注册拓展类需要加载的目录
		for(int var3 = 0; var3 < var2; ++var3) {
		    MetaIndex.registerDirectory(var1[var3]);
		}
		 // 返回拓展类加载器
		return new Launcher.ExtClassLoader(var1);
	    }
	});
    } catch (PrivilegedActionException var1) {
	throw (IOException)var1.getException();
    }
}

// 获取拓展类加载器的需要加载的目录
private static File[] getExtDirs() {
    String var0 = System.getProperty("java.ext.dirs");
    File[] var1;
    if (var0 != null) {
	StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
	int var3 = var2.countTokens();
	var1 = new File[var3];

	for(int var4 = 0; var4 < var3; ++var4) {
	    var1[var4] = new File(var2.nextToken());
	}
    } else {
	var1 = new File[0];
    }
    return var1;
}

从上面代码中可以看出 拓展类加载器读取了java.ext.dirs的环境变量的值,所以我们这里可以通过在 java的启动参数添加环境变量就可以修改环境变量加载的目录,比如java -Djava.ext.dirs=/tmp/classes加载并注册之后通过new的方式返回了拓展类加载器。

AppClassLoader 源码

static class AppClassLoader extends URLClassLoader {


        public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
            final String s = System.getProperty("java.class.path");
            final File[] path = (s == null) ? new File[0] : getClassPath(s);


            return AccessController.doPrivileged(
                new PrivilegedAction<AppClassLoader>() {
                    public AppClassLoader run() {
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
        }

        ......
    }

可以看到AppClassLoader加载的就是java.class.path下的路径。

那为什么没有Bootstrap ClassLoader这个类呢?

因为Bootstrap ClassLoader是C++写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件。

 

总结

  • 初始化顺序: Bootstrap ClassLoader->ExtClassLoader(加载路径:java.ext.dirs)->AppClassLoader(加载路径:java.class.path) 。
  • ExtClassLoader和AppClassLoader都继承与URLClassLoader,并不是AppClassLoader继承ExtClassLoader。他们是包含关系,AppClassLoader包含了ExtClassLoader。
  • 线程上下文类加载器设置的是AppClassLoader。
  • AppClassLoader构造时传入了ExtClassLoader,说明AppClassLoader的parent就是ExtClassLoader。

 

ClassLoader的loadClass源码

loadClass是jvm加载类回调的方法。源码如下:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // native方法,查找是否加载了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                       //有parent 先调用parent的loadClass去加载 
                        c = parent.loadClass(name, false);
                    } else { // ExtClassLoader 没有parent
                        // 这里返回的是null。 bootstrapClassloader为null
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    // parent搞不定, 自己加载
                    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) {
               //进行LINK,装入引用类,如超类,接口,字段,方法中使用的本地变量。
                resolveClass(c);
            }
            return c;
        }
    }
//   URLClassLoader 里面的findClass
    protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                       // 把 com.a.b.A.class 换成路径格式: com/a/b/A.class
                        String path = name.replace('.', '/').concat(".class");
                      //将上面路径的文件 包装成Resource
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                               // defineClass 干的事情:
                               // 1.加载类环境下的name路径的class文件变成字节流
                               //2. 调用native的defineClass1方法把class字节转换成Class对象
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

总结

  • 每次加载时,调用native方法findLoadedClass,判断是否已经加载,已经加载的不再加载了。
  • 每个ClassLoader内部都有一个成员变量ClassLoader parent , 加载类时,优先调用parent的loadClass方法去加载,如果parent为空或者loadClass返回null就由自己去加载。
  • loadClass时,先调用defineClass将class全限定名配合加载的环境变量转成具体的文件路径,然后得到class的字节流,然后交给native方法的defineClass1去解析字节流转换成Class对象。
  • 最后通过resolveClass 进行LINK,装入引用类,如超类,接口,字段,方法中使用的本地变量。

其实上面交给parent优先加载的方式叫做:双亲委托

为什么要使用双亲委托这种模型呢?

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使 用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式, 就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中 ClassLoader搜索类的默认算法。

 

改变加载类路径的测试

假如你写了个Main类,然后用下面代码

public class Book {
	public static void main(String[] args) throws ClassNotFoundException {
		Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass("debug_jdk8.Main");
		System.err.println(clazz.getClassLoader());
	}
}

你猜打印出什么

sun.misc.Launcher$AppClassLoader@73d16e93

代码分析

Thread.currentThread().getContextClassLoader() 在前面分析Launcher的源码时 已经讲过就是 AppClassLoader ,而且AppClassLoader 的parent变量值为ExtClassLoader。

然后根据上面loadClass的源码,首先会调用到AppClassLoader的loadClass方法,由于parent不为空,则递归到ExtClassLoader的loadClass方法,然后ExtClassLoader的parent为空,则就交给ExtClassLoader去defineClass,但是由于ExtClassLoader的默认加载环境路径是%JRE_HOME%\lib\ext ,就找不到我们的Main,于是loadClass返回null。

ExtClassLoader加载返回null后就继续交给AppClassLoader 的defineClass,这里我们就加载成功了。

我们调整下启动参数

# 指定 ExtClassLoader 的搜索路径 
-Djava.ext.dirs=C:\Users\Administrator\Desktop\中转站

注意 这个路径千万不要在AppClassLoader 的搜索路径下!然后我们将debug_jdk8/Main.class 放入到C:\Users\Administrator\Desktop\中转站 目录下,然后在运行上面实例程序

sun.misc.Launcher$ExtClassLoader@15db9742

发现现在就是用我们的 ExtClassLoader 加载器去加载你写的 Main.class文件了。

三个加载器加载的路径设置

  • BootstrapClassLoader的加载路径由 sun.boot.class.path 指定。修改路径参数: -Dsun.boot.class.path=加载路径 。
  • ExtClassLoader的加载路径由 java.ext.dirs 指定。修改路径参数: -Djava.ext.dirs=加载路径 。ExtClassLoader加载时会搜索指定路径下的所有子目录,也就是说它会搜索java.ext.dirs所指定下的所有子目录下的class文件或jar文件。
  • AppClassLoader的加载路径由 java.class.path 指定。修改路径参数: -Djava.class.path=加载路径 。AppClassLoader不会搜索java.class.path下的子目录的,所以在在加载子目录中的资源文件时要指定相对目录。

 

如何自定义类加载器

如果我们的java代码在网络 或者 非AppClassLoader搜索的路径上时,就需要自定义类加载器来加载了。只要重写ClassLoader的findClass方法就行了。

加载网络的一个demo

public class MyURLClassLoader extends ClassLoader {
    private String url;
 
    public MyURLClassLoader(String url) {
        this.url = url;
    }
 
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            String path = url+ "/"+name.replace(".","/")+".class";
            URL url = new URL(path);
            InputStream inputStream = url.openStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int len = -1;
            byte buf[] = new byte[1024];
            while((len=inputStream.read(buf))!=-1){
                baos.write(buf,0,len);
            }
            byte[] data = baos.toByteArray();
            inputStream.close();
            baos.close();
            return defineClass(name,data,0,data.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
 
    public static void main(String[] args) throws Exception{
        MyURLClassLoader classLoader = new MyURLClassLoader("http://localhost:8080/examples");
        Class clazz = classLoader.loadClass("com.hadluo.Demo");
        clazz.newInstance();
    }
}

上面代码很简单,就是把网络上的 http://localhost:8080/examples/com/hadluo/Demo.class 转换成字节流然后交给ClassLoader 的defineClass方法 去解析得到Class对象。

自定义Classloader实现不同版本的jar共存

有这样一个变态需求,有两个不同版本的dubbo.jar包,我们需要在项目中动态指定要运行哪个版本,改如何做?

dubbo-v1.jar 制作

package classloader;

public class Dubbo {
	public void invoke() {
		System.err.println("我是Dubbo的V1版本");
	}
}

将上面代码export成dubbo-v1.jar ,接下来准备另一个版本的,注意类的全路径一定是相同的。

package classloader;

public class Dubbo {
	public void invoke() {
		System.err.println("我是Dubbo的V2版本");
	}
}

将上面代码export成dubbo-v2.jar。 客户端将两个jar引入,然后运行

public class Client {

	public static void main(String[] args) {
		Dubbo dubbo = new Dubbo();
		dubbo.invoke();
	}
}

程序运行:

我是Dubbo的V1版本

下面我们用自定义ClassLoader来实现两个版本的调用

public class Client {

	public static void main(String[] args) throws Exception {
		invokeJar("C:/Users/皮吉/Desktop/dobbo-v1.jar");
		invokeJar("C:/Users/皮吉/Desktop/dobbo-v2.jar");

	}

	static class MyClassLoader extends ClassLoader {
		private String jarPath;

		public MyClassLoader(String jarPath) {
			this.jarPath = jarPath;
		}

		@Override
		public Class<?> loadClass(String className) throws ClassNotFoundException {
			byte[] b;
			try {
				// 只对我们的Dubbo类进行 class构造
				if (className.startsWith("classloader.Dubbo")) {
					b = findJar(className);
					return defineClass(className, b, 0, b.length);
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			return super.loadClass(className);
		}

		// 从jar中找到指定class转换成字节流
		private byte[] findJar(String className) throws IOException {
			String tmp = className.replaceAll("\\.", "/");
			@SuppressWarnings("resource")
			JarFile jar = new JarFile(jarPath);
			JarEntry entry = jar.getJarEntry(tmp + ".class");
			InputStream is = jar.getInputStream(entry);
			ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
			int nextValue = is.read();
			while (-1 != nextValue) {
				byteStream.write(nextValue);
				nextValue = is.read();
			}
			return byteStream.toByteArray();
		}

	}

	private static void invokeJar(String jar) throws Exception {
		MyClassLoader v = new MyClassLoader(jar);
		Class<?> clazz = v.loadClass("classloader.Dubbo");
		Object object = clazz.newInstance();
		Method method = clazz.getMethod("invoke", null);
		method.invoke(object, null);
	}
}

上面实际上是生成了2个不同的MyClassLoader对象,所以这两个实现了隔离,互不干扰,都可以调用。

 

如何动态编译一个java文件,然后加载到JVM ?

思路: 利用 javax.tools.JavaCompiler 编译器去把java源代码编译获取到Class的字节流,然后通过ClassLoader的defineClass方法将其加载到JVM当中。

我的实例

public class Main {
	public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException, InstantiationException, IllegalAccessException {
		//java代码
		String javaCode = "	package debug_jdk8s;			"+
					  "		public class Book {			"+
					  "			public int i = 10;		"+
					  "		}							";
		// 编译,加载class
                Class<?> clazz = CompilerUtils.CACHED_COMPILER.loadFromJava("debug_jdk8s.Book", javaCode);
		Object a = clazz.newInstance();
		System.err.println(a);
	}
}

CompilerUtils 是我写的工具类,带有编译缓存的编译加载器,可在左边的qq向我索取。

基于此,我们可以做 线上服务不重启就让dubbo新增了一个提供者。提供一个思路:

  • dubbo服务机器启动时,用定时器监控一个文件目录,比如:/usr/local/dubbo/code
  • 用python脚本或者其它的脚本,监控git上面代码有新增的dubbo接口,然后将代码检出到dubbo服务的机器上/usr/local/dubbo/code。
  • dubbo服务的定时器收到新增的代码,表示这个接口要新增到dubbo service里面,于是执行上面的CompilerUtils去编译生成,一个interface接口和对应的实现类。
  • 然后利用dubbo的动态注册功能,去将上面的接口和实现类导入到dubbo中,完成。

Android 使用classloader加载现有apk classloader 加载jar_java

这样就通过了服务不重启就让dubbo新增了一个提供者。

 

如何去修改 AppClassLoader应用加载器 已经加载到JVM 当中的Class?

Java Agent技术。

Java Agent 是在 JDK1.5 引入的,是一种可以动态修改 Java 字节码的技术。Java 类编译之后形成字节码被 JVM 执行,在 JVM 在执行这些字节码之前获取这些字节码信息,并且通过字节码转换器对这些字节码进行修改,来完成一些额外的功能,这种就是 Java Agent 技术。

比如我有一个需求,我们的业务java服务正在运行,有一天我们定位到系统中某方法可能出现异常,我们要在不修改代码重新发版的情况下,统计出此方法的耗时,怎么办?

业务服务代码:

public class OrderService {
	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 100000; i++) {
			makeOrder(23, "110112");
			Thread.sleep(1000);
		}
	}

	private static String makeOrder(int userId, String itemId) throws InterruptedException {
		System.err.println("有人下单>> userId=" + userId + ",itemId=" + itemId);
		Thread.sleep(new Random().nextInt(5) * 1000);
		return System.currentTimeMillis() + "";
	}
}

我们不通过改源代码,想为makeOrder 增加一个耗时方法打印,于是我们开始制作Agent工具jar。

新启一个项目

package com.hadluo.test;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
import com.hadluo.test.Javassists;

public class FixAgent {

	private static String targetClassName = "jvm.OrderService";
	private static String targetMethdName = "makeOrder";

	/***
	 * Instrumentation接口位于jdk1.6包java.lang.instrument包下,Instrumentation指的是可以独立于应用程序之外的代理程序,<br>
	 * 可以用来监控和扩展JVM上运行的应用程序,相当于是JVM层面的AOP
	 * 
	 * @param args
	 * @param inst
	 */
	public static void agentmain(String args, Instrumentation inst) {
		inst.addTransformer(new ClassFileTransformer() {
			@Override
			public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
					ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
				try {
					System.err.println(targetClassName);
					System.err.println(classBeingRedefined.getName());
					// 是我们要代理的类我们才处理
					if (targetClassName.equals(classBeingRedefined.getName())) {
						System.err.println("enter");
						// 修改字节码class
						classfileBuffer = Javassists.reDefineClass(classBeingRedefined.getName(), targetMethdName);
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
				return classfileBuffer;
			}
		}, true);
		for (Class<?> clazz : inst.getAllLoadedClasses()) {
			if (clazz.getName().equals(targetClassName)) {
				try {
					// 上面的addTransformer只是针对未加载的class进行增加代理层,retransformClasses让jvm对已经加载的class重新加上代理层
					inst.retransformClasses(clazz);
				} catch (UnmodifiableClassException e) {
					e.printStackTrace();
				}
			}
		}
	}

}

agentmain会在main函数执行前执行。Instrumentation工具的addTransformer方法就可以重新指定修改后的class字节数组。然后调用Instrumentation的retransformClasses 就可以对已经被JVM加载过的class进行class修改替换。

上面还有一个Javassists 修改class的工具

package com.hadluo.test;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class Javassists {

	/***
	 * 修改 class字节码,为指定方法增加打印时间
	 * 
	 * @param className
	 * @return
	 * @throws Exception
	 */
	public static byte[] reDefineClass(String className, String methdName) throws Exception {
		try {
			ClassPool pool = new ClassPool();
			pool.appendSystemPath();
			// 定义类
			CtClass ctClass = pool.get(className);
			// 需要修改的方法
			CtMethod method = ctClass.getDeclaredMethod(methdName);
			// 增加本地变量
			method.addLocalVariable("start", CtClass.longType);
			//增加统计耗时代码
			method.insertBefore("start = System.currentTimeMillis();\n");
			String pre = "\"" + className + "." + methdName + "()方法耗时:\"";
			String after = "System.out.println(" + pre + " + (System.currentTimeMillis()-start) + \"ms\");";
			method.insertAfter(after);
			return ctClass.toBytecode();
		} catch (Throwable e) {
			System.err.println("===== " + e.getMessage());
			e.printStackTrace();
		}
		return null;
	}

}

我们就是为源方法增加了一个耗时打印。

接下来我们要在src 目录下新建MANIFEST.MF文件,用于agent的一些参数,注意:最后一行必须是空行。

Android 使用classloader加载现有apk classloader 加载jar_类加载器_02

接下来,我们需要将上面代码export 出一个 jar包并制定MANIFEST.MF文件

项目右键->export->Jar File

Android 使用classloader加载现有apk classloader 加载jar_类加载器_03

几层next后,指定MANIFEST.MF文件

Android 使用classloader加载现有apk classloader 加载jar_加载_04

jar包就生成好了,接下来我们编写绑定程序,用于指定哪个客户端java进程生效我们编写的agent代码。

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

public class Test {

	public static void main(String[] args)
			throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {
		// 查找所有的jvm 进程
		List<VirtualMachineDescriptor> list = VirtualMachine.list();
		for (VirtualMachineDescriptor descriptor : list) {
			if (descriptor.displayName().equals("") || descriptor.displayName().equals(Test.class.getName())) {
				// 过滤本进程
				continue;
			}
			System.err.println("main启动类名称:" + descriptor.displayName() + ",id=" + descriptor.id());
		}
		System.err.println("请输入要监控的进程pid:");
		String id = new Scanner(System.in).next();
		VirtualMachine vm = VirtualMachine.attach(id);
		// 加载 agent jar包
		vm.loadAgent("C:\\Users\\皮吉\\Desktop\\fix-agent.jar");
		// 开始绑定
		vm.detach();
	}
}

代码解释:运行到客户端机器上面,输入要绑定的进程id,没报错,就成功为业务代码makeOrder增加了耗时打印。

找不到VirtualMachineDescriptor 类的话,就把jdk的tools.jar加到工程里面来。

运行实例

Android 使用classloader加载现有apk classloader 加载jar_加载_05