当前位置:Java -> Java 执行之旅:从加载器到内存模型

Java 执行之旅:从加载器到内存模型

你是否曾经想过,当你点击Java程序中的“运行”按钮时,在幕后会发生什么?这个过程涉及一系列复杂的步骤,从编译和将代码加载到内存中到管理数据结构(如堆和栈)中的数据。

在这里,我们将探讨运行基本Java程序的步骤,重点介绍加载程序、编译器、运行器和内存模型的作用。考虑一个简单的Java程序,使用递归计算数字“n”的阶乘。

正如你所知,数字的阶乘由递归关系定义:

F(n) = n * F(n-1),其中基本情况为 F(0) = 1 和 F(1) = 1。

让我们来了解这个程序的编译、加载和运行的各个步骤,以及了解该程序的内存管理方面。

示例程序: 阶乘计算

public class Factorial {
    public static void main(String[] args) {
        int n = 5;
        int result = factorial(n);
        System.out.println("Factorial of " + n + " is: " + result);
    }
    public static int factorial(int n) {
        if (n == 0 || n == 1) {
            return 1;
        } else {
            return n * factorial(n - 1);
        }
    }
}


1. 编译器: 性能的构建者

我们的旅程以编译器开始。Java编译器(javac)将我们可读的Java代码翻译成JVM可以理解的字节码。

当我们发出javac Factorial.java命令时,Java编译器(javac)将Factorial.java源代码编译成字节码。

编译器 - 架构性能

在编译过程中,编译器会检查语法错误,并验证代码的类型安全,确保数据类型在代码中正确且一致地使用。任何错误或不一致之处都会被标记出来,让程序员可以修改代码。

2. 加载程序: 构建舞台

舞台已经准备好,现在是加载程序的时候了,这是Java虚拟机(JVM)的一个重要组成部分。在启动Java程序时,加载程序从类路径获取字节码,包括Factorial类。这个字节码是我们代码的独立于平台的表示形式。

加载程序 - 准备执行环境

在像Factorial示例这样的简单Java程序中,Java标准库中的实用类,如java.lang包中的实用类可能会与Factorial类一起加载到内存中。

一些最初可能会加载进内存的java.lang包中的实用类包括:

  1. Object类: Java中的每个类都隐式扩展了Object类,因此当JVM启动时,会将它加载到内存中。 Object类提供了equalshashCodetoString等基本方法。
  2. String类: String类在Java程序中经常用于操作字符串。它被加载到内存中以支持涉及字符串的操作。
  3. System类: System类提供对系统属性和I/O流的访问。它通常用于控制台输入/输出、环境变量和与系统相关的操作。
  4. Math类: Math类提供数学函数和常量。它在Java程序中经常用于算术计算。
  5. ClassLoader类: ClassLoader类负责动态加载Java类到JVM中。虽然程序可能不会直接引用这个类,但它参与了类加载过程。

实用类

这些实用类对于Java程序的基本功能至关重要,它们会通过加载程序自动加载到JVM的内存中,同时也加载Factorial类。它们提供了常用于Java应用程序的基础功能。

3. 运行器: 启动行动

随着程序的展开,JVM协调执行,扮演“运行器”的角色。主方法充当入口点,标志着行动的开始。

内存管理、异常处理和方法调用会无缝发生。

内存管理、异常处理和方法调用都是无缝进行的。

  • 对象及其实例变量都被分配在堆上。方法调用和局部变量则在栈上找到它们的位置。
  • 应用中的每个线程都有自己的栈帧,为方法调用和数据存储提供了私有空间。
  • 在示例程序中,当主方法被调用时,一个新的栈帧会出现在栈上,包含像nresult这样的局部变量。
  • 调用factorial方法会触发一系列递归调用和连续栈帧的生成。
  • 每次递归调用,栈便会膨胀出新的栈帧,每个栈帧封装了方法的参数和局部变量。
  • 随着方法调用的返回,栈帧平稳地退回,将控制权交还给它们的前驱。
  • 最终,当主方法执行结束时,Java程序也就结束了。

4. 内存模型:幕后

让我们深入了解内存管理以理解内存分配。

在幕后,Java内存模型指导着数据在执行过程中的存储和访问。堆是一个共享的内存池,用于容纳动态分配的对象,而栈则为方法调用和局部变量提供了专用空间。

在执行过程中,内存扮演了协调执行的关键角色。

在我们的示例程序中,像nresult这样的原始变量会找到它们在栈上的位置,在它们各自的栈帧内。

随着factorial方法的递归调用展开,栈帧被创建和弹出,展示了方法调用的动态性。

虽然在我们的例子中没有说明,如果有的话,对象及它们的实例变量会被分配在堆上。

但是,为什么对象在堆上分配空间,而不是在栈上呢?让我们来讨论一下这个问题。

对象被分配在堆上,是因为它们是在运行时创建的,而且是动态的。在Java中创建的对象可能使用作用域标识符“public”、“private”、“protected”和“package-private”(默认)来指定它们的生存期。这使得它们的生命周期延伸到了它们被创建的方法或块的作用域之外。将对象放在堆上允许它们在单个方法调用之后持续存在,使它们可以被程序的多个部分访问。

内存模型

但是,对这些对象的引用通常存储在栈上或在堆上的其他对象内。

这个引用实质上是指向堆中对象位置的内存地址。因此,虽然实际的对象数据存放在堆上,对该对象的引用却存储在栈上。

应该注意的是,虽然对象的引用通常存储在栈上,但是对象也可以直接从堆引用。这种情况发生在一个对象包含对另一个对象的引用作为其实例变量之一时。在这种情况下,对第二个对象的引用存储在堆上为第一个对象分配的内存中。

接下来举个例子来说明这两种情况:

public class Example {
    public static void main(String[] args) {
        // Creating an object and storing its reference on the stack
        MyClass obj1 = new MyClass();

        // Creating another object and storing its reference in the instance variable of the first object
        obj1.setAnotherObject(new AnotherClass());
    }
}

class MyClass {
    private AnotherClass anotherObject;

    public void setAnotherObject(AnotherClass obj) {
        this.anotherObject = obj;
    }
}

class AnotherClass {
    // Class definition
}


在这个示例中:

obj1main()方法中被创建后,MyClass对象的引用被存储在栈上。

当调用obj1.setAnotherObject(new AnotherClass())时,会在堆上创建一个AnotherClass对象,并且它的引用会在堆上MyClass对象内部存储。

堆和栈之间的无缝交互因此,虽然对象在堆上分配,但是对这些对象的引用通常存储在栈上或在堆上的其他对象内,这取决于它们的使用上下文。

堆和栈之间的无缝交互确保了高效的内存分配和释放,促进了我们的Java程序的无缝执行。

结论

当我们对Java程序的执行过程开始反思时,我们将反思加载器、编译器、运行器以及内存模型之间复杂的相互作用,这些相互作用为每个Java程序的顺利运行提供了动力。从将代码加载到内存中到管理数据结构,JVM确保了Java程序的无缝执行。

因此,下次你运行Java程序时,不妨花一点时间去欣赏那些幕后的魔法,它们使一切成为可能。并且在掌声响起时,记住从加载器到内存模型的旅程,为你的代码铺平了道路。

视频

推荐阅读: 腾讯为什么不能制作出《原神》这么优秀的游戏?

本文链接: Java 执行之旅:从加载器到内存模型