Java刚诞生的宣传口号:一次编写,处处运行(Write Once, Run Anywhere),其中字节码是构成平台无关的基石,也是语言无关性的基础。html
Java虚拟机不和包括Java在内的任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联,这使得任何语言的均可以使用特定的编译器将其源码编译成Class文件,从而在虚拟机上运行。java
任何一个Class文件都对应着惟一一个类或接口的定义信息,但反过来讲,Class文件实际上它并不必定以磁盘文件的形式存在。mysql
Class文件是一组以8位字节为基础单位的二进制流。c++
各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎所有是程序运行的必要数据,没有空隙存在。
Class文件格式采用一种相似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。git
无符号数属于基本的数据类型,以u一、u二、u四、u8来分别表明1个字节、2个字节、4个字节和8个字节的无符号数,无符号数能够用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。github
表是由多个无符号数或者其余表做为数据项构成的复合数据类型,全部表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。web
整个class类的文件结构以下表所示:面试
占用大小 | 字段描述 | 数量 |
---|---|---|
占用大小 | 字段描述 | 数量 |
u4 | magic:魔数,用于标识文件类型,对于java来讲是0xCAFEBABE | 1 |
u2 | minor_version:次版本号 | 1 |
u2 | major_version:主版本号 | 1 |
u2 | constant_pool_count:常量池大小,从1开始而不是0。当这个值为0时,表示后面没有常量 | 1 |
cp_info | constant_pool:#常量池 | constant_pool_count-1 |
u2 | access_flags:访问标志,标识这个class是类仍是接口、public、abstract、final等 | 1 |
u2 | this_class:类索引 #类索引查找全限定名的过程 | 1 |
u2 | super_class:父类索引 | 1 |
u2 | interfaces_count:接口计数器 | 1 |
u2 | interfaces:接口索引集合 | interfaces_count |
u2 | fields_count:字段的数量 | 1 |
field_info | fields:#字段表 | fields_count |
u2 | methods_count:方法数量 | 1 |
method_info | methods:#方法表 | methods_count |
u2 | attributes_count:属性数量 | 1 |
attribute_info | attrbutes:#属性表 | attributes_count |
可使用javap -verbose输出class文件的字节码内容。sql
下面按顺序对这些字段进行介绍。数据库
每一个Class文件的头4个字节称为魔数(Magic Number),它的惟一做用是肯定这个文件是否为一个能被虚拟机接受的Class文件。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,由于文件扩展名能够随意地改动。文件格式的制定者能够自由地选择魔数值,只要这个魔数值尚未被普遍采用过同时又不会引发混淆便可。
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(MinorVersion),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1以后的每一个JDK大版本发布主版本号向上加1高版本的JDK能向下兼容之前版本的Class文件,但不能运行之后版本的Class文件,即便文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
常量池中常量的数量是不固定的,因此在常量池的入口须要放置一项u2类型的数据,表明常量池容量计数值(constant_pool_count)。与Java中语言习惯不同的是,这个容量计数是从1而不是0开始的。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
用于识别一些类或者接口层次的访问信息,包括:
这三项数据来肯定这个类的继承关系。
描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量。
而字段叫什么名字、字段被定义为何数据类型,这些都是没法固定的,只能引用常量池中的常量来描述。
字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出本来Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
描述了方法的定义,可是方法里的Java代码,通过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为“Code”的属性里面。
与字段表集合相相似的,若是父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但一样的,有可能会出现由编译器自动添加的方法,最典型的即是类构造器“<clinit>”方法和实例构造器“<init>”
存储Class文件、字段表、方法表都本身的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在Code属性表中。
Java虚拟机的指令由一个字节长度的、表明着某种特定操做含义的数字(称为操做码,Opcode)以及跟随其后的零至多个表明此操做所需参数(称为操做数,Operands)而构成。
因为限制了Java虚拟机操做码的长度为一个字节(即0~255),这意味着指令集的操做码总数不可能超过256条。
大多数的指令都包含了其操做所对应的数据类型信息。例如:
iload指令用于从局部变量表中加载int型的数据到操做数栈中,而fload指令加载的则是float类型的数据。
大部分的指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型。不是每种数据类型和每一种操做都有对应的指令,有一些单独的指令能够在必要的时候用在将一些不支持的类型转换为可被支持的类型。大多数对于boolean、byte、short和char类型数据的操做,实际上都是使用相应的int类型做为运算类型。
加载和存储指令用于将数据在帧栈中的局部变量表和操做数栈之间来回传递。
上面带尖括号的指令其实是表明的一组指令,如iload_0、iload_一、iload_2和iload_3。这些指令把操做数隐含在名称内,不须要进行取操做数的动做。
运算或算术指令用于对两个操做数栈上的值进行某种特定运算,并把结果从新存入到操做栈顶,可分为整型数据和浮点型数据指令。byte、short、char和boolean类型的算术指令使用int类型的指令代替。
能够将两种不一样的数值类型进行相互转换,
控制转移指令可让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,能够认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。控制转移指令以下。
是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。
在java程序中,显式抛出异常的操做都由athrow指令来实现。而在java虚拟机中,处理异常不是由字节码指令来实现的,而是采用异常表来完成的
java虚拟机能够支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。方法级的同步是隐式的,利用方法表结构中的ACC_SYNCHRONIZED访问标志得知。指令序列的同步是由monitorenter和monitorexit两条指令支持。
这是一个很是典型的面试题,标准回答以下:
通常来讲,咱们把 Java 的类加载过程分为三个主要步骤:加载、连接、初始化。
1. 加载(Loading)
此阶段中Java 将字节码数据从不一样的数据源读取到 JVM 中,并映射为 JVM 承认的数据结构(Class 对象),这里的数据源多是各类各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;若是输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。 加载阶段是用户参与的阶段,咱们能够自定义类加载器,去实现本身的类加载过程。
2. 连接(Linking)
这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程当中。这里可进一步细分为三个步骤:
3. 初始化(initialization)
这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动做,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
双亲委派模型:
简单说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,不然尽可能将这个任务代理给当前加载器的父加载器去作。使用委派模型的目的是避免重复加载 Java 类型。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verificatio)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为链接(Linking)。
于初始化阶段,虚拟机规范则是严格规定了有且只有5种状况必须当即对类进行“初始化”(而加载、验证、准备天然须要在此以前开始):
关于静态变量的初始化,必需要注意如下三种状况下是不会触发类的初始化的:
下面是测试程序:
public class SuperClass { static { System.out.println("SuperClass init!"); } public static int value = 123; } public class SubClass extends SuperClass { static { System.out.println("Subclass init!"); } } public class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLOWORLD_STRING = "hello world"; }
如下是对三种状况的测试程序:
public class NotInitialization { public static void main(String[] args) { // 1. 只有直接定义这个字段的类才会被初始化,所以经过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。 // Result: SuperClass init! 123 System.out.println(SubClass.value); // 2. 经过数组定义来引用类,不会触发此类的初始化 SuperClass[] superClasses = new SubClass[10]; // 3. 常量在编译阶段会存入调用类的常量池中,本质上并无直接引用到定义常量的类,所以不会触发定义常量的类的初始化 // Result: hello world System.out.println(ConstClass.HELLOWORLD_STRING); } }
在加载阶段,虚拟机须要完成下列3件事:
验证是链接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。验证阶段大体上会完成下面4个阶段的检验动做:
是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
这个阶段中有两个容易产生混淆的概念须要强调一下,首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一块儿分配在Java堆中。
其次,这里所说的初始值“一般状况”下是数据类型的零值,假设一个类变量的定义为:
public static int value=123;
那变量value在准备阶段事后的初始值为0而不是123,由于这时候还没有开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,因此把value赋值为123的动做将在初始化阶段才会执行。
表7-1列出了Java中全部基本数据类型的零值:
假设上面类变量value的定义变为:public static final int value=123;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用是能无歧义地定位到目标便可。符号引用与虚拟机实现的内存布局无关,引用的目标不必定已经加载到内存中。各类虚拟机实现的内存布局能够各不相同,可是它们能接受地符号引用必须是一致的,由于符号引用地字面量形式明肯定义在java虚拟机规范地Class文件格式中。
直接引用(Direct References):直接引用能够是直接指向目标的指针、相对偏移量或是一个能直接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不一样虚拟机实例上翻译出来的直接引用通常不会相同。若是有了直接引用,那引用的目标一定已经在内存中存在。
类初始化是类加载过程的最后一步,在这个阶段才真正开始执行类中的字节码。初始化阶段是执行类构造器<clinit>()
方法的过程。
<clinit>()
方法与类的构造函数(<init>()
方法)不一样,它不须要显式调用父类构造器,虚拟机会保证在子类的<clinit>()
方法执行以前,父类的<clinit>()
方法已经执行完毕。<clinit>()
方法先执行,所以父类中定义的静态语句块要先于子类执行。<clinit>()
方法对于类或接口来讲不是必需的,若是一个类中没有静态语句块,也没有对变量赋值操做,那么编译器能够不为这个类生成<clinit>()
方法。<clinit>()
方法,但与类不一样的是,执行接口的<clinit>()
方法不须要先执行父接口的<clinit>()
方法,只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也同样不会执行接口的<clinit>()
方法。类加载器虽然只用于实现类的加载动做,但在java程序中起到的做用却远不止类加载阶段。
对于任意一个类,都须要由加载它的类加载器和这个类自己一同确立其在java虚拟机中的惟一性,每一个类加载器,都拥有一个独立的类命名空间。当一个Class文件被不一样的类加载器加载时,加载生成的两个类一定不相等(equals()、isAssignableFrom()、isInstance()、instanceof关键字的结果为false)。
从java虚拟机的角度来看,只存在两种不一样的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用c++实现,是虚拟机的一部分;另外一种是全部其余的类加载器,这些类加载器都由java实现,独立于虚拟机外部,而且所有继承自抽象类java.lang.ClassLoader。java提供的类加载器主要分如下三种:
双亲委派模型的工做过程是:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的类加载器都是如此,所以全部的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈本身没法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试本身去加载。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一块儿具有了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,不管哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,所以Object类在程序的各类类加载器环境中都是同一个类。相反,若是没有使用双亲委派模型,由各个类加载器自行去加载的话,若是用户本身编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不一样的Object类,Java类型体系中最基础的行为也就没法保证,应用程序也将会变得一片混乱。
首先看一下实现双亲委派模型的代码,逻辑就是先检查类是否已经被加载,若是没有则调用父加载器的loadClass()方法,若是父加载器为空则默认使用启动类加载器做为父加载器。若是父类加载失败,抛出ClassNotFoundException异常后,再调用本身的findClass()方法进行加载。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 先从缓存查找该class对象,找到就不用从新加载 Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { //若是找不到,则委托给父类加载器去加载 c = parent.loadClass(name, false); } else { //若是没有父类,则委托给启动加载器去加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // 若是都没有找到,则经过自定义实现的findClass去查找并加载 c = findClass(name); } } if (resolve) {//是否须要在加载时进行解析 resolveClass(c); } return c; } }
在实现本身的类加载器时,一般有两种作法,一种是重写loadClass方法,另外一种是重写findClass方法。其实这两种方法本质上差很少,毕竟loadClass也会调用findClass,可是最好不要直接修改loadClass的内部逻辑,以避免破坏双亲委派的逻辑。推荐的作法是只在findClass里重写自定义类的加载方法。
下面例子实现了文件系统类加载器,
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } }
Class.forName是Class类的方法public static Class<?> forName(String className) throws ClassNotFoundException
ClassLoader.loadClass是ClassLoader类的方法public Class<?> loadClass(String name) throws ClassNotFoundException
Class.forName和ClassLoader.loadClass均可以用来进行类型加载,而在Java进行类型加载的时刻,通常会有多个ClassLoader可使用,并可使用多种方式进行类型加载。
class A { public void m() { A.class.getClassLoader().loadClass(“B”); } }
在A.class.getClassLoader().loadClass(“B”)
;代码执行B的加载过程时,通常会有三个概念上的ClassLoader提供使用。
SCL和TCCL能够理解为在代码中使用ClassLoader的引用进行类加载,而CCL却没法获取到其引用,虽然在代码中CCL == A.class.getClassLoader() == SCL。CCL的加载过程是由JVM运行时来控制的,是没法经过Java编程来更改的。
为何须要破坏双亲委派?
由于在某些状况下父类加载器须要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器没法加载到须要的文件,以Driver接口为例,因为Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,好比mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,而后进行管理,可是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就须要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个状况。
Tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个web应用本身的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委托。
如何破坏?
Tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个web应用本身的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委托。
Tomcat是个web容器, 那么它要解决什么问题:
Tomcat 若是使用默认的类加载机制行不行 ?
答案是不行的。为何?
第一个问题,若是使用默认的类加载器机制,那么是没法加载两个相同类库的不一样版本的,默认的累加器是无论你是什么版本的,只在意你的全限定类名,而且只有一份。
第二个问题,默认的类加载器是可以实现的,由于他的职责就是保证惟一性。
第三个问题和第一个问题同样。
第四个问题,咱们要怎么实现jsp文件的热修改(楼主起的名字),jsp 文件其实也就是class文件,那么若是修改了,但类名仍是同样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会从新加载的。那么怎么办呢?咱们能够直接卸载掉这jsp文件的类加载器,因此你应该想到了,每一个jsp文件对应一个惟一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。从新建立类加载器,从新加载jsp文件。
Tomcat 如何实现本身独特的类加载机制?
前面3个类加载和默认的一致,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat本身定义的类加载器,它们分别加载/common/*
、/server/*
、/shared/*
(在tomcat 6以后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*
中的Java类库。其中WebApp类加载器和Jsp类加载器一般会存在多个实例,每个Web应用程序对应一个WebApp类加载器,每个JSP文件对应一个Jsp类加载器。
从图中的委派关系中能够看出:
CommonClassLoader能加载的类均可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader本身能加载的类则与对方相互隔离。
WebAppClassLoader可使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并经过再创建一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
下图展现了Tomcat的类加载流程:
当tomcat启动时,会建立几种类加载器:
1. Bootstrap 引导类加载器
加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下)
2. System 系统类加载器
加载tomcat启动的类,好比bootstrap.jar,一般在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下。
3. Common 通用类加载器
加载tomcat使用以及应用通用的一些类,位于CATALINA_HOME/lib下,好比servlet-api.jar
4. webapp 应用类加载器
每一个应用在部署后,都会建立一个惟一的类加载器。该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。
典型面试题
tomcat 违背了java 推荐的双亲委派模型了吗?
违背了,双亲委派模型要求除了顶层的启动类加载器以外,其他的类加载器都应当由本身的父类加载器加载。tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵照这个约定,每一个webappClassLoader加载本身的目录下的class文件,不会传递给父类加载器。
若是tomcat 的 Common ClassLoader 想加载 WebApp ClassLoader 中的类,该怎么办?
可使用线程上下文类加载器实现,使用线程上下文加载器,可让父类加载器请求子类加载器去完成类加载的动做。
参考:
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。典型栈帧结构:
下面对各个部分进行仔细介绍:
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,虚拟机规范中并无明确指定一个Slot应占用的内存空间大小,只是规定每一个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这样能够屏蔽32位跟64位虚拟机在内存空间上的差别。
虚拟机经过索引定位的方式使用局部变量表,索引值的范围从0到最大Slot数量,索引n对应第n个Slot。局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,即this。
为了尽量的节省栈帧空间,局部变量表中的Slot是能够重用的,同时这也影响了垃圾收集行为。即对已使用完毕的变量,局部变量表仍持有该对象的引用,致使对象没法被GC回收,占用大量内存。这也是“不使用的对象应手动赋值为null”这条推荐编码规则的缘由。不过从执行角度使用赋null值的操做来优化内存回收是创建在对字节码执行引擎概念模型的理解之上,代码在通过编译器优化后才是虚拟机真正须要执行的代码,这时赋null值会被消除掉,所以更优雅的解决办法是以恰当的变量做用域来控制变量回收时间。
操做数栈(Operand Stack)也常称操做栈,它是一个后入先出(Last In First Out,LIFO)栈。方法在执行过程当中,经过各类字节码指令对栈进行操做,出栈/入栈。java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操做数栈。
每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用时为了执行方法调用过程当中的动态链接(Dynamic Linking)。
当一个方法开始执行后,只有两种方式能够退出这个方法:
执行引擎遇到任意一个方法返回的字节码指令,这个时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),这种退出方式称为正常完成出口(Normal Method Invocation Completion)。
方法执行过程当中遇到了异常,而且这个异常没有在方法体内获得处理,不管是java虚拟机内部产生的异常,仍是代码使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会致使方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion),这时不会给它的上层调用者产生任何返回值。
方法退出的过程实际上就等同于把当前栈帧出栈,所以退出时可能执行的操做有:
虚拟机规范容许具体的虚拟机实现增长一些规范里没有描述的信息到栈帧中,称之为栈帧信息。
方法调用并不等同于方法执行,方法调用阶段的惟一任务就是肯定被调用方法的版本,即调用哪个方法,暂时还不涉及方法内部的具体运行过程,就是类加载过程当中的类方法解析。
解析就是将Class的常量池中的符号引用转化为直接引用(内存布局中的入口地址)。
在java虚拟机中提供了5条方法调用字节码指令:
System.exit(1); ==>编译 iconst_1 ;将1放入栈内 ;执行System.exit() invokestatic java/lang/System/exit(I)V
//<init>方法 new StringBuffer() ==>编译 new java/lang/StringBuffer ;建立一个StringBuffer对象 dup ;将对象弹出栈顶 ;执行<init>()来初始化对象 invokespecial java/lang/StringBuffer/<init>()V //父类方法 super.equals(x); ==>编译 aload_0 ;将this入栈 aload_1 ;将第一个参数入栈 ;执行Object的equals()方法 invokespecial java/lang/Object/equals(Ljava/lang/Object;)Z //私有方法 与父类方法相似
X x; ... x.equals("abc"); ==>编译 aload_1 ;将x入栈 ldc "abc" ;将“abc”入栈 ;执行equals()方法 invokevirtual X/equals(Ljava/lang/Object;)Z
List x; ... x.toString(); ==>编译 aload_1 ;将x入栈 ;执行toString()方法 invokeinterface java/util/List/toString()Z
在编译阶段就能够肯定惟一调用版本的方法有:静态方法(类名)、私有方法、实例构造器(
指在运行时对类内相同名称的方法根据描述符来肯定执行版本的分派,多见于方法的重载。
下面的例子中,输出结果均为hello guy
。
“Human”称为变量的静态类型(Static Type),或者叫作的外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中均可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量自己的静态类型不会被改变,而且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可肯定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
代码中定义了两个静态类型相同但实际类型不一样的变量,但虚拟机(准确地说是编译器)在重载时是经过参数的静态类型而不是实际类型做为断定依据的。而且静态类型是编译期可知的,所以,在编译阶段,Javac编译器会根据参数的静态类型决定使用哪一个重载版本,因此选择了sayHello(Human)做为调用目标。全部依赖静态类型来定位方法执行版本的分派动做称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,所以肯定静态分派的动做实际上不是由虚拟机来执行的。
指对于相同方法签名的方法根据实际执行对象来肯定执行版本的分派。编译器是根据引用类型来判断方法是否可执行,真正执行的是实际对象方法。多见于类多态的实现。
动态分配的实现,最经常使用的手段就是为类在方法区中创建一个虚方法表。虚方法表中存放着各个方法的实际入口地址。若是某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。若是子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。PPT图中,Son重写了来自Father的所有方法,所以Son的方法表没有指向Father类型数据的箭头。可是Son和Father都没有重写来自Object的方法,因此它们的方法表中全部从Object继承来的方法都指向了Object的数据类型。
Java语言常常被人们定位为“解释执行”语言,在Java初生的JDK1.0时代,这种定义还比较准确的,但当主流的虚拟机中都包含了即时编译后,Class文件中的代码到底会被解释执行仍是编译执行,就成了只有虚拟机本身才能准确判断的事情。再后来,Java也发展出来了直接生成本地代码的编译器[如何GCJ(GNU Compiler for the Java)],而C/C++也出现了经过解释器执行的版本(如CINT),这时候再笼统的说“解释执行”,对于整个Java语言来讲就成了几乎没有任何意义的概念。
基于栈的指令集:指令流中的指令大部分都是零地址指令,它们依赖操做数栈进行工做。
基于寄存器的指令集:最典型的就是X86的地址指令集,通俗一点,就是如今咱们主流的PC机中直接支持的指令集架构,这些指令集依赖寄存器工做。
举个简单例子,分别使用这两种指令计算1+1的结果,基于栈的指令集会是这个样子:
iconst_1 iconst_1 iadd istore_0
两条iconst_1指令连续把两个常量1压入栈后,iadd指令把栈顶的两个值出栈、相加,而后将结果放回栈顶,最后istore_0把栈顶的值放到局部变量表中的第0个Slot中。
若是基于寄存器的指令集,那程序可能会是这个样子:
mov eax, 1 add eax, 1
mov指令把EAX寄存器的值设置为1,而后add指令再把这个值加1,将结果就保存在EAX寄存器里面。
基于栈的指令集:
优势:可移植、代码相对更紧凑、编译器实现更简单等
缺点:执行速度慢、完成相同功能的指令数量更多、栈位于内存中基于寄存器的指令集:
优势:速度快
缺点:与硬件结合紧密
参考连接:
本文由『后端精进之路』原创,首发于博客 http://teckee.github.io/ , 转载请注明出处
搜索『后端精进之路』关注公众号,马上获取最新文章和价值2000元的BATJ精品面试课程。