JVM系列(一):JVM内存模型

一、JVM介绍

JVM(Java virtual machine)是一种虚拟机,本身用C语言编写,用来屏蔽不同操作系统的细节,使得Java代码经过一次编译即可在不同的系统上运行。如图所示:
我们用javac命令,就是将Java源文件(.java)编译成Java字节码文件(.class)文件,而jvm会把.class文件翻译成机器码以实现Java的跨平台性。
这里跟C和C++做个比较:

  • C和C++语言也是跨平台的,不过他们的跨平台是在编译器级别的,不同平台要用不同编译器来编译代码,并且在源码级别要对不同平台做相应处理,所以C和C++有“一次编写,到处编译”的说法。
  • Java的跨平台是通过JVM实现的,JVM帮开发者屏蔽了不同平台的细节,同样的源码只经过一次编译就可以在不同的平台上运行,这也就是Java的“一次编写,到处运行”。同时这也是Java速度比C、C++慢的原因,因为Java代码要经过JVM的翻译才可以跟操作系统交互,并且Java的垃圾回收机制也会消耗大量时间。


二、JVM的内存模型

JVM的内存模型即Java的运行时数据区,也就是Java程序在运行时,各种数据存放的地方,基本结构如图:




图一是JVM内存基本结构,分为线程共享区和非线程共享区,图二是比较详细的版本,其中堆又分为新生代和老年代,新生代又分为Eden区和两个Survivor区,下面针对图一来详细说明JVM内存结构。

  1. 非线程共享区
  • 程序计数器: 程序计数器是用于标识当前线程执行的字节码文件的行号指示器。多线程情况下,每个线程都具有各自独立的程序计数器,所以该区域是非线程共享的内存区域。
    通俗点说,程序计数器用来记录当前线程的代码执行到哪里了,那么为啥要记录呢?我一行一行执行不就行了吗?其实在多线程环境中,如果当前线程被挂起了,那恢复的时候怎么知道上次执行到哪里了?这时候程序计数器的作用就来了。
    比如说有个Hello.java:
public class Hello {

    int add() {
        int a = 1;
        int b = 2;
        int c = a + b;
        return c;
    }

    public static void main(String[] args) {
        Hello h = new Hello();
        int result = h.add();
        System.out.println(result);
    }
}

我们用javap -c命令来对class文件进行反汇编:

Compiled from "Hello.java"
public class file.Hello {
  public file.Hello();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  int add();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: istore_3
       8: iload_3
       9: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class file/Hello
       3: dup
       4: invokespecial #3                  // Method "":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method add:()I
      12: istore_2
      13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      16: iload_2
      17: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
      20: return
}

代码中Code下的行号,就是程序计数器了。

  • 虚拟机栈
    虚拟机栈描述的是Java方法执行时的内存,每个方法在执行时都会创建一个栈帧(stack frame),每个方法从开始到结束都对应着一个栈帧从入栈到出栈的过程。栈帧又包含了局部变量表、操作数栈、动态链接和方法出口4个部分。

    • 局部变量表:存放方法参数和局部变量
    • 操作数栈: 操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。以上面Hello类为例,
      iconst_1的作用就是定义第一个int常量,istore_1则将该值压入操作数栈,等运算的时候再进行弹栈(iload_1)。
    • 动态链接: 在运行时常量池中存有大量的符号引用,当栈帧A想调用栈帧B的方法时,就需要以B方法的符号引用作为参数,将符号引用转为直接引用来进行调用,这些符号引用一部分在类的加载阶段(解析)或第一次使用的时候就转化为了直接引用(指向数据所存地址的指针或句柄等),这种转化称为静态链接。而相反的,另一部分在运行期间转化为直接引用,就称为动态链接。
    • 方法出口:分为正常完成出口和异常完成出口
      正常完成出口:执行任意一个方法返回(如:return)的字节码指令,如有返回值则返回给调用者。
      异常完成出口:在方法执行过程中发生异常,且没有在方法体中进行处理。异常完成出口退出时,不会给上层调用者任何返回值。
  • 本地方法栈
    和虚拟机栈类似,只不过本地方法栈是为native方法服务的。

  1. 线程共享区
  • 方法区
    存放类信息,如类的字段、方法、常量等信息,在jdk1.8之前,方法区是位于永久代的,在jdk1.8之后,永久代已经被移除,方法区放在了元数据空间里。
    元数据空间(meta area)和之前的永久代类似,存放了类的一些基本数据和常量池,但元空间不是在JVM里的,而是使用的本地内存。


  • 存放Java对象和数组对象的地方,jvm内存结构中,堆占的空间是最多的,也是Java垃圾收集器工作的地方。
    堆分为新生代和老年代,新生代又分为Eden区和两个Survivor区,关于堆和垃圾收集器的内容,放在后面的文章讲解。

你可能感兴趣的:(JVM系列(一):JVM内存模型)