万字长文详述 - 带你了解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);//false
    System.out.println(intern == str1);//false
    System.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);//true
    System.out.println(intern == str1);//true
    System.out.println(intern == str2);//true

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于YOLOv8m的船舶检测(附数据集和Coovally操作步骤)

本文主要内容:详细介绍了船舶检测整个过程&#xff0c;从创建数据集到训练模型再到预测结果全部可视化操作与分析。 文末有数据集获取方式&#xff0c;请先看检测效果 现状 船舶检测和识别是一项重要的任务&#xff0c;它涉及到航运安全、港口管理、海洋保护等方面&#xff0c…

YOLOv10涨点改进轻量化双卷积DualConv,完成涨点且计算量和参数量显著下降

本文独家改进:双卷积由组卷积和异构卷积组成,执行3x3 和 1x1 卷积运算Q代替其他卷积核仅执行 1x1 卷积。 DualIConv 显着降低了深度神经网络的计算成本和参数数量,同时在某些情况下令人惊讶地实现了比原始模型略高的精度。 我们使用 DualConv 将轻量级 MobileNetV2 的参数数量…

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

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

代码随想录——电话号码的字母组合(Leetcode17)

题目链接 回溯 class Solution {List<String> res new ArrayList<String>();StringBuilder str new StringBuilder();HashMap<String, String> Sites new HashMap<String, String>();public List<String> letterCombinations(String digit…

经验分享,xps格式转成pdf格式

XPS 是一种电子文档格式、后台打印文件格式和页面描述语言。有时候微软默认打印机保存的是xps格式&#xff0c;我们如何转换为pdf格式呢&#xff0c;这里分享一个免费好用的网站&#xff0c;可以实现。 网站&#xff1a;https://xpstopdf.com/zh/ 截图&#xff1a;

CAS Apereo 5.3.16 实现单点登录

1.CAS部署 服务端下载地址:cas5.3 1.下载好打开后,复制target/cas/WEB-INF/classes/META-INF/spring.factories target/cas/WEB-INF/classes/services下的Apereo-10000002.json和HTTPSandIMAPS-10000001.json target/cas/WEB-INF/classes下的application.properties和log4j…

访问jlesage/firefox镜像创建的容器中文乱码问题

目录 介绍总结 介绍 最近在使用jlesage/firefox镜像创建容器的时候&#xff0c;发现远程管理家里网络的时候中文会出现乱码&#xff0c;导致整个体验非常的不好&#xff0c;网上查找资料说只要设置环境变量ENABLE_CJK_FONT1 就可以解决问题&#xff0c;抱着试一试的态度还真的成…

苏州辰安塑业携塑料托盘、塑料物流箱解决方案亮相2024杭州快递物流展

苏州辰安塑业携塑料托盘、吹塑托盘、塑料卡板箱、塑料周转箱、塑料物流箱、塑料垃圾桶解决方案盛装亮相2024杭州快递物流展&#xff01; 展位号&#xff1a;3C馆A51 苏州辰安塑业有限公司&#xff0c;是一家专业从事塑料托盘、吹塑托盘、塑料卡板箱、塑料周转箱、塑料物流箱、…

北京十大金牌律师事务所(2024年权威高胜诉率推荐)

律师职业本身&#xff0c;是一个看起来很美、说起来很烦、听起来很阔、做起来很难的职业。所谓术业有专攻&#xff0c;律师的专业就是解决法律纠纷&#xff0c;负责为个人和组织提供法律咨询和代理法律服务。律师在执行其职责时需要遵守道德准则和法律规定&#xff0c;并以客户…

Compose 可组合项 - DatePicker、DatePickerDialog

一、概念 一般是以对话框的形式呼出&#xff0c;DatePickerDialog 就是对 DatePicker 的一个简单对话框封装。 Composable fun DatePicker( state: DatePickerState, modifier: Modifier Modifier, dateFormatter: DatePickerFormatter remember { DatePickerFor…

JSR303校验

校验的需求 前端请求后端接口传输参数&#xff0c;需要校验参数。 在controller中需要校验参数的合法性&#xff0c;包括&#xff1a;必填项校验、数据格式校验等在service中需要校验业务规则&#xff0c;比如&#xff1a;课程已经审核过了&#xff0c;所以提交失败。 servi…

面料识别技术:AI如何提升服装行业的效率与创新

面料识别技术&#xff1a;AI如何提升服装行业的效率与创新 随着人工智能技术的飞速发展&#xff0c;AI大模型在各个领域都展现出了其强大的应用潜力。特别是在服装行业&#xff0c;AI大模型的图片识别技术正在成为面料识别和设计创新的重要工具。本文将探讨AI大模型在服装行业…

vivado PIN

描述 引脚是基元或层次单元上的逻辑连接点。引脚允许 要抽象掉单元格的内容&#xff0c;并简化逻辑以便于使用。引脚可以 是标量的&#xff0c;包含单个连接&#xff0c;或者可以定义为对多个进行分组的总线引脚 信号在一起。 相关对象 引脚连接到一个单元&#xff0c;并且可以…

羊城杯 2020 a_piece_of_java

考点:JDBC反序列化打CC链动态代理类触发readobject 一眼看过去 好像只有一个mysql-connector-java 可以利用jdbc 可能的攻击路径就有1) Mysql服务器任意文件读取 2) JDBC反序列化打依赖链 出现了一个不常见的依赖库 serialkiller 做了反序列化的过滤器 可以尝试查看其源码 htt…

LabVIEW盾构机状态监测

随着城市化的加速&#xff0c;地铁成为了城市交通的重要组成部分。为了保障地铁施工安全和效率&#xff0c;提出了一种基于LabVIEW的地铁施工盾构异常状态监测方法。该方法利用LabVIEW软件进行数据采集和处理&#xff0c;通过异常监测技术实时监控盾构机的运行状态&#xff0c;…

期末考试老师怎样发成绩

期末成绩的公布&#xff0c;总是让老师感到焦虑。成绩&#xff0c;这一张张的数字&#xff0c;承载着学生一学期的努力&#xff0c;也牵动着家长们的心。 传统的成绩公布方式&#xff0c;写成绩条让学生带回家&#xff0c;或是通过私发家长的方式&#xff0c;都存在一定的弊端。…

如何用多线程执行 unittest 测试用例实现方案

前言 使用python做过自动化测试的小伙伴&#xff0c;想必都知道unittest和pytest这两个单元测试框架&#xff0c;其中unittest是python的官方库&#xff0c;功能相对于pytest来要逊色不少&#xff0c;但是uniitest使用上手简单&#xff0c;也受到的很多的小伙伴喜爱。一直以来都…

数据治理服务解决方案(35页WORD)

方案介绍&#xff1a; 本数据治理服务解决方案旨在为企业提供一站式的数据治理服务&#xff0c;包括数据规划、数据采集、数据存储、数据处理、数据质量保障、数据安全及合规等方面。通过构建完善的数据治理体系&#xff0c;确保企业数据的准确性、完整性和一致性&#xff0c;…

Linux操作系统以及一些操作命令、安装教程

Web课程完结啦&#xff0c;这是Web第一天的课程大家有兴趣可以传送过去学习 http://t.csdnimg.cn/K547r Linux-Day01 课程内容 Linux简介 Linux安装 Linux常用命令 1. 前言 1.1 什么是Linux Linux是一套免费使用和自由传播的操作系统。说到操作系统&#xff0c;大家比…

代码随想录算法训练营第二十六天|39. 组合总和、 40.组合总和II、 131.分割回文串

39. 组合总和 题目链接&#xff1a;39. 组合总和 文档讲解&#xff1a;代码随想录 状态&#xff1a;卡了一会儿 思路&#xff1a;先排序&#xff0c;方便剪枝。允许数字重复使用&#xff0c;因此递归调用时传入当前索引i。 题解&#xff1a; public class Solution {// 用于存…