JVM(三)

news2025/3/15 4:22:12

在上一篇中,介绍了JVM组件中的类加载器,以及相关的双亲委派机制。这一篇主要介绍运行时的数据区域

JVM架构图:

JDK1.8后的内存结构:

 (图片来源:https://github.com/Seazean/JavaNote)  

而在运行时数据区域中,根据线程是否共享可以进行分类:

  • 线程不共享:程序计数器,本地方法栈,Java虚拟机栈。
  • 线程共享:堆,方法区。

1、程序计数器

        1.1、概述

        简称PC寄存器,用于存储当前线程正在执行的指令的地址或者下一条即将执行的指令的地址。在Java虚拟机中,每个线程都有自己独立的程序计数器,它是线程私有的,不会被线程切换所影响。

        它记录了当前线程正在执行的字节码指令的地址。当线程执行一般方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址,当线程执行的是本地方法源码中被native关键字修饰的方法)时,程序计数器的值为空(Undefined)。

        程序计数器的作用有以下几点:

  • 线程切换恢复: 当线程切换回来时,虚拟机通过程序计数器来确定线程上次执行到的位置,从而继续执行。(例如我现在有A,B两个线程并发执行某个方法,该方法有10条指令,A线程首先获得了执行权,在执行到第4条指令时CPU的时间分片结束,B线程获得到了执行权,从第1条指令开始执行,等待CPU时间分片再次结束,假设A线程获得了执行权,就从第4条指令继续执行。)
  • 指令定位: 程序计数器指示了当前正在执行的虚拟机指令的地址,帮助虚拟机准确定位下一条需要执行的指令。
  • 异常处理: 虚拟机使用程序计数器来记录异常处理代码的起始地址,以便异常处理完成后能够继续执行原来的代码。
  • 线程间通信: 在多线程环境下,程序计数器也可以用于线程间通信,例如实现轻量级的线程协作机制。
        1.2、案例

        例如有如下的一段代码

public class Demo1 {
    public static void main(String[] args) {
        int i = 0;
        if (i ==0){
            i--;
        }
        i++;
    }
}

        它的字节码指令是

 0 iconst_0
 1 istore_1
 2 iload_1
 3 ifne 9 (+6)
 6 iinc 1 by -1
 9 iinc 1 by 1
12 return

        其中每一行开头处的0,1等代表偏移量,在字节码或者内存中,偏移量表示了某个数据项相对于起始地址的偏移量,以字节为单位。

        在加载阶段,虚拟机将字节码的指令读取到内存后,会将偏移量转换为内存地址:

        代码的执行过程中,程序计数器会记录下一行字节码指令的地址,执行完当前指令,虚拟机的执行引擎会根据程序计数器执行下一条指令。


2、栈       

        首先明确一个概念:栈区别于队列,是一种先进后出的数据结构,类似于弹夹,先压入的子弹最后打出,后压入的子弹最先打出。

        并且在多线程环境下,栈之间是相互独立的,这一点在JUC并发编程篇中做过验证。

        在JVM中,栈又是由三部分组成:

  • 局部变量表:存放运行时的所有局部变量
  • 操作数栈:用于存放执行过程中的临时数据
  • 帧数据:包含动态链接,方法出口,异常表引用等
        2.1、局部变量表

        我现在有一段代码

public class Demo2 {
    public static void main(String[] args) {
        int i = 10;
        long j = 20;
    }
}

        编译后通过jclasslib查看:

        表头的含义:

  • Nr.:代表当前元素的编号,在案例中0代表args,1代表i,2代表j。
  • 起始PC:表示该局部变量的作用域的起始位置,即该局部变量在方法中有效的起始位置:

        这段字节码大致的含义是:

  1. 0 bipush 10: 这条字节码将整数10推送到操作数栈顶。bipush指令用于将一个字节(-128到127之间的整数)推送到操作数栈顶。

  2. 2 istore_1: 将操作数栈顶的整数值(之前推送的10)存储到索引为1的本地变量中。istore_1指令将整数值存储到本地变量表中索引为1的位置。

  3. 3 ldc2_w #2 <20>: 将一个常量(在常量池中的索引为2的项,可能是一个long或double类型的常量)推送到操作数栈顶。

  4. 6 lstore_2: 将操作数栈顶的long类型常量值(之前推送的常量)存储到索引为2的本地变量中。lstore_2指令将long类型的值存储到本地变量表中索引为2的位置。

  5. 7 return: 从当前方法返回,没有返回值。return指令用于从当前方法返回,结束方法的执行。

         由此可知,当i变量经过了1,2两步后,才算赋值完成,所以i的作用域是从3开始。j同理。

  • 长度:表示该局部变量的作用域的长度,即该局部变量在方法中有效的长度。
  • 序号:表示该局部变量在局部变量表中的索引位置。局部变量表是按索引顺序存储局部变量的,索引从0开始递增。

        而在实例方法中(区别于被static关键字修饰的静态方法),序号为0的位置会存放一个this。代表调用该方法的对象:

public class Demo2 {
    public static void main(String[] args) {

    }

    public void test1(){
        int i = 10;
        long j = 20;
    }
}


        如果是带有参数的方法,方法的参数也是会存放在局部变量表中的,例如main方法的args参数,在第一个案例中就有所体现。

        例如在某个实例方法中,有两个参数,并且有两个局部变量,那么在局部变量表中就会有5个元素。


        局部变量表中的序号也是能复用的:

public class Demo2 {
    public static void main(String[] args) {

    }

    public void test1(int k,int m){
        {
            int a = 1;
            int b = 2;
        }
        {
            int c = 1;
        }
        int i = 0;
        long j = 1;
    }
}

        上面的案例,在0号索引处存放了this,然后将参数k,m放在了1,2号索引处,第一个代码块中的a,b放在3,4号索引处。

        然后执行第二个代码块,a,b的作用范围已经结束了。就会把c放在原先a的3索引的位置。

        最后执行给i,j赋值的语句,c的作用范围也结束了,就会把i放在原先c的3索引位置,j方法原先b的4索引位置。

        2.2、操作数栈

        操作数栈的深度是在编译期就提前确定的:

        2.3、帧数据
        2.3.1、动态链接

        动态链接是指在方法调用时,JVM需要确定被调用方法的实际地址或者说是方法在内存中的具体位置。由于Java是一种面向对象的语言,方法调用可能涉及到多态性,即被调用方法的具体实现可能在运行时才能确定。

        动态链接会有以下的步骤:

  1. 查找方法: 当一个方法被调用时,JVM需要查找该方法的具体实现。首先,它会根据方法调用指令中的符号引用(Symbolic Reference)去找到对应的类和方法,这个过程叫做解析。

  2. 解析: 解析阶段会将符号引用解析为直接引用(Direct Reference),即找到被调用方法在内存中的具体位置。这个过程可能会涉及到类加载、链接等步骤。

  3. 绑定: 绑定是将方法调用指令与被调用方法的具体实现关联起来的过程。动态绑定是在运行时根据对象的实际类型来确定方法的具体实现。这种机制允许在程序运行时实现多态性。

        简单来说,动态链接表现在编译期无法确定,只能在运行期间将符号引用转换为直接引用。(编译和链接阶段,函数调用只是一个符号引用,不包含实际的地址。)

        与之相对的是静态链接,在编译阶段,所有的函数调用在链接时就被确定为了直接引用,所有的库函数以及其他被调用的函数的代码都会被复制到可执行文件中。(可执行文件在运行时不再依赖外部的库,因为所有的依赖关系在编译时已经被解决了。)

        一般的场景是,如果没有依赖外部的库或动态链接库,是一个独立的执行文件,则是静态链接。如果你的程序需要使用系统提供的共享库或第三方库,则是动态链接。

        2.3.2、异常表

        异常表是一种数据结构,用于管理和处理Java程序中的异常。异常表存储在方法的字节码中,并由JVM在方法执行期间使用。

public class Demo1 {
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 1;
        } catch (Exception e) {
            i = 2;
        }
    }
}

        对应的字节码指令:

 0 iconst_0
 1 istore_1
 2 iconst_1
 3 istore_1
 4 goto 10 (+6)         -- 如果没有发生异常,就直接跳到第十步。
 7 astore_2
 8 iconst_2
 9 istore_1
10 return

        对应的异常表

        其中起始PC和结束PC就是try...catch块的作用范围,跳转PC为出现异常时执行的代码,捕获类型为捕获何种异常,在案例中是所有Exception类型的。


        在栈中,是可能存在内存溢出问题的,通常的原因是递归没有正确设置退出条件,导致栈溢出。

public class Demo1 {

    static int count = 0;

    public static void main(String[] args) {
        test1();
    }

    private static void test1() {
        System.out.println(count++);
        test1();
    }
}

        在执行了大约9800次的时候发生了栈溢出(StackOverflowError)。

        栈的大小是可以通过JVM参数进行设置的,如果没有设置栈的大小,JVM也会创建一个默认大小的栈,其大小取决于不同的操作系统。

        如果需要手动修改栈的大小,可以通过JVM参数:-Xss栈大小 实现:

        例如我将其设置成为了512M:

        如果局部变量过多,操作数栈深度过大也会影响栈内存的大小。

3、堆

        堆内存是用于存储对象实例的内存区域,是 Java 程序中最主要的内存区域之一。堆内存由 JVM 在运行时动态分配和管理,用于存储所有通过New关键字创建的对象实例以及数组对象。

        3.1、对象实例

        栈中的局部变量表,可以存放堆上对象的引用:

        同时堆的内存也会存在溢出现象(OutOfMemoryError):

public class Demo1 {
    public static void main(String[] args) throws InterruptedException, IOException {

        ArrayList<Object> objects = new ArrayList<Object>();
        while (true){
            objects.add(new byte[1024 * 1024 * 100]);
        }
    }
}


         我们也可以通过arthas工具的dashboard命令进行堆内存使用情况的查看:

  • used:代表当前已使用的内存。
  • total:代表虚拟机分配的可用堆内存。
  • max:是java虚拟机可以使用的最大堆内存。

        简单来说,当used大于等于total时,total会扩容,但是最大不能超过max。

        我们通过在上面的案例的循环中加上

        while (true){
            System.in.read();
            objects.add(new byte[1024 * 1024 * 100]);
//            Thread.sleep(1000);
        }

        验证一下,当执行了两次循环后发生了扩容:

        堆内存的大小也是可以通过JVM命令去设置的,如果没有设置,max默认是系统最大运行内存的1/4,total是1/64。

        修改total的命令是:-Xms,修改max的命令是:-Xmx 其中Xms必须大于1M,Xmx必须大于2M,建议将Xms和Xmx设置成相同的值。

        3.2、字符串常量池

        字符串常量池用于存储代码中定义的常量字符串。在JDK1.8中,字符串常量池不位于方法区中,而是在堆中(运行时常量池位于直接内存的元空间中)。

        例如我现在有以下的代码:

public class Demo2 {
    public static void main(String[] args) {
        String a = "1";
        String b = "2";
        String c = "12";
        String d = a + b;
        System.out.println(c == d);
    }
}

        最终运行的结果是什么?答案是false,通过分析字节码指令,其原因在于,当我们执行String d = a + b 时,在字节码的层面是创建一个StringBuilder的对象,创建的对象会被放在堆内存中。

        而c变量的值12是放在字符串常量池中的,所以指向的不是同一个地址(c指向的是字符串常量池中的12,d指向的是堆中的12),使用 == 判断的结果是false。

        修改一下上面的案例:

public class Demo3 {
    public static void main(String[] args) {
        String a = "1";
        String b = "2";
        String c = "12";
        String d = "1" + "2";
        System.out.println(c == d);
    }
}

        运行结果是true,执行 String d = "1" + "2";时不会产生新的对象,而是从字符串常量池中找到c变量的12。

        3.3、静态变量

        在JDK1.8后,静态变量存放在堆中,静态变量是属于类的,而不是属于类的实例,因此它们只会在类被加载时被初始化,并且在整个应用程序的生命周期内存在,直到应用程序结束或者类被卸载。

4、本地内存

        4.1、方法区

        用于存储类信息、常量、静态变量和即时编译器编译后的代码等数据。

        主要包含了:

  1. 类信息存储: 方法区主要用于存储加载的类信息,包括类的结构信息、字段信息、方法信息、父类信息、接口信息等。每个加载的类都有对应的 Class 对象在方法区中存储。

  2. 常量池: 方法区包含了常量池(Constant Pool),用于存储类中的常量信息,如字符串常量、基本类型常量、符号引用等。常量池在类加载时被创建,包括编译时生成的常量和运行时生成的常量

  3. 静态变量: 方法区还存储了类的静态变量,即被static修饰的类级别的变量。这些变量在类加载时被初始化,并在整个应用程序的生命周期内保持不变。

  4. 即时编译器产生的代码: 方法区还用于存储即时编译器(Just-In-Time Compiler,JIT)编译后的本地机器代码,这些代码用于提高 Java 程序的执行效率。

  5. 运行时常量池: 除了类加载时的常量池,方法区还包含了运行时常量池,它是在类加载完成后在方法区中动态生成的,用于存储运行时解析的常量信息。

        在JDK1.8之后,方法区中的永久代(Permanent Generation)元数据区(Metaspace)所取代。

        复习一下,在类的生命周期的加载阶段,类加载器加载完成后,JVM会将读取到的字节码信息保存到内存的方法区中,生成一个InstanceKlass对象,保存类的基本信息。

        方法中的静态常量池,连接阶段后,会将符号引用改变成直接引用。(连接阶段中的解析阶段,会将常量池中的符号引用替换成直接引用)。

        上面提到过栈和堆都有可能存在内存溢出的问题,而方法区同样可能会内存溢出:

  • 在JDK1.7及以前的版本中,方法区位于堆中的永久代空间。
  • 在JDK1.8及以后的版本中,方法区位于元空间中,和堆一样是独立的空间。(本地内存

        这样就造成了,在JDK1.7以前的版本,方法区大小受限于堆的大小,而之后的版本,方法区的大小则取决于操作系统的直接内存大小。

         同样也可以使用-XX:MaxMetaspaceSize= 命令分配元空间的大小。

        4.2、直接内存

        是一种在 Java 中进行内存分配和管理的机制,它不同于传统的 Java 堆内存和栈内存。直接内存并不是由 JVM 直接管理的,而是由操作系统管理的一块内存区域。

        主要用于提高IO的效率,优势在于它可以通过操作系统的零拷贝技术来实现高效的数据传输。在进行 I/O 操作或者进行大规模数据处理时,直接内存能够直接与操作系统进行交互,避免了数据的多次复制和拷贝,从而提高了系统的性能和效率。

        NIO在读写文件时,会将其放入直接内存,并且在上维护对直接内存地址的引用。

        如果需要创建直接内存,可以使用:

ByteBuffer directBuffer = ByteBuffer.allocateDirect(size);

        而直接内存和堆,栈,方法区一样,同样会存在内存溢出的问题:

        如果需要手动调整直接内存大小,可以通过JVM命令-XX:MaxDirectMemorySize = 大小


补充:

运行时常量池和常量池表:

  • 运行时常量池是每个类或接口的一部分,用于存储编译时生成的字面量常量和符号引用。除了字符串常量外,运行时常量池还包含其他类型的常量,如整数常量、浮点数常量等。运行时常量池是类加载过程中的一部分,在类加载后会被存储在方法区(JDK 8 及之前)或元空间(JDK 8 及之后)中。
  • 常量池表是 class 文件中的一部分,用于存储编译时生成的常量信息。它包含了类或接口中的所有常量,包括字符串常量、符号引用、方法名、字段名等。常量池表中的每个常量都有一个索引,可以通过索引来访问常量池中的具体内容。运行时常量池实际上是常量池表在运行时被加载到内存中的形式之一。

        常量池表在类加载后成为运行时常量池。

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

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

相关文章

SpringBoot+Vue开发记录(五)-- 数据库设计

我去&#xff0c;时隔这么久又开始了QAQ。主要是还是自己太懒了。 本篇文章的主要内容是数据库设计。 先简单创建个数据库&#xff1a; 这是创建好了的&#xff1a; 一、数据库设计 先就做一个很简单的设计&#xff0c;里面就只有用户和题。 大概就这样&#xff1a; 二、创…

【学习笔记】Windows GDI绘图目录

题外话 不知几时开始&#xff0c;觉得学习过程中将内容记录下来&#xff0c;有助于加强记忆&#xff0c;还方便后续查找&#xff0c;顺便帮助有需要的人&#xff0c;更有来自您阅读、点赞、收藏和评论时给我带来的动力与兴奋。 目录 【学习笔记】Windows GDI绘图(一)图形概述…

如何从U盘恢复误删除的文件

在许多情况下&#xff0c;用户可能会发现其U盘上的数据误删&#xff0c;并且无法访问或恢复它。在这篇文章中&#xff0c;我们将看到如何使用命令提示符尝试从U盘恢复损坏的文件和数据。我们还将列出一些免费的U盘恢复软件及其独特的功能&#xff0c;以便在前一种方法无法产生所…

PVE 虚拟机环境下删除 local-lvm分区

1、删除逻辑卷 lvremote pve/data 2、扩展逻辑卷 lvextend -l 100%FREE -r pve/root 3、 修改存储目录内容 点击 Datacenter - Storage &#xff08;1&#xff09;删除local-lvm分区 &#xff08;2&#xff09;编辑local分区&#xff0c;在内容一项中勾选所有可选项。

unity开发Hololens,使用unity自带的UGUI

hololens 使用UGUI 新建画布&#xff0c;添加组件&#xff0c; 画布模式改成WorldSpace&#xff0c;这样在能在3D场景里 随意的移动位置&#xff0c; 添加NearIteractionTouchaBleUnityUI、CanvasUtility组件 EaventsToReceive改成Pointer&#xff0c; 这样&#xff0c;UGUI的…

Idea中flume的Interceptor的编写教程

1.新建-项目-新建项目 注意位置是将来打包文件存放的位置&#xff0c;即我们打包好的文件在这/export/data个目录下寻找 2. 在maven项目中导入依赖 Pom.xml文件中写入 <dependencies> <dependency> <groupId>org.apache.flume</groupId> <artifa…

渲染管线——应用阶段

知识必备——CPU和GPU 应用阶段都做了什么 应用阶段为渲染准备了什么 1.把不可见的数据剔除 2.准备好模型相关数据&#xff08;顶点、法线、切线、贴图、着色器等等&#xff09; 3.将数据加载到显存中 4.设置渲染状态&#xff08;设置网格需要使用哪个着色器、材质、光源属性等…

【NumPy】NumPy性能优化与内存管理:解锁高效编程的高级策略

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

《Ai学习笔记》自然语言处理 (Natural Language Processing):常见机器阅读理解模型(上)02

Glove 词向量&#xff1a; 在机器理解中的词的表示&#xff1a; 词袋&#xff08;bow,bag of words&#xff09; one-hot 词向量 word2vec glove 目的&#xff1a;将一个词转换成一个向量 Word2vec 是一种用于生成词向量的工具包&#xff0c;由Google在2013年开源推出…

HMI设计:再谈上位机与下位机,附海量案例图

上期回顾&#xff1a;HMI界面之&#xff1a;上位机界面设计&#xff0c;一文扫盲 一、上位机负责控制和决策&#xff0c;下位机负责采集和执行 上位机和下位机是两个概念&#xff0c;通常用于描述计算机系统中不同层次的设备或组件。 上位机&#xff08;Host Computer&#x…

vue3 vite动态根据字符串加载组件

1 原理 import.meta.glob() 其实不仅能接收一个字符串&#xff0c;还可以接收一个字符串数组&#xff0c;就是匹配多个位置 let RouterModules import.meta.glob(["/src/view/*/*.vue", "/src/view/*.vue"]);这样我们就拿到了相对路劲的组件对象&#xf…

【学习笔记】Windows GDI绘图(五)图形路径GraphicsPath详解(上)

文章目录 图形路径GraphicsPath填充模式FillMode构造函数GraphicsPath()GraphicsPath(FillMode)GraphicsPath(Point[],Byte[])和GraphicsPath(PointF[], Byte[])GraphicsPath(Point[], Byte[], FillMode)和GraphicsPath(PointF[], Byte[], FillMode)PathPointType 属性FillMode…

最新版npm详解

如&#xff1a;npm中搜索 jQuery image.png image.png 接地气的描述&#xff1a;npm 类似于如下各大手机应用市场 image.png image.png 查看本地 node 和 npm 是否安装成功 image.png image.png 或 npm install -g npm image.png image.png image.png image.png image.…

Spring Boot集成Picocli快速入门Demo

1.什么是Picocli&#xff1f; Picocli是一个单文件命令行解析框架&#xff0c;它允许您创建命令行应用而几乎不需要代码。使用 Option 或 Parameters 在您的应用中注释字段&#xff0c;Picocli将分别使用命令行选项和位置参数填充这些字段。使用Picocli来编写一个功能强大的命…

16.线性回归代码实现

线性回归的实操与理解 介绍 线性回归是一种广泛应用的统计方法&#xff0c;用于建模一个或多个自变量&#xff08;特征&#xff09;与因变量&#xff08;目标&#xff09;之间的线性关系。在机器学习和数据科学中&#xff0c;线性回归是许多入门者的第一个模型&#xff0c;它…

蓝桥杯Web开发【大学组:省赛】2022年真题

1.水果拼盘 目前 CSS3 中新增的 Flex 弹性布局已经成为前端页面布局的首选方案&#xff0c;本题可以使用 Flex 属性快速完成布局。 1.1 题目问题 建议使用 flex 相关属性完成 css/style.css 中的 TODO 部分。 禁止修改圆盘的位置和图片的大小。相同颜色的水果放在相同颜色的…

根据Depth Quality Tool的z轴误差值确认相机是否需要进行相机内参校准

下载Depth Quality Tool深度质量验证工具 网盘链接【RealSense SDK v2.55.1】 链接&#xff1a;https://pan.baidu.com/s/1NrlbwNDBUL8wpWfVwbpMwA?pwd2jl0 提取码&#xff1a;2jl0 打开Depth Quality Tool深度质量验证工具 找一面墙作为目标&#xff0c;将摄像头水平对准墙…

ISCC 2024|Misc

FunZip ISCC{xoMjL8NuYRRb} Number_is_the_key ISCC{Sanoyq6qGIPF} 精装四合一 四张图片尾部都存在多余数据&#xff0c;把多余数据分别提取出来保存成文件&#xff0c;未发现规律。根据提示&#xff0c;预计需要将四部分多余数据进行合并。提取四个部分前16个字节&#xff0…

Golang并发编程-协程goroutine的信道(channel)

文章目录 前言一、信道的定义与使用信道的声明信道的使用 二、信道的容量与长度三、缓冲信道与无缓冲信道缓冲信道无缓冲信道 四、信道的初体验信道关闭的广播机制 总结 前言 Goroutine的开发&#xff0c;当遇到生产者消费者场景的时候&#xff0c;离不开 channel&#xff08;…

C语言 | Leetcode C语言题解之第97题交错字符串

题目&#xff1a; 题解&#xff1a; bool isInterleave(char* s1, char* s2, char* s3) {int n strlen(s1), m strlen(s2), t strlen(s3);int f[m 1];memset(f, 0, sizeof(f));if (n m ! t) {return false;}f[0] true;for (int i 0; i < n; i) {for (int j 0; j &l…