主要是对ClassLoader相关的知识进行一个总结,讨论和解决以下问题:
- Class和ClassLoader是什么关系,Class.forName 和ClassLoader.loadClass的联系与区别?
- new的过程是什么,在jvm中如何执行,new和classLoader有什么关系,被new的对象的classLoader是什么,为什么new如此重要?
- 什么是类型隔离,为什么要类型隔离,如何隔离,和ClassLoader有什么关系?
这些问题你确定能很深入和很体系化的回答吗,而不是只大致的知道一个表面,如果不能,那就一起来看看吧!
1、Class.forName & ClassLoader.loadClass
Class.forName(String anme)和ClassLoader.loadClass(String name)都可以用来进行类型加载,而在java进行类型加载时,一般会有多个ClassLoader可以使用,并可以使用多种方式进行类型加载。
class A {
public void m() {
A.class.getClassLoader().loadClass(“B”);
}
}
在A.class.getClassLoader().loadClass(“B”);代码执行B的加载过程时,一般会有三个概念上的ClassLoader提供使用:
- CurrentClassLoader,称之为当前类加载器,简称CCL,在代码中对应的就是类型A的类加载器;
- SpecificClassLoader,称之为指定类加载器,简称SCL,在代码中对应的是 A.class.getClassLoader(),如果使用任意的ClassLoader进行加载,这个ClassLoader都可以称之为SCL;
- ThreadContextClassLoader,称之为线程上下文类加载器,简称TCCL,每个线程都会拥有一个ClassLoader引用,而且可以通过Thread.currentThread().setContextClassLoader(ClassLoader classLoader)进行切换。
1.1 Class.forName与SystemDictionary
Class.forName(static方法)是根据给定的类型全名从CCL中加载指定的类型。
public static Class> forName(String className)
throws ClassNotFoundException {
Class> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
Reflection.getCallerClass(),获取到调用Class.forName方法的类,隐含意义就是CCL。
具体的加载的逻辑在native方法forName0中定义。
Class.c中对应实现的逻辑:
JNIEXPORT jclass JNICALL
Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname,
jboolean initialize, jobject loader)
{
// 略
cls = JVM_FindClassFromClassLoader(env, clname, initialize,
loader, JNI_FALSE);
done:
if (clname != buf) {
free(clname);
}
return cls;
}
实现细节在JVM_FindClassFromClassLoader中定义,可以看到调用Class.forName会使用JVM_FindClassFromClassLoader这个函数来进行类型加载,我们需要注意的时clname和loader这两个变量,一个是类的全限定名,另一个是ClassLoader,而Class.forName所使用的ClassLoader是CCL。
在jvm.cpp中FindClassFromClassLoader的对应实现是:
jclass find_class_from_class_loader(JNIEnv* env, Symbol* name, jboolean init, Handle loader, Handle protection_domain, jboolean throwError, TRAPS) {
// Security Note:
// The Java level wrapper will perform the necessary security check allowing
// us to pass the NULL as the initiating class loader.
klassOop klass = SystemDictionary::resolve_or_fail(name, loader, protection_domain, throwError != 0, CHECK_NULL);
// 略
}
SystemDictionary,系统字典,这个数据结构是保存Java加载类型的数据结构,以类的全限定名再加上类加载器作为key,进而确定Class引用。类型加载时,以ClassLoader和需要加载的类型全限定名作为参数在SystemDictionary中进行查询,如果能够查询到则返回。如果无法找到,则调用loader.loadClass(className)进行加载,这一步将进入到Java代码中。
klassOop SystemDictionary::resolve_or_fail(Symbol* class_name, Handle class_loader, Handle protection_domain, bool throw_error, TRAPS) {
klassOop klass = resolve_or_null(class_name, class_loader, protection_domain, THREAD);
if (HAS_PENDING_EXCEPTION || klass == NULL) {
KlassHandle k_h(THREAD, klass);
// can return a null klass
klass = handle_resolution_exception(class_name, class_loader, protection_domain, throw_error, k_h, THREAD);
}
return klass;
}
resolve_or_null在SystemDictionary.c中会调用到resolve_ instance_ class_ or_ null,其中关键的地方是,如果class还没有被加载,则会先进行“load_ instance_class”
klassOop SystemDictionary::resolve_instance_class_or_null(Symbol* name, Handle class_loader, Handle protection_domain, TRAPS) {
class_loader = Handle(THREAD, java_lang_ClassLoader::non_reflection_class_loader(class_loader()));
//......
if (!class_has_been_loaded) {
k = load_instance_class(name, class_loader, THREAD);
}
// Verify protection domain. If it fails an exception is thrown
validate_protection_domain(k, class_loader, protection_domain, CHECK_(klassOop(NULL)));
return k();
}
而load_instance_class进一步调用java代码(JavaCalls::call_virtual),classLoader.loadClass(String name),classLoader.loadClass则依据双亲委派原则进行类型的加载。
instanceKlassHandle SystemDictionary::load_instance_class(Symbol* class_name, Handle class_loader, TRAPS) {
instanceKlassHandle nh = instanceKlassHandle(); // null Handle
ResourceMark rm(THREAD);
JavaThread* jt = (JavaThread*) THREAD;
PerfClassTraceTime vmtimer(ClassLoader::perf_app_classload_time(),
ClassLoader::perf_app_classload_selftime(),
ClassLoader::perf_app_classload_count(),
jt->get_thread_stat()->perf_recursion_counts_addr(),
jt->get_thread_stat()->perf_timers_addr(),
PerfClassTraceTime::CLASS_LOAD);
Handle s = java_lang_String::create_from_symbol(class_name, CHECK_(nh));
// Translate to external class name format, i.e., convert '/' chars to '.'
Handle string = java_lang_String::externalize_classname(s, CHECK_(nh));
JavaValue result(T_OBJECT);
KlassHandle spec_klass (THREAD, SystemDictionary::ClassLoader_klass());
if (MustCallLoadClassInternal && has_loadClassInternal()) {
JavaCalls::call_special(&result,
class_loader,
spec_klass,
vmSymbols::loadClassInternal_name(),
vmSymbols::string_class_signature(),
string,
CHECK_(nh));
} else {
JavaCalls::call_virtual(&result,
class_loader,
spec_klass,
vmSymbols::loadClass_name(),
vmSymbols::string_class_signature(),
string,
CHECK_(nh));
}
oop obj = (oop) result.get_jobject();
if ((obj != NULL) && !(java_lang_Class::is_primitive(obj))) {
instanceKlassHandle k = instanceKlassHandle(THREAD, java_lang_Class::as_klassOop(obj));
if (class_name == k->name()) {
return k;
}
}
// Class is not found or has the wrong name, return NULL
return nh;
}
}
整个class.forName(String name)加载过程,以及与classLoader.loadClass(String name)的交互关系如下图:
对以上过程做一个简要的梳理与总结:
步骤 | 说明 |
---|---|
1 | 调用Class.forName(className)方法,该方法会调用native的JVM实现,调用前该方法会确定准备好需要加载的类名以及ClassLoader,将其传递给native方法 |
2 | 进入到JVM实现后,首先会在SystemDictionary中根据类名和ClassLoader组成hash,进行查询,如果能够命中,则返回 |
3 | 如果加载到则返回 |
4 | 如果在SystemDictionary中无法命中,将会调用Java代码:ClassLoader.loadClass(类名),这一步将委派给Java代码,让传递的ClassLoader进行类型加载 |
5 | 以URLClassLoader为例,ClassLoader确定了类文件的字节流,但是该字节流如何按照规范生成Class对象,这个过程在Java代码中是没有体现的,其实也就是要求调用ClassLoader.defineClass(byte[])进行解析类型,该方法将会再次调用native方法,因为字节流对应Class对象的规范是定义在JVM实现中的 |
6 | 进入JVM实现,调用SystemDictionary的resolve_stream方法,接受byte[],使用ClassFileParser进行解析 |
7 | SystemDictionary::define_instance_class |
8 | 如果类型被加载了,将类名、ClassLoader和类型的实例引用添加到SystemDictionary中 |
9 | 返回 |
10 | 返回 |
11 | 从Java实现返回到Java代码的defineClass,返回Class对象 |
12 | 返回给loadClass(Classname)方法 |
13 | 返回给Java实现的SystemDictionary,因为在resolve_class中调用的ClassLoader.loadClass。这里会做出一个判断,如果加载Class的ClassLoader并非传递给resolve_class的ClassLoader,那么会将类名、传递给resolve_class的ClassLoader以及类型的实例引用添加到SystemDictionary中 |
14 | 返回给Class.forName类型实例 |
1.2 ClassLoader.loadClass
分析了Class.forName之后,再看ClassLoader.loadClass()就会变得简单很多,这个ClassLoader就是一个SCL,而ClassLoader.loadClass()只是相当于一个简单的方法调用。
2、new & class.newInstance & Constructor
2.1 new & newInstance & Constructor 的比较
类的加载方式不同
在使用class.newInstance()方法的时候,必须保证这个类已经加载并且已经连接了,而这可以通过Class的静态方法forName()来完成的。
使用关键字new创建一个类的时候,这个类可以没有被加载,一般也不需要该类在classpath中设定,但可能需要通过classlaoder来加载。
所调用的构造方法不尽相同
- new关键字能调用任何构造方法。
- class.newInstance()只能调用无参构造方法。
执行效率不同
- new关键字是强类型的,效率相对较高。
- class.newInstance()是弱类型的,效率相对较低。
综上分析可以得出:
new A() = Class.forName("A").newInstance();
class.newInstance() & Constructor.newInstance(...)
- Class.newInstance() 只能够调用无参的构造函数,即默认的构造函数。
- Class.newInstance() 抛出所有由被调用构造函数抛出的异常。
- Class.newInstance() 要求被调用的构造函数是可见的,也即必须是public类型的。
- Constructor.newInstance() 在特定的情况下,可以调用私有的构造函数。
- Constructor.newInstance() 可以根据传入的参数,调用任意构造构造函数。
2.2 new的jvm实现过程
new关键字在jvm中被映射为_new指令,具体实现逻辑在bytecodeInterpreter.cpp中。
CASE(_new): {
u2 index = Bytes::get_Java_u2(pc+1);
constantPoolOop constants = istate->method()->constants();
if (!constants->tag_at(index).is_unresolved_klass()) {
// Make sure klass is initialized and doesn't have a finalizer
oop entry = constants->slot_at(index).get_oop();
klassOop k_entry = (klassOop) entry;
instanceKlass* ik = (instanceKlass*) k_entry->klass_part();
if ( ik->is_initialized() && ik->can_be_fastpath_allocated() ) {
size_t obj_size = ik->size_helper();
oop result = NULL;
// If the TLAB isn't pre-zeroed then we'll have to do it
bool need_zero = !ZeroTLAB;
if (UseTLAB) {
result = (oop) THREAD->tlab().allocate(obj_size);
}
if (result == NULL) {
need_zero = true;
// Try allocate in shared eden
retry:
HeapWord* compare_to = *Universe::heap()->top_addr();
HeapWord* new_top = compare_to + obj_size;
if (new_top <= *Universe::heap()->end_addr()) {
if (Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {
goto retry;
}
result = (oop) compare_to;
}
}
if (result != NULL) {
// Initialize object (if nonzero size and need) and then the header
if (need_zero ) {
HeapWord* to_zero = (HeapWord*) result + sizeof(oopDesc) / oopSize;
obj_size -= sizeof(oopDesc) / oopSize;
if (obj_size > 0 ) {
memset(to_zero, 0, obj_size * HeapWordSize);
}
}
if (UseBiasedLocking) {
result->set_mark(ik->prototype_header());
} else {
result->set_mark(markOopDesc::prototype());
}
result->set_klass_gap(0);
result->set_klass(k_entry);
SET_STACK_OBJECT(result, 0);
UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
}
}
}
// Slow case allocation
CALL_VM(InterpreterRuntime::_new(THREAD, METHOD->constants(), index),
handle_exception);
SET_STACK_OBJECT(THREAD->vm_result(), 0);
THREAD->set_vm_result(NULL);
UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
}
_new的逻辑主要分为两大块:
- fast case allocation,要创建对象的类已经加载
- slow case allocation,要创建对象的类未加载
slow case allocation
slow case allocation 会调用 InterpreterRuntime::_new 方法,调用的时候传入了三个参数:
- THREAD,线程
- METHOD->constants,方法对应的常量池
- index,要创建的类在常量池中的索引
InterpreterRuntime::_new
// interpreterRuntime.cpp
IRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* thread, ConstantPool* pool, int index))
Klass* k_oop = pool->klass_at(index, CHECK);
instanceKlassHandle klass (THREAD, k_oop);
// Make sure we are not instantiating an abstract klass
klass->check_valid_for_instantiation(true, CHECK);
// Make sure klass is initialized
klass->initialize(CHECK);
oop obj = klass->allocate_instance(CHECK);
thread->set_vm_result(obj);
IRT_END
加载Klass的代码在pool->klass_at函数里面:
klassOop constantPoolOopDesc::klass_at_impl(constantPoolHandle this_oop, int which, TRAPS) {
CPSlot entry = this_oop->slot_at(which);
if (entry.is_oop()) {
return (klassOop)entry.get_oop();
}
//......
if (do_resolve) {
Handle loader = Handle(THREAD, instanceKlass::cast(this_oop->pool_holder())->class_loader());
// this_oop must be unlocked during resolve_or_fail
oop protection_domain = Klass::cast(this_oop->pool_holder())->protection_domain();
Handle h_prot (THREAD, protection_domain);
klassOop k_oop = SystemDictionary::resolve_or_fail(name, loader, h_prot, true, THREAD);
KlassHandle k = KlassHandle(THREAD, k_oop);
return k();
}
//......
}
最终会调用klass_at_impl,从SystemDictionary中加载对应的klass(SystemDictionary::resolve_or_fail)。注意,SystemDictionary::resolve_or_fail所使用classLoader为instanceKlass::cast(this_oop->pool_holder())->class_loader()),this_oop->pool_holder()即当前对象,所以在使用new的时候,new对象的classLoader和CCL有关。
那么被new对象的classLoader是什么?是CCL?也不一定,只能说new的过程会调用到CCL(ClassLoader.loadClass(...))。但是在CCL的loadClass中具体怎么加载一个对象,具体是那个ClassLoader加载的还不确定,比如pandora中的mouduleClassLoader.loadClass(...)则需要看被加载的对象具体是什么来选择不同的classLoader来进行隔离加载。
总结:使用new初始化一个对象时,如果被初始化的对象类型还没有加载则,需要先进行加载,而在加载类型的过程中会走到SystemDictionary::resolve_or_fail,根据前面的分析SystemDictionary::resolve_or_fail会调用到java方法classLoader::loadClass(String name),这里的classLoader是CCL,但是具体类型怎么load还是要看CCL::loadClass的具体实现。
最后为什么new的过程如此重要,因为一般加载对象的不会全部使用classLoader,而new是一个非常常用的手段。深入的了解new的过程可以加深对java的ClassLoader和SystemDictionary体系的认识,同时也和下面内容有关系,java中为什么要进行类型隔离,如果进行类型隔离,类型隔离和classLoader有什么关系,如果使用classLoader进行隔离那么new出来的对象是否也进行了隔离。
3、类型隔离
3.1 隔离背景&原因
在大型分布式应用中,一般都会涉及到多个中间件应用,比如:PRC中间件、消息中间件、配置管理、数据库等,而众多中间件依赖的jar可能有冲突,比如RPC依赖log4j:1.1,而消息中间件依赖log4j:2.1,但是maven的jar管理机制只会加载其中的一个版本,比如加载了log4j:1.1,那么消息中间件就有可能出现不可预计的问题。随着系统的复杂,jar的管理与加载问题就如同潘多拉一样变得不可控制,如果不做控制和隔离,一个中间件的升级就可能影响到其他的中间件。因此迫切需要解决以下问题:
- 中间件与中间件之间依赖的jar包之间的冲突;
- 中间件与应用系统之间依赖的jar包之间的冲突;
- 中间件的升级管理
3.2 pandora隔离机制的实现
pandora的总体结构如下图:
隔离机制
使用不同的classLoader来实现隔离,pandora中涉及到的ModuleClassLoader中相关的ClassLoader
- moduleClassLoader:pandora插件类,用于加载插件的导出类和内部类
- extClassLoader:java核心ClassLoder用于加载java核心类
- pandoraClassLoader:pandora的ClassLoader用于加载pandora的容器的类(不是插件类)
- systemClassLoader:系统ClassLoader用于加载系统路径下的类
- bizClassLoader:用于加载pandora的导入类
pandora内部的加载体系如下图:
隔离级别
pandora中需要考虑哪些类需要被隔离和共用
- pandora容器类、插件导出类和应用容器(spring)隔离
- pandora容器的类与插件的类隔离
- pandora内部插件之间需要隔离,导出类、导入类、非导出类(包内部类)、内部共用类
- pandora内部插件的类与系统类隔离,插件自己的类直接不需要隔离
隔离实现
pandora插件导出类与应用容器的隔离
pandora的每个插件都有自己专属的moduleClassLoader,其导出类也是使用moduleClassLoader加载,应用容器(spring)的加载则是有webAppClassLoader来完成,这样就可以实现pandora插件导出类与应用容器的隔离。
PandoraContainer::getExportedClasses()会获取所有pandora插件的所有导出类,被输出到AliTomcat的MapRepositoryImpl中。
public Map> getExportedClasses() {
SharedClassService sharedClassService = serviceContainer.getService(SharedClassService.class);
Map> sharedClassMap = sharedClassService.getSharedClassMap();
if (sharedClassMap == null || sharedClassMap.isEmpty()) {
System.err.println("Pandora export empty classes! please check pandora config.");
}
return sharedClassMap;
}
AliTomcat中继承了webAppClassLoaderBase,实现了AliWebappClassLoaderBase,其findClass和loadClass被重写,在加载类的过程中,先从pandora的导出类中查找和加载,如果能加载到则使用pandora的导出类(webAppClassLoader所加载),如果查找和加载不到则继续交由webAppClassLoaderBase,这样就实现pandora插件导出类与应用容器的隔离。
pandora的类加载和隔离方式实际上打破的classLoader的双亲委派原则。
整体的classLoader加载体系如下图:
pandora插件与pandora容器的隔离
ModuleClassLoader::loadClassInternal中会根据不同的加载类类型使用不同的classLoader,从而进行类型的隔离。
private Class> loadClassInternal(String name, boolean resolve) throws PandoraLoaderException {
if (StringUtils.isEmpty(name)) {throw new PandoraLoaderException("class name is blank.");}
// 1. 已经加载的类
Class> clazz = resolveLoaded(name);
if (clazz != null) {return clazz;}
// 2. 加载JDK相关的类
// 隔离pandora插件类与java核心类 - extClassLoader
clazz = resolveBootstrap(name);
if (clazz != null) {return clazz;}
// 3. com.taobao.pandora开头的选择从PandoraClassLoader中加载
// pandora的类与插件类隔离 - pandoraClassLoader
clazz = resolvePandoraClass(name);
if (clazz != null) {return clazz;}
// 4. 从共享缓存加载
// pandora插件之间的共享类隔离 - moduleClassLoader
clazz = resolveShared(name);
if (clazz != null) {return clazz;}
// 5. 根据import语义从bizClassLoader中加载
// pandora插件类与导入类隔离 - bizClassLoader
clazz = resolveImport(name);
if (clazz != null) {return clazz;}
// 6. 从当前classpath下加载
// 插件内部的类不隔离,直接使用同一个ModuleClassLoader
clazz = resolveClassPath(name);
if (clazz != null) {return clazz;}
// 7. 从bizClassLoader中加载,如果有bizClassloader,或者说usebizclassloader设置成为了true
clazz = resolveExternal(name);
if (clazz != null) {return clazz;}
// 8. 从SystemClassLoader下加载,解决agent加载的问题
// pandora插件类与系统类隔离 - systemClassLoader
clazz = resolveSystemClassLoader(name);
if (clazz != null) {
if (resolve) {resolveClass(clazz);}
return clazz;
} else {
throw new PandoraLoaderException("[Module-Loader] " + moduleName + ": can not load class {" + name + "} after all phase.");
}
}
加载策略
为了保证JDK中类加载的安全性、模块之间的隔离性,以上8种类加载方式按照如下的顺序进行尝试:
- 首先,要保证JDK类加载的安全性,因此1(resolveLoaded)和2(resolveBootstrap)排在最前面;
- 其次,需要保证Pandora容器类加载的正确性,因此5(resolvePandoraClass)排在第3;
- 对于模块而言,保证其声明的语义是最重要的,因此3(resolveImport)排在第4;
- 然后,需要实现模块间的共享,因此4(resolveShared)排在第5;
- 在共享语义均完成的情况下,加载自己的,也就是7(resolveClassPath)排在第6;
- 如果步骤均失败,那么尝试从外部加载,也就是8(resolveExternal)排在第7;
- 最后,为了解决Agent问题,尝试从SystemClassLoader中加载,也就是6(resolveSystemClassLoader)。