1 内存区域
1.1 运行时数据区域
运行时数据区域可以划分为由所有线程共享的方法区、堆和线程隔离的虚拟机栈、本地方法栈、程序计数器。
1.1.1 程序计数器(Program Counter Register)-线程隔离
程序计数器是一块较小的内存空间,它是当前线程所执行的字节码的行号指示器。
Java虚拟机的多线程是通过多线程轮流切换、分配处理器执行时间的方式来实现的。为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地方法(Native),这个计数器值则应为空。
该内存区域是唯一一个不存在OutOfMemoryError
情况的区域
1.1.2 Java虚拟机栈(Java Virtual Machine Stack)-线程隔离
Java虚拟机栈是线程私有的,它的生命周期与线程相同。每个Java方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期Java虚拟机基本数据类型、对象引用和returnAddress类型(指向了一条字节码指令的地址)
局部变量表中的存储空间以局部变量槽(Slot)来表示,long和double类型的数据占用两个变量槽,其余数据类型占用一个。
局部变量表所需内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间时完全确定的,方法运行期间不会改变局部变量表的大小(即变量槽的数量)
如果线程请求的栈深大于虚拟机所允许的深度,将抛出StackOverflowError
;如果Java虚拟机栈容量可以动态扩展,当扩展时无法申请到足够的内存,会抛出OutOfMemoryError
。
1.1.3 本地方法栈(Native Method Stacks)-线程隔离
虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用本地(Native)方法服务。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError
异常和OutOfMemoryError
异常
1.1.4 堆(Java Heap)-线程共享
Java堆是虚拟机所管理的内存中最大的一块,Java堆是被所有线程所共享的一块内存区域,在虚拟机启动时创建。
The heap is the runtime data area from which memory for all class instances and arrays is allocated.
所有的对象实例以及数组都应当在堆上分配
- 《Java虚拟机规范》
Java堆的作用是存放对象实例,几乎所有的对象实例都在这里分配内存。随着即时编译技术的发展,Java对象实例分配在堆上也变得不是那么绝对了。
Java堆也称GC堆,是垃圾收集器管理的区域。大部分垃圾收集器都是基于分代收集理论设计的,所以往往我们会将Java堆划分为新生代、老年代等不同区域,以便更好地回收内存。
虽然Java堆是线程共享的,但是我们也可以为各个线程划分出私有的分配缓冲区,以便更快地分配堆内存
Java堆既可以被实现成固定大小的,也可以是可扩展的(一般是可扩展的,通过-Xms和-Xmx设定堆大小)。当Java堆内存不足以完成实例分配时,将会抛出OutOfMemoryError
。
1.1.5 方法区(Method Area)-线程共享
方法区也是各线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。《Java虚拟机规范》将此区域描述为堆的一个逻辑部分。
HotSpot团队将垃圾收集器的分代收集理论扩展至方法区,使用永久代来实现方法区,省去了编写专门的针对方法区的内存管理代码。这种设计使方法区容易遇到内存溢出问题,同时会使HotSpot虚拟机在少数情况下与别的虚拟机有不同的表现。
后续HotSpot舍弃了永久代的设计,移出永久代里的字符串常量池、静态变量放至堆中。采用本地内存来实现方法区,更名永久代为元空间,在该区域保存类型信息(主要)、代码缓存、运行时常量池等数据
字符串常量池和运行时常量池有什么区别呢?
在还存在永久代的设计方案里,字符串常量池是被包括在运行时常量池中的,JDK1.7后字符串常量池就被移至堆中。
运行时常量池作为方法区的一部分,存放着Class文件中的常量池表(Constant Pool Table)这项信息。这张表里存放着编译期生成的各种字面量与符号引用(包括由符号引用翻译出来的直接引用)。
字符串常量池类似一个缓存区。考虑到大量频繁地创建字符串,会影响到程序的性能,因此专门为字符串常量开辟了字符串常量池,用户创建字符串常量时,首先查询常量池中是否存在该字符串,如果存在直接返回引用;不存在时,创建新的字符串常量并放入池中。
方法区的内存,也可以固定大小或者可扩展大小,该区域甚至可以选择不实现垃圾收集。这块区域的垃圾收集目标主要是针对常量池的回收和对类型的卸载。如果方法区无法满足新的内存分配需求,会抛出OutOfMemoryError
异常
1.1.6 直接内存(Direct Memory)
直接内存不属于虚拟机运行时数据区的一部分。
某些机制(如,NIO)可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,避免在Java堆和Native堆中来回复制数据
直接内存不受Java堆大小的限制,但仍受本机总内存大小的限制。当程序员根据实际内存情况配置各内存区域大小参数(如,-Xmx)时,遗漏直接内存的使用空间,就容易导致动态扩展时出现OutOfMemoryError
1.2 对象
1.2.1 对象的创建
创建对象(不包括数组和Class对象)通常是通过字节码new
指令。
创建流程包括:
- 检查这个指令的参数是否能在常量池(方法区)中定位到一个类的符号引用
- 检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,必须先执行相应的类加载过程
- 为新生对象分配内存(对象所需内存的大小在类加载完成后便可完全确定
- 将分配到的内存空间都初始化为零值
- 对对象进行必要的设置,即在对象的对象头(Object Header)中存放一些信息(如,GC分代年龄信息、是否启用偏向锁、这个对象是哪个类的实例等)
new
指令之后会接着执行invokespecial
指令,即<init>()
方法,按照程序员的意愿对对象进行初始化(Java编译器会在遇到new
关键字的地方同时生成new
指令与invokespecial
指令)
其中第3步,在堆上为对象分配空间有两种方式:
1.
1.2.2 对象的内存布局
1.2.3 对象的访问
1.3 内存溢出
2 垃圾回收
3 工具检测
4 参考文献
- 参考书籍:《深入理解Java虚拟机》
- 深刻理解运行时常量池、字符串常量池 - 掘金 (juejin.cn)