0
点赞
收藏
分享

微信扫一扫

字节一面:说说 Java 内存管理


点击“终码一生”,关注,置顶公众号

每日技术干货,第一时间送达!

您可能会想,如果您使用 Java 进行编程,您需要了解内存的工作原理吗?Java 有自动内存管理,一个漂亮而安静的垃圾收集器,它在后台工作以清理未使用的对象并释放一些内存。

因此,作为一名 Java 程序员,您无需为诸如销毁对象之类的问题而烦恼,因为它们不再被使用。然而,即使这个过程在 Java 中是自动的,它也不能保证任何事情。由于不知道垃圾收集器和 Java 内存是如何设计的,您可能会拥有不符合垃圾收集条件的对象,即使您不再使用它们。

因此,了解内存在 Java 中的实际工作方式很重要,因为它使您能够编写高性能和优化的应用程序,这些应用程序永远不会因OutOfMemoryError. 另一方面,当你发现自己处于糟糕的境地时,你将能够快速找到内存泄漏。

首先,让我们看一下内存在 Java 中通常是如何组织的:


字节一面:说说 Java 内存管理_垃圾收集器

字节一面:说说 Java 内存管理_java_02

内存结构

通常,内存分为两大部分:栈区堆区(这里不讨论方法区)。 请记住,这张图片中内存类型的大小与现实中的内存大小不成比例。与栈相比,堆是一个巨大的内存量。

1、栈

栈内存负责保存对堆对象的引用和存储值类型(在 Java 中也称为原始类型),它保存值本身而不是对堆中对象的引用。

此外,栈上的变量具有一定的可见性,也称为作用域。仅使用来自活动范围的对象。例如,假设我们没有任何全局范围变量(字段),只有局部变量,如果编译器执行一个方法体,它只能从堆栈中访问方法体中的对象。它无法访问其他局部变量,因为它们超出了范围。一旦方法完成并返回,堆栈的顶部就会弹出,并且活动范围会发生变化。

也许你注意到上图中显示了多个堆栈内存。这是因为 Java 中的堆栈内存是按线程分配的。因此,每次创建和启动线程时,它都有自己的堆栈内存——并且不能访问另一个线程的堆栈内存。

2、堆

这部分内存将实际对象存储在内存中。这些由堆栈中的变量引用。例如,让我们分析以下代码行中发生的情况:


StringBuilder builder = new StringBuilder();

字节一面:说说 Java 内存管理_java_03

该 new 关键字负责确保堆上有足够的空闲空间,在内存中创建一个 StringBuilder 类型的对象,并通过堆栈上的“builder”引用来引用它。

每个运行的 JVM 进程只存在一个堆内存。因此,无论有多少线程正在运行,这都是内存的共享部分。实际上,堆结构与上图中显示的有点不同。堆本身被分成几个部分,这有利于垃圾收集的过程。

最大堆栈和堆大小未预定义——这取决于正在运行的机器。但是,在本文后面,我们将研究一些 JVM 配置,这些配置将允许我们为正在运行的应用程序显式指定它们的大小。

3、参考类型

如果您仔细查看内存结构图片,您可能会注意到表示对堆中对象的引用的箭头实际上是不同类型的。这是因为,在 Java 编程语言中,我们有不同类型的引用:强引用、弱引用、软引用和虚引用。引用类型之间的区别在于它们所引用的堆上的对象在不同的标准下有资格进行垃圾收集。让我们仔细看看它们中的每一个。

1. 强引用

这些是我们都习惯的最流行的引用类型。在上面的 StringBuilder 示例中,我们实际上持有对堆中对象的强引用。堆上的对象在有指向它的强引用时不会被垃圾回收,或者如果它可以通过强引用链强访问。

2.弱引用

简单来说,堆中对象的弱引用很可能在下一次垃圾回收过程之后无法生存。弱引用创建如下:


WeakReference<StringBuilder> reference = new WeakReference<>(new StringBuilder());

字节一面:说说 Java 内存管理_java_04

弱引用的一个很好的用例是缓存场景。想象一下,您检索了一些数据,并且希望将其也存储在内存中——可以再次请求相同的数据。另一方面,您不确定何时或是否会再次请求此数据。所以你可以保持对它的弱引用,如果垃圾收集器运行,它可能会破坏你在堆上的对象。因此,过了一会儿,如果你想检索你引用的对象,你可能会突然取回一个 null值。缓存场景的一个很好的实现是集合WeakHashMap<K,V>。如果我们WeakHashMap在 Java API 中打开该类,我们会看到它的条目实际上扩展了 WeakReference该类并使用其ref 字段作为映射的键:


/**
* The entries in this hash table extend WeakReference, using its main ref
* field as the key.
*/

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
}

字节一面:说说 Java 内存管理_java_05

一旦 WeakHashMap 中的一个键被垃圾回收,整个条目就会从映射中删除。

3. 软引用

这些类型的引用用于对内存更敏感的场景,因为只有在应用程序内存不足时才会对这些引用进行垃圾收集。因此,只要没有紧急需要释放一些空间,垃圾收集器就不会触及软可达对象。Java 保证在抛出OutOfMemoryError. Javadocs 声明,“所有对软可访问对象的软引用都保证在虚拟机抛出 OutOfMemoryError 之前已被清除。”

与弱引用类似,软引用的创建方式如下:


SoftReference < StringBuilder > reference = new  SoftReference <> ( new  StringBuilder ());

字节一面:说说 Java 内存管理_java_06

4. 虚引用

用于安排事后清理操作,因为我们确定对象不再存在。仅与引用队列一起使用,因为.get()此类引用的方法将始终返回null。这些类型的引用被认为比终结器更可取。

4、如何引用字符串

Java 中的类型的处理方式略有不同。字符串是不可变的,这意味着每次对字符串执行操作时,实际上都会在堆上创建另一个对象。对于字符串,Java 在内存中管理一个字符串池。这意味着 Java 尽可能存储和重用字符串。对于字符串文字,这主要是正确的。例如: 


String localPrefix = "297"; //1
String prefix = "297"; //2

if (prefix == localPrefix)
{
System.out.println("Strings are equal" );
}
else
{
System.out.println("Strings are different");
}

字节一面:说说 Java 内存管理_java_07

运行时,会打印出以下内容:

 字符串相等

因此,事实证明,在比较 String 类型的两个引用之后,它们实际上指向了堆上的相同对象。但是,这对于计算的字符串无效。假设我们在上面代码的//1行有以下更改


String localPrefix = new Integer(297).toString(); //1

字节一面:说说 Java 内存管理_垃圾收集器_08

输出:

 字符串不一样

在这种情况下,我们实际上看到堆上有两个不同的对象。如果我们认为计算的字符串会经常使用,我们可以通过.intern()在计算字符串的末尾添加方法来强制 JVM 将其添加到字符串池中:


String localPrefix = new Integer(297).toString().intern(); //1

字节一面:说说 Java 内存管理_java_09

添加上述更改将创建以下输出:

 字符串相等

5、垃圾收集过程

如前所述,根据堆栈中的变量对堆中对象的引用类型,在某个时间点,该对象符合垃圾收集器的条件。


字节一面:说说 Java 内存管理_java_10

字节一面:说说 Java 内存管理_字符串_11

符合垃圾标准的对象 

例如,所有红色的对象都有资格被垃圾收集器收集。您可能会注意到堆上有一个对象,它对也在堆上的其他对象具有强引用(例如,可能是一个引用其项目的列表,或者一个具有两个引用类型字段的对象)。但是,由于来自堆栈的引用丢失了,它不能再被访问,所以它也是垃圾。

  • 为了更深入地了解细节,让我们首先提到一些事情:

  • 这个过程由Java自动触发,由Java决定何时以及是否启动这个过程。

  • 这实际上是一个昂贵的过程。当垃圾收集器运行时,应用程序中的所有线程都会暂停(取决于 GC 类型,稍后将讨论)。

这实际上是一个比垃圾收集和释放内存更复杂的过程。

即使 Java 决定何时运行垃圾收集器,您也可以显式调用System.gc()并期望垃圾收集器在执行这行代码时运行,对吗?

这是一个错误的假设。

您只是要求 Java 运行垃圾收集器,但是否这样做也取决于它。System.gc()无论如何,不建议显式调用 。

由于这是一个非常复杂的过程,并且可能会影响您的性能,因此它以一种智能的方式实现。为此使用了所谓的“标记和扫描”过程。Java 分析堆栈中的变量并“标记”所有需要保持活动状态的对象。然后,清理所有未使用的对象。

所以实际上,Java 不会收集任何垃圾。事实上,垃圾越多,被标记为活动的对象越少,这个过程就越快。为了使这一点更加优化,堆内存实际上由多个部分组成。我们可以使用Java JDK 附带的工具JVisualVM来可视化内存使用情况和其他有用的东西。您唯一需要做的就是安装一个名为Visual GC的插件,它可以让您查看内存的实际结构。让我们放大一点并分解大图:


字节一面:说说 Java 内存管理_字符串_12

字节一面:说说 Java 内存管理_字符串_13

堆内存代 

当一个对象被创建时,它被分配到Eden(1)空间。因为伊甸园空间不大,所以很快就满了。垃圾收集器在 Eden 空间上运行并将对象标记为活动的。

一旦一个对象在垃圾收集过程中幸存下来,它就会被移动到所谓的幸存者空间S0(2)中。垃圾收集器第二次在 Eden 空间运行时,会将所有幸存的对象移动到S1(3)空间。此外,当前位于S0(2)上的所有内容都被移动到S1(3)空间中。

如果一个对象在 X 轮垃圾回收中存活下来(X 取决于 JVM 实现,在我的例子中是 8 轮),它很可能会永远存活下来,并且它会被移入Old(4)空间。

综上所述,如果你看一下垃圾收集器图(6),每次它运行时,你可以看到对象切换到幸存者空间并且伊甸园空间获得了空间。等等等等。老年代也可以被垃圾回收,但由于与伊甸园空间相比,它占据了更大的内存部分,因此不会经常发生。Metaspace (5)用于在 JVM 中存储有关已加载类的元数据。

呈现的图片实际上是一个 Java 8 应用程序。在 Java 8 之前,内存的结构有点不同。元空间实际上称为 PermGen。空间。例如,在 Java 6 中,这个空间还存储了字符串池的内存。因此,如果您的 Java 6 应用程序中有太多字符串,它可能会崩溃。

6、垃圾收集器类型

实际上,JVM 有三种垃圾收集器,程序员可以选择使用哪一种。默认情况下,Java 根据底层硬件选择要使用的垃圾收集器类型。

 1. 串行 GC——单线程收集器。主要适用于数据使用量小的小型应用程序。可以通过指定命令行选项来启用:-XX:+UseSerialGC

 2. Parallel GC——从名字上看,Serial 和 Parallel 的区别就是Parallel GC 使用多个线程来执行垃圾收集过程。这种 GC 类型也称为吞吐量收集器。可以通过显式指定选项来启用它:-XX:+UseParallelGC

 3. 主要是并发 GC——如果你还记得,在本文前面,有人提到垃圾收集过程实际上非常昂贵,当它运行时,所有线程都会暂停。但是,我们有这种主要是并发的 GC 类型,它表明它与应用程序并发工作。但是,它“大部分”是并发的是有原因的。它不能 100% 与应用程序同时工作。线程暂停一段时间。尽管如此,为了获得最佳的 GC 性能,暂停保持尽可能短。实际上,有两种类型的并发 GC:

 3.1 垃圾优先——高吞吐量和合理的应用暂停时间。使用以下选项启用:-XX:+UseG1GC

 3.2 并发标记扫描——应用程序暂停时间保持在最低限度。可以通过指定选项来使用它:-XX:+UseConcMarkSweepGC. 从 JDK 9 开始,不推荐使用这种 GC 类型。

7、技巧和窍门

  • 为了最大限度地减少内存占用,请尽可能限制变量的范围。请记住,每次弹出堆栈的顶部范围时,来自该范围的引用都会丢失,这可能会使对象有资格进行垃圾收集。

  • 明确引用null 过时的引用。这将使那些引用的对象有资格进行垃圾收集。

  • 避免使用终结器。他们减慢了这个过程,他们不保证任何事情。首选幻像参考进行清理工作。

  • 不要在应用弱引用或软引用的地方使用强引用。最常见的内存陷阱是缓存场景,即数据保存在内存中,即使它可能不需要。

  • JVisualVM 还具有在某个点进行堆转储的功能,因此您可以分析每个类占用多少内存。

  • 根据您的应用程序要求配置您的 JVM。在运行应用程序时显式指定 JVM 的堆大小。内存分配过程也很昂贵,因此为堆分配合理的初始和最大内存量。如果您知道从一开始就使用较小的初始堆大小是没有意义的,那么 JVM 将扩展此内存空间。使用以下选项指定内存选项:

  1. 初始堆大小-Xms512m——将初始堆大小设置为 512 兆字节。

  2. 最大堆大小-Xmx1024m- 将最大堆大小设置为 1024 兆字节。

  3. 线程堆栈大小-Xss1m- 将线程堆栈大小设置为 1 兆字节。

  4. 年轻代大小-Xmn256m——将年轻代大小设置为 256 兆字节。

  • 如果 Java 应用程序崩溃, OutOfMemoryError并且您需要一些额外的信息来检测泄漏,请使用参数运行该进程 –XX:HeapDumpOnOutOfMemory,这将在下次发生此错误时创建一个堆转储文件。

  • 使用该 -verbose:gc选项获取垃圾收集输出。每次进行垃圾收集时,都会生成一个输出。

8、结论

了解内存是如何组织的可以让您在内存资源方面编写良好和优化的代码。有利的是,您可以通过提供最适合您正在运行的应用程序的不同配置来调整正在运行的 JVM。如果使用正确的工具,发现和修复内存泄漏是一件容易的事。

PS:防止找不到本篇文章,可以收藏点赞,方便翻阅查找哦


举报

相关推荐

0 条评论