0%

jvm-运行时数据区域

主要记录一下学习阅读周志明先生的《深入理解Java虚拟机》,并且总结相关知识。

运行时数据区域

Java虚拟机会在执行Java程序时把内存划分为不同的内存区域。不同的内存区域的生命周期、所产生的异常都是不一样的。Java虚拟机所管理的内存包括以下几个运行时数据区域:

程序计数器

该内存区域能够产生的异常: 程序计数器是Java运行时数据区域唯一一个不会发生OOM异常的区域。

程序计数器(Program Counter Register):可以把它看作是执行的字节码的行号指示器。字节码解释工作就是通过程序计数器的值来选取下一条需要执行的指令。程序中分支、循环、跳转、异常处理以及线程的刮挂起/恢复都是由程序计数器来指示。

线程私有:多线程是通过CPU上下文切换,并且分配处理时间,实际上同一时刻都只会执行一条指令。为了在切换过程中能够准确地找到下一条执行指令。程序计数器是线程私有的

对于Java方法,程序计数器记录的是当前正在执行的字节码指令的地址;对于Native方法,程序计时器的值为空(Undefined)。

Java虚拟机栈

线程私有:在Java方法被调用时,Java虚拟机会为此创建一个栈帧(Stack Frame),用来存储局部变量表操作数栈动态连接方法出口等信息,每个方法从调用到完成,对应一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表
  1. 存储数据
  • 存储了在编译期可知的Java基本数据类型(Java的8大基本数据类型): booleanbytecharshortintfloatlongdouble
  • 存储了对象的引用(Reference类型,可能是一个指向对象的起始地址的引用指针,或者是一个能标识对象位置的句柄等)
  • 存储了方法执行的字节码指令的地址,Return Address
  1. 存储方式

局部变量表中的存储空间用局部变量槽(slot)来表示。其中64位的longdouble占用了两个变量槽。其他数据类型占一个

局部变量表所占用的内存空间在编译时就已经被分配好,在运行期间是无法改变的

该内存区域能够产生的异常

  • Out Of Memory Error:根据书中介绍,在HotSpot虚拟机中,虚拟机栈很难出现内存溢出异常,方法所需的栈帧内存大小在编译期间就已经确定了,因此只要能申请内存空间成功,就一定不会出现OOM。而当申请失败时,会直接导致OOM。
  • StackOverFlow Error:如果线程执行的栈深度大于了虚拟机所允许的深度,会抛出该异常。例如死递归(无递归出口和递归终止条件)。

本地方法栈

本地方法栈与虚拟机栈大体相同,唯一的区别是本地方法栈(Native Method Stacks)是为虚拟机调用本地方法服务的。

Java堆

首先,Java堆是内存空间最大的一块区域。用来存放Java对象。

线程共享:Java堆是所有线程共享的,Java对象的内存分配几乎都在Java堆上完成。

管理方式:Java堆是通过垃圾收集器来进行管理的,通过垃圾回收(Garbage Collected)来对所有对象进行管理。

Java堆能产生的异常:Java堆内存大小是可以扩展的,在启动Java虚拟机时,通过配置Jvm参数-Xms:初始堆内存大小-Xmx:最大堆内存大小,来指定堆内存。因此当没有足够的空间来为对象分配内存时,会抛出OOM异常。

方法区

方法区(Method Area):存储了已被虚拟机加载的类基本信息、静态变量、常量以及被即时编译器(Just In Time,JIT)编译好的代码缓存等,比较常见的就是:字符串常量池。

永久代(JDK8之前):永久代是实现Java方法区的一种方案。Java虚拟机规范中并未规定方法区的具体实现细节。永久代为了沿用堆中的垃圾回收算法进行管理,因此是放在Java堆上,但是由于这种设计模式更容易导致OOM的发生。目前已被元空间(Meta Space)替代。采用本地内存来实现方法区。

该区域能产生的异常:当无法为常量等分配内存空间时,会抛出OOM异常。

运行时常量池

运行时常量池:是方法区的组成部分之一。方法区会存储Class文件的相关信息:类的版本、字段、方法、接口等类的描述信息。同时,也会存储编译期生成的各种字面量和符号引用,这部分信息则是存储在运行时常量池(Constant Pool Table)中。

动态性:运行时常量池是具备动态性的,在运行期间,也是可以往里面放新的常量的,例如java.lang.String#intern,当字符串不存在时,则会往常量池放入该字符串,并返回字符串的地址。

该区域能产生的异常:既然是方法区的一部分,那么肯定也是在无法分配内存时抛出OOM异常。

直接内存(非Java运行时数据区域)

直接内存:不受Java虚拟机内存限制,但是会受到本机内存的限制,Java中有用到操作直接内存的,比较常见的是NIO包,基于网络和缓存实现的IO模式,可以通过Native方法分配直接内存,从而提高性能。

DirectByteBuffer构造器代码,直接通过UnSafe包进行内存分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
DirectByteBuffer(int cap) {                   // package-private

super(-1, 0, cap, cap, null);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);

long base = 0;
try {
base = UNSAFE.allocateMemory(size); //分配内存
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
UNSAFE.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

该区域能产生的异常:当各个区域的内存总和大于物理内存总和时,也是会抛出OOM异常。

-------------本文结束感谢您的阅读-------------