Java虚拟机(二)—— 内存区域/运行时数据区域

在 Java 开发中,Java 虚拟机会自动管理内存,开发人员不需要像 C、C++ 语言那样为每一个 new 操作去写配对的 delete/free 代码,而且 Java 虚拟机管理内存,不容易出现内存泄漏和内存溢出的问题。但是一旦出现内存泄露和内存溢出,而又不了解虚拟机是如何管理的内存的,排查问题将十分艰难。

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域, 这些区域有各自的用途,以及创建和销毁时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。

Java 虚拟机结构包括运行时数据区域、执行引擎、本地库接口和本地方法库,其中类加载子系统并不属于 Java 虚拟机的内部结构。

Java 虚拟机所管理的内存将包括以下几个运行时数据区域:程序计数器、Java 虚拟机栈、本地方法栈、Java 堆、方法区(运行时常量池)、直接内存。其中,程序计数器、Java 虚拟机栈、本地方法栈是所有线程隔离的区域,Java 堆、方法区(运行时常量池)是线程共享区域。

Java 虚拟机

1. 程序计数器(Program Counter Register)

register [ˈredʒɪstər] 寄存器

在虚拟机概念模型中,字节码解释器工作时就是通过改变程序计数器来选取下一条需要执行的字节码指令的。为了保证程序能够连续地执行下去,处理器必须具有某些手段来确定下一条指令的地址,而程序计数器正是起到这种作用。

Java 虚拟机的多线程是通过线程轮流切换并分配处理器(CPU)执行时间的方式来实现的,在任何一个确定的时候,一个处理器(对于是多核处理器来说是一个内核)都只会执行一条线程中的指令,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器(也叫 PC 寄存器),来记录要执行的下一条指令的位置,各条线程之间程序计数器互不影响,独立存储,这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则为空(Undefined)。

程序计数器是一块较小的内存空间。此内存区域是 JVM 中唯一个一个不会 OOM(Out Of Memory)的区域。

2. Java 虚拟机栈(Java Virtual Machine Stacks)

virtual [ˈvɜːrtʃuəl] adj. [计] 虚拟的; stack [stæk] 栈 frame [freɪm] 帧

每一个 Java 虚拟机线程都有一个线程私有的 Java 虚拟机栈,它的生命周期与线程相同,与线程是同时创建的。Java 虚拟机栈存储线程中 Java 方法调用的状态,包括局部变量、参数、返回值以及运算的中间结果等。

一个 Java 虚拟机栈:

Java 虚拟机栈

当线程调用一个 Java 方法时,虚拟机压入一个新的栈帧到该线程的 Java 虚拟机栈中,在该方法执行完成后,这个栈帧就从 Java 虚拟机中弹出。每个方法从调用直到完成的过程,就对应着一个栈帧从入栈(push)到出栈(pop)的过程:

栈帧

一个 Java 虚拟机栈包含了多个栈帧(Stack Frame),一个栈帧用来存储局部变量表、操作数栈、动态链接、方法出口等信息:

栈帧

Java 虚拟机规范中定义了两种异常情况:

  • 如果线程请求分配的栈内容超过了 Java 虚拟机所允许的最大容量,Java 虚拟机会抛出 StackOverflowError;
  • 如果 Java 虚拟机栈可以动态扩展(大部分 Java 虚拟机都可以动态扩展),但是扩展时无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的 Java 虚拟机栈,则会抛出 OutOfMemoryError 异常;
2.1 局部变量表(Local Variables)

variables ['verɪəbl] n. [数] 变量

局部变量表存放了编译期可知的各种 Java 虚拟机基本数据类型(byte、short、int、long、float、double、char、boolean)、对象引用(reference 类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针) 和 returnAddress 类型(指向了一条字节码指定的地址,returnAddress 类型的数据只存在于字节码层面,与编程语言无关)。

这些数据在局部变量表中的存储空间以局部变量槽(Slot 32位 )来表示,其中 64 位长度的 long 和 double 类型的数据会占用 2 个变量槽,其余的数据类型只占 1 个。

slot [slɑːt] 狭槽

局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间完全是确定的(字节码文件中方法表的 Code 属性的 max_locals 数据项中定义了该表容量的最大值),在方法的运行期间也不会改变局部变量表的大小。

查看局部变量表:(查看局部变量表命令:javac -g 文件目录/Test.java + javap -v 文件目录/Test.class)

对于用 static 修饰的静态方法:

public class Test {
  public static void method() {
    Date date = new Date();
    long number = 200L;
    double salary = 6000.0;
    int count = 1;
  }
}

在这里插入图片描述

由上图可知:Date 引用类型占用了 1 个 slot(第 0 个),long 类型的 number 占用了 2 个 slot (第 1 个和第 2 个),double 类型的 salary 占用了 2 个 slot(第 3 个和第 4 个),int 类型的 count 占用了 1 个 slot (第 5 个)。其中 locals 代表局部变量表的大小。

对于普通方法:

public class Test {
    public void method() {
        Date date = new Date();
        long number = 200L;
        double salary = 6000.0;
        int count = 1;
    }
}

在这里插入图片描述

非静态方法的局部变量表首位存放了 this 对象,这也是静态方法内部无法使用 this 的原因,因为静态方法的局部变量表中没有 this 对象。其中 locals 代表局部变量表的大小。

对于有参数的方法:

public class Test {

  public static void method(String s) {
    int count = 1;
  }
}

在这里插入图片描述

变量槽的重复使用: 在方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果程序计数器已经超过了某个变量的作用域,那么这个变量对应的槽就可以交给其他变量来使用。

2.2 操作数栈/操作栈/ LIFO(last-in first-out) 栈、后进先出栈

操作数栈的每个元素可以是任意的 Java 数据类型,包括 long 和 double。32 位数据占用栈空间为 1,64 位数据占用栈空间为 2。 其最大深度在编译时就被写到了 Code 属性的 max_stacks 中。

查看操作数栈:

public class Test {

  public static void method1() {
    int a = 1;
  }

  public static void method2() {
    long b = 2L;
  }

  public static void method() {
    int a = 1;
    long b = 2L;
  }
}

栈的深度

方法开始执行时,操作数栈时空的,方法执行过程中,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,一个完整的方法执行期间包含多个这样入栈/出栈的过程。

下面通过一个例子来说明程序计数器、局部变量表和操作数栈时如何相互配合完成一次方法的执行的:

public class Test {

    public static void method() {
        int a = 15;
        int b = 1;
        int c = a + b;
    }
}

在查看字节码指令之前,先记录下几个入栈出栈的字节码指令含义:

  • 当 int 取值 -1 ~ 5 采用 iconst 指令入栈;
  • 取值 -128 ~ 127(byte 有效范围)采用 bipush 指令入栈;
  • 取值 -32768 ~ 32767 (short有效范围)采用 sipush 指令入栈;
  • 取值 -2147483648 ~ 2147483647(int 有效范围)采用 ldc 指令入栈;
  • istore,栈顶元素出栈,保存到局部变量表中;
  • iload,从局部变量表中加载数据入栈;

操作数栈

bipush

istore_1

iconst_1

istore_2

iload_1

iload_2

iadd

istore_3

如果被调用的方法带有返回值的话,其返回值会被压入当前栈帧的操作数栈中。

2.3 动态链接(Dynamic Linking)/指向运行时常量池的方法引用

dynamic [daɪˈnæmɪk] 动态的

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。

在源文件在编译成字节码文件时,所有的变量和方法引用都会作为符号引用保存在字节码文件的常量池中(字节码文件的常量池中存有大量的符号引用),字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。

这些符号引用一部分会在类加载阶段或者第一次使用的时候转化为直接引用,这种转化为静态解析。另一部分将会在每一次运行期间转化为直接引用,这部分成为动态链接。

比如说:描述一个方法调用了另外一个方法时,就是通过常量池中指向方法的符号引用来表示的,动态链接的作用就是为了将这些符号引用转化为方法的直接引用。

动态链接

2.4 返回地址
  • 当一个方法开始执行之后,只有两种方式可以退出这个方法:
    • 第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值以及返回值类型将根据遇到的方法返回字节码指令来决定,这种退出的方式成为正常完成出口。
    • 第二种退出方式是在方法的执行过程中出现了异常,并且这个异常没有在方法体内得到处理,无论是 JVM 内部产生的异常或者是使用 throw 关键字产生的异常,只要在本方法的异常处理表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口。此种情况下,方法是不会给上层调用者返回任何值的。
    • 无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。
  • 方法退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有)压入调用者栈帧的操作数栈中,调用程序计数器(PC 寄存器)的值以指向方法调用指令后面的一条指令地址;
2.5 关于递归

当方法调用 return,数据将从 stack 中 pop 出来,这样程序才能回到调用者出继续执行。

递归算法有时会产生很深的调用,从而耗尽空间:当一个方法递归调用自己时,会拷贝一份当前方法的数据并创建一个新的栈帧 push 到栈中。因此,递归的每层调用都需要创建一个新的栈帧,这样的结果是,栈中越来越多的内存将会随着递归调用而被消耗,如果递归调用自己一万次,那将会产生一万个栈帧。

Java 虚拟机允许虚拟机栈的大小固定不变或者动态扩展:

  • 固定情况下:如果线程请求分配的栈容量超过了 Java 虚拟机所允许的最大容量,Java 虚拟机会抛出 StackOverflowError。
  • 可动态扩展:如果 Java 虚拟机栈可以动态扩展(大部分 Java 虚拟机可以动态扩展),但是扩展时无法申请到足够的内存,或者在创建新的线程时没有足够的内存区创建对应的 Java 虚拟机栈,则会抛出 OutOfMemoryError 异常。

3. 本地方法栈(Native Method Stacks)

Java 虚拟机实现可能要用到 C Stacks 来支持 Native 语言,这个 C Stacks 就是本地方法栈(Native Method Stack)。它与 Java 虚拟机栈类似,只不过本地方法栈是用来支持 Native 方法的。如果 Java 虚拟机不支持 Native 方法,并且也不依赖于 C Stacks,可以无需支持本地方法栈。

本地方法栈与虚拟机栈所发挥的作用是非常相似的。其区别只是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到本地(Native)方法服务,本地方法是用 C 语言实现的。

在 Java 虚拟机规范中对本地方法栈的语言和数据结构等没有强制规定,因此具体的 Java 虚拟机可以自由实现它,比如 HotSpot VM 将本地方法栈和 Java 虚拟机栈合二为一。

与 Java 虚拟机栈类似,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

4. Java 堆(Java Heap)

heap [hiːp] 堆 allocation [ˌæləˈkeɪʃn] 配置

Java 堆(Java Heap)是被所有线程共享的运行时内存区域,是虚拟机所管理的内存中最大的一块。在虚拟机启动的时候创建。

Java 堆用来存放对象实例,几乎所有的对象实例都在这里分配内存。 堆空间申请了,但是并不一定会全部使用。

Java 堆存储的对象被垃圾收集器管理,也被称为 GC 堆(Garbage Collected Heap),这些受管理的对象无法显式地销毁。(堆是 GC 管理的主要区域)

从内存回收的角度来分,Java 堆可以粗略的分为新生代和老年代。从内存分配的角度 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB),以提高对象分配时的效率。不管如何划分,Java 堆存储的内容是不变的,进行划分是为了能更快地回收或者分配内存。Java 堆存储的内容是不变的,进行划分是为了能更快地回收或者分配内存。

Java 堆的容量可以是固定的,也可以是动态扩展的。Java 堆所使用的内存在物理上不需要连续,逻辑上连续即可。 这点就像用磁盘空间去存储文件一样,并不要求每个文件都连续存放。

Java 虚拟机规范中定义了一种异常情况:如果在堆中没有足够的内存来完成实例分配,并且堆也无法进行扩展时,则会抛出 OutOfMemoryError 异常。

对象创建的时候,是在堆上分配还是在栈上分配?—— 和对象的类型以及在 Java 类中存在的位置有关系

Java 对象可以分为基本数据类型(byte、short、int、long、float、double、boolean、char)和普通对象。对于普通对象来说,Java 虚拟机会在堆上创建对象,在其他地方使用的其实是它的引用,比如说把这个引用保存在局部变量表中;对于基本数据类型来说有两种情况,如果是在方法体内声明,就会在栈上直接分配,其他情况都是在堆上分配。

堆与栈
  • 在 Java 中,main 函数是程序执行的入口,也是栈的起点
  • 栈解决程序的运行问题,即程序如何执行(或者说是如何处理数据);堆解决的是数据存储的问题。从软件设计的角度来看,栈代表了处理逻辑,而堆代表了数据。
  • 在 Java 中每个线程都有相应的栈与之对应,而堆是被所有线程共享的
  • 面向对象就是堆和栈的完美结合,对象的属性也就是数据,放在堆中;而对象的行为就是运行逻辑,放在栈中
  • 一个对象的大小是不可估计的,或者可以说是动态变化的,但是在栈中,一个对象只对应一个引用(4B/4byte)
  • 基本类型放在栈中,是因为其占用的空间一般是 1~8B/8byte,需要的空间比较少,因为是基本类型,长度固定,所以不会出现动态增长的情况,因此在栈中存储就可以了。

5. 方法区(Method Area)

方法区(Methos Area)是被所有线程共享的运行时内存区域,用来存储已经被 Java 虚拟机加载出来的类的结构信息,包括运行时常量池、字段和方法信息、静态变量等数据。

方法区是 Java 堆的逻辑组成部分,但它有一个别名叫做“非堆”(Non-Heap),目的是与 Java 堆分开。

Java 虚拟机规范对于方法区的约束是非常宽松的,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外, 甚至还可以选择不实现垃圾收集。 相对而言,垃圾收集行为在这个区域的确比较少出现,但并非数据进入了方法区就如“永久代”代的名词一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。 一般来说,这个区域的的回收效果比较难令人满意,尤其是对类型的卸载,条件相当苛刻,但是这部分区域的回收有时又的确有必要。

Java 虚拟机规范中定义了一种异常情况:如果方法区的内存空间不满足内存分配需求时,Java 虚拟机会抛出OutOfMemoryError 异常。

运行时常量池(Runtime Constant Pool)

运行时常量池(Runtime Constant Pool)并不是运行时数据区域的其中一份子,而是方法区的一部分。.class 文件中有一项信息是常量池表(Constants Pool Table),它是用来存放编译时期生成的字面量和符号引用,这些内容会在类加载后存放在方法区的运行时常量池中。

  • 字面量包括字符串(String a = “b”),基本类型的常量(final 修饰的变量);
  • 符号引用则包括类和方法的全限定名(例如 String 这个类,它的全限定名就是 Java/lang/String)、字段名的名称和描述符以及方法的名称和描述符;

运行时常量池是全局共享的,多个类共用一个运行时常量池,如果 .class 常量池表中存在多个相同的字符串,在运行时常量池也只会存在一个。 比如说:类中的一个字符串常量存放在 .class 文件常量池表中,在类加载完成之后,JVM 会把这个字符串常量放在运行时常量池中,并在解析阶段,指定该字符串对象的索引值。

public class Test {
   int a = 1;

   public void method() {
       int b = 2;
   }

}

字节码文件常量池

Java 虚拟机规范中定义了一种异常情况:当创建类或接口时,如果构造运行时常量池所需的内存查过了方法区所能提供的最大值,Java 虚拟机会抛出 OutOfMemeoryError 异常。

6. 直接内存(Direct Memory)

直接内存不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机管理的内存区域。

在 JDK1.4 中,新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)和缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用,这样就避免了在 Java 堆和 Native 堆中来回复制数据。

直接内存不受堆大小的限制,但受本机内存限制,会出现 OutOfMemory 异常。

参考

http://www.weixueyuan.net/a/9.html
https://www.cnblogs.com/xingzc/p/5756119.html
https://www.cnblogs.com/code-duck/p/13559193.html
https://blog.csdn.net/antony1776/article/details/89843145
https://blog.csdn.net/m0_37989980/article/details/111224776?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-1.no_search_link

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Java虚拟机主要分为以下几个内存区域: 1. 程序计数器(Program Counter Register) 2. Java虚拟机栈(Java Virtual Machine Stacks) 3. 本地方法栈(Native Method Stacks) 4. Java堆(Java Heap) 5. 方法区(Method Area) 6. 运行时常量池(Runtime Constant Pool) 7. 直接内存(Direct Memory) 其中,程序计数器、Java虚拟机栈、本地方法栈都是线程私有的内存区域Java堆、方法区、运行时常量池、直接内存则是线程共享的内存区域。 ### 回答2: Java虚拟机(JVM)中有几个重要的内存区域,它们分别是堆、栈、方法区、程序计数器和本地方法栈。 堆是Java程序最主要的内存区域之一,用于存储对象实例和数组。所有通过关键字new创建的对象都会在堆上分配内存。堆是被所有线程共享的内存区域,每个对象的实例变量在堆中占用一定的空间。 栈是一个线程私有的内存区域,用于存储线程的方法调用。每个线程在执行方法时都会在栈中创建一个栈帧,栈帧包含方法的参数、局部变量和返回值等信息。栈的操作是后进先出的,所以栈也被称为LIFO(Last In First Out)数据结构。 方法区(JDK 8及以前版本称为“永久代”)用于存储类的元数据信息,如类名、方法名、字段名和运行时常量池等。方法区也是被所有线程共享的内存区域。 程序计数器是一块较小的内存区域,它用来指示线程当前执行的字节码指令地址。每个线程都有一个独立的程序计数器,因此它是线程私有的。 本地方法栈与栈类似,但用于执行本地方法调用(Native Method)。本地方法是用C或C++等本地语言编写的方法,它们可以直接在系统级别访问硬件设备或其他资源。本地方法栈也是线程私有的。 总之,Java虚拟机内存区域包括堆、栈、方法区、程序计数器和本地方法栈。这些内存区域各有不同的作用,用于支持Java程序的执行和对象的管理。 ### 回答3: 在Java虚拟机中,主要有以下几个内存区域: 1. 程序计数器(Program Counter Register): 程序计数器是一块较小的内存区域,它保存着当前线程正在执行的字节码指令的地址。每个线程都有自己独立的程序计数器。 2. Java栈(Java Stack): Java栈用于存储Java方法的局部变量、参数、方法返回值以及一些操作数。每个线程的方法在执行时都会创建一个对应的栈帧,栈帧中存放了方法的信息。 3. 本地方法栈(Native Method Stack): 本地方法栈与Java栈类似,但它是为Native方法服务的。Native方法是使用其他语言(如C、C++)编写的方法,本地方法栈用于保存这些方法的本地变量。 4. Java堆(Java Heap): Java堆是Java虚拟机所管理的内存中最大的一块区域,被所有线程共享。Java堆用于存储对象实例和数组,是垃圾回收的主要区域Java堆可以分为新生代和老年代两个区域。 5. 方法区(Method Area): 方法区用于存储已被加载的类信息、常量、静态变量等数据。在方法区中还有一个叫做运行时常量池的区域,它存放着每个类的运行时常量信息。 6. 运行时常量池(Runtime Constant Pool): 运行时常量池是方法区中的一部分,用于保存编译器生成的各种字面量和符号引用。 除了以上几个内存区域Java虚拟机还包括了直接内存(Direct Memory)。直接内存并不是Java内存区域的一部分,它使用的是操作系统的内存,但可以被Java虚拟机直接访问,通常用于NIO(New IO)操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值