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

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

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

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

相关文章

Vivado报错集合

Synth 8-5535 报错代码 [Synth 8-5535] port <clk_0> has illegal connections. It is illegal to have a port connected to an input buffer and other components. The following are the port connections : Input Buffer:Port I of instance clkin1_ibufg(IBUF) i…

SwiftUI中List的样式及使用详解(添加、移动、删除、自定义滑动)

SwiftUI中的List可是个好东西&#xff0c;它用于显示可滚动列表的视图容器&#xff0c;类似于UITableView。在List中可以显示静态或动态的数据&#xff0c;并支持垂直滚动。List是一个数据驱动的视图&#xff0c;当数据发生变化时&#xff0c;列表会自动更新。 针对List&#x…

树莓派4B 有电但无法启动

试过多个SD卡&#xff0c;反复烧系统镜像都无法启动。接HDMI显示器没有信号输出&#xff0c;上电后PWR红灯长亮&#xff0c;ACT绿灯闪一下就不亮了&#xff0c;GPIO几个电源脚有电&#xff0c;芯片会发热&#xff0c;测量多个TP点电压好像都正常。 ……

华为造车布局全曝光,对标奔驰、迈巴赫等

文 | Auto芯球 作者 | 雷慢 这一刻&#xff0c;我承认我格局小了&#xff0c; 就在刚刚&#xff0c;余承东曝光了华为智选车的布局计划&#xff0c; 华为问界、智界、享界等&#xff0c;将全面对标奔驰、迈巴赫、劳斯莱斯等车系&#xff0c; 这布局&#xff0c;确实是世界…

揭秘《庆余年算法番外篇》:范闲如何使用维吉尼亚密码解密二皇子密信

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

【yolov10】使用自己的数据集训练目标检测模型

【yolov10】使用自己的数据集训练目标检测模型 一、anaconda安装二、环境配置三、数据集制作1、labelimg的安装2、使用labelimg 四、正片1、下载yolov10源码2、数据集目录3、训练4、推理 一、anaconda安装 直接参考前一篇博客&#xff1a; https://blog.csdn.net/m0_71523511/…

击穿盲点——【网络安全】社会工程学中的网络欺骗

社会工程学起源于上世纪60年代左右&#xff0c;是一种通过人际交流的方式来获得情报的非技术渗透手段。这种手段无需过多技术要求&#xff0c;却非常有效&#xff0c;目前已成为危害企业网络安全的重大威胁之一。著名黑客凯文米特尼克在《反欺骗的艺术》中曾提到&#xff0c;人…

SpringBoot+Vue开发记录(七)-- 跨域文件与Restful风格

本篇文章的主要内容是关于项目的跨域配置和给项目添加restful风格接口。 重点是文件粘贴 文章目录 一、 跨域二、Restful风格1. 什么是restful风格&#xff1f;2. 项目文件结构3. 新建文件4. 在Controller中进行修改 一、 跨域 跨域问题暂时也就那样&#xff0c;解决方法就是…

云计算-No-SQL 数据库 (No-SQL Database)

DynamoDB简介 (Introduction to DynamoDB) AWS DynamoDB 是亚马逊提供的一种 NoSQL 数据库&#xff0c;适用于需要快速访问的大规模应用程序。NoSQL 数据库指的是非关系型数据库&#xff08;或许应该称为“非关系数据库”&#xff09;。关系型数据库是你之前可能使用过的熟悉的…

深入Django项目实战与最佳实践

title: 深入Django项目实战与最佳实践 date: 2024/5/19 21:41:38 updated: 2024/5/19 21:41:38 categories: 后端开发 tags: Django 基础项目实战最佳实践数据库配置静态文件部署高级特性 第一章&#xff1a;Django项目架构与设计原则 Django框架概述 Django是一个高级的P…

Next.js里app和pages文件夹的区别

最近开始学 Next.js&#xff0c;因为纯自学&#xff0c;有时候网上找到的学习资料都是几年前的&#xff0c;难免会有点 outdated&#xff0c;因此当自己创建的项目结构和视频里呈现的结构不一致时&#xff0c;难免会有点困惑。 例如&#xff0c;今天遇到的第一个问题就是&…

CAD二次开发(2)-将直线对象添加到CAD图形文件

1. 准备工作 创建一个类库项目&#xff0c;如下&#xff1a; 2. 分析Line对象 Line类的初始化方法和参数 using Autodesk.AutoCAD.DatabaseServices; Line line new Line();Line 继承Curve 继承Entity 继承DBObject 继承Drawable 继承RXObject 初始化方法有两个&#xf…

大模型分布式训练并行技术分享

目前业内解决大模型问题&#xff0c;基本以多节点、分布式方案为主。分布式方案具体的实施时&#xff0c;又分为数据并行、参数并行、流水线并行等&#xff0c;针对具体的业务场景采取合适的并行方案方可带来更高的效率。 后续结合业内主流的分布式框架&#xff0c;具体介绍各种…

数据库(5)——DDL 表操作

表查询 先要进入到某一个数据库中才可使用这些指令。 SHOW TABLES; 可查询当前数据库中所有的表。 表创建 CREATE TABLE 表名( 字段1 类型 [COMMENT 字段1注释] ...... 字段n 类型 [COMMENT 字段n注释] )[COMMENT 表注释]; 例如&#xff0c;在student数据库里创建一张studen…

网络安全等级保护:正确配置 Linux

正确配置 Linux 对Linux安全性的深入审查确实是一项漫长的任务。原因之一是Linux设置的多样性。用户可以使用Debian、Red Hat、Ubuntu或其他Linux发行版。有些可能通过shell工作&#xff0c;而另一些则通过某些图形用户界面&#xff08;例如 KDE 或 GNOME&#xff09;工作&…

APP安全测试汇总【网络安全】

APP安全测试汇总 一.安装包签名和证书 1.问题说明 检测 APP 移动客户端是否经过了正确签名&#xff0c;通过检测签名&#xff0c;可以检测出安装包在签名后是否被修改过。如 果 APP 使⽤了 debug 进⾏证书签名&#xff0c;那么 APP 中⼀部分 signature 级别的权限控制就会失效…

Unity 生成物体的几种方式

系列文章目录 unity工具 文章目录 系列文章目录前言&#x1f449;一、直接new的方式创建生成1-1.代码如下1-2. 效果图 &#x1f449;二、使用Instantiate创建生成&#xff08;GameObject&#xff09;2-1.代码如下2-2.效果如下图 &#x1f449;三.系统CreatePrimitive创建生成3…

数据结构之栈和队列(超详解

目录 一.栈 1.栈的基本概念 2.栈的基本操作 3.栈的储存结构 ①栈的顺序储存 (1)基本概念 (2)代码实现 ②栈的链式储存 (1)基本概念 (2)代码实现 二.队列 1.队列的基本概念 2.队列的基本操作 3.队列的储存结构 ①队列的链式储存 (1)基本概念 ​编辑 (2)代码实现 ②…

Spring MVC+mybatis 项目入门:旅游网(一)项目创建与准备

个人博客&#xff1a;Spring MVCmybatis 项目入门:旅游网&#xff08;一&#xff09;项目创建与准备 | iwtss blog 先看这个&#xff01; 这是18年的文章&#xff0c;回收站里恢复的&#xff0c;现阶段看基本是没有参考意义的&#xff0c;技术老旧脱离时代&#xff08;2024年辣…