JAVA程序运行与虚拟机之上,运行时需要内存空间。
JAVA虚拟机执行JAVA程序的过程中会把它管理的内存划分为不同的数据区域方便管理。
虚拟机管理内存数据区域划分如下图:
数据区域分类:
- 元空间 (MateSpace) (原来的方法区)
- 虚拟机栈 (VM Stack)
- 本地方法栈 (Native Method Stack)
- 堆 (Heap)
- 程序计数器 (Program Counter Register)
- 直接内存 (Direct Memory)
- 元空间与直接内存,都属于本地内存
说明:
1. 程序计数器
行号指示器,字节码指令的分支、循环、跳转、异常处理、线程恢复(CPU切换),每条线程都需要一个独立的计数器,线程私有内存互不影响,该区域不会发生内存溢出异常。
2. 虚拟机栈(支持虚拟机进行方法调用与方法执行的数据结构)
是线程私有的,声明周期与线程相同,虚拟机栈是Java方法执行的内存模型,每个方法被执行时都会创建一个栈帧,即方法运行期间的基础数据结构,栈帧用于存储:局部变量表(Local Stack Frame)、操作数栈(Operand Stack)、动态链接(Dynamic Linking)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在堆部分会谈到)的引用(Reference to runtime constant pool)、方法出口(返回地址(Return Address))等,每个方法执行中都对应虚拟机栈帧从入栈到处栈的过程。
当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么在使用递归方法的时候容易导致栈内存溢出的现象了
一个线程中方法调用可能很长,很多方法都处于执行状态。对于执行引擎来说,只有处于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与之相关联的方法称为当前方法(Current Method)
异常类型:stackOverFlowError 线程请求栈深度大于虚拟机允许深度
OutOfMemory 内存空间耗尽无法进行扩展
虚拟机栈是为虚拟机执行java方法服务(也就是字节码)
3. 本地方法栈
与虚拟机栈类似,虚拟机栈为Java程序服务,本地方法栈支持虚拟机的运行服务,具体实现由虚拟机厂商决定,也会抛出 stackOverFlowError、OutOfMemory异常。
功能与java虚拟机栈十分类似,区别在于,本地方法栈为虚拟机使用到的native方法服务(调用本地lib库接口)
本地方法栈是虚拟机使用的native方法
4. 堆
是虚拟机管理内存中最大的一部分,被所有线程共享,用于存放对象实例(对象、数组),物理上不连续的内存空间,由于GC收集器,分代收集,所以划分为:新生代 Eden、From SurVivor空间、To SurVivor空间,allot buffer(分配空间),可能会划分出多个线程私有的缓冲区,老年代。JVM8中把运行时常量池移到堆区进行存储
JDK7/JDK8包含 运行时常量池
堆空间内存分配(默认情况下)
老年代 : 三分之二的堆空间
年轻代 : 三分之一的堆空间
eden区: 8/10 的年轻代空间
survivor0 : 1/10 的年轻代空间
survivor1 : 1/10 的年轻代空间
5. 元空间(原来的方法区)
与堆一样属于线程共享的内存区域,用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码(动态加载OSGI)等数据。理论上属于java虚拟机的一部分,为了区分开来叫做 Non-Heap非堆。
Class的版本、字段、接口、方法等,及编译期生成的各种字面量、符号引用,编译类加载后存放在该区域。
这个区域可以选择不进行垃圾回收,该区域回收目的主要是常量池的回收,及类型的卸载class,内存区不足时会抛出OutOfMemory异常
运行时常量池:
String数据类型的时候已经详细叙述过,简单的来class文件在编译后除了存储一些类的版本、字段、方法、接口等元数据信息外,还有一部分信息是常量池,这个常量池我们称之为“静态常量池”,只是作为一种持久化数据存储在硬盘上,代表编译期生成的各种字面量和符号引用(最常见的就是字符串常量),那么这类信息被加载到内存中就会以运行时常量池的形式存在内存中,JDK7以前这类信息被存储在方法区,但是JDK7/JDK8都已经移到了堆区。这类数据变量的好处简单来说就是如果堆区中已经存在一个数据变量,即使再创建一个这样的变量,那么JVM将会直接指向已经创建好的数据,而不会再分配内存区域,这样一方面加快数据的创建,另一方面节省内存空间!但是实际上的机制要复杂一些,可以参考我之前讲述的String类去理解!(引用)
6. 直接内存
直接内存不属于虚拟内存区域,是一种基于通道与缓冲区的IO方式,可以使用本地函数直接分配堆外内存,在堆中存储引用的外部内存地址,通过引用完成对直接引用内存的操作,1.4之后提供的NIO显著提高效率,避免了堆内存与Native内存的来回复制操作,不受虚拟机内存控制,会抛出OUtOfMemory异常。
NIO 中的DirectBuffer会用到直接内存,在某些场景,可以有效提高效率
7.类信息的存储位子
全局变量:存储于堆中,是对象私有
静态变量:静态成员变量、常量池由方法区迁移到堆
8.类静态成员变量的存储位置及JVM的内存划分: (引用)
类静态成员变量的访问方式,但静态成员变量存储在哪里呢?在网上查阅不少资料,发现好多内容都是过时的了,其中主流观点是静态成员变量存放在方法区。JDK8之前,静态成员变量确实存放在方法区;但JDK8之后就取消了“永久代”,取而代之的是“元空间”,永久代中的数据也进行了迁移,静态成员变量迁移到了堆中(方法区是JVM的规范,永久代是方法区的具体实现)。
注:静态成员变量、常量池由方法区迁移到堆