Java 虚拟机 - 快速指南


Java 虚拟机 - 简介

JVM 是一个规范,并且可以有不同的实现,只要它们遵守规范即可。规格可以在下面的链接中找到 - https://docs.oracle.com

Oracle 有自己的 JVM 实现(称为 HotSpot JVM),IBM 有自己的 JVM(例如 J9 JVM)。

下面给出了规范中定义的操作(来源 - Oracle JVM 规范,请参阅上面的链接) -

  • “类”文件格式
  • 数据类型
  • 原始类型和值
  • 引用类型和值
  • 运行时数据区域
  • 镜框
  • 对象的表示
  • 浮点运算
  • 特殊方法
  • 例外情况
  • 指令集总结
  • 类库
  • 公共设计,私人实施

JVM是一个虚拟机,一个抽象计算机,有自己的ISA、自己的内存、堆栈、堆等。它运行在主机操作系统上,并向主机操作系统提出对资源的需求。

Java 虚拟机 - 架构

HotSpot JVM 3 的架构如下所示 -

建筑学

执行引擎由垃圾收集器和JIT编译器组成。JVM 有两种类型:客户端和服务器。这两者共享相同的运行时代码,但所使用的 JIT 有所不同。稍后我们将详细了解这一点。用户可以通过指定 JVM 标志-client-server来控制要使用的风格。服务器 JVM 专为服务器上长时间运行的 Java 应用程序而设计。

JVM 有 32b 和 64b 版本。用户可以通过在 VM 参数中使用 -d32 或 -d64 来指定要使用的版本。32b 版本最多只能寻址 4G 内存。由于关键应用程序在内存中维护大量数据集,64b 版本可以满足这一需求。

Java 虚拟机 - 类加载器

JVM 以动态方式管理类和接口的加载、链接和初始化过程。在加载过程中,JVM 会找到类的二进制表示形式并创建它。

在链接过程中,加载的类被组合到JVM的运行时状态,以便它们可以在初始化阶段执行。JVM基本上使用存储在运行时常量池中的符号表来进行链接过程。初始化包括实际执行链接的类

装载机的类型

BootStrap类加载器位于类加载器层次结构顶部。它加载 JRE 的lib目录中的标准 JDK 类。

扩展类加载器位于类加载器层次结构的中间,是引导类加载器的直接子级,并加载 JRE 的 lib\ext 目录中的类

应用程序类加载器位于类加载器层次结构的底部,并且是应用程序类加载器的直接子级。它加载CLASSPATH ENV变量指定的 jar 和类。

链接

链接过程包括以下三个步骤 -

验证- 这是由字节码验证程序完成的,以确保生成的 .class 文件(字节码)有效。如果不是,则会抛出错误并且链接过程停止。

准备- 内存分配给类的所有静态变量,并使用默认值初始化它们。

解决方案- 所有符号内存引用都替换为原始引用。为了实现这一点,需要使用类方法区的运行时常量内存中的符号表。

初始化

这是类加载过程的最后阶段。静态变量被赋予原始值并执行静态块。

Java 虚拟机 - 运行时数据区域

JVM 规范定义了程序执行期间所需的某些运行时数据区域。其中一些是在 JVM 启动时创建的。其他的是线程本地的,仅在创建线程时创建(并在线程销毁时销毁)。下面列出了这些 -

PC(程序计数器)寄存器

它对于每个线程来说是本地的,包含线程当前正在执行的 JVM 指令的地址。

它对于每个线程来说是本地的,并在方法调用期间存储参数、本地变量和返回地址。如果线程需要的堆栈空间超过允许的空间,则可能会发生 StackOverflow 错误。如果堆栈是动态可扩展的,它仍然会抛出 OutOfMemoryError。

它在所有线程之间共享,并包含在运行时创建的对象、类的元数据、数组等。它在 JVM 启动时创建,在 JVM 关闭时销毁。您可以使用某些标志(稍后会详细介绍)来控制 JVM 要求操作系统提供的堆量。必须注意不要要求内存太少或太多,因为它对性能有重要影响。此外,GC 管理该空间并不断删除死对象以释放空间。

方法区

该运行时区域是所有线程共用的,并且在 JVM 启动时创建。它存储每个类的结构,例如常量池(稍后详细介绍)、构造函数和方法的代码、方法数据等。JLS 没有指定该区域是否需要进行垃圾收集,因此也没有指定该区域的实现JVM 可能会选择忽略 GC。此外,这可能会也可能不会根据应用程序的需要进行扩展。JLS 对此没有任何强制要求。

运行时常量池

JVM 维护每个类/每个类型的数据结构,该结构在链接加载的类时充当符号表(其众多角色之一)。

本机方法堆栈

当线程调用本地方法时,它进入了一个新的世界,在这个世界中,Java虚拟机的结构和安全限制不再阻碍它的自由。本机方法可能可以访问虚拟机的运行时数据区域(这取决于本机方法接口),但也可以执行它想做的任何其他操作。

垃圾收集

JVM 管理 Java 中对象的整个生命周期。一旦创建了对象,开发人员就无需再担心它了。如果对象死亡(即不再有对它的引用),G​​C 会使用多种算法之一(串行 GC、CMS、G1 等)将其从堆中弹出。

在GC过程中,对象在内存中移动。因此,在该过程正在进行时,这些对象不可用。在此过程期间必须停止整个应用程序。这种暂停被称为“停止世界”暂停,并且是一个巨大的开销。GC 算法的主要目的是减少这个时间。我们将在接下来的章节中详细讨论这一点。

由于 GC,内存泄漏在 Java 中非常罕见,但也有可能发生。我们将在后面的章节中看到如何在 Java 中创建内存泄漏。

Java 虚拟机 - JIT 编译器

在本章中,我们将了解 JIT 编译器,以及编译型语言和解释型语言之间的区别。

编译语言与解释语言

C、C++ 和 FORTRAN 等语言都是编译语言。他们的代码以针对底层机器的二进制代码形式交付。这意味着高级代码会被专门为底层架构编写的静态编译器立即编译为二进制代码。生成的二进制文件不会在任何其他体系结构上运行。

另一方面,像 Python 和 Perl 这样的解释语言可以在任何机器上运行,只要它们有一个有效的解释器。它逐行遍历高级代码,将其转换为二进制代码。

解释的代码通常比编译的代码慢。例如,考虑一个循环。解释器将为循环的每次迭代转换相应的代码。另一方面,编译后的代码将使翻译只有一次。此外,由于解释器一次只能看到一行,因此它们无法执行任何重要的代码,例如改变编译器等语句的执行顺序。

我们将研究下面这种优化的一个例子 -

将内存中存储的两个数字相加。由于访问内存可能会消耗多个 CPU 周期,因此优秀的编译器会发出指令从内存中获取数据,并仅在数据可用时才执行加法。它不会等待,同时执行其他指令。另一方面,在解释过程中不可能进行此类优化,因为解释器在任何给定时间都不知道整个代码。

但是,解释型语言可以在任何具有该语言的有效解释器的机器上运行。

Java是编译型还是解释型?

Java 试图找到一个中间立场。由于 JVM 位于 javac 编译器和底层硬件之间,因此 javac(或任何其他编译器)编译器以字节码形式编译 Java 代码,特定于平台的 JVM 可以理解字节码。然后,当代码执行时,JVM 使用 JIT(即时)编译将字节码编译为二进制。

热点

在典型的程序中,只有一小部分代码被频繁执行,而通常正是这些代码显着影响了整个应用程序的性能。这些代码段称为热点

如果某段代码只执行一次,那么编译它就会浪费精力,而解释字节码会更快。但如果该节是热门节并且被执行多次,JVM 就会转而编译它。例如,如果多次调用某个方法,则编译代码所需的额外周期将被生成的更快的二进制文件所抵消。

此外,JVM 运行特定方法或循环的次数越多,它收集的用于进行各种优化的信息就越多,从而生成更快的二进制文件。

让我们考虑以下代码 -

for(int i = 0 ; I <= 100; i++) {
   System.out.println(obj1.equals(obj2)); //two objects
}

如果解释此代码,解释器将为每次迭代推断 obj1 的类。这是因为 Java 中的每个类都有一个 .equals() 方法,该方法是从 Object 类扩展的并且可以被重写。因此,即使每次迭代 obj1 都是字符串,仍然会进行推导。

另一方面,实际发生的情况是,JVM 会注意到,对于每次迭代,obj1 属于 String 类,因此,它将直接生成与 String 类的 .equals() 方法相对应的代码。因此,不需要查找,并且编译后的代码执行速度会更快。

只有当 JVM 知道代码的Behave方式时,这种Behave才有可能发生。因此,它会在编译代码的某些部分之前等待。

下面是另一个例子 -

int sum = 7;
for(int i = 0 ; i <= 100; i++) {
   sum += i;
}

对于每个循环,解释器从内存中获取“sum”的值,将“I”添加到其中,然后将其存储回内存中。内存访问是一项昂贵的操作,通常需要多个 CPU 周期。由于此代码运行多次,因此它是一个热点。JIT将编译这段代码并进行以下优化。

“sum”的本地副本将存储在特定于特定线程的寄存器中。所有操作都将对寄存器中的值进行,当循环完成时,该值将被写回内存。

如果其他线程也访问该变量怎么办?由于其他线程正在对变量的本地副本进行更新,因此它们会看到过时的值。在这种情况下就需要线程同步。一个非常基本的同步原语是将“sum”声明为易失性的。现在,在访问变量之前,线程将刷新其本地寄存器并从内存中获取值。访问它后,该值立即写入内存。

以下是 JIT 编译器完成的一些常规优化 -

  • 方法内联
  • 消除死代码
  • 优化调用站点的启发式方法
  • 不断折叠

Java 虚拟机 - 编译级别

JVM 支持五个编译级别 -

  • 口译员
  • 全面优化的 C1(无分析)
  • 具有调用和后沿计数器的 C1(轻型分析)
  • C1 具有完整的分析
  • C2(使用前面步骤中的分析数据)

如果您想禁用所有 JIT 编译器并仅使用解释器,请使用 -Xint。

客户端与服务器 JIT

使用 -client 和 -server 激活各自的模式。

客户端编译器 (C1) 比服务器编译器 (C2) 更早开始编译代码。因此,当 C2 开始编译时,C1 已经编译了代码段。

但在等待期间,C2 会分析代码,比 C1 更了解代码。因此,如果优化抵消了它等待的时间,则可以用来生成更快的二进制文件。从用户的角度来看,需要在程序的启动时间和程序运行所需的时间之间进行权衡。如果启动时间很重要,则应使用 C1。如果应用程序预计运行很长时间(通常是部署在服务器上的应用程序),最好使用 C2,因为它生成更快的代码,从而大大抵消任何额外的启动时间。

对于 IDE(NetBeans、Eclipse)和其他 GUI 程序等程序,启动时间至关重要。NetBeans 可能需要一分钟或更长时间才能启动。当 NetBeans 等程序启动时,会编译数百个类。在这种情况下,C1编译器是最好的选择。

请注意,C1 - 32b 和 64b有两个版本。C2 仅出现在64b中。

分层编译

在 Java 的旧版本中,用户可以选择以下选项之一 -

  • 口译员 (-Xint)
  • C1(-客户端)
  • C2(-服务器)

它来自 Java 7。它使用 C1 编译器启动,随着代码变得越来越热,切换到 C2。可以使用以下 JVM 选项激活它:-XX:+TieredCompilation。默认值在 Java 7 中设置为 false ,在 Java 8 中设置为 true

在五层编译中,分层编译使用1 -> 4 -> 5

Java 虚拟机 - 32b 与 64b

在32b机器上,只能安装32b版本的JVM。在 64b 机器上,用户可以在 32b 和 64b 版本之间进行选择。但其中存在一些细微差别,可能会影响 Java 应用程序的性能。

如果Java应用程序使用的内存少于4G,即使在64b机器上我们也应该使用32b JVM。这是因为在这种情况下,内存引用仅为 32b,并且操作它们比操作 64b 地址更便宜。在这种情况下,即使我们使用 OOPS(普通对象指针),64b JVM 的性能也会更差。使用OOPS,JVM可以在64b JVM中使用32b地址。然而,操作它们会比真正的 32b 引用慢,因为底层本机引用仍然是 64b。

如果我们的应用程序要消耗超过 4G 内存,我们将不得不使用 64b 版本,因为 32b 引用只能寻址不超过 4G 的内存。我们可以将这两个版本安装在同一台计算机上,并可以使用 PATH 变量在它们之间切换。

Java 虚拟机 - JIT 优化

在本章中,我们将学习 JIT 优化。

方法内联

在这种优化技术中,编译器决定用函数体替换函数调用。下面是一个相同的例子 -

int sum3;

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

public static void main(String…args) {
   sum3 = add(5,7) + add(4,2);
}

//after method inlining
public static void main(String…args) {
   sum3 = 5+ 7 + 4 + 2;
}

使用这种技术,编译器可以节省机器进行任何函数调用的开销(它需要将参数压入堆栈并弹出)。因此,生成的代码运行得更快。

方法内联只能对非虚函数(未被重写的函数)进行。考虑一下如果“add”方法在子类中被重写并且包含该方法的对象的类型直到运行时才知道,会发生什么情况。在这种情况下,编译器将不知道要内联什么方法。但是,如果该方法被标记为“final”,那么编译器很容易知道它可以是内联的,因为它不能被任何子类覆盖。请注意,根本不能保证最终方法始终是内联的。

无法访问和死代码消除

无法访问的代码是任何可能的执行流都无法访问的代码。我们将考虑以下示例 -

void foo() {
   if (a) return;
   else return;
   foobar(a,b); //unreachable code, compile time error
}

死代码也是无法访问的代码,但在这种情况下编译器确实会吐出错误。相反,我们只是收到警告。每个代码块(例如构造函数、函数、try、catch、if、while 等)都有自己的 JLS(Java 语言规范)中定义的不可访问代码规则。

不断折叠

要理解常量折叠概念,请参阅下面的示例。

final int num = 5;
int b = num * 6; //compile-time constant, num never changes
//compiler would assign b a value of 30.

Java 虚拟机 - 垃圾收集

Java 对象的生命周期由 JVM 管理。一旦程序员创建了一个对象,我们就不需要担心它的其余生命周期。JVM会自动找到那些不再使用的对象并从堆中回收它们的内存。

垃圾收集是 JVM 执行的一项主要操作,根据我们的需求对其进行调整可以为我们的应用程序带来巨大的性能提升。现代 JVM 提供了多种垃圾收集算法。我们需要了解应用程序的需求来决定使用哪种算法。

您无法在 Java 中以编程方式释放对象,就像在 C 和 C++ 等非 GC 语言中那样。因此,Java 中不能有悬空引用。但是,您可能有空引用(引用 JVM 永远不会存储对象的内存区域)。每当使用空引用时,JVM 都会抛出 NullPointerException。

请注意,虽然由于 GC 而很少在 Java 程序中发现内存泄漏,但它们确实会发生。我们将在本章末尾创建内存泄漏。

现代 JVM 使用以下 GC

  • 串行收集器
  • 吞吐量收集器
  • CMS收集器
  • G1收集器

上述每种算法都执行相同的任务 - 查找不再使用的对象并回收它们在堆中占用的内存。一种简单的方法是计算每个对象拥有的引用数量,并在引用数量变为 0 时立即释放它(这也称为引用计数)。为什么这很天真?考虑一个循环链表。它的每个节点都会有一个对其的引用,但整个对象不会从任何地方被引用,并且应该被释放,理想情况下。

JVM 不仅释放内存,还将小内存块合并成更大的内存块。这样做是为了防止内存碎片。

简单来说,典型的 GC 算法执行以下活动 -

  • 寻找未使用的物品
  • 释放它们在堆中占用的内存
  • 合并碎片

GC 必须在运行时停止应用程序线程。这是因为它在运行时移动对象,因此无法使用这些对象。此类停止称为“stop-the-world 暂停”,我们在调整 GC 时的目标是最大限度地减少这些暂停的频率和持续时间。

内存合并

内存合并的简单演示如下所示

内存合并

阴影部分是需要释放的对象。即使回收了所有空间后,我们也只能分配最大大小 = 75Kb 的对象。即使我们有 200Kb 的可用空间,如下所示

阴影部分

Java 虚拟机 - 分代 GC

大多数 JVM 将堆分为三代——年轻代 (YG)、老年代 (OG) 和永久代(也称为终身代)。这种想法背后的原因是什么?

实证研究表明,大多数创建的对象的寿命都很短 -

实证研究

来源

https://www.oracle.com

正如您所看到的,随着时间的推移分配越来越多的对象,幸存的字节数变得越来越少(通常)。Java对象的死亡率很高。

我们将研究一个简单的例子。Java 中的 String 类是不可变的。这意味着每次需要更改 String 对象的内容时,都必须创建一个新对象。假设您在循环中对字符串进行了 1000 次更改,如下面的代码所示 -

String str = “G11 GC”;

for(int i = 0 ; i < 1000; i++) {
   str = str + String.valueOf(i);
}

在每次循环中,我们创建一个新的字符串对象,并且在上一次迭代期间创建的字符串变得无用(即,它没有被任何引用引用)。该对象的生命周期只是一次迭代 - 它们将立即被 GC 收集。这些短命对象被保存在堆的年轻代区域中。从年轻代收集对象的过程称为次要垃圾收集,它总是会导致“停止世界”暂停。

当年轻代被填满时,GC 会进行一次小型垃圾回收。死亡对象被丢弃,存活对象被移动到老年代。应用程序线程在此过程中停止。

在这里,我们可以看到这种一代设计所提供的优势。年轻一代只是堆的一小部分,很快就会被填满。但处理它所花费的时间比处理整个堆所花费的时间少得多。因此,在这种情况下,“停止世界”的停顿要短得多,尽管更频繁。我们应该始终以较短的停顿为目标,而不是较长的停顿,即使它们可能更频繁。我们将在本教程的后面部分详细讨论这一点。

年轻代分为两个空间——伊甸园和幸存者空间。在 eden 收集期间幸存的对象被移动到幸存者空间,而那些在幸存者空间中幸存下来的对象被移动到老年代。年轻一代在收集时会被压缩。

当对象被移动到老一代时,它最终会被填满,并且必须被收集和压缩。不同的算法对此采取不同的方法。其中一些会停止应用程序线程(这会导致长时间的“停止世界”暂停,因为老一代与年轻代相比相当大),而其中一些会在应用程序线程继续运行时同时执行此操作。这个过程称为full GC。CMS 和 G1是两个这样的收集器。

现在让我们详细分析这些算法。

串行GC

它是客户端类机器(单处理器机器或 32b JVM、Windows)上的默认 GC。通常,GC 是高度多线程的,但串行 GC 不是。它有一个线程来处理堆,并且每当它执行 Minor GC 或 Major GC 时,它就会停止应用程序线程。我们可以通过指定标志来命令 JVM 使用此 GC:-XX:+UseSerialGC。如果我们希望它使用某种不同的算法,请指定算法名称。请注意,老年代在主要 GC 期间被完全压缩。

气相色谱吞吐量

此 GC 在 64b JVM 和多 CPU 计算机上是默认的。与串行GC不同,它使用多个线程来处理年轻代和年老代。正因为如此,GC也被称为并行收集器。我们可以通过使用以下标志来命令 JVM 使用此收集器:-XX:+UseParallelOldGC-XX:+UseParallelGC(对于 JDK 8 及以上版本)。应用程序线程在执行主要或次要垃圾收集时停止。与串行收集器一样,它在主要 GC 期间完全压缩年轻代。

吞吐量GC收集YG和OG。当伊甸园填满时,收集器将其中的活动对象弹出到 OG 或幸存者空间之一(下图中的 SS0 和 SS1)。死去的物体被丢弃以释放它们所占据的空间。

YG GC之前

YG GC之前

YG GC后

YG GC后

在完整GC期间,吞吐量收集器清空整个YG、SS0和SS1。操作后,OG 仅包含活动对象。我们应该注意到,上述两个收集器在处理堆时都会停止应用程序线程。这意味着在主要 GC 期间会出现长时间的“stop-the-world”暂停。接下来的两种算法旨在消除它们,但代价是更多的硬件资源 -

内容管理系统收集器

它代表“并发标记-清除”。它的作用是使用一些后台线程定期扫描老年代并清除死对象。但在 Minor GC 期间,应用程序线程会停止。然而,停顿非常小。这使得 CMS 成为一个低暂停收集器。

该收集器需要额外的 CPU 时间来在运行应用程序线程时扫描堆。此外,后台线程只收集堆,不执行任何压缩。它们可能会导致堆变得碎片化。随着这种情况持续下去,在某个时间点之后,CMS 将停止所有应用程序线程并使用单个线程压缩堆。使用以下 JVM 参数告诉 JVM 使用 CMS 收集器 -

“XX:+UseConcMarkSweepGC -XX:+UseParNewGC”作为 JVM 参数告诉它使用 CMS 收集器。

气相色谱之前

气相色谱之前

气相色谱后

气相色谱后

请注意,收集是同时进行的。

G1GC

该算法的工作原理是将堆划分为多个区域。与 CMS 收集器类似,它在执行较小 GC 时会停止应用程序线程,并使用后台线程来处理老年代,同时保持应用程序线程继续运行。由于它将老一代划分为多个区域,因此在将对象从一个区域移动到另一个区域时,它会不断压缩它们。因此,碎片是最小的。您可以使用标志:XX:+UseG1GC告诉您的 JVM 使用此算法。与 CMS 一样,它也需要更多的 CPU 时间来处理堆并同时运行应用程序线程。

该算法设计用于处理较大的堆(> 4G),这些堆被划分为多个不同的区域。其中一些区域由年轻一代组成,其余区域由老一代组成。YG 的清除方式是传统的——所有应用程序线程都停止,并且所有在老一代或幸存者空间中仍然存活的对象。

请注意,所有 GC 算法都将堆分为 YG 和 OG,并使用 STWP 清除 YG。这个过程通常非常快。

Java 虚拟机 - 调整 GC

在上一章中,我们了解了各种 Generational Gc。在本章中,我们将讨论如何调整 GC。

堆大小

堆大小是影响 Java 应用程序性能的重要因素。如果它太小,那么它会经常被填满,因此,GC 必须经常收集它。另一方面,如果我们只是增加堆的大小,尽管需要不太频繁地收集它,但暂停的时间会增加。

此外,增加堆大小会对底层操作系统造成严重影响。使用分页,操作系统使我们的应用程序看到比实际可用的内存多得多的内存。操作系统通过使用磁盘上的一些交换空间,将程序的非活动部分复制到其中来管理此操作。当需要这些部分时,操作系统将它们从磁盘复制回内存。

假设一台机器有 8G 内存,JVM 看到 16G 虚拟内存,JVM 不会知道系统上实际上只有 8G 可用。它只会向操作系统请求 16G,一旦获得该内存,它将继续使用它。操作系统将不得不交换大量数据进出,这对系统来说是一个巨大的性能损失。

然后是在此类虚拟内存的完整 GC 期间发生的暂停。由于GC会作用于整个堆进行收集和压缩,因此它必须等待很长时间才能将虚拟内存从磁盘换出。在并发收集器的情况下,后台线程将不得不等待很长时间才能将数据从交换空间复制到内存。

那么这里就出现了我们应该如何决定最佳堆大小的问题。第一条规则是永远不要向操作系统请求比实际存在的内存更多的内存。这样就完全避免了频繁交换的问题。如果计算机安装并运行多个 JVM,那么所有这些 JVM 的总内存请求量将小于系统中实际存在的 RAM。

您可以使用两个标志来控制 JVM 的内存请求大小 -

  • -XmsN - 控制请求的初始内存。

  • -XmxN - 控制可以请求的最大内存。

这两个标志的默认值取决于底层操作系统。例如,对于在 MacOS 上运行的 64b JVM,-XmsN = 64M 且 -XmxN = 至少 1G 或总物理内存的 1/4。

请注意,JVM 可以自动在两个值之间进行调整。例如,如果它注意到发生了太多 GC,只要内存大小低于 -XmxN 并且满足所需的性能目标,它就会不断增加内存大小。

如果您确切知道应用程序需要多少内存,则可以设置 -XmsN = -XmxN。在这种情况下,JVM 不需要找出堆的“最佳”值,因此 GC 过程变得更加高效。

世代规模

您可以决定要将多少堆分配给 YG,以及要将多少堆分配给 OG。这两个值都会通过以下方式影响我们应用程序的性能。

如果 YG 的大小很大,那么收集的频率就会降低。这将导致升级到 OG 的对象数量减少。另一方面,如果将 OG 的大小增加太多,那么收集和压缩它会花费太多时间,这会导致 STW 长时间暂停。因此,用户必须在这两个值之间找到平衡。

以下是可用于设置这些值的标志 -

  • -XX:NewRatio=N: YG 与 OG 的比率(默认值 = 2)

  • -XX:NewSize=N: YG的初始大小

  • -XX:MaxNewSize=N: YG的最大尺寸

  • -XmnN:使用此标志将 NewSize 和 MaxNewSize 设置为相同的值

YG 的初始大小由 NewRatio 的值通过给定公式确定 -

(总堆大小)/(newRatio + 1)

由于newRatio的初始值为2,因此上式给出YG的初始值为总堆大小的1/3。您始终可以通过使用 NewSize 标志显式指定 YG 的大小来覆盖此值。该标志没有任何默认值,如果没有显式设置,YG 的大小将继续使用上述公式计算。

Permagen 和元空间

永久元和元空间是 JVM 保存类元数据的堆区域。该空间在 Java 7 中称为“permagen”,在 Java 8 中称为“元空间”。该信息由编译器和运行时使用。

您可以使用以下标志控制永久文件的大小:-XX: PermSize=N-XX:MaxPermSize=N。元空间的大小可以使用-XX:Metaspace- Size=N-XX:MaxMetaspaceSize=N来控制。

当未设置标志值时,永久元和元空间的管理方式存在一些差异。默认情况下,两者都有默认的初始大小。但是,虽然元空间可以根据需要占用尽可能多的堆,但永久元只能占用默认的初始值。例如,64b JVM 的最大永久大小为 82M 堆空间。

请注意,由于元空间可以占用无限量的内存(除非指定不这样做),因此可能会出现内存不足错误。每当调整这些区域的大小时,就会发生完整的 GC。因此,在启动过程中,如果有很多类被加载,元空间可能会不断调整大小,从而导致每次都发生完整的 GC。因此,如果初始元空间大小太小,大型应用程序需要花费大量时间来启动。增加初始大小是一个好主意,因为它可以减少启动时间。

虽然永久元和元空间保存类元数据,但它不是永久的,并且空间由 GC 回收,就像对象的情况一样。这通常是在服务器应用程序的情况下。每当您对服务器进行新部署时,都必须清理旧的元数据,因为新的类加载器现在需要空间。该空间由 GC 释放。

Java 虚拟机 - Java 中的内存泄漏

本章我们将讨论Java中的内存泄漏概念。

以下代码在 Java 中造成内存泄漏 -

void queryDB() {
   try{
      Connection conn = ConnectionFactory.getConnection();
      PreparedStatement ps = conn.preparedStatement("query"); // executes a
      SQL
      ResultSet rs = ps.executeQuery();
      while(rs.hasNext()) {
         //process the record
      }
   } catch(SQLException sqlEx) {
      //print stack trace
   }
}

在上面的代码中,当方法退出时,我们还没有关闭连接对象。因此,在触发 GC 并将连接对象视为不可访问之前,物理连接保持打开状态。现在,它将调用连接对象上的最终方法,但是,它可能不会被实现。因此,该对象在此周期中不会被垃圾回收。

接下来会发生同样的事情,直到远程服务器发现连接已经打开很长时间并强制终止它。因此,没有引用的对象会长时间保留在内存中,从而造成泄漏。