Java虚拟机(一)Java运行时数据区域

Java虚拟机所管理的内存包括以下几个运行时数据区域

其中,程序计数器、虚拟机栈、本地方法栈是线程私有的,堆、方法区是线程共享的。

1.程序计数器

  程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。
  在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能回到正确的执行位置,每个线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,这类内存区域称为线程私有的内存

2.虚拟机栈

  Java虚拟机栈也是线程私有的,它的生命周期与线程相同。每个方法在执行的时候会在虚拟机栈中创建一个栈帧,用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

  大部分都知道java内存分为,这个分法比较粗糙,这里所指的”栈“其实就是现在所讲的虚拟机栈,或者是虚拟机栈中的局部变量表部分。

2.1 局部变量表

  局部变量表是一组变量值存储空间,用于存放方法参数和方法内的局部变量,包括编译期可知的各种基本数据类型(boolen、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身)。
  虚拟机为局部变量分配内存所使用的最小单位是Slot(槽),其中64位的long和double类型的数据会占用2个局部变量空间(Slot),其余数据类型只占一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

2.2 操作数栈

  操作数栈和局部变量表一样,也被组织成一个数组,但与前者不同的是,它不是通过索引来访问,而是通过标准的栈操作来访问。比如,如果某个指令把一个值压入到操作数栈中,稍后另外一个指令就可以弹出这个值来使用。
  虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如,iadd指令就要从操作栈中弹出两个整数,执行加法运算,其结果又压回操作数栈中。下面的例子,她演示了虚拟机是如何利用局部变量表操作数栈把两个int类型的局部变量相加,再把结果保存到第三个局部变量的。

1
2
3
4
iload_0    // push the int in local variable 0 onto the stack  
iload_1 // push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2; // pop int, store into local variable 2

  在这个字节码序列中,前两个指令iload_0和iload_1将存储在局部变量表中索引为0和1的整数压入操作数栈中,其后iadd指令从操作数栈中弹出两个数并且相加,再将结果压入操作数栈。第四条指令istore_2则从操作数栈总弹出一个数并把她存储到局部变量表索引为2的位置。
  下图表述了这个过程中局部变量表和操作数栈的变化情况,图中没有使用过的局部变量区和操作数栈区域以空白标识。

2.3 动态链接

2.4 方法出口

3.本地方法栈

  本地方法栈和虚拟机栈所发挥的功能是非常相似的,她们之间的区别不过是虚拟机栈为执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用的Native方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现她。甚至有的虚拟机将本地方法栈与虚拟机栈合二为一。

4.堆

  堆是Java虚拟机所管理的内存中最大的一块,是被所有线程共享的,在虚拟机启动的时候创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配,还有数组。
  Java堆是垃圾收集器管理的主要区域,java堆可以细分为:新生代和老年代,再细致一点,新生代可以分为Eden空间、Survivor1空间、Survivor2空间。从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)

5.方法区

  方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据。在虚拟机规范中,方法区是在虚拟机启动的时候创建,虽然方法区是堆的逻辑组成部分,但却有一个别名叫Non-Heap(非堆)。
  很多人把方法区和永久代混合,但两者并不等价,方法区和永久代的关系很想java中接口和类的关系。HotSpot把GC分代收集扩展至方法区,也可以说是用永久代来实现方法区,这样,HotSpot的垃圾收集器就可以像管理java堆那样管理方法区而不用专门为方法区编写内存管理代码的工作。
  自HotSpot在1.8之后,已经取消了永久代,改为元空间。

元空间

  元空间没有使用堆内存,而是与堆不相连的本地内存区域。所有理论上系统可以使用的内存有多大,元空间就有多大,所以不会存在永久代存在时的内存溢出问题。
  永久代的调优时比较困难的,虽然可以设置永久代的大小,但比较难确定一个合适的值,因为受影响的因素有很多,比如类数量的多少、产量数量的多少等,太小容易发生内存溢出,太大又会浪费空间,而且堆空间也会变小(假设java程序所使用的总内存大小不变)。因此,这项改造是很有必要的,其它一些方面也可以进行优化。