目录

zrlong 的个人博客

希望大家都能保护好自己身上的特质,无论是五年还是十年,永远善良,不服输,热爱你所热爱。在漫长岁月的变迁里,是这些让你永远迷人,富有生命力。

X

JVM之运行时数据区

运行时数据区

java虚拟机在执行Java程序的时候会把它管理的内存划分为若干数据区域。如下图

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器。由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个单独的程序计数器,因此它是“线程私有”的。

Java虚拟机栈

Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,生命周期跟线程相同。虚拟栈中存放着一个个栈帧,每调用一个方法对应着一个栈桢,栈帧里存放着局部变量表、操作数栈、动态链接、方法返回地址、一些附加信息,方法执行完毕对应的栈帧就从虚拟机栈中弹出。

  • 局部变量表 (Local Variables),也成为局部变量数组或者本地变量表,是由数组实现。存放了编译期可知的各种Java虚拟机基本数据类型 (boolean、byte、char、short、int、float、long、double)、对象引用 (reference类型)、returnAddress类型 (指向一条字节码指令的地址)。
  • 操作数栈 也常被称为操作栈。和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。但是和前者不同的是,它不是通过索引来访问,而是通过标准的栈操作—压栈和出栈—来访问的。比如,如果某个指令把一个值压入到操作数栈中,稍后另一个指令就可以弹出这个值来使用。虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。
  • 动态链接 ,每一个栈帧内部都包含一个指向运行时常量池 中该栈帧所属方法引用 ,包含这个引用就是为了支持当前方法的代码能够实现动态链接
  • 方法返回地址 ,存放调用了该B方法的上一级方法A(栈帧)的 pc寄存器 中的值 (即要执行的下一条指令的指令地址)。将指令地址返回给执行引擎去执行方法A的下一条指令。方法正常退出时,调用者的pc计数器的值作为返回地址 ,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
  • 附加信息,允许携带与Java虚拟机实现相关的一些附加信息 。例如:对程序调试提供支持的信息。

本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的功能非常相似,它是为虚拟机调用本地方法服务。

对于本地方法栈的语言,实现方法,与数据结构来说没有强行规定,具体由虚拟机根据需求自由实现,随着语言的发展有些Java虚拟机(Hot-Spot虚拟机)直接将本地方法栈和虚拟机栈合二为一,与虚拟机栈相同,本地方法栈也会出现栈深度溢出或栈扩展失败抛出的错误。

Java堆

Java堆(Java Heap)是虚拟机管理内存最大的一块,被所有线程共享,在虚拟机启动时创建。用来存放对象实例,“几乎”所有的对象都在折这里创建。由于即时编译技术的不断发展,逃逸分析技术日益强大,栈上分配、标量替换等优化手段让Java实例对象都在堆上分配变得不那么绝对。不同的垃圾回收器为了回收方便,会将堆分为不同的区域。

TLAB (Thread Local Allocation Buffer,线程本地分配缓冲区 )是 Java 中内存分配的一个概念,它是在 Java 堆中划分出来的针对每个线程的内存区域,专门在该区域为该线程创建的对象分配内存。它的主要目的是在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。TLAB 本质上还是在 Java 堆中的,因此在 TLAB 区域的对象,也可以被其他线程访问。启用了 TLAB 之后(-XX:+UseTLAB, 默认是开启的),JVM 会针对每一个线程在 Java 堆中预留一个内存区域,在预留这个动作发生的时候,需要进行加锁或者采用 CAS 等操作进行保护,避免多个线程预留同一个区域。一旦某个区域确定划分给某个线程,之后该线程需要分配内存的时候,会优先在这片区域中申请。这个区域针对分配内存这个动作而言是该线程私有的,因此在分配的时候不用进行加锁等保护性的操作。

方法区

方法区(Method Area)与堆一样,是各个线程共享的内存区域。用于储存被虚拟机加载的类的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。JDK 8之前方法区也被称为“永久代”,但本质上方法区和“永久代”不是等价的,“永久代”是方法区的实现。

到了JDK 7的时候已经把原来放在永久代的字符串常量池、静态变量等移到了Java堆中,到了JDK 8 的时候完全废除了“永久代”的概念,在本地直接内存中实现元空间(Meta-space)来代替,把JDK 7中剩余的内容也全部移到了元空间。

移除永久代的原因:

  • 为永久代设置空间大小是很难确定的。

    在某些场景下,如果动态加载类过多,容易产生 Perm 区的 OOM。如果某个实际 Web 工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现 OOM。而元空间和永久代最大的区别在于,元空间不在虚拟机中,而是使用本地内存,所以默认情况下,元空间的大小仅受本地内存限制

  • 对永久代进行调优较困难

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池表(Constant Pool Table),用于存放编译器生成的字面量和符号引用,这部分会被放到运行时常量池中。除了会保存Class文件中描述的符号引用外,还会把有符号引用翻译出来的直接引用也储存在运行时常量池中。具备动态性,运行期间也可添加内容。因为是方法区的一部分,所以也会有内存限制,内存申请失败时会报OutOfMemoryError。

直接内存

直接内存 (Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是也可能出现OutOfMemoryError。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于**通道(Channel) 缓冲区(Buffer)**的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

优点 :使用堆外内存的好处是,可以提升性能。比如常规情况下,把java堆内部的数据进行远程发送,需要先把堆内部的数据拷贝到直接内存里面,也就是拷贝到堆外内存,然后在发送。如果把对象分配到直接内存里面,发送的时候就可以省掉复制的哪一步操作。

缺点:没有jvm帮助管理内存,需要我们自己来管理堆外内存,防止内存溢出 。为了避免一直没有FULL GC,最终导致物理内存被耗完。我们会指定直接内存的最大值,通过-XX:MaxDirectMemerySize来指定,当达到阈值的时候,调用system.gc()来进行一次Full gc,把那些没有被使用的直接内存回收掉。


标题:JVM之运行时数据区
作者:zrlong
地址:http://blog.zrlong.top/articles/2022/04/01/1648820109436.html