JVM底层之类加载器详解

一、类加载器

JVM中有两种类型的类加载器,由C++编写的及由Java编写的。除了启动类加载器(Bootstrap Class Loader)是由C++编写的,其他都是由Java编写的。由Java编写的类加载器都继承自类java.lang.ClassLoader。

JVM还支持自定义类加载器。
各种类加载器之间存在着逻辑上的父子关系,但不是真正意义上的父子关系,因为它们直接没有从属关系。

JVM底层之类加载器详解_第1张图片
1.1、启动类加载器

因为启动类是由C++编写的,所以当我们通过Java程序去看显示的是null。

负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)

启动类加载器不像其他类加载器有实体,它是没有实体的,JVM将C++处理类加载的一套逻辑定义为启动类加载器。因此,启动类加载器是无法被Java程序调用的。

加载路径:

URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
    System.out.println(urL);
}

open_jdk源码:

int JNICALL
JavaMain(void * _args)
{
    ……
    mainClass = LoadMainClass(env, mode, what);
    ……
}

static jclass
LoadMainClass(JNIEnv *env, int mode, char *name)
{
    jmethodID mid;
    jstring str;
    jobject result;
    jlong start, end;
    jclass cls = GetLauncherHelperClass(env);
    NULL_CHECK0(cls);
    if (JLI_IsTraceLauncher()) {
        start = CounterGet();
    }
    NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls,
                "checkAndLoadMain",
                "(ZILjava/lang/String;)Ljava/lang/Class;"));

    str = NewPlatformString(env, name);
    CHECK_JNI_RETURN_0(
        result = (*env)->CallStaticObjectMethod(
            env, cls, mid, USE_STDERR, mode, str));

    if (JLI_IsTraceLauncher()) {
        end   = CounterGet();
        printf("%ld micro seconds to load main class\n",
               (long)(jint)Counter2Micros(end-start));
        printf("----%s----\n", JLDEBUG_ENV_ENTRY);
    }

    return (jclass)result;
}

jclass
GetLauncherHelperClass(JNIEnv *env)
{
    if (helperClass == NULL) {
        NULL_CHECK0(helperClass = FindBootStrapClass(env,
                "sun/launcher/LauncherHelper"));
    }
    return helperClass;
}

jclass
FindBootStrapClass(JNIEnv *env, const char* classname)
{
   if (findBootClass == NULL) {
       findBootClass = (FindClassFromBootLoader_t *)dlsym(RTLD_DEFAULT,
          "JVM_FindClassFromBootLoader");
       if (findBootClass == NULL) {
           JLI_ReportErrorMessage(DLL_ERROR4,
               "JVM_FindClassFromBootLoader");
           return NULL;
       }
   }
   return findBootClass(env, classname);
}

JVM_ENTRY(jclass, JVM_FindClassFromBootLoader(JNIEnv* env,
                                              const char* name))
  JVMWrapper2("JVM_FindClassFromBootLoader %s", name);

  // Java libraries should ensure that name is never null...
  if (name == NULL || (int)strlen(name) > Symbol::max_length()) {
    // It's impossible to create this class;  the name cannot fit
    // into the constant pool.
    return NULL;
  }

  TempNewSymbol h_name = SymbolTable::new_symbol(name, CHECK_NULL);
  Klass* k = SystemDictionary::resolve_or_null(h_name, CHECK_NULL);
  if (k == NULL) {
    return NULL;
  }

  if (TraceClassResolution) {
    trace_class_resolution(k);
  }
  return (jclass) JNIHandles::make_local(env, k->java_mirror());
JVM_END

这套逻辑做的事情就是通过启动类加载器加载类sun.launcher.LauncherHelper,执行该类的方法checkAndLoadMain,加载main函数所在的类,启动扩展类加载器、应用类加载器也是在这个时候完成的。

1.2、拓展类加载器

负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。

 public static void main(String[] args) {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();

        URLClassLoader urlClassLoader = (URLClassLoader) classLoader;

        URL[] urls = urlClassLoader.getURLs();
        for (URL url : urls) {
            System.out.println(url);
        }
    }

可以通过java.ext.dirs指定。

1.3、应用类加载器

负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。

默认加载用户程序的类加载器

查看类加载器加载的路径

public static void main(String[] args) {
        String[] urls = System.getProperty("java.class.path").split(":");

        for (String url : urls) {
            System.out.println(url);
        }

        System.out.println("================================");

        URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();

        URL[] urls1 = classLoader.getURLs();
        for (URL url : urls1) {
            System.out.println(url);
        }
    }

可以通过java.class.path指定

1.4、自定义类加载器
继承类java.lang.ClassLoader


二、类加载器加载的类是如何存储的

问题1: 不同的类加载器加载同一个类,相等吗?同一个类加载器多次加载一个类的时候,会加载几次?

答:不相等!这涉及到类加载器加载的类的存储空间,不同类加载器加载类之后存储的空间是不同的。
JVM底层之类加载器详解_第2张图片
我们都知道,类加载后是存储在方法区的,而方法区中的类其实是按照类加载器分开存储的。所以即使不同的类加载器加载了同一个类,它们也是存储在不同的空间中的。

而同一个类加载器多次加载一个类的时候,最终只会加载一次! 类加载器在加载一个类之前,会通过全限定名去判断该类加载器的空间中是否已经存在该类,如果不存在,才会去加载。

这里加载后的类指的是instanceKlass。

其实,JVM中是存在双亲委派模式的,所以上述情况在真实场景下并不会发生,这里只是为了更好的了解类加载器是如何存储instanceKlass的。


三、双亲委派

可以参考这篇文章:https://zhuanlan.zhihu.com/p/269214344

如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。

JVM底层之类加载器详解_第3张图片
3.1、类加载过程
app -> ext -> bootstrap

AppClassLoader去加载一个类:

  1. AppClassLoader: 首先判断自己的类加载器存储空间中是否存在该类,存在直接返回,不存在,委托Ext加载;
  2. ExtClassLoader: 存在,直接返回;不存在委托bootstrap去加载;
  3. BootStrapClassLoader: 存在,直接返回;不存在,抛出异常,ClassNotFound.

上面的描述是错误的!!! 向上委托即在已经加载的类缓存中如果没有找到要加载的类,这时候就会向下委托,首先根加载器查看自己的加载路径中是否存在这个类,存在则加载,不存在则继续向下委托,直到自定义类加载器还没有找到这个类,则报错ClassNotFound.
JVM底层之类加载器详解_第4张图片

可以参考这篇文章:https://zhuanlan.zhihu.com/p/269214344

3.2、双亲委派机制特点

  • 效率
  • 安全:Java类文件class可以被反编译,所以程序中的核心算法需要使用C++编写,然后使用JNI去调用。因为C++是反汇编,不易于阅读。或者可以将class文件进行加密,左移或者右移,然后再自定义类加载器进行解密。
  • 提供了拓展点

3.3、局限性

  • 无法做到不委派
  • 无法做到向下委派

3.4、双亲委派在JVM中的实现代码

 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) {
                    	// this 是AppClassLoader, this.parent是ExtClassLoader
                        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;
        }
    }


四、打破双亲委派 - SPI 机制

我们需要明确一个概念,什么叫打破双亲委派机制?如何打破?

  • 不委派
  • 向下委派

我们知道,java中有一个driver接口,有启动类加载器加载。而像MySQL, Oracle等厂商,需要实现这个接口。

在记载的时候,启动类加载器就需要去加载MySQL,Oracle提供的第三方实现类。

而我们都知道,这些自己提供的类都是有应用类加载器进行加载的,启动类加载器是加载不到的。

那么此时,就需要向下委派加载,需要使用线程上下文类加载器。需要使用ServiceLoader 去调用线程上下文类加载器Thread.currentThread().getContextClassLoader()去加载。

 public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

数据库的Driver也是使用的这种方式来打破双亲委派机制。

Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类就是我们上文介绍的java.util.ServiceLoader。
JVM底层之类加载器详解_第5张图片
SpringBoot服务发现,也使用了SPI机制。


五、线程上下文类加载器 - ServiceLoader

1、是什么

一种特殊的类加载器,可以通过Thread获取,基于此可实现逆向委托加载

2、为什么

为了解决双亲委派的缺陷而生

3、怎么做

/获取
Thread.currentThread().getContextClassLoader()
    
// 设置
Thread.currentThread().setContextClassLoader(new Classloader_4());
ClassLoader cl = Thread.currentThread().getContextClassLoader();

默认设置的类加载器是AppClassLoader,自己也可以设置:

// 通过上下文类加载去加载一个service
 ServiceLoader<PayService> services = ServiceLoader.load(PayService.class);
 // 设置线程上下文类加载器
 Thread.currentThread().setContextClassLoader(Test1.class.getClassLoader());

六、自定义类加载器

我们自定义一个类加载器来加载类,看看能否正常加载。

package com.jihu.test.class_loader;

/**
 * 自定义类加载器
 */
public class CustomClassLoader1 extends ClassLoader {

    public static void main(String[] args) throws ClassNotFoundException {
        CustomClassLoader1 customClassLoader1 = new CustomClassLoader1();
        Class<?> clazz1 = customClassLoader1.loadClass("com.jihu.test.class_loader.BootStrapClassLoaderTest");
        System.out.println("clazz1 : " + clazz1);
        System.out.println("clazz1 hashcode: " + clazz1.hashCode());
        System.out.println("clazz1 classLoader: " + clazz1.getClassLoader());

        CustomClassLoader1 customClassLoader2 = new CustomClassLoader1();
        Class<?> clazz2 = customClassLoader2.loadClass("com.jihu.test.class_loader.BootStrapClassLoaderTest");
        System.out.println("clazz2 : " + clazz2);
        System.out.println("clazz2 hashcode: " + clazz2.hashCode());
        System.out.println("clazz2 classLoader: " + clazz2.getClassLoader());

        System.out.println("clazz1 == clazz2 ? " + (clazz1 == clazz2));
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("CustomClassLoader1 findClass...");
        return null;
    }
}

我们来看结果:

clazz1 : class com.jihu.test.class_loader.BootStrapClassLoaderTest
clazz1 hashcode: 1956725890
clazz1 classLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
clazz2 : class com.jihu.test.class_loader.BootStrapClassLoaderTest
clazz2 hashcode: 1956725890
clazz2 classLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
clazz1 == clazz2 ? true

Process finished with exit code 0

我们从结果看到,类可以被成功加载,但是此时加载类的类加载器是AppClassLoader。

此时clazz1和clazz2是相等的,因为他们都是由同一个类加载AppClassLoader加载的,存储在同一个空间中。

问题:类的hashcode是什么?

类的hashcode是类的内存地址。但是String类不是。

七、沙箱机制

沙箱安全机制的目的是为了防止打破双亲委派,篡改系统类,是为了保护系统类。
JVM底层之类加载器详解_第6张图片
看openjdk源码会看到有这样的判断AccessController.doPrivileged
JVM底层之类加载器详解_第7张图片
比如我定义了一个类名为String所在包为java.lang,因为这个类本来是属于jdk的,如果没有沙箱安全机制的话,这个类将会污染到我所有的String,但是由于沙箱安全机制,所以就委托顶层的bootstrap加载器查找这个类,如果没有的话就委托extsion,extsion没有就到aapclassloader,但是由于String就是jdk的源代码,所以在bootstrap那里就加载到了,先找到先使用,所以就使用bootstrap里面的String,后面的一概不能使用,这就保证了不被恶意代码污染.

JVM底层之类加载器详解_第8张图片

你可能感兴趣的:(#,[LB-子牙],性能调优专题:JVM,java,jvm)