java虚拟机的理解

Content: 这是看完周志明老师的深入理解java虚拟机之后的个人理解。仅供参考。

Time  : 2016/11/19

1.      首先本文按照一个java程序的流程来写。

2.      这里按照过程讲一下。首先是你写了一段代码。然后javac(不用ide,这样会更好一点)。Javac是编译。变成中间代码,class文件。

涉及 编译原理里面的解释和编译。

2.1. 编译和解释:

2.1.1.       编译型语言在编译过程中生成目标平台的指令,解释型语言在运行过程中才生成目标平台的指令。

2.1.2.       虚拟机的任务是在运行过程中将中间代码翻译成目标平台的指令。

2.1.3.       编译相当于先点菜后吃饭,解释就是吃火锅。

 

2.1.4.       Java原来是分到编译里面的。但是有些人任务在虚拟机里实际是解释。这种概念理解就好。这里编译就是把你的代码变成class文件。

3.      然后就是class文件的知识。使用winHex或者HexEdit等工具查看class文件就会发现里面都是16进制,以一个字节(两个16进制数字)为单位。

3.1. 首先要明白,class文件的格式是很严格的。因为里面没有空格,没有任何的解释或者修饰字符。不想xml那样你可以理解或者符合人类的阅读习惯。当然,这样也有好处,就是效率比较高。

3.2  前面四个自己就是CAFEBABE,也就是java的标志,成为魔数。各类文件都有魔数,而咖啡宝贝则是java的class文件的标志。

3.3. 然后是版本号。高版本可以兼容低版本,但是低版本绝对不能执行高版本。这是后面执行class文件的时候jvm必须要检测的(验证)。而后是常量池。这是个值得一说的地方。(一会再查一查)

3.4. Class文件里面实际只有两种数据,就是无符号数和表。无符号数一般是对应着每种类型表的index,根据index查找对应数据类型。然后后面的自己就是这种数据类型的表。上面说class文件里面只有16进制的数字,但是每个字节(也有多于单个字节的,会读取多个字节合到一起)代表的是一个index,对应着常量池的一种类型。因为常量池类型有一张表,然后按照index的值查找就可以知道其代表的类型了。当然还有表,也是类似。也就是一般,先查找常量类型,然后每种常量类型有固定的结构(一种表),然后后面的字节就是按照顺序的排列了。

3.5. 之所以说常量池重要,是因为他和我们在程序里的常量是不同的。因为这里的常量是虚拟机的概念,就是类名(官方叫做类的符号引用),数字,数量值和字符串值。所以后面会看到很多都需要常量池,尤其执行的时候动态都需要常量池里面的符号引用。

3.6. 还有别的类型,像访问标志(public之类),父类索引,字段表集合方法表集合,属性表集合记住,这都是有严格结构的。尤其,属性表集合,因为方法的具体代码放在这里。当然,还有别的许多。知道就好。不过单纯看这些肯定无法知晓其中妙处。感觉能把自然语言翻译成16进制是一件很奇妙的事。

3.7. 然后就是字节码指令。

4.      然后就是java,就是执行。

4.1. 首先是类的加载。类加载分为加载链接初始化。当然,可以分的更细。(class不一定存在文件磁盘里,也可能有jvm直接生产,比如array类)加载,就是根据类的全限定名获取class文件,把这个字节流的静态存储结构变成方法区的数据结构(这里涉及jvm的内存结构,看文末第7点)。并在内存里面生成一个Class对象,用来作为方法去这个类的各种对象的访问入口。

这里有两点,就是jvm的内存结构(文末第7点),还有class对象,也就是java很有名的反射机制。加载分为数组类型和非数组类型。因为数组类型是jvm内部生成的,所以有所不同。但是这不是说他不需要类加载器。

4.2. 然后是链接。链接分为验证,准备,解析。

验证就是上面的文件格式,元数据(是否有父类,继承是否合规),字节码验证(防止出现不合规)符号引用验证等。总之,是字面意思的验证)。

然后是准备。准备阶段的意思是给类变量赋值。这里的赋值是初始值。比如int赋值0.不是给他=后面的值。对象实例肯定要到新建对象的时候才会赋值。但是类变量也是要到初始化的时候才会把=后面的赋值,此时会把赋值操作收集到里面。初始化就是执行clinit。但是,final例外。他会在此时就赋给=后面的值。其实也很好理解,就是为了防止也许对象还会给类变量赋值。所以就等到初始化的时候了。

然后是解析。解析是吧符号引用替换为直接引用的过程。直接引用的目标一定已经在内存中存在了。解析的过程也伴随着加载。也就是先后顺序是这样,但是是开始的先后顺序。但是加载和解析以及前面的验证等是可以同时进行的。

一般解析jvm会有机制防止多次解析(就是第一次之后后面会更容易)。还有字段解析,就是从子类到父类接口一个一个找。

4.3. 然后就是初始化:

初始化发生的条件一共五中:

New和调用static,,反射,父类,main主类等。

初始化阶段是执行类构造器Clinit()的过程,首先clinit收集类变量的赋值和静态的赋值,按顺序。然后执行。

关于类加载器的双亲委派机制。不要被双亲迷惑了,其实就是parents,因为复数,所以翻译成双亲,但是其实就是父类。也就是如果要加载,加载器会先给父类,父类可以加载就让父类加载,不可以的话才是自己。这就是双亲委派机制。这样做是为了防止重复加载。比如Object,如果哪个类加载器都加载,就会有很多Object类(因为类构造器和类放到一起才能唯一标识一个类)。加载器分为三种:启动类加载器,负责加载JAVA_HOME/lib下面的类,扩展类加载器,JAVA_HOME/lib/ext下面的类。应用程序类加载器。一般用户类用此。默认。除非自己定义。另外,上面三种类加载器是特殊的父子关系,因为他们不是继承,是组合。但是又有继承的那个意思。

 

5.      下面是关于虚拟机字节码执行引擎的。所谓虚拟机,是相对于物理机而言的。至于物理机,就是计算机组成+操作系统了。这里的执行便是对前面的加载到jvm的内存用class文件里面的方法进行操作。

5.1. 首先jvm用的是操作数栈,不是寄存器。物理机现在多用寄存器,因为更快。但是操作数栈更方便。因为他是把操作数和指针压进同一个栈,每次操作如果有操作数,肯定是下面的出站数据。

5.2. 还有动态链接,就是静态链接相对的。有一部分是加载期直接转化为直接引用,但是有一部分是运行时的。

5.3. 后面一个很重要的一点就是方法的调用。当然,这时的方法已经都是指令了,只需要压进栈就可以了。参考class的文件结构。但是有另外一点,关于重载和多态方法调用。所谓分派。分为静态分派和动态分派。

5.3.1. 所谓静态分派,就是编译期,并不是加载期。因为编译期是确定要调用的方法的名字,也就是符号引用。加载期是把符号引用换成直接引用。这时候还没有加载,没有Class对象。想必到这里就可以明白了静态的含义了。静态分派是根据声明时的变量来确定的,这就是重载。因为这时候没有初始化,没有Class对象,无从知道new后面的类型的。还有就是重载的另一个原则,就是最佳匹配。如果无法确定最佳,那么就会报错。

5.3.2. 动态分派就是相对而言,根据多态代码的字节码,这种实现主要根据invokevirtual这个指令来完成的。Invokevirtual会先找到操作数栈顶第一参数的实际类型,然后再此类型里面找符合权限的方法,找到则停,找不到则往父类里面找。

6.      然后就是程退出(当然,不一定退出才gc,运行是也会发生),gc:

Gc算是java最具特色的地方了。Jvm书中有一句话很好,那就是C++和java之间有一道gc隔起来的围墙,墙里面的人想出去,墙外的人想进来。至于java和C++的比较久比较著名了,其实就是效率和便利的权衡。这里不谈。这里主要讲gc的算法。

6.1. 首先需要说一点,就是如何判断对象已死?

原来常用的是引用计数法。那就是如果有引用指向这个对象,那么这个对象的计数就不为0,就算是活的。但是这样无法处理互相引用的情况。于是,可达性分析便来了。

所谓可达性分析,就是根据栈(运行时)和方法区里面类静态的引用和常量引用(因为只要能在编译期确定的,jvm一定会选择在编译期确定)来索引对象。凡是可以达到的就标记可达。最后没有标记的就认为已死。

6.2. Gc算法:

首先是标记清除,很简单,就是把已死的对象标记,然后暂停虚拟机清除。但是比较麻烦。现在一般不用。主要用下面两种。

然后是复制清除。这时候就需要讲一下分代机制了。所谓分代,就是把堆分为新生(young)代和老年代(old),不同的代用不同的gc算法。这样效率更高。因为对于新生代,新陈代谢旺盛,gc比较频繁,这样的话用复制清除比较好。所谓复制清除,就是把young代分为三部分,8(Eden):1(Survivor):(Survivor)1,每次把Eden和其中一个Servivor的所有活着的对象复制到另外Young里面,然后清空其他两块。下次就是Eden和这次的这个young。这样速度更快。不需要标记和整理内存了。当然,刚刚的复制时候会出现一个问题,就是万一survivor空间不够怎么办。于是有了担保机制。那就是存在千分之一左右的概率Survivor不够用。如果不够,就把无处可放的对象放到老年代里面去。称为担保机制。还有就是老年代,就是存放一些比较大型的对象,或者年龄比较大的对象。这里会有年龄的记录的。此法多用于新生代。

然后是标记整理,因为标记清除之后,腾出来的空间到处都有,不方便使用,所以加一步整理,把小空间整理成大的主要用于老年代

7.      最后补充一下关于jvm的内存:

Jvm的内存:

感觉主要明白jvm划分的几个区以及各自作用即可。

首先jvm分为函数计数器,虚拟机栈,本地方法栈,java堆,方法区,常量池。前三个都是线程私有的。因为计数器和栈都是每个线程都需要一个(可以根据下面的功能自己体会)。

7.1. 程序计数器:字节码的行号指示器。

7.2. 栈:分为虚拟机栈和本地方法栈,其实分成一个也行。只不过细分一下。虚拟机栈是运行是的内存,如果学过操作系统可以比照操作系统运行时的栈来理解,所以主要存放的是局部变量表(运行时需要的内存,其中有各种引用)操作数栈动态链接最后说一点,就是栈是运行才有的,不执行class代码,里面啥也没有。

 

7.3. 本地方法栈测试一些本地方法。所谓本地方法,就是一些底层的方法,比如c语言的一些方法。因为java本身也是c写成的。

7.4. Java堆就比较著名了,就是存放对象的地方。当然,后面gc也会进行细分。这里只需要知道所有对象都会放到这里,主要是new,也有不是new的。

7.5. 方法区,主要存放类的代码。方法区就是Hotspot的永久代

7.6. 常量池是方法去的一部分,但是细分一下,就被分出来了。主要存放的是字面量和符号引用,这些都是编译期就知道的。。所谓字面量就是1,w,这样的。并不是static和final。静态变量和常量是放到方法区的,和类同在。

7.7. 对象的产生过程,以最广泛的new为例,就是先new,然后查找符号引用,加载,链接(分配内存),初始化。(给类分配内存不是在初始化。初始化时给对象分配内存。一个在堆,一个在方法区)

7.8. 关于对象的访问定位:两种方式,就是句柄或者直接指针。

句柄:java栈本地变量表->(句柄池)(java堆)->两个指针,指向对象实例数据(堆),指向类型数据(方法区)。

直接指针就是节省了指向对象的示例数据指针。

HotSpot用的是第二种,因为快。当然,不那么灵活。

你可能感兴趣的:(java)