java代码是怎么在计算机上运行的?jvm各组件在代码执行时都起到什么作用?

前言

        java是高级程序语言,对于java程序员来说不会直接和操作系统与硬件打交道,这些都是jvm底层来完成的。下面来看下java代码从编写到在jvm中运行都经历了哪些步骤,jvm各组件分别起到什么作用 

  

  • java代码执行过程

下面我们就根据一段简单的java代码来探讨java程序在计算机上是怎么运行的,每一步都是一个复杂的过程,这里先理解整体过程对细节不做深入探讨。

public class Test {

    public static void main(String[] args) {
        int c=add(1,2);
        System.out.println("c:"+c);
    }

    public static int add(int a, int b){
        return a+b;
    }
}

  java代码是在jvm中运行的,先看下jvm的组成

本地.java源代码经过编译会生成.class文件,注意的是class文件不等于字节码,class文件包含:魔数,版本号,校验值MD5,字节码等,jvm可以识别和执行字节码指令,此时代码源文件存放在计算机本地还不会产生任何作用

  查看一下编译后的class文件 ,先不用关心字节码语法和代表的意思   

C:\>javac Test.java

C:\>javap -v -l Test
Classfile /C:/Test.class
  Last modified 2024-7-12; size 683 bytes
  MD5 checksum 0c63c52fc0582e8770fdd0c374901fa5
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#23        // java/lang/Object."<init>":()V
   #2 = Methodref          #11.#24        // Test.add:(II)I
   #3 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Class              #27            // java/lang/StringBuilder
   #5 = Methodref          #4.#23         // java/lang/StringBuilder."<init>":()V
   #6 = String             #28            // c:
   #7 = Methodref          #4.#29         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #8 = Methodref          #4.#30         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   #9 = Methodref          #4.#31         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = Methodref          #32.#33        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Class              #34            // Test
  #12 = Class              #35            // java/lang/Object
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               add
  #20 = Utf8               (II)I
  #21 = Utf8               SourceFile
  #22 = Utf8               Test.java
  #23 = NameAndType        #13:#14        // "<init>":()V
  #24 = NameAndType        #19:#20        // add:(II)I
  #25 = Class              #36            // java/lang/System
  #26 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
  #27 = Utf8               java/lang/StringBuilder
  #28 = Utf8               c:
  #29 = NameAndType        #39:#40        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #30 = NameAndType        #39:#41        // append:(I)Ljava/lang/StringBuilder;
  #31 = NameAndType        #42:#43        // toString:()Ljava/lang/String;
  #32 = Class              #44            // java/io/PrintStream
  #33 = NameAndType        #45:#46        // println:(Ljava/lang/String;)V
  #34 = Utf8               Test
  #35 = Utf8               java/lang/Object
  #36 = Utf8               java/lang/System
  #37 = Utf8               out
  #38 = Utf8               Ljava/io/PrintStream;
  #39 = Utf8               append
  #40 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #41 = Utf8               (I)Ljava/lang/StringBuilder;
  #42 = Utf8               toString
  #43 = Utf8               ()Ljava/lang/String;
  #44 = Utf8               java/io/PrintStream
  #45 = Utf8               println
  #46 = Utf8               (Ljava/lang/String;)V
{
  public Test();
    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=2, args_size=1
         0: iconst_1
         1: iconst_2
         2: invokestatic  #2                  // Method add:(II)I
         5: istore_1
         6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: new           #4                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
        16: ldc           #6                  // String c:
        18: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: iload_1
        22: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        25: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        28: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        31: return
      LineNumberTable:
        line 4: 0
        line 5: 6
        line 6: 31

  public static int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: ireturn
      LineNumberTable:
        line 9: 0
}
SourceFile: "Test.java"

  也可以使用IDEA插件jclasslib进行结构化的查看

  • 程序启动

一个能独立运行的java程序必须要有main方法,main方法是jvm执行程序的入口,就是程序启动的入口,当在IDE中执行run或者使用java Test命令时,jre(主要就是jre下的java.c文件)会创建一个jvm进程,操作系统会根据jvm设定的参数为jvm进程分配内存空间并初始化jvm,这个过程主要包括设置运行时数据区的各个内存区域的内存(虚拟机栈,堆,元空间等),初始化jvm内部结构,加载必要系统类库(rt包下的类),然后类加载启动类Test,最后在启动类找到执行入口main方法,如果启动类没有main方法就会报错,如下图

C:\>javac Test.java

C:\>java Test
错误: 在类 Test 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

  • 类加载器

上一步说到jvm进程创建后会加载系统类库rt包和启动类Test,类加载是jvm能够运行java类的前提条件,要想执行本地的代码需要通过类加载器把class文件加载到jvm的方法区生成类的结构信息和运行时常量池(就是把class常量池加载到方法区,区别就是运行时常量池在运行期间动态可修改),同时会在堆区生成类的class对象(是java.lang.Class类型对象不是实例对象),使用jdk自带工具查看堆dump可以发现堆内已经存在类Test但还没有实例

我们也可以用另一种方式判断是否进行了类加载,如下图:同一个类只会被加载一次,静态代码块在类加载时执行,从控制台结果可以看到执行main方法前先进行了主类Test的加载

类加载一般遵循使用时加载,类加载的时机还有:

  • 创建类的实例
  • 类的静态方法被调用
  • 使用了类的一个静态成员变量(获取值和赋值)
  • 调用newInstance和Class.forName等反射方法

  • 堆区

上面还提到类加载时会生成类的class对象放到堆里,堆是jvm最大的一块内存区域,所有线程共享,存储实例对象,数组,类的class对象实例,也是发生GC的区域

  • 虚拟机栈

jvm进程创建完毕并加载完成启动主类,下面会真正开始执行main方法,首先jvm会创建一个主线程,每个线程都会分配一个线程私有的虚拟机栈内存空间,虚拟机栈负责整个线程的方法调用过程,虚拟机栈与线程的生命周期是相同的。

  • 栈帧

开始执行main方法,主线程的虚拟机栈压入main方法栈帧,栈帧组成部分有操作数栈,局部变量表,动态链接,方法返回地址,栈帧就是方法调用和方法执行的内存模型,栈帧出栈入栈就是方法的调用和退出。每一个方法调用都会向虚拟机栈压入一个栈帧,方法退出时会弹出栈帧,下图是主线程执行时方法出入栈过程

下面详细分析下栈帧结构:

  • 操作数栈

  和虚拟机栈结构一样,用于方法执行时保存计算过程的中间结果

  • 局部变量表

用于存储方法参数方法内部定义的局部变量的空间,下面看下add方法的局部变量表

  • 动态链接

动态链接发生在一个方法调用另一个方法时,需要把方法的符号引用转化为直接引用。java实例方法调用存在继承重写,实现,多态的情况,只能在运行时通过真实类型才能确定方法的直接引用。静态方法和私有方法调用在编译时就能确定直接引用,所以不会存在动态链接,下面看个代码示例理解下符号引用和直接引用:

图中main方法中调用e的字节码体现的就是符号引用,Son的e方法形参是Father,编译期是无法确定实参到底会是Father还是子类Son,只有运行时才能确定实参的真实类型,然后找到真正会调用的方法地址。

  • 方法返回地址

方法退出时回到被上层方法调用时的位置

  • 程序计数器

线程私有,每个线程都会分配一个独有的程序计数器,它记录着线程下一个要执行的字节码的位置,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令,用于实现分支、‌循环、‌跳转、‌异常处理以及线程恢复等基础功能。‌

  • 执行引擎

上一步的字节码解释器是个啥?方法字节码在栈和pc计数器的配合下一步一步的执行,但问题是程序最终都会由cpu执行,而cpu只能执行机器码,那么字节码的执行是怎么和cpu关联上的?这里就要讲到jvm的执行引擎:

  1. 执行引擎是 Java 虚拟机的核心之一,其组成部分包括解释器、即时编译器。
  2. 解释器逐条解释执行字节码的,把字节码翻译成机器码,并通过操作系统和硬件的协调最终由cpu执行。由于操作系统和硬件架构的不同,执行引擎会生成不同指令集对应的机器码,这也是java语言可以跨平台的根本原因。
  3. 即时编译器负责把热点代码翻译为机器码缓存起来,以后调用时直接交给cpu执行
  • 本地方法和本地方法栈

和java虚拟机栈功能相似,不过本地方法一般是由C,C++编写的操作系统级别的类库。这种设计主要是为了提高性能:一是因为C和C++等语言通常更接近硬件,能够更直接地控制硬件资源,二是这种底层调用使用本地方法不需要经过jvm解释执行效率更高

总结

高级语言的本质就是封装了操作系统和硬件的使用细节,高级语言只是描述数据结构和算法,而真正的执行是由操作系统协调硬件完成的。jvm通过操作系统提供的接口来请求和管理内存空间,但对象的管理、分配和释放过程则是由 JVM 在其运行时环境中完成的。操作系统的角色是为 JVM 提供内存资源,但不直接参与到 Java 对象的高级管理操作中。

  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值