自己编写的Java代码,是如何在各种各样的操作系统上运行起来的?
Java文件通过javac编译成class文件,这种中间码被称为字节码,然后由jvm加载字节码,运行时解释器将字节码解释为一行行机器码来执行,在程序运行期间,即时编译器能会针对热点代码将该部分字节码编译成机器码以获得更高的执行效率。在整个运行时,解释器和即时编译器相互配合使Java程序几乎能够达到和编译型语言一样的执行速度。
jvm加载字节码的过程称为类加载,类加载流程的目的:把一份被javac编译过的class文本文件,通过加载,生成某种形式的Class数据结构进入内存,程序可以调用这个数据结构来构造出object这个过程是在运行时进行的,这也是Java动态拓展性的根基
- 这张图表现了一个类的生命周期,完整一点的话,我们可以在最开始加上javac编译阶段。而“类加载"只包括加载、连接、初始化这三个过程。
- 需要区分“类加载”与“加载”,加载只是类加载的第一个环节。
- 解析部分是灵活的,它可以在初始化环节之后再进行,实现所谓的“后期绑定”这点后面在讲到解析环节时会详细讲。其他环节的顺序不可改变。
加载
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,在加载阶段,Java虚拟机需要完成以下三件事情:
- 1)通过一个类的全限定名来获取定义此类的二进制字节流。
- 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
加载是一个读取Class文件,将其转化为某种静态数据结构存储在方法区内,并在堆中生成一个便于用户调用的java.lang.Class类型的对象的过程。
《Java虚拟机规范》对这三点要求其实并不是特别具体,留给虚拟机实现与Java应用的灵活度都是相当大的。例如“通过一个类的全限定名来获取定义此类的二进制字节流”这条规则,它并没有指明二进制字节流必须得从某个Class文件中获取,确切地说是根本没有指明要从哪里获取、如何获取。仅仅这一点空隙,Java虚拟机的使用者们就可以在加载阶段搭构建出一个相当开放广阔的舞台,例如:
- 从ZIP压缩包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
- 从网络中获取,这种场景最典型的应用就是Web Applet。
- 运行时计算生成,这种场景使用得最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass()来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
- 由其他文件生成,典型场景是JSP应用,由JSP文件生成对应的Class文件。
- 从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP Netweaver)可以选择
- 把程序安装到数据库中来完成程序代码在集群间的分发。
- 可以从加密文件中获取,这是典型的防Class文件被反编译的保护措施,通过加载时解密Class文件来保障程序运行逻辑不被窥探。
- ……
相对于类加载过程的其他阶段,非数组类型的加载阶段(准确地说,是加载阶段中获取类的二进
制字节流的动作)是开发人员可控性最强的阶段。加载阶段既可以使用Java虚拟机里内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员通过定义自己的类加载器去控制字节流的获取方式(重写一个类加载器的findClass()或loadClass()方法),实现根据自己的想法来赋予应用程序获取运行代码的动态性。
连接:验证
对文件格式的验证发生在加载阶段,如果通过才能顺利加载,顺利加载后,此时方法区内虽然已经存在了该class的静态结构,堆中也存在了该class类型的对象但是这并不代表着JVM已经完全认可了这个类,如果程序想要使用这个类那么就必须进行连接,而连接的第1步就是进一步对这个类进行验证。我们来看看到底对方法区内的class静态结构进行了哪些方面的验证?
第一点是元数据验证,第二点是字节码验证。简单概括来说就是对class静态结构进行语法和语义上的分析保证其不会产生危害虚拟机的行为。如果这两个步骤验证通过,那么虚拟机会姑且认为该class是安全的,但是这并不意味着验证已经完全结束了还有一道对符号引用进行验证的步骤,是在解析阶段内发生的。而解析阶段我们之前也提到过,它可以在初始化阶段之前或者之后进行。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。符号引用验证的主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机将会抛出一个java.lang.IncompatibleClassChangeError的子类异常。
连接:准备
为该类型中定义的静态变量赋零值
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初
始值的阶段,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了
首先是这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值
如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,类变量value的定义为:public static final int value = 123;编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据Con-stantValue的设置将value赋值为123。
连接:解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
那什么是符号引用和直接引用呢?
当一个Java类被编译成class文件之后,比如一个类A,它引用了B这个类,在编译阶段,A这个类是不知道B有没有被编译的,而且此时B也一定没有被加载,所以A不知道B的实际地址,这个时候在A的class文件中将会用一个字符串来代表B的地址。这个字符串就被称为符号引用。在运行的时候,如果触发了A的类加载,到解析阶段发现B没有加载,这个时候,会触发B的类加载,此时A中的字符串会被B的实际地址所代替,这就叫直接引用。
解析又分为静态解析和动态解析,因为Java有多态机制,如果上面提到的B是一个实体类,那么这样的解析称为静态解析,如果B是一个接口或者是抽象类的时候,这个时候就没有办法确定这个引用的实际地址,既然没有那就先留着。等到运行阶段发生了调用,这个时候虚拟机中的调用栈将会得到具体的类型信息,这个时候再进行解析就可以得到明确的直接引用。这个过程就叫做动态解析。这也是为什么解析阶段会发生在初始化阶段之后,实现后期绑定。
当解析步骤完成意味着整个连接部分的完成,这也就是说外部加载的Java类,已经成功的引入到了你的程序中。
初始化
初始化阶段就很简单了,先判断代码中是否存在主动资源初始化操作,如果有的话,那么执行。主动资源初始化动作是指class层面的一些静态代码块,成员变量的赋值操作。 肯定是不包括构造函数的,构造函数是对象层面的,这个class是类层面的。只有显示的调用new指令才会调用构造函数进行对象的实例化,这是对象层面。
回顾
从JVM的角度来看,加载阶段的读取二进制流这个动作,以及初始化阶段这两个部分开放了主导权给用户用户可以自由控制,而剩下的所有部分都是由虚拟机全权包揽,由其内部来完成。