JVM与GC调优(三)-运行时数据区篇

本文最后更新于:2024年4月22日 下午

了解内存结构中相关信息,程序计数器、虚拟器栈、本地方法和本地接口栈、堆、方法区

概述

整个JVM构成里面,由三部分组成:类加载系统、运行时数据区、执行引擎

按照线程使用情况和职责分成两大类

线程独享 (程序执行区域)

  • 不需要垃圾回收

  • 虚拟机栈、本地方法栈、程序计数器

线程共享 (数据存储区域)

  • 垃圾回收

  • 存储类的静态数据和对象数据

  • 堆和方法区

一、堆(Heap)

核心概述

Java堆在JVM启动时创建内存区域去实现对象、数组与运行时常量的内存分配,它是虚拟机管理最大的,也是垃圾回收的主要内存区域

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
  • Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间
  • 堆内存的大小是可以调节的
  • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区( Thread Local Allocation Buffer,TLAB)。

堆空间大小的设置

新生代与老年代的参数设置

二、虚拟机栈/栈(Stack)

Java虚拟机分堆区、栈区、方法区

核心概述

Java 虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应这一次次的 Java 方法调用。

作用

主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果、并参与方法的调用和返回

特点

  • Java虚拟机栈也是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)
  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
     - 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;

  (当前大部分JVM都可以动态扩展,只不过JVM规范也允许固定长度的虚拟机栈)

  • Java虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧。

栈的单位:栈帧(Stack Frame)

每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在

方法与栈帧的关系:

栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息

image-20220928204912987

栈帧的内部结构:

image-20220928205653573

栈的FILO原理

image-20220928211031108

JVM直接对Java栈的操作只有两个:

  • 每个方法执行,伴随着压栈(push)
  • 执行结束后的出栈(pop)

遵循先进后出后进先出原则

栈帧中的信息

每个栈帧存在以下信息

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法返回地址
  • 一些附加信息

*局部变量表(本地变量表【Local veriables】)

定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量

这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型

image-20220928220835279

注意:

1、非静态方法的话 会有 this 变量 在 index 为0 的位置

2、doublelong 占据两个 slot位 ,一个是 4位

3、栈帧当中的局部变量表的槽位是可以被重复利用的,出了作用域就会被销毁,后面定义的变量就会占据被销毁的变量的位置

4、局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

*操作数栈(Operaand Stack)

也叫表达式栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。并非采用索引访问。

作用:

主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间;

public void testAddOperation(){
    byte i = 15;
    int j = 8;
    int k = i + j;
}

image-20220928224543209

image-20220928224548943

image-20220928224554113

image-20220928224600714

image-20220928224605206

image-20220928224610548

image-20220928224618969

image-20220928224622772

动态链接(Dynamic Linking)

指向运行时常量池的方法引用

  • 每一个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
  • 在 Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在 class 文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

相关问题

栈 jdk5以后默认都是1M,栈空间比较小

栈中存在垃圾回收吗?

栈中不存在垃圾回收

栈中可能抛出的异常是什么?

Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。

  • 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError 异常。

  • 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutOfMemoryError 异常。

如何设置栈内存的大小?

-Xss size (即:-XX:ThreadStackSize) 一般默认为512k-1024k,取决于操作系统。

三、本地方法栈

本地方法栈虚拟机栈相似,区别就是虚拟机栈为虚拟机执行Java服务(字节码服务),而本地方法栈为虚拟机使用到的Native方法(比如C++方法)服务**。

简单地讲,一个Native Method就是一个Java调用非Java代码的接口。或者更为底层的接口

作用

调用native方法直接与操作系统与硬件打交道。多线程底层就是这么实现的

四、 方法区

方法区(Method Area)是可供各个线程共享的运行时内存区域

方法区本质上是Java语言编译后代码存储区域,它存储每一个类的结构信息,例如:运行时常量池、成员变量、方法数据、构造方法和普通方法的字节码指令等内容。很多语言都有类似区域。

五、 字符串常量池

class常量池:一个class文件只有一个class常量池

  • 字面量:数值型(int、float、long、double)、双引号引起来的字符串值等

  • 符号引用:Class、Method、Field等

运行时常量池:一个class对象有一个运行时常量池

  • 字面量:数值型(int、float、long、double)、双引号引起来的字符串值等

  • 符号引用:Class、Method、Field等

字符串常量池:全局只有一个字符串常量池

  • 双引号引起来的字符串值

字符串常量池存储

为了提高匹配速度, 即更快的查找某个字符串是否存在于常量池 Java 在设计字符串常量池的时候,还搞了一张StringTable, StringTable里面保存了字符串的引用。StringTable类似于HashTable(哈希表)。在JDK1.7+,StringTable可以通过参数指定 -XX:StringTableSize=5555

特点

对象使用双引号字符串赋值和使用new String等的区别

  • 单独使用””引号创建的字符串都是常量,编译期就已经确定存储到String Pool中。

  • 使用new String(“”)创建的对象会存储到heap中,是运行期新创建的。

  • 使用只包含常量的字符串连接符如”aa”+”bb”创建的也是常量,编译期就能确定已经存储到StringPool中。

  • 使用包含变量的字符串连接如”aa”+s创建的对象是运行期才创建的,存储到heap中。

  • 运行期调用String的intern()方法可以向String Pool中动态添加对象

六、程序计数器(Program Counter Register)

PC 寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码

  • 它是一块很小的内存空间,几乎可以忽略不记。也是运算速度最快的存储区域。

  • 在 JVM 规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。

  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行 native方法,则是未指定值(undefined)(因为PC寄存器是java层面的,本地方法栈是C/C++层面的)。

  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成;

  • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令;

  • 它是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

为什么要设置成 线程私有的 ?

CPU需要不停的切换线程,在切换回来的时候,A线程的PC寄存器里就记录了 A线程执行到哪里了 B线程的PC寄存器里就记录了 B线程执行到哪里了,所以需要 是线程私有的。


JVM与GC调优(三)-运行时数据区篇
https://hyq965672903.gitee.io/posts/8be32240.html
作者
灼华
发布于
2022年9月25日
许可协议