JVM(三)

在上一篇中,介绍了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 文件中的一部分,用于存储编译时生成的常量信息。它包含了类或接口中的所有常量,包括字符串常量、符号引用、方法名、字段名等。常量池表中的每个常量都有一个索引,可以通过索引来访问常量池中的具体内容。运行时常量池实际上是常量池表在运行时被加载到内存中的形式之一。

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

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

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

相关文章

Redis第18讲——Redis和Redission实现延迟消息

即使不是做电商业务的同学&#xff0c;也一定知道订单超时关闭这种业务场景&#xff0c;这个场景大致就是用户下单后&#xff0c;如果在一定时间内未支付&#xff08;比如15分钟、半小时&#xff09;&#xff0c;那么系统就会把这笔订单给关闭掉。这个功能实现的方式有很多种&a…

Windows远程连接命令?

Windows操作系统提供了多种远程连接命令&#xff0c;使用户可以通过网络连接到远程计算机&#xff0c;并在远程操作系统上执行操作。远程连接命令可方便实现远程工作、故障排查和系统维护等任务。本文将介绍几种常见的Windows远程连接命令及其基本使用方法。 远程连接命令 Win…

面向对象编程的奥秘:封装与继承

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、封装的魅力 封装的应用 封装示例 二、继承的力量 继承的应用 继承示例 三、总结 一…

OpenWrt U盘安装使用 详细教程 x86/64平台 软路由实测 系列一

1 官方稳定 版:OpenWrt 23.05 OpenWrt Downloads #根据实际情况选择 PC支持uefi,选择版本&#xff1a;https://downloads.openwrt.org/releases/23.05.3/targets/x86/64/openwrt-23.05.3-x86-64-generic-ext4-combined-efi.img.gz 2 rufus 制作U盘启动 3 制作好的U盘,接入主…

Maven多环境打包配置

一、启动时指定环境配置文件 在启动springboot应用的jar包时&#xff0c;我们可以指定配置文件&#xff0c;通常把配置文件上传到linux服务器对应jar包的同级目录&#xff0c;或者统一的配置文件存放目录 java -jar your-app.jar --spring.config.location/opt/softs/applicat…

新能源汽车的电驱热管理

前言 新能源汽车的电驱热管理是指维持电动汽车电池、电机和电控系统在适宜的工作温度范围内&#xff0c;保障车辆高效、安全、稳定运行的技术方案。随着新能源汽车的快速发展和普及&#xff0c;电驱热管理技术也日益成为关注焦点。本文将从电池、电机和电控系统三个方面介绍新…

Linux--线程的认识(一)

线程的概念 线程&#xff08;Thread&#xff09;是操作系统中进行程序执行的最小单位&#xff0c;也是程序调度和分派的基本单位。它通常被包含在进程之中&#xff0c;是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流&#xff0c;一个进程中可以并发多个线…

【leetcode面试经典150题】-80. 删除有序数组中的重复项 II

【leetcode面试经典150题】-80. 删除有序数组中的重复项 II 1 题目介绍2 个人解题思路2.1 代码2.2 思路 3 官方题解 1 题目介绍 给你一个有序数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使得出现次数超过两次的元素只出现两次 &#xff0c;返回删除后数组…

【DZ模板】克米设计APP手机版本地化+完美使用

模版介绍 【DZ模板】价值288克米设计APP手机版DZ模板 数据本地化完美使用 腾讯官方出品discuz论坛DIY的后台设置&#xff0c;功能齐全&#xff0c;论坛功能不亚于葫芦侠&#xff0c;自定义马甲&#xff0c;自定义认证&#xff0c;自定义广告&#xff0c;完全可以打造出自己想…

Redis教程(十五):Redis的哨兵模式搭建

一、搭建Redis一主二从 分别复制三份Redis工作文件夹&#xff0c;里面内容一致 接着修改7002的配置文件&#xff0c;【redis.windows-service.conf】 port 7002 改成 port 7002 slaveof 127.0.0.1 7001 7003也同样修改 port 7003 slaveof 127.0.0.1 7001 这样就指定了700…

从0开始带你成为Kafka消息中间件高手---第三讲

从0开始带你成为Kafka消息中间件高手—第三讲 实际上来说&#xff0c;每次leader接收到一条消息&#xff0c;都会更新自己的LEO&#xff0c;也就是log end offset&#xff0c;把最后一位offset 1&#xff0c;这个大家都能理解吧&#xff1f;接着各个follower会从leader请求同…

读人工智能时代与人类未来笔记14_管控人工智能

1. 管控人工智能 1.1. 历史上的战场进一步推进到与数字网络相连的所有地方 1.2. 数字程序现在控制着一个由众多实体系统构成的庞大且仍在不断增长的领域&#xff0c;而且越来越多的此类系统已实现网络化 1.2.1. 在某些情况下甚至连门锁和冰箱都实现了网络化 1.2.2. 这催生出…

vue3中element-plus下拉菜单与图标的使用

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 http://218.75.87.38:9666/ 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; h…

十、通配符和正则表达式

10.1 通配符 通配符是由shell处理的, 它只会出现在 命令的“参数”里。当shell在“参数”中遇到了通配符 时&#xff0c;shell会将其当作路径或文件名去在磁盘上搜寻可能的匹配&#xff1a;若符合要求的匹配存在&#xff0c;则进 行代换(路径扩展)&#xff1b;否则就将该通配…

21.Happens-Before原则

文章目录 Happens-Before原则1.Happens-Before规则介绍2.规格介绍2.1.顺序性规则(as-if-serial)2.2.volatile规则2.3.传递性规则2.4.监视锁规则2.5.start规则2.6.join()规则 Happens-Before原则 JVM内存屏障指令对Java开发工程师是透明的&#xff0c;是JMM对JVM实现的一种规范和…

HE TB PPDU MU-RTS

看起来像是MU-RTS的触发帧的应答不是HE TB PPDU&#xff0c;而是传统得的帧&#xff0c;应答CTS。 非AP 的STA&#xff0c;是不能发送触发帧&#xff0c;也就是说&#xff0c;触发帧&#xff0c;只能是由AP发送给STA

RedHat9 | DNS剖析-配置主DNS服务器实例

一、实验环境 1、BIND软件包介绍 BIND软件是一款开放源码的DNS服务器软件&#xff0c;由美国加州大学Berkeley分校开发和维护&#xff0c;全称为Berkeley Internet Name Domain。该软件在DNS&#xff08;域名系统&#xff09;领域具有重要地位&#xff0c;是目前世界上使用最…

网站笔记:huggingface model memory calculator

Model Memory Utility - a Hugging Face Space by hf-accelerate 这个工具可以计算在 Hugging Face Hub上托管的大型模型训练和执行推理时所需的vRAM内存量。模型所需的最低推荐vRAM内存量表示为“最大层”的大小&#xff0c;模型的训练大约是其大小的4倍&#xff08;针对Adam…

AI播客下载:The Logan Bartlett Show Podcast(AI创业投资主题)

Logan Bartlett Show Podcast是一个播客&#xff0c;主持人Logan Bartlett与科技界的领导者以及投资者进行对话&#xff0c;讨论他们在运营或投资企业中学到的经验教训&#xff0c;主要集中在科技创投领域。 Logan Bartlett 是 Redpoint Ventures 的投资人&#xff0c;并且在该…

用户态下屏蔽全局消息钩子 —— ClientLoadLibrary 指针覆盖

目录 前言 一、研究 SetWindowsHookEx 的机制 二、概念验证 三、运行效果分析 四、总结与展望 参考文献 原文出处链接&#xff1a;[https://blog.csdn.net/qq_59075481/article/details/139206017] 前言 SetWindowsHookEx 函数帮助其他人员注入模块到我们的进程&#x…