跳到主要内容

01、调优实战 - 运行时数据区域

1 运行时数据区域

 

1.1 程序计数器

一块较小的内存空间(线程私有的内存);

1、因为JVM是多线程运行的,所以会有多个线程来并发的执行不同的指令;

2、因此每个线程都会有自己的一个程序计数器,用来记录当前线程执行到了哪一条字节码指令了;字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令:分支、循环、跳转等。

3、唯一一个在Java虚拟机规范中没有任何OutOfMemoryError的区域

1.2 虚拟机栈

线程私有的;每个(java)方法在执行的时候会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法从调用到执行完成的过程,就是栈帧在栈中入栈到出栈的过程。

局部变量表

存放编译期可知的基本数据类型、对象引用、returnAddress类型。所需的内存空间会在编译期完成分配,进入一个方法时在帧中的局部变量空间是完全确定的,不会运行时改变。

字节码程序可以将计算的结果缓存在局部变量表之中;JVM将局部变量表当成一个数组,依次存放this指针(仅非静态方法)、所传入的参数、以及字节码中的局部变量;

示例:

public void foo(long l, float f) {
             
               
 {
             
               
   int i = 0;
 }
 {
             
               
   String s = "Hello, World";
 }
}
    

这是一个实例方法,因此局部变量表数组的第0个元素存放this指针;

第一个参数是long类型,所以数组的第1,2个元素存放所传入的long类型参数的值;

第三个元素是float类型,所以数组的第3个元素存放所传入的float类型参数的值;

方法中的两个代码块中有两个局部变量i和s,由于局部变量的生命周期没有重合之处,因此Java编译器可以将它们编排到同一个元素中;即数组的第4个元素为i或者s;

 

存储在局部变量表中的值,需要加载至操作数栈中,才能进行计算,得到计算结果后再存储至局部变量表数组中;这些加载、存储指令是区分类型的:

 

操作数栈

  • 在解释执行过程中,每当为Java方法分配栈帧时,JVM需要开辟一块额外的空间作为操作数栈,来存放计算的操作数及返回结果

  • 具体来说:执行指令之前,JVM要求该指令的操作数已被压入操作数栈中;在执行指令时,JVM会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中;

示例:int类型的 1 + 2:

在执行iadd指令之前,栈顶的两个元素分别为1和2:
*  

在指令执行时,iadd指令将弹出这两个元素,并将求得的和int值3压入栈中:

 

由于iadd指令只消耗栈顶的两个元素,因此对于离栈顶距离为2的元素?,iadd指令并不关心它是否存在,更不会对其进行修改。

一般情况下,操作数栈的压入弹出操作都是一条条指令完成的,唯一的例外情况是在抛异常时,JVM会清除操作数栈上的所有内容,而后将异常实例的引用压入操作数栈上。

  • 线程请求的栈深度大于虚拟机允许的深度,抛出SatckOverFlowError异常;虚拟机动态扩展时,无法申请到足够的内存,抛出OutOfMemoryError异常。

1.3 本地方法栈

  • 为虚拟机中使用到的Native方法服务,对本地方法栈中方法使用的语言、使用方式、数据结构没有强制规定,虚拟机可自由实现。
  • 占用的内存区大小也不是固定的,可以根据需要动态的扩展或收缩。

1.4 Java堆

  • Java堆是Java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时就创建;
  • 存放对象实例,几乎所有新创建的对象实例都在堆上分配。也是垃圾收集器管理的主要区域;
  • 无法满足内存分配需求时,抛出OutOfMemoryError。
  • Java堆可以说是我们JVM中最重要的一块内存区域,后续的各种JVM调优等也跟Java堆密切相关,将在后面的文章中进行详细介绍;

1.5 方法区

  • 方法区是一个“概念”,是Java虚拟机规范中定义的概念,一个逻辑上“非堆”的运行时数据区;

  • 用于存储 类信息、常量(static final修饰)、静态变量、即时编译器编译后的代码 等数据;

  • 方法区的实现:

  • 在Java7以前,HotSpot虚拟机中,方法区的实现被称为“永久代(PermGen Space)”(其他类型虚拟机不存在);因为在物理上方法区使用的是由JVM开辟的堆内存,所以和Java堆共享内存且由垃圾收集器统一分配和管理;

  • 字符串常量池(String Pool)此时放置在 永久代中;

  • 在Java7的时候,开始了移除永久代的工作;将存储在永久代的部分数据转移到了Java Heap或者是Native Heap;

  • 符号引用(Symbols)转移到了native heap;

  • 字面量(interned strings)转移到了java heap;

  • 静态变量/常量 转移到了java heap

  • 字符串常量池(String Pool)也转移到了 java heap

在Java8以后,Hotspot完全改变了方法区的实现;将原本由JVM管理内存的方法区内存移到了虚拟机以外的计算机本地内存(也就是直接内存),称为元数据空间(Metaspace);即现在的方法区实际存在在元空间,不再和Java堆共享内存了;

字符串常量池(String Pool)仍然保持在 java heap;

方法区也是被所有线程共享的内存区域;

无法满足内存分配需求时,抛出OutOfMemoryError。

  • 移除永久代的原因
  • 永久代的大小受限(-XX:MaxPermSize=N),在JDK6中字符串常量池还处于永久代中,容易发生内存溢出 java.lang.OutOfMemoryError: PermGen;
  • 为了 JRockit和Hotspot的融合,因为除了Hotspot外其他虚拟机都没有永久代的概念;(出处:JDK文档);
  • 另外,把永久代改成Metaspace 有利于垃圾回收器的发展和优化;

1.5.1 运行时常量池

  • 每个class文件中有一个class常量池(constant_pool table),它里面存放了

  • 类的版本、字段、方法、接口等描述信息;

  • 编译期生成的各种字面量(Literal)和符号引用(Symbolic References);

  • 当执行类加载的时候,jvm会将class常量池中的内容加载到 运行时常量池中;所以 运行时常量池和class文件常量池是一一对应的,它就是基于class文件的常量池来构建的;

  • 运行时常量池位于方法区中,是另外的一块存储区域;更细致一点的描述:

方法区存放着类信息、运行时常量池、常量、静态变量(JDK1.8移入了 java heap);

运行时常量池中存放着 字面量和符号引用;

 

1.5.2 直接内存(堆外内存)

  • 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
  • 在JDK 1.4中新加入了NIO类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作;这样避免了在Java堆和Native堆中来回复制数据,可以提供更佳的性能;
  • 直接内存的分配不会受到Java堆大小的限制,但是会受到本机总内存的限制。
  • 直接内存会在后面的文章中进行详细介绍