鸿 网 互 联 www.68idc.cn

Java 虚拟机类加载机制和字节码执行引擎

来源:互联网 作者:佚名 时间:2015-08-17 11:53
我们知道Java代码编译后生成的是字节码,那虚拟机是如何加载这些class字节码文件的呢?加载之后又是如何进行方法调用的呢?Java有

引言

我们知道Java代码编译后生成的是字节码,那虚拟机是如何加载这些class字节码文件的呢?加载之后又是如何进行方法调用的呢?

一 类文件结构 无关性基石

Java有一个口号叫做一次编写,到处运行。实现这个口号的就是可以运行在不同平台上的虚拟机和与平台无关的字节码。这里要注意的是,虚拟机也是中立的,只要是符合规范的字节码,都可以被虚拟机接受,例如Groovy,JRuby等语言,都会生成符合规范的字节码,然后被虚拟机所运行,虚拟机不关心字节码由哪种语言生成。

类文件结构

class类文件是一组以8位字节为基础的二进制流,它包含以下几个部分:

魔数和class文件版本:类文件开头的四个字节被定义为CAFEBABE,只有开头为CAFEBABE的文件才可以被虚拟机接受,接下来四个字节为class文件的版本号,高版本JDK可以兼容以前版本的class文件,但不能运行以后版本的class文件。
常量池:可以理解为class文件中的资源仓库,它包含两大类常量:字面量和符号引用,字面量包含文本字符串,声明为final的常量值等,符号引用包含类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
访问标志:常量池结束后,紧接着两个字节表示访问标志,用于识别一些类或接口层次的访问信息,例如是否是public,是否是static等。
类索引,父类索引,和接口索引集合:类索引用来确定这个类的全限定名,父类为父类的全限定名,接口索引集合为接口的全限定名。
字段表集合:用于描述接口或者类中声明的变量,但不包含方法中的变量。
方法表集合:用于表述接口或者类中的方法。
属性表集合:class文件,字段表,方法表中的属性都源自这里。

二 类加载机制 虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换分析和初始化,最终形成可以被虚拟节直接使用的JAVA类型,这就是虚拟机的类加载机制。

类从被加载到虚拟机内存到卸载出内存的生命周期包括:加载->连接(验证->准备->解析)->初始化->使用->卸载。

初始化的5种情况: 类加载过程

加载
加载是类加载的第一个阶段,虚拟机要完成以下三个过程:

验证
目的是确保class文件字节流信息符合虚拟机的要求。

准备
为static修饰的变量赋初值,例如int型默认为0,boolean默认为false。

解析
虚拟机将常量池内的符号引用替换成直接引用。

初始化
初始化是类加载的最后一个阶段,将执行类构造器< init>()方法,注意这里的方法不是构造方法。该方法将会显式调用父类构造器,接下来按照java语句顺序为类变量和静态语句块赋值。

类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性。举一个例子:

package com.sinaapp.gavinzhang.bean; import java.io.InputStream; { { ClassLoader myClassLoader = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try{ String fileName = name.substring(name.lastIndexOf(".")+1)+".class"; InputStream is = getClass().getResourceAsStream(fileName); if(is == null) { System.out.println(fileName+ "is not find"); return super.loadClass(name); } System.out.println("fileName: "+fileName); byte[] b = new byte[is.available()]; is.read(b); return defineClass(name,b,0,b.length); }catch (Exception E) { throw new ClassCastException(name); } } }; try { Object obj = myClassLoader.loadClass("com.sinaapp.gavinzhang.bean.Resource").newInstance(); Object obj1 = Class.forName("com.sinaapp.gavinzhang.bean.Resource").newInstance(); System.out.println(obj instanceof com.sinaapp.gavinzhang.wesound.bean.Resource); System.out.println(obj1 instanceof com.sinaapp.gavinzhang.wesound.bean.Resource); }catch (Exception e) { e.printStackTrace(); } } }

结果为:

101847_3jDH_1983603.png

可以看到,由自定义的加载类只能获取同包下的class,而系统的class不能被加载,而且由Class.forName()获取的类与自定义加载类得到的类不是同一个类。

根据五种初始化的条件,父类也会被初始化,但是,上边的代码运行结果显示,父类和接口都没有被初始化,这又是怎么回事呢?

系统提供了三种类加载器,分别是:

我们自定义的ClassLoader继承自应用程序类加载器,当自定义类加载器找不到所加在的类时,会使用启动类加载器进行加载,当启动类加载器加载不到时,由扩展类加载,扩展类加载不到时有应用程序类加载。这也是为什么上边的代码能够成功运行的原因。

三 字节码执行引擎 运行时栈帧结构

 中讲到虚拟机栈是线程私有的,线程中会为运行的方法创建栈帧。

105250_tJWk_1983603.png

栈帧是虚拟机栈的栈元素,栈帧存储了局部变量表,操作数栈,动态连接,返回地址等信息。每一个方法的调用都对应着一个栈帧在虚拟机栈中的入栈和出栈。

  • 局部变量表由方法参数,方法内定义的局部变量组成,,容量以变量槽(Slot)为最小单位。如果该方法不是static方法,则局部变量表的第一个索引为该对象的引用,用this可以取到。

  • 操作数栈最开始为空,由字节码指令往栈中存数据和取数据,方法的返回值也会存到上一个方法的操作数栈中。

  • 动态连接含有一个指向常量池中该栈帧所属方法的引用,持有该引用是为了进行动态分派。

  • 方法返回地址存放的是调用该方法的pc计数器值,当方法正常返回时,就会把返回值传递到上层方法调用者。当方法中发生没有可被捕获的异常,也会返回,但是不会向上层传递返回值。

  • 方法调用

    Java是一门面向对象的语言,它具有多态性。那么虚拟机又是如何知道运行时该调用哪一个方法?

    静态分派是在编译期就决定了该调用哪一个方法而不是由虚拟机来确定,方法重载就是典型的静态分派。
    动态分派是在虚拟机运行阶段才能决定调用哪一个方法,方法重写就是典型的动态分派。

    网友评论
    <