一、JVM介绍
JVM(Java virtual machine)是一种虚拟机,本身用C语言编写,用来屏蔽不同操作系统的细节,使得Java代码经过一次编译即可在不同的系统上运行。如图所示:
我们用javac命令,就是将Java源文件(.java)编译成Java字节码文件(.class)文件,而jvm会把.class文件翻译成机器码以实现Java的跨平台性。
这里跟C和C++做个比较:
- C和C++语言也是跨平台的,不过他们的跨平台是在编译器级别的,不同平台要用不同编译器来编译代码,并且在源码级别要对不同平台做相应处理,所以C和C++有“一次编写,到处编译”的说法。
-
Java的跨平台是通过JVM实现的,JVM帮开发者屏蔽了不同平台的细节,同样的源码只经过一次编译就可以在不同的平台上运行,这也就是Java的“一次编写,到处运行”。同时这也是Java速度比C、C++慢的原因,因为Java代码要经过JVM的翻译才可以跟操作系统交互,并且Java的垃圾回收机制也会消耗大量时间。
二、JVM的内存模型
JVM的内存模型即Java的运行时数据区,也就是Java程序在运行时,各种数据存放的地方,基本结构如图:
图一是JVM内存基本结构,分为线程共享区和非线程共享区,图二是比较详细的版本,其中堆又分为新生代和老年代,新生代又分为Eden区和两个Survivor区,下面针对图一来详细说明JVM内存结构。
- 非线程共享区
- 程序计数器: 程序计数器是用于标识当前线程执行的字节码文件的行号指示器。多线程情况下,每个线程都具有各自独立的程序计数器,所以该区域是非线程共享的内存区域。
通俗点说,程序计数器用来记录当前线程的代码执行到哪里了,那么为啥要记录呢?我一行一行执行不就行了吗?其实在多线程环境中,如果当前线程被挂起了,那恢复的时候怎么知道上次执行到哪里了?这时候程序计数器的作用就来了。
比如说有个Hello.java:
public class Hello {
int add() {
int a = 1;
int b = 2;
int c = a + b;
return c;
}
public static void main(String[] args) {
Hello h = new Hello();
int result = h.add();
System.out.println(result);
}
}
我们用javap -c
命令来对class文件进行反汇编:
Compiled from "Hello.java"
public class file.Hello {
public file.Hello();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
int add();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: iload_3
9: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // class file/Hello
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method add:()I
12: istore_2
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: iload_2
17: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
20: return
}
代码中Code下的行号,就是程序计数器了。
-
虚拟机栈
虚拟机栈描述的是Java方法执行时的内存,每个方法在执行时都会创建一个栈帧(stack frame),每个方法从开始到结束都对应着一个栈帧从入栈到出栈的过程。栈帧又包含了局部变量表、操作数栈、动态链接和方法出口4个部分。- 局部变量表:存放方法参数和局部变量
- 操作数栈: 操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。以上面Hello类为例,
iconst_1
的作用就是定义第一个int常量,istore_1
则将该值压入操作数栈,等运算的时候再进行弹栈(iload_1
)。 - 动态链接: 在运行时常量池中存有大量的符号引用,当栈帧A想调用栈帧B的方法时,就需要以B方法的符号引用作为参数,将符号引用转为直接引用来进行调用,这些符号引用一部分在类的加载阶段(解析)或第一次使用的时候就转化为了直接引用(指向数据所存地址的指针或句柄等),这种转化称为静态链接。而相反的,另一部分在运行期间转化为直接引用,就称为动态链接。
- 方法出口:分为正常完成出口和异常完成出口
正常完成出口:执行任意一个方法返回(如:return)的字节码指令,如有返回值则返回给调用者。
异常完成出口:在方法执行过程中发生异常,且没有在方法体中进行处理。异常完成出口退出时,不会给上层调用者任何返回值。
本地方法栈
和虚拟机栈类似,只不过本地方法栈是为native方法服务的。
- 线程共享区
方法区
存放类信息,如类的字段、方法、常量等信息,在jdk1.8之前,方法区是位于永久代的,在jdk1.8之后,永久代已经被移除,方法区放在了元数据空间里。
元数据空间(meta area)和之前的永久代类似,存放了类的一些基本数据和常量池,但元空间不是在JVM里的,而是使用的本地内存。堆
存放Java对象和数组对象的地方,jvm内存结构中,堆占的空间是最多的,也是Java垃圾收集器工作的地方。
堆分为新生代和老年代,新生代又分为Eden区和两个Survivor区,关于堆和垃圾收集器的内容,放在后面的文章讲解。