JVM之java程序运行原理分析

JVM之java程序运行原理分析本文是JVM入门教程的第一篇。在这篇博客中,将介绍JVM的基本结构和相关的概念,并通过一个简单的示例,演示java程序的运行过程。 如上图所示,java代码经过编译之后,产生了class文件。java程序运行时,JVM会为class文件分配一个内存空间,存储它运行时的信息,即J…

概述

本文是JVM入门教程的第一篇。在这篇博客中,将介绍JVM的基本结构和相关的概念,并通过一个简单的示例,演示java程序的运行过程。

JVM运行时数据区

JVM之java程序运行原理分析 如上图所示,java代码经过编译之后,产生了class文件。java程序运行时,JVM会为class文件分配一个内存空间,存储它运行时的信息,即JVM运行数据区。JVM运行数据区按照线程是否独占或共享,进一步划分为若干个不同的数据区域。其中,线程共享部分包括方法区和堆内存,线程独占部分包括虚拟机栈、本地方法栈、程序计数器。

方法区

简而言之,方法区主要用来存储class文件的一些数据,比如虚拟机加载的类信息、常量、静态变量、即时编译器编译过后的代码等数据。它是虚拟机规范中的一个逻辑区划。具体实现根据不同的虚拟机而不同。比如HotSpot在java7中方法区放在永久代,java8中方法区放在元数据空间并且通过GC机制对这个区域进行管理。

堆内存

类加载之后,我们就可能需要用这些类去创建一些对象。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。其中,堆内存还可以细分为老年代、新生代(Eden、From Survivor、To Survivor)。

程序计数器

程序计数器是一块较小的内存空间,可以看作当前线程所执行的字节码的行号指示器,记录的是当前线程所执行的位置。CPU同一时间,只会执行一条线程中的指令,而线程切换后,通过线程独立的线程计数器所指向的字节码的行号,就能回到上次最后指向的位置,并且继续执行剩下的字节码。

虚拟机栈

每个线程都在这个这个空间有一个私有的空间。线程栈由多个栈帧组成。一个线程会执行一个或多个方法,一个方法对应着一个栈帧。栈帧的内容包含:局部变量表,操作数栈、动态连接、方法返回地址、附加信息等。栈内存默认最大是1M,超出则抛出StackOverflowError。

本地方法栈

和虚拟机栈功能类似,虚拟机栈是为虚拟机执行java方法而准备的,本地方法栈是为虚拟机使用Native本地方法而准备的。他和虚拟机栈的实现一样,超出大小也会抛出StackOverflowError。具体实现由不同的虚拟机厂商决定。

实例演示

运行环境

  • 系统: win10
  • JDK:jdk1.8
  • 16进制文件查看工具:winhex

代码演示

在这里,我们定义了一个简单的java类Demo1,在类的main方法中,定义了简单的计算逻辑,我们可以很快地看出,打印出来的结果是55。但这不是重点,我们主要是用来展示java程序是如何在JVM中运行的。

public class Demo1{
    public static void main(String[] args){
        int x = 500;
        int y = 100;
        int a = x / y;
        int b = 50;
        System.out.println(a + b);
    }
}

我们在命令行中定位到这个java文件所在,进行编译。

javac Demo1.java

使用winhex查看16进制的class文件

由于class文件的内容是由很多个16进制的字节组成的。windows默认不能直接直接读取,因此我们可以通用安装winhex软件来读取class文件。(下载地址:www.x-ways.net/winhex/ )直接下载、解压、安装winhex软件,将class文件拖拽到软件界面,就可以查看class文件的内容了。 JVM之java程序运行原理分析 class文件包含java程序代码执行的字节码,数据严格按照格式紧凑排列在class文件的二进制流,中间无任何分隔符。文件开头有一个0xcafebabe(16进制)特殊的标志。 JVM之java程序运行原理分析

使用javap查看class文件

为了更好地阅读class文件的内容,我们可以使用javap命令,解析这个文件,并将文件内容写到Demo1.txt中。

javap -v Demo1.class > Demo1.txt

在当前java文件所在的目录下,就会生成一个Demo1.txt文件,我们打开这个文件,文件的内容如下:

Classfile /C:/Java高级工程师项目/jvm_demo/Demo1.class
  Last modified 2020-8-22; size 414 bytes
  MD5 checksum ae6fa820973681b35609c75631cb255b
  Compiled from "Demo1.java"
public class Demo1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref #5.#14 // java/lang/Object."<init>":()V
   #2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
   #4 = Class #19 // Demo1
   #5 = Class #20 // java/lang/Object
   #6 = Utf8 <init>
   #7 = Utf8 ()V
   #8 = Utf8 Code
   #9 = Utf8 LineNumberTable
  #10 = Utf8 main
  #11 = Utf8 ([Ljava/lang/String;)V
  #12 = Utf8 SourceFile
  #13 = Utf8 Demo1.java
  #14 = NameAndType #6:#7 // "<init>":()V
  #15 = Class #21 // java/lang/System
  #16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
  #17 = Class #24 // java/io/PrintStream
  #18 = NameAndType #25:#26 // println:(I)V
  #19 = Utf8 Demo1
  #20 = Utf8 java/lang/Object
  #21 = Utf8 java/lang/System
  #22 = Utf8 out
  #23 = Utf8 Ljava/io/PrintStream;
  #24 = Utf8 java/io/PrintStream
  #25 = Utf8 println
  #26 = Utf8 (I)V
{
  public Demo1();
    descriptor: ()V
    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

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: sipush        500
         3: istore_1
         4: bipush        100
         6: istore_2
         7: iload_1
         8: iload_2
         9: idiv
        10: istore_3
        11: bipush        50
        13: istore        4
        15: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_3
        19: iload         4
        21: iadd
        22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
        25: return
      LineNumberTable:
        line 3: 0
        line 4: 4
        line 5: 7
        line 6: 11
        line 7: 15
        line 8: 25
}
SourceFile: "Demo1.java"

class文件信息

我们先来看Classfile这部分内容:

Classfile /C:/Java高级工程师项目/jvm_demo/Demo1.class
  Last modified 2020-8-22; size 414 bytes
  MD5 checksum ae6fa820973681b35609c75631cb255b
  Compiled from "Demo1.java"

它描述了class文件所在的路径,最后的更新时间,所占字节大小,md5的校验码,以及指明了从哪个java文件编译过来。

class内容-基本信息

public class Demo1
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER

在这里,major version表示主版本号(版本号规则:JDK5,6,7,8分别对应49,50,51,52),minor version表示次版本号,flag表示访问标志,访问标志的含义可以参见下表。 JVM之java程序运行原理分析

class内容-常量池

Constant pool:
   #1 = Methodref          #5.#14         // java/lang/Object."<init>":()V
   #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #17.#18        // java/io/PrintStream.println:(I)V
   #4 = Class              #19            // Demo1
   #5 = Class              #20            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = Utf8               SourceFile
  #13 = Utf8               Demo1.java
  #14 = NameAndType        #6:#7          // "<init>":()V
  #15 = Class              #21            // java/lang/System
  #16 = NameAndType        #22:#23        // out:Ljava/io/PrintStream;
  #17 = Class              #24            // java/io/PrintStream
  #18 = NameAndType        #25:#26        // println:(I)V
  #19 = Utf8               Demo1
  #20 = Utf8               java/lang/Object
  #21 = Utf8               java/lang/System
  #22 = Utf8               out
  #23 = Utf8               Ljava/io/PrintStream;
  #24 = Utf8               java/io/PrintStream
  #25 = Utf8               println
  #26 = Utf8               (I)V

这里存放的是类信息包含的静态常量,编译之后就能确认。这里使用的标识所对应的含义,参见下表。 JVM之java程序运行原理分析 我们对照着Demo1.java文件和Demo1.txt文件,参照上面的表格,可以看到常量池中存储着哪些常量: 比如,java默认都继承了Object类,所以这里会引用Object类和相关无参构造方法的常量,同时,我们在代码中使用了System.out.println()方法,所以也引用了System.out.println()方法相关的类、方法、字段等常量。然后,我们系统的编码是UTF-8,常量池中也会记录相关引用的UTF-8编码的字符串常量。(#8中的code表示方法表,#9中的LineNumberTable用来表示java源代码的行号和字节码指令的对应关系,都是一些默认的常量,在这里我们了解即可)

class内容-构造方法

  public Demo1();
    descriptor: ()V
    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

这部分内容,描述的是class文件的构造方法信息。Demo1这个示例中,我们并没有写构造函数,由此可见,没有定义构造函数时,会有隐式的无参构造函数。flags中的ACC_PUBLIC表示是public类型,可以参考上面的class内容-基本信息的表格。code表示方法表。“stack=1, locals=1, args_size=1”分别表示,方法对应栈帧中操作数栈的深度是1,本地变量的个数是1,参数个数是1。本地变量和参数的个数都为1,是因为构造方法本身默认包含了this参数。

我们接着往下看。“0: aload_0” 表示从局部变量0中装载引用类型值入栈,在这里的局部变量0就是this变量,前面的数字,是偏移量(字节)。“1: invokespecial #1”表示编译时方法绑定调用#1对应的方法,即Object的无参构造方法。“4: return”则表示void函数返回。”LineNumberTable”表示源代码与字节码指令的对应关系。“line 1: 0”说明源代码的第一行指向偏移量为0的字节码指令,在这里即是“0: aload_0”。

看到这里,可能有些同学会有一些困惑,偏移量到底是什么,是怎么计算来的。下面我来简单的说一下。

  1. 偏移量表示相对入口地址偏移,单位是字节。比如偏移量为1,则说明偏移了一个字节的地址
  2. 每个字节会存放一个指令,或者一个参数
  3. 下一个字节码的偏移量=当前偏移量+当前指令的个数+参数的个数

以上面的“1: invokespecial #1”为例子,可以看出,当前的偏移量为1,#1是指向常量值的索引,在这里我们不能直接简单地认为参数的个数是1个,而是需要查阅官方的java虚拟机规范文档的,连接我放在附录中。 JVM之java程序运行原理分析 通过查阅官方文档,我们可以知道,使用invokespecial这个指令时,需要带两个参数的。所以下一个字节码的偏移量=1+1+2=4。即“4: return”中所对应的偏移量。当然,一般来说,不需要我们自己去计算偏移量的。这里只是通过官方文档,来满足我们小小的好奇心而已哈哈!

class内容-main方法

以下是class文件的main方法的操作码信息,在讲解main方法的运行流程之前,我们先对程序进行完整的运行分析。

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: sipush        500
         3: istore_1
         4: bipush        100
         6: istore_2
         7: iload_1
         8: iload_2
         9: idiv
        10: istore_3
        11: bipush        50
        13: istore        4
        15: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_3
        19: iload         4
        21: iadd
        22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
        25: return
      LineNumberTable:
        line 3: 0
        line 4: 4
        line 5: 7
        line 6: 11
        line 7: 15
        line 8: 25

程序完整运行分析(一)

JVM之java程序运行原理分析 由上图可知,java源代码经过编译,形成class字节码文件。JVM在在加载类时,会将类信息、运行时常量池、字符串常量等都加载到方法区中。对于HotSpot虚拟机来说,1.7以前,方法区是存放在永久代中的,从1.8开始,方法区开始被称为元数据空间。

程序完整运行分析(二)

JVM之java程序运行原理分析 类加载后,程序就开始运行了。jvm这时候就会创建线程来执行代码。这时候就需要在虚拟机栈、程序计数器中分配相应的空间。(这里不涉及到本地方法栈,因为我们的代码都是java代码)每个线程都有独占的空间,程序计数器也会有相应的字节码指令地址。

程序完整运行分析(三)

JVM之java程序运行原理分析 线程在运行时,需要在程序计数器开辟小小的空间,用来记录当前线程执行代码的位置。同时需要在虚拟机栈开辟一个空间。每个线程对应的一个虚拟机栈。一个虚拟机栈对应着多个栈帧。栈帧是方法对应的操作。其实main方法是程序的入口。main方法栈帧中包含本地变量表和操作数栈。下面我们就来分析上面main方法的执行过程。

“stack=3, locals=5, args_size=1″的含义是方法对应栈帧中操作数栈的深度为3、本地变量个数为5个(args、x、y、a、b),参数变量个数为1个(args)。

偏移量 字节码指令 操作描述 程序计数器位置 本地变量表 操作数栈
0 sipush 500 将500这个数值压入操作数栈 0 args 500
3 store_1 弹出操作数栈栈顶500,保存到本地变量表1 3 args,500
4 bipush 100 将100这个数值压入操作数栈 4 args,500 100
6 istore_2 弹出操作数栈栈顶元素,这里是100,保存到本地变量表2 6 args,500,100
7 iload_1 读取本地变量表1,压入操作数栈,这里是500 7 args,500,100 500
8 iload_2 读取本地变量表2,压入操作数栈,这里是100 8 args,500,100 100,500
9 idiv 将栈顶两int类型出栈、相除,结果入栈,500/100=5 9 args,500,100 5
10 store_3 将栈顶int类型值出栈、保存到局部变量3中 10 args,500,100,5
11 bipush 50 将50压入操作数栈 11 args,500,100,5 50
13 istore 4 弹出操作数栈栈顶元素,这里是50,保存到本地变量表4 13 args,500,100,5,50
15 getstatic #2 获取常量池#2对应的静态字段的值,即System.out静态变量 15 args,500,100,5,50 #2
18 iload_3 读取本地变量3,即5,压入操作数栈 18 args,500,100,5,50 5,#2
19 iload 4 读取本地变量4,即50,压入操作数栈 19 args,500,100,5,50 50,5,#2
21 iadd 将栈帧两个int类型数出栈、相加,结果入栈 21 args,500,100,5,50 55,#2
22 invokevirtual #3 创建新的栈帧,方法中的参数从操作数栈中弹出,压入新的虚拟机栈,虚拟机开始执行虚拟机栈最上面的栈帧。这里的#3即System.out.println()方法 22 args,500,100,5,50 #2,55(新的虚拟机栈)
25 return void函数返回,main方法执行结束 25 args,500,100,5,50

附录

  1. 点击下载 JVM指令字节码表
  2. java虚拟机规范官方文档

今天的文章JVM之java程序运行原理分析分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/16195.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注