Loading

《深入理解Java虚拟机》01-Java内存区域

运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。

根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将包含以下几个运行时数据区域

qXk8oQ.png

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。如果线程正在执行一个Java方法,那么该程序计数器记录的是正在执行的虚拟机字节码指令的地址;如果线程正在执行一个本地(Native)方法,这个计数器的值应为空Undefined

在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器。分支、循环、跳转、异常处理、线程恢复都需要依赖这个计数器完成。

Java虚拟机的多线程是通过各线程轮流获得CPU时间片来完成的,程序计数器存储了当前线程执行的位置。所以为了线程切换后能够正确的恢复到应该执行的位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为线程私有的内存。

程序计数器的生命周期随着线程的创建而创建,随着线程的结束而销毁。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域

Java虚拟机栈

qXZDj1.png

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,每个方法从被调用至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址等信息。局部变量表主要用于存储方法参数及定义在方法体内部的局部变量。包括基本数据类型(boolean、byte、char、short、int、long、float、double)和对象引用(可能是指向对象起始地址的引用指针,也可能是代表对象的句柄或其他与此对象相关的位置)和return address类型(指向一条字节码指令的地址)。局部变量表的大小是在编译时期确定下来的,局部变量表的存储单位是局部变量槽(Slot),64位长度的数据类型(如long和double类型的数据)会占用两个Slot,其余的数据类型只会占用一个局部变量槽slot。

Java虚拟机栈也是线程私有的,它的声明周期随着线程的开始而创建,随着线程的结束而销毁。在程序运行中Java虚拟机栈可能会出现两种错误:

  • StackOverflowError异常:如果Java虚拟机栈不可以动态扩展且线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
  • OutOfMemoryError异常:如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存空间会抛出OutOfMemoryError异常

本地方法栈

本地方法栈和Java虚拟机栈的作用是非常相似的,区别是Java虚拟机栈是为虚拟机执行Java方法服务的,而本地方法栈则是为虚拟机使用本地(Native)方法服务的。在HotSpot虚拟机中把本地方法栈和Java虚拟机栈合二为一,放在同一块区域中,但是他们本质上还是两种不同的东西。

本地方法栈和Java虚拟机栈一样,也是线程私有的,生命周期随着线程而创建和销毁。本地方法栈也会在栈深度溢出时抛出StackOverflowError异常或栈扩展空间失败时抛出OutOfMemoryError异常

Java堆

Java堆(Java Heap)是虚拟机所管理的内存中最大的一块区域,Java堆内存区域的作用是存放对象实例,Java中几乎所有的对象和数组都在堆里分配内存(从JDK1.7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或未被外面使用,即未逃逸出去,那么对象可以直接在栈上分配内存)。

Java堆是垃圾收集器管理的主要区域,因此也被称为GC堆Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为新生代和老年代;再细分一点还有Eden、Survivor、多个线程私有的分配缓冲区等空间。进一步划分堆空间的意义是为了更好的回收内存或更快的分配内存。

根据《Java虚拟机规范》的规定,Java堆可以处于物理上无连续的内存空间中,但在逻辑上它应该是连续的。但对于大对象例如数组类型,多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的存储空间。

堆是被所有线程共享的一片区域,它在虚拟机启动时创建。如果Java堆中没有内存完成实例分配并且堆也无法再扩展时,Java虚拟机将抛出OutOfMemoryError异常

方法区

方法区(Method Area)的作用是用于存储已被虚拟机加载的类信息、常量、静态变量和即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把它描述为堆的一个逻辑部分,但是没有规定方法区的实现,所以在不同的虚拟机上,方法区的实现是不同的。

永久代和元空间就是方法区的两种不同的实现,永久代是JDK1.8之前方法区的实现方式,元空间是JDK1.8之后方法区的实现方式,下面是从JDK1.6到JDK1.8版本中方法区实现的演变过程

LC8ov9.png

LCGuKs.png

LCGMbq.png

在JDK1.6及之前方法区的数据完全是由永久代来实现,由于永久代的设计使方法区更容易遇到内存溢出的问题,所以在JDK1.7中HotSpot虚拟机就将方法区中的静态变量和字符串常量池放入堆中存放,在JDK1.8之后将剩余的数据使用元空间来实现

方法区和Java堆一样是所有线程共享的内存区域,也会在内存溢出的时候抛出OutOfMemoryError异常

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分,Class文件中除了有类的版本、字段信息、方法信息、接口信息等之外还有一项信息是常量池表,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池相比于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译器内才能产生,也就是说并于只有预置与Class文件内的常量才能进去运行时常量池,在程序运行期间也可以将常量放入常量池中,例如String类的intern()方法。

运行时常量池是方法区的一部分,所以也是所有内存共享的区域,也会在常量池无法申请到内存时抛出OutOfMemoryError异常

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分区域使用频繁并且也会产生OutOfMemoryError异常

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

直接内存不受堆内存空间大小的限制,但是既然是内存就会收到本机总内存和处理器寻址空间的限制。

posted @ 2023-02-26 15:59  edws  阅读(29)  评论(0编辑  收藏  举报