APP下载
报价宝  ›  科技  › 

大众点评资深架构师分享—Java虚拟机器器类档案结构+字节码执行引擎

报价宝 来源:baojiabao.com 发布时间:2019-10-13 09:56:00 11月15日更新
报价宝综合消息大众点评资深架构师分享—Java虚拟机器器类档案结构+字节码执行引擎

导语

我们知道java程式码编译后生成的是字节码,那虚拟机器是如何载入这些class字节码档案的呢?载入之后又是如何进行方法呼叫的呢?

一 类档案结构

无关性基石

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

类档案结构

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

魔数和class档案版本:类档案开头的四个字节被定义为CAFEBABE,只有开头为CAFEBABE的档案才可以被虚拟机器接受,接下来四个字节为class档案的版本号,高版本JDK可以相容以前版本的class档案,但不能执行以后版本的class档案。

常量池:可以理解为class档案中的资源仓库,它包含两大类常量:字面量和符号引用,字面量包含文字字串,宣告为final的常量值等,符号引用包含类和界面的全限定名,字段的名称和描述符,方法的名称和描述符。

访问标志:常量池结束后,紧接着两个字节表示访问标志,用于识别一些类或界面层次的访问资讯,例如是否是public,是否是static等。

类索引,父类索引,和界面索引集合:类索引用来确定这个类的全限定名,父类为父类的全限定名,界面索引集合为界面的全限定名。

字段表集合:用于描述界面或者类中宣告的变数,但不包含方法中的变数。

方法表集合:用于表述界面或者类中的方法。

属性表集合:class档案,字段表,方法表中的属性都源自这里。

二 类载入机制

虚拟机器把描述类的资料从class档案载入到内存,并对资料进行校验,转换分析和初始化,最终形成可以被虚拟节直接使用的JAVA型别,这就是虚拟机器的类载入机制。

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

初始化的5种情况:

使用new关键字例项化物件时,读取或设定一个类的静态字段,除被final修饰经编译结果放在常量池的静态字段,呼叫类的静态方法时。使用java.lang.reflect包方法对类进行反射呼叫时。(Class.forName())。初始化子类时,如果父类没有初始化。虚拟机器启动时main方法所在的类。当使用JDK1.7动态语言支援时,java.lang.invoke.MethodHandle例项解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic的方法控制代码,且对应类没有进行初始化。类载入过程

载入

载入是类载入的第一个阶段,虚拟机器要完成以下三个过程:1)通过类的全限定名获取定义此类的二进位制字节流。2)将字节流的储存结构转化为方法区的执行时结构。3)在内存中生成一个代表该类的Class物件,作为方法区各种资料的访问入口。

验证

目的是确保class档案字节流资讯符合虚拟机器的要求。

准备

为static修饰的变数赋初值,例如int型预设为0,boolean预设为false。

解析

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

初始化

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

类载入器

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

package com.sinaapp.gavinzhang.bean;

import java.io.InputStream;

public class App

{

public static void main( String[] args )

{

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();

}

}

}

结果为:

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

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

系统提供了三种类载入器,分别是:启动类载入器(Bootstrap ClassLoader),该载入器会将/lib目录下能被虚拟机器识别的类载入到内存中。扩充套件类载入器(Extension ClassLoader),该载入器会将/lib/ext目录下的类库载入到内存。应用程序类载入器(Application ClassLoader),该载入器负责载入使用者路径上所指定的类库。

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

三 字节码执行引擎

执行时栈帧结构

http://my.oschina.net/jiangmitiao/blog/470426 中讲到虚拟机器栈是执行绪私有的,执行绪中会为执行的方法建立栈帧。

栈帧是虚拟机器栈的栈元素,栈帧储存了局部变量表,算子栈,动态连线,返回地址等资讯。每一个方法的呼叫都对应着一个栈帧在虚拟机器栈中的入栈和出栈。

区域性变量表由方法引数,方法内定义的区域性变数组成,容量以变数槽(Slot)为最小单位。如果该方法不是static方法,则区域性变量表的第一个索引为该物件的引用,用this可以取到。

算子栈最开始为空,由字节码指令往栈中存资料和取资料,方法的返回值也会存到上一个方法的算子栈中。

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

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

方法呼叫

java是一门面向物件的语言,它具有多型性。那么虚拟机器又是如何知道执行时该呼叫哪一个方法?

静态分派是在编译期就决定了该呼叫哪一个方法而不是由虚拟机器来确定,方法过载就是典型的静态分派。

动态分派是在虚拟机器执行阶段才能决定呼叫哪一个方法,方法重写就是典型的动态分派。

动态分派的实现:当呼叫一个物件的方法时,会将该物件的引用压栈到算子栈,然后字节码指令invokevirtual会去寻找该引用实际型别。如果在实际型别中找对应的方法,且访问许可权足够,则直接返回该方法引用,否则会依照继承关系对父类进行查询。实际上,如果子类没有重写父类方法,则子类方法的引用会直接指向父类方法。

基于栈的字节码执行引擎

不管是解释型语言还是编译型语言,机器都无法理解非二进位制语言。高阶语言转化成机器语言都遵循现代经典编译原理。即执行前对程式源代码进行词法和语法分析,构建抽象语法树。C语言等编译型语言会由单独的执行引擎做这些工作,而Java语言等解释型语言语法抽象树由jvm完成。jvm可以选择通过直译器来解释字节码执行还是通过优化器生成机器程式码来执行。

常用的两套指令集架构分别是基于栈的指令集和基于暂存器的指令集。

基于栈的指令集更多的通过入栈出栈来实现计算功能,例如1+1

iconst_1 ;将1入栈

iconst_1 ;将1入栈

iadd ;将栈顶两个元素取出相加并将结果入栈

基于暂存器的指令集更多的是使用暂存器来进行操作,例如1+1

mov eax,1 ;向eax中存1

add eax,1 ;eax总体来说,基于栈的指令集会慢一些,但是它与暂存器无关,更容易实现到处执行的目标。

总结

又到了该总结的时候了,类载入机制面试中很容易被问到,不幸的是,当时我并没有看这方面的知识。

class类档案结构的每一个部分都可以再深入下去,类档案结构是采用结构体的方式储存的,那么怎么知道集合的长度,各个属性又是怎么被标记的。

类载入机制中有且仅有的五种触发初始化的情况。类载入器的分类。

栈帧的结构,以及方法呼叫。

java语言的方法呼叫分为静态多分派,动态单分派。

在这里小编整理了一些视讯资料和书籍资料,免费分享给大家,希望能给大家带来帮助

关注作者+转发,私信回复关键词(Java)即可免费获取这一套资料

文章标签: 报价宝 降噪耳机价格 耳机价格 红米手机价格 华为手机价格 小米手机价格 电视机价格 笔记本电脑价格 笔记本价格 汽车价格 汽车价格 笔记本电脑价格 电视机价格 华为手机价格 降噪耳机价格