前言
Github:https://github.com/yihonglei/jdk-source-code-reading(java-jvm)
一 JVM 运行时数据区
Java 虚拟机在执行程序的过程中会把它管理的内存划分为若干个不同的数据区域。
这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,
有些区域则依赖用户线程的启动和结束而建立和销毁。
根据《Java虚拟机规范(Java SE 7版)》分为如下几个运行时数据区域,Java8 方法区被
元空间替代。
"绿色"部分为线程共享数据区域,"黄色"部分为线程私有数据区域。
二 线程私有内存
Java 虚拟机的多线程是通过线程轮流切换并分配 CPU(处理器)执行时间的方式实现的,
在任何一个确定时刻,一个 CPU(对多核处理器来说是一个内核)都只会执行一条线程中的
指令。CPU 切换的速度非常之快,让我们觉得它是在进行多线程运行,本质上它还是单一
运行的。
在 Java 线程中,每一个线程都有自己独立的内存区域,主要包括序计数器、虚拟机栈、
本地方法栈,也即是线程私有内存,就是内存模型图中"黄色"部分,主要作用是维持线程的
安全、稳定、高效的运转。
注意:
线程私有不存在多线程并发的资源竞争问题,因为其享有的内存是互不影响的,不存在
并发问题。与线程私有相对的是线程公有内存,这一部分内存是线程共享的,
也就是图中"绿色"部分,这一部分内存会出现多线程并发"资源竞争"问题。
三 程序计数器
程序计数器是线程私有的一块较小的内存空间,可以看作是当前线程所执行的字节码的
行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是改变程序计数器的值来获取
下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要
依赖程序计数器来完成。比如,线程切换后能够恢复到正确的执行位置,是因为每一条线程
都有独自的程序计数器,各线程之间计数器又是互不影响,独立存储的,所以线程切换才能
恢复到切换前的正确执行位置,因为不会有别的线程对上次执行的位置做修改。
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令
的地址;如果正在执行的是 Native 方法,这个计数器的值则为空(undefined)。
该区域是 Java 虚拟机中唯一一块不会出现内存溢出异常(OutOfMemoryError)的区域。
总结:
1)程序计数器是线程私有较小内存空间,是程序运行的指示灯。
2)该区域是 Java 虚拟机中唯一一块不会出现内存溢出异常的区域。
四 Java 虚拟机栈
Java 虚拟机栈与程序计数器一样属于线程私有内存,其生命周期与线程生命周期相同。
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的时候会创建一个栈帧
(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法
从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的 8 大基本类型(byte,short,int,long,float,double,
char,boolean)、对象引用(reference 类型,它不等同于对象本身,可能是一个指向对象
起始地址的引用指针,也可能是指向一个代表的句柄或其他与此对象相关的位置)和
returnAddress 类型(指向了一条字节码指令的地址)。
其中 64 位长度的 long 和 double 类型数据占用 2 个局部变量空间(Slot),其余数据类型
占用 1 个。局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法
所需要在栈帧中分配多大的局部变量空间是确定的,在方法运行期间不会改变局部变量表大小。
注意,该区域可能会出现两种异常情况:
1)如果线程请求的栈深度大于虚拟机所允许的深度,抛出 StackOverflowError 异常。
2)如果虚拟机可以动态扩展,当扩展时无法申请到内存,抛出 OutOfMemoryError 异常。
总结:
1)虚拟机栈生命周期与线程生命周期相同。
2)每个方法在执行的时候会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、
方法出口等信息。局部变量表所需要的内存空间在编译期间完成分配,存储基本类型、
reference、returnAddress。
3)会抛出 StackOverflowError 和 OutOfMemoryError 异常。
五 本地方法栈
本地方法栈与虚拟机栈十分相似,两者的区别在于虚拟机栈为虚拟机执行 Java 方法服务,
而本地方法栈则为虚拟机使用本地方法服务。
注意:本地方法栈同样也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
六 Java 堆
Java 堆是 Java 虚拟机所管理的内存中最大的一块,所有线程共享区域,在虚拟机启动时
创建。该区域用于存放对象,当 new 一个对象或数组都在这里分配内存。基本上所有的对象
实例都存在这里,但是随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、
标量替换优化技术将会导致一些微妙的变化发生,所有对象都分配在堆上变得不是哪么绝对。
Java 堆是收集器的主要管理区域。由于现在收集器基本都采用分代收集算法,Java 堆可以
细分为新生代(YoungGeneration)、老年代(OldGeneration);新生代再细一点分为
Eden 空间、From Survivor 空间、To Survivor区域。无论如何划分,都与存放内容无关,
无论什么区域还是存储的对象实例,划分这么细的根本目的是为了更好回收内存或者更快的
分配内存。
Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘
空间一样。在实现中,可以实现为固定大小的,也可以是可以扩展的,现在主流的虚拟机都是
按照可以扩展来实现的,可以通过 -Xmx 和 -Xms 控制堆的最大内存或最小内存。需要注意的
是,空间是有限的,当在堆中没有内存完成实例分配,也就是内存不够使了,并且堆也无法
扩展时,抛出 OutOfMemoryError 异常。
总结:
1)堆为虚拟机中内存最大的一块,线程共享,在虚拟机启动时创建。
2)堆内存唯一的目的就是存储对象实例,即 new 对象和数组都在堆上分配内存。
3)堆内存分为新生代和老年代,新生代又可以分为 Eden 空间、From survivor 空间、
To Survivor 区域。
4)堆内存可扩展,可以通过参数控制大小。-Xmx 设定堆最大内存、-Xms 设定堆最小内存、
默认 64M。
5)当在堆中没有内存完成实例分配,也就是内存不够使了,并且堆也无法扩展时,
抛出 OutOfMemoryError 异常。
七 方法区(永久代)[元空间]
方法区与堆一样,也是线程共享区域,用于存储虚拟机加载的类信息、运行时常量池、
静态变量、即时编译器编译后的代码等数据,比如我们在代码中定义的 Constant 常量就会在
这个区域存储。在 jdk1.7 及其之前,方法区是堆的一个“逻辑部分”(一片连续的堆空间),
为了与 Java 堆区分开,也叫做 Non-Heap(非堆),通过 –XX:MaxPermSize 指定大小。
在 Java 虚拟机规范中,它是属于堆的逻辑部分。
在这个区域中,它也会有垃圾回收器工作,这个区域叫做“永久代”,之所以叫做永久代,
因为它比新生代和老年代拥有更长的生命周期,但是并不是在这个区域它就会万事大吉了,
永久代依然会存在垃圾回收的情况,只不过相对来说较少。该区域可能会抛出
OutOfMemoryError 异常。从 jdk1.7 已经开始准备“去永久代”的规划,jdk1.7 的 HotSpot 中,
已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中,(常量池除字符串
常量池还有 class 常量池等),这里只是把字符串常量池移到堆内存中;在 jdk1.8 中,
方法区已经不存在,原方法区中存储的类信息、编译后的代码数据等已经移动到了
元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存
(NativeMemory),大小限制于本地内存,可以使用 -XX:MetaspaceSize 和
-XX:MaxMetaspaceSize 指定元空间大小。
jdk1.3~1.6、jdk1.7、jdk1.8 中方法区的变迁。
总结:
1)方法区属于线程共享区域,用于存储虚拟机加载的类信息、运行时常量池、静态变量、
即时编译器编译后的代码等数据。
2)该区域会抛出 OutOfMemoryError 异常。
去永久代的原因:
1)字符串存在永久代中,容易出现性能问题和内存溢出。
2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,
太小容易出现永久代溢出,太大则容易导致老年代溢出。
3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
八 运行时常量池
运行时常量池主要是方法区的一部分,class 文件除了有类的版本、字段、方法接口等
描述信息外,还有一项信息是常量池,用于存储编译期生成的各种字面量和符号引用,
这部分内容将在类加载后进入方法区运行时常量池中存放。也可能抛出 OutOfMemoryError 异常。
九 直接内存
直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用,在 JDK 1.4 引入的 NIO
提供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存,
然后使用 DirectByteBuffer 对象作为这块内存的引用进行操作(详见: Java I/O 扩展),
这样就避免了在 Java 堆和 Native 堆中来回复制数据,因此在一些场景中可以显著提高性能。
显然,本机直接内存的分配不会受到 Java 堆大小的限制,但既然是内存,则肯定还是会受到
本机总内存大小及处理器寻址空间的限制,因此动态扩展时也会出现 OutOfMemoryError 异常。
文献参考
《深入理解Java虚拟机》 (第二版) 周志明 著;