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中,完成。
这样就通过了服务不重启就让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的一些参数,注意:最后一行必须是空行。
接下来,我们需要将上面代码export 出一个 jar包并制定MANIFEST.MF文件
项目右键->export->Jar File
几层next后,指定MANIFEST.MF文件
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加到工程里面来。
运行实例