分享

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

 piyanat 2020-04-29

一、你了解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; } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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

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

然后用javap -verbose 进行反编译

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

生成如下:

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'

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

执行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 } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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();
  • 1
  • 2

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); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

jdk1.8下运行结果为

false
true
  • 1
  • 2

分析:

  • 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
  • 1
  • 2

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

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多