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

这篇具有很好参考价值的文章主要介绍了JVM原理:JVM运行时内存模型(通俗易懂)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

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

正文

文章中的JVM是以HotSpot为例:

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

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

虚拟机栈

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

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

自定义栈的大小
-Xss 512k

下面看下栈的结构:

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

局部变量表

局部变量表保存着方法中定义的局部变量,如基本数据类型、指向引用类型的地址指针、以及 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代码难度较高,直接调用现成的库效率更高;

本地方法的调用

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

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

虚拟机堆

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

堆结构

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

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

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

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.class
  Last modified 2021-10-12; size 569 bytes
  MD5 checksum 7f4f0fe4b6e6d04ddaf30401a7b04f07
  Compiled from "HelloWorld.java"
public class org.memory.jvm.t5.HelloWorld
  minor version: 0
  major version: 49
  flags: 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: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       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.out
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         // 从常量池中符号地址为 #3 的地方加载常量 hello world
         3: ldc           #3                  // String hello world
         // 从常量池中符号地址为 #3 的地方获取要执行的方法描述,并执行方法输出hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         // main方法返回
         8: return
    // ==================================解释器读取上面的JVM指令解释并执行================================
      // 行号映射表
      LineNumberTable:
        line 9: 0
        line 10: 8
      // 局部变量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       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来设定永久代最大可分配空间。

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

JDK1.7时的方法区

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

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

JDK1.7后的方法区

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

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

程序计数器

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

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

总结

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

内存模型了解之后,我们知道了数据存哪里了,那内存只有那么大,而程序一直运行,会不断占用内存空间,程序又是如何保证内存一直能放得下数据,那就得学习垃圾回收了。文章来源地址https://www.toymoban.com/news/detail-478675.html

到了这里,关于JVM原理:JVM运行时内存模型(通俗易懂)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 区分什么是Java内存模型(JMM)和 JVM运行时数据区

    Java的内存区域和内存模型是不一样的东西,内存区域是指 JVM 运行时将数据分区域存储,强调对内存空间的划分 。 而内存模型(Java Memory Model,简称 JMM )是 定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式 ,如果我们要想深入了解Java并

    2024年02月11日
    浏览(50)
  • 通俗易懂的GPT原理简介

            GPT是自然语言处理领域中最受欢迎的模型之一。GPT全称为Generative Pre-trained Transformer,是一种基于Transformer模型的预训练语言模型。在接下来的文章中,我们将详细介绍GPT的原理。         在了解GPT原理之前,需要了解以下一些基础知识: 1.自然语言处理:自然语言处

    2024年02月05日
    浏览(44)
  • 最通俗易懂的讲解HTTPS的加密原理【多图、易懂】

    目录 前言 HTTPS加密原理概述 HTTP 为什么不安全 安全通信的四大原则 HTTPS 通信原理 对称加密:HTTPS 的最终加密形式 非对称加密:解决单向的对称密钥的传输问题 数字证书:解决公钥传输信任问题 证书一整个被掉包怎么办? 总结 其它 HTTPS 相关问题 什么是双向认证? 什么是

    2024年02月05日
    浏览(61)
  • 通俗易懂的ChatGPT的原理简介

          本文介绍了ChatGPT的原理及应用。ChatGPT是基于GPT模型的自然语言处理技术,可以进行语言理解、文本生成、对话生成等多种应用。其基本原理是通过训练大规模语料库中的数据,生成模型,从而实现自然语言处理的任务。本文着重介绍了ChatGPT的训练方法和应用场景,并

    2023年04月19日
    浏览(41)
  • 通俗易懂生成对抗网络GAN原理(二)

    学习李宏毅机器学习课程总结。 前面学习了GAN的直观的介绍,现在学习GAN的基本理论。现在我们来学习GAN背后的理论。 假设x是一张图片(一个高维向量),如64 * 64 * 3的图片,每个图片都是高维空间中的一个点。为了画图方便,我们就画成二维上的点。在高维空间中,只有

    2024年02月12日
    浏览(46)
  • 数据结构:Mysql索引原理(通俗易懂)

    在工作中如果经常写业务代码,说白了就是CURD,比如做一个查询功能,我们会将前端参数传递到后端后拼接SQL查询数据,那为了提高用户体验,查询速度肯定是越快越好,这就要求我们能够对SQL调优,让查询速度更快。 由于数据需要持久化,且数据量庞大,所以只能将数据

    2024年02月11日
    浏览(51)
  • 202 vue2的响应式原理 通俗易懂!

    Object.defineProperty + 依赖追踪 。 在Vue实例化过程中,会 递归 地将 每个数据对象 的 属性 转换为 getter/setter ,并维护一个 依赖收集器(Dep) 。 每个属性 都有一个关联的 watcher ,当 数据发生 变化时 , watcher 会 被通知 并 更新视图 。 Vue 2.x 实现响应式数据: vue实例化 时,会

    2024年02月06日
    浏览(41)
  • Vue中的MVVM模型(通俗易懂版)

    MVVM是Model-View-ViewModel的简写,是M - V - VM 三部分组成。 本质:是MVC改进版 MVVM就是将其中View的状态和行为抽象化,其中ViewModel将试图(即View)和业务逻辑分开,它可以去除Model的数据的同时帮忙处理View中由于需要展示内容而涉及的业务逻辑。 MVVM采用: 双向数据绑定。 View中

    2023年04月26日
    浏览(31)
  • 用通俗易懂的方式讲解:CatBoost 算法原理及案例

    前面已讲了7节,为方便大家学习,我总结在一起,无论是日常实践还是面试使用,都非常方便,喜欢记得收藏 用通俗易懂的方式讲解:逻辑回归模型及案例(Python 代码) 用通俗易懂的方式讲解:决策树模型及案例(Python 代码) 用通俗易懂的方式讲解: 随机森林及案例(

    2024年04月12日
    浏览(43)
  • 【通俗易懂】git原理、安装及连接gitlab,github

    目录 一、GIT原理【这部分也挺简单,可以看看,如果没时间可以直接跳到第二部分】 SVN与Git的的区别 二、安装Git   2.1 获取Git安装程序 2.2 Git安装过程 三、Git连接Gitlab 3.1 gitlab准备工作 3.2 本地计算机准备工作及配置git   四、Git连接Github Git 是一种分布式版本控制系统,用

    2024年02月22日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包