本篇文章是本人阅读《深入理解JVM》和《java虚拟机规范》时的笔记。
JVM是HotSpot,jdk1.7。
大神绕路,不喜勿喷。

1 JVM内存模型

JVM内存模型

2 程序计数器(PC)

每个线程都会有自己私有的程序计数器(PC)。可以看作是当前线程所执行的字节码的行号指示器。
也可以理解为下一条将要执行的指令的地址或者行号。字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、 循环、 跳转、 异常处理、 线程上下文切换,线程恢复时,都要依赖PC.

  • 如果线程正在执行的是一个Java方法,PC值为正在执行的虚拟机字节码指令的地址
  • 如果线程正在执行的是Native方法,PC值为空(未定义)

JVM内存模型

说白了,PC就是一块内存区域。存放着下一条要执行的指令的地址。

3 虚拟机栈(VM Stack)

3.1 简介

VM Stack也是线程私有的区域。他是java方法执行时的字典:它里面记录了局部变量表、 操作数栈、 动态链接、 方法出口等信息。

在《java虚拟机规范》一书中对这部分的描述如下:

栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
栈帧的存储空间分配在 Java 虚拟机栈( §2.5.5)之中,每一个栈帧都有自己的局部变量表( Local Variables, §2.6.1)、操作数栈( OperandStack, §2.6.2)和指向当前方法所属的类的运行时常量池( §2.5.5)的引用。

VM-Stack

说白了,VM Stack是一个,也是一块内存区域
所以,他是有大小的。虽然有大小,但是一般而言,各种虚拟机的实现都支持动态扩展这部分内存。

  • 如果线程请求的栈深度太大,则抛出StackOverflowError
  • 如果动态扩展时没有足够的大小,则抛出OutOfMemoryError

3.2 StackOverflowError

以下代码肯定会导致StackOverflowError:

1
2
3
4
5
6
7
public static void m1() {
m1();
}
public static void main(String[] args) {
m1();
}
1
2
Exception in thread "main" java.lang.StackOverflowError
at xxx.xxx.xxx.m1(JavaVMStackSOF.java:10)

4 本地方法栈(Native Method Stack)

Java 虚拟机实现可能会使用到传统的栈(通常称之为“ C Stacks”)来支持 native 方法( 指使用 Java 以外的其他语言编写的方法)的执行,这个栈就是本地方法栈( Native MethodStack)。

VM Stack是为执行java方法服务的,此处的Native Method Stack是为执行本地方法服务的。
此处的本地方法指定是和具体的底层操作系统层面相关的接口调用了(这部分太高高级了,不想深究……)。

《java虚拟机规范》中没有对这部分做具体的规定。所以就由VM的实现者自由发挥了。
有的虚拟机(比如HotSpot)将VM Stack和Native Method Stack合二为一,所以VM的另一种内存区域图就如下面所示了:

JVM内存模型-虚拟机栈和本地方法栈合二为一

5 Java堆(Heap)

5.1 简介

在 Java 虚拟机中,堆( Heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。

以下是本人对《java虚拟机规范》一书中对Java堆的介绍的总结:

  • 在虚拟机启动的时候就被创建
  • 是所有线程共享的内存区域
  • 存储了被自动内存管理系统所管理的各种对象
    • 这些受管理的对象无需,也无法显式地被销毁
    • 自动内存管理系统:Automatic StorageManagement System,也即是常说的”Garbage Collector(垃圾收集器)”
    • 并未指明用什么具体的技术去实现自动内存管理系统
  • Java 堆的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩
  • Java 堆所使用的内存不需要保证是连续的
  • 如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那 Java 虚拟机将会抛出一个OutOfMemoryError 异常
  • 实现者应当提供给程序员或者最终用户调节 Java 堆初始容量的手段
    • 对于可以动态扩展和收缩 Java 堆来说,则应当提供调节其最大、最小容量的手段
  • 所有的对象实例以及数组都要在堆上分配

JVM-Heap

至于堆内存的详细情况,将在后续的GC相关文章中介绍。

5.2 堆内存中的OutOfMemoryError

以下示例代码肯定导致堆内存溢出:

1
2
3
4
5
6
public static void main(String[] args) {
ArrayList<Integer> list = Lists.newArrayList();
while (true) {
list.add(1);
}
}

无限制的往list中添加元素,无论你的堆内存分配的多大,都会有溢出的时候。

1
java.lang.OutOfMemoryError: Java heap space

6 方法区(Method Area)

方法区是由所有线程共享的内存区域。
方法区存储的大致内容如下:

  • 每一个类的结构信息
    • 运行时常量池( Runtime Constant Pool)
    • 字段和方法数据
    • 构造函数和普通方法的字节码内容
  • 类、实例、接口初始化时用到的特殊方法

以下是本人对《java虚拟机规范》一书中对方法区的介绍的总结:

  • 在虚拟机启动的时候被创建
  • 虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集
  • 不限定实现方法区的内存位置和编译代码的管理策略
  • 容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。
  • 方法区在实际内存空间中可以是不连续的
  • Java 虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段
    • 对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段
  • 如果方法区的内存空间不能满足内存分配请求,那 Java 虚拟机将抛出一个OutOfMemoryError 异常

6.1 运行时常量池(Runtime Constant Pool)

《java虚拟机规范》中对常量池的介绍:

运行时常量池( Runtime Constant Pool)是每一个类或接口的常量池( Constant_Pool,§4.4)的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。
运行时常量池扮演了类似传统语言中符号表( SymbolTable)的角色,不过它存储数据范围比通常意义上的符号表要更为广泛。

运行时常量池

每一个运行时常量池都分配在 Java 虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。

  • 当创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

7 直接内存(Direct Memory)

此处的直接内存并不是由JVM管理的内存。他是利用本地方法库直接在java堆之外申请的内存区域。
比如NIO中的DirectByteBuffer就是操作直接内存的。

直接内存的好处就是避免了在java堆和native堆直接同步数据的步骤。但是他并不是由JVM来管理的。

当然,这部分内存区域的操作也可能会抛出OutOfMemoryError

参考文章

  • 《深入理解JVM》
  • 《Java虚拟机规范》-JDK1.7使用maven创建scala工程.md