目录
1 程序计数器(Program Counter Register)
- 当前线程所执行的字节码行号指示器(逻辑);
- 通过改变计数器的值来选取下一条需要执行的字节码指令;
- 线程私有;
- 对Java方法计数,如果是Native方法则计数器的值为Undefined;
- 只是计数,不会发生内存泄漏;
2 Java 虚拟机栈
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
可以通过-Xss这个虚拟机参数来指定每个线程的Java虚拟机栈内存大小:
java -Xss512M HackTheJava
该区域可能抛出以下异常:
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
局部变量表和操作数栈:
- 局部变量表:包含方法执行过程中的所有变量
- 操作数栈:入栈、出栈、复制、交换、产生消费变量
public class JVMTest {
public static int add(int a ,int b) {
int c = 0;
c = a + b;
return c;
}
}
javap -verbose JVMTest
解读上述指令:
- stack = 2 说明栈的深度是 2 ;locals = 3 说明有 3 个本地变量 ;args_size = 2 说明该方法需传入 2 个参数;
- load 指令表示入操作数栈,store 表示出操作数栈;
- 当int取值-1 ~ 5采用iconst指令,取值-128 ~ 127采用bipush指令,取值-32768 ~ 32767采用sipush指令,取值 -2147483648~2147483647采用 ldc 指令;
执行 add(1,2),说明局部变量表和操作数栈的关系:
3 本地方法栈
- 本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务;
- 本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理;
4 堆
- 所有对象都在这里分配内存,是垃圾收集的主要区域(“GC 堆”);
- 现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:
- 新生代(Young Generation):主要存放新创建的对象,内存相对较小,垃圾回收比较频繁;
- 老年代(Old Generation):主要存放JVM认为生命周期比较长的对象(经过几次新生代的垃圾回收后仍然存在),内存相对较大,垃圾回收不很频繁;
- 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常;
- 可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值;
java -Xms3550M -Xmx3550M HackTheJava
Java 3种内存分配策略
-
静态存储:编译时确定每个数据目标在运行时的存储空间需求;
-
栈式存储:数据区需求在编译时未知,运行时模块入口前确定;
-
堆式存储:编译时和运行时模块入口前都无法确定,动态分配;
栈和堆的联系
引用对象、数组时,栈里定义变量保存堆中目标的首地址。
栈和堆的区别
- 物理地址:栈物理地址分配连续,性能好;堆内存物理地址分配对对象不连续,性能差些;
- 分配内存:栈分配的内存在编译期确定,大小固定;堆分配的内存是在运行期确定,大小不固定;堆内存远远大于栈;
- 存放内容:栈中存放局部变量,关注的是程序方法的执行;堆中存放对象和数组,关注的是数据的存储;
- 是否线程私有:栈属于某个线程私有;堆对所有线程可见,可被所有线程访问;
- 异常:栈扩展失败,会抛出 StackOverflowError;堆内存不足,会抛出 OutOfMemoryError;
5 方法区
- 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
- 和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常;
- 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现;
- HotSpot 虚拟机把它当成永久代来进行垃圾回收,但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常;
- 为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,元空间位于本地内存中,而不是在虚拟机内存中;
- 方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式;
- 在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中,元空间存储类的元信息,静态变量和常量池等放入堆中;
元空间(MetaSpace)与永久代(PermGen)的区别
元空间使用本地内存,而永久代使用 JVM 的内存。
元空间(MetaSpace)相比永久代(PermGen)的优势
-
常量池存在永久代中,容易出现性能问题和内存溢出;
-
类和方法的信息大小难以确定,给永久代的大小指定带来困难;
-
永久代会为 GC 带来不必要的复杂性;
6 运行时常量池
-
运行时常量池是方法区的一部分;
-
Class 文件中的常量池(编译期生成的字面量和符号引用)会在类加载后被放入这个区域;
-
除了有在编译期生成的常量,还允许动态生成常量,例如 String 类的 intern();