Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁时间,有些区域随着虚拟机进程的启动而存在,有些区域依赖用户线程的启动和结束而建立和销毁。
Java 虚拟机运行时数据区:
程序计数器:
是一块较小的内存空间,可以看做使当前线程所执行字节码的行号,字节码解释器工作时就是通过改变这计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复都需要依赖这个计数器。
如果执行的是一个 Java 方法,这个计数器记录的是正在执行的字节码指令的地址;若是 Java 的 Native 方法,计数器值为空。此外,这个区域是 Java 虚拟机规范中唯一一个没有 OutOfMemoryError 异常的区域。
Java虚拟机栈:
和程序计数器一样是线程私有的,生命周期与线程相同,它所描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表、操作栈数、动态链接、方法出口等),压入栈中,当每一个方法执行完,对应的栈帧就会从虚拟机栈中弹出。
局部变量表中存放编译期中已知各种基本的数据类型、对象的引用(指向对象的引用指针、句柄或者其相关的地址)和 returnAddress 类型。局部变量表需要的内存空间在编译期间完成分配,当进入一个方法时,栈帧中所需要分配多大的局部变量表空间是确定的,运行期间其大小不会改变。
Java 虚拟机规范中在这规定了有两个异常:StackOverflowError(请求的栈深度超过栈的深度时) 和 OutOfMemoryError(可动态扩展但内部空间不足的时候)。
本地方法栈:
与虚拟机栈发挥的作用非常相似,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
在虚拟机规范中对本地方法栈中方法使用的语言、数据结构并没有强制规定,虚拟机可以自由的实现它。HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一。可能会抛出的异常和虚拟机栈一样。
Java 堆(Heap):
它是虚拟机管理的内存中最大的一块地方。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动是创建。其目的是就会存放 Java 对象的实例(几乎所有实例在这里分配内存)。
它也是垃圾收集器管理的主要区域。现在收集器基本都采用分代算法,Java 堆可以分为新生代和老年代,再细一点新生代有 Eden 空间、From Survivor 空间、To Survivor 空间。
在这里提一下永久带 PermGen(存放类的元数据,在heap中,不是所有虚拟机都有永久带概念),在 HotSpot VM 中,存在于 JDK 8 之前的版本,在 JDK 8 以及之后,永久带被移除,相对引入了一个新的内存区域(本地内存中)叫 Metaspace(元空间)。
从 Java 8 中删除 PermGen 的原因:
PermGen有很多缺点:
- 启动时固定大小:难以调整,所需的大小取决于类的数量,方法的大小,常量池的大小。
- 内部热点类型是 Java 对象:可以完整的 GC 进行移动,不透明的,不是强类型且难以调试的元数据一起移动。
- 简化完整集合:为每个集合添加了用于元数据的特殊迭代器。
- 现在,无需并发GC暂停,就可以同时取消分配类数据。
- PermGen 限制的未来改进实现。
MetaSpace 的优势:
在性能和内存管理方面,MetaSpace 有很多优势:
- 利用Java Specification属性:类和关联的元数据生存期与类加载器匹配。
- 每个加载器存储区域:元空间
- 仅线性分配。
- 没有单独的回收(重定义类和类加载失败除外)
- 没有GC扫描或压缩。
- 没有元空间对象的重定位。
元空间调整
要设置最大元空间大小,可以使用-XX:MaxMetaspaceSize标志,并且默认情况下根据机器内存限制是无限的。如果未指定此最大限制标志,则Metaspace将根据运行时的应用程序需求动态调整大小。
此更改将在将来实现其他优化和功能
- 应用程序类数据共享。
- 年轻的收藏优化,G1类卸载。
- 元数据大小减少和内部JVM占用空间项目。
方法区:
和Java堆一样,是各个线程共享的内存区域,用于存储虚拟机加载的类信息、常量、静态常量、即时编译后的代码等数据。Java虚拟机规范把方法区(Non-Heap)和Java堆区分开来。内存无法满足时会跑出OutOfMemoryError。
运行时常量池:
方法区的一部分。Class文件除了有累的版本、字段、方法、接口等描述信息外,还有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。