@cxm-2016
2016-11-25T21:36:18.000000Z
字数 4556
阅读 3470
JVM知识
版本:3
作者:陈小默
声明:禁止商业,禁止转载
在第一节中Java:JVM内存管理(一)中,我们介绍了JVM的内存结构,在介绍JVM如何管理内存之前,我们先要接触JVM的体系结构与工作方式。
在我们初学Java的时候一定听说过Java这么介绍自己:Java是一款面向对象的跨平台编程语言。当我们学习过Java之后,我们就会对其面向对象思想有深刻的理解,但是我们仍然不明白Java到底是如何实现一次编译到处运行的呢?
JVM的全称是Java Virtual Machine(Java虚拟机),它通过模拟一台计算机来达到一台计算机所拥有的功能。那么一台真正的计算机应该具有什么样的功能呢?[1]
指令集是指能够操作CPU和控制计算机系统的一系列指令的集合,由于每一个型号的CPU作用不尽相同,所以厂商都会为自己的设备单独定义一套指令集。目前市面上主流的两套指令集从体系结构上划分位精简指令集(RISC,Reduced Instruction Set Computing)和复杂指令集(CISC,Complex Instruction Set Computing),这也就造成了像C/C++这样的直接操作硬件的语言编译出的可执行文件在其他平台上并不能正常运行的原因。
指令集是可以直接被计算机识别的机器码,也就是说它必须以二进制形式存放在计算机中,而汇编语言是机器码的助记符。也就是说,我们编写的每一条汇编指令,都是与机器码一一对应的。
不同的CPU架构的寄存器和段不相同,自然无法使用相同的指令集,但是现在不同的芯片厂商往往会兼容不同的指令集以吸引客户。
Java的跨平台特性正是基于JVM的规范,JVM和实体机一样有一套自己的指令集,这个指令集能够被JVM解析执行。这个指令我们称之为JVM的字节码指令,凡是符合JVM规范的class字节码文件都可以被JVM执行。
于是Java的跨平台特性就此产生,我们在任何平台上编写编译的代码都是符合JVM虚拟机规范的,而不同平台上的JVM会进行二次翻译,也就是将字节码指令翻译为JVM所在的计算机的机器码。
Java语言并不是只能运行在JVM之上,只要实现了相应的编译器Java语言就可以运行在任何平台之上(比如J++),也可以被编译为本地代码直接运行在操作系统之上,比如,Linux上的GCJ(GNU Compiler for Java)就可以把Java语言编译为本地代码直接执行。同样的,JVM上也不是只能执行Java语言,只要实现了适当的编译器,将其他语言编译为JVM上的字节码,就可以在JVM上运行。比如,JRuby,Jython以及Groovy等其他JVM语言,都会通过相应的编译器或是解释器转化为.class,然后在JVM上运行。由于JVM并不关心.class文件是由Java、JRuby、Jython等转化而来,只要这个文件结构正确并能通过class文件校验。因此,由于.class文件屏蔽了Java、JRuby等上层语言的差异,所以Java、Kotlin、Groovy等可以相互调用。[2]
JVM的基本结构由4部分组成:
计算机只接受机器指令,其他高级语言必先经过编译器编译成计算机指令才能被计算机正确的执行,所以从高级语言到机器语言之间必须有个翻译的过程。
每当我们创建一个新的线程的时候,JVM会为这个线程创建一个栈,同时为这个栈分配一个PC寄存器,并且这个PC寄存器会指向这个线程中的第一行可执行代码。每当调用一个新的方法的时候会在这个栈上创建一个新的栈帧数据结构,这个栈帧会保留这个方法的一些元信息,比如方法的局部变量、一些用来支持常量池的解析、正常方法的返回以及异常处理机制等。
Oolong是一种属于汇编的编程语言,我们之所以要介绍这门语言,是因为它能够帮助我们更好的理解class文件结构中的类信息,这些信息都是面向JVM的,也就是说只有JVM能够认识它们。所以我们先将class文件中的二进制信息转换为易读的Oolong语言,可以帮助我们理解class文件的结构。
由于资源太难找,本人打包了一个Oolong项目,提供下载CSDN:面积分Oolong汇编查看器下载,使用时将此jar包放在与待查看的class文件同级目录下,然后双击运行即可。或者用命令行对某一指定的class文件进行反汇编Oolong.jar C:\Users\cxm\Desktop\Hello.class
就可以对桌面上的Hello.class文件进行反汇编。得到一个后缀位.j的文件。
首先我们通过一个具体的例子来查看执行引擎的执行过程:
public class Test{
public static void main(String[] args){
int a = 2;
int b = 4;
int c = (a + b) * 10;
}
}
以下是使用Oolong得到的反汇编代码
.source Test.java
.class public super Test
.super java/lang/Object
.method public <init> ()V
.limit stack 1
.limit locals 1
.line 1
l0: aload_0
l1: invokespecial java/lang/Object/<init> ()V
l4: return
.end method
.method public static main ([Ljava/lang/String;)V
.limit stack 2
.limit locals 4
.line 3
l0: iconst_2
l1: istore_1
.line 4
l2: iconst_4
l3: istore_2
.line 5
l4: iload_1
l5: iload_2
l6: iadd
l7: bipush 10
l9: imul
l10: istore_3
.line 6
l11: return
.end method
我们只看方法,首先我们看到开篇第一个方法是来自Object的Init方法,这个方法用来对对象进行初始化,是隐式的。
第二个方法才是我们的main方法
.method public static main ([Ljava/lang/String;)V
.limit stack 2
.limit locals 4
第一行表示这个一个方法.method其访问权限是公开的public,其存储方式的静态的static,其方法名位main,其中参数[表示这是一个数组,L表示这不是一个基本数据类型,其类型是String,返回值是Void。
第二行的 stack 2 说明该方法的操作栈的最大长度为2
第三行的 locals 4 表示该方法使用了局部变量栈的最大长度为4
.line 3
l0: iconst_2
l1: istore_1
.line 3表示当前运行的代码在源文件中的第三行,该行所做的操作是,将常数2入栈,将栈顶元素移至局部变量区第一位存储
.line 4
l2: iconst_4
l3: istore_2
第四行的操作是将int类型的常量4入栈,然后在将其移动到局部变量区的第二位
.line 5
l4: iload_1
l5: iload_2
l6: iadd
l7: bipush 10
l9: imul
l10: istore_3
第五行的操作稍显复杂,14:这里先从局部变量区中取出第一位(int 2)放到操作栈中,15:再将局部变量区中取出的第二位(int 4)压到操作栈中,16:执行加法指令(将栈中连续两次出栈的值相加再将结果压栈),17:将常量10压栈,19:执行乘法操作(将栈中连续两次出栈的值相乘并入栈)
.line 6
l11: return
第六行有一个空返回值的return语句,这也说明了当我们没有显式的添加return语句时,编译器会自动帮我们加上。
接下来我们分析方法调用的实现,源代码如下:
public class Test{
public static void main(String[] args){
int a = 2;
int b = 4;
int c = add(a,b) * 10;
}
public static int add(int a,int b){
return a+b;
}
}
以下是通过Oolong生成的汇编代码
.source Test.java
.class public super Test
.super java/lang/Object
.method public <init> ()V
.limit stack 1
.limit locals 1
.line 1
l0: aload_0
l1: invokespecial java/lang/Object/<init> ()V
l4: return
.end method
.method public static main ([Ljava/lang/String;)V
.limit stack 2
.limit locals 4
.line 3
l0: iconst_2
l1: istore_1
.line 4
l2: iconst_4
l3: istore_2
.line 5
l4: iload_1
l5: iload_2
l6: invokestatic Test/add (II)I
l9: bipush 10
l11: imul
l12: istore_3
.line 6
l13: return
.end method
.method public static add (II)I
.limit stack 2
.limit locals 2
.line 9
l0: iload_0
l1: iload_1
l2: iadd
l3: ireturn
.end method
这里我们可以看到这样一段代码
l6: invokestatic Test/add (II)I
这是一段跳转指令,调用一个Test类中方法签名为add (II)I
的方法。