运行时数据区域
程序计数器
- 程序计数器可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的的字节码指令,它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都依赖这个计数器完成。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码的指令地址。如果正在执行的是本地(Native)方法,这个计数器的值应为空。
- 此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
JAVA虚拟机栈
- Java虚拟机栈是线程私有的,它的生命周期与线程相同。
- Java虚拟机栈里面存放很多栈帧。每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储 局部变量表,操作数栈,动态链接,方法出口等信息。 每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 在编译Java源码的时候,栈帧需要多大的局部变量表,需要多深的操作数栈已经被分析计算了出来,并写入到方法表的Code属性之中。换而言之,一个栈桢需要分配多少内存,并不会收到程序运行期变量数据的影响,而仅仅取决于程序源码和具体的虚拟机实现的占内存布局形式。
因为:比如递归的时候,都知道栈的深度受传进来参数的影响。正确想法是:一直递归的话,会一直调用这个方法,就会产生很多栈帧,但每个栈帧的操作数栈的深度是只和变量和程序代码有关系的。(举个例子分析下gcd函数的调用) 这样的话,递归次数过多,栈帧过多,导致StackOverflowError。 - 一个线程的方法调用链可能会很长(我猜这儿说的是递归),以Java程序员的角度来看,同一时刻,同一条线程里面,在调用堆栈的所有方法都同时处于执行状态。而对于执行引擎来讲,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧”,与这个栈帧所关联的方法成为“当前方法”。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
局部变量表
- 局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。其容量以变量槽为最小单位(《Java虚拟机规范》并没有明确指出一个变量槽应占用的内存空间的大小)。
- 当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。如果执行的是实例方法(没有被static修饰的方法),那局部变量表的第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数按照参数表顺序排列,占用从1开始的局部变量槽,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的变量槽。
- 为了节省栈帧所耗用的内存空间,局部变量表中的变量槽是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体。如果当前字节码PC计数器的值已经超过了某个变量的作用域,那么这个变量就可以交给其他变量重用。
副作用:某些情况下变量槽的复用会直接影响到系统的垃圾收集行为。(举例如下) - 1.类加载的准备阶段,进行内存分配的仅包括类变量(而不包括实例变量,实例变量将会在对象实例化时跟随对象一起分配在Java堆中) 准备阶段过后,类变量的初始值分别赋值为0,null或false等等。不过 准备阶段会将字段属性表中存在ConstantValue属性的字段初始化为ConstantValue属性所指定的初始值。
2.类加载的初始化阶段,就是执行类构造< clinit>()方法的过程。< clinit>()编译器自动收集所有类变量的赋值动作和静态语句块的语句合并产生的。(类加载具体见另一篇博客)
3.字节码执行过程时,遇到一条字节码new指令时,首先去检查这个指令的参数是否能在常量池定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析,初始化。如果没有,那必须先执行相应的类加载过程。类加载检查通过后,虚拟机为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。(怎么分配对象的内存以及类的加载过程具体见另一篇博客) new指令后一般会跟随invokespecial指令(Java编译器遇到new关键字的地方同时生成这两条字节码指令,但如果直接通过其他方式产生的则不一定如此),invokespecial指令用于调用实例构造器()方法、私有方法和父类中的方法。new指令之后会接着执行()方法,按照程序员的意愿对对象初始化。调用无参构造函数的话就是0、null、false等等。
总而言之:局部变量表没有赋初始值的话,访问会报错。因为他会在字节码文件的方法表的Code属性中的code用来存储Java源程序编译后生成的字节码指令。里面指令iconst等指令(假如没有给局部变量赋值 就不会有这些指令 但也会为之分配相应的变量槽滴) 的意思是 直接将代码中的局部变量的值放入变量槽中。全程并没有对局部变量初始化的(调用方法时虚拟机分配栈帧,里面有局部变量表,但并没有初始化这部分内存成0、null、false等等)
操作数栈
- 在做算术运算的时候是通过将运算涉及的操作数压入栈顶后调用运算指令来进行的。
- 调用其他方法的时候是通过操作数栈顶来进行方法参数的传递。
- 方法有返回值的时候,把返回值放入操作数栈顶,并返回。
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。(我觉着这里是因为 重写 运行的时候才知道调用哪个方法。而静态解析时,调用这个方法本来指令参数都会有指向常量池的该方法的引用。动态连接时,生成的invokevirtual指令后面跟的 一个指向运行时常量池 被什么类修饰的这个类的所属的方法的引用 是无效的,所以栈帧需要一个位置存储 一个该指令运行时解析完成后返回的直接引用()这个才是正确的引用 )
方法返回地址
- 一个方法使用异常出口的方式退出,是不会给它的上层调用者提供任何返回值。
- 方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。一般来说,方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器的值,而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。
本地方法栈
本地方法栈与虚拟机栈的区别只是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)服务。
Java堆
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。Java世界里“几乎”所有的对象实例都在这里分配内存。
方法区
方法区用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。因此,我们得到了结论,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现。
- 在JDK1.7之前,hotspot虚拟机对方法区的实现为永久代。
- 在JDK1.7 字符串常量池、静态变量 被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代。
- 在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池、静态变量还在堆中, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)
- ----- 运行时常量池也是方法区的一部分。Class文件的其中一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容在类加载后存放到方法区的 运行时常量池。
-----运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生。运行期间也可以将新的常量放入池中。如 String类的intern()方法。(挖坑) - 比较常见的是:方法区中有类的虚方法表。
原因:动态分派的方法的盘本选择过程需要运行时在接受这类型的方法的元数据中搜索合适的目标方法,因此,Java虚拟机实现基于执行性能的考虑,真正运行时一般不会如此频繁的去反复搜索类型元数据。面对这种情况,一种基础而常见的优化手段是为类型在方法区中建立一个虚方法表,与此对应的,在invokeinterface执行时也会用到接口方法表,使用虚方法表索引来代替元数据查找以提高性能。为何使用元空间替代永久代
个人觉得有以下几点原因:- 持久代很难进行调优:JVM加载的class的总数,方法的多少(虚方法表不是大多也放在方法区嘛) 等都很难确定,因此对永久代的大小的制定难以确定。太小的永久代导致永久代内存溢出,太大的永久代则容易导致虚拟机内存紧张。
- 降低GC难度:方法区的垃圾收集主要回收两部分的内容:废弃的常量和不再使用的类型。而判定一个类型是否属于“不再被使用的类”的条件比较苛刻。放在永久代加大了GC的难度。而用元空间实现的话:每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元空间。当一个类加载器被垃圾收集器标记为不再存活,其对应的元空间会被回收,在元空间的回收过程中没有重定位和压缩等操作。但是元空间内的元数据会进行扫描来确定Java的引用。
为什么字符串常量池要放到堆区
原因:开发中会有大量的字符串被创建,永久代满了就会发生full gc。导 致gc停顿时间太长,将它放到堆里,可及时回收内存。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。