JVM-内存模型(运行时数据区)

目录

    • [1. 线程私有](#1. 线程私有)
      • [1.1 程序计数器(PC寄存器)](#1.1 程序计数器(PC寄存器))
      • [1.2 虚拟机栈](#1.2 虚拟机栈)
      • [1.3 本地方法栈](#1.3 本地方法栈)
    • [2. 线程共享](#2. 线程共享)
      • [2.1 堆](#2.1 堆)
      • [2.2 方法区](#2.2 方法区)
    • [3. 直接内存](#3. 直接内存)

参考 pdai全栈知识体系-JVM-内存结构

1. 线程私有

1.1 程序计数器(PC寄存器)

  • 作用:PC 寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。
  • 程序计数器是⼀块小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器⼯作时通过改变这个计数器的值来选取下⼀条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
  • 由于Java虚拟机的多线程是由多线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器是一个内核)都只会执行一条线程中的指令,因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有⼀个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为"线程私有"的内存。
  • 从上面的介绍中我们知道程序计数器主要有两个作用:
    • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
    • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
  • 注意:程序计数器是唯⼀⼀个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

1.2 虚拟机栈

  • 与程序计数器⼀样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java方法执行的内存模型,每次方法调用的数据 都是通过 传递的,每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame) ,对应着一次次 Java 方法调用。
  • 作用:主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
  • Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java 虚拟机栈是由⼀个个栈帧 组成,而每个栈帧中都拥有:局部变量表操作数栈动态链接方法信息 。)
    • 局部变量表

      是一组变量值存储空间,主要用于存储方法参数和定义在方法体内的局部变量 ,包括编译器可知的各种 Java 虚拟机基本数据类型 (boolean、byte、char、short、int、float、long、double)、对象引用 (reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址,已被异常表取代)。不存在线程安全问题(局部变量表建立在线程的栈上,是线程的私有数据)

    • 操作数栈

      操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间

    • 动态链接

      • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。
      • 在 Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在 Class 文件的常量池中 。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
    • 方法出口

      • 用来存放调用该方法的 PC 寄存器的值
      • 一个方法的结束有两种方式:
        1. 正常执行完成
        2. 出现未处理的异常,非正常退出
        3. 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值
      • 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分信息。
      • 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
  • Java 虚拟机栈会出现两种错误: StackOverFlowErrorOutOfMemoryError
    • StackOverFlowError : 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候 ,就抛出StackOverFlowError 错误。一般是由于递归导致的无限嵌套调用递归方法。
    • OutOfMemoryError : 若 如果虚拟机在扩展栈时无法申请到足够的内存空间 。就会抛出 OutOfMemoryError 错误。
  • Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。

1.3 本地方法栈

  • 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java虚拟机栈合⼆为⼀。
  • 本地方法被执行的时候,在本地方法栈也会创建⼀个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
  • 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowErrorOutOfMemoryError 两种错误。如果线程请求分配的栈容量超过本地方法栈允许的最大容量 ,Java 虚拟机将会抛出一个 StackOverflowError 异常。如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈 ,那么 Java虚拟机将会抛出一个OutofMemoryError异常

栈是运行时的单位,而堆是存储的单位。

栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪。

2. 线程共享

2.1 堆

  • 存放对象实例,几乎所有的对象实例都在这里分配内存

  • Java堆是垃圾收集器管理的主要区域

  • 从内存回收(GC)的角度看,由于现在的垃圾收集器基本都采用分代收集算法 ,所以java堆还可以细分为新生代和老年代。

    • 新生代
      是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为Eden 区ServivorFromServivorTo 三个区。
      • Eden区:
        Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
      • ServivorFrom
        上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
      • ServivorTo
        保存Eden区经过一次MintorGC后的幸存者。
      • MinerGC的过程(复制-->清空-->互换)
        • eden、servivorFrom 复制到 ServivorTo,年龄+1
        • 清空 eden、servivorFrom
        • ServivorTo 和 ServivorFrom 互换
    • 老年代
      1. 主要存放应用程序中生命周期长的内存对象。
      2. 老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
      3. MajorGC 采用标记清除算法 :首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的 时候,就会抛出 OOM(Out of Memory)异常。
    • 永久代

      1. 永久代是 HotSpot 虚拟机对虚拟机规范中方法区的⼀种实现⽅式。
      2. 在 Java8 中,永久代已经被移除,被一个称为"元数据区"(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存 。因此,默认情况下,元空间的大小仅受本地内存限制 。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。
      3. 使用永久代实现方法区容易遇到内存溢出的问题(永久代有-XX:MaxPermSize的上限)

2.2 方法区

  • Java 虚拟机规范把方法区描述为堆的⼀个逻辑部分,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • hotspot中方法区的实现:通过堆的永久代 来实现,JDK1.8方法区替换成直接内存中的元空间
  • 运行时常量池
    运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版
    本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。 Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。

3. 直接内存

  • 直接内存并不是虚拟机运行时数据区的⼀部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使⽤。而且也可能导致 OutOfMemoryError 错误出现。
  • JDK1.4 中新加⼊的 NIO(New Input/Output) 类,引入了⼀种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过⼀个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在⼀些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
  • 本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存⼤小以及处理器寻址空间的限制。

面试总结,如有不足,欢迎指正

相关推荐
ThisIsClark2 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
王佑辉2 小时前
【jvm】内存泄漏与内存溢出的区别
jvm
大G哥4 小时前
深入理解.NET内存回收机制
jvm·.net
泰勒今天不想展开4 小时前
jvm接入prometheus监控
jvm·windows·prometheus
东阳马生架构1 天前
JVM简介—3.JVM的执行子系统
jvm
程序员志哥1 天前
JVM系列(十三) -常用调优工具介绍
jvm
后台技术汇1 天前
JavaAgent技术应用和原理:JVM持久化监控
jvm
程序员志哥1 天前
JVM系列(十二) -常用调优命令汇总
jvm
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭1 天前
聊聊volatile的实现原理?
java·jvm·redis