文章已同步至GitHub开源项目: JVM底层解析
代码编译的结果从本地机器指令码转化为字节码,是存储格式发展的一小步,但却是编程语言发展的一大步 —— 《深入理解JVM虚拟机》周志明·著
Java虚拟机将描述类的数据从class字节码文件加载到内存,并且对数据进行校验,转化,解析,初始化的工作,最终形成在内存中可以直接使用的数据类型。这个过程叫做虚拟机的类加载机制。
关于类加载的时机,《Java虚拟机规范》中并没有明确规定。这点可以由虚拟机的具体实现决定。
但是类的初始化阶段,规范中明确规定当某个类没有进行初始化,只有以下6中情况才会触发其初始化过程。
new
,getStatic
,putStatic
,invokeStatic
,这四条字节码指令的时候,如果改类型没有进行初始化,则会触发其初始化。也就是如下情况 new
关键字进行创建对象的时候。java.lang.invoke.MethidHandle
实例最后的解析结果为REF_getStatic
,REF_putStatic
,REF_invokeStatic
,REF_newInvokeSpecial
四种类型的方法句柄,并且这个句柄对应的类没有被初始化。对于以上6中触发类的初始化条件,在JVM规范中有一个很强制的词,if and only if
(有且只有)。这六种行为被称为对类进行主动引用
,除此之外,其他引用类的方式均不会触发类的初始化。
类加载的过程主要分为三个阶段 加载,链接,初始化。 而链接阶段又可以细分为验证,准备,解析三个子阶段。
接下来,我们详细分析下类加载的过程。
加载过程需要完成以下三个事情:
全限定名
获取定义此类的二进制字节流
;
静态存储结构
转化为方法区的运行时数据结构
;
数据的访问入口
《Java虚拟机规范 》对这三点的要求并不是特别的具体。因此,留给虚拟机实现于Java的应用的灵活度都是很大的。
在第一步通过一个类的全限定名
获取字节流的时候,并没有规范一定是从字节码文件获取,更没有规定是从本地文件中获取。因此,虚拟机的实现者就可以在加载阶段就构建出一个相当开放的舞台。
加载结束之后,外部的二进制字节流就会以JVM所设定的格式存在于方法区中了。之后会在堆中实例一个java.lang.class类型的对象,这个对象作为程序访问方法区中的类型数据的入口。
文件格式验证
CAFEBABE
开头文件格式验证不止以上,上面所列举的只是从HotSpot虚拟机源码中摘抄的一部分。只有通过这个阶段的验证之后,这一段字节流才会进入虚拟机内存中进行存储,之后的过程都是基于方法区中的存储结构进行的。不会直接读取字节流了。
源数据验证
用于保证字节码中的代码符合《Java语言规范》
字节码验证
此过程保证代码是符合逻辑的,对代码的流程进行判断,保证不会出现危害虚拟机安全的情况。
如果一个类型中的方法体没有通过次阶段,那它一定是有问题的。但是,不可以认为只要通过此阶段验证,一定没有问题。通过程序去校验程序的逻辑是无法做到绝对准确的。
符号引用验证
。
此阶段验证符号引用是否合法,主要用于解析阶段的前置任务。
主要用于判断 该类中是否存在缺少后者被禁止访问它依赖的某些外部类,字段,方法等资源。
CONSTANT_Class_info
/CONSTANT_Fieldref_info
、CONSTANT_Methodref_info
等。
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有三个,如下所示:
Java虚拟机对class文件采用的是按需加载的方式,
也就是说当需要使用该类时才会将它的class文件加载到内存生成的class对象。
而且加载某个类的class文件时,java虚拟机采用的是双亲委派模式。
即把请求交由父类处理,它是一种任务委派模式
通过查看最顶层父类ClassLoader的loaderClass方法,我们可以验证双亲委派机制。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查此类是否被加载过了
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) {
// 抛出异常说明父类无法加载
}
if (c == null) {
//父类无法加载的时候,由子类进行加载。
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
//记录加载时间已经加载耗时
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_HOME/lib
中的类,而不是加载用户自定义的类。此时,程序可以正常编译,但是自己定义的类无法被加载运行。
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载, 而引导类加载器在加载过程中会先加载jdk自带的文件(rt.jar包中的java\lang\String.class), 报错信息说没有main方法就是因为加载的是rt.jar包中的String类。 这样可以保证对java核心源代码的保护,这就是沙箱安全机制.
JVM必须知道一个类型是有启动类加载器加载的还是由用户类加载器加载的。如果一个类型由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的会议部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证两个类型的加载器是相同的。
文章已同步至GitHub开源项目: JVM底层解析