Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。
运行时数据区域
- Java 虚拟机在执行 Java 程序的过程中会把他所管理的内存
划分为若干个不同的数据区域
。这些区域都有各自的用途,以及创建和销毁的时间。来看看各内存分配图(图片来源网络):
程序计数器
- 程序计数器是一块
较小的内存空间
,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能
都需要依赖这个计数器来完成。 - 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。故,
线程私有
。 - 此内存区域是
唯一一个
在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
Java 虚拟机栈
- Java 虚拟机栈也是
线程私有
的,它和 Java 线程一起创建,生命周期与线程相同。 - 每个方法在执行时都会创建一个栈帧用于存储
局部变量表、操作数栈、动态链表、方法出口等信息
。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 - 局部变量表存放了编译期可知的各种
基本数据类型
(boolean、byte、char、short、int、float、double、long)、对象引用
和returnAddress 类型
(指向了一条字节码指令的地址)。 - 局部变量表所需的内存空间
在编译期间完成分配
,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的
,在方法运行期间不会改变局部变量表的大小。 - 在 Java 虚拟机规范中,对于区域规定了两种异常状况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出
StackOverflowError
异常; - 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出
OutOfMemoryError
异常。
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出
- 可以使用
-XSS
参数去调整虚拟机栈的大小。
本地方法栈
- 本地方发栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而
本地方法栈为虚拟机使用到的 Native 方法服务
。 线程私有
,会抛出 StackOverflowError 和 OutOfMemoryError 异常。
Java 堆
- 对于大多数应用来说,Java 堆是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被
所有线程共享
的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的是存放对象实例
,几乎所有的对象实例都在这里分配内存。 - Java 堆是
垃圾收集器管理的主要区域
。- 从内存回收的角度看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的有Eden 空间、From Survivor 空间、To Survivor 空间等。
- 从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(TLAB)。不过如何划分,都与存放内容无关,无论哪个区域,存储的都
仍然是对象实例
,进一步划分的目的是为了更好地回收内存,或者更快地分配内存
。
- Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果堆中
没有内存完成实例分配,并且堆也无法再扩展时
,将会抛出 OutOfMemoryError 异常。
方法区
- 方法区与 Java 堆一样,是各个
线程共享
的内存区域,它用于存储已被虚拟机加载的类信息、运行时常量池、静态变量、即时编译器编译后的代码
等数据。 - Java 虚拟机对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以
选择不实现垃圾回收
。 - 当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
直接内存
- 直接内存并
不是虚拟机运行时数据区的一部分
,也不是 Java 虚拟机规范中定义的内存区域。 - 显然,本机的
直接内存的分配不会受到 Java 堆大小的限制
,但还是会受到本机总内存大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现 OutOfMemoryError 异常。
附加:Java 中几种常量池的区分
JVM 常量池主要分为 Class 文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类对象常量池。
Class 文件常量池
。Class 文件是一组以字节为单位的二进制数据流,在 Java 代码的编译期间,我们编写的 Java 文件就被编译为 .class 文件格式的二进制数据存放在磁盘中,其中就包括 Class 文件常量池。运行时常量池
:当类加载到内存中后,JVM 就会将 Class 文件常量池中的内容存放到运行时的常量池中;在解析阶段,JVM 会把符号引用替换为直接引用,但是运行时常量池的内容并不全部来自 Class 常量池;在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是 String.intern()。全局字符串常量池
:字符串常量池是 JVM 所维护的一个字符串实例的引用表,在 HotSpot VM 中,它是一个叫做 StringTable 的全局表。在字符串常量池中维护的是字符串实例的引用,底层 C++ 实现就是一个 HashTable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。基本类型包装类对象常量池
:Java 中基本类型的包装类的大部分都实现了常量池技术,这些类是 Byte、Short、Integer、Long、Character、Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这 5 种整型的包装类也只是在对应值小于等于 127 时才可使用对象池,也即对象不负责创建和管理大于 127 的这些类的对象。
详情请参考:http://tangxman.github.io/2015/07/27/the-difference-of-java-string-pool/
笔记来源:《深入理解Java虚拟机》第二章 2.2 运行时数据区域(P38)。