Java虚拟机之运行时数据区域
导语:相信如果大家学过Java,那么应该对JVM多少都会了解一点,无论是在面试中还是在我们实际应用的时候,但是我们知道随着JDK的不断更新,他的数据区域也在相应的发生变化,本片博客将主要针对于JDK1.8之前的版本和JDK1.8之前的数据区域进行详细的介绍
根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)先来一张模型图JDK1.6的:
注:方法区是虚拟机规范中对运行时数据区划分的一个内存区域,不同的虚拟机厂商可以有不同的实现,而HotSpot虚拟机以永久代来实现方法区,所以方法区是一个规范,而永久代则是其中的一种实现方式
程序计数器
程序计数器(也可以称作为PC寄存器)是一块较小的内存空间,他可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条执行的字节码指令,比如分支、异常、循环、跳转、线程恢复等功能都需要这个计数器
每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为"线程私有"的内存。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是native方法,这个计数器则为空。
Java虚拟机栈
Java虚拟机栈是线程私有的,他的生命周期与线程相同。每个Java方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M
在Java虚拟机规范中,对这个区域规定了两种异常状况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,会抛出StackOverflowError(栈溢出)异常
- 如果虚拟机栈可以动扩展(当前大部分的Java虚拟机都可以动态扩展,但是Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError(内存溢出)异常
本地方法栈
本地方法栈与Java虚拟机栈是相似的,他们之间的区别就是Java虚拟机栈是为虚拟机执行Java(也就是字节码)服务,而本地方法栈是为本地的native方法服务
本地方法一般是用其他语言(C,C++,汇编…)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特殊处理
与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常
堆
Java堆是Java虚拟机所管理内存中最大的一块,是可以被线程共享的一块区域,在虚拟机启动时被创建,几乎所有的对象实例都在这里分配内存(不是那么绝对,因为随着JIT编译器发展,栈上分配和标量替换优化技术导致一些微妙的变化发生,再次不过多解释)
Java堆是垃圾回收管理(GC)的主要区域,现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成新生代和老年代两块,再细致还可以分为Eden空间、From Survivor空间和To Survivor空间等
命令行上执行java -XX:+PrintFlagsFinal -version
输出结果有好多行,这里取出其中有关的两个参数:
[Global flags]
.
.
//新生代Eden/Survivor空间的初始比例
uintx InitialSurvivorRatio = 8 {product}
//Old区和Young区的内存比例
uintx NewRatio = 2 {product}
.
.
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
-
老年代:默认情况下三分之二的堆空间
-
年轻代:默认情况下三分之一的堆空间
- Eden区:8/10的年轻代
- From Survivor:1/8的年轻代
- To Survivor:1/8的年轻代
Java堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常;可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
java -Xms1M -Xmx2M HackTheJava
方法区
- 方法区与Java堆一样,是各个线程共享的区域,他用于存储已被虚拟机加载的类信息,常量,静态变量,及时编译器编译后的代码等数据
- 和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常;对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现
- HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中
- 方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中
运行时常量池
- 运行时常量池是方法区的一部分
- Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域
- 运行时常量池相对于Class文件中的常量池的另外一个特性是具备动态性,除了在编译期生成的常量,在运行期间也可以将新的常量放入池中,从而动态生成,例如 String 类的 intern()
直接内存
- 直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分也被频繁使用,而且也可能会发生StackOverflowError异常出现
- 在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据
java1.8内存模型图
1.8同1.7或者之前比,最大的差别就是:(元空间)元数据区取代了永久代,元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
- Jdk1.6及之前: 有永久代, 常量池1.6在方法区
- Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池1.7在堆
- Jdk1.8及之后: 无永久代,常量池1.8在元空间
根据上面情况在此提出两个问题:
- 为什么(元空间)元数据区要替换永久代?
- 现在的元数据区是什么样的?
解决这两个问题之前我们先了解下方法区和永久代的关系:
好多人认为方法区等同于永久代,永久代既然没了,方法区也就没了。但其实方法区只是一种逻辑上的概念,永久代指物理上的堆内存的一块空间,这块实际的空间完成了方法区存储字节码、静态变量、常量的功能等等,因此,现在元空间也可以认为是新的方法区的实现了其实方法区和永久代的关系,就像Java中的类和接口,类实现接口,而永久代就是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式
为什么(元空间)元数据区要替换方永久代?
- 元空间没有使用堆内存,而是与堆不相连的本地内存区域。所以,理论上系统可以使用的内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题
- 永久代的调优是很困难的,虽然可以设置永久代的大小,但是很难确定一个合适的大小,因为其中的影响因素很多,比如类数量的多少、常量数量的多少等
- 永久代中对象的位置也会随着一次full GC发生移动,比较消耗虚拟机性能,而元空间里的对象的位置是固定的
- HotSpot虚拟机的每种类型的垃圾回收器都需要特殊处理永久代中的元数据,导致回收效率偏低
- 将元数据从永久代剥离出来,不仅实现了对元空间的无缝管理,还可以简化Full GC以及对以后的并发隔离类元数据等方面进行优化
- 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致
现在的元数据区是什么样的?
- 其实现在的元数据区的存储内容和原来的方法区或者说是永久代的内容是差不多的,比如一些常量池,类信息,还有class的static变量等等,详细也可以查看更多内容
结语:到此针对于JVM运行时数据区域就基本介绍完了,对于JVM更详细的内容博主会在之后的博客中继续讲解,本片博客如有不正之处,请多指正!