JVM学习-运行时数据区域

在冷风中我凌乱了半小时,只因健康码刷不出来

目录

JVM学习-运行时数据区域

前言

本系列文章梳理了对《深入理解Java虚拟机》和《Java虚拟机规范(Java SE 8版)》两本书的学习内容。

其中本文对JAVA运行时的数据区的基础知识知识进行整理。我们如果要对程序内存占用高的问题进行分析,首先我们需要了解具体是什么数据导致内存占用高,然后对具体的问题再具体分析。

运行时数据区

Java虚拟机在执行Java程序的过程中,会把它所管理的内存划分以下几个区域:程序计数器、Java堆、方法区、虚拟机栈 、本地方法栈。另外还有不在Java虚拟机直接管理的堆外内存,也被称为直接(Native)内存。

Java运行环境是单进程多线程的,多个线程通过线程切换轮流分配处理器执行时间的方式来实现的,而实际的线程调度是由操作系统控制的。用户线程通过程序计数器和虚拟机栈用来存储线程执行所必须的上下文信息。每个线程都有自己的程序计数器和虚拟机栈。

程序计数器

程序计数器在JAVA虚拟机规范中称为Program Counter Register,即为PC寄存器,它可以看作当前线程所执行的字节码行号指示器,字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。

需要注意,只有执行的是非本地(Native)方法,程序寄存器才会记录JAVA虚拟机正在执行的字节码指令地址,若当前执行方法是本地方法,则程序计数器的值为空(Undefined)。

Java虚拟机栈

和程序计数器一样,每一个JAVA虚拟机线程都有自己私有的JAVA虚拟机栈。Java虚拟机规范允许Java虚拟机栈被实现为固定大小,也允许动态扩展和收缩。

当线程请求的栈深度大于虚拟机允许的栈深度,则会抛出StackOverflowError异常。当栈动态扩展无法申请到足够的内存时,则会排除OutOfMemoryError异常。

每个方法执行的时候当前执行线程会在Java虚拟机栈中分配当前方法的栈帧,用于存储局部变量表、操作数栈、动态链接。当方法执行完后,栈帧就会被丢弃,继续执行下一个栈帧。

局部变量表

局部变量表用于存储基础数据类型、对象引用和returnAddress类型。

局部变量表实际上就是一个数组,数组的一个元素被称为局部变量槽(Slot),一个槽大小为32位。局部变量表所需的内存空间是在编译时分配,运行时局部变量所占用的空间是确定的,也就是数组的槽数。基础数据类型占用1个或2个槽,对象引用和returnAddress类型占用1个槽。

基础数据类型

JAVA有8个基础数据类型:boolean、byte、char、short、int、float、long、double。其中long和double占用2个槽,其他基础数据类型都占用1个槽。

局部变量使用索引进行定位访问。局部变量的索引值从0开始。调用实例方法时,第0个局部变量用于存储当前对象实例(即this关键字)。局部变量从第1个开始;而调用静态方法时,局部变量从第0个开始。

对象引用

对象引用包含指向对象的起始地址的引用指针或指向代表对象的句柄。
其中指向对象起始地址的引用可能是对象、数组或接口。

returnAddress

returnAddress是一个指针,指向一条虚拟机指令的操作码。这些操作码包括jsrretjsr_w。在JDK 7之前,这些操作码用于实现finally语句块的跳转和返回。从JDK 7开始,虚拟机已不允许这几个操作码了,改为冗余finally块代码(在每个catch块后生成冗余的finally代码)实现,因此returnAddress类型基本就没用了。

操作数栈

每个栈帧内部都包含一个后进先出(LIFO)的操作数栈。操作数栈的最大深度由编译期决定。操作数栈中保存了局部变量表或对象实例中的常量或变量值。在调用方法时,也保存调用方法的参数和返回值。

若局部变量是long或double类型,则需要占用2个单位的栈深度。

举个例子,当执行以下代码。右边注释的[]表示操作数栈,左边时栈底,右边是栈顶。

//          //[]
int a = 1;  //[1]   
int b = 2;  //[1,2]
int c = a+b;//[3]->[]

注意:c=a+b,通过iadd读取栈顶的2个数相加后重新入到操作数栈,因此操作数栈中的内容为3,然后从操作数栈中出栈保存到c变量中,操作数栈就空了。

动态链接

每个栈帧内部都包含当前方法所在类型的运行时常量池的引用,以便对当前方法的代码实现动态链接。

在编译时,会将调用的方法或成员变量通过符号引用的方式保存。动态链接的作用就是将以符号引用所表示的方法转换为方法的直接引用。

浅谈前端常用脚手架cli工具及案例

符号引用也被称为描述符(Descriptor),是通过特定的语法来表示的。调用的方法的符号引用称为方法描述符(Method Descriptor),成员变量称为字段描述符(Parameter Descriptor)。

方法返回地址

当通过动态链接调用其他类方法时,栈帧中需要保存被调用的位置,以便方法调用完成后可以返回到被调用时的位置。

当方法正常调用完成后,则栈帧正常恢复局部变量表、操作数栈和调用者的程序计数器正确的位置,若有返回值,则将返回值压入到调用者的栈帧的操作数栈中。

当方法异常调用完成后,则会导致Java虚拟机抛出异常,若当前方法没有任何可以处理该异常的异常处理器,则当前方法的操作数栈和局部变量表都会被丢弃,随后恢复到调用者的栈帧,此时不会有任何返回值压入到调用者的操作数栈中。同时将异常交易给调用者的异常处理器处理。

Java堆

Java虚拟机中,Java堆用于保存各种对象实例,是Java虚拟机所管理的内存中最大的一块,并且该内存被所有线程所共享。
Java栈由线程自动创建和销毁,栈帧由方法的创建和销毁自动管理。而Java堆则由垃圾收集器进行自动收集并回收。垃圾收集器在不同场景下通过最优的垃圾收集算法对垃圾继续收集。

为了提高垃圾收集性能,Java堆将空间分为新生代、老年代。新生代又被分为Eden区和Survivor区。

通常情况下对象都被创建在新生代中的Eden区,随后随着垃圾回收的进行,未被回收的对象则被逐步从新生代转移到老年代,具体垃圾回收相关细节不在这里讨论。

若新生代的空间不足以创建对象,则可能直接被创建到老年代

方法区

方法区用于存储被虚拟机加载的类信息、静态变量、JIT后的代码字节码缓存、运行池常量。虚拟机规范把方法区列为堆的一部分,但是虚拟机实现可以不实现方法区的自动垃圾回收,而是依赖于对常量池和类型的卸载来完成。

类型信息

类型信息包括代码中的类名、修饰符、字段描述符和方法描述符。在class文件中,类型信息并不是我们代码中直接使用的字符串,而是由内部的表现形式的字符串。

字段描述符

字段描述符用于表示类、实例和局部变量。比如用L表示对象,用[表示数组等。

字段描述符内部解释表如下图所示。

字段描述符 类型 含义
B byte 有符号的字节型数
C char unicode字符码点,UFT-16编码
D double 双精度浮点数
F float 单精度浮点数
I int 整型数
J long 长整数
L className reference className的类的实例
S short 有符号短整数
Z boolean 布尔值true/false
[ referebce 一维数组
方法描述符

方法描述符表示0个或多个参数描述符以及1个返回值描述符,用于表示方法的签名信息。若返回值为void则用V表示。

方法描述符的格式: (参数描述符) + 返回值描述符
比如Object m(int i, double d, Thread t)(){}方法可以表示为(IDLjava/lang/Thread;)Ljava/lang/Object;

  • Iint类型的字段描述符
  • Ddouble类型的字段描述符
  • Ljava/lang/ThreadThread类型的内部描述符
  • Ljava/lang/Object是方法的返回值为object类型

方法描述符分割各标识符的符号不用.,而用/表示。

public class SymbolTest{
    private final static String staticParameter = "1245";
    public static void main(String[] args) {
        String name = "jake";
        int age = 54;
        System.out.println(name);
        System.out.println(age);
    }
} 

上面一个简单的例子,编译通过后,可以通过javap -s xxx.class命令查看内部签名。

 D:\study\java\symbolreference\out\production\symbolreference>javap -s com.company.SymbolTest
Compiled from "SymbolTest.java"
public class com.company.SymbolTest {
  public com.company.SymbolTest();
    descriptor: ()V

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
}

可以看出无参构造函数的方法描述符为()V,main方法的方法描述符为([Ljava/lang/String;)V

运行时常量池

运行时常量池保存了编译期常量和运行期常量。编译期常量是在编译时编译器生成的字面量和符号引用。字面量指的是代码中直接写的字符串或数值等常量或声明为final的常量值。比如string str="abc"int value = 1这里的abc1都属于字面量。运行期常量值的是运行期产生的新的常量,比如String.intern()方法产生的字符串常量会被保存到运行时常量池缓存起来复用。
运行时常量在方法区中分配,在加载类和接口到虚拟机后就会创建对应的运行时常量。若创建运行时常量所需的内存空间超过了方法区所能提供的最大值,则会抛出OutOfMemoryError异常。

还是上面的代码示例,通过javap -v可以输出包括运行时常量的附加信息。下面列出了了部分常量输出内容。

D:\study\java\symbolreference\out\production\symbolreference>javap -v com.company.SymbolTest
...
Constant pool:
   #1 = Methodref          #7.#28         // java/lang/Object."<init>":()V
   #2 = String             #29            // jake
   #3 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;
  ...
   #7 = Class              #36            // java/lang/Object
   #8 = Utf8               staticParameter
   #9 = Utf8               Ljava/lang/String;
  #10 = Utf8               ConstantValue
  #11 = String             #37            // 1245
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  ...
  #18 = Utf8               Lcom/company/SymbolTest;
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               args
  #22 = Utf8               [Ljava/lang/String;
  #23 = Utf8               name
  #24 = Utf8               age
  #25 = Utf8               I
  #26 = Utf8               SourceFile
  #27 = Utf8               SymbolTest.java
  #28 = NameAndType        #12:#13        // "<init>":()V
  #29 = Utf8               jake
  ...
  #35 = Utf8               com/company/SymbolTest
  #36 = Utf8               java/lang/Object
  #37 = Utf8               1245
  ...

通过输出的静态常量信息可以很清楚的看出JVM编译时对字面量和符号引用的处理,包括类型名、变量名、方法等都用符号来代替了。比如第一个常量为对象类构造方法java/lang/Object."<init>":()V。去除其他不相关的常量,最终的符号引用和字面量关系如下表。

索引 类型
0 Methodref #7.#28(java/lang/Object."<init>":()V)
...
7 Class #36
...
12 Utf8 <init>
13 Utf8 ()V
...
28 NameAndType #12:#13("<init>":()V)
...
36 Utf8 java/lang/Object

实现方式

在JDK1.7之前,HotSpot是使用GC的永久代来实现方法区,省去了专门编写方法区的内存管理代码。
从JDK1.8开始,使用元空间替代永久代来存放方法区的数据。元空间属于本地内存。简而言之使用了本地内存替换堆内存来存放方法区的数据。

若方法区内存空间不满足内存分配的请求时,将抛出OutOfMemoryError异常。

本地方法栈

若虚拟机支持本地方法,则需要提供本地方法栈,本地方法栈在线程创建的时候按线程分配。HotSpot虚拟机将本地方法栈和虚拟机栈合二为一。

本地方法栈和虚拟机栈一样也会抛出StackOverflowErrorOutOfMemoryError异常。

参考文档

  1. JVM jsr和ret指令始终理解不了?returnAddress又怎么理解呢?
  2. 如何理解ByteCode、IL、汇编等底层语言与上层语言的对应关系?
  3. The Java Virtual Machine Instruction Set
  4. 《深入理解Java虚拟机》
  5. 《Java虚拟机规范(Java SE 8版)》

本文地址:https://www.cnblogs.com/Jack-Blog/p/14332247.html
作者博客:杰哥很忙
欢迎转载,请在明显位置给出出处及链接

备份和还原Windows DHCP服务器

相关推荐

发表评论

路人甲

网友评论(0)