1. 程序计数器

现在多线程越来越普遍了,但是对于单核处理器而言,同一个时刻只能够执行一行指令。多个线程的同时执行,实际上是通过线程切换来实现的。一种简单的方式就是,每个线程执行一段时间后,就切换到另外一个线程去执行。当线程A执行到某行字节码指令时被挂起,这个时候切换到线程B执行一段时间后,又需要切换回来执行线程A,那么需要从上一次中断的地方继续执行。所以需要每一个线程都有一个程序计数器,用来存储当前需要执行的字节码指令的地址。

2. 栈空间

每个线程都有一个执行方法,而方法的局部变量表、操作数栈、动态链接和方法出口信息需要存储在栈空间中。当执行线程的主方法时,会在栈空间中分配一个栈帧给这个线程用来存储前面描述的各种信息。线程执行完成后,则释放这段栈帧。栈帧可以看作是栈的子区域。

JVM规定了栈的最大深度,当栈所需要的空间不够时,如果不能够扩展栈空间,那么会抛出StackOverflowError异常。如果能够扩展栈空间,但是无法申请到可用的空间时,则会抛出OutOfMemoryError异常。

在JVM中,根据执行方法类型的不同,又分成了虚拟机栈和本地方法栈。

3. 堆空间

前面所说的程序计数器和栈空间都是线程私有的,而JVM的堆空间则是所有线程共有的。根据JVM规范规定:所有对象和数组都要在堆上进行分配。粗略的将,栈上存储的是对象的引用,而堆上存储的才是对象本身。同样,JVM也规定了堆空间的最大值,当堆空间不够并且无法申请额外的空间时,也会抛出OutOfMemoryError异常。

在JVM中,堆也称为GC堆,因为堆空间是垃圾回收器主要进行内存回收的部分。

4. 方法区

方法区也是所有线程共有的,用于存储已经被加载的类信息、常量、静态变量、即时编译器编译后的代码。比如字面值常量“ddd”就是存储在方法区的。这也就解释了为什么下面这行代码是重复创建对象的:

String str = new String("ddd");         // 不推荐的做法

因为“ddd”本身就是存储在方法区的一个独立且唯一的对象,所以这里不必调用String的构造函数重新创建一个对象:

String str = "ddd";             // 推荐的做法