JVM内存模型有这篇文章就够了

标签: Java底层  字符串  jvm  java  jdk

一、你了解JVM内存模型吗

在这之前需要知道

内存寻址过程
在这里插入图片描述
地址空间划分

  • 内核空间是用于连接硬件,调度程序联网等服务
  • 用户空间,才是java运行的系统空间

我们知道JVM是内存中的虚拟机,主要使用内存进行存储,所有类、类型、方法,都是在内存中,这决定着我们的程序运行是否健壮、高效。

JVM内存模型图——JDK1.8

在这里插入图片描述

  • 线程私有:程序计数器、虚拟机栈、本地方法栈
  • 线程共享:MetaSpace、Java堆
    下面我们会对图中五个部分进行详细说明

1.1、程序计数器

  • 当前线程所执行的字节码行号指示器(逻辑)
  • 通过改变计数器的值来选取下一条需要执行的字节码指令
  • JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令,为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程计数器不会互相影响。所以,程序计数器和线程是一对一的关系即(线程私有
  • 对Java方法计数,如果是Native方法则计数器值为Undefined,Native方法是由非Java代码实现的外部接口
  • 程序计数器是为了防止内存泄漏
    在后边的举例中我们可以看到程序计数器的作用。

1.2、Java虚拟机栈(Stack)

  • Java方法执行的内存模型
  • 生命周期和线程是相同的,每个线程都会有一个虚拟机栈,栈的大小在编译期就已经确定了
  • 栈的变量随着变量作用域的结束而释放,不需要jvm垃圾回收机制回收。
  • 包含多个栈帧
    • 栈帧包含
      • 局部变量表
        • 包含方法执行过程中的所有变量(所有类型)
      • 操作数栈
        • 入栈、出栈、复制、交换、产生消费变量
      • 动态连接
      • 返回地址
        +在这里插入图片描述

在Java虚拟机栈中,一个栈帧对应一个方法,,方法执行时会在虚拟机栈中创建一个栈帧,而且当前虚拟机栈只能有一个活跃的栈帧,并且处于栈顶,当前方法结束后,可能会将返回值返回给调用它的方法,而自己将会被弹出栈(即销毁),下一个栈顶将会被执行。

举例说明:

ByteCodeSample.java

package com.mtli.jvm.model;

/**
 * @Description:测试JVM内存模型
 * @Author: Mt.Li
 * @Create: 2020-04-26 17:47
 */
public class ByteCodeSample {
    public static int add(int a , int b) {
        int c= 0;
        c = a + b;
        return c;
    }
}

对其进行编译生成.class文件

javac com/mtli/jvm/model/ByteCodeSample.java

然后用javap -verbose 进行反编译

javap -verbose com/mtli/jvm/model/ByteCodeSample.class

生成如下:

Classfile /E:/JavaTest/javabasic/java_basic/src/com/mtli/jvm/model/
ByteCodeSample.class
  Last modified 2020-4-26; size 289 bytes
  MD5 checksum 2421660bb241239f1a67171bb771521f
  Compiled from "ByteCodeSample.java"
public class com.mtli.jvm.model.ByteCodeSample
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
// 描述类信息
Constant pool:
   #1 = Methodref          #3.#12         // java/lang/Object."<ini
t>":()V
   #2 = Class              #13            // com/mtli/jvm/model/Byt
eCodeSample
   #3 = Class              #14            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               add
   #9 = Utf8               (II)I
  #10 = Utf8               SourceFile
  #11 = Utf8               ByteCodeSample.java
  #12 = NameAndType        #4:#5          // "<init>":()V
  #13 = Utf8               com/mtli/jvm/model/ByteCodeSample
  #14 = Utf8               java/lang/Object
 // 以上是常量池(线程共享)
{
  public com.mtli.jvm.model.ByteCodeSample();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/O
bject."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
// 以上是初始化过程
  public static int add(int, int);
    descriptor: (II)I  // 接收两个int类型变量
    flags: ACC_PUBLIC, ACC_STATIC // 描述方法权限和类型
    Code:
      stack=2, locals=3, args_size=2 // 操作数栈深度 、 容量  、参数数量
         0: iconst_0
         1: istore_2
         2: iload_0
         3: iload_1
         4: iadd
         5: istore_2
         6: iload_2
         7: ireturn
      LineNumberTable:
        line 10: 0 // 这里的第0行对应我们代码中的第10行
        line 12: 2
        line 13: 6
}
SourceFile: "ByteCodeSample.java"

执行add(1,2)

以下是程序在JVM虚拟机栈中的执行过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WTPww4yl-1588037601095)(七、你了解Java的内存模型吗.assets/image-20200426182038122.png)]
图不是很清楚,我来说一下过程,最下边的是程序计数器(前边提到的),最上边是操作指令,中间是局部变量表和操作数栈(位置从0开始)

  • 最开始,我们int c = 0,所以操作数栈顶初始值为0,局部变量表存储变量值。
  • istore_2 就是出栈的意思,将0放入变量表2的位置
  • iload_0 就是入栈,将1复制并压入操作数栈
  • 然后将位置在1的值“2”压入栈
  • 在栈中执行add方法,得到“3”
  • 将栈顶“3”取出到变量表的2位置
  • 再次将“3”压入栈,准备return
  • 方法返回值

执行完之后,当前线程虚拟机栈的栈帧会弹出,对应的其他方法与当前栈帧的连接释放、引用释放,它的下一个栈帧成为栈顶。

1.1.1、java.lang.StackOverflowError问题

我们知道,一个栈帧对应一个方法,存放栈帧的线程虚拟栈是有深度限制的,我们调用递归方法,每递归一次,就会创建一个新的栈帧压入虚拟栈,当超出限度后,就会报此错误。

举例说明:

package com.mtli.jvm.model;

/**
 * @Description:斐波那契
 * F(0)=0,F(1)=1,当n>=2的时候,F(n) = F(n-1) + F(n-2),
 * F(2) = F(1) + F(0) = 1,F(3) = F(2) + F(1) = 1+1 = 2
 * 0, 1, 1, 2, 3, 5, 8, 13, 21, 34...
 * @Author: Mt.Li
 * @Create: 2020-04-26 18:33
 */
public class Fibonacci {
    public static int fibonacci(int n) {
        if(n>=0){
            if(n == 0) {return 0;}
            if(n == 1) {return 1;}
            return fibonacci(n-1) +fibonacci(n-2);
        }
        return n;

    }

    public static void main(String[] args) {
        System.out.println(fibonacci(0));
        System.out.println(fibonacci(1));
        System.out.println(fibonacci(2));
        System.out.println(fibonacci(3));
        System.out.println(fibonacci(1000000));
        // java.lang.StackOverflowError
    }
}

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kXQkjtgK-1588037891688)(七、你了解Java的内存模型吗.assets/image-20200426185224796.png)]

解决方法是限制递归次数,或者直接用循环解决。

还有就是,由JVM管理的虚拟机栈数量也是有限的,也就是线程数量也是有限定。

由于栈帧在方法返回后会自动释放,所有栈是不需要GC来回收的。

1.3、本地方法栈

  • 与虚拟机栈相似,主要作用于标注了native的方法

1.4、元空间(MetaSpace)

元空间(MetaSpace)在jdk1.7之前是属于永久代(PermGen)的,两者的作用就是记录class的信息,jdk1.7中,永久代被移入堆中解决了前面版本的永久代分配内存不足时报出的OutOfMemoryError,jdk1.8之后元空间替代了永久代

  • 元空间使用本地内存,而永久代使用的是jvm的空间

1.4.1、MetaSpace相比PermGen的优势

  • 字符串常量池存在永久代中,容易出现性能问题和内存溢出(空间大小不如元空间)
  • 类和方法的信息大小难以确定,给永久代的大小指定带来了困难
  • 永久代会为GC带来不必要的复杂性
  • 方便HotSpot与其他JVM如Jrockit的集成

1.5、Java堆(Heap)

  • 对象实例的分配区域,实例在此处分配内存
  • java堆可以处于不连续的物理空间中,只要逻辑上是连续的即可
  • 是GC管理的主要区域,按照GC分代回收的方法,java堆又分为新生代老生代(以后会出一篇GC相关的)

二、JVM三大性能调优参数 -Xms -Xmx -Xss的含义

  • -Xss:规定了每个线程虚拟机栈(堆栈)的大小(一般情况下256k足够)
  • -Xms:堆的初始值
  • -Xmx:堆能达到的最大值

三、Java内存模型中堆和栈的区别——内存分配策略

需要先了解

  • 静态存储:编译时确定每个数据目标在运行时的存储空间需求,不允许有可变的程序存在,比如循环
  • 栈式存储:数据区需求在编译时未知,运行时模块入口前确定。存储局部变量,定义在方法中的都是局部变量,所以,方法先进栈,创建栈帧等操作,方法一旦返回,即变量离开作用域,则栈帧释放,变量也会释放。(生命周期短)
  • 堆式存储:编译时或运行时模块入口都无法确定,动态分配。堆存储的是数组和对象,存储结构复杂,所需空间更多,哪怕是实体中的一个属性数据消失,这个实体也不会消失。(生命周期长)

区别

  • 管理方式:栈自动释放,堆需要GC
  • 空间大小:栈比堆小
  • 碎片相关:栈产生的碎片远小于堆
  • 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
  • 效率:栈的效率比堆高,堆更灵活
  • 联系:引用对象、数组时,栈里面定义变量保存堆中目标的首地址
    在这里插入图片描述

四、元空间、堆、线程独占部分间的联系——内存角度

我们来看下面这个例子:
在这里插入图片描述
以下是各个部分包含的内容:
在这里插入图片描述

  • 元空间里面存着类的信息,比如方法、变量
  • java堆中存放对象实例
  • 线程独占:用来保存变量的值即变量的引用、对象的地址引用,记录行号,用来记录代码的执行

五、不同JDK版本之间的intern()方法的区别——JDK6 VS JDK6+

说到这里我们不得不提一下String.intern()方法在jdk版本变更中的不同

String s = new String("a");
s.intern();

JDK6:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。

JDK6+:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用

我们看一个例子:

public class InternDifference {
    public static void main(String[] args) {
        String s = new String("a");
        s.intern();
        String s2 = "a";
        System.out.println(s == s2);

        String s3 = new String("a") + new String("a");
        s3.intern();
        String s4 = "aa";
        System.out.println(s3 == s4);
    }
}

jdk1.8下运行结果为

false
true

分析:

  • s在创建的时候使用new方式创建,这里会在堆中就会有一个值为"a"的对象,intern()之后,intern()会将首次遇到的字符串放到常量池中,此时常量池中就有"a",发现常量池中有"a"。创建s2的时候,看到常量池中已经有"a"了,于是,s2直接指向常量池"a"的地址,而s是指向堆中对象的地址,故返回false
  • 我们再来看s3,s3则直接在堆中创建"aa",第一个"a",intern原本是要将第一个遇见的"a"放入常量池的,但是常量池中已经存在"a"了,于是便不会管,new 的第二个"a"也不会管,但是到"aa"的时候,发现常量池中并没有"aa",于是,直接将s3的引用放入常量池,而不是副本,这样s4在创建的时候,发现常量池中有引用,便直接指向引用,而该引用是指向堆中的s3,故结果为true。

jdk1.6下结果

false
false

第一个false跟上边的一样,第二个false是因为jdk1.6的intern()发现常量池中没有"aa",则直接将此字符串对象添加到常量池中,两个"aa"的地址是不一样的,一个是堆中的一个是常量池中的,故结果也是false。

版权声明:本文为weixin_42173451原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42173451/article/details/105805231

智能推荐

C语言小函数—二进制与十六进制

测试如下 “` int main() { long int num = 15; } “`...

仿微博或微信的文章多图显示(自定义MultiImageView)

按照一般的规矩,先上张图来供大伙看看 如果大致是大伙们需要实现的功能,不烦一观 自定义MultiImageView 工具类 具体使用 app.gradle中添加依赖 implementation 'com.github.bumptech.glide:glide:4.8.0' AndroidManifest.xml中配置联网权限 <uses-permission android:name=&q...

经典进程同步和互斥问题

经典进程同步与互斥问题 前言 一、生产者-消费者问题 1.问题描述 2.问题分析 3.代码 二、读者-写者问题 1.问题描述&&分析 2.代码 三、哲学家进餐问题 1.问题描述&&分析 2.代码 四、理发师问题 1.问题描述&&分析 2.代码 前言 在多道程序设计环境中,进程同步是一个非常重要的问题,下面讨论几个经典的进程同步问题。 一、生产者-消费...

java设计模式——ThreadLocal线程单例

1、定义一个ThreadLocal线程单例,代码如下: 2、定义一个多线程类,代码如下: 3、定义一个测试类,代码如下: 4、输出结果,如下图:...

【tensorflow】线性模型实战

线性模型:y = 1.477 * x + 0.089   1. 采样数据 采样噪声eps在均值0,方差0.01的高斯分布中,而后在均匀分布U(0,1)中,区间[-10,10]进行n=100次随机采样:   2. 计算误差 循环计算每个点的预测值与真是值之间差的平方并累加,从而获得训练集上的均芳误差损失值。   3. 计算梯度   4. 梯度更新 对权重w和偏...

猜你喜欢

常见损失函数和评价指标总结(附公式&代码)

网上看到一篇很实用的帖子关于常见损失函数和评价指标,收藏下来 本文转载于https://zhuanlan.zhihu.com/p/91511706 ------------------------------------------------------------------------------------------------------------------------------...

为什么 4G/5G 的直播延时依然很高

通信技术的发展促进了视频点播和直播业务的兴起,4G 和 5G 网络技术的进步也使得流媒体技术变得越来越重要,但是网络技术并不能解决流媒体直播的高延迟问题。 本文不会介绍网络对直播业务的影响,而是会分析直播中常见的现象 — 主播和观众之间能够感觉到的明显网络延迟。除了业务上要求的延迟直播之外,有哪些因素会导致视频直播的延迟这么高呢? live-streaming  图 1 - ...

springboot 过滤器Filter vs 拦截器Interceptor 详解

1 前言       最近接触到了过滤器和拦截器,网上查了查资料,这里记录一下,这篇文章就来仔细剖析下过滤器和拦截器的区别与联系。 2 拦截器与过滤器之间的区别 从上面对拦截器与过滤器的描述来看,它俩是非常相似的,都能对客户端发来的请求进行处理,它们的区别如下: 作用域不同 过滤器依赖于servlet容器,只能在 servlet容器,web环境下使用 拦截器依赖于sp...

IDEA环境--JavaWeb项目【分页功能实现】

参考链接:https://www.jianshu.com/p/d108d0cd9acf 1、前言 最近在写一些项目,遇到要使用分页功能的地方,就简单的学习了一下,在此总结一下具体实现的过程以及遇到的问题。 分页功能:当我们写一下web项目时会遇到一个页面要显示很多数据,一下子都显示出来效率会很低,也不美观。这就要用到分页,其作用也就是将数据分割成多个页面来进行显示。 2、项目介绍 这只是一个简单的...

517【毕设课设】基于单片机仓库家庭防火防盗报警系统

【资源下载】下载地址如下: https://docs.qq.com/doc/DTlRSd01BZXNpRUxl 功能简要说明: 1.51单片机+1602液晶+按键+烟雾检测传感器MQ+红外检测+蜂鸣器+DHT11温湿度传感器; 2.按键设置烟雾报警浓度值,温度报警值; 3.当达到报警条件,蜂鸣器响; 5.电路板为PCB腐蚀所做,图文件为altiumdesigner工程文件。 6.程序采用C语言编写...