zl程序教程

您现在的位置是:首页 >  Java

当前栏目

JVM--内存模型

2023-04-18 12:33:30 时间
安卓高级工程师想要做性能优化、NDK、设计架构时的健全性等工作时,必须是对JVM有一定的了解。技术的路越往上走,就越需要对底层的理解。计算机原理、c/c++语言、JVM原理、数据结构与算法等知识缺一不可。在学习的过程中,一开始觉得知识是线性的,就像一个数组,只需要不断往里面填充数据,然而后面越学习,越发现知识是广度的,到达一定程度后,我们想学习一个知识,往往需要学习一连串的其他知识,其中不乏需要使用其他编程语言,也渐渐明白了全栈工程师的由来,技术的路上没有尽头,只有不断的学习
JVM的内容偏向概念,不像代码执行就能够看到结果,总结起来也比较麻烦,有条件的还是看下JVM的相关书籍比较好,总结不到位的地方也可以相互探讨

一.JVM内存模型

Java是一种解释型语言,首先需要编译成class文件,再交由JVM装载,最终JVM会解释成系统可以直接运行的机器码。性能方面肯定是不如直接编译成机器码的,那么为什么Java不直接编译成机器码呢?因为不同操作系统识别机器码的规则是不同的,而我们写的代码只有一份,JVM来负责转义成不同操作系统下的机器码,带来的好处就是:一次编译,到处运行

上图为JVM加载class到内存中内存模型,JVM带来的好处除了跨平台外,还有自动内存分配,这也是我们需要了解的重点,c/c++程序员想要使用大量数据时,需要动态申请内存,并在适当的时机手动释放这片内存,一旦忘记释放,造成野指针,那么就会内存泄漏。而JVM帮助我们从申请内存、释放内存的繁琐工作中释放出来

二、线程共享与私有

JVM在运行程序时,分成两块数据区,共享数据区和私有数据区

1.共享数据区

试想以下我们Java代码中哪些代码是固定的?哪些代码是动态的? 固定的可以用文本来表示,比如类名、类中的属性、常量、方法名、代码执行顺序等。其实就是我们写的代码 动态的是文本无法表示的,如变量进行一系列运算得到的值。其实就是需要cpu介入的运算

接下来,看一段代码:

public class Hello {

    public int test() {
        int a = 3;
        int b = 4;

        return a + b;
    }

    public static void main(String[] args) {
        Hello h = new Hello();
        h.test();
    }

}
1.1方法区

我们使用javac执行编译后,得到class文件,如果我们运行该程序,执行了main函数,其中对hello对象进行了实例化,那么JVM会加载该class文件,并存储到方法区(元空间)

Hello.class加载.png

此时的class文件已经被转义了,我们可以使用javap命令来反编译查看上面代码在JVM是什么样子的

javap -v Hello.class

不需要细看,后面会详细介绍test方法的内容

Classfile /C:/Users/tyqhc/Documents/javaworkspace/myJava/out/production/myJava/Hello.class
  Last modified 2021-9-27; size 525 bytes
  MD5 checksum 74b6dc87932a59d4af95208b0eb1edb7
  Compiled from "Hello.java"
public class Hello
  SourceFile: "Hello.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#25         //  java/lang/Object."<init>":()V
   #2 = Class              #26            //  Hello
   #3 = Methodref          #2.#25         //  Hello."<init>":()V
   #4 = Methodref          #2.#27         //  Hello.test:()I
   #5 = Class              #28            //  java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               LHello;
  #13 = Utf8               test
  #14 = Utf8               ()I
  #15 = Utf8               a
  #16 = Utf8               I
  #17 = Utf8               b
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               args
  #21 = Utf8               [Ljava/lang/String;
  #22 = Utf8               h
  #23 = Utf8               SourceFile
  #24 = Utf8               Hello.java
  #25 = NameAndType        #6:#7          //  "<init>":()V
  #26 = Utf8               Hello
  #27 = NameAndType        #13:#14        //  test:()I
  #28 = Utf8               java/lang/Object
{
  public Hello();
    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 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   LHello;

  public int test();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_3
         1: istore_1
         2: iconst_4
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: ireturn
      LineNumberTable:
        line 4: 0
        line 5: 2
        line 7: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       8     0  this   LHello;
               2       6     1     a   I
               4       4     2     b   I

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class Hello
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method test:()I
        12: pop
        13: return
      LineNumberTable:
        line 11: 0
        line 12: 8
        line 13: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      14     0  args   [Ljava/lang/String;
               8       6     1     h   LHello;
}

Java中每一个类(我们不讨论内部类)对应一个class文件,class文件的信息(类元信息)是存放在方法区的,这是共享数据区,并且同一个类加载器只会加载一份,当我们需要实例化该类的对象时,会从方法区查找,如果以前加载过了,那么直接使用。谁都可以实例化这个类,自然它就是存放在共享数据区了

1.2堆

方法区存放着class信息,而堆中存放了实例化的对象,同一个类的对象可以被实例化多次,对象是可以被其他线程使用的,所以堆也是共享数据区。实例化对象时,对象中有一个对象头,其中有个类型指针会指向方法区的类元信息,下面的图看看就好,不必深究

对象头组成.png

2.私有数据区

除了方法区和堆,其他的都是私有数据区,前面已经提到了,私有数据区都是方法运行时的数据,需要cpu介入

2.1栈帧

Java栈也称为虚拟机栈、线程栈,叫什么其实无所谓,重要的是里面的内容:栈帧

栈帧主要分为四个部分组成:

  • 局部变量表:存放着局部变量以及其值
  • 操作数栈:存放着运算时的临时数据
  • 动态链接:多态相关
  • 方法出口:进入到下一个方法的栈桢中

每个方法调用时,都会新建一个栈帧,然后执行方法中的代码,上面test方法中的汇编指令为:

         0: iconst_3
         1: istore_1
         2: iconst_4
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: ireturn

用一张gif图,来表示test方法是如何在栈帧中操作的:

2.2程序计数器与本地方法栈

程序计数器:程序计数器就是临时记录方法运行到哪一行了,程序运行实际并不存在并行,而是不同的线程不断的抢占cpu,然后执行一段时间,又重新开始竞争,只是执行时间太短,导致人根本感受不到,当一个方法运行到某一行的时候开始重新竞争了,就需要记录下当前方法运行到哪了,用来下次cpu被该方法抢占时,从上次中断的地方继续执行

本地方法栈:本地方法栈用来运行c/c++代码,结构和Java栈是相同的,区别是c/c++代码动态申请的内存由自己手动管理

对于内存模型,只要了解下就行了,对于移动端开发,我们做内存优化,针对的是堆中的内存,下一篇将介绍堆中的内存结构