Java 程序是如何运行起来的

0 Java 简介

首先简单介绍一下 Java 的历史与 Java 的核心机制。

Java 的历史

Java 最初产生于 SUN 公司的 “Green” 小组,该小组由 James Gosling 领导,最初的名字叫 Oak,在发现该名称已被注册之后,改名为 java。从 1996 年 1 月到现在,Java 经历了不断的演变。1999 年 6 月,Java 体系被分为三个方向:J2ME(微型版,移动端)、J2SE(桌面版,桌面端) 和 J2EE(企业版,服务端)。如今 JavaSE 的长期支持版(LTS)已经发展到了 11 版。

Java 的核心机制

Java 的两个基本的核心机制分别是 Java 虚拟机(JVM)和 Java 的垃圾回收器(GC):

  1. JVM 可以理解为一个可运行 Java 字节码的虚拟计算机系统。正是 JVM 的存在使得 Java 语言具有了跨平台的特性,因为所有的 Java 原码都会被首先编译成 Java 字节码文件,从而交给 JVM 处理,而不直接与具体的操作系统通信。所以,对于不同的平台,有对应的不同的 JVM 来实现代码与平台的分离,从而实现了所谓的“一次编译,到处执行”。

  2. Java 中的 GC 负责自动回收 JVM 中不再使用的内存空间。GC 的垃圾回收工作不需要由外部干预,从而消除了程序员回收无用内存的责任(在C/C++ 中这个工作是需要程序员负责考虑的)。GC 是 JVM 提供的一种系统线程跟踪存储空间的分配情况,当 JVM 处于空闲时,它会检查并释放掉那些可以被释放的存储空间。GC 是无法被精准控制和干预的,在 Java 程序运行的过程中它是自主启动并运行的。

1 JDK、JRE和JVM之间的关系

  • JDK:Java Development Kit,Java 开发工具包 - 开发人员进行 Java 软件开发测试的一套工具

  • JRE:Java Runtime Environment,Java 运行时环境 - Java 软件成品运行所依赖的环境

  • JVM:Java Virtual Machine,Java 虚拟机 - Java 语言实现跨平台运行的一种软件

  • 三者的关系是:JDK包含JRE,JRE包含JVM,像下图这样。

2 Java 编译与运行过程

  • java程序的运行包括两个非常重要的阶段:编译阶段运行阶段。它们的大致过程如下图。

  • 编译:

    • java 源文件通过 JDK 中的编译器 javac 通过的编译,编译操作方式如下:

      javac -encoding 编码方式 -d 输出为字节码文件的路径 代码(相对)路径

      如:javac -encoding utf8 -d ../out/ ./src/*.java,它表示以 utf-8 格式编码 ./src/ 路径下的所有 Java 源文件,生成的字节码文件放到上一级目录的 out/ 路径下。

    • 编译阶段的主要任务是检查 java 源程序语法,符合 java 语法规则能正常生成 字节码文件 xxx.class。字节码文件是只有 JVM 才能理解的文件,通过 JVM 解释为不同操作系统的可执行二进制机器码。字节码文件是程序最终执行需要的文件,而不是 java 源文件。字节码文件可以拷贝到任何操作系统中运行,实现跨平台的效果。

    • 一个 java 源文件可以生成多个字节码文件,每个字节码文件对应源文件中定义的一个类;

  • 运行:

    • 使用JDK自带的执行工具/命令(java.exe)运行程序:

      java 包名.类名

      如:java com.somedomain.util.DataMonitor,包名实际上已目录结构的形式存在,并且通常以反向域名的方式形成多级目录结构,比如这里对应的目录结构为 ./com/somedomain/util/,最后跟类名 DataMonitor,它必须是在指定包内(目录结构下)存在的可运行字节码文件(类中包含作为程序入口的 public static void main() 方法)。

    • 运行阶段过程:
      java.exe 启动 JVM,JVM启动类加载器(ClassLoader),类加载器在硬盘上搜索对应的字节码文件,将其装载到 JVM 当中,JVM 将字节码文件解释成二进制数据,操作系统执行二进制和底层硬件平台进行交互,完成相应程序效果。

3 程序运行过程的内存分析

  • JVM 内存空间主要分为三块(还有其他空间):方法区内存堆内存栈内存

  • 方法片段属于字节码文件的一部分,字节码文件在类加载时,将其放到了方法区中。所以,JVM中的三块主要内存空间中最先有数据的是方法区内存,其中存放了代码片段。代码片段在方法区内存中只有一份,可以被重复使用。

  • 方法只有在被调用时才会被动态地分配所属空间。每次调用都需要为该方法在栈内存中分配独立的运行空间,此时发生压栈动作。方法执行结束后,对应的内存空间会全部释放,此时发生弹栈动作。下面是示例代码和方法执行过程的内存变化图。

public class UserTest {
    public static void main(String[] args) {
        int i = 10;
        add(i); // 传递i中的字面值10
        System.out.println("value of i in main() is " + i); // 10
    }

    public static void add(int i) {
        i++;
        System.out.println("value of i in add() is " + i); // 11
    }
}

传递字面值

  • 运行阶段在方法体中声明的局部变量,在栈内存中为其分配空间。而局部变量的参数传递实际上是传递的参数地址中所存的内容,这个内容可以是字面值,也可以是对象地址 。
    • 字面值传递中,接收方参数(等号左边)的值被修改,而传递方参数(等号右边)的值不受影响。

    • 对象地址的传递中,由于两个参数保存的内容指向同一对象地址,因此两方的操作指向同一片内存空间。下面是相应示例代码和方法执行过程的内存变化图。

public class UserTest {
    public static void main(String[] args) {
        User u = new User("Tomas", 56);
        System.out.println(u.name+"'s age before add() is "+u.age); // 1
        add(u); // 传递u的内存地址
        System.out.println(u.name+"'s age after add() is "+u.age); // 10
    }

    public static void add(User u) {
        u.age ++;
        System.out.println(u.name+"'s age within add() is "+u.age);
    }
}

class User {
    String name;
    int age;
    public User(String n, int a) {
        name = n;
        age = a;
    }
}

传递对象地址

参考

  1. 关于 JVM JDK 和 JRE 最详细通俗的解答

  2. B站教程

posted @ 2021-04-01 16:41  alterwl  阅读(1090)  评论(0)    收藏  举报