引言

我们知道,JAVA的平台无关性离不开JVM,使得JAVA代码与计算机硬件和 操作系统解耦。关于JVM,贯穿了JAVA编译、执行、优化等方方面面,今天,我们先来看看JVM有哪些区域组成。

运行时数据区

背一下:堆、栈(虚拟机栈、本地方法栈)、程序计数器、方法区(包含常量池)。除此之外还有本地方法接口和方法库,让方法跑起来的执行引擎。

这些都是干嘛的,怎么运作的?

  • :存对象用的,我们new出来的对象存在这里,占据虚拟机内存的半壁江山。在jdk7之后,字符串的常量池也被移到堆中(其他常量池还在方法区)。正因为堆占得内存大,所以是垃圾回收的主要对象,毕竟收益更加明显。提到垃圾,就想到了垃圾分类,没错,JVM也搞垃圾分类。这个我们在第二节讲。
  • :栈,大家都不陌生,stack,后进先出。是的,JVM采用的就是基于栈的执行引擎,就是方法的执行时基于栈操作的,如代码 a + b,你可能会看到类似操作,iload_1;iload_2;iadd;不一定完全一致,但表达的意思是将a和b陆续压栈,执行add操作,并将操作结果放入栈顶。
    另外:就是栈帧,它是java方法执行的基本单位,执行一个方法就是一个栈帧,栈帧里面就包含方法用到的 局部变量表、操作栈、方法在常量池的引用、返回地址
    虚拟机栈和本地方法栈是类似的。
  • 程序计数器(PC):记录线程方法的执行行号。因为每个线程在执行过程中,可能会访问磁盘IO等,当结束的时候需要接着从哪继续执行,所以记录线程执行到的位置是有必要的。就行你平时看书插个书签一样。还有类似跳转、异常、循环等,接下来执行的代码是需要有一个标识的。

栈和程序计数器是线程私有的,很好理解,每个线程都会并发(多核cpu还会并行)执行不同的方法,在一个虚拟机栈上无法满足需求,且无法搞清楚谁是谁的方法。既然栈是每个线程一个,PC当然是人手一个。另外,在执行本地方法的过程中,PC记录的行号为空,因为它不知道本地方法是怎样的,真因为如此,在执行本地方法的时候,不会发生线程切换吧?否则当轮到cpu时间片时,又怎么知道从哪里继续执行呢!

  • 方法区:就是“存方法信息的地方”,相比它的字面义,它存的东西还更多,加载后的类信息、常量(final)、静态常量(static)、JIT编译后的机器码都存在方法区,其中主要关注的是:常量池。常量池中包含的信息很多,类定义,方法定义,字段定义的等引用都存在常量池中。
  • 本地方法接口和本地库:这个主要大概了解一下JAVA有很多操作是调用C++编写的类库的(native方法),比如UnSafe类,这部分就是本地方法。
  • 执行引擎:前面也说了,JVM采用的是基于栈的执行引擎,相对的还有"基于寄存器的执行引擎",两个各有优缺点,比如基于栈是基于内存的,可移植性较强,但性能低一些。基于寄存器的与硬件关联度较高,不利于移植等。

讲了那么多,我们串联起来讲一下

举个例子:

class User{
 public final static String hello = "HELLO WORLD";
 private String name;
 public String getName() {
 	return name;
 }
 public static void main(String[] args){
	User user = new User(); 
	user.getName();
}
}

执行这段代码是一个大概怎样的过程:

  • 首先是javac编译,也叫前端编译,是将*.java文件编译为*.class字节码文件的过程。字节码文件是一个符合虚拟机规范,格式规整的字节码文件,这里就涉及到编译原理的知识了(词法分析、语法分析生成AST、语义分析、校验、解语法糖等操作)。
  • 类加载,将生成的字节码文件加载到JVM中,这里就涉及到classLoader的相关知识(如双亲委派模型,SPI等),加载的过程会涉及到验证,解析,类的初始化(clinit),把类中的常量、方法引用、类的引用放到方法区。
  • 执行,这里就开始执行代码了,java是解释执行的语言(当然也有编译执行JIT的帮助),当遇到new 的字节码时,执行引擎就会在堆中开辟一块空间(涉及指针碰撞、空闲列表等),并初始化(对象头不初始化)。堆中生成的对象仍然会保留着对方法区类,常量的引用。而执行方法的时候就是基于栈的操作,前面也有提到。

简单点说大概就是这样,有一个大概的概念。
说的有什么问题,欢迎指点。