JVM入门-内存结构

2020-08-04

java虚拟机(java virtual machine,JVM),一种能够运行java字节码的虚拟机。作为一种编程语言的虚拟机,实际上不只是专用于Java语言,只要生成的编译文件匹配 JVM对加载编译文件格式要求,即编译后是一个class文件,那么任何语言都可以由JVM编译运行。比如kotlin、scala等。

一、java代码编译执行过程

java代码编译和执行的整个过程包含了以下三个重要的机制:

  1. 源码编译:通过 java源码编译器将 java代码编译成 JVM字节码( .java => .class)
  2. 类加载:通过ClassLoader及其子类来完成 JVM的类加载
  3. 类执行:字节码被装入内存,进入 JVM虚拟机,被解释器解释执行。

总的来说如下:

  • Java源文件 —-> 编译器 —-> 字节码文件 —> 进入 JVM —-> 机器码

image-20200730144140596

java平台由java虚拟机和java应用程序接口搭建,java语言则是进入平台的通道, 用Java语言编写并编译 的程序可以运行在这个平台上

二、JVM简介

  1. JVM是可运行Java代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。JVM是运行在操作系统之上的,它与硬件没有直接的交互。

  2. 当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。

  3. 三种JVM

    • Sun公司的HotSpot;

    • BEA公司的JRockit;

    • IBM公司的J9 JVM;

      在 JDK1.7及其以前我们所使用的都是Sun公司的HotSpot,但由于Sun公司和BEA公司都被oracle收购,jdk1.8将采用Sun公司的HotSpot和BEA公司的JRockit两个JVM中精华形成jdk1.8的 JVM。

三、JVM整体结构

image-20200803155826020

  • 类加载子系统

    负责加载 .class文件,class文件在文件开头有特定的文件标示,并且ClassLoader负责class文件的加载等,至于它是否可以运行,则由Execution Engine决定。

      ① 定位和导入二进制class文件

      ② 验证导入类的正确性

      ③ 为类分配初始化内存

      ④ 帮助解析符号引用.

  • 本地接口

    ​ 本地接口的作用是融合不同的编程语言为 Java所用,它的初衷是融合C/C++程序,在Java诞生的时候C/C++横行的时候,要想立足,必须有调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体作法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。

    ​ 目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机,或者Java系统管理生产设备,在企业级应用中已经比较少见, 因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等。

  • 运行时数据区(内存结构)

    从整个计算机内存中开辟一块内存存储 Jvm需要用到的对象,变量等,分为:方法区,堆,虚拟机栈,程序计数器,本地方法栈。下面会详细介绍。

  • 执行引擎

    执行引擎负责解释命令,提交操作系统执行。

四、JVM内存结构

先给出jvm的运行时数据区结构

image-20200731162639319

注意,其中淡蓝色部分跟随虚拟机启动而存在,线程共享

淡紫色区域跟随线程启动而存在,线程私有

下面针对几个模块进行解释:

1.程序计数器

每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。

2.栈Stack

运行单位。线程私有,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表,操作数栈,动态链接,方法出口等信息)。每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。

  在 Java虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出Stack OverflowError异常;如果虚拟机栈可以动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

下面对线程私有结构进行解剖:

  • JVM执行字节码,线程创建后,都会产生程序计数器(PC)和栈(Stack)以及程序计数器,程序计数器指向方法区中的方法字节码(下一条指令地址),栈中存放一个个栈帧,栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。一个方法从调用开始到结束就是栈帧从入栈到出栈的过程。

    局部变量区

    用于存放方法中的局部变量和参数,包括各种基本类型数据(boolean、byte、char、short、int、float、long、double)、对象引用、returnAddress类型(指向一条字节码指令的地址)

    操作数栈

    用于存放方法执行过程中产生的中间结果,不能存储结果,结果会保存在局部变量区。

    动态链接

    在类加载过程中,把符号引用(常量池中的字符串)转化为直接引用的过程

    详细:在类加载的过程当中,可能会使用的一些方法,这些方法还需要去load一些类;在类里面有运行时常量池,而运行时常量池存在方法区,运行时常量池存放了字符串,这些字符串是为了找到真正在堆里面的内存地址,获得堆里面的实例从而化为直接引用,然后就可以去执行实例中的方法

    方法出口

    包含正常的return或异常的抛出

  • 一个栈帧需要分配多少内存仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式。在多线程的时候,对于执行引擎来说,只有位于栈顶的方法才是在运行的,被称为“当前栈帧”。

线程私有结构:

image-20200730152146181

java线程与JVM内存的关系:在java中每new一个线程,jvm都是向操作系统请求new一个本地线程,此时操作系统会使用剩余的内存空间来为线程分配内存,而不是使用jvm的内存。

待完善:https://www.cnblogs.com/benwu/articles/8025258.html

3.本地方法栈

​ 虚拟机的 Native 方法执行的内存区。底层并不是通过Java去实现的,而是通过C/C++去实现的,Java仅仅负责调度,我们无需关心具体如何实现,也无法看到具体如何实现。

​ 例如创建线程的start方法,最终调用的是native 的start0方法,底层是通过C++去实现的,我们无法看到。

​ 与虚拟机栈一样也会抛出Stack OverflowError异常和OutOfMemoryError异常。

4.方法区

类的所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在这里定义。简单来说,所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中,虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是为了和Java的堆区分开

5.堆Heap

存储单位。用于存放对象实例以及数组,几乎所有对象都在堆上分配内存,当对象无法在该空间申请到内存时将抛出OutOfMemoryError异常。同时也是垃圾收集器管理的主要区域。

​ 一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行。

​ 此处图长度不正确,新生代占1/3,老年代占2/3。

image-20200803164204712

4.1 新生代(Young Generation)

类出生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。

新生代分为两部分:伊甸区(Eden space)和幸存者区(Survivor space),所有的类都是在伊甸区被new出来的。

幸存区又分为From和 To区。当Eden区的空间用完是,程序又需要创建对象,JVM的垃圾回收器将Eden区进行垃圾回收(Minor GC),将Eden区中的不再被其它对象应用的对象进行销毁。然后将Eden区中剩余的对象移到From Survivor区。若From Survivor区也满了,再对该区进行垃圾回收,然后移动到To Survivor区。

15次

4.2 老年代(Old Generation)

新生代经过多次GC仍然存活的对象移动到老年区。若老年代也满了,这时候将发生Major GC(也可以叫Full GC),进行老年区的内存清理。若老年区执行了Full GC之后发现依然无法进行对象的保存,就会抛出OOM(OutOfMemoryError)异常。

执行Full GC时会停止所有的用户线程(STW),只保留垃圾回收线程,回收完之后才会去执行用户线程。调优的目的就是为了减少FUll GC的执行时间和次数。

4.3 元空间(Meta Space)

在 JDK1.8之后,元空间替代了永久代,它是对JVM规范中方法区的实现,区别在于元数据区不在虚拟机当中,而是用的本地内存,永久代在虚拟机当中,永久代逻辑结构上也属于堆,但是物理上不属于。

为什么移除了永久代

参考官方解释http://openjdk.java.net/jeps/122

大概意思是移除永久代是为融合HotSpot与 JRockit而做出的努力,因为JRockit没有永久代,不需要配置永久代。

image-20200803164634536

其他

javac xxx.java 可以把java文件编译成class文件

javap -c 可以把class文件反汇编为指令集 javap -c xx.class > xx.txt 把反汇编的内容输出到txt

参考:

Jvm运行时数据区

JVM入门——JVM内存结构

学习地址