本系列是用来记录《深刻理解Java虚拟机》这本书的读书笔记。方便本身查看,也方便你们查阅。java
欲速则不达,欲达则欲速!数组
第六章:类文件结构安全
讲完了自动内存管理,咱们来讲说执行子系统。执行子系统讲解的是JVM如何执行程序。数据结构
Class文件概述jvm
这篇咱们只讲讲Class文件。Class文件又名类文件或字节码文件。javac将.java文件(源代码)编译成class文件(字节码),jvm再将.class文件解释成机器码。ide
Class文件中包含的是java虚拟机指令集和符号表以及若干其它辅助信息。其是一组以8字节为基础单元的二进制流,没有空隙存在。优化
其存储数据的结构有两种:无符号数和表。编码
(1)无符号数是用来描述数字,索引引用,数量值或按照UTF-8编码构成字符串值。属于基本的数据类型,以u1,u2,u4,u8分别表明1个字节,2个字节,4个字节,8个字节spa
(2)表是由多个无符号数或其它表做为数据项构成的复合数据类型,以“_info”结尾。翻译
其特色是:在class文件中,哪一个字节表明什么含义,长度是多少,前后顺序如何,都不容许改变。
Class文件组成部分
对于Class的组成,在上图中已经罗列的很清楚了。还需再对常量池进行一下强调:当虚拟机运行时,须要从常量池得到对应的符号引用,再在类建立时或运行时解析、翻译到具体的内存地址之中。
第七章:类加载机制
1、类加载器
JVM的类加载是经过ClassLoader及其子类来完成的,类的层次关系和加载顺序能够由下图来描述:
一、基本概念
类加载机制是把类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终造成能够被虚拟机直接使用的java类型。这一系列的过程都是在程序运行期间完成的。
二、使用情景
对于一个非数组类的加载阶段,可使用系统提供的引导类加载器来完成,也能够由用户自定义的类加载器去完成。
对于数组类而言,其由java虚拟机直接建立,不经过类加载器。
三、双亲委派机制
双亲委派机制是类加载所采起的一种方式。若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把这个请求委托给父类加载器去完成。每一层的类加载器均是如此。只有当父加载器反馈本身没法完成这个请求时,子加载器才会尝试本身去加载。
类比到现实:小明想买一个玩具推土机,可他又很差意思直接张口。因此,发生了下面的对话。
小明去问他爸爸:爸爸你有挖土机吗?
爸爸说:没有哎
接着爸爸问爷爷:爸爸爸爸,你有挖土机吗?
爷爷说:没有哎
接着爷爷问太爷爷:爸爸爸爸,你有挖土机吗?
太爷爷说:我也没有。让重孙子去买一个吧。
结果小明就高高兴兴地本身去买了一个玩具挖土机。
问题来了:若是爷爷有一台挖土机怎么办?那小明只能玩爷爷那个了,不能本身去买了。类比到类加载机制里,就是若是某广父类能对此类进行加载,那应用程序类或自定义这些子类就不用本身加载了。
四、分类
启动类加载器是使用C++实现的,是虚拟机自身的一部分。
其它类加载器是由java语言实现的,独立于虚拟机外部,而且所有继承自抽象类java.lang.ClassLoader。
五、好处
以string类为例,用户本身写了一个string类的实现,对此类进行加载时,只会委派给启动类加载器来对JDK中本来的string类进行加载,而自定义的string类永远不会被调用,这样保证了系统的安全。
2、何时进行类加载
只有如下5中方式必须当即对类进行加载
一、使用new实例化对象的时候;读取或配置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候;调用一个类的静态方法的时候。
二、使用java.lang.reflect包的方法对类进行反射调用的时候。若是类没有进行过初始化,则须要先触发其初始化。
三、当初始化一个类的时候,若是发现其父类还没进行过初始化,则须要先触发其父类的初始化。
四、当虚拟机启动时,用户须要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类。
3、类加载过程详述
类加载过程分为5步。大部分都是由虚拟机主导和控制的,除了如下两种情形:
开发人员能够经过自定义类加载器参与
会执行开发人员的代码去初始化变量和其它资源
一、加载
虚拟机须要完成的事情:
(1)经过一个类的全限定名来获取定义此类的二进制字节流。
(2)将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
(3)在内存区生成一个表明这个类的java.lang.Class对象,做为方法区这个类的各类数据的访问入口。
二、验证
验证的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全。
其分为4个步骤:文件格式验证,元数据验证,字节码验证,符号引用验证。
其中文件格式验证是直接对字节流进行操做的,其他3项是在方法区中进行的。
三、准备
此阶段时正式为类变量分配内存并设置类变量初始值的阶段。其是在方法区中进行分配。有两个注意点:
(1)此时只是对类变量(static修饰的变量)进行内存分配,而不是对象变量。给对象分配内存是在对象实例化时,随着对象一块儿分配到java堆中。
(2)若是一个类变量没有被final修饰,则其初始值是数据类型的零值。好比int类型时0,boolean类型时false。举个例子说明:
public static int value = 123;
在准备阶段事后的初始值为0而不是123,由于这个时候还没有开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类加载器<clinit>()方法之中。因此把value赋值为123的动做将在初始化阶段才会执行。
public static final int value=123;
此时由于final,因此在准备阶段value就已经被赋值为123了。
四、解析
解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。可对类或接口、字段、类方法、接口方法等进行解析。
(1)符号引用是什么
符号引用即便包含类的信息,方法名,方法参数等信息的字符串,它供实际使用时在该类的方法表中找到对应的方法。
(2)直接引用是什么
直接引用就是偏移量,经过偏移量能够直接在该类的内存区域中找到方法字节码的起始位置。
符号引用时告诉你此方法的一些特征,你须要经过这些特征去找寻对应的方法。
直接引用就是直接告诉你此方法在哪。
五、初始化
此阶段时用于初始化类变量和其它资源,是执行类构造器<clinit>()方法的过程,此时才真正开始执行勒种定义的java程序代码。
第八章:字节码执行引擎
JVM中的执行引擎在执行java代码的时候,通常有解释执行(经过解释器执行)和编译执行(经过即时编译器产生本地代码执行)两种选择。
1、栈帧
一、基本概念
(1)、定义
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它位于虚拟机栈里面。
(2)、做用
每一个方法从调用开始到执行完成的过程当中,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
(3)、特色
二、局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
//方法参数 max(int a,int b)
局部变量和类变量(用static修饰的变量)不一样
//全局变量 int a; //局部变量 void say(){ int b=0; }
类变量有两次赋初始值的过程:准备阶段(赋予系统初始值)和初始化阶段(赋予程序定义的初始值)。因此即便初始化阶段没有为类变量赋值也不要紧,它仍然有一个肯定的初始值。
但局部变量不同,若是定义了,但没赋初始值,是不能使用的。
三、操做栈
当一个方法刚刚开始执行的时候,这个方法的操做栈是空的,在方法的执行过程当中,会有各类字节码指令往操做栈中写入和提取内容,也就是出栈、入栈操做。
例如,计算:
int a = 2+3;
当执行iadd指令时,会将2和3出栈并相加,而后将相加的结果5出栈。
四、动态连接
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用做为参数。这些符号引用分为两个部分:
(1)静态解析:在类加载阶段或第一次使用的时候就转化为直接引用。
(2)动态连接:在每一次运行期间转化为直接引用。
五、返回地址
当一个方法开始执行后,只有两种方式能够退出这个方法:正常退出、异常退出。不管采用何种退出方式,在方法退出以后,都须要返回到方法被调用的位置,程序才能继续执行。
(1)正常退出:调用者的PC计数器做为返回地址,栈帧中通常会保存这个计数器值。
(2)异常退出:返回地址是经过异常处理器表来肯定的,栈帧中通常不会保存这部分信息。
2、方法调用
一、解析
对“编译器可知,运行期不可变”的方法进行调用称为解析。符合这种要求的方法主要包括:
(1)静态方法,用static修饰的方法
(2)私有方法,用private修饰的方法
二、分派
分派讲解了虚拟机如何肯定正确的目标方法。分派分为静态分派和动态分派。讲解静动态分派以前,咱们先看个多态的例子。
Human man=new Man();
在这段代码中,Human为静态类型,其在编译期是可知的。Man是实际类型,结果在运行期才可肯定,编译期在编译程序的时候并不知道一个对象的实际类型时什么。
(1)静态分派
全部依赖静态类型来定位方法执行版本的分派动做称为静态分派。它的典型应用是重载。
public class StaticDispatch{ static abstract class Human{ } static class Man extends Human{ } static class Woman extends Human{ } public void say(Human hum){ System.out.println("I am human"); } public void say(Man hum){ System.out.println("I am man"); } public void say(Woman hum){ System.out.println("I am woman"); } public static void main(String[] args){ Human man = new Man(); Human woman = new Woman(); StaticDispatch sr = new StaticDispatch(); sr.say(man); sr.say(woman); } }
运行结果是:
I am human I am human
为何会产生这个结果呢?
由于编译器在重载时,是经过参数的静态类型而不是实际类型做为判断依据的。在编译阶段,javac编译器会根据参数的静态类型决定使用哪一个重载版本,因此两个对say()方法的调用实际为sr.say(Human)。
(2)动态分派
在运行期根据实际类型肯定方法执行版本的分派过程,它的典型应用是重写。
public class DynamicDispatch{ static abstract class Human{ protected abstract void say(); } static class Man extends Human{ @Override protected abstract void say(){ System.out.println("I am man"); } } static class Woman extends Human{ @Override protected abstract void say(){ System.out.println("I am woman "); } } public static void main(String[] args){ Human man = new Man(); Human woman = new Woman(); man.say(); woman.say(); man=new Woman(); man.say(); } }
I am man I am woman I am woman
这彷佛才是咱们平时敲的java代码。对于方法重写,在运行时才肯定调用哪一个方法。因为Human的实际类型时man,所以调用的是man的name方法。其他的同理。
动态分派的实际依赖于方法区中的虚方法表,它里面存放着各个方法的实际入口地址。若是某个方法在子类中被重写了,那子类方法表中的地址将会替换为指向子类实际版本的入口地址,不然,指向父类的实际入口。
(3)单分派和多分派
方法的接收者与放的参数统称为方法的宗量,分为单分派和多分派。
单分派是指根据一个宗量就能够知道调用目标(即应该调用哪一个方法),多分派须要根据多个宗量才能肯定调用目标。
在静态分派中,须要调用者的实际类型和方法参数的类型才能肯定方法版本,因此其是多分派型。
在动态分派中,已经知道了参数的实际类型,因此此时只需知道方法调用者的实际类型就能够肯定出方法版本,因此其是单分派类型。
综上,java是一门静态多分派,动态单分派的语言。
3、字节码解释执行引擎
虚拟机中的字节码解释执行引擎是基于栈的。下面经过一段代码来仔细看一下其解释的执行过程。
public int calc(){ int a = 100; int b = 200; int c = 300; return (a + b) * c; }
第一步:将100入栈。
第二步:将操做栈中的100出栈并存放到局部变量中,后面的200,300同理。
第三步:将局部变量表中的100复制到操做栈顶。
第四步:将局部变量表中的200复制到操做栈顶。
第五步:将100和200出栈,作整型加法,最后将结果300从新入栈。
第六步:将第三个数300从局部变量表复制到栈顶。接下来就是将两个300出栈,进行整型乘法,将最后的结果90000入栈。
第七步:方法结束,将操做数栈顶的整型值返回给此方法的调用者。
鸣谢:特别感谢做者周志明提供的技术支持!