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

news2024/11/26 21:23:42

 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

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

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

相关文章

NettyのEventLoopChannel

Netty的重要组件&#xff1a;EventLoop、Channel、Future & Promise、Handler & Pipeline、ByteBuf 本篇主要介绍Netty的EventLoop和Channel组件。 1、Netty入门案例 服务器端的创建&#xff0c;主要分为以下步骤&#xff1a; 创建serverBootstrap对象。配置服务器的…

10.Docker Compose容器编排

文章目录 Compose简介安装和卸载步骤核心概念compose文件两要素 使用步骤Compose常用命令微服务测试本地编码打包编写Dockerfile文件构建镜像 不使用Compose调试使用Compose调试WordPress测试验证增量更新 Compose简介 ​ docker建议我们每一个容器中只运行一个服务,因为docke…

Misc之图片隐写

前几天忙高数和c考试去了。。。Web毫无进展&#xff0c;学学这个放松一下 一、工具准备 这里目前使用的工具为kali上的工具和安装在电脑上的Winhex&#xff0c;010editor&#xff0c;Stegsolve 二、png图片隐写 这里我就直接用题目学习了&#xff0c;也是参考了csdn上大佬的…

05通讯录管理系统——添加联系人

功能描述&#xff1a;实现添加联系人功能&#xff0c;联系人上限为1000人&#xff0c;联系人信息包括姓名、性别、年龄、联系电话、家庭住址。 添加联系人实现步骤&#xff1a; 1.设计联系人结构体 2.设计通讯录结构体 3.main函数中创建通讯录 4.封装添加联系人函数 5.测…

软考系统规划与管理师伴读脑图第9章

周末发系统规划与管理师的试听视频&#xff0c;占用了发送次数&#xff0c;所以上周的脑图推迟了今天发出。 不知不觉已经发到了第9章&#xff0c;感叹这就是坚持积累下来的力量&#xff0c;其实考试也是一样的道理。

《骑行健身:“柳叶刀”研究揭示的健康与经济双赢策略》

在这个物价飞涨、经济压力日益加重的时代&#xff0c;普通人如何在不增加额外负担的情况下提升生活质量&#xff1f;《柳叶刀》的最新研究为我们揭开了一个意想不到的秘密&#xff1a;坚持健身&#xff0c;尤其是骑行&#xff0c;竟等同于每年为自己赚取了一笔不小的财富。这一…

多叉树的DFS深度优先遍历,回溯法的基础算法之一

一、前言 多叉树一般用于解决回溯问题。 想必大家都学过二叉树&#xff0c;以及二叉树的深度优先遍历和广度优先遍历&#xff0c;我们思考&#xff1a;能不能将二叉树的DFS转化为多叉树的DFS&#xff1f; 二、多叉树的结构 多叉树的本质&#xff0c;就是一棵普通的树&#x…

C语言数据存储大小端问题

大小端 什么是大小端 大端模式&#xff08;Big-endian&#xff09;&#xff0c;是指数据的高字节&#xff0c;保存在内存的低地址中&#xff0c;而数据的低字节&#xff0c;保存在内存的高地址中; 小端模式&#xff08;Little-endian&#xff09;&#xff0c;是指数据的高字…

mcms-5.2.8环境部署

1 数据库 1.1 新建数据库 1.2 导入数据表 2 tomcat配置 2.1 在IDEA中tomcat环境并配置 首先添加tomcat服务器并配置 配置Artifacts&#xff08;这里配置不正确的话&#xff0c;在运行时会报错&#xff1a;Error during artifact deployment. See server log for details.&am…

日常销售数据分析为什么重要?三个维度全面分析日常销售数据

在当今电子商务的浪潮席卷全球的时代&#xff0c;网店如雨后春笋般涌现&#xff0c;并且竞争日趋激烈。在这样一个充满挑战与机遇的环境中&#xff0c;如何洞察市场动向&#xff0c;把握消费者需求&#xff0c;实现销售业绩的稳步增长&#xff0c;成为每一位电商运营者必须面对…

【1990-2023】上市公司高新技术企业数据(Excel+stata)+do代码

数据简介&#xff1a;根据《上市公司资质认定信息文件》 数据进行整理。筛选“认定项目类型” 为“高新技术企业”&#xff1b;筛选“认定对象身份”为“上市公司本身”&#xff0c;根据“认定时间”和“有效期限”判断当年是否为高新技术企业。有效期限通常为3年&#xff0c;缺…

4.类,方法,对象

1.1.2. 面向对象程序设计的三大特征 1.1.2.1. 封装 面向对象编程核心思想之一就是将数据和对数据的操作封装在一起&#xff0c;形成一般的概念&#xff0c;比如类的概念。 1.1.2.2. 继承 继承体现了一种先进的编程模式。子类可以继承父类的属性和方法。 1.1.2.3. 多态 多…

CMake从安装到精通

目录 引言 1. CMake的安装 2. CMake的原理 3. CMake入门 3.1 CMakeLists.txt与注释 3.2 版本指定与工程描述 3.3 生成可执行程序 3.4 定义变量与指定输出路径 3.5 指定C标准 3.6 搜索文件 3.7 包含头文件 4. CMake进阶 4.1 生成动静态库 4.2 链接动静态库 4.…

【图像分割】DSNet: A Novel Way to Use Atrous Convolutions in Semantic Segmentation

DSNet: A Novel Way to Use Atrous Convolutions in Semantic Segmentation 论文链接&#xff1a;http://arxiv.org/abs/2406.03702 代码链接&#xff1a;https://github.com/takaniwa/DSNet 一、摘要 重新审视了现代卷积神经网络&#xff08;CNNs&#xff09;中的atrous卷积…

计算机组成原理(四)Cache存储器

文章目录 Cache存储器的基本原理cache命中率、平均访问时间、效率地址映射全相联映射直接映射组相联映射 查找算法cache 存储器替换策略cache 存储器-写操作策略习题 Cache存储器的基本原理 Cache是一种高速缓冲寄存器&#xff0c;是为了解决CPU和主存之间速度不匹配而采用的一…

检索增强生成(RAG)的挑战与优化措施

如何理解检索增强生成&#xff08;RAG&#xff09; 简单来说&#xff0c;RAG就是让LLM通过外部知识源获取额外信息&#xff0c;从而生成更准确、更符合上下文的答案&#xff0c;并减少错误信息&#xff08;或称为“幻觉”&#xff09;的产生。 我们都知道&#xff0c;最先进的…

计数排序(Counting Sort)

计数排序&#xff08;Counting Sort&#xff09; 计数排序是一个非基于比较的排序算法&#xff0c;该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时&#xff0c;快于任何比较排序算法。排序思路: 1.找出待排序数组最大值2.定义一个索引最大…

Python学习打卡:day08

day8 笔记来源于&#xff1a;黑马程序员python教程&#xff0c;8天python从入门到精通&#xff0c;学python看这套就够了 目录 day858、数据容器(序列)的切片序列的常用操作——切片 59、序列的切片课后练习60、集合的定义和操作集合的定义集合的操作添加新元素移除元素从集合…

NATAPP-内网穿透工具----下载与配置

NATAPP-内网穿透工具 基于ngrok的国内高速内网穿透服务&#xff0c;natapp提供了一种便利的方式&#xff0c;使得开发和测试过程更加高效&#xff0c;尤其是在需要进行远程调试或展示时。无论是进行web开发、微信和支付宝的本地开发调试&#xff0c;还是简单地从外部网络访问家…

如何根据使用场景选购3D扫描仪?

三维扫描建模是指通过专业的三维扫描仪对产品进行三维数据的采集&#xff0c;快速获取物体精确的3D数据&#xff0c;实现1:1复刻原物体&#xff0c;扫描后所得的数字化3D模型以obj、fbx、glb、gltf等格式保存。 积木易搭自主研发多款三维扫描设备&#xff0c;拥有多项国家专利&…