JVM中有两种类型的类加载器,由C++编写的及由Java编写的。除了启动类加载器(Bootstrap Class Loader)是由C++编写的,其他都是由Java编写的。由Java编写的类加载器都继承自类java.lang.ClassLoader。
JVM还支持自定义类加载器。
各种类加载器之间存在着逻辑上的父子关系,但不是真正意义上的父子关系,因为它们直接没有从属关系。
因为启动类是由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: 不同的类加载器加载同一个类,相等吗?同一个类加载器多次加载一个类的时候,会加载几次?
答:不相等!这涉及到类加载器加载的类的存储空间,不同类加载器加载类之后存储的空间是不同的。
我们都知道,类加载后是存储在方法区的,而方法区中的类其实是按照类加载器分开存储的。所以即使不同的类加载器加载了同一个类,它们也是存储在不同的空间中的。
而同一个类加载器多次加载一个类的时候,最终只会加载一次! 类加载器在加载一个类之前,会通过全限定名去判断该类加载器的空间中是否已经存在该类,如果不存在,才会去加载。
这里加载后的类指的是instanceKlass。
其实,JVM中是存在双亲委派模式的,所以上述情况在真实场景下并不会发生,这里只是为了更好的了解类加载器是如何存储instanceKlass的。
可以参考这篇文章:https://zhuanlan.zhihu.com/p/269214344
如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。
3.1、类加载过程
app -> ext -> bootstrap
AppClassLoader去加载一个类:
上面的描述是错误的!!! 向上委托即在已经加载的类缓存中如果没有找到要加载的类,这时候就会向下委托,首先根加载器查看自己的加载路径中是否存在这个类,存在则加载,不存在则继续向下委托,直到自定义类加载器还没有找到这个类,则报错ClassNotFound.
可以参考这篇文章:https://zhuanlan.zhihu.com/p/269214344
3.2、双亲委派机制特点:
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;
}
}
我们需要明确一个概念,什么叫打破双亲委派机制?如何打破?
我们知道,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。
SpringBoot服务发现,也使用了SPI机制。
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类不是。
沙箱安全机制的目的是为了防止打破双亲委派,篡改系统类,是为了保护系统类。
看openjdk源码会看到有这样的判断AccessController.doPrivileged
比如我定义了一个类名为String所在包为java.lang,因为这个类本来是属于jdk的,如果没有沙箱安全机制的话,这个类将会污染到我所有的String,但是由于沙箱安全机制,所以就委托顶层的bootstrap加载器查找这个类,如果没有的话就委托extsion,extsion没有就到aapclassloader,但是由于String就是jdk的源代码,所以在bootstrap那里就加载到了,先找到先使用,所以就使用bootstrap里面的String,后面的一概不能使用,这就保证了不被恶意代码污染.