Java程序运行基本原理
Java程序运行基本原理
Java的跨平台性
java代码通过编译后生成.class文件运行在java虚拟机上,同一个.class文件通过虚拟机会得到不同的机器指令(Windows和Linux的机器指令不同),但是最终执行的结果却是相同的。java虚拟机屏蔽了底层操作系统指令上的差异,从而实现java语言的跨平台性。
.class文件的内容
.class文件包含java程序运行的字节码,是一组以8位字节为基础严格按照规定的格式紧凑排列的二进制流,中间无任何分隔符(如下图);二进制文件不易于阅读,我们可以通过class字节码工具:javap输出易于阅读的字节码内容。
java运行时的数据区
Java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。如下图
线程独占:每个线程都会有它的独立空间,随线程的生命周期而创建和销毁
线程共享:所有线程都能访问的内存数据,随虚拟机或者GC而创建和销毁
方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,它是一块各个线程共享的内存区域。
堆内存:是JVM所管理的内存中最大的一块,在虚拟机启动时创建,用于存储对象实例,也是线程共享区域。堆内存还可以细分为:老年代、新生代(Eden、From Survivor、To Survivor),垃圾回收器GC主要管理的就是堆内存空间。如果满了就会触发OOM。
虚拟机栈:虚拟机栈是线程独占的,它的生命周期与线程相同,默认栈内存大小是1M。
线程栈会由多个栈帧组成。
栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,对应的就是一个栈帧在虚拟机栈中入栈到出栈的过程。
一个线程会执行一个或多个方法,每个方法会对应一个栈帧。
本地方法栈:本地方法栈和虚拟机栈作用类似,区别是虚拟机栈是对java方法的执行服务,本地方法栈是对native方法(直接与操作系统交互的原生方法)的执行服务。
程序计数器:是一块比较小的内存区域,可以看做当前线程执行的字节码行号指示器,记录当前线程执行字节码的位置。多线程是通过分片cpu执行时间的方式来实现的,线程切换后cpu要恢复到之前的正确执行位置,就需要通过独立的程序计数器来完成。
根据class文件分析Java代码的执行
下面通过一段测试代码,先编译成class,分析javap命令的解析结果,看看Java代码的执行。
public class apprun {
public static void main(String[] args) {
Integer i = 40;
synchronized (args) {
i++;
}
Integer j = i + 1;
System.out.println(j);
}
}
//编译
javac apprun.java
//javap查看内容
javap -verbose apprun.class
class文件javap后输出的解析结果包含以下几个部分:版本号/访问控制、常量池、构造方法、程序入口main方法、Exception table、LineNumberTable、StackMapTable。我们主要对程序入口main方法进行分析,看代码的底层执行。
程序入口main方法
程序运行的详细指令,具体指令代表的意义请参考java 虚拟机字节码指令表
下图是本次程序编译后的指令以及注释
本测试程序的执行图进行了中文注释说明,其中有几个点需要特别说明下:
1、本测试中用了synchronized关键字进行加锁,在底层的指令中,对应的是monitorenter加锁和monitorexit释放锁,monitorenter操作的目标一定要是一个对象,类型是reference,Reference实际就是堆里的一个存放对象的地址。
2、本段程序在底层中一共有6个局部变量,除了代码里直接定义的i和j,还有方法参数args、加锁时的args的对象引用、i++计算时引入的临时变量、异常处理时的变量
3、对于i++操作,通过指令可以看出底层是这样实现的:
_temp = i; (引入临时变量,赋值为40)
i = i + 1;(执行完add操作后,将加完的值41再填入i中,此时i完成了自增变为了41)
i++ = _temp;(i++的值还是40)
如果不是很清晰,可以改一下代码,加一个赋值操作,编译后再对比,可以看的更直观。
版本号/访问控制
版本号在二进制的class文件中是紧随在魔数0xCAFEBABE之后的4个字节。访问标志名对应的含义可参照
常量池
在二进制的class文件中,常量池是紧随在版本号之后的。常量池中的常量是不固定的,所以它的长度也是未定的。
常量池中主要存放两大常量:字面量和符号引用。
字面量即我们java语言定义的常量,如文本字符串、声明为final的常量值等。
符号引用属于编译原理方面的概念,包括类和接口的全限定名、字段的名字和描述符、方法的名称和描述符
构造方法
在代码里我们没有定义构造函数,但会生成隐式的无参数构造方法
程序完整运行的分析
1、java源代码经过编译后转为字节码,jvm将代码中所有的类信息、常量、静态变量、即时编译器编译后的代码等数据加载都到方法区(1.7之前称为永久代,1.8开始称为元数据空间)。
2、类信息加载完成之后,jvm会创建线程来执行代码,线程会在虚拟机栈和程序计数器创建其独占的空间。本地方法栈是用于native方法的创建。
3、在线程的独占空间内,程序计数器记录当前线程执行字节码的位置。
虚拟机栈则存放多个栈帧,栈帧是方法对应的操作,线程就是不断的执行一个或者多个方法,对应的就会有一个或者多个栈帧。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息,每个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
下面以我们的测试代码为例,对栈帧的操作过程进行一个简单的解析。
图中截取了部分javap解析后main方法的指令,只进行一个简要的栈帧操作过程分析,具体的指令执行,请参考程序入口main方法中的注释详情