JVM原理:JVM运行时内存模型(通俗易懂)

目录

  • 前言
  • 正文
    • 虚拟机栈
      • 局部变量表
      • 操作数栈
      • 动态链接
      • 方法返回地址
    • 本地方法栈
      • 本地方法存在的意义
      • 本地方法的调用
    • 虚拟机堆
      • 堆结构
        • Eden区
        • Survivor区域
        • 老年代Old区
        • 常用参数指令
    • 方法区
      • 常量池
      • 运行时常量池
      • 方法信息
      • 类信息
      • 域信息
      • JDK1.7前的方法区
      • JDK1.7时的方法区
      • JDK1.7后的方法区
    • 程序计数器
  • 总结

前言

做了几年开发,平时除了写代码造BUG和修复BUG之外,偶尔也会遇到反馈说程序较慢问题,要对程序性能排查与优化就得更深入学习,学习JVM可以帮助我们加深对JAVA的理解,让我们具备一定的性能排查与调优的能力,无非就是让程序别太卡或者别挂了,那挂了目前我遇到的主要是内存泄漏后导致OOM,或者内存分配不当,当机器内存不足时出发了Linux的保护机制,自动kill调占用内存最高的程序;所以我们要了解平时创建的对象、变量是如何存储的,这些知识点可以帮我们更好解决问题。

正文

文章中的JVM是以HotSpot为例:

JVM运行时数据区包括虚拟机栈、堆、方法区、程序计数器、本地方法栈。

在这里插入图片描述

虚拟机栈

虚拟机栈是线程私有的,也就是创建一个线程时,就会分配一个私有的栈,这个比较好理解,我们平时创建多线程时,每个方法里面的局部变量都是新的一份;

虚拟机栈维护了栈帧,每个方法都可以看作是一个栈帧。方法调用时会创建一个栈帧并且压栈,栈的特点是先进后出,比较符合我们程序的调用流程,比如A方法调用B方法,首先将创建栈帧A并压栈,当A方法中调用了B方法时创建栈帧B压栈,由于栈是先进后出,所以JVM在拿指令调用时,会先拿栈帧B进行调用。

自定义栈的大小
-Xss 512k

下面看下栈的结构:

在这里插入图片描述

局部变量表

局部变量表保存着方法中定义的局部变量,如基本数据类型、指向引用类型的地址指针、以及 returnAddress 类型。

局部变量表的数据随着栈帧的创建而存在,随着栈帧的销毁而销毁,这个比较好理解,每个方法的局部变量都是独立的,当方法调用完成后,局部变量就消失了,所以局部变量表是线程安全的。

栈是有空间大小的,所以当调用的方法较多时,会创建大量的栈帧,而栈帧又是占用内存空间,当创建栈帧时内存不足会导致stackoverflow栈溢出,栈溢出不会导致整个程序挂掉,但会导致当前线程挂掉。所以平常写递归要留意,不能出现死循环,不然最后就会报stackoverflow;

操作数栈

操作数栈也是栈结构,先入后出。操作数栈就是在程序计算指令执行前后用于保存临时数据的空间;

举个例子吧:

int a=1;
int b=1;
int c=a+b;

程序要执行以上的计算时

1、执行存值指令,将a=1放入操作数栈中

2、执行存值指令,将b=1放入操作数栈中

3、执行相加指令,出栈拿到a、b的值进行运算

4、将运算结果存入操作数栈中

5、执行赋值指令,将结果赋值给c,也就是存入局部变量表里

动态链接

我们编写Person类后,将其编译成class字节码文件,使用类加载器将其加载到内存中,此时会将类名、修饰符、变量名、方法名、方法返回值等类信息存入到常量池中,这里我们简单理解为维护了key-value表,如#01:0X71;

当调用方法时,会将指向方法地址的符号#01进行解析替换,此时指向方法地址的值就不在是#01,而是0X71了;所以动态链接会在程序运行时将间接地址引用转为直接地址引用;

方法返回地址

当一个返回调用结束后,返回值给上个方法,而且上个方法接收到返回值之后继续往下进行。而方法返回地址就是记录上个栈帧的位置,此时的栈帧出栈后,将返回结果存入下个栈帧的操作数栈中,然后执行赋值指令将值存入下个栈帧的局部变量表中;

这里我们知道会有两种返回方式,一种是程序正常退出,此时会返回值(如果有定义的话);一种是当前方法执行过程中抛出异常中断了方法,此时不会返回值;

本地方法栈

本地方法栈和虚拟机栈结构差不多,区别是虚拟机栈是为java方法(java写的代码)而设立的,而本地方法栈是为java代码中的native修饰的代码而设立的;并不是所有的虚拟机都有本地方法栈,如Sun HotSpot虚拟机直接就把本地方法栈和虚拟机栈合二为一。

本地方法存在的意义

本地方法会调用C语言或者C++类库,比如内核为了安全,要调度硬件时,不会轻易给你权限进行调度,你需要调用内核提供好的函数库接口,由它先来进行安全检查,再帮你进行调度。另一方面操作底层使用java代码难度较高,直接调用现成的库效率更高;

本地方法的调用

在这里插入图片描述

上图案例有点像调用Thread的start方法开辟新线程来执行Runnable的run方法过程;

虚拟机堆

当我们new一个对象时,会开虚拟机堆中开辟一块内存空间,引用类型和数组的数据都是存放在堆中。它是JVM中内存最大的一块空间,所有线程都可以访问它,所以它是共享,非线程安全的。

堆结构

我们程序中经常导致性能问题的地方就是堆了,由于它的空间很大,而程序长期运行必然会产生很多没有用的垃圾对象,所以JVM使用垃圾回收器对没有被引用的对象进行回收,垃圾回收过程会STW,用户线程会挂起,所以就必须减少STW的时间,垃圾回收算法后续文章会进行讲解;

由于堆空间很大,垃圾回收是需要对整个空间进行扫描的(Full GC),为了让垃圾回收得更快,这里将堆空间划分为年轻代和老年代,年轻代包括eden区、survivor0、survivor1区域;

在这里插入图片描述

Eden区

new对象时,会在Eden区开辟内存空间,当Eden区满了之后,会触发年轻代垃圾回收YGC

Survivor区域

这里有两块Survivor是因为YGC垃圾回收采用复制算法,将没有被垃圾回收的对象拷贝到另一块空间;

如Student对象刚开始在Eden区,当触发YGC之后,由于Student对象还被其它对象引用,所以不会进行回收。由于是采用复制算法,会将没有回收的对象迁移到Survivor中,当第二次YGC之后,Student对象还未被回收,将survivor0迁移到survivor1中。第三次YGC时,又将survivor1中没有被回收迁移到survivor0中。

那这些没有被垃圾回收的对象,一直占用着空间,如果占用较多时,YGC的频率就会特别高,所以这里引入了年龄的概念。每次YGC后,未被回收的将年龄+1,当年龄到达一定阈值时(默认15),迁移到老年代中,降低YGC频率。如果Survivor区满了,则直接进入老年代。

老年代Old区

老年代主要存放YGC过程中,一直没有被回收的对象。当老年代满了之后会触发FULL GC,此时老年代和年轻代都会进行垃圾回收,这个时间是比较久的,所以我们程序优化中要尽量减低FULL GC发生的频率;

常用参数指令

设置堆最小值:-Xms

设置堆最大值:-Xmx

设置年轻代大小:-Xmn

设置Eden:survivor0:survivor1比例:-XX:SurvivorRatio

晋升老年代的动态年龄: -XX:MaxTenuringThreshold

方法区

类编译后被加载到内存中后,其类修饰符、类名、父类信息、方法名称、变量名称等存入方法区的常量池中,方法区是各个线程共享的,JVM关闭时该区域空间就会被释放;

常量池

如下案例:

public class HelloWorld {public static void main(String[] args) {System.out.println("hello world");}
}

JVM翻译成指令后:

// ===========================================类的描述信息===============================================
Classfile /xx/xx/xx/xx/HelloWorld.classLast modified 2021-10-12; size 569 bytesMD5 checksum 7f4f0fe4b6e6d04ddaf30401a7b04f07Compiled from "HelloWorld.java"
public class org.memory.jvm.t5.HelloWorldminor version: 0major version: 49flags: ACC_PUBLIC, ACC_SUPER// ===========================================常量池===============================================
Constant pool:#1 = Methodref          #6.#20         // java/lang/Object."<init>":()V#2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;#3 = String             #23            // hello world#4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V#5 = Class              #26            // org/memory/jvm/t5/HelloWorld#6 = Class              #27            // java/lang/Object#7 = Utf8               <init>#8 = Utf8               ()V#9 = Utf8               Code#10 = Utf8               LineNumberTable#11 = Utf8               LocalVariableTable#12 = Utf8               this#13 = Utf8               Lorg/memory/jvm/t5/HelloWorld;#14 = Utf8               main#15 = Utf8               ([Ljava/lang/String;)V#16 = Utf8               args#17 = Utf8               [Ljava/lang/String;#18 = Utf8               SourceFile#19 = Utf8               HelloWorld.java#20 = NameAndType        #7:#8          // "<init>":()V#21 = Class              #28            // java/lang/System#22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;#23 = Utf8               hello world#24 = Class              #31            // java/io/PrintStream#25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V#26 = Utf8               org/memory/jvm/t5/HelloWorld#27 = Utf8               java/lang/Object#28 = Utf8               java/lang/System#29 = Utf8               out#30 = Utf8               Ljava/io/PrintStream;#31 = Utf8               java/io/PrintStream#32 = Utf8               println#33 = Utf8               (Ljava/lang/String;)V// =======================================虚拟机中执行编译的方法===========================================
{public org.memory.jvm.t5.HelloWorld();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 7: 0LocalVariableTable:Start  Length  Slot  Name   Signature0       5     0  this   Lorg/memory/jvm/t5/HelloWorld;// main方法JVM指令码public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)V// main方法访问修饰符描述flags: ACC_PUBLIC, ACC_STATIC// main方法中的代码执行部分// ===============================解释器读取下面的JVM指令解释并执行===================================             Code:stack=2, locals=1, args_size=1// 从常量池中符号地址为 #2 的地方,先获取静态变量System.out0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;// 从常量池中符号地址为 #3 的地方加载常量 hello world3: ldc           #3                  // String hello world// 从常量池中符号地址为 #3 的地方获取要执行的方法描述,并执行方法输出hello world5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V// main方法返回8: return// ==================================解释器读取上面的JVM指令解释并执行================================// 行号映射表LineNumberTable:line 9: 0line 10: 8// 局部变量表LocalVariableTable:Start  Length  Slot  Name   Signature0       9     0  args   [Ljava/lang/String;
}

上面类经过编译成.class文件后,再然后将class文件反编译为JVM指令码后,我们可以看到常量池中记录了类的名称、方法名等符号引用,还有#23等字面量

  • 字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;如: #23 = Utf8 hello world
  • 符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。如: #3 = String #23

运行时常量池

运行时常量池是当类被加载到内存时的版本,上述案例可以看到每个类都会存在常量池,所以当常量池被加载进内存,将数据放入运行时常量池之后,也是每个类都有一份,此时符号引用会被解析成直接引用;

如上述案例中的 #3 = String #23 会变成 #3 = String hello world

运行时常量池是一个统称,它包括了字符串常量池、类名称常量、方法名称常量、静态变量池、基础数据常量池等;

方法信息

存储方法要运行的指令、局部变量表、返回类型等信息

类信息

存储类的描述信息、枚举、接口、父类等信息

域信息

域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)

JDK1.7前的方法区

此类版本的方法区实现叫永久代,它存在于JVM内存之内,堆内存之外,这块区域由于有空间大小限制,当JVM加载太多的Class类时,垃圾回空间跟不上存入空间时,会报会报内存溢出异常OutOfMemoryError:PermGen space。

下面命令可用来指定空间大小:

  • -XX:PermSize来设置永久代初始分配空间。

  • -XX:MaxPermSize来设定永久代最大可分配空间。

在这里插入图片描述

JDK1.7时的方法区

此版本将字符串常量和静态变量池放到了堆中,其它还是在永久代中

在这里插入图片描述

JDK1.7后的方法区

此版本的方法区,由metaSpace实现,存在于本地内存中,也就是JVM外的内存空间,它受限与物理内存,当物理内存不足时,会内存溢出;

在这里插入图片描述

程序计数器

每个线程都有一个程序计数器用于记录当前线程要执行的指令地址;由于程序运行一般是多线程,单CPU数量少于线程数量时,就会存在并发,每个线程会获得CPU的执行权。如执行线程A时,由于CPU分配的时间片到了,此时将当前线程挂起,线程B获取CPU的使用权,当线程B执行完毕后或者时间片用完后,需要恢复线程A的执行,此时如果想CPU从上次执行点开始往下执行的话,就需要记录之前的指令;

程序计数器记录着当前线程要执行的下条执行,当CPU从程序计数器拿到指令的引用之后,需要将下条指令的引用地址维护进来。如果是调用native方法时,程序计数器的记录值就为空。

总结

JVM理论知识点很多,平常如果很少实操这些东西是很难真正懂的,中文社区的JVM文章五花八门,很多写的都不一样。这篇文章有些按自己的理解不一定准确,但是我感觉这些知识点有利于我们解决问题也就够了,如有错误望指出修改。

内存模型了解之后,我们知道了数据存哪里了,那内存只有那么大,而程序一直运行,会不断占用内存空间,程序又是如何保证内存一直能放得下数据,那就得学习垃圾回收了。

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

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

相关文章

【MYSQL篇】Update语句原理详解

文章目录 前言缓冲池Buffer PoolInnoDB 内存结构redo logundo logBinlog 总结 前言 前面的文章我们已经对MySQL的查询语句的执行流程进行了说明&#xff0c;感兴趣的可以去看看&#xff1a; 【MySQL篇】Select语句原理详解 本篇文章我们来聊聊 MySQL更新语句的执行原理。更新…

【JavaSE】方法

目录 【1】一个小例子 【2】方法概念及使用 【2.1】什么是方法(method) 【2.2】方法定义 【2.3】方法调用的执行过程 【2.4】实参和形参的关系(重要) 【1.5】没有返回值的方法 【2】函数重载 【2.1】为什么需要方法重载 【2.2】方法重载概念 【2.3】方法签名 【3】…

卷积神经网络--猫狗系列之下载、导入数据集

(由于是学习&#xff0c;所以文章会有一些报错及解决办法) 在Kaggle()获取数据集&#xff1a;&#xff08;没有账号先去注册一个账号&#xff0c;在注册时可能会出现的问题见Kaggle注册出现一排“Captcha must be filled out.”&#xff01;&#xff09; https://www.kaggle.…

vue3+wangEditor5/vue-quill自定义上传音频+视频

一.各种编辑器分析 Quill 这是另一个常用的富文本编辑器&#xff0c;它提供了许多可定制的功能和事件&#xff0c;并且也有一2个官方的 Vue 3 组件 wangEditor5 wangEditor5用在Vue3中自定义扩展音频、视频、图片菜单&#xff1b;并扩展音频元素节点&#xff0c;保证音频节…

曝光调整和曝光融合论文粗读

曝光调整论文调研 M. Afifi, K. G. Derpanis, B. Ommer and M. S. Brown, “Learning Multi-Scale Photo Exposure Correction,” 2021 IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), Nashville, TN, USA, 2021, pp. 9153-9163, doi: 10.1109/CVPR4…

linux epoll/select使用区分和实例对比

Linux内核poll&#xff0c;ppoll&#xff0c;epoll&#xff0c;select代码位置&#xff1a; poll&#xff0c;ppoll&#xff0c;select相关内核实现在在fs/select.c中; epoll_ctl和epoll_wait相关函数在fs/eventpoll.c中 epoll实测不支持监听普通文件&#xff0c;select可以…

基于Python+MySQL所写的医院管理系统

点击以下链接获取源码资源&#xff1a; https://download.csdn.net/download/qq_64505944/87985429?spm1001.2014.3001.5503 目录 摘要 I 1 需求分析 1 1.1 任务描述 1 1.2 需求分析的过程 1 1.3 业务需求 2 1.4 功能描述 2 2 总体设计 3 2.1 系统开发环境 3 2.2 系统功能流…

26488-24-4,Cyclo(D-Phe-L-Pro),具有良好的生物相容性

资料编辑|陕西新研博美生物科技有限公司小编MISSwu​ 【产品描述】 Cyclo(D-Phe-L-Pro)环(D-苯丙氨酸-L-脯氨酸)&#xff0c;环二肽是由两个氨基酸通过肽键环合形成&#xff0c;是自然界中小的环状肽。由于其存在两个酰胺键即四个可以形成氢键的位点&#xff0c;环二肽可以在氢…

系统没有“internet信息服务(IIS)管理器”

系统没有“internet信息服务&#xff08;IIS&#xff09;管理器” 解决方案1.打开控制面板&#xff0c;找到并打开程序。2.找到并打开程序里的启用或关闭windows功能。3.在‘Internet Information Services’下的‘web管理工具’中找到IIS相关功能&#xff0c;在前面的复选框中…

Java Spring多线程

Java Spring多线程 开启一个线程1 继承java.lang.Thread类2 实现java.lang.Runnable接口3 实现Callable接口4 实现线程池ThreadPoolExecutor 操作线程线程的状态1 等待线程 join()2 中断线程 interrupt()3 守护线程&#xff08;Daemon Thread&#xff09; Java线程池Executors …

pinia状态管理

1.pinia是什么&#xff1f; 官网解释&#xff1a; Pinia 是 Vue 的存储库&#xff0c;它允许您跨组件/页面共享状态。 2.为什么要使用pinia&#xff1f; 优点&#xff1a; Vue2和Vue3都支持&#xff0c;这让我们同时使用Vue2和Vue3的小伙伴都能很快上手。pinia中只有state、…

MySQL:七种 SQL JOINS 的实现(图文详解)

MySQL&#xff1a;7种SQL JOINS的实现 前言一、图示表示二、代码举例1、INNER JOIN&#xff08;内连接&#xff09;2、LEFT JOIN&#xff08;左连接&#xff09;3、RIGHT JOIN&#xff08;右连接&#xff09;4、OUTER JOIN&#xff08;全连接&#xff09;5、LEFT EXCLUDING JOI…