Java 内存区域-运行时数据区域

  最近在学习JVM 的相关知识。一开始看的比较快,对JVM 运行时数据区域只有一个模糊的概念,不太清楚不同内存区域里面到底存放了那些数据,所以在此记录。

  我们都知道Java 与C、C++ 最大的区别就是内存管理领域(Java 有内存动态分配和垃圾收集技术)。在《深入理解Java虚拟机》中描述C、C++ 对内存的管理:对于从事C、C++ 的程序开发人员来说,在内存管理区域,他们即是拥有最高权力的”皇帝”,又是从事最基础工作的”劳动人员”——既拥有每个对象的”所有权”,又担负着每个对象生命开始到终结的维护任务。

  对于Java 来说,由于Java 程序是交给JVM 执行的,所以了解Java 内存区域其实就是了解JVM 内存区域。

一、 JVM运行时数据区域

  • 什么是运行时数据区域?

这里我们需要了解一下Java 程序的执行流程:
这里写图片描述

如上图所示,首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区)

  • 看看正规的描述

Java 虚拟机在执行Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有点区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。根据《Java 虚拟机规范》的规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区。
(蓝色为线程私有,橙色为共有)
JVM运行时数据区

二、各数据区介绍

下面我们来分别了解一下各个数据区:

1. 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器(也有人称PC 寄存器)。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

解释到这里可能有的小伙伴还是很懵。前面我们谈到Java 程序的执行流程里面谈到了

Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后
由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。

由于Java 虚拟机的多线程是通过轮流切换并分配处理器执行时间的方式来实现的,任何时候,一个处理器都只会执行一条线程中的指令。因此为了线程切换后能够恢复发到正确的位置,我们就需要程序计数器来记录程序执行的位置。举个CPU 的解释栗子:当你在看砖头书的时候你妈叫你去吃饭,作为一个肥宅,肯定快乐的扔掉书就跑了。但你又继续啃书的时候才发现不知道自己看到哪了。怎么解决?下次卡个书签呗。程序计数器就是这个道理,记录程序的字节码的执行位置,当当前线程重新拥有CPU 时可以继续执行剩下的代码。

通过上面的解释可以很清晰的理解不同的线程肯定不能共用一个程序计数器,每条线程都需要有一个独立的的程序计数器,各条线程间计数器互不影响,独立存储。我们称这类内存区域为”线程私有”。在JVM规范中规定,如果线程执行的是一个Java方法,则程序计数器中保存的是当前需要执行的指令的字节码地址;如果线程执行的是native方法,则程序计数器中的值是空(undefined)。前面我们提到了:程序计数器是一个很小的内存地址。此内存区域是唯一一个在Java 虚拟机中没有规定任何内存泄露(OutOfMemoryError)情况的区域,程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变。就像书签一样,不管卡在那,它自己不会变。

2. Java 虚拟机栈

Java 虚拟机栈(Java Vitual Machine Stack)跟C 中栈有点类似。与程序计数器一样,Java 虚拟机栈也是线程私有的。虚拟机栈描述的是Java 方法执行的内存模型:

每个方法在执行的同时会创建一个栈帧(Stack
Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。

周大大的解释可以说是很精简了。程序每执行一个方法,就会分配一个栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么在使用递归方法的时候容易导致栈内存溢出的现象了。
Java 虚拟机栈

  • 局部变量表

局部变量表,顾名思义,就是用来存储在编译器可知的基本数据类型的变量(8种基本数据类型);对象引用(reference 类型, 对于引用类型的变量,存的是指向对象起始地址的引用指针)和returnAddress 类型(方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址)。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。

  • 操作数栈

操作数栈,想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

  • 两种异常状况
    StackOverflowError 异常
     如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常。
    OutOfMemoryError 异常
     如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

3. 本地方法栈

本地方法栈和虚拟机栈发挥的作用十分相似。同样是线程私有,它们之间的区别不过是虚拟机栈为Java 方法服务,而本地方法栈为虚拟机使用到的Native 方法服务。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError 异常和OutOfMemoryError 异常。

4. Java 堆

Java 堆(Java Heap)是Java 虚拟机所管理的最大的一块内存。Java 堆是被所有线程共享。Java 堆的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都是在这里分配内存。这一点在《Java 虚拟机规范》中的描述:

所有的对象实例以及数组都要在堆上分配,但是随着JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化将会导致一些微妙的变化发生,所有的对象在堆上分配也变得不那么“绝对”了。

Java 堆是垃圾收集器的主要区域,因此很多时候也叫“GC”堆。

5. 方法区

方法区(Method Area)与堆一样,是被哥哥线程共享的内存区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

运行时常量池(Runtime Constant Pool)是方法区的一部分,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。

在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将字符串常量池从永久代移除了。

6. 直接内存

直接内存(Direct Memory)不是Java 虚拟机规范中定义的内存区域。

在JDK1.4 中新加入的NIO 类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O 形式,他可以使用Native 函数直接分配堆外内存,然后通过一个存储在Java 堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场所显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。

本机直接内存受本机总内存限制。

参考资料:

  https://www.cnblogs.com/dolphin0520/p/3613043.html
  
  《深入理解Java虚拟机》

猜你喜欢

转载自blog.csdn.net/qq_36906627/article/details/81840125
今日推荐