JVM基础及内存区域

共 9879字,需浏览 20分钟

 ·

2021-06-29 17:37

JVM基础知识

首先要了解JVM的基础知识,知道JVM在Java中起到了什么作用?以及JVM的一些概念。

Java从编译到执行

c385e3518007ed81a7a67f59c5b30b17.webpjava从编译到执行:java文件通过javac编译成class文件,通过JVM中ClassLoader类加载器执行class文件,一般会字节码解析器执行也可能会通过JIT编译器执行,通过执行引擎编译成机器码,由硬件处理。Java文件 -> 编译器 -> 字节码 -> JVM -> 机器码

JDK、JVM、JRE区别

  • JVM:JVM只是一个翻译,把class文件翻译成机器码,JVM不会自己生成代码,需要自己编写代码,同时还需要很多依赖类库,这时就需要用到JRE了。
  • JRE:JRE除了包含JVM之外,还提供了很多类库,也就是很多jar包,它提供了一些即插即用的功能比如文件操作、连接网络、I/O操作等,这些都是JRE提供的基础类库。JVM标准加上实现的一大堆基础类库,就组成了Java的运行时环境(Java Runtime Environment)JRE
  • JDK:对于服务器可能只需要JRE就可以了,但是对于程序员只有JRE还不够,程序员要写完代码、编译代码、调试代码、打包代码,甚至有时候还需要反编译代码,这时候就需要使用JDK,JDK包含了:javac(编译代码)、java、jar(打包代码)、javap(反编译)。

JVM的跨平台性与语言无关性

JVM 不识别.java文件而是识别.class文件字节码,所以JVM和语言是解耦的,可以编译成class的语言都可以在JVM上运行。JVM只识别字节码,JVM和语言解耦的,没有直接关联,所以JVM是和语言无关的。注意JVM运行不是翻译Java文件,而是识别class文件,比如Scala、kotlin、groovy它们都可以编译成字节码文件,所以可以在JVM上跑,这也就是说JVM是跨平台的。047f66a8b693ab9f59a703d9058d4baa.webpJVM的跨平台性和语言无关性,如下图所示:编译成字节码交给JVM处理为机器码,在设备上运行5f167712bf9f35e20f39b2117a52e967.webp

常见的JVM实现

998b8201af692396cef12c6cf738d5fd.webpimage.png
  • Hotspot: 目前使用最多的虚拟机,可以通过执行java --version 查看你现在使用虚拟机的名字
f7a735e3ff4c203b19e1066d73e102c3.webpimage.png
  • J9: IBM 有自己的 java 虚拟机实现,它的名字叫做 J9. 主要是用在 IBM 产品(IBM WebSphere 和 IBM 的 AIX 平台上)
  • TaobaoVM只有一定体量、一定规模的厂商才会开发自己的虚拟机,比如淘宝有自己的 VM,它实际上是 Hotspot 的定制版,专门为淘宝准备的,阿里、天 猫都是用的这款虚拟机。
  • LiquidVM它是一个针对硬件的虚拟机,它下面是没有操作系统的(不是 Linux 也不是 windows),下面直接就是硬件,运行效率比较高。
  • zing它属于 zual 这家公司,非常牛,是一个商业产品,很贵!它的垃圾回收速度非常快(1 毫秒之内),是业界标杆。它的一个垃圾回收的算法后来被Hotspot 吸收才有了现在的 ZGC。

JVM的知识模块

82c2654a8fe3a27bd3c1549fa228084f.webpJVM有非常庞大的知识体系:比如内存结构、垃圾回收、类加载、执行引擎、类文件结构、监控工具、性能调优等等。学习JVM要把握重点,在JVM的所有知识体系中或多或少都和内存结构有关,比如垃圾回收回收的就是内存、类加载加载到的地方也是内存、性能优化也涉及内存、执行引擎和内存密不可分、类文件结构和内存设计有关、监控工具也会监控内存,JVM也是一个虚拟化的操作系统,处理虚拟指令外,还需要虚拟化的内存,而这个虚拟化的内存就是JVM的内存区域。学习JVM首先从内存结构开始。

JVM的内存区域

jvm内存区域图:3f98d9e04d8d049bbf1cad71747d13f9.webp

JVM 是Java虚拟机,类似一个操作系统,class就是指令,比如一个操作系统有8G的内存,其中3G为虚拟内存(运行时数据区)剩下的5G可以理解为JVM的直接内存,这个虚拟内存就是JVM的运行时数据区域,另外还有一个直接内存不是运行时数据区域的一部分,但是会频繁使用。

运行时数据区域

运行时数据区域:Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域

  • 线程私有区域
    • 虚拟机栈
    • 本地方法栈
    • 程序计数器
  • 线程共享区
    • 运行时常量池
    • 方法区

虚拟机栈

存储当前线程运行Java方法所需的数据,指令、返回地址

虚拟机栈中主要包括栈帧,而栈帧包括:局部变量、操作数栈、动态链接、完成出口 在实际代码中,一个线程是可以运行多个方法的。如下代码:main -> A -> B -> C, 运行代码,线程1来运行,就会有一个对应的虚拟机栈,同时在执行每个方法的时候都会打包成一个栈帧。

/**
 * 虚拟机栈
 */

public class MouthedOrStack {
    public static void main(String[] args) {
        A();
    }

    private static void A() {
        B();
    }

    private static void B() {
        C();
    }

    private static void C() {

    }
}

代码的执行过程:如下图所示,执行main()方法的时候入栈,执行A()方法的时候入栈.... 当C()方法执行完了,C()方法出栈,接着B方法运行完了出栈,A方法运行完了出栈,最后main方法执行完了出栈。这个就是Java方法运行对虚拟机栈的一个影响。虚拟机栈就是用来存储线程运行方法中的数据的,每一个方法对应一个栈帧1f3e4acd10625caaa53a69c59bd35498.webp:::tips 虚拟机栈是基于线程的,哪怕只有一个main()方法,也是以线程的方式运行的,在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期和线程一样的 :::

栈的大小限制:-Xss 设置线程堆栈大小,不同的操作系统,不同的位数虚拟机栈的大小是不同的。查看jvm的设置

-Xsssize
Sets the thread stack size (in bytes). Append the letter k or K to indicate KB, m or M to indicate MB, g or G to indicate GB. The default value depends on the platform:

Linux/ARM (32-bit): 320 KB

Linux/i386 (32-bit): 320 KB

Linux/x64 (64-bit): 1024 KB

OS X (64-bit): 1024 KB

Oracle Solaris/i386 (32-bit): 320 KB

Oracle Solaris/x64 (64-bit): 1024 KB

The following examples set the thread stack size to 1024 KB in different units:

-Xss1m
-Xss1024k
-Xss1048576
This option is equivalent to -XX:ThreadStackSize.

虚拟机栈常见的错误:栈溢出:StackOverflowError   常见的场景:一般无限循环递归会造成这个错误 只有压栈没有弹出栈,虚拟机栈内存也不是无限大的,它是有大小限制的如下代码:

/**
 * 栈异常
 */

public class StackError {
    public static void main(String[] args) {
        A();
    }

    public static void A() {
        A();
    }
}

虚拟机栈存储的主要是栈帧

什么是栈帧 (重点):::tips 在每个Java方法被调用的时候,都会创建一个栈帧,并入栈。一旦方法完成响应的调用,则出栈 :::如下图:fa1ff847d78125a73a749546b2d188ef.webp根据上述的讲解,我们都知道虚拟机栈主要是存储当前线程运行Java方法中的指令、数据、返回地址如上图所示,那么每一个方法都是一个栈帧,而栈帧中存储着方法中的变量数据、指令、返回等 栈帧主要包括:局部变量表、操作数栈、动态链接、返回地址。根据如下的代码,来模拟一个方法调用后再栈帧中的处理过程

public class OperandStack {
    public int test() {
        int a = 0;
        int b = 1;
        int z = (a + b) * 10;
        return z;
    }

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

下面我们来模拟一下上述代码中的test方法是如何在虚拟机栈中运行的。如下图是一个,默认状态的虚拟机栈,假设test()在线程1中执行,可以看到局部变量表中有一个this这个是默认的指向的当前的对象。首先,我们需要有一个正确的认知,既然JVM是Java的一个虚拟机,那么JVM就具备一个操作系统所具备的核心功能:CPU + 主内存 + 缓存,那么JVM是一个模拟版的操作系统,JVM执行引擎(CPU) + 栈、堆等(主内存) + 操作数栈(缓存),首先要知道一个操作系统是如何执行的,比如执行1+1计算,那么这个计算是在CPU中执行,直接结果放到缓存中的,那么JVM也是同样的原理,JVM是通过执行引擎计算,将计算结果存入操作数栈中。理解这个原理我们就可以很轻松的理解虚拟机栈的运行过程。1068bf931447d65432675b137aa6c092.webp我们都知道JVM是处理class文件中的指令,那么我们需要把上述的Java源代码,编译成class文件,然后通过javap -c指令反汇编,来查看class文件中的指令 如下代码就是编译后的class字节码文件

public class course01.OperandStack {
  public course01.OperandStack();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4return

  public int test();
    Code:
       0: iconst_0
       1: istore_1
       2: iconst_1
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: bipush        10
       9: imul
      10: istore_3
      11: iload_3
      12ireturn

  public static void main(java.lang.String[])
;
    Code:
       0new           #2                  // class course01/OperandStack
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #4                  // Method test:()I
      12: pop
      13return
}

这里面涉及到了一些指令,这些指令不需要死记硬背,而是需要去理解,可以根据我提供的链接,直接查找这个指令的意思即可。首先执行:int a = 0;在字节码的指令中首先看到的是:iconst_0 这个指令是什么意思呢?直接从上述我提供的链接中查找, 意思就是将一个常量0加载到操作数栈,哦原来这个意思就是将int a = 0的常量值放到操作数栈中,这时候线程1中的虚拟机栈就变成了如下:操作数栈中多了一个常量0916521896a28fb02572306af72db8553.webp我们继续往下走,一般指令都是按照顺序执行的,指令是不会跳跃执行,所以我们继续顺着指令,下一个指令执行的是:istore_1 查找istore指令是什么意思:讲一个数值从操作数栈存储到局部变量表中,哦原来是这个意思,就是从操作数栈的栈顶取值然后放到局部变量表中,不知道大家有没有注意一个问题istore_1 为什么是1而不是0呢?这是因为局部变量表中在0的位置默认有一个this指向当前的对象。(理解每一个步骤及参数的意义是很重要的) 这是JVM执行istore_1这个指令,此时虚拟机栈中的情况:局部变量表的1的位置多了一个数值0的常量,此时操作数栈是没有数据的。因为操作数栈的数据出栈存入到局部变量表中了。5892325c35e11711d2d2f9243892d002.webp那么剩下的两个指令执行,我相信就不用在细说了吧  2: iconst_1 将常量1存入操作数栈中3: istore_2 将操作数栈的数据存入到局部变量表中 此时,虚拟机栈中的情况: 局部变量表中存储着:this、0、1,操作数栈是空的,我们需要理解操作数栈其实就是缓存,主要用来临时存储计算结果的我们不可能在缓存中长久保存数据。(如果还不理解建议学习一下操作系统的基础)bdf57838d741431856915a9688c44a34.webpOK,继续执行指令:iload指令,这个指令是将一个局部变量加载到操作数栈iload_1 :将局部变量表中1的位置存储的数据,加载到操作数栈中iload_2 :将局部变量表中2的位置存储的数据,加载到操作数栈中 思考:为什么又要加载到操作数栈呢?例如CPU要计算数据,需要从缓存中拿取数据计算,然后将计算结果存入到缓存中。JVM的操作也是同样的原理 此时,虚拟机栈中的运行情况:d4e421617dc0f65cddcaca0fc141efbe.webp继续执行指令:iadd : 算法指令 用于对两个操作数栈上的数值进行某种特定的运算,并把结果重新存入到操作数栈顶 此时虚拟机栈的运行情况:首先从操作数栈取出两个数据,由执行引擎进行计算:(a + b) 得到的结果1存入到操作数栈的栈顶d5540fd9eaff6f60c2aa4f55ed95e9b9.webp继续执行指令:bipush        10 :把一个数值推送到操作数栈栈顶,这里其实执行的代码就相当于:(a+b)*10 bipush指令直接将数值10推送到操作数栈的栈顶中。4190028f6c6b8a0e8a1ab836e687bfc8.webp然后执行指令:imul :运算指令,对操作数栈中的两个数据进行乘法运算 那么,此时虚拟机栈中的运行情况:10和1进行乘法运算,得到结果10存入到操作数栈中fe7d45d8ce348935c06b8d00a532858a.webp继续执行指令:istore_3 将操作数栈存储到局部变量表中,此时虚拟机栈的运行情况如下:38fd924c137eed7f27f8b985be6ae097.webp继续执行指令:iload_3 :将局部变量表3的数据,加载到操作数栈中ireturn :将操作数栈中的数据取出,然后压人调用者的栈帧的操作数栈中 此时test()方法执行完毕,会从线程1 的虚拟机栈中出栈,释放 那么此时虚拟机栈的运行情况:此时虚拟机栈中,只剩下main栈帧,而test()方法已经执行完毕了出栈了,需要注意的是main栈帧的操作数栈中有一个常量10这个常量10就是test栈帧执行ireturn 指令,压入到main栈帧的操作数栈中的。c15f306036e971d365f97216e1b60ad0.webpmain方法执行完毕从虚拟机栈中出栈,OK那么此时,我们的整个代码就执行结束了。其实整个过程就是虚拟机栈的执行的过程,相信大家都理解了虚拟机栈的作用以及运行过程了,嗯....可以吊打面试官了。

  • 局部变量表:变量和引用变量 :::tips 局部变量表,用于存放局部变量就是方法中的变量,首先它是一个32位的长度,主要存放Java的八大基础数据类型,如果是64位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,只需要存放它的一个引用地址即可。默认会有一个this当前类的对象的引用 :::

  • 操作数栈

操作数栈存放Java方法的操作数的,它就是一个栈结构先进后出。操作数栈就是用来操作,操作的元素可以是任意的Java数据类型,所以当一个方法刚刚开始的时候,这个方法的操作数栈就是空的。操作数栈本质上是JVM执行引擎的一个工作区,也就是所方法在执行,才会对操作数栈进行操作,如果代码不执行,操作数栈其实就是空的。一般操作系统:需要有这些东西CPU + 主内存 + 缓存 :::tips JVM是一个模拟版的操作系统,JVM执行引擎(CPU) + 栈、堆等(主内存) + 操作数栈(缓存) ::: 指令:都是有执行引擎来处理的

ICONST_0(iconst_<n>) : 将一个常量0(n)压入操作数栈
ISTORE 1 (istore_<n>):表示将操作数栈存入到局部变量表 下标为1(n)的位置
ILOAD 1 :加载存储指令 将局部变量下标为1的值 加载到操作数栈
ILOAD 2 :加载存储指令 将局部变量下标为2的值 加载到操作数栈
IADD : 算法指令 两条数据从操作数栈出栈相加(执行引擎进行计算) ,运算后的结果入到操作数栈(why?) 而执行引擎相当于CPU不做数据的存储,操作数栈相当于缓存,可以存储中间数据
BIPUSH 10 : 将10常量压入操作数栈
IMUL : 算法指令 乘法。 操作数栈出栈,执行引擎计算得到的结果,存入操作数栈
ISTORE 3 :操作数栈出栈,存入到局部变量表下标为3的位置
ILOAD 3 : 将局部变量下标为3的值 加载到操作数栈
IRETURN : 方法的返回指令: 因为执行引擎都是处理操作数栈中的数据
  • 动态链接

Java语言的特性多态,具体的会在后面的章节中单独讲解。

  • 完成出口 返回地址

正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表<非栈帧中的>来确定)

程序计数器

指向当前线程正在执行的字节码的指令地址

注意 :::tips 程序计数器是唯一不会发生OOM的内存溢出 ::: 其实在讲虚拟机栈运行过程的时候,图中有一个程序计数器。程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如:分支、循环、跳转、异常、线程恢复等都依赖于计数器。为什么要有程序计数器这个东西呢?JVM中的程序计数器 映射了操作系统:CPU时间片轮转机制。由于Java是多线程语言,当执行的线程数量超过CPU核数时,线程之间会根据时间片轮询争夺CPU资源。如果一个线程的时间片用完了,或者是其他原因导致这个线程的CPU资源被提前抢夺,那么这个退出的线程就需要单独一个程序计数器,来记录下一条的运行的指令. 如下图,需要程序计数器来记录运行的指令。57a40a944c000829945af9cdf84b5816.webp因为 JVM 是虚拟机,内部有完整的指令与执行的一套流程,所以在运行 Java 方法的时候需要使用程序计数器(记录字节码执行的地址或行号),如 果是遇到本地方法(native 方法),这个方法不是 JVM 来具体执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器, 这个会记录本地代码的执行的地址,所以在执行 native 方法时,JVM 中程序计数器的值为空(Undefined)。

本地方法栈

执行的是native关键字的方法,native的关键字的方法是在C/C++中实现的,例如hashcode()方法,有一个动态链接hashcode.dll,本地方法并不是Java实现的。为什么会有本地方法栈的原因是:虚拟机规范中规定的。(后续版本中虚拟机栈和本地方法栈合二为一了) 为什么Java要用native方法呢?历史的一些原因,Java实现不了的用c/c++去实现 本地方法栈和虚拟机栈是非常相似的一个区域,只不过本地方法栈服务的对象时native方法。

方法区

方法区与堆空间类似,也是一个共享内存的区域,方法区是线程共享的,加入两个线程都视图访问方法区中的同一个类信息,而这个类还没有装入JVM,那么此时就只允许一个线程加载它,另一个线程必须等待。在HotSpot虚拟机、Java7版本中已经将永久代的静态变量和运行时常量池转移到堆中了,其余部分则存储在JVM的非堆内存中,而Java8版本已经将方法区中实现的永久代去掉了,并用元空间代替了之前的永久代,并且元空间的存储位置是本地内存。逻辑划分 JDK1.7 永久代、JDK1.8 元空间

放class文件,解析,放到常量池 运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时常量池是方法区的一部分。运行时常量池相对于Class常量池的另外一个重要特征具备动态性,我会在后期文章中详细讲解常量池 运行时常量池:

  • class文件加载到常量池--- 符号引用替换为直接引用 例如:person类引用了Tool工具类 内存地址(直接引用)

堆是JVM上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。包括常说的垃圾回收、操作的对象就是堆。堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。堆一般设置成可伸缩的 随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收,这个过程就叫做GC(Garbage Collection) 对于普通对象,JVM首先会在堆上创建对象,然后在其他使用的地方使用它的引用,例如,把这个引用保存在虚拟机栈的局部变量表中。对于基本数据类型来说,有两种情况:

  • 在方法体内声明的基本数据类型的对象,他就会在栈上直接分配。
  • 其他情况都是在堆上分配的。

堆大小参数: -Xms:堆的最小值; -Xmx:堆的最大值; -Xmn:新生代的大小; -XX:NewSize;新生代最小值; -XX:MaxNewSize:新生代最大值;

直接内存

直接内存不属于运行时数据区域,例如给JVM分配了8G内存,虚拟内存3G,剩下的5G就是直接内存也可以成为堆外内存。

直接内存有一种更加科学的叫法,堆外内存。JVM 在运行时,会从操作系统申请大块的堆内存,进行数据的存储;同时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的 内存也就是堆外内存。

它不是虚拟机运行时数据区的一部分,也不是 java 虚拟机规范中定义的内存区域;如果使用了 NIO,这块区域会被频繁使用,在 java 堆内可以用directByteBuffer 对象直接引用并操作;

这块内存不受 java 堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异 常。

小结: 1、直接内存主要是通过 DirectByteBuffer 申请的内存,可以使用参数“MaxDirectMemorySize”来限制它的大小。

2、其他堆外内存,主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申请的内存。堆外内存的泄漏是非常严重的,它的排查难度高、影响大,甚至会造成主机的死亡。后续章节会详细讲。

同时,要注意 Oracle 之前计划在 Java 9 中去掉 sun.misc.Unsafe API。这里删除 sun.misc.Unsafe 的原因之一是使 Java 更加安全,并且有替代方案。目前我们主要针对的 JDK1.8,JDK1.9 暂时不放入讨论范围中。 EHcache、消息中间件大量的使用直接内存。mac 启动HSDBsudo java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB 启动HSDB工具


浏览 53
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报