关于class loader有太多太多的文章和图来讲过程。我就不多说了。以下是我认为的一些要点。
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
以上是ClassLoader的loadClass方法的摘要。我们可以看到并发和重复加载控制,双亲委派的逻辑(parent.loadClass(name, false))。
findClass后就能拿到Class对象,然后再resolve。findClass的实现都是在子类中,但是都需要经过defineClass
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
//preDefineClass method
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
忽略ProtectionDomain,因为涉及java的安全策略,我也不了解,并且不影响class load的过程理解。
注意一下preDefineClass中的两个if。它对传入的class的全名有一定约束,特别是不能以java.开头。由于defineClass是final的,所以如果你用自定义加载器也没法加载这样的类。
至于defineClass和剩下的resolve都是native方法,并且resolve不是在load的是否必须的。但是在你创建实例的时候JVM是已经对这个class resolve过了。
所以load一个class文件的核心其实是defineClass以及对应native方法。native就包含了对class文件内容的校验等。解析是另一个方法resolveClass。
以下代码不严谨处麻烦忽略!!!!!!
public class ClzLoaderSimp extends ClassLoader{
static String sf = "C:\\Users\\Hello.class";
@Override
public Class<?> loadClass(String name){
try {
InputStream is = new FileInputStream(new File(sf));
byte[] clzData = new byte[is.available()];
is.read(clzData);
return defineClass(name, clzData, 0, clzData.length);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws ClassNotFoundException{
ClassLoader loader= new ClzLoaderSimp();
Class<?> a = loader.loadClass("Hello");
System.out.println(a.hashCode());
}
}
以上class loader运行时输出
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
......
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
......
原因是没有遵循双亲委派。
当我们尝试加载Hello类时,发现父类Object没有加载,所以会先加载父类(这也是class加载过程中的一个点)。在我们的class尝试load父类时没有通过preDefineClass校验抛出了异常。
按理说Object这么基础的类早该已经加载了,为什么又要加载呢?因为JVM内部对一个类的唯一标识是全路径名(包名+类名)+该类的class loader。
由于我们没有遵循双亲委派,并且jvm内部内部已经加载的java/lang/Object的class loader也不是ClzLoaderSimp,所以没有找到对应父类。这个时候就出现了上述的行为和错误。
参考jvm代码,当然这里面的实现细节对java开发者来说没有什么意义(起码我是这么认为的,至今没碰到设计一个类时考虑如何能让jvm 更快加载)
jobject loader = (caller == NULL) ? NULL : get_class_loader(env, caller);
jobject pd = (caller == NULL) ? NULL : JVM_GetProtectionDomain(env, caller);
return Unsafe_DefineClass_impl(env, name, data, offset, length, loader, pd);
static jclass jvm_define_class_common(JNIEnv *env, const char *name,
jobject loader, const jbyte *buf,
jsize len, jobject pd, const char *source,
jboolean verify, TRAPS)
详细过程还是非常冗长的,其中在解析完clss文件的数据后会有一个方法
SystemDictionary::find_or_define_instance_class
Symbol* name_h = k->name(); // passed in class_name may be null
ClassLoaderData* loader_data = class_loader_data(class_loader);
unsigned int d_hash = dictionary()->compute_hash(name_h, loader_data);
int d_index = dictionary()->hash_to_index(d_hash);
以及void SystemDictionary::define_instance_class(instanceKlassHandle k, TRAPS)
Symbol* name_h = k->name();
unsigned int d_hash = dictionary()->compute_hash(name_h, loader_data);
int d_index = dictionary()->hash_to_index(d_hash);
check_constraints(d_index, d_hash, k, class_loader_h, true, CHECK);
// Register class just loaded with class loader (placed in Vector)
// Note we do this before updating the dictionary, as this can
// fail with an OutOfMemoryError (if it does, we will *not* put this
// class in the dictionary and will not update the class hierarchy).
// JVMTI FollowReferences needs to find the classes this way.
if (k->class_loader() != NULL) {
methodHandle m(THREAD, Universe::loader_addClass_method());
JavaValue result(T_VOID);
JavaCallArguments args(class_loader_h);
args.push_oop(Handle(THREAD, k->java_mirror()));
JavaCalls::call(&result, m, &args, CHECK);
}
我们可以看到jvm在存储class数据时候需要class的name(包含包名)以及对应classloader数据来做hash。
在这个过程中其实可以去看看class文件的解析代码,包含了魔数,minor version等在介绍class文件结构时提到的东西。
ClassFileParser::parseClassFile(Symbol* name,
ClassLoaderData* loader_data,
Handle protection_domain,
KlassHandle host_klass,
GrowableArray<Handle>* cp_patches,
TempNewSymbol& parsed_name,
bool verify,
TRAPS)
回到java的class loader,简单的来讲可以改成如下
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> clzInst = null;
try {
clzInst = getParent().loadClass(name);
}catch (ClassNotFoundException e) {
}
if(clzInst!=null){
return clzInst;
}
try(InputStream is = new FileInputStream(new File(sf)) ) {
byte[] clzData = new byte[is.available()];
is.read(clzData);
return defineClass(name, clzData, 0, clzData.length);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
或者单纯重写findClass,就像URLClassLoader一样
@Override
protected Class<?> findClass(String name) {
Path p = Paths.get("C:\\Users\\",name.replace('.', '/').concat(".class"));
try(InputStream is = new FileInputStream(p.toFile()) ) {
byte[] clzData = new byte[is.available()];
is.read(clzData);
return defineClass(name, clzData, 0, clzData.length);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
举个例子,实现相同class name的重复加载。我们有两个zip,其中都有一个BoyCry对Cry接口的实现。
通过不同的class loader 能同时加载两个zip中的Boycry。进而实现很多功能,比如插件等等。
关于这一块有个很好的文章可参考,其中有我这个例子中没有错误 https://www.ibm.com/developerworks/cn/java/j-lo-hotswapcls/index.html
public interface Cry {
void doCry();
}
public class DiffLoader {
public static Map<String,Class> buildExample() throws MalformedURLException, ClassNotFoundException {
Map<String,Class> data = new HashMap<>();
String name = "BoyCry";
String basePath = "C:\\Users\\MyCode\\";
Path p1 = Paths.get(basePath,"1","BoyCry1.zip");
ClassLoader loader1 = new URLClassLoader(new URL[]{p1.toUri().toURL()});
data.put("1",loader1.loadClass(name));
Path p2 = Paths.get(basePath,"2","BoyCry2.zip");
ClassLoader loader2 = new URLClassLoader(new URL[]{p2.toUri().toURL()});
data.put("2",loader2.loadClass(name));
return data;
}
public static void main(String[] args) throws Exception{
Map<String,Class> data = buildExample();
Cry cry =null;
cry = (Cry) data.get("1").newInstance();
cry.doCry();
cry = (Cry) data.get("2").newInstance();
cry.doCry();
}
}
上文提到没有遵守双亲委派时会jvm会尝试加载java/lang/Object,但是因为preDefineClass的校验而失败。我们尝试通过Unsafe直接调用native的方法是否就可以呢?当然不可以!
请看例子 在运行前我们要自己创建一个Object类,包名是java.lang放到对应路径下(C:\Users\MyCode\)
public class UnsafeClzLoaderSimp extends ClassLoader{
static String sf = "C:\\Users\\MyCode\\";
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Path p = Paths.get(sf,name.replace('.', '/').concat(".class"));
try(InputStream is = new FileInputStream(p.toFile()) ) {
byte[] clzData = new byte[is.available()];
is.read(clzData);
return getUnSafe().defineClass(name,clzData,0, clzData.length,this,null);
} catch (IOException | NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
static Unsafe getUnSafe() throws NoSuchFieldException, IllegalAccessException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return(Unsafe) field.get(null);
}
public static void main(String[] args) throws ClassNotFoundException{
ClassLoader loader= new UnsafeClzLoaderSimp();
Class<?> a = loader.loadClass("Hello");
System.out.println(a.hashCode());
}
}
运行结果
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at sun.misc.Unsafe.defineClass(Native Method)
at UnsafeClzLoaderSimp.loadClass(UnsafeClzLoaderSimp.java:19)
at sun.misc.Unsafe.defineClass(Native Method)
at UnsafeClzLoaderSimp.loadClass(UnsafeClzLoaderSimp.java:19)
at UnsafeClzLoaderSimp.main(UnsafeClzLoaderSimp.java:34)
如果看了上文提到parse file的native代码,再往下看应该就知道原因了
SystemDictionary::resolve_from_stream方法
if (!HAS_PENDING_EXCEPTION &&
!class_loader.is_null() &&
parsed_name != NULL &&
!strncmp((const char*)parsed_name->bytes(), pkg, strlen(pkg))) {
// It is illegal to define classes in the "java." package from
// JVM_DefineClass or jni_DefineClass unless you're the bootclassloader
//省略一堆代码..........................
const char* fmt = "Prohibited package name: %s";
size_t len = strlen(fmt) + strlen(name);
char* message = NEW_RESOURCE_ARRAY(char, len);
jio_snprintf(message, len, fmt, name);
Exceptions::_throw_msg(THREAD_AND_LOCATION,
vmSymbols::java_lang_SecurityException(), message);
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。