万字长文详述 - 带你了解Jvm虚拟机运行时数据区

 JVM虚拟机,对大部分Java程序员而言,是既熟悉又陌生的存在,Java程序在虚拟机的自动内存管理机制帮助下,减少了绝大部分的内存管理工作。但也正是因为如此,虚拟机如果出现了内存溢出或者泄露的情况,问题排查、BUG修复也成了一项异常艰苦的工作。

 本系列,是基于《JVM高级特性与最佳实现第3版》这本书,整理的个人学习笔记,旨在学习过程中,结合其他许多资料和博客,对以往模糊不清晰的知识点予以深度梳理。

 首先看一下运行时数据区域的定义。JVM虚拟机在执行Java程序的过程中,会把自身所管理的内存划分为多个区域,每个区域的用途、生命周期各不相同。说到运行时数据区域,必须了解的一本著作是《Java虚拟机规范》,这本书是Oracle官方发布的虚拟机开发规范,是所有厂商开发虚拟机时都要遵守的,也就是除了我们最常使用的Oracle虚拟机(HotSpot),像IBM的J9虚拟机,还有其他的厂商所开发的虚拟机,都必须要遵守这本书中定义的很多细节规范。在《Java虚拟机规范》中规定的运行时数据区域包含如下几个部分。

在这里插入图片描述
 这张图是从《JVM高级特性与最佳实践》中直接拿出来的,仔细看,除了真正的运行时数据区,还包含了和执行引擎本地接口本地方法库的交互。所以,今天的主要内容是梳理并学习运行时数据区域里面包含的程序计数器,,方法区,虚拟机栈,本地方法栈这5块内存区域。

1. 程序计数器

  • 程序计数器的工作原理是什么

 程序计数器存储了当前线程正在执行的虚拟机字节码指令(Java方法),比如下图PC寄存器的数字5可以理解为程序计数器所记录的数据,当前线程获取到时间片的时候,执行引擎会读取程序计数器的值,并找到5所对应的指令后进行运算。

 字节码解释器工作时,会不断地改变这个计数器的值,来记录下一条需要执行的字节码执行的行号。

在这里插入图片描述

  • 程序计数器存储字节码执行有什么用

 因为CPU需要不断的切换各个线程,当线程切换回来后,需要知道接着从哪里开始执行,字节码解释器通过改变程序计数器的值,来明确下一条应该执行什么字节码指令。

  • 程序计数器为什么没有内存溢出

 程序计数器使用了非常小的一块内存空间,只用于记录字节码指令地址,不会有内存膨胀的问题。在官方的《JVM虚拟机规范》中也没有规定此区域有任何的OOM的情况。

2. 虚拟机栈和本地方法栈

 虚拟机栈和本地方法栈同属于栈内存的一部分,不同点虚拟机栈执行的是Java方法,而本地方法栈执行的是Native方法。

  • Java方法执行的线程内存模型

 在JVM管理的内存中,有一块区域是虚拟机栈,每个线程创建时,都会在内存划分出一块线程所属的内存,线程销毁时所述内存被回收。

 线程执行Java程序时,会从某个Java方法开始,比如main()方法。Java方法被调用的时候,会在当前线程的栈内存中创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,返回方法,通常还会包含一些附加信息。

 这里的局部变量表包含了方法内部的基本数据类型(int/shot/long等),对象的引用(比如指向对象的指针),returnAddress类型(指向字节码指令地址的指针)。操作数栈是一个先进后出的结构,在方法执行的过程中,字节码指令会往操作数栈中写入或者读取数据,也就是入栈和出栈。动态链接为了支持方法调用过程中,将符号引用转换为直接引用。方法返回地址用于记录在方法执行完成退出或者抛出异常时,能够返回到当前方法被调用到的位置。

 下面,我们假设 main 方法内部调用a方法,a方法内部调用了b方法,b方法内部调用了c方法。

在这里插入图片描述

  • 本地方法栈

 在《Java虚拟机规范》中,对本地方法栈并没有做严格的要求,所以在具体实现中,HotSpot虚拟机中,直接把本地方法栈和虚拟机栈合二为一。在不同的操作系统中,要求的栈帧大小是不一样的,比如64位的Windows系统下,最低不能小于180k,否则虚拟机将无法启动。

  • OOM内存溢出

 在实际开发中,可能会碰到栈内存溢出的情况,但通常在现在的机制下,无论是栈帧太大或者虚拟机栈容量太小,当新的栈帧无法分配内存时,Hotspot虚拟机抛出的都是StackOveflowError的异常。

3. 堆

 堆内存是我们开发中最常使用到,也是出现问题最多的地方,使用不当经常会碰到堆内存溢出的情况。一个JVM实例只存在一个堆内存,是属于所有线程的,堆也是内存管理的核心区域,存放的主要是实例对象和数组。当然,需要明确的是 并不是所有的实例对象都会在堆中分配

  • 内存分配方式

 《JVM虚拟机规范》中,规定堆内存在逻辑上是连续的,在物理上可以是不连续的。所以Jvm对于两种不同情况的堆内存管理有 指针碰撞空闲列表,而决定使用哪种内存管理方式,则是由堆所采用的垃圾收集算法,是不是具备 内存空间压缩的能力。

指针碰撞:垃圾收集算法具备 内存空间压缩的能力,堆中使用过的内存放在一边,未使用的内存放在另一边,中间放着一个指针,每次分配新内存只需要移动指针就可以。在这里插入图片描述

空闲列表:堆内存并不规整,已使用和未使用的内存区域通过jvm维护的一张列表记录下来,这张表就是 空闲列表,当有对象分配时,只要在这张表上找到合适的记录,分配完成之后再更新表的数据就可以了。

在这里插入图片描述
 堆内存可以被设置为固定的大小,也可以设置成可以扩展的,通过-Xms和-Xmx两个参数来配置。

  • 堆内存的区域划分

 在内存回收的角度来看,由于很多GC收集器是基于分代理论设计的,所有经常会在讨论堆内存时,划分“新生代”、“老年代”、“永久代”这些经典的分代。但在讨论这些概念时,必须要声明使用的是哪种垃圾收集器为前提。在《Java虚拟机规范》一书中并没有对堆内存进行详细划分,这些堆内部分区的概念,仅仅是某些垃圾收集器的设计风格或者实现原理。在G1垃圾收集器出现之后,这样的分区概念逐渐被弱化,甚至已经不再体提及。

在这里插入图片描述
  上述图中,左侧为经典的分代设计,右侧为G1收集器的Region分区设计。

  • 堆内存分配的线程安全问题

 由于堆内存是可以被所有线程共享的,所以这里很容易出现线程不安全的问题。我们假设一个例子,两个线程都要进行对象的创建,A线程刚刚划分了一块内存准备分配对象,还没来得及将指针调整到正确位置,B线程获取到时间片以后,又读取了指针位置进行内存分配,这样就会出现问题。目前,虚拟机采用的方式基本上是CAS+失败重试的方式,还有一种是使用TLAB技术,也就是本地线程分配缓冲区。

 在虚拟机开启了TLAB (Thread-Local Allocation Buffer)功能的情况下,线程初始化的时候,虚拟机会在堆上划分出一小块TLAB内存来给当前线程使用,这样每个线程都有自己的空间,在分配内存时,就会在这块内存上分配,互相之间也不存在竞争的情况,解决线程安全性的同时也能提升效率。下面,我们假设在使用了内存分代技术的前提下,开启了TLAB的功能。

在这里插入图片描述

 需要说明的是,线程专属内存并不是所有的操作都会独享,只是在分配这一个动作上是独享的,而对于读取、对象的移动和回收都是可以被其他线程操作的。比如,分代情况下,TLAB是分配在Eden区的,但如果执行垃圾回收就有可能被移动到Survivor区。

 还有一点需要说明的是,TLAB的空间其实很小,对于大对象的分配还是有可能在堆上直接分配的,具体步骤是先去尝试在TLAB上分配,空间如果不如就判断需要进入老年代还是在Eden区域分配,在执行这样的操作时,就需要配上CAS+失败重试的操作。

 由于TLAB在分配的时候,是从堆中划分,类似于对象分配原理一样,在为线程分配TLAB内存空间时,需要进行并发控制来避免线程不安全的问题。所以,“堆是线程共享的内存区域”这句话也不完全正确,正是因为TLAB的存在,使得线程有了独享各自内存区域的可能。

 下面是一篇很好的关于TLAB技术的解析:< Hotspot Java对象创建和TLAB源码解析 >

4. 方法区

 方法区也是线程共享的内存区域,与Java堆不同的是,方法区中存储的是加载的类型信息、常量、静态变量、即时编译后缓存的代码数据等。在JDK8前后,方法区的实现做了比较大的改动,从永久代调整为元空间,我们来看一下这个过程。

在这里插入图片描述

 首先说明,不管是永久代还是元空间,都仅仅是jdk不同版本下,对方法区的实现方式不同而已,在《Java虚拟机规范》中并没有规定任何实现细节。我们来看一下永久代元空间的概念和定义。

4.1 永久代


永久代设计初衷是为了把堆分代的思路扩展到方法区,使得垃圾收集器可以像管理Java堆一样来管理方法区的内存,省去一部分专门给方法区写垃圾回收器的开发工作。但实际上,这种设计方式一直都带来了一些问题,我们看下永久代的特点。

  • 存在于JDK7及以前
  • 位置属于JVM堆内存的一部分
  • 存储内容包含JVM加载的类的信息、常量池、静态变量JIT编译后的代码等
  • 特点:
    • 设置参数(-XX:PermSize和-XX:MaxPermSize)对永久代大小有固定限制
    • 如果永久代内存空间不足,会抛出异常:java.lang.OutOfMemoryError: PermGen space

4.2 元空间


 随着oracle陆续收购了JRocket和HotSpot虚拟机,后续版本里面,就永久舍弃了永久代,使用本地内存,也就是元空间来实现方法区。我们看下元空间的特性:

  • 存在于JDK8及以后的版本

  • 位置上,不在虚拟机的堆内存中了,而是使用本地内存(操作系统内存),但是从操作系统的角度来看,元空间仍然是进程的一部分内存,这里的区别在于,与jvm堆内存相比,元空间不受JVM堆的大小限制。

  • 元空间的大小只会受到本地内存的限制,虽然可以通过一些参数去调整元空间的大小(比如 -XX:MetaspaceSize和-XX:MaMetaspaceSize),但是这些参数不会严格控制元空间的大小,而是用于垃圾收集器启动阈值和元空间自动增长的控制。

    • -XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
    • -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。

在这里插入图片描述

5. 常量池

 其实,我们应该听过好几种常量池,或这个在不同版本里面常量池的不同种实现,在这里,我们来梳理一下。

5.1 Class文件常量池:

  • Class文件常量池又可以叫静态常量池编译期常量池,此常量池实在Class文件编译阶段,由编译器生成并存储在字节码文件中的,是以二进制的形式记录在生成的.class文件中,所以在类文件编译之后,此常量池中记录的东西就不会再变化了。
  • Class文件常量池中存储的是类文件中包含的常量数据,以及对其他类、方法或者字段的符号引用,是每个类的私有常量池,与类一一对应。

5.2 运行时常量池:

  • 运行时常量池 也可以叫做动态常量池 ,类加载后在JVM中动态生成运行时常量池,每个类都会有自己的运行时常量池。类加载时,会把编译期间生成的类信息和常量等(存储在Class文件常量池中),会加载到类对应的运行时常量池中。
  • 运行时常量池相对于Class文件常量池一个特性是具备动态性,在类加载期间通过反射生成的类、方法等,也会动态加载到运行时常量池中。
  • 运行时常量池位于方法区中(1.7的永久代或1.8的者元空间),均不属于堆内存。

5.3 字符串常量池:

字符串常量池是jvm中一个特殊的存储区域,提供为所有线程可以共享的常量,所以在全局中仅此一份,可以提高存储空间、提升性能。

 由于字符串具有的不可变性,一旦创建,其值不能再改变。当我们在创建一个字符串时,jvm会首先检查字符串产量池中是否存在,如果存在就直接返回现有对象的引用,否在会创建此字符串并返回引用。这个机制在不同的java版本中有不同的表现。

jdk 1.7版本前:字符串常量池位于方法区(永久代)中,但是由于永久代的大小是固定的,而且难以调整,当存储的数据量超过永久代的最大容量时,就会抛出PermGen Space的错误,这种固定大小的设置限制了Java应用的扩展性,尤其是在大量使用字符串、动态生成类和大量使用反射的场景下。

 在这些版本中,字符串常量池位于方法区,而不在Java堆中,当我们在代码中创建一个字符串字面量时,如String s = "Hello",Jvm会检查字符串常量池中是否存在内容为"Hello"的字符串对象:

  • 如果存在,Jvm会直接返回该字符串对象在防范区中的地址引用;
  • 如果不存在,Jvm会在方法区的字符串常量池中创建一个新的字符串对象,并且返回这个对象的地址引用;
	// 此代码案例是运行在jdk1.6中//这一行代码中,在堆中产生了一个字符串为“12”的对象,但是在方法区中并没有“12”这个字符串,// 但需要注意的是,方法区中已经有了“1”和“2”的字符串。String str1 = new String("1") + new String("2");// 这一行代码中, str1.intern() 判断产量池中并没有“12”,于是在常量池中创建一个“12”的字符串,// 此时返回的是常量池中“12”这个字符串的地址信息。对于intern和str1来说,一个是在堆中的地址,// 一个是在常量池中的地址,所以,肯定是不相等的,所以intern == str1为false;String intern = str1.intern();// 这一行代码中,首先去常量池中查看有没有“12”,发现已经有了字符串“12”,这时候是字符串而不是引用,// 于是返回常量池中“12”这个字符串的地址信息,其实和intern是一样的返回值,所以intern==str2为true;// 同理,str1和str2,str1是堆中的地址信息,str2是常量池中的地址信息,所以str2==str1,也是false;String str2 = "12";System.out.println(str1 == str2);//falseSystem.out.println(intern == str1);//falseSystem.out.println(intern == str2);//true

jdk 1.7版本及其后:从Jdk1.7开始,字符串常量池被异动到了Java堆中。当我们创建一个字符串对象时,如String s = "Hello",此处创建的字符串对象位于Jvm的堆中,这里不是直接创建在字符串常量池的内存中,到这个时候,字符串常量池中存放的是指向Java堆中字符串对象的引用:

  • 如果已经存在一个等于"hello"的字符串对象的引用,直接返回该引用,注意是返回在堆中的引用地址;
  • 如果不存在,首先在堆内存中创建一个字符串对象,然后把堆中的地址引用存放在字符串常量池中,然后再返回该地址(这里的地址是堆中的地址引用);
// 以下代码是运行在jdk1.8中// 这一行代码中,在堆中产生了一个字符串为“12”的对象,但是在方法区中并没有“12”这个字符串,// 但需要注意的是,方法区中已经有了“1”和“2”的字符串。String str1 = new String("1") + new String("2");// 这一行代码中, str1.intern() 判断产量池中并没有“12”,于是将堆中“12”这个对象的地址信息复制到方法区中,// 并返回该地址信息(堆中的地址信息)。所以,intern的返回值与str1是一样的地址信息,intern == str1为true;String intern = str1.intern();// 这一行代码中,首先常产量池中查看有没有“12”,发现已经有了字符串“12”的引用,于是不再创建,// 直接返回该地址信息(堆中的地址信息)。所以,其实str2和str1是一样的地址信息,// str1==str2为true;intern的返回值与str2也是一样的地址信息,所以intern == str2为true。String str2 = "12";System.out.println(str1 == str2);//trueSystem.out.println(intern == str1);//trueSystem.out.println(intern == str2);//true

参考文章:https://blog.csdn.net/qq_45659753/article/details/131922160

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/29020.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++ SIMD性能优化

// 使用SIMD指令优化的向量加法 //<mmintrin.h> MMX //<xmmintrin.h> SSE //<emmintrin.h> SSE2 //<pmmintrin.h> SSE3 //<tmmintrin.h> SSSE3 //<smmintrin.h> SSE4.1 //<nmmintrin.h> SSE4.2 //<wmmintrin.h> AES //<imm…

一文细谈SNN的基本数学原理,LIF模型,STDP与STBP学习方法

首先本文是读完 如何看待第三代神经网络SNN&#xff1f;详解脉冲神经网络的架构原理、数据集和训练方法 原创-CSDN博客 一文通俗入门脉冲神经网络(SNN)第三代神经网络-CSDN博客 两篇文章的总结&#xff0c;文章仅用于学习。 本文主要讨论STDP和STBP方法。 我们都知道&…

JavaEE、SSM基础框架、JavaWeb、MVC(认识)

目录 一、引言 &#xff08;0&#xff09;简要介绍 &#xff08;1&#xff09;主要涉及的学习内容 &#xff08;2&#xff09;学习的必要性 &#xff08;3&#xff09;适用学习的人群&#xff08;最好有这个部分的知识基础&#xff09; &#xff08;4&#xff09;这个基础…

FFmpeg中内存分配和释放相关的源码:av_malloc函数、av_mallocz函数、av_free函数和av_freep函数分析

一、av_malloc函数分析 &#xff08;一&#xff09;av_malloc函数的声明 av_malloc函数的声明放在在FFmpeg源码&#xff08;本文演示用的FFmpeg源码版本为5.0.3&#xff0c;该ffmpeg在CentOS 7.5上通过10.2.1版本的gcc编译&#xff09;的头文件libavutil/mem.h中&#xff1a;…

Science | 稀土开采威胁马来西亚的生物多样性

马来西亚是一个生物多样性热点地区&#xff0c;拥有超过17万种物种&#xff0c;其中1600多种处于濒临灭绝的风险。马来西亚的热带雨林蕴藏了大部分的生物多样性&#xff0c;并为全球提供重要的生态系统效益&#xff0c;同时为土著社区带来经济和文化价值。同时马来西亚具有可观…

Python批量保存Excel文件中的图表为图片

Excel工作簿作为一款功能强大的数据处理与分析工具&#xff0c;被广泛应用于各种领域&#xff0c;不仅能够方便地组织和计算数据&#xff0c;还支持用户创建丰富多彩的图表&#xff0c;直观展示数据背后的洞察与趋势。然而&#xff0c;在报告编制、网页内容制作或分享数据分析成…

30.保存游戏配置到文件

上一个内容&#xff1a;29.添加录入注入信息界面 以 29.添加录入注入信息界面 它的代码为基础进行修改 效果图&#xff1a; 首先在我们辅助程序所在目录下创建一个ini文件 文件内容 然后首先编写一个获取辅助程序路径的代码 TCHAR FileModule[0x100]{};GetModuleFileName(NUL…

Apache Paimon系列之:Append Table和Append Queue

Apache Paimon系列之&#xff1a;Append Table和Append Queue 一、Append Table二、Data Distribution三、自动小文件合并四、Append Queue五、压缩六、Streaming Source七、Watermark Definition八、Bounded Stream 一、Append Table 如果表没有定义主键&#xff0c;则默认为…

离散数学复习

1.关系的介绍和性质 &#xff08;1&#xff09;序偶和笛卡尔积 两个元素按照一定的顺序组成的二元组就是序偶&#xff0c;使用尖括号进行表示&#xff0c;尖括号里面的元素一般都是有顺序的&#xff1b; 笛卡尔积就是有两个集合&#xff0c;从第一个集合里面选择一个元素&am…

github国内加速访问有效方法

这里只介绍实测最有效的一种方法&#xff0c;修改主机的Hosts文件&#xff0c;如果访问github网站慢或者根本无法访问的时候可以采用下面方法进行解决。 1、搜索一个IP查询网站 首先百度搜索选择一个IP查询的网站&#xff0c;这里我用下面这个网站&#xff08;如果该网站失效…

相约北京“信通院数据智能大会”

推动企业数智化转型发展&#xff0c;凝聚产业共识&#xff0c;引领行业发展方向&#xff0c;摩斯将参与信通院首届“数据智能大会”&#xff08;6月19-20日&#xff0c;北京&#xff09;。 本次大会设置多个主题论坛&#xff0c;将发布多项研究成果&#xff0c;分享产业最新实…

如何通过改善团队合作来提高招聘效率

当招聘顶尖人才时&#xff0c;时间就是一切。招聘效率取决于团队快速响应和完成任务的能力&#xff0c;但招聘经理和面试官并不总是最关心重要的招聘任务。更重要的是&#xff0c;求职者的经历取决于准备好的面试官是否准时出现。有时候最好的候选人会接受另一份工作&#xff0…

Spring Cloud Alibaba Nacos持久化配置

所谓的持久化就是将Nacos配置持久化存储到数据库里面&#xff0c;在0.7版本之前&#xff0c;在单机模式时nacos使用嵌入式数据库实现数据的存储&#xff0c;不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力。 ① 找到并执行sql脚本 这里路径为&#xff1a;n…

时间复杂度 空间复杂度分析

时间复杂度就是需要执行多少次&#xff0c;空间复杂度就是对象被创建了多少次。 O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(2^n) < O(n!) < O(n^n) 这里写目录标题 时间复杂度O(1)O(logn)、O(nlogn)O(mn)、O(m*n)最好、最坏情况时间复杂度平均情况…

32、循环语句while+until

一、循环控制语句 双层循环和循环语句的使用&#xff0c;while和until的语法使用 1、进入调试模式 在脚本里第一行写入set -x bash -x 脚本 1.1、echo 打印 continue&#xff1a;跳出当次&#xff0c;后续的条件成立&#xff0c;继续执行。 break&#xff1a;一旦break&am…

实时数仓Hologres V2.2发布,Serverless Computing降本20%

Highlight 新发布Serverless Computing&#xff0c;提升大任务稳定性&#xff0c;同时可降低20%计算成本 引擎性能优化&#xff0c;TPC-H 1TB测试相对V1.X 提升100% 实时湖仓加速架构升级&#xff0c;支持Paimon&#xff0c;直读ORC、Parquet数据性能提升5倍以上 新增实例监…

LLM中表格处理与多模态表格理解

文档处理中不可避免的遇到表格&#xff0c;关于表格的处理问题&#xff0c;整理如下&#xff0c;供各位参考。 问题描述 RAG中&#xff0c;对上传文档完成版式处理后进行切片&#xff0c;切片前如果识别文档元素是表格&#xff0c;那么则需要对表格进行处理。一般而言&#x…

JupyterLab使用指南(二):JupyterLab基础

第2章 JupyterLab基础 2.1 JupyterLab界面介绍 JupyterLab的用户界面非常直观和灵活。它包括文件浏览器、工作区、多标签页、命令面板和侧边栏等功能。以下是各个部分的详细介绍&#xff1a; 2.1.1 文件浏览器 文件浏览器位于界面左侧&#xff0c;用于导航和管理文件。你可…

计算机网络:网络层 - 虚拟专用网 VPN 网络地址转换 NAT

计算机网络&#xff1a;网络层 - 虚拟专用网 VPN & 网络地址转换 NAT 专用地址与全球地址虚拟专用网 VPN隧道技术 网络地址转换 NAT网络地址与端口号转换 NAPT 专用地址与全球地址 考虑到 IP 地址的紧缺&#xff0c;以及某些主机只需要和本机构内部的其他主机进行通信&…

cbsd创建ubuntu jail 时下载系统慢的问题解决

下载时速度慢 使用cbsd创建ubuntu jail的时候 cbsd jconstruct-tui 提示&#xff1a; no base dir in: /usr/jails/basejail/base_amd64_amd64_jammy Select base sources:0 .. CANCELa .. build b .. extract c .. pkg d .. repo 选了pkg没找到 fetch: https://pkg.convec…