java-jvm-5-classloader

57 阅读5分钟

classloader

类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。

class字节码

javap -p main.class

class加载过程

java-classloader.jpeg

  1. loading => 加载Class对象到JVM, Class<?> clazz = ?
    1. .java=>.class; 存储到byte[]
    2. bytes[] => Class<?> clz= 加载到jvm
  2. Link,校验Class
    • 验证:java语言规范
    • 准备:==>(为static修饰的成员变量分配内存,并设置jvm指定的默认值)
    • 解析:
  3. intialize
    1. static代码块加载,并给static属性赋值.
    2. 构造函数,分配heap空间
    3. Class.forName(name,true); true就是指定获取class是否初始化
  4. using
  5. unloading(代码实现)=>PG做GC
 // 不会初始化操作,只是加载class,默认不执行Link,看源码
 // 自己实现loadclass时就只是load
Class<?> clazz = Classloader.loadClass();  
// initilize = false不初始化,默认初始化-true,
Class.forName();

static执行的阶段

// 1. 准备阶段(link):a分配内存,默认复制=0
// 2. 初始化阶段(init): 复制=1
private static int a = 1;

5个阶段,intinalize阶段,Class.forName默认是执行的。

双亲委派

java-classloaders.jpeg

  • 父类加载器并不是通过继承关系来实现的,而是采用组合实现的

  • jvm启动时通过启动类加载其他加载器,再通过其他加载器加载class

  • Bootstrap ClassLoader(启动类加载器)

    • JAVA_HOME\lib目录
    • -Xbootclasspath 指定
    • C++实现native
  • Extension ClassLoader(扩展类加载)

    • JAVA_HOME\lib\ext
    • 系统变量java.ext.dirs=
    • java实现
  • Application ClassLoader(应用类加载器)

    • 加载classpath=

加载类的3个方式

  1. java -classpath=
  2. Classloader.loadClass  只加载
  3. Class.forName 加载 + 初始化

classloader加载流程(委托给父类去加载:通过组合实现的)

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载.

java-classloaders2.jpeg

采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载。

这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。

尤其是在需要运行时加载的长场景Thread.currentThread().getContextClassLoader();保证同一个对象。

自己实现类加载

ClassLoader源码解读

1.loadclass

loadclass()-主加载方法,加载Class到内存

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
}
// 实现
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;
    }
}
  1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
  2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false).没有调用bootstrap类加载器来加载。
  3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。

2.findclass

findclass() - 查找Class对象,通过类名

protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);}
  • 抽象类ClassLoader的findClass函数默认是抛出异常的需要被overwrite.而前面我们知道,loadClass在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的findClass函数,因此我们必须要在loadClass这个函数里面实现将一个指定类名称转换为Class对象.

  • Java提供了defineClass方法,通过这个方法,就可以把一个字节数组转为Class对象

    • 通过编译java源文件生成字节码
    • 通过加载class字节码生成class对象

3.defineClass

defineClass() - 通过字节码定义Class,String->字节码Class

protected final Class<?> defineClass(String name, byte[] b, int off, int len)throws ClassFormatError  {
    return defineClass(name, b, off, len, null);
}

自定义实现的Classloader

//自定义类加载器
public class MyClassLoader extends ClassLoader {
    //加载当前目前目录下的class文件,.class文件的目录
    private String classLocation;
    public MyClassLoader(ClassLoader parent, String classLocation) {
        super(parent);
        this.classLocation = classLocation;
    }
    //从classLocation加载.class文件
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByteFromClass(name);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            throw new ClassNotFoundException();
        }
    }
    //读取文件的字节码 .class
    private byte[] loadByteFromClass(String clsName) throws IOException {
        clsName = clsName.replaceAll("\\.", "/");
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(classLocation.concat("/").concat(clsName).concat(".class"));
            // 估计读取文件的大小
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            return data;
        } finally {
            fis.close();
            }
    }
    public static void main(String[] args) {
        MyClassLoader myClassLoader = new MyClassLoader(getSystemClassLoader(), "/Users/a002");
        try {
            //实际系统的目录
            //Users/a002/com/cook/code/javax/Test.class
            Class clazz = myClassLoader.loadClass("com.cook.code.javax.Test");
            Object obj = clazz.newInstance();
            Method testMethod = clazz.getDeclaredMethod("test", null);
            testMethod.invoke(obj, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/*
OUT:
Test加载器: class com.cook.code.javax.launcher.MyClassLoader
Test父类加载器:(JDK默认类加载器) sun.misc.Launcher$AppClassLoader@14dad5dc
*/

OSGI(open service Gateway intiative)

面向服务的模块化热插拔功能,动态加载服务的实现

JDK-SPI

参考Java-SPI.md文件

ps

  1. classloader

class.forname() 与classload.loadclass()的区别

  • class.forname 能初始化,load,verify,link,init
  • 与classload.loadclass:只能到load,verify,link

问什么双亲委托

String有final修饰,可以不被继承,不被修改; 自定义classLoader可以加载自己的String(跳过final), 双亲委派保证优先加载系统的String。