初识JVM-JVM内存结构

1 前言

什么是JVM?我们来看一下维基百科的答案

A Java virtual machine (JVM) is a virtual machine that enables a computer to run Java programs as well as programs written in other languages and compiled to Java bytecode. The JVM is detailed by a specification that formally describes what is required of a JVM implementation. Having a specification ensures interoperability of Java programs across different implementations so that program authors using the Java Development Kit (JDK) need not worry about idiosyncrasies of the underlying hardware platform.

Java虚拟机(JVM)是一种虚拟机,它使计算机能够运行Java程序以及用其他语言编写的程序(如Groovy,Scala),并编译成Java字节码。JVM由规范描述了JVM实现所需的规范。使用规范确保Java程序在不同实现之间的互操作性,以便使用Java开发工具包(JDK)的程序作者不必担心底层硬件平台的特质。
其中虚拟机指的是

指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。

我们常见的虚拟机有VM Ware,Virtual Box,JVM
而今天我们要介绍的内容是虚拟机中很重要的一个概念,虚拟机的内存结构

2 虚拟机内存结构

JVM内存结构.jpg

其中最重要的概念是 方法区,JAVA堆,JAVA栈,指令计数器(PC)

  1. 指令计数器(PC)
    每个线程都有独立的程序计数器,各线程的互不影响,用于存储下一条执行的虚拟机指令地址(对于Native方法则为空undefined)

  2. JAVA栈(VM Stack)
    我们知道是一种先进后出的数据结构,它有点像机关枪的弹夹先被放进去的子弹最后被打出来。
    JVM会为每一个线程创建一个栈,JAVA中的栈是以栈帧为基本的数据单元,在一个线程栈里面会有很多个栈帧(Frame),每一个栈帧对应一个方法。例如:我们一般在main方法写的程序,叫做主线程中main方法的栈帧。而这个main方法的栈帧存储着,该方法运行时所需要的数据。当main方法调用其他方法例如max()方法。那么max方法所对应的栈帧就会进行压栈操作,成为当前的栈帧。当max()方法执行结束之后,当前的栈帧就会出栈,main方法重新成为当前的栈帧。
    那么一个栈帧主要存储着哪些数据呢?主要包含下面的数据

    • 局部变量表
    • 操作数栈
    • 方法返回地址
    • 动态链接

    2.1 局部变量表:
    局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,其中存放的数据的类型是编译期可知的各种基本数据类型、对象引用(reference)和(returnAddress)类型(它指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成计算的,即在Java程序被编译成Class文件时,就确定了所需分配的最大局部变量表的容量。当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

    2.2 操作数栈:
    操作数栈又常被称为操作栈,操作数栈的最大深度也是在编译的时候就确定了。32位数据类型所占的栈容量为1, 64位数据类型所占的栈容量为2。当一个方法开始执行时,它的操作栈是空的,在方法的执行过程中,会有各种字节码指令(比如:加操作、赋值元算等)向操作栈中写入和提取内容,也就是入栈和出栈操作。Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。因此我们也称Java虚拟机是基于栈的,这点不同于Android虚拟机,Android虚拟机是基于寄存器的。基于栈的指令集最主要的优点是可移植性强,主要的缺点是执行速度相对会慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的优点是执行速度快,主要的缺点是可移植性差。

    2.3 动态链接:
    每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如 final、static 域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

    2.4 方法返回地址:
    当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上等同于把当前栈帧出站,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。

  1. JAVA堆(Heap)
    所有线程共享的内存区域,用于存放对象实例
  2. 方法区(Method Area)
    线程共享,用于存放已加载的类、常量、静态变量、JIT编译后的代码等数据。
    对于HotSpot虚拟机用户而言,经常将方法区称为永生代(Permanent Generation),是因为HotSpot虚拟机用永生代实现方法区,用GC管理方法区。
  3. 综合案例
    综合例子.jpg
public class Sample {
    private String name;
    public Sample(String name) {
        this.name = name;
    }
    public void printName() {
        System.out.println(name);
    }
}
public class appMain {
    public static void main(String[] args) {
        Sample test1 = new Sample("测试1");
        Sample test2 = new Sample("测试2");   
        test1.printName();
        test2.printName();
    }
}

程序的执行流程如下。

  1. appMain类信息(即appMain.class对象)通过类加载器加载进方法区
  2. test1,test2 是自定义类Sample类的两个引用,放置在主线程main方法对应的栈帧中。
  3. Sample类的两个实例放置在堆区中。
  4. 分别调用在test1,test2,关于Sample存储在方法区中的printName()方法。
  5. 返回main方法,程序结束。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 135,067评论 19 139
  • 3.邺城春秋 在袁绍死后袁绍集团的内部政治纷争当中,冀州豪强被削弱。在曹操彻底消灭袁绍集团之后,取得了冀州这样一个...
    张七公子阅读 1,116评论 0 13
  • 最近在接 spring cloug hystrix 熔断,了解一下熔断的基本概念和原理。此篇原文是马丁花的文章 h...
    holysu阅读 4,457评论 3 3
  • 我不是艺术家,也不是诗人,我只是在刻画痛苦。 千百年来的阴影魔障,交织着差别甚小的忧郁。 我点燃火焰,将自己燃烧在...
    陈高兴_阅读 1,395评论 1 11