1、基本介绍:
运行时数据区通常包括这5个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。
2、程序计数器:【PC寄存器,每个线程都有一个单独的PC寄存器】
- 是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都要依赖这个计数器来完成。
- 程序计数器(Program Counter Register),也有称作为PC寄存器。想必学过汇编语言的朋友对程序计数器这个概念并不陌生,在汇编语言中,程序计数器是指CPU中的寄存器,
它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址,一旦该指令被执行,pc寄存器会被更新至下条指令的地址)
,当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。 - 虽然JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是JVM中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示 执行哪条指令的。
- 由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。
- 在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。
- 由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
3、java虚拟机栈:【每个线程都会有一个自己的Java栈,线程安全】
- Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈。
- Java栈中存放的是一个个的栈帧(Stack Frame),
每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息
。 - 当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。
- 讲到这里,大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了以及为什么栈区的空间不用程序员去管理了(当然在Java中,程序员基本不用关系到内存分配和释放的事情,因为Java有自己的垃圾回收机制),这部分空间的分配和释放都是由系统自动实施的。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。【递归在自己方法还没执行完的时候又再次去调用自身方法,再次创建了一个栈帧】
- 由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java虚拟机栈,互不干扰。
- 下图表示了一个Java栈的模型:
3.1、局部变量表(Local Variable Table):
存储方法中的局部变量(包括在方法中声明的非静态变量)、函数形参。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用
。- 在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。
- 变量槽(Variable Slot):
- 局部变量表的容量以变量槽为最小单位,每个变量槽都可以存储32位长度的内存空间,例如boolean、byte、char、short、int、float、reference。
- 对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的Slot空间,也就是相当于把一次long和double数据类型读写分割成为两次32位读写。
- 对于引用对象实例来说,虚拟机都能从引用中直接或者间接的查找到对象的以下两点 :在Java堆中的数据存放的起始地址索引。所属数据类型在方法区中的存储的类型数据。
- 在方法执行时,虚拟机使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用。(在方法中可以通过关键字this来访问到这个隐含的参数)。其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot。
- 变量槽Slot复用:
- 为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,也就是说当PC计数器的指令指已经超出了某个变量的作用域(执行完毕),那这个变量对应的Slot就可以交给其他变量使用。
- 优点 : 节省栈帧空间。
- 缺点 : 影响到系统的垃圾收集行为。(如大方法占用较多的Slot,执行完该方法的作用域后没有对Slot赋值或者清空设置null值,垃圾回收器便不能及时的回收该内存。)
3.2、操作数栈:
- 如果需要执行任何中间操作,操作数栈作为运行时工作区去执行指令。
- 操作数栈和局部变量表一样,在编译时期就已经确定了该方法所需要分配的操作数栈的最大容量。
- 操作数栈的每一个元素可用是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型占用的栈容量为2。
- 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作。
- 例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的。
3.3、指向运行时常量池的引用:
- 因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
3.4、方法返回地址:
- 当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
- 当一个方法开始执行后,只有2种方式可以退出这个方法 :
- 异常退出:在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。
- 方法返回指令:执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。
- 无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。
- 一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。
- 而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
4、本地方法栈:【每个线程都会有一个自己的本地方法栈】
- 本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。
- 本地方法栈保存本地方法信息。对每一个线程,将创建一个单独的本地方法栈。
5、堆:【每个JVM同样只有一个堆区,线程共享,线程不安全,主内存】
- 如果是成员变量,或者定义在方法外对象的引用,它们存储在堆中。
Java中的堆是用来存储对象本身,它们相应的成员变量,以及数组(当然,数组引用是存放在Java栈中的)
。只不过和C语言中的不同,在Java中,程序员基本不用去关心空间释放的问题,Java的垃圾回收机制会自动进行处理。因此这部分空间也是Java垃圾收集器管理的主要区域。另外,堆是被所有线程共享的,在JVM中只有一个堆。-Xms
:初始堆大小即最小内存值-Xmx
:堆最大可用大小-Xmn
:新生代空间的大小-XXSurvivorRatio
:定义了新生代中Eden区域和Survivor区域(From幸存区或To幸存区)的比例,默认为8,也就是说Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10。
计算公式:
Eden = (R*Y)/(R+1+1)
From = Y/(R+1+1)
To = Y/(R+1+1)
R:SurvivorRatio比例
Y:新生代空间大小
6、方法区:【每个JVM只有一个方法区,线程共享,线程不安全】
- 方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,
存储了所有类级别数据(包括类的名称等)、静态变量、静态方法、常量、编译器编译后的代码、运行时常量池等
。 - 在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将原本放在永久代的字符串常量池移至堆中。
- 运行时常量池(Runtime Constant Pool)是方法区的一部分。
7、永久代:【PermGen】
- 方法区和“PermGen space”的区别:前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。
- 所以说永久代就是HotSpt虚拟机对于方法区的实现。
-XX:PermSize
:永久代的初始大小。-XX:MaxPermSize
:永久代的最大空间。
8、元空间:【Metaspace】
- JDK 1.8 中,已经没有方法区(永久代),而是将方法区直接放在一个与堆不相连的本地内存区域(Native Memory),这个区域被叫做元空间。
- JDK 1.8 中,这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace) 。
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize
:初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。-XX:MaxMetaspaceSize
:最大空间,默认是没有限制的。-XX:MinMetaspaceFreeRatio
:在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。-XX:MaxMetaspaceFreeRatio
:在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。
9、为啥移除永久代,引入元空间:
9.1、永久代缺点:
- 字符串存在永久代中,现实使用中易出问题, 由于永久代内存经常不够用或发生内存泄露,爆出异常” java.lang.OutOfMemoryError: PermGen“
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
9.2、引入元空间好处:
- 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。因此,我们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。最终用户可以为元空间设置一个可用空间最大值,如果不进行设置,JVM会自动根据类的元数据大小动态增加元空间的容量。