vlambda博客
学习文章列表

JDK17 |java17学习 第 8 章 多线程和并发处理

Chapter 9: JVM Structure and Garbage Collection

本章将为您提供 Java 虚拟机 (JVM) 的结构和行为的概述,这些虚拟机比较复杂超出您的预期。

JVM 根据编码逻辑执行指令。它还查找应用程序请求的 .class 文件并将其加载到内存中,验证它们,解释字节码(即,将它们转换为特定于平台的二进制代码) ,并将生成的二进制代码传递给中央处理器(或多个处理器)执行。除了应用程序线程之外,它还使用多个服务线程。其中一个名为 garbage collection (GC) 的服务线程执行从未使用的对象中释放内存的重要步骤。

通过完成本章,您将了解 Java 应用程序执行的构成、JVM 和 GC 内部的 Java 进程以及 JVM 的一般工作原理。

在本章中,我们将介绍以下主题:

  • Java应用程序执行
  • Java 进程
  • JVM的结构
  • 垃圾收集

Technical requirements

要执行本章提供的代码示例,您将需要以下内容:

  • 装有 Microsoft Windows、Apple macOS 或 Linux 操作系统的计算机
  • Java SE 版本 17 或更高版本
  • 您选择的 IDE 或代码编辑器

第 1 章Java 17 入门。包含本章代码示例的文件可在 GitHub 上的 https:// /github.com/PacktPublishing/Learn-Java-17-Programming.git 存储库,在 examples/src/main/java/com/packt/learnjava/ch09_jvm 文件夹。

Java application execution

在我们了解JVM的工作原理之前,让我们回顾一下如何运行应用程序,记住以下语句用作同义词:

  • 运行/执行/启动主类。
  • 运行/执行/启动主要方法。
  • 运行/执行/启动/启动应用程序。
  • 运行/执行/启动/启动 JVM 或 Java 进程。

还有几种方法可以做到这一点。在第 1 章中,< em class="italic">Java 17 入门,我们向您展示了如何使用 IntelliJ IDEA 运行 main(String[]) 方法。在本章中,我们将仅重复一些已经说过的内容,并添加其他可能对您有所帮助的变体。

Using an IDE

任何 IDE 都允许您运行 main() 方法。在IntelliJ IDEA中,可以通过三种方式完成:

  1. 单击 main() 方法名称旁边的绿色三角形:
JDK17 |java17学习 第 8 章 多线程和并发处理
  1. 使用绿色三角形至少执行一次 main() 方法后,将添加类的名称 <一个 id="_idIndexMarker1062"> 到下拉菜单(在顶行,绿色三角形的左侧):
    JDK17 |java17学习 第 8 章 多线程和并发处理
  1. 打开 Run 菜单并选择类的名称。您可以选择几个选项:
JDK17 |java17学习 第 8 章 多线程和并发处理

在之前的截图中,还可以看到 Edit Configurations...选项 。这可用于设置在开始时传递给 main() 方法的所有 程序参数,以及一些其他选项:

JDK17 |java17学习 第 8 章 多线程和并发处理

Program arguments 字段允许在 java 命令中设置参数。例如,让我们在此字段中设置一二三:

JDK17 |java17学习 第 8 章 多线程和并发处理

此设置将导致以下 java 命令:

java -DsomeParameter=42 -cp . \
       com.packt.learnjava.ch09_jvm.MyApplication one two three

我们可以在 main() 方法中读取这些参数:

public static void main(String... args){
    System.out.println("Hello, world!"); 
                              //prints: Hello, world!
    for(String arg: args){
        System.out.print(arg + " ");     
                              //prints: one two three
    }
    String p = System.getProperty("someParameter");
    System.out.println("\n" + p);        //prints: 42
}

Edit Configurations 屏幕上的另一个可能设置位于 Environment variables 字段中。可以使用 System.getenv() 从应用程序访问的环境变量。例如,让我们设置环境变量 x 和 y ,如下所示:

JDK17 |java17学习 第 8 章 多线程和并发处理

如果按照前面的屏幕截图所示完成,x 和 y 的值不仅可以在 main() 方法中读取,还可以在应用程序的任何位置使用 System.getenv(“varName”) 方法读取。在我们的例子中,可以按如下方式检索 x 和 y 的值:

String p = System.getenv("x");
System.out.println(p);            //prints: 42
p = System.getenv("y");
System.out.println(p);            //prints: 43

VM options 字段允许您设置 java 命令选项。例如,如果您输入 -Xlog:gc,IDE 将形成以下 java 命令:

java -Xlog:gc -cp . com.packt.learnjava.ch09_jvm.MyApplication

-Xlog:gc 选项要求显示 GC 日志。我们将在下一节中使用这个选项来演示 GC 是如何工作的。 -cp . 选项(cp 代表 classpath)表示 该类位于文件树上从当前目录(输入命令的目录)开始的文件夹中。在我们的例子中,.class 文件位于 com/packt/learnjava/ch09_jvm 文件夹中,其中 com 是当前目录的子文件夹。类路径可以包含 JVM 必须查找应用程序执行所需的 .class 文件的许多位置。

使用 Modify options 链接显示 VM options,如下所示:

JDK17 |java17学习 第 8 章 多线程和并发处理

对于这个演示,让我们在 VM 选项 -DsomeParameter=42 strong> 字段如 如下截图所示:

JDK17 |java17学习 第 8 章 多线程和并发处理

现在 someParameter 的值不仅可以在 main() 方法中读取,还可以在应用程序代码的任何位置读取,如下所示:

String p = System.getProperty("someParameter");
System.out.println("\n" + p);    
                     //prints someParameter set as VM option -D

java 命令的其他参数也可以在 Edit Configurations 屏幕上设置。我们鼓励您在该屏幕上花一些时间并查看可能的选项。

Using the command line with classes

现在,让我们命令行运行MyApplication。提醒您,主类如下所示:

package com.packt.learnjava.ch09_jvm;
public class MyApplication {
   public static void main(String... args){
      System.out.println("Hello, world!");
                                //prints: Hello, world!
      for(String arg: args){
         System.out.print(arg + " ");
                                //prints all arguments
      }
      String p = System.getProperty("someParameter");
      System.out.println("\n" + p); 
                     //prints someParameter set as VM option -D
   }
}

首先,它必须使用 javac 命令进行编译。命令行在 Linux 类型的平台上如下所示(假设您在项目的根目录中打开终端窗口,在 pom.xml 所在的文件夹中):

javac src/main/java/com/packt/learnjava/ch09_jvm/MyApplication.java

在 Windows 上,该命令看起来类似:

javac src\main\java\com\packt\learnjava\ch09_jvm\MyApplication.java

编译后的 MyApplication.class 文件与 MyApplication.java 放在同一文件夹中。现在,我们可以使用 java 命令执行编译后的类:

java -DsomeParameter=42 -cp src/main/java \
  com.packt.learnjava.ch09_jvm.MyApplication one two three

注意 -cp 指向 src/main/java 文件夹(路径相对于当前文件夹),其中主类的包启动。结果如下:

JDK17 |java17学习 第 8 章 多线程和并发处理

我们还可以将两个编译后的类放在一个 .jar 文件中并从那里运行它们。

Using the command line with JAR files

将编译后的文件作为 .class 文件保存在文件夹中并不总是很方便,尤其是 当很多 同一框架的编译文件属于不同的包,并作为单个库分发。在这种情况下,编译后的 .class 文件通常一起归档在 .jar 文件中。这种存档的格式与 .zip 文件的格式相同。唯一的区别是 .jar 文件还包括一个清单文件,其中包含描述存档的元数据(我们将在下一节中详细讨论清单)。

为了演示如何使用它,让我们创建一个 .jar 文件,其中包含 ExampleClass.class 文件和另一个 .jar 文件,其中包含 MyApplication.class,使用以下命令:

cd src/main/java
jar -cf myapp.jar 
               com/packt/learnjava/ch09_jvm/MyApplication.class
jar -cf example.jar \
        com/packt/learnjava/ch09_jvm/example/ExampleClass.class

请注意,我们需要在 .class 文件的包开始的文件夹中运行 jar 命令。

现在,我们可以运行应用程序,如下所示:

java -cp myapp.jar:example.jar \
                     com.packt.learnjava.ch09_jvm.MyApplication

.jar 文件位于当前文件夹中。如果我们想从另一个文件夹执行应用程序(让我们回到根目录,cd ../../..),命令应该如下所示:

java -cp 
src/main/java/myapp.jar:src/main/java/example.jar\
            com.packt.learnjava.ch09_jvm.MyApplication

请注意,每个 .jar 文件都必须单独列在类路径中。仅指定所有 .jar 文件所在的文件夹(如 .class 文件的情况)并不好足够的。您还必须添加一个星号(通配符,*),如下所示:

java -cp "src/main/java/*" \
           com.packt.learnjava.ch09_jvm.MyApplication

注意 包含 .jar 文件的文件夹路径的引号.没有引号,这是行不通的。

Using the command line with an executable JAR file

可以避免在命令行中指定主类。相反,我们可以创建一个可执行.jar文件。这可以通过将主类的名称(您需要运行且包含 main() 方法的名称)放入清单文件中来完成。以下是步骤:

  1. 创建一个名为 manifest.txt 的文本文件(名称无关紧要,但这个名称可以明确意图),其中包含以下行:
    主类:com.packt.learnjava.ch09_jvm.MyApplication 

冒号后面必须有一个空格(:),最后必须有一个不可见的换行符,所以请确保你按下了Enter 键,您的光标已跳到下一行的开头。

  1. 执行以下命令:
    cd src/main/java  jar -cfm myapp.jar manifest.txt \          com/packt/learnjava/ch09_jvm/*.class \          com/packt/learnjava/ch09_jvm/example/*.class<按钮类="copy-clipboard-button">复制

注意 jar 命令选项 (fm) 的顺序和 myapp.jar 的顺序manifest.txt 文件。它们必须相同,因为 f 代表 jar 命令将要创建的文件,而 m 代表清单源。如果您在 mf 中包含选项,则文件必须列为 manifest.txt myapp.jar

  1. 现在,我们可以使用以下命令运行应用程序:
         java -jar myapp.jar 

其他创建可执行 .jar 文件的方法要容易得多:

jar cfe myjar.jar 
com.packt.learnjava.ch09_jvm.MyApplication \
             com/packt/learnjava/ch09_jvm/*.class\
             com/packt/learnjava/ch09_jvm/example/*.class

前面的命令会自动生成具有指定主类名称的清单:c 选项代表 创建一个新存档,f选项代表存档文件名,e 选项表示应用程序入口点。

Java processes 

您可能已经猜到了,JVM 对 Java 语言和源代码一无所知。它只知道如何读取字节码。它从 .class 文件中读取字节码和其他信息,将字节码转换(解释)为特定于当前平台(运行 JVM 的平台)的二进制代码指令序列,并将生成的二进制代码传递给执行它的微处理器。在谈到这种转换时,程序员通常将其称为 Java 进程 或简称为 进程。

JVM 通常被称为JVM 实例。这是因为每次执行 java 命令时,都会启动一个新的 JVM 实例,该实例专用于将特定应用程序作为具有自己分配内存的单独进程运行(大小为内存设置为默认值或作为命令选项传入)。在这个 Java 进程中,多个线程正在运行,每个线程都有自己分配的内存。有些是由 JVM 创建的服务线程;其他是由应用程序创建和控制的应用程序线程。

这就是 JVM 执行编译代码的总体情况。但是如果你仔细阅读JVM规范,你会发现关于JVM的process这个词也被用来描述JVM内部进程。 JVM 规范确定了在 JVM 中运行的其他几个通常不会被程序员提及的进程,除了 可能是 类加载进程。

这是因为大多数时候,我们可以在不了解内部 JVM 进程的情况下成功编写和执行 Java 程序。但是偶尔,对 JVM 内部工作原理的的一些一般性了解有助于我们确定某些问题的根本原因。这就是为什么在本节中,我们将简要概述 JVM 内部发生的所有进程。然后,在接下来的部分中,我们将讨论 JVM 的内存结构及其功能的其他方面,这些方面可能对程序员有用。

两个子系统运行 JVM 的内部进程:

  • 类加载器:这个 读取 .class 文件并填充方法区域在 JVM 的内存中与类相关的数据:
    • 静态字段
    • 方法字节码
    • 描述类的类元数据
  • 执行引擎:这使用以下属性执行字节码:
    • 对象实例化的堆区域
    • 用于跟踪已调用方法的 Java 和本机方法堆栈
    • 一个回收内存的 GC 进程

在 JVM 主进程内部运行的一些进程如下:

  • 类加载器执行的进程,例如:
    • 类加载
    • 类链接
    • 类初始化

由执行引擎执行的进程,例如:

  • 类实例化
  • 方法执行
  • GC
  • Application termination

    JVM 架构

    JVM 架构可以描述为有两个子系统—— classloader 和 执行引擎 – 运行服务进程和应用程序线程使用运行时数据内存区域,例如方法区域、堆和应用程序线程堆栈。 线程 是轻量级的进程,比JVM执行进程需要更少的资源分配.

此列表可能会给您一种印象,即这些过程是按顺序执行的。在某种程度上,这是真的,如果我们只谈论一个类的话。在加载 类之前,无法对其进行任何操作。我们只能在前面的所有流程都完成后才能执行一个方法。但是,例如,GC 不会在对象停止使用后立即发生(请参阅 垃圾收集 部分)。此外,当发生未处理的异常或其他错误时,应用程序可以随时退出。

只有类加载器进程受 JVM 规范规范。执行引擎的实现很大程度上取决于每个供应商。它基于实现作者设定的语言语义和性能目标。

执行引擎的进程处于不受 JVM 规范规范的领域。有常识、传统、已知和经过验证的解决方案,以及可以指导 JVM 供应商的实施决策的 Java 语言规范。但没有单一的规范性文件。好消息是最流行的 JVM 使用类似的解决方案——或者至少从高层次上看是这样。

考虑到这一点,让我们更详细地讨论前面列出的七个过程中的每一个。

Classloading

根据 JVM 规范,加载阶段包括通过文件名(在类路径中列出的位置)找到 .class 文件并在内存中创建其表示。

加载的第一个类是在命令行中传递的类,其中的 main(String[]) 方法它。类加载器读取 .class 文件,对其进行解析,并使用静态字段和方法字节码填充方法区域。它还创建一个描述类的java.lang.Class实例。然后,类加载器链接类(参见类链接部分),初始化它(参见类初始化部分),然后然后将其传递给执行引擎以运行其字节码。

main(String[]) 方法是进入应用程序的入口。如果它调用另一个类的方法,则该类必须在类路径中找到、加载和初始化;只有这样才能执行它的方法。如果这个——刚刚加载的——方法调用了另一个类的方法,那么这个类也必须被找到、加载和初始化,等等。这就是 Java 应用程序的启动和运行方式。

 main(String[]) 方法

每个类都可以有一个 main(String[]) 方法并且经常这样做。这种方法用于将类作为独立应用程序独立运行,以用于测试或演示目的。这种方法的存在不会使类 main。只有在 java 命令行或 < code class="literal">.jar 文件清单。

话虽如此,让我们继续讨论加载过程。

如果您查看 java.lang.Class 的 API,您将不会在其中看到公共构造函数。类加载器自动创建它的实例。这与 getClass() 方法返回的实例相同,您可以在任何 Java 对象上调用该方法。

它不携带类的静态数据(保留在方法区域中),也不携带状态值(它们位于执行期间创建的对象中)。它也不包含方法字节码(这也存储在方法区域中)。相反,Class 实例提供了描述类的元数据——它的名称、包、字段、构造函数、方法签名等。此元数据不仅对 JVM 有用,而且对应用程序也有用。

笔记

所有由类加载器在内存中创建并由执行引擎维护的数据被称为 类型的二进制表示。

如果 .class 文件包含错误或不符合特定格式,则终止该过程。这个意味着加载的类格式和它的字节码已经被加载过程验证过了。在下一个过程开始时进行更多验证,称为 类链接。

这是加载过程的高级描述。它执行三个任务:

  1. 查找并读取 .class 文件
  2. 根据方法区内部数据结构解析
  3. 使用类元数据创建 java.lang.Class 的实例

Class linking

根据JVM规范,类链接解析加载的类的引用,以便执行该类的方法。

这是链接过程的高级描述。它执行三个任务:

  • 验证类或接口的二进制表示:尽管 JVM 可以合理地预期 .class 文件是由Java 编译器和所有指令都满足语言的约束和要求,不能保证加载的文件是由已知的编译器实现或编译器生成的。这就是为什么链接过程的第一步是验证。这确保了类的二进制表示在结构上是正确的,这意味着以下内容:
    • 每个方法调用的参数与方法描述符兼容。
    • 返回指令与其方法的返回类型相匹配。
    • 其他一些检查和验证过程,具体取决于 JVM 供应商。
  • 在方法区准备静态字段:验证完成后,在方法区创建接口或类(静态)变量, 初始化为其类型的默认值。其他类型的初始化,例如由程序员指定的显式赋值和静态初始化块,被推迟到称为类初始化的过程(参见类初始化部分)。
  • 将符号引用解析为指向方法区的具体引用:如果加载的字节码引用了其他方法、接口或类,则将符号引用解析为指向方法区的具体引用方法区,由解析过程完成。如果引用的接口和类尚未加载,类加载器会根据需要查找并加载它们。

Class initialization

根据JVM规范,初始化是通过执行类初始化方法来完成的。当执行程序员定义的初始化(在静态块和静态分配中)时会发生这种情况,除非 类已经应另一个类的请求进行了初始化。

该语句的最后一部分很重要,因为该类可能会被不同的(已加载的)方法多次请求,并且还因为 JVM 进程由不同的线程执行并且可能同时访问同一个类。所以,不同线程之间需要协调(也叫称为同步),这大大复杂化了 JVM 的实现。

Class instantiation

这一步可能永远不会发生。从技术上讲,由 new 运算符触发的实例化过程是执行过程的第一步。如果 main(String[]) 方法(它是静态的)仅使用其他类的静态方法 ,则此实例化永远不会发生。这就是为什么将这个过程与执行分开是合理的。

这个活动有非常具体的任务:

  • 为堆区域中的对象(其状态)分配内存
  • 将实例字段初始化为默认值
  • 为 Java 和本机方法创建线程堆栈

当第一个方法(不是构造函数)准备好执行时开始执行。对于每个应用程序线程,都会创建一个专用的运行时堆栈,其中每个方法调用都被捕获在堆栈帧中。例如,如果发生异常,我们在调用 printStackTrace() 方法时从当前堆栈帧中获取数据。

Method execution

main(String[]) 方法 开始执行。它可以创建其他应用程序线程。

执行引擎读取字节码,对其进行解释,并将二进制码发送给微处理器执行。它还维护一个计数每个方法被调用的次数和频率。如果计数超过某个阈值,则执行引擎使用编译器,称为 just-in-time (JIT) 编译器, 将方法字节码编译为本机代码。这样,下次调用该方法时,无需解释即可准备就绪。这大大提高了代码性能。

当前正在执行的指令和下一条指令的地址保存在程序计数器(PC) 注册。每个线程都有专用的 PC 寄存器。它还可以提高性能并跟踪执行情况。

Garbage collection

垃圾收集器识别不再被引用的对象,并且可以从内存中删除。

一个 Java 静态方法 System.gc(),可用于以编程方式触发 GC,但它的直接不保证执行。每个 GC 周期都会影响应用程序的性能,因此 JVM 必须在内存可用性和足够快地执行字节码的能力之间保持平衡。

Application termination

可以通过几种方式以编程方式终止应用程序(以及 JVM 停止或退出):

  • 正常终止,没有错误状态码
  • 由于未处理的异常而异常终止
  • 强制程序退出,有或没有错误状态代码

如果没有异常和无限循环,则 main(String[]) 方法以 return 语句或在其最后一条语句执行后完成。一旦发生这种情况,主应用程序线程就会将控制流传递给 JVM,并且 JVM 也会停止执行。那是美好的结局,许多应用程序在现实生活中都享受它。我们的大多数示例,除了那些我们展示了异常或无限循环的示例,也都成功退出了。

然而,Java 应用程序还可以通过其他方式退出,其中一些方式也非常优雅——其他方式则不然。如果主应用程序线程创建了子线程,或者换句话说,程序员编写了生成其他线程的代码,那么即使是优雅的退出也可能并不容易。这完全取决于已创建的子线程的类型。

如果其中任何一个是用户线程(默认),那么即使在主线程退出后,JVM 实例也会继续运行。只有在所有用户线程都完成后,JVM 实例才会停止。主线程可以请求子用户线程完成。但在它退出之前,JVM 会继续运行。这意味着该应用程序仍在运行。

但是如果所有子线程都是守护线程,或者没有子线程在运行,那么一旦主应用程序线程退出,JVM 实例就会停止运行。

应用程序在异常情况下如何退出取决于代码设计。我们在第 4 章中谈到了这一点a>,异常处理,同时讨论异常处理的最佳实践。如果线程在 main(String[]) 或类似的高级方法中的 try-catch 块中捕获所有异常,则取决于应用程序(以及编写代码的程序员)来决定如何最好地进行 - 尝试更改 输入数据并重复 代码块生成异常,记录错误并继续,或退出。另一方面,如果异常仍未处理并传播到 JVM 代码中,则线程(发生异常的位置)停止执行并退出。接下来会发生什么取决于线程的类型和其他一些条件。以下 是四个可能的选项:

  • 如果没有其他线程,JVM 将停止执行并返回错误代码和堆栈跟踪。
  • 如果具有未处理异常的线程不是主线程,则其他线程(如果存在)继续运行。
  • 如果主线程抛出了未处理的异常并且子线程(如果存在)是守护进程,它们也会退出。
  • 如果至少有一个用户子线程,则 JVM 继续运行,直到所有用户线程退出。

还有一些方法可以以编程方式强制应用程序停止:

  • System.exit(0);
  • Runtime.getRuntime().exit(0);
  • Runtime.getRuntime().halt(0);

所有这些方法都会强制 JVM 停止执行任何线程并退出并以作为参数传入的状态码(在我们的示例中为 0):

  • 零表示正常终止
  • 非零值表示异常终止

如果 Java 命令是由某个脚本或其他系统启动的,则状态码的值可用于自动化有关下一步的决策。但这已经在应用程序和 Java 代码之外。

第一个 两个方法具有相同的功能 因为这就是 System.exit() 实现:

public static void exit(int status) { 
    Runtime.getRuntime().exit(status); 
}

要在 IDE 中查看源代码,只需单击方法即可。

当某个线程调用 RuntimeSystem< 的 exit() 方法时,JVM 退出/code> 类,或 Runtime 类的 halt() 方法,并且退出或停止操作由安全经理。 exit()halt() 的区别在于 halt() 强制 JVM 立即退出,而 exit() 执行可以使用 Runtime.addShutdownHook() 设置的附加操作方法。但是这些选项很少被主流程序员使用。

JVM’s structure

JVM的结构可以用它在内存中的运行时数据结构和使用运行时数据的两个子系统——类加载器和执行引擎来描述。

Runtime data areas

JVM 内存的每个 运行时数据区域属于 以下两个类别之一:

  • 共享区域,其中包括以下内容:
    • 方法区:类元数据、静态字段和方法字节码
    • 堆区:对象(状态)
  • 未共享区域,专用于特定应用程序线程,包括以下内容:
    • Java 堆栈:当前帧和调用者帧,每个帧都保持 Java(非本机)方法调用的状态:
      1. 局部变量的值
      2. 方法参数值
      3. 用于中间计算的操作数的值(操作数堆栈)
      4. 方法返回值(如果有)
  • PC 寄存器:下一条要执行的指令
  • 原生方法栈:原生方法调用的状态

我们已经讨论过程序员在使用引用类型时必须小心,除非需要这样做,否则不要修改对象本身.在多线程应用程序中,如果可以在线程之间传递对对象的引用,我们必须格外小心,因为有可能同时修改相同的数据。不过,从好的方面来说,这样的共享区域可以——而且经常被——用作线程之间的通信方法。

Classloaders

类加载器执行以下三个函数:

  • 读取 .class 文件
  • 填充方法区域
  • 初始化尚未由程序员初始化的静态字段

Execution engine

执行引擎执行以下操作:

  • 实例化堆区域中的对象
  • 使用程序员编写的初始化程序初始化静态和实例字段
  • 在 Java 堆栈中添加/删除框架
  • 使用要执行的下一条指令更新 PC 寄存器
  • 维护本机方法堆栈
  • 记录方法调用并编译流行的方法调用
  • 最终确定对象
  • 运行 GC
  • 终止应用程序

Garbage collection

自动内存管理是 JVM 的一个重要方面,它使程序员 无需以编程方式执行此操作。在 Java 中,清理内存并允许它被重用的进程称为 GC。

Responsiveness, throughput, and stop-the-world

GC 的有效性影响到两个主要的应用程序特性——响应性和吞吐量:

  • 响应速度:这是应用程序响应(带来必要数据)请求的速度来衡量的;例如,网站返回页面的速度,或者桌面应用程序响应事件的速度。响应时间越短,用户体验越好。
  • 吞吐量:这表示应用程序在单位时间内可以完成的工作量;例如,一个 Web 应用程序可以服务多少请求,或者数据库可以支持多少事务。数字越大,应用程序可能产生的价值就越多,它可以支持的用户请求就越多。

同时,GC需要移动数据,这在允许数据处理的情况下是不可能完成的,因为引用会发生变化。这就是为什么 GC 需要时不时地停止应用程序线程执行一段时间。这称为 stop-the-world。 这些时间越长,GC 完成工作的速度就越快,应用程序冻结的持续时间就越长,最终会增长到足以影响应用程序的响应能力和吞吐量。

幸运的是,可以使用 Java 命令选项调整 GC 的行为,但这超出了本书的范围。相反,我们将提供 GC 主要活动的高级视图——检查堆中的对象并删除那些在任何线程堆栈中没有引用的对象。

Object age and generation

基本的GC 算法确定每个对象的多大age 一词是指对象已存活的收集周期数。

JVM启动时,堆是空的,分为三个部分:

  • 年轻一代
  • 老一代或终身一代
  • 用于容纳标准区域大小 50% 或更大的对象的巨大区域

年轻一代有三个方面:

  • 伊甸园空间
  • 幸存者 0 (S0)
  • 幸存者 1 (S1)

新创建的对象被放置在 Eden 中。当它填满时,会启动一个次要 GC 进程。它删除未引用和循环引用的对象并将其他对象移动到 S1 区域。在下一次次要收集期间,S0 和 S1 交换角色。引用的对象从 Eden 和 S1 移动到 S0。

在每次 次收集期间,将达到一定年龄的对象移动到老年代。作为该算法的结果,老年代包含比某个年龄更早的对象。这个区域比年轻代更大,因此,GC 过程更昂贵,并且不像年轻代那样频繁发生。但最终会被检查(经过几次次要收集后)。未引用的对象被删除,内存被碎片整理。清理老年代被认为是一个主要的收集。

When stop-the-world is unavoidable

一些对象是在老年代并发收集的,而一些是使用stop-the-world暂停来收集的。步骤如下:

  1. 初始标记:这标记了可能引用老年代对象的幸存区域(根区域)。这是使用停止世界暂停来完成的。
  2. 扫描:搜索幸存者区域以查找对老年代的引用。这是在应用程序继续运行时同时完成的。
  3. 并发标记:这会在整个堆上标记活动对象,并在应用程序继续运行时同时完成。
  4. Remark:在这个阶段,活动对象已经被标记,这是使用 stop-the-world 暂停来完成的。
  5. Cleanup:计算活动对象的年龄,释放区域(使用 stop-the-world),并将它们返回到空闲列表。这是同时进行的。

为了帮助进行 GC 调整,JVM 为垃圾收集器、堆大小和运行时编译器提供了与平台相关的默认选择。但幸运的是,JVM 供应商一直在改进和调整 GC 过程,因此大多数应用程序在默认 GC 行为下工作得很好。

Summary

在本章中,您了解了如何使用 IDE 或命令行来执行 Java 应用程序。现在,您可以编写应用程序并以适合给定环境的方式启动它们。关于 JVM 结构及其进程(类加载、链接、初始化、执行、GC 和应用程序终止)的知识使您能够更好地控制应用程序的执行以及关于 JVM 性能和当前状态的透明度。

在下一章中,我们将讨论和演示如何通过 Java 应用程序管理(插入、读取、更新和删除)数据库中的数据。我们还将简要介绍 SQL 语言及其基本数据库操作,包括如何连接数据库、如何创建数据库结构、如何使用 SQL 编写数据库表达式以及如何执行它们。

Quiz

回答以下问题以测试您对本章的了解:

  1. 选择所有正确的陈述:
    1. IDE 无需编译即可执行 Java 代码。
    2. IDE 使用已安装的 Java 来执行代码。
    3. IDE 在不使用 Java 安装的情况下检查代码。
    4. IDE 使用 Java 安装的编译器。
  2. 选择所有正确的陈述:
    1. 应用程序使用的所有类都必须列在类路径中。
    2. 应用程序使用的所有类的位置必须列在类路径中。
    3. 如果类位于类路径中列出的文件夹中,编译器可以找到该类。
    4. 主包的类不需要在类路径中列出。
  3. 选择所有正确的陈述:
    1. 应用程序使用的所有 .jar 文件都必须列在类路径中。
    2. 应用程序使用的所有 .jar 文件的位置必须列在类路径中。
    3. JVM 只能在类路径中列出的 .jar 文件中找到一个类。
    4. 每个类都可以包含 main() 方法。
  4. 选择所有正确的陈述:
    1. 每个包含清单的 .jar 文件都是可执行的。
    2. 如果 java 命令使用了 -jar 选项,则忽略 classpath 选项。
    3. 每个 .jar 文件都有一个清单。
    4. 可执行的 .jar 是带有清单的 ZIP 文件。
  5. 选择所有正确的陈述:
    1. 类加载和链接可以在不同的类上并行工作。
    2. 类加载将类移动到执行区域。
    3. 类链接连接两个类。
    4. 类链接使用内存引用。
  6. 选择所有正确的陈述:
    1. 类初始化为实例属性赋值。
    2. 每次类被另一个类引用时都会进行类初始化。
    3. 类初始化为静态属性赋值。
    4. 类初始化为java.lang.Class的实例提供数据。
  7. 选择所有正确的陈述:
    1. 类实例化可能永远不会发生。
    2. 类实例化包括对象属性初始化。
    3. 类实例化包括堆上的内存分配。
    4. 类实例化包括执行构造函数代码。
  8. 选择所有正确的陈述:
    1. 方法执行包括二进制代码生成。
    2. 方法执行包括源代码编译。
    3. 方法执行包括重用由 JIT 编译器生成的二进制代码。
    4. 方法执行计算每个方法被调用的次数。
  9. 选择所有正确的陈述:
    1. 在调用 System.gc() 方法后立即开始垃圾收集。
    2. 可以使用或不使用错误代码来终止应用程序。
    3. 一旦抛出异常,应用程序就会退出。
    4. 主线程是用户线程。
  10. 选择所有正确的陈述:
    1. JVM 具有跨所有线程共享的内存区域。
    2. JVM 具有未跨线程共享的内存区域。
    3. 类元数据在所有线程之间共享。
    4. 方法参数值不跨线程共享。
  11. 选择所有正确的陈述:
    1. 类加载器填充方法区域。
    2. 类加载器在堆上分配内存。
    3. 类加载器写入 .class 文件。
    4. 类加载器解析方法引用。
  12. 选择所有正确的陈述:
    1. 执行引擎在堆上分配内存。
    2. 执行引擎终止应用程序。
    3. 执行引擎运行垃圾回收。
    4. 执行引擎初始化尚未由程序员初始化的静态字段。
  13. 选择所有正确的陈述:
    1. 数据库可以支持的每秒事务数是吞吐量度量。
    2. 当垃圾收集器暂停应用程序时,它被称为 stop-all-things。
    3. 网站返回数据的速度有多慢是衡量响应能力的指标。
    4. 垃圾收集器清除作业的 CPU 队列。
  14. 选择所有正确的陈述:
    1. 对象年龄以自对象创建以来的秒数衡量。
    2. 对象越旧,就越有可能从内存中删除。
    3. 清理老年代是一个主要的收集。
    4. 将对象从年轻代的一个区域移动到年轻代的另一个区域是次要收集。
  15. 选择所有正确的陈述:
    1. 可以通过设置javac命令的参数来调整垃圾收集器。
    2. 可以通过设置java命令的参数来调整垃圾收集器。
    3. 垃圾收集器根据其逻辑工作,无法根据设置的参数更改其行为。
    4. 清理老年代区域需要停顿一下。