初入JVM - 运行时数据区探索(二)

在编写和运行Java应用程序的过程中,我们常常会遇到各种与内存管理相关的问题。这些问题的根源往往在于对Java虚拟机(JVM)内部工作原理的不了解。在这篇博客中,我们将深入探讨JVM的运行时数据区,揭开这些看似神秘却又至关重要的概念。
Java虚拟机为了高效地执行Java代码,将其内存划分为不同的区域,包括程序计数器、虚拟机栈、本地方法栈、堆以及方法区等。每个区域都有其特定的用途和特点,共同协作以确保程序的稳定运行。
通过理解JVM的运行时数据区,我们可以更好地诊断并解决诸如内存泄漏、性能瓶颈等问题。同时,熟悉这些数据区的工作原理也将有助于我们编写出更加高效且健壮的Java应用程序。
在这篇文章中,我们将逐步剖析每个数据区的特点、作用以及它们之间的交互方式。无论你是Java新手还是经验丰富的开发者,我都希望这个系列能为你提供有价值的洞见,并帮助你提升对JVM的理解。

接上一篇:初入JVM-运行时数据区初探(一)

image.png

运行时数据区

一、程序计数器
  • 介绍:
    • 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器(说通俗一些就是JVM将类文件编译形成一行行的指令后,需要有一个标记表示程序即将执行的指令是在哪一行);image.png
  • 为什么要有程序计数器呢?
    • 因为程序是多线程运行的,涉及到线程间的频繁切换执行,所以就需要这样一个东西来记录我当前线程该执行哪一个指令了,避免线程间频繁切换导致程序运行指令错乱;这样也说明了为什么程序计数器是线程私有的,而不是想方法区和堆一样是线程共有的;
  • 生命周期
    • 上边说到了,程序计数器是线程私有的,那也说明了 程序计数器是随着线程的创建而创建,随着线程的销毁而释放的。
  • 注意
    • 当程序执行的是navite本地方法是,程序计数器的值是undefined。且程序计数器记录的是当前方法的指令行号,如果A方法调用了B方法,那么在B方法中 计数器是从0开始的。可以理解为程序计数器是针对于栈帧的。(栈帧在下边虚拟机栈中讲解)
二、虚拟机栈
  • 介绍
    • 每个线程在创建的时候都会创建一个栈,虚拟机栈也是属于**“线程私有的”生命周期与线程一致;**是用来存储调用的方法,每一个方法为一个栈帧(栈帧为栈的最小单位);虚拟机栈遵循“先进后出”、“后进先出”的规则,正在执行的方法在栈顶(为当前方法),当一个方法执行完毕后该栈帧就会弹栈,前一个栈帧到栈顶作为当前方法。其中栈帧中又包括 局部变量表、操作数栈、动态链接、方法返回地址以及一些附加信息;
    • 虚拟机栈不存在垃圾回收,因为是随着线程的销毁而一并进行销毁的。
    • Java 虚拟机规范允许 Java虚拟机栈的大小是动态的或者是固定不变的;通常情况下,虚拟机栈的大小是由JVM在启动时根据可用内存和其他运行时条件动态分配的。如果未指定特定的大小,JVM会选择一个合适的值来保证程序可以正常运行。
      • 如果采用大小固定不变,当请求的栈容量超过JVM设定的最大容量时会抛出StackOverflowError 异常;
      • 动态扩展时,当请求的栈容量超过当前最大容量时,会从堆内存中申请内存,当无法申请到足够的内存时会抛出OutOfMemoryError异常。
  • 如何设置栈空间的大小呢?
    • 我们可以通过参数 -Xss 来设置最大栈空间大小,栈的大小决定程序可调用方法的最大深度。

上边说了栈帧中又包括了**局部变量表、操作数栈、动态链接、方法返回地址以及一些附加信息。**那我们接下来就来看一下栈帧中的这几部分内容;如图:
image.png

1. 局部变量表
  • 介绍
    • 存储方法中入参、局部变量(基本类型、引用类型地址);且**局部变量表所需要的容量大小是编译期确定下来的;**局部变量表中的数据只对当前方法有效;
    • 局部变量表所需要的容量大小是编译期确定下来的;
    • 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到调用者的局部变量表中;
    • 变量表中的数据区是可以被复用的(例如变量A作用域在某个if块儿内有效,if块儿外有变量B、C,那么在该方法中的变量在编译后实际只有两个数据区,因为变量A的数据区被复用了)
    • 这里为什么会有三个呢?因为this也占用了一个槽位;实际槽位中为this、a、c。c复用了b的槽位。image.png
  • 疑问
    • Q: 为什么局部变量表的容量实在编译期确定下来的呢?
    • A: 在编译期确定容量大小时需要进行静态分析和类型推断,通过分析方法的代码和变量的作用域,确定方法中需要存储的局部变量的数量和类型。
2. 操作数栈
  • 介绍
    • 主要用于存储方法中的计算结果,当作临时存储的一个数据区域。当方法被调用时创建一个空的操作数栈,跟随方法的结束而销毁。如果被调用方法是有返回值的,返回值也会被压入操作数栈,并更新程序计数器指向下一条指令;
  • 疑问
    • Q: 调用的方法存在返回值时,将返回值压入操作数栈,那调用者是怎么拿返回值的?操作数栈不是跟随方法的结束而销毁吗?
    • A: 当一个方法调用返回时,它的操作数栈并不会立即销毁。相反,这个操作数栈会被保留下来,并且其中的返回值会被返回到调用者的方法。具体来说,当一个方法执行完毕并准备返回时,它会将返回值压入自己的操作数栈。然后,JVM会从当前方法的帧中弹出,并恢复调用者的帧作为当前帧。此时,调用者的操作数栈还在,只是被暂停使用了。现在,由于调用者的方法成为当前方法,其操作数栈又可以继续使用。接下来,JVM会检查被调用方法是否有返回值。如果有,它会从刚刚恢复的操作数栈顶获取这个返回值,并将其放入调用者对应的位置。例如,如果是一个赋值语句,那么返回值会被复制到左操作数所代表的变量中;如果是方法链式调用,那么返回值会被传递给下一个方法。最后,JVM会清理被调用方法的局部变量表和操作数栈,因为它们不再需要。而调用者的方法则继续执行,直到它也完成并返回。
3. 动态链接
  • 介绍
    • 动态链接是指在程序运行时将程序中的符号引用和实际的内存地址进行关联的过程,在编译过程中,方法和变量通常以符号引用的形式存在,而不是直接指向实际的内存地址。这是因为在编译时,编译器无法确定这些符号引用对应的具体内存地址,只有在运行时才能确定。
    • 在运行时动态链接会根据符号引用关联实际的内存地址,过程可以分为两步
      • 符号解析:动态链接器会过呢据符号引用的名称,在程序可执行文件或共享库中查找对应的符号定义;
      • 地址重定位:当找到符号的定义时,动态链接器就会符号引用替换为实际内存地址,这样程序就可以调用对应的方法或访问变量了。
    • 将符号引用转换为调用方法的直接引用与方法的绑定机制有关
      • 静态链接:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
      • 动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接
    • 图中箭头所指就是符号引用,指向的是常量池中的地址。image.png
4. 方法返回地址
  • 介绍
    • 用于方法的返回值存储。方法的结束一般有两种,一种是正常退出,另一种是出现异常;但不论哪种方式退出, 在方法退出后都返回到该方法被调用的位置。正常退出时,方法调用的返回值为返回地址,即调用该方法的指令的下一条指令的地址(将值赋予左边接收的变量)。而通过异常退出的,返回地址需要通过异常表来确定,栈帧中一般不会保存这部分信息。
5. 其他附加信息
  • 例如,对程序调试提供支持的信息,但这些信息取决于具体的虚拟机实现。了解即可。
三、本地方法栈
  • 介绍
    • 类似与虚拟机栈,只不过该栈中存储的是navite表示的方法,即本地方法;
四、堆
  • 介绍

    • 堆为运行时数据区最大的一块内存区域,被所有线程共享的区域,“几乎”所有的对象都存储在堆中;堆划分为年轻代、老年代、元空间(jdk1.8之前为永久代);
    • 新生代:存储新创建的对象以及未达到老年代标准的对象信息(会有一个标记标志对象的年龄,默认是15岁,对象才会进入老年代);新生代又分为Eden区、Survivro区(Form区、To区);默认比例为8:1:1;
    • 老年代:存储达到指定条件的对象 或 大对象(指需要大量连续内存空间的对象),这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝;
    • 元空间:元空间是在本地内存(Native memory)上分配的,而不是在堆内存中。这意味着元空间的大小不受堆大小的限制,并且可以根据需要动态扩展。不过,如果元空间耗尽,将会抛出java.lang.OutOfMemoryError: Metaspace异常。元空间存储的内容包括:类的信息、常量池(字面量(如字符串和数字)以及符号引用(如类名、方法名、字段名等))、静态变量、即时编译器生成的代码;元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 指定;
  • 对象的分配过程

    • 新创建的对象会放入Eden区
    • 当Eden区填满时,再来一个新对象,发现内存不够,会发生Minor GC,清除无用对象,将剩余对象放入Survivor Form区
    • 随后Survivor的Form区和To区发生转换,To区变为Form区,Form区变为To区(可以理解为将存活对象放入了To区)
    • 等下一次Eden再次填满时,会再次发生Minor GC,会清理Eden区和Survivor区的无用对象。清理完毕后将存活对象放入Form区,然后Form区和To区再此进行转换。如果To区有剩余存活对象,那么Form区和To区都会有对象,每次发生转换时对象的年龄都会进行+1;
    • 等对象年龄到达15岁时,会进入老年代(或大对象会直接进入老年代)
    • 当老年代内存区域不足时,会发生 Major GC,清理老年代的无用对象。若清理完后内存还是不足,会触发Full GC,清理整个 Java 堆和方法区的垃圾对象。
    • 再不足就会OOM
  • 疑问

    • Q: 如何设置堆内存大小
    • A: 通过使用JVM参数-Xms和-Xmx来设置,一般这两个会设置成一样的,避免GC后再需要重新分隔计算堆的大小,从而提高性能;默认初始堆内存大小为电脑内存大小/64;最大内存为电脑内存大小/4;
    • Q: “几乎”所有的对象都存储在堆中, 为什么不是所有
    • A: 随着技术的演进,JVM会根据逃逸分析、标量替换进行对对象的判别是再栈 上分配还是在堆上分配;栈上分配会减少GC的压力。而堆上分配又衍生出**“TLAB”, **这个是说因为堆是被所有线程共享的,是会发生并发安全的,所以会有锁机制,但这样会影响效率,所以会优先 “TLAB”,这种是 会为每个线程划分出一小片区域存储对象,默认情况下,该区域大小为Eden的1%;一旦对象在 TLAB 空间分配内存失败时,JVM 就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存。
五、方法区
  • 介绍
    • 方法区(Method Area)是Java虚拟机(JVM)中一个特殊的内存区域,用于存储类的结构信息和运行时常量池。在Java 8及更高版本中,HotSpot JVM使用元空间(Metaspace)替代了永久代(PermGen),但它们的功能相似,都用来存放方法区中的数据。也是线程的共享区域,方法区也被成为堆的逻辑区,非堆。
    • 方法区通常包含以下内容:
      • 类的信息:(类名、访问权限(public、protected、private等)、接口信息、父类信息、字段描述符、方法描述符等。)
      • 常量池:字面量(如字符串和数字)以及符号引用(如类名、方法名、字段名等)。
      • 静态变量:类变量(static修饰的变量)和接口变量。
      • 即时编译器生成的代码:如果使用的是HotSpot JVM,并且启用了JIT(Just-In-Time)编译器,那么经过优化的代码也会存储在方法区中。
  • 运行时常量池
    • 一个 Java 源文件中的类、接口,编译后产生一个字节码文件。而 Java 中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候用到的就是运行时常量池。
    • 常量池可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
    • 在加载类和结构到虚拟机后,就会创建对应的运行时常量池
六、总结
  • 首先,我们介绍了程序计数器,它负责跟踪线程执行的位置。然后,我们研究了虚拟机栈和本地方法栈,它们分别用于管理Java方法和本地方法的调用。接下来,我们探讨了堆,它是存储对象实例的主要内存区域。此外,我们还了解了方法区,其中包含了类的信息、常量池和静态变量等数据。
    通过这次探索,我们不仅了解了JVM如何管理内存,还明白了这些数据区域对于优化Java应用程序的重要性。熟悉这些数据区域的工作原理将有助于我们更好地诊断并解决诸如内存泄漏、性能瓶颈等问题,同时也能帮助我们编写出更加高效且健壮的Java应用程序。

在这篇博客中,我们深入探讨了Java虚拟机(JVM)的运行时数据区。通过细致的分析和讨论,我们对每个数据区域的特点、作用以及它们之间的交互有了更深入的理解。
在结束这次旅程之际,我希望你已经收获了关于JVM运行时数据区的重要知识,并能够将其应用到实际的编程工作中。无论你是Java新手还是经验丰富的开发者,我都鼓励你继续深入学习和探索JVM的更多细节,以提升你的编程能力。

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值