JVM 分区(运行时数据区域)
文章目录
- JVM 分区(运行时数据区域)
- 前言
- 1. 程序计数器
- 2. Java 虚拟机栈
- 3. 本地方法栈
- 4. Java 堆
- 5. 方法区
- 6. 运行时常量池
- 7. 直接内存
前言
之前在说多线程的时候,提到了JVM虚拟机的分区内存,如下所示,那次简单鸽了一下没有详细的介绍JVM的各个分区的主要功能/生命周期等内容,在这里补上。
本博客通过图文/代码示例等,详细的介绍了JVM的各个分区的功能,写作不易,求个关注!
参考资料《深入理解Java虚拟机》
1. 程序计数器
程序计数器是一块较小的内存空间,它是线程所有的,随着线程的出现而出现,随着线程的消亡而消亡,它可以看做是当前线程所执行的字节码的行号指示器。
Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。当线程执行时会被CPU调度,时间分片结束后会被再次挂起,直到再一次被CPU调度的时候,就会再次执行一个分片单位。为了保证线程在恢复调度时候能正确的在原有进度上继续向下执行,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
**如果正在执行的是本地(Native)方法,这个计数器值值应该为空。**程序计数器是唯一一个没有规定任何OutOfMemoryError情况的区域。
2. Java 虚拟机栈
虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧[1](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出StackOverflowError异常
如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError(OOM)异常
以下面代码为例,讲一下栈与栈帧:
public class TestA {
public static void main(String[] args) {
hello();
}
public static void hello() {
System.out.println("Hello World!I'm Jim.kk!")
}
}
- 以上代码在开始执行的时候,会遇到main方法,此时main方法入栈
- 当遇到hello方法的时候,hello方法入栈
- hello方法内有一个print方法,print方法入栈
- print方法执行完毕,print方法出栈
- hello方法执行完毕,hello方法出栈
- main方法执行结束,main方法出栈
- 线程结束,栈消亡
3. 本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
不过,《Java虚拟机规范》并没有强制规定本地方法栈一定要独立出来,因此有的Java虚拟机(如Hot-Spot虚拟机)直接把本地方法栈和虚拟机栈合二为一。
本地方法栈也会在栈深度溢出或栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
4. Java 堆
Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。
Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。
Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。
堆既可以被设计成事固定大小的,也可以被设计成事可扩展的,如果是可扩展的话,如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
以下内容存储在堆中:
public class JimTest {
// 1. 实例变量|存储在堆中
private int num1;
public static void main(String[] args) {
// 2. 普通对象|存储在堆中
List<String> list = new ArrayList<>();
// 3. 数组|存储在堆中
int[] ints = new int[10];
// 4. 字符串对象|存储在堆中(注意这里是字符串对象)
String str1 = new String("CSDN/Jim.kk");
// 字符串子面量|不不不不不不不存储在堆中(注意这里是不存在堆中)
String str2 = "CSDN/Jim.kk";
}
}
5. 方法区
方法区(线程共享),用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
在JDK7之前,程序员喜欢称呼方法区为“永久代”,但是永久代并不等价于方法区,只不过HotSpot的设计团队奖收集器的分带设计扩展至方法区,用永久代来实现方法区而已,这样就能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。但这并不是一个好主意,这种设计导致了Java应用更容易遇到内存溢出的问题(永久代有-XX:MaxPermSize的上限,即使不设置也有默认大小,而J9和JRockit只要没有触碰到进程可用内存的上限,例如32位系统中的4GB限制,就不会出问题),而且有极少数方法(例如String::intern())会因永久代的原因而导致不同虚拟机下有不同的表现。
在JDK 6的时候HotSpot开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了[1],到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了JDK 8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
注意,JDK8开始,不存在永久代,取而代之的是元空间!
并非进入该区域的内容就是永久存在了,该区域的内存也会进行回收,回收目标主要是针对常量池的回收和对类型的卸载。
以下是一些存储在方法区中的内容:
public class MethodAreaExample {
// 静态变量|存储在方法区中
private static int staticVar = 42;
// 构造方法|字节码存储在方法区中
public MethodAreaExample(int value) {
this.instanceVar = value;
}
// 静态方法|字节码存储在方法区中
public static void staticMethod() {
System.out.println("This is a static method.");
}
// 普通方法|字节码存储在方法区中
public void instanceMethod() {
System.out.println("This is an instance method.");
}
}
在开发中我们经常会写一些工具类,里面有非常多的静态方法,我们可以通过
类名.方法名()
的方式调用这些方法,同时我们也经常提到静态内容是属于类的,因此静态的信息成员属性、成员方法会被存储在方法区中,另外既然都叫方法去了,那么方法的字节码肯定也是存储在里面的。
6. 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种基本类型的常量、字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
注意,除了运行时常量池外,还有一个Class常量池,Class常量池是在编译的时候就能确定的,但是运行时常量池是动态的,Java并不要求常量一定只有编译期才会产生,也就是说,并非预置入Class文件中的常量池才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中。比如我们通过反射创建的常量等。
以下是存储在常量池中的举例
public class CompileTimeConstantPoolExample {
// 编译时常量池中的常量
private static final int CONSTANT_INT = 42;
private static final String CONSTANT_STRING = "CSDN/Jim.kk";
public static void main(String[] args) {
// 字符串字面量,存储在编译时常量池中
String str1 = "CSDN/Jim.kk";
// 比较字符串常量
System.out.println(str1 == "CSDN/Jim.kk"); // true,因为常量池中的字符串是同一对象
// 字符串对象,存储在堆中
String str2 = new String("CSDN/Jim.kk");
// 比较堆中的字符串对象和常量池中的字符串常量
System.out.println(str1 == str2); // false,因为str2是在堆中新创建的对象
}
}
以下是存储在运行时常量池中的举例
public class RuntimeConstantPoolExample {
public static void main(String[] args) {
// 字符串字面量,存储在运行时常量池中
String str1 = "CSDN/Jim.kk";
// 通过 new 创建的字符串对象,存储在堆中
String str2 = new String("CSDN/Jim.kk");
// 调用 intern() 方法,将字符串对象添加到运行时常量池中,但是由于常量池中已经存在CSDN/Jim.kk字符串,因此这里其实是与str1共享一个字符串
String str3 = str2.intern();
// 比较引用
System.out.println(str1 == str3); // true,因为 str3 是运行时常量池中的引用,与 str1 相同
}
}
7. 直接内存
直接内存并不是虚拟机内存的一部分,可以理解成是直接使用物理内存条上的地址。
由于在JDK1.4中新加入了NIO,引入了基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样的操作能够在一些场景中显著提高性能,因为避免了Java堆和Native堆中来回复制数据。
如果调用的内存超过物理机内存大小,依然会抛出OutOfMemoryError异常,毕竟没有了就没法继续申请了。
这里补充一嘴,在计算机体系结构中,32位和64位通常指的是处理器的数据总线宽度和寻址能力,这直接影响到系统可以支持的最大内存大小,一般来说32位可用的最大内存是232字节,即4GB,而64位的电脑可以使用264的内存,是17,179,869,184 GB,16,384 TB(但是一般都会受到CPU限制,比如现在的电脑虽然都是64位了,但是有的CPU最大仅支持32GB的内存条,有的最大支持64GB)。
对着一部分有疑问的话可以学一下NIO或者Netty。