tips:本篇文章基于Hotspot JVM
与JDK 1.8
所撰写。
内存区域
我们首先来根据一张图初步了解一下内存区域的划分:
因为我发现每一版块都有好多东西要说,故把各区域单拿出来一一说明。
首先介绍JVM中可以说最不起眼但是又无比重要的组件——程序计数器。
程序计数器
程序计数器是最小的一块内存区域,它可以看作是当前线程所执行的字节码的行号指示器。
每条线程需要有一个独立的程序计数器(线程私有),以保证线程切换后能恢复到正确的执行位置。
要理解程序计数器,首先我们要知道Java虚拟机的多线程是如何工作的:
Java虚拟机的多线程是通过线程轮流切换分配处理执行时间的方式来实现的。
在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条程序中的指令。
知道了多线程的机制,程序计数器理解起来就不难了,下面我们举个很简单的例子:
假如我在同一时间只能做一件事(就像JVM线程执行一样),正当我在看直播的时候,妈妈叫我去打酱油,可是直播又不能在某个时刻暂停,怎么办?这时程序计数器就派上了用场,我用它来保存直播进度,也就是从开播开始到现在的时刻,这个时候当我打完酱油回来,重新翻看今天的直播录像,将时刻从程序计数器中取出来,就可以继续我的进度往下去看。
程序计数器所做的,正是帮助JVM,完成多线程的轮转。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。
这个记录值在实现上可能有两种形式:
- 相对该方法字节码开始处的偏移量,叫做
bytecode index
。 - 该Java字节码指令在内存里的地址,叫做
bytecode pointer
。
下面我们对一个简单的Java程序进行反编译:
public class Test {
public static void main(String[] args) {
int a = 1;
String b = "Hello world!";
System.out.println(b);
}
}
编译后,在终端输入javap -verbose Test.class
进行反编译,可以看到字节码指令信息(多余部分因篇幅原因删去):
public static void main(java.lang.String[]);
Code:
stack=2, locals=3, args_size=1
0: iconst_1
1: istore_1
2: ldc #2 // String Hello world!
4: astore_2
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: aload_2
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
}
可以看到,操作指令旁边的那些数字,也就是0, 1, 2… ,这些就是字节码指令的偏移地址,也是计数器所要存放的东西。
而如果执行的是本地方法(native),计数器中的值为空(undefined)。
至于原因,我们简单了解一下什么是native方法:
简单来说,native方法就是Java用来调用非Java代码的接口,在Java中native方法并不提供实现,其实现由非Java语言完成,Java只负责来调用。
所以,native方法是Java通过JNI(Java Native Interface ,提供了若干的API以实现Java和其他语言的通信)调用其他语言来实现,并未编译成需要执行的字节码指令,所以无法统计,自然计数器中的值就为空了。
而当线程需要调用native方法的时候,只需要重新启动一个线程,两个线程相互独立,即可避免程序执行混乱。
字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。
分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
参考文献
- 《深入理解Java虚拟机:JVM高级特性与最佳实践》
- 《Java虚拟机规范 Java SE 8版》