本文略微记录了jvm的类加载流程。暂时未涉及OOP和Klass相关的知识。
一、java代码的类加载机制
这个大家都懂,双亲委派机制,首先附上类加载器的继承关系
其中AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader没有父加载器。
我们知道jdk中自配的ClassLoader是,调用其ClassLoader的loadClass方法来完成类加载的
- 虽然
URLClassLoader
和AppClassLoader
都复写了loadClass方法,但是两者的实现都是做了下类名检查就调用了父类的loadClass方法。因此最后实现是ClassLoader#loadClass
.
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;
}
}
上述代码逻辑大致如下:
先由findLoadedClass
->native方法findLoadedClass0
,检查是否是已经加载的类,如果没加载过且父加载器不为空,则尝试由父加载器来加载。
如果父加载器为空,那么需要调用findBootstrapClassOrNull
->底层的native方法findBootstrapClass
来检查是否是顶级类加载器BootstrapClassLoader
加载过的类。
如果不是,那么则调用findClass由子类自己去加载。我们看一下 URLClassLoader
是怎么干的(此处忽略代码)
在URLClassLoader#findClass
中,根据类加载器的加载路径(包括jar),找到对应的文件,最后调用defineClass
->native方法defineClass1
将java层的类注册到jvm中,那么下次再获取的时候就可以从findLoadedClass
方法中找到了。
双亲委派模型有以下几个优点:
- 安全,父加载器优先于子加载器,核心的类库只能由jvm自己去加载。
- 共享,子加载器可以直接使用父加载器加载过的类,使得系统不会多次加载一个类。
二、jvm中的类加载机制
从上述分析可以得到,java类的核心加载,格式解析,校验均是由底层来完成的。class文件格式可以参考文章记一次解析class文件过程
从实现方式来看,可以分为两大类。1.顶级类加载器bootStrapClassLoader
加载jdk核心类库;2.用户代码由native方法defineClass1
方式加载到jvm中的类,其中jvm中实现类加载的类是SystemDictionary
。
2.1 jdk类加载流程
之前在<<JVM源码分析(一) -- java启动流程>>中说到,java在启动时会调用jvm库的create_vm
->init_globals
方法时,完成一些重要的初始化工作,其中重要的有
parse_vm_init_args
解析vm参数,vm参数在里面均可找到。universe_init
初始化gc策略和,全局堆 和tlab等。tlab是在eden区切出一小块,(每个线程均有一个,具体是ThreadLocalAllocBuffer::initialize
,没看懂是如何计算的,有人说是256k)。vmSymbols::initialize
创建基础类型(int,boolean...)的klass句柄。SystemDictionary::initialize
加载jdk中重要的类....
jdk中定义了一些重要的类名为_well_known_klasses
,_well_known_klasses
的组成可以由systemDictionary.hpp#WK_KLASSES_DO
枚举到,涵盖了Object,String,Class,...一系列重要的类,SystemDictionary::initialize
负责将_well_known_klasses
中的类都加载到jvm中,流程如下:
遍历顶级加载器的加载路径,用
LazyClassPathEntry::open_stream
寻找到要加载的类的class文件流调用
ClassFileParser::parseClassFile
执行具体的从文件流读取数据,根据class文件格式标准解析class文件,生成对应的instanceKlass对象。调用
SystemDictionary::find_or_define_instance_class
->SystemDictionary::update_dictionary
->Dictionary::add_klass
将生成的Klass对象存起来。Dictionary
是个hash表实现,使用的也是开链法解决hash冲突。
2.2 用户自定义类加载流程
除却jvm初始化加载的类,其他类的加载是由java程序触发的,调用native方法defineClass1
。
流程和初始化加载类流程差不多。由java代码传入文件流句柄和类名,调用底层代码SystemDictionary::resolve_from_stream
。其中主要是两件事,1.ClassFileParser::parseClassFile
读取文件生成Klass。2.调用SystemDictionary#define_instance_class
将类加载到Klass字典中。
2.3 手动测试用户类加载
上面说了jdk中的类是如何被加载到jvm的,下面测试下一个自定义java类的加载流程。
类被加载的时机有许多种:new
关键字,Class.forName
方法,初始化某个类的子类,调用类的静态方法等等。为了简单测试,下面使用了Class.forName
来测试,测试代码
import java.lang.reflect.InvocationTargetException;
import sun.misc.Launcher;
public class Hello {
public static void main(String[] args) {
System.out.println("begin");
try {
Thread.sleep(10000);
Class<?> c = Class.forName("printClass");
Thread.sleep(10000);
Class<?> c2 = Class.forName("printClass");
//c.getMethod("print").invoke(c.newInstance());
c.getMethod("print").invoke(c);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class printClass{
public static void print(){
System.out.println("123");
}
}
2.3.1 测试类的加载流程
Sleep 10000的作用是为了在attach到进程时,jdk类已经加载完毕,可以直接跑到Class<?> c = Class.forName("printClass");
后的断点,在jvm参数中加入参数-XX:+TraceClassLoading
可以将加载的类打印出来。命令行显示printClass
还未加载,堆栈如下
和上述分析结果大致一样,java部分堆栈有缺失,但是我们可以根据以上的分析补充上缺失的堆栈和流程,依次是
- 调用'forName'方法加载
printClass
类 - jvm调用java方法
loadClass
-
printClass
不是顶级类加载加载的,于是调用URLClassLoader#findClass
- jvm的DoPrivileged方法
- jvm校验权限->调用defineClass
- jvm加载类
随后,jvm调用SystemDictionary::update_dictionary
->Dictionary::add_klass
将类加入已加载列表。插入字典的堆栈如下
2.3.2 测试已加载的类查找流程
在第二次调用forName
方法时,findLoadedClass
已经可以返回结果。查看堆栈
- 调用
SystemDictionary::find
方法查找类,函数中根据类加载器信息和类名信息计算hash值。 - 根据hash值从
Dictionary
类中查找到DictionaryEntry
- 从
DictionaryEntry
中获取Klass信息。
2.3.3 新建对象流程
当java对象调用new关键字新建class对象的时候,会调用到底层的InterpreterRntime::_new(JavaThread* thread, ConstantPool* pool, int index)
方法。
IRT_ENTRY(void, InterpreterRntime::_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);
// At this point the class may not be fully initialized
// because of recursive initialization. If it is fully
// initialized & has_finalized is not set, we rewrite
// it into its fast version (Note: no locking is needed
// here since this is an atomic byte write and can be
// done more than once).
//
// Note: In case of classes with has_finalized we don't
// rewrite since that saves us an extra check in
// the fast version which then would call the
// slow version anyway (and do a call back into
// Java).
// If we have a breakpoint, then we don't rewrite
// because the _breakpoint bytecode would be lost.
oop obj = klass->allocate_instance(CHECK);
thread->set_vm_result(obj);
IRT_END
重点是
klass_at
方法,index参数是该类常量区标识自身类名的位置。最后调用SystemDictionary::resolve_or_fail
方法将Klass对象取出。校验并确保初始化
调用
allocate_instance
从堆中分配内存.