我们已知道java加载器双亲委派机制,也知道调用loadClass方法可自定义加载类。不管我们在JAVA中通过JNI的方式(ClassLoader JNI接口的实现源码在jdk/src/share/native/java/lang/ClassLoader.c中)或者在虚拟机中直接调用,最终关系都离不开以下几个类:
虚拟机自动加载以java.lang.String为例
Class load in JVM init
- 加载器初始化
Threads::create_vm()《openJdk的启动流程》中的方法->
init_globals()–>
classLoader_init()–>
ClassLoader::initialize()
- 加载
Threads::create_vm()->
initialize_class(vmSymbols::java_lang_String(), CHECK_0)–>
SystemDictionary::resolve_or_fail–>
SystemDictionary::resolve_instance_class_or_null->
SystemDictionary::load_instance_class->
ClassLoader::load_classfile->
ClassFileParser::parseClassFile
- 链接
Threads::create_vm()->
initialize_class(vmSymbols::java_lang_String(), CHECK_0)–>
InstanceKlass::initialize–>
InstanceKlass::link_class->
InstanceKlass::link_class_impl
ClassLoader JNI Call
- 加载
class.getClassLoader().loadClass (java中的方法)->
classLoader.findBootstrapClass–>
SystemDictionary::resolve_or_null–>
SystemDictionary::resolve_instance_class_or_null->
SystemDictionary::load_instance_class->
ClassLoader::load_classfile->
ClassFileParser::parseClassFile
- 链接
InterpretrRuntime:_new (java 中 new语法,触发)->
InstanceKlass::initialize–>
InstanceKlass::link_class->
InstanceKlass::link_class_impl
ClassLoader
ClassLoader类的定义在classfile/classLoader.hpp中,ClassLoader是用于加载Java核心类文件。
//ClassLoader是一个静态类,里面全是全局d定义
class ClassLoader: AllStatic {
//ClassPathEntry类指针,ClassPathEntry用于表示单个classpath路径,所有的ClassPathEntry实例以链表的形式关联起来,_first_entry表示链表的第一个实例
static ClassPathEntry* _first_entry;
// 表示链表的最后一个实例
static ClassPathEntry* _last_entry;
//ClassPathEntry实例以链表数量
static int _num_entries;
// 用于保存已经加载过的包名
static PackageHashtable* _package_hash_table;
//以下几个方法ClassPathEntry链表相关的操作方法:
//setup_search_path
//contains_entry
//add_to_list
//num_classpath_entrie,
//classpath_entry
//create_class_path_entry
//update_class_path_entry_list
//加载zip文件读取写入等操作的动态链接库
//load_zip_library
//创建虚拟机时的初始化的方法,其调用路径
//Threads::create_vm()《openJdk的启动流程》中的方法->
//init_globals()-->
//classLoader_init()-->
//ClassLoader::initialize()
void ClassLoader::initialize() {
assert(_package_hash_table == NULL, "should have been initialized by now.");
EXCEPTION_MARK;
if (UsePerfData) {
//如果开始性能检测则初始化各计数器
NEWPERFTICKCOUNTER(_perf_accumulated_time, SUN_CLS, "time");
NEWPERFTICKCOUNTER(_perf_class_init_time, SUN_CLS, "classInitTime");
}
//加载读写zip文件的动态链接库
load_zip_library();
//设置加载核心jar包的搜索路径,从系统参数Arguments中获取
//使用参数-XX:+TraceClassPaths会打印出具体
//bootstrap路径
//classpath
//path
setup_bootstrap_search_path();
//如果是惰性启动加载,即启动时不加载rt.jar等文件
if (LazyBootClassLoader) {
//设置meta_index_path,设置完成后会触发对meta_index_path下文件的解析
setup_bootstrap_meta_index();
}
}
//根据类名加载指定类文件的方法
instanceKlassHandle ClassLoader::load_classfile(Symbol* h_name, TRAPS) {
ResourceMark rm(THREAD);
//获取类名
const char* class_name = h_name->as_C_string();
//-XX:+TraceClassLoadingPreorder参数能打印出来
EventMark m("loading class %s", class_name);
ThreadProfilerMark tpm(ThreadProfilerMark::classLoaderRegion);
stringStream st;
st.print_raw(h_name->as_utf8());
st.print_raw(".class");
//获取文件名
const char* file_name = st.as_string();
ClassLoaderExt::Context context(class_name, file_name, THREAD);
//ClassFileStream表示Class文件的字节流
ClassFileStream* stream = NULL;
int classpath_index = 0;
ClassPathEntry* e = NULL;
instanceKlassHandle h;
{
//从第一个ClassPathEntry开始遍历所有的ClassPathEntry
e = _first_entry;
while (e != NULL) {
stream = e->open_stream(file_name, CHECK_NULL);
//如果检查返回false则返回null,check方法默认返回true
if (!context.check(stream, classpath_index)) {
return h; // NULL
}
//如果找到目标文件则跳出循环
if (stream != NULL) {
break;
}
e = e->next();
++classpath_index;
}
}
//如果找到了目标class文件
if (stream != NULL) {
//构建一个ClassFileParser实例
ClassFileParser parser(stream);
//构建一个ClassLoaderData实例
ClassLoaderData* loader_data = ClassLoaderData::the_null_class_loader_data();
Handle protection_domain;
TempNewSymbol parsed_name = NULL;
//解析并加载class文件,注意此时并未开始链接
//-XX:+TraceClassLoading可打出加载好的日志
instanceKlassHandle result = parser.parseClassFile(h_name,loader_data,protection_domain,parsed_name,context.should_verify(classpath_index),THREAD);
//如果解析异常
if (HAS_PENDING_EXCEPTION) {
ResourceMark rm;
if (DumpSharedSpaces) {
//打印异常
tty->print_cr("Preload Error: Failed to load %s", class_name);
}
return h;
}
//调用ClassLoader的add_package方法,把当前类的包名加入到_package_hash_table中
h = context.record_result(classpath_index, e, result, THREAD);
} else {
//没有找到目标文件
if (DumpSharedSpaces) {
tty->print_cr("Preload Warning: Cannot find %s", class_name);
}
}
return h;
}
在HotSpot VM中用ClassLoader表示类加载器,可以使用ClassLoader::load_class()加载磁盘上的字节码文件,但是类加载器的相关数据却是存放在ClassLoaderData,简称CLD。源码中很多CLD字样指的就是类加载器的数据。每个类加载器都有一个对应的CLD结构,这是一个重要的数据结构
CLD存放了所有被该ClassLoader加载的类、当前类加载器的Java对象表示、管理内存的metaspace等。另外CLD还指示了当前类加载器是否存活、是否需要卸载等。除此之外,CLD还有一个next字段指向下一个CLD,所有CLD连接起来构成一幅CLD图,即ClassLoaderDataGraph。
通过调用
ClassLoaderDataGraph::classes_do可以在垃圾回收过程中很容易地遍历该结构找到所有类加载器加载的所有类。
Symbol
Symbol类的定义位于oops/symbol.hpp中,Symbol表示一个规范化的字符串形式的描述符,如方法Object m(int i, double d, Thread t) {…}对应的方法描述符就是(IDLjava/lang/Thread;)Ljava/lang/Object.关于描述述的概念在《class文件和字节码解析》
//vmSymbols::java_lang_String()就是虚拟机提前创建好的java.lang.String的描述符
class Symbol : MetaspaceObj {
volatile short _refcount;//表示该Symbol的引用计数
unsigned short _length;//UTF-8字符串的长度
int _identity_hash;//hash唯一标识码
jbyte _body[1];//用于存储描述符对应的字符串
//可以操作_body属性的
//byte_at_put(int index, int value)
//打印具体字符串内容
//as_C_string(),as_utf8()
//引用计数相关
//refcount(),increment_refcount(),decrement_refcount()
}
SymbolTable
SymbolTable的定义位于classfile/symbolTable.hpp中,对应于C/C++编译过程的符号表,用于保存管理所有的Symbol实例,StringTable就是Java特有的字符串常量池.用作快速查找字符串
SystemDictionary
SystemDictionary类的定义在classfile/systemDictionary.hpp中,是一个系统字典类,用于保存所有已经加载完成的类。
//SystemDictionary相当于类加载的一个统一入口,同时提供查找已加载的类和加载新的类的服务
class SystemDictionary : AllStatic {
//JVM对不同数据设置不同的hashMap容量,保存在_primelist中,当前hashMap要多大的容量取决于_sdgeneration的值
static int _sdgeneration;
static const int _primelist[_prime_array_size];
//实际保存已加载类的HashMap
//key使用类名加classloader的形式
//dictionary()->compute_hash(name_h, loader_data);
static Dictionary* _dictionary;
//当类加载的过程中临时存储键值对的地方,底层数据结构同Dictionary类
static PlaceholderTable* _placeholders;
//JVM 将类信息加载, 解析成为元数据,并根据是否需要修改,将其分类为 Read-Only 部分和 Read-Write 部分。然后,将这些元数据直接存储在文件系统中,作为所谓的 Shared Archive.
// 共享架构下用于保存已加载类的HashMap
static Dictionary* _shared_dictionary;
// 发生修改的次数,类加载或者删除都会增加该计数器
static int _number_of_modifications;
// 系统类加载器的对象锁
static oop _system_loader_lock_obj;
// 保存类加载器加载约束的HashTable
static LoaderConstraintTable* _loader_constraints;
// 保存类解析错误的HashTable
static ResolutionErrorTable* _resolution_errors;
// Invoke methods (JSR 292)
//保存MethodHandle调用的解析结果
static SymbolPropertyTable* _invoke_method_table;
//系统类加载器的引用
static oop _java_system_loader;
//属性操作的相关方法
//check_constraints,add_placeholder,add_klass,dictionary
//根据类加载器和类名加载类的方法
//resolve_or_fail,resolve_or_null,resolve_super_or_fail
//根据class文件流,类加载器和类名加载类的方法
//parse_stream,resolve_from_stream
//解析类的最终都会进入此方法
instanceKlassHandle SystemDictionary::load_instance_class(Symbol* class_name, Handle class_loader, TRAPS) {
instanceKlassHandle nh = instanceKlassHandle(); // null Handle
//如果class_loader为空,即Java类加载器无法加载该类了
if (class_loader.is_null()) {
instanceKlassHandle k;
//目标类未加载
if (k.is_null()) {
//执行目标类的加载
k = ClassLoader::load_classfile(class_name, CHECK_(nh));
}
//如果已经加载则查找加载的类
//并把其加入到_dictionary中
if (!k.is_null()) {
k = find_or_define_instance_class(class_name, class_loader, k, CHECK_(nh));
}
return k;
} else {
ResourceMark rm(THREAD);
assert(THREAD->is_Java_thread(), "must be a JavaThread");
JavaThread* jt = (JavaThread*) THREAD;
//构建JNI调用的参数,即类名
Handle s = java_lang_String::create_from_symbol(class_name, CHECK_(nh));
Handle string = java_lang_String::externalize_classname(s, CHECK_(nh));
//调用的结果
JavaValue result(T_OBJECT);
//结果的处理器
KlassHandle spec_klass (THREAD, SystemDictionary::ClassLoader_klass());
//调用Java的类加载器加载特定类
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
assert(result.get_type() == T_OBJECT, "just checking");
oop obj = (oop) result.get_jobject();
//检查访问权限
if ((obj != NULL) && !(java_lang_Class::is_primitive(obj))) {
instanceKlassHandle k =
instanceKlassHandle(THREAD, java_lang_Class::as_Klass(obj));
//检查类名是否一致
if (class_name == k->name()) {
return k;
}
}
//类加载失败,返回空对象
return nh;
}
}
}
ClassFileParser&Verifier
- ClassFileParser类的定义在classfile/classFileParser.hpp中,类解析器,用来解析class文件,它利用ClassFileStream读取class文件的输入流作为ClassFileParser.parseClassFile核心方法的输入。
Veritier类的定义在classfile/Veritier.hpp中,类验证器。其核心方法是Verifier::verify
InstanceKlass
做为oop-klass二分模型的主要类.nstanceKlass是HotSpot VM中一个非常重要的数据结构,java.lang.Class在Java层描述对象的类,而InstanceKlass在虚拟机层描述对象的类,它记录类有哪些字段,名字是什么,类型是什么,类名是什么,解释器如何执行它的方法等信息。类加载的一个完整流程如下:
- 分配InstanceKlass所需内存
(InstanceKlass::allocate_instance_klass);
- 使用parse_stream()得到的数据填充InstanceKlass的字段,如major/minor version;
- 如果引入了miranda方法,设置对应
flag(set_has_miranda_methods);
- 初始化itable(
`klassItable::setup_itable_offset_table); - 初始化OopMapBlock(fill_oop_maps);
- 分配klass对应的java.lang.Class,在Java层描述类
(java_lang_Class::create_mirror);
- 生成Java8的default方法
(DefaultMethods::generate_default_methods);
- 得到完整的InstanceKlass
InstanceKlass{
void initialize_impl(instanceKlassHandle this_oop, TRAPS) {
//完成此类的链接,如果已链接则会立即返回
this_oop->link_class(CHECK);
DTRACE_CLASSINIT_PROBE(required, InstanceKlass::cast(this_oop()), -1);
bool wait = false;
// Step 1
{
//获取对象锁
oop init_lock = this_oop->init_lock();
ObjectLocker ol(init_lock, THREAD, init_lock != NULL);
Thread *self = THREAD; // it's passed the current thread
// Step 2
//如果正在初始化则等待初始化完成
while(this_oop->is_being_initialized() && !this_oop->is_reentrant_initialization(self)) {
wait = true;
ol.waitUninterruptibly(CHECK);
}
//等待超时返回
if (this_oop->is_being_initialized() && this_oop->is_reentrant_initialization(self)) {
DTRACE_CLASSINIT_PROBE_WAIT(recursive, InstanceKlass::cast(this_oop()), -1,wait);
return;
}
//初始化完成返回
if (this_oop->is_initialized()) {
DTRACE_CLASSINIT_PROBE_WAIT(concurrent, InstanceKlass::cast(this_oop()), -1,wait);
return;
}
//状态异常,抛出异常
if (this_oop->is_in_error_state()) {
DTRACE_CLASSINIT_PROBE_WAIT(erroneous, InstanceKlass::cast(this_oop()), -1,wait);
ResourceMark rm(THREAD);
const char* desc = "Could not initialize class ";
const char* className = this_oop->external_name();
size_t msglen = strlen(desc) + strlen(className) + 1;
char* message = NEW_RESOURCE_ARRAY(char, msglen);
if (NULL == message) {
// Out of memory: can't create detailed error message
THROW_MSG(vmSymbols::java_lang_NoClassDefFoundError(), className);
} else {
jio_snprintf(message, msglen, "%s%s", desc, className);
THROW_MSG(vmSymbols::java_lang_NoClassDefFoundError(), message);
}
}
// 设置状态,初始化进行中
this_oop->set_init_state(being_initialized);
this_oop->set_init_thread(self);
}
// Step 7
//如果不是一个接口而是一个类则需要初始化它的父类
if (!this_oop->is_interface()) {
//获取父类
Klass* super_klass = this_oop->super();
//初始化父类
if (super_klass != NULL && super_klass->should_be_initialized()) {
super_klass->initialize(THREAD);
}
//实现的接口存在默认方法则初始化接口
if (!HAS_PENDING_EXCEPTION && this_oop->has_default_methods()) {
this_oop->initialize_super_interfaces(this_oop, THREAD);
}
//初始化异常,抛出异常
if (HAS_PENDING_EXCEPTION) {
Handle e(THREAD, PENDING_EXCEPTION);
CLEAR_PENDING_EXCEPTION;
{
EXCEPTION_MARK;
this_oop->set_initialization_state_and_notify(initialization_error, THREAD);
CLEAR_PENDING_EXCEPTION;
}
DTRACE_CLASSINIT_PROBE_WAIT(super__failed, InstanceKlass::cast(this_oop()), -1,wait);
THROW_OOP(e());
}
}
// Step 8
{
assert(THREAD->is_Java_thread(), "non-JavaThread in initialize_impl");
JavaThread* jt = (JavaThread*)THREAD;
DTRACE_CLASSINIT_PROBE_WAIT(clinit, InstanceKlass::cast(this_oop()), -1,wait);
//执行静态方法
//final static变量和接口的其值为编译期常数的域首先初始化(在准备阶段执行)
//按文本顺序执,下面三种情况会会javac整理进
//类变量的初始化器 static int b = 4;
//静态初始化函数 static {...}
//接口域初始化器 static int a = 4;
this_oop->call_class_initializer(THREAD);
}
// Step 9
if (!HAS_PENDING_EXCEPTION) {
//设置状态初始化完成
this_oop->set_initialization_state_and_notify(fully_initialized, CHECK);
{ ResourceMark rm(THREAD);
debug_only(this_oop->vtable()->verify(tty, true);)
}
}
else {
//初始化失败,抛出异常
Handle e(THREAD, PENDING_EXCEPTION);
CLEAR_PENDING_EXCEPTION;
JvmtiExport::clear_detected_exception((JavaThread*)THREAD);
{
EXCEPTION_MARK;
this_oop->set_initialization_state_and_notify(initialization_error, THREAD);
CLEAR_PENDING_EXCEPTION; // ignore any exception thrown, class initialization error is thrown below
JvmtiExport::clear_detected_exception((JavaThread*)THREAD);
}
DTRACE_CLASSINIT_PROBE_WAIT(error, InstanceKlass::cast(this_oop()), -1,wait);
if (e->is_a(SystemDictionary::Error_klass())) {
THROW_OOP(e());
} else {
JavaCallArguments args(e);
THROW_ARG(vmSymbols::java_lang_ExceptionInInitializerError(),
vmSymbols::throwable_void_signature(),
&args);
}
}
DTRACE_CLASSINIT_PROBE_WAIT(end, InstanceKlass::cast(this_oop()), -1,wait);
}
bool link_class_impl(
instanceKlassHandle this_oop, bool throw_verifyerror, TRAPS) {
//如果类状态错误,则抛出异常
if (this_oop->is_in_error_state()) {
ResourceMark rm(THREAD);
THROW_MSG_(vmSymbols::java_lang_NoClassDefFoundError(),
this_oop->external_name(), false);
}
//如果类已链接则返回
if (this_oop->is_linked()) {
return true;
}
assert(THREAD->is_Java_thread(), "non-JavaThread in link_class_impl");
JavaThread* jt = (JavaThread*)THREAD;
instanceKlassHandle super(THREAD, this_oop->super());
if (super.not_null()) {
//如果父类是一个接口则抛出异常
if (super->is_interface()) {
ResourceMark rm(THREAD);
Exceptions::fthrow(
THREAD_AND_LOCATION,
vmSymbols::java_lang_IncompatibleClassChangeError(),
"class %s has interface %s as super class",
this_oop->external_name(),
super->external_name()
);
return false;
}
//递归,完成父类的链接
link_class_impl(super, throw_verifyerror, CHECK_false);
}
//完成当前类实现的所有接口的链接
Array* interfaces = this_oop->local_interfaces();
int num_interfaces = interfaces->length();
for (int index = 0; index < num_interfaces; index++) {
HandleMark hm(THREAD);
instanceKlassHandle ih(THREAD, interfaces->at(index));
link_class_impl(ih, throw_verifyerror, CHECK_false);
}
//某些情况下链接父类的时候会把子类链接了,此时做检查是否已链接
if (this_oop->is_linked()) {
return true;
}
{
//初始化对象锁
oop init_lock = this_oop->init_lock();
ObjectLocker ol(init_lock, THREAD, init_lock != NULL);
//已连接
if (!this_oop->is_linked()) {
//重写,重定位过的,不在进行验证
if (!this_oop->is_rewritten()) {
{
//完成字节码验证
bool verify_ok = verify_code(this_oop, throw_verifyerror, THREAD);
if (!verify_ok) {
return false;
}
}
//再次校验是否验证完成
if (this_oop->is_linked()) {
return true;
}
//重写支持更好的解释器运行性能,向常量池添加缓存cache
//并调整相应字节码的常量池索引,指向cache
this_oop->rewrite_class(CHECK_false);
} else if (this_oop()->is_shared()) {
ResourceMark rm(THREAD);
char* message_buffer;
Handle loader = this_oop()->class_loader();
Handle pd = this_oop()->protection_domain();
//依赖约束检查
bool verified = SystemDictionaryShared::check_verification_dependencies(this_oop(),
loader, pd, &message_buffer, THREAD);
if (!verified) {
THROW_MSG_(vmSymbols::java_lang_VerifyError(), message_buffer, false);
}
}
//重定位,为java方法配置解释器或者编译器入口
//完成方法链接,即方法的入参和返回值的类型的链接
this_oop->link_methods(CHECK_false);
//初始化vtable和itable
ClassLoaderData * loader_data = this_oop->class_loader_data();
if (!(this_oop()->is_shared() &&
loader_data->is_the_null_class_loader_data())) {
ResourceMark rm(THREAD);
this_oop->vtable()->initialize_vtable(true, CHECK_false);
this_oop->itable()->initialize_itable(true, CHECK_false);
}
//设置类的状态为链接完成
this_oop->set_init_state(linked);
if (JvmtiExport::should_post_class_prepare()) {
Thread *thread = THREAD;
assert(thread->is_Java_thread(), "thread->is_Java_thread()");
//发布JVMTI事件
JvmtiExport::post_class_prepare((JavaThread *) thread, this_oop());
}
}
}
return true;
}
}
类的状态转换
- 加载,类加载器加载的过程中会对类文件做格式检查
- 链接,对加载到内存的类文件的二进制数据做约束检查(验证),静态字段设置默认值(准备),将常量池中的符合解析成正确的内存地址调用(解析)
- 初始化,就是类的静态属性的初始化
此过程对象拥有以下7种状态
加载
加载过程主要体现在ClassLoader.load_classfile方法中,其它核心部分为
ClassFileParser.parseClassFile。就是把class文件的内容(有规则的,oop-klass模型)放到内存。
1.使用-XX:+TraceClassLoadingPreorder,会写出
[Loading ClassFileTest from file:/Users/moyao/IdeaProjects/simple/out/production/simple/]
2.类解析器将按虚拟机规范定义从Class文件格式读取数据,并对数据进行加工(此图出自《hotspot实战》,和openjdk8有所出入)
3.使用-XX:+TraceClassLoading,会写出
[Loaded ClassFileTest from file:/Users/moyao/IdeaProjects/simple/out/production/simple/]
4.使用-XX:+TraceClassResolution,会写出,
哪个类来自哪个文件
RESOLVE ClassFileTest java.lang.Object ClassFileTest.java:1
链接
连接过程主要体现在InstanceKlass.bool link_class_impl方法中,就把各个class串连起来,形成一个整体。
类加载得到InstanceKlass后,此时的InstanceKlass虽然有了类的字段、字段个数、类名、父类名等信息,但是还不能使用,因为有些关键信息仍然缺失。HotSpot VM的执行模式是解释器与JIT编译器混合的模式,当一个Java方法/循环被探测到是“热点”,即执行了很多次时,就可能使用JIT编译器编译它然后从解释器切换到执行后的代码再执行它。
那么,如何让方法同时具备可解释执行、可执行编译后的机器代码的能力呢?HotSpot VM的实现是在方法中放置解释器、编译器的入口地址,需要哪种模式就进入哪种入口。
第二个问题,在哪里设置这些入口呢?结合类的实现过程,在前面的类加载中没有提到,而后面的类初始化会执行代码,说明在执行代码时入口已设置,即它们是在类链接阶段设置的。类链接源码位于
InstaceKlass::link_class_impl(),源码很长,主要有5个步骤:
1)字节码验证(verify_code);
2)字节码重写(rewrite_class);
3)方法链接(link_method);
4)初始化vtable(虚表)和itable(接口表);
5)链接完成(set_init_state)。
验证
验证阶段,确保类或接口的二进制信息有效性。字节码验证可以确保字节码是结构性正确的。举个例子,if_icmpeq字节码判断两个整数是否相等并根据结果做跳转,结构性正确就是指跳转位置必须位于该方法内这一事实。又比如,一个方法返回boolean、byte、char、short、int中的任意一种类型,那么结构性正确要求该方法的返回字节码必须是ireturn而不能是freturn、lreturn等。字节码验证的代码位于classfile/verifier.cpp,它是一个对于程序安全运行很重要但是对于源码分析又不必要的部分。
使用-XX:+UnlockDiagnosticVMOptions -XX:+VerboseVerification 或者-XX:+TraceClassInitialization,会写出
Verifying class ClassFileTest with new format
Verifying method ClassFileTest.()V
StackMapTable: frame_count = 0
table = {
}
Verifying method ClassFileTest.main([Ljava/lang/String;)V
StackMapTable: frame_count = 0
table = {
}
Verifying method ClassFileTest.getA()I
StackMapTable: frame_count = 0
table = {
}
End class verification for: ClassFileTest
准备
准备阶段的任务是创建类或者接口的静态字段,并用默认值初始化这些字段(final static 声明的会在些阶段设置,其它的只会设置默认值),这个阶段不会执行任何的虚拟机指令,在初始化阶段会有显示的初始化器来初始化这些字段,所以准备阶段不做初始化。
在类创建之后的任何时间,任何时间都可以执行准备,但一定要保证在初始化阶段前完成。
默认值初始化静态字段
ClassFileParser::parseClassFile
java_lang_Class::create_mirror
解析
解析是根据运行时常量池里的符号引用来动态决定具体值的过程,其目标主要是将常量池中的4种符号引用转换成直接引用(运行时实际地址)。
- 类
- 接口
- 字段
- 类方法和接口方法
主要的解析方法
link_class_impl
rewrite_class
字节码重写
字节码重写器(Rewritter)位于interpreter/rewriter.cpp,它实现了如下功能。
finalize方法重写
本章最开始使用javap反编译了类Foo的字节码,其中包括Foo构造函数。Foo的构造函数默认调用Object构造函数,Object构造函数只有固定的三条字节码:aload0, invokespecial,return。
当某个类重写了Object.finalize()方法时,在运行时,最后一条字节码return会被重写器重写为
_return_register_finalizer。这是一条非标准的字节码,在Java虚拟机规范中没有要求,是虚拟机独有的字节码,如果虚拟机在执行时发现是非标准的_return_register_finalizer,则会额外执行很多代码(代码清单2-7):插入机器指令判断当前方法是否重写finalize,如果重写,则经过一个很长的调用链,最终调用java.lang.ref.Finalizer的register()。
代码清单2-7 重写finalize额外需要执行的代码
instanceOop InstanceKlass::register_finalizer( ... )
{
instanceHandle h_i( THREAD, i ); JavaValue result( T_VOID );
JavaCallArguments args( h_i );
/* 对应java.lang.ref.Finalizer的register方法(该类为package-private) */
methodHandle mh( THREAD, Universe::finalizer_register_method() );
JavaCalls::call( &result, mh, &args, CHECK_NULL );
return(h_i() );
}
register()会将重写了finalize()的对象放入一个链表,等待后面垃圾回收对链表每个对象执行finalize()方法。
2. switch重写
重写器还会优化switch语句的性能。它根据switch的case个数是否小于-XX:BinarySwitchThreshold[1](默认5)选择线性搜索switch或者二分搜索switch。线性搜索可以在线性时间内定位到求值后的case,二分搜索则保证在最坏情况下,在O(logN)内定位到case。switch重写使用的二分搜索算法如代码清单2-8所示:
代码清单2-8 二分搜索伪代码(Edsger W. Dijkstra, W.H.J. Feijen)
int binary_search( int key, LookupswitchPair* array, int n )
{
int i = 0, j = n;
while ( i + 1 < j )
{
int h = (i + j) >> 1;
if ( key < array[h].fast_match() )
j = h;
else
i = h;
}
return(i);
}
方法链接
方法链接是链接阶段乃至整个类可用机制中最重要的一步,它直接关系着方法能否被虚拟机执行。本节从方法在虚拟机中的表示开始,详细描述方法链接过程。
1. Method数据结构
OpenJDK 8以后的版本是用Method这个数据结构,在JVM层表示Java方法,位于oops/method.cpp,里面包含了解释器、编译器的代码入口和一些重要的用于统计方法性能的数据。“HotSpot”的中文意思是“热点”,指的是它能对字节码中的方法和循环进行Profiling性能计数,找出热点方法或循环,并对其进行不同程度的优化。这些Profiling数据就存放在MethodData和MethodCounter中。热点探测与方法编译是一个复杂又有趣的过程。
Method另一个重要的字段是_intrinsic_id。如果某方法的实现广为人知,或者某方法另有高效算法实现,对于它们,即便使用JIT编译性能也达不到最佳。为了追求极致的性能,可以将这些方法视作固有方法(Intrinsic Method)或者知名方法(Well-known Method),解放CPU指令集中所有支持的指令,由虚拟机工程师手写它们的实现。_intrinsic_id表示固有方法的id,如果该id有效,即该方法是固有方法,即便方法有对应的Java实现,虚拟机也不会走普通的解释执行或者编译Java方法,而是直接跳到该方法对应的手写的固有方法实现例程并执行。
所有固有方法都能在classfile/vmSymbols.hpp中找到,一个绝佳的例子是java.lang.Math。对于Math.sqrt(),用Java或者JNI均无法达到极致性能,这时可以将其置为固有方法,当虚拟机遇到它时只需要一条CPU指令fsqrt(代码清单2-9),用硬件级实现碾压软件级算法:
Math.sqrt固有方法实现
/* 32位:使用x87的fsqrt */
void Assembler::fsqrt()
{
emit_int8( (unsigned char) 0xD9 );
emit_int8( (unsigned char) 0xFA );
}
/* 64位:使用SSE2的sqrtsd */
void Assembler::sqrtsd( XMMRegister dst, XMMRegister src )
{
...
int encode = simd_prefix_and_encode( ... );
emit_int8( 0x51 );
emit_int8( (unsigned char) (0xC0 | encode) );
}
2. 编译器、解释器入口
Method的其他数据字段会在后面陆续提到,目前方法链接需要用到的数据只是图2-2右侧的各个入口地址,具体如下所示。
_i2i_entry:定点解释器入口。方法调用会通过它进入解释器的世界,该字段一经设置后面不再改变。通过它一定能进入解释器。
_from_interpreter_entry:解释器入口。最开始与_i2i_entry指向同一个地方,在字节码经过JIT编译成机器代码后会改变,指向i2c适配器入口。
_from_compiled_entry:编译器入口。最开始指向c2i适配器入口,在字节码经过编译后会改变地址,指向编译好的代码。
_code:代码入口。当编译器完成编译后会指向编译后的本地代码。
有了上面的知识,方法链接的源码就很容易理解了。如代码清单2-10所示,链接阶段会将i2i_entry和_from_interpreter_entry都指向解释器入口,另外还会生成c2i适配器,将_from_compiled_entry也适配到解释器:
方法链接实现
void Method::link_method( ... )
{
/* 如果是CDS(Class Data Sharing)方法 */
if ( is_shared() )
{
address entry = Interpreter::entry_for_cds_method( h_method );
if ( adapter() != NULL )
{
return;
}
} else if ( _i2i_entry != NULL )
{
return;
}
/*
* 方法链接时,该方法肯定没有被编译(因为没有设置编译器入口)if (!is_shared()) {
* 设置_i2i_entry和_from_interpreted_entry都指向解释器入口
*/
address entry = Interpreter::entry_for_method( h_method );
set_interpreter_entry( entry );
}
...
/* 设置_from_compiled_entry为c2i适配器入口 */
(void) make_adapters( h_method, CHECK );
}
各种入口的地址不会是一成不变的,当编译/解释模式切换时,入口地址也会相应切换,如从解释器切换到编译器,编译完成后会设置新的_code、_from_compiled_entry和_from_interpreter_entry入口;如果发生退优化(Deoptimization),从编译模式回退到解释模式,又会重置这些入口。关于入口设置的具体实现如代码
编译器/解释器入口的设置
void Method::set_code(...) {
MutexLockerEx pl(Patching_lock, Mutex::_no_safepoint_check_flag);
// 设置编译好的机器代码
mh->_code = code;
...
OrderAccess::storestore();
/* 设置解释器入口点为编译后的机器代码 */
mh->_from_compiled_entry = code->verified_entry_point(); OrderAccess::storestore();
if ( !mh->is_method_handle_intrinsic() )
mh->_from_interpreted_entry = mh->get_i2c_entry();
}
void Method::clear_code( bool acquire_lock /* = true */ )
{
MutexLockerEx pl( ... );
/* 清除_from_interpreted_entry,使其再次指向c2i适配器 */
if ( adapter() == NULL )
{
_from_compiled_entry = NULL;
} else {
_from_compiled_entry = adapter()->get_c2i_entry();
}
OrderAccess::storestore();
/* 将_from_interpreted_entry再次指向解释器入口 */
_from_interpreted_entry = _i2i_entry;
OrderAccess::storestore();
/* 取消指向机器代码 */
_code = NULL;
}
3. C2I/I2C适配器
在上述代码中多次提到c2i、i2c适配器,如图2-3所示。所谓c2i是指编译模式到解释模式(Compiler-to-Interpreter),i2c是指解释模式到编译模式(Interpreter-to-Compiler)。由于编译产出的本地代码可能用寄存器存放参数1,用栈存放参数2,而解释器都用栈存放参数,需要一段代码来消弭它们的不同,适配器应运而生。它是一段跳床(Trampoline)代码,以i2c为例,可以形象地认为解释器“跳入”这段代码,将解释器的参数传递到机器代码要求的地方,这种要求即调用约定(Calling Convention),然后“跳出”到机器代码继续执行。
如图2-3所示,两个适配器都是由
SharedRuntime::generate_i2c2i_adapters生成的,该函数会在里面进一步调用geni2cadapter()生成i2c适配器。由于代码较多,这里只以i2c适配器的生成为例(见代码清单2-12),对c2i适配器感兴趣的读者可自行反向分析。
i2c入口适配器生成
void SharedRuntime::gen_i2c_adapter( ... )
{
/* 将解释器栈顶放入rax */
__ movptr( rax, Address( rsp, 0 ) );
...
/* 保存当前解释器栈顶到saved_sp__ movptr(r11, rsp); */
if ( comp_args_on_stack )
{
...
}
__ andptr( rsp, -16 );
/* 将栈顶压入栈作为返回值,本地代码执行完毕后返回解释模式,即使用这个地址 */
__ push( rax );
const Register saved_sp = rax;
__ movptr( saved_sp, r11 );
/* 获取本地代码入口放入r11,这是解释执行到本地代码执行的关键步骤 */
__ movptr( r11, ... Method::from_compiled_offset() );
/* 从右向左逐个处理位于解释器的方法参数 */
for ( int i = 0; i < total_args_passed; i++ )
{
/* 如果参数类型是VOID,就不将该参数从解释器栈转移到编译后的代码执行的栈 */
if ( sig_bt[i] == T_VOID )
{
continue;
}
/* 获取解释器方法栈最右边参数偏移到ld_off */
int ld_off = ...;
/* 获取解释器方法栈最右边的前一个参数偏移到next_off */
int next_off = ld_off - Interpreter::stackElementSize;
/*
* r_1和r_2都表示32位,组合起来构成一个VMRegPair表示64位。如果是
* 64位则r_2无效,所以下面代码的r_2->is_valid()相当于判断是否为64位
*/
VMReg r_1 = regs[i].first();
VMReg r_2 = regs[i].second();
if ( !r_1->is_valid() )
{
continue;
}
/* 如果本地代码执行栈要求解释器栈参数放到栈中 */
if ( r_1->is_stack() )
{
/*
* 获取本地代码执行栈距栈顶偏移int st_off = ...;
* 用r13做中转,将解释器栈参数放入r13,再移动到本地代码执行栈
*/
if ( !r_2->is_valid() )
{
__ movl( r13, Address( saved_sp, ld_off ) );
__ movptr( Address( rsp, st_off ), r13 );
} else {
/* 这里表示32位,一个槽放不下long和double */
…
}
}
/* 如果本地代码执行栈要求解释器栈参数放到通用寄存器中 */
else if ( r_1->is_Register() )
{
Register r = r_1->as_Register();
/* 寄存器直接执行mov命令即可,不需要r13中转 */
if ( r_2->is_valid() )
{
const int offset = ...;
__ movq( r, Address( saved_sp, offset ) );
} else {
__ movl( r, Address( saved_sp, ld_off ) );
}
}else { /* 如果本地代码执行栈要求解释器栈参数放到XMM寄存器中 */
if ( !r_2->is_valid() )
{
__ movflt( r_1->as_XMMRegister(), ... );
} else {
__ movdbl( r_1->as_XMMRegister(), ... );
}
}
}
...
/* r11保存了本地代码入口,所以跳到r11执行本地代码 */
__ jmp( r11 );
}
...
// r11保存了本地代码入口,所以跳到r11执行本地代码
__ jmp(r11);
}
适配器的逻辑清晰,但是由于使用了类似汇编的代码风格,看起来比较复杂。可以这样理解适配器:想象有一个解释器方法栈存放所有参数,然后有一个本地方法执行栈和寄存器,如图2-4所示,适配器要做的就是将解释器执行栈的参数传递到本地方法执行栈和寄存器中。
4. CDS
最后,方法链接还有个细节:在设置入口前,它会区分该方法是否是CDS(Class Data Sharing,类数据共享)方法,并据此设置不同的解释器入口。
CDS是JDK5引入的特性,它把最常用的类从内存中导出形成一个归档文件,在下一次虚拟机启动可使用mmap/MapViewOfFile等函数将该文件映射到内存中直接使用而不再加载解析这些类,以此加快Java程序启动。如果有多个虚拟机运行,还可以共享该文件,减小内存消耗。
但是CDS只允许Bootstrap类加载器加载类共享文件,适用场景非常有限,所以JEP 310于Java 10引入了新的AppCDS(Application ClassData Sharing,应用类数据共享),让Application类加载器和Platform类加载器甚至自定义类加载器也能拥有CDS。
AppCDS对于快速启动、快速执行、立即关闭的应用程序有不错的效果,使用代码清单2-13的命令可以开启AppCDS:
代码清单2-13 使用AppCDS
$java -Xshare:off -XX:DumpLoadedClassList=class.lit HelloWorld
$java -Xshare:dump -XX:SharedClassListFile=class.list -XX:SharedArchiveFile=hello.jsa HelloWorld
$java -Xshare:on -XX:SharedArchiveFile=hello.jsa HelloWorld
AppCDS并不是故事的全部,它虽然可以导出更多类,但是使用比较麻烦,需要三步:
1)运行第一次,生成类列表;2)运行第二次,根据类列表从内存中导出类到归档文件;
3)附带着归档文件运行第三次。
为此,JEP 350于Java 13引入了DynamicCDS,它可以消除AppCDS的第一步,在第一次运行程序退出时将记录了本次运行加载的CDS没有涉及的类自动导出到归档文件,第二次直接附带归档文件运行即可。
初始化
初始化对类和接口来说就是执行它的初始化方法,只有在发生下列行为时,类或者接口才会被初始化:
- 执行new,getstatic,putstatic,invokestatic指令时
- 初次调用方法句柄java.lang.invoke.MethodHanlder实例,该实例的种类是- REF_getstatic,REF_putstatic,REF_invokestatic
- 调用Class类或者反射类库中的某些反射方法
- 对类的某个子类进行初始化时
- 该类被选定为Java虚拟机启动时的初始类
主要的初始化方法
InstanceKlass:: initialize_impl
类可用三部曲的最后一步是类初始化。《Java虚拟机规范》的第5章对初始化流程有非常详尽的描述,指出整个类的初始化流程有12步。
1)获取类C的初始化锁LC。
2)如果另外一个线程正在初始化C,那么释放锁LC,阻塞当前线程,直到另一个线程初始化完成。
3)如果当前线程正在初始化C,那么释放LC。
4)如果C早已初始化,不需要做什么,那么释放LC。
5)如果C处于错误的状态,初始化不可能完成,则释放LC并抛出NoClassDefFoundError。
6)否则,标示当前线程正在初始化C,释放LC。然后初始化每个final static常量字段,初始化顺序遵照代码写的顺序。
7)下一步,如果C是类而不是接口,初始化父类和父接口。
8)下一步,查看C的断言是否开启。
9)下一步,执行类或者接口的初始化方法。
10)如果初始化完成,那么获取锁LC,标示C已经完全初始化,通知所有等待的线程,然后释放LC。
11)否则,初始化一定会遇到类问题,抛出异常E。如果类E是Error或者它的子类,那么创建一个
ExceptionInitializationError对象,将E作为参数,然后用该对象替代下一步的E。如果因为OutOfMemoryError原因不能创建ExceptionInitializationError实例,则使用OutOfMemoryError实例作为下一步E的替代品。
12)获取LC,标示C为错误状态,通知所有线程,然后释放LC,以上一步的E作为本步的终止。
为了通用性和抽象性,可能《Java虚拟机规范》在语言描述方面比较学究。要想直观了解类初始化过程,可以阅读
InstanceKlass::initialize_impl()源码实现。HotSpot VM几乎是按照Java虚拟机规范要求的步骤进行的,只是看起来更简单明了。不难看出,上面步骤很多都是为了处理错误和异常情况,真正意义上的初始化其实是第9步,如代码清单2-14所示:
代码清单2-14 类初始化
void InstanceKlass::initialize_impl( TRAPS )
{
/* Step 8 (虚拟机文档的第9步对应源码第8步,因为源码省略了文档第8步的处理) */
call_class_initializer( THREAD );
}
void InstanceKlass::call_class_initializer( TRAPS )
{
/* 如果启用了编译重放则跳过初始化 */
if ( ReplayCompiles && ... )
{
return;
} /* 获取初始化方法,包装成一个methodHandle */
methodHandle h_method( THREAD, class_initializer() );
/* 调用初始化方法 */
if ( h_method() != NULL )
{
JavaCallArguments args; /* 无参数 */
JavaValue result( T_VOID );
JavaCalls::call( &result, h_method, &args, CHECK );
}
}
类初始化首先会判断是否开启了编译重放(Replay Compile)。使用“-XX:CompileCommand=option,ClassName::MethodName,DumpInline”可以将一个方法的编译信息存放到文件,这样就可以在下一次运行时使用-XX:+ReplayCompiles -XX:ReplayDataFile=file从文件读取编译数据,并创建编译任务投入编译队列,然后进入阻塞状态,在编译完成后继续执行程序。这种“第一次运行存放编译任务→第二次运行获取编译任务→第二次执行编译”的过程就是编译重放。
编译重放固定了编译顺序,而固定的编译顺序减少了虚拟机的不确定性,可用于JIT编译器性能数据分析和GC性能数据分析等场景。除此之外,虚拟机参数
-XX:ReplaySuppressInitializers=
0:不做特殊处理;
1:将所有类初始化代码视为空;
2:将所有应用程序类初始化代码视为空;
3:允许启动时运行类初始化代码,但是在重放编译时忽略它们。
处理了编译重放后,虚拟机会调用class_initializer()函数,该函数返回当前类的
静态代码块
public class ClinitTest {
private static int k;
private static Object obj = new Object();
static {
k = 12;
}
public static void main( String[] args )
{
new ClinitTest();
}
}
Java编译器会将静态字段的初始化代码也放入
虚拟机和Java沟通的两座桥梁是JNI和JavaCalls,Java层使用JNI进入JVM层,而JVM层使用JavaCalls进入Java层。JavaCalls可以在HotSpotVM中调用Java方法,main方法执行也是使用这种JavaCalls实现的。关于JavaCalls在第4章会详细讨论。
类的重定义
加载、链接、初始化是标准的类可用机制,除此之外,Java提供了一个用于特殊场景的类重定义功能,由JDK 5引入的
java.lang.instrument.Instrumentation实现。
Instrumentation可以在应用程序运行时修改或者增加类的字节码,然后替换原来的类的字节码,这种方式又称为热替换,如代码清单2-16所示:
Num类重定义
/* Num.java */
public class Num {
public int getNum()
{
return(3);
}
}
/* java -javaagent:AgentMain.jar ... */
import java.lang.instrument.Instrumentation;
public class AgentMain {
public static void premain( String args, Instrumentation inst )
{
inst.addTransformer( (loader, className, classBeingRedefined,
protectionDomain, byteCode)->{
/* 修改Num.getNum()的字节码,使它返回1 */
if ( "Num".equals( className ) )
{
byteCode[261] = 4;
}
return(byteCode);
} ); try {
inst.retransformClasses( Num.class );
} catch ( UnmodifiableClassException e ) {
e.printStackTrace();
}
}
}
在这段代码中,AgentMain首先添加了类字节码转换器,然后触发Num类的转换。这时会调用之前添加的类字节码转换器,在上面的例子中,转换器将修改Num.getNum的代码,使它返回整数1。然后每当需要加载一个类时,虚拟机会检查类是否为Num类,如果是则修改它的字节码。如果将Instrumentation与asm、cglib、Javaassist等字节码增强框架结合使用,开发者可以灵活地在运行时修改任意类的方法实现,这样无须修改源代码,也无须重编译运行就能改变方法的行为,达到近似热更新的效果。
注意,如果类字节码转换器没有修改字节码,正确的做法是返回null,如果修改了字节码,应该创建一个新的byte[]数组,将原来的byteCode复制到新数组中,然后修改新数组,而不是像代码清单2-16一样修改原有的byteCode再返回。这样直接修改byteCode可能会造成虚拟机崩溃的情况。
Instrumentation的底层实现是基于JVMTI(Java虚拟机工具接口)的RedefineClasses。虚拟机创建VM_RedefineClasses,投递给VMThread,然后等待VMThread执行
VM_RedefineClasses::redefine_single_class重定义一个类。类的重定义是一个烦琐的过程,它会移除原来类(the_class)中的所有断点,所有依赖原来类的编译后的代码都需要进行退优化,原来类的方法、常量池、内部类、虚表、接口表、调试信息、版本号、方法指纹等数据也会一并被替换为新的类定义(scratch_class)中的数据。