性能文章>【译】一篇完整介绍Java内存管理的原理>

【译】一篇完整介绍Java内存管理的原理转载

2月前
234416

使用 Java 进行编程,需要了解内存的工作原理吗?

Java 有自动内存管理机制,一个漂亮而安静的垃圾收集器——GC,它在后台会默默清理未使用的对象并释放一些内存。那么是否意味着,作为一名 Java 程序员,就无需为诸如销毁对象之类的问题而烦恼呢?

当然不是,即使这个过程在 Java 中是自动的,它也不能保证任何事情。由于不知道垃圾收集器和 Java 内存是如何设计的,就会出现不符合垃圾收集条件的对象,从而出现各种内存问题。

所以,了解内存在 Java 中的内存管理的实际工作过程很重要,因为能够让你写出更高性能的程序,这些程序永远不会跳出OutOfMemoryError. 同时,即使出现问题,你也能够快速找到内存泄漏的原因,并解决它。

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

内存结构

内存结构

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

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

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

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

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

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

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

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

引用类型

如果仔细查看内存结构图片,你可能会注意到表示对堆中对象的引用箭头实际上是不同类型的。

这是因为,在 Java 编程语言中,我们有不同类型的引用:强引用、弱引用、软引用和虚引用。引用类型之间的区别在于它们所引用的堆上的对象在不同的​​标准下有资格进行垃圾收集。让我们仔细看看它们中的每一个。

1. 强引用

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

2.弱引用

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

弱引用的一个很好的用例是缓存场景。想象一下,您检索了一些数据,并且希望将其也存储在内存中——可以再次请求相同的数据。另一方面,你不确定何时或是否会再次请求此数据。所以你可以保持对它的弱引用,如果垃圾收集器运行,它可能会破坏你在堆上的对象。

因此,过一会儿,如果你想检索你引用的对象,你可能会突然取回一个 null值。缓存场景的一个很好的实现是集合WeakHashMap<K,V>。如果我们WeakHashMap在 Java API 中打开该类,我们会看到它的条目实际上扩展了 WeakReference该类并使用其ref 字段作为映射的键:

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

3. 软引用

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

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

4. 虚引用

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

如何引用字符串

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

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

  Strings are equal

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

输出:

 Strings are different

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

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

 Strings are equal

垃圾收集过程

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

垃圾合格对象

符合垃圾收集标准的对象 

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

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

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

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

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

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

当然,这是一个错误的假设。

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

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

所以实际上,Java 不会收集任何垃圾。而且垃圾越多,被标记为活动的对象越少,这个过程就越快。

为了使这一点更加优化,堆内存实际上由多个部分组成。我们可以使用Java JDK 附带的工具JVisualVM来可视化内存使用情况和其他有用的东西。您唯一需要做的就是安装一个名为Visual GC的插件,它可以让您查看内存的实际结构。让我们放大一点并分解大图:

图片标题

当一个对象被创建时,它被分配到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 应用程序中有太多字符串,它可能会崩溃。

垃圾收集器类型

实际上,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 类型。

更多垃圾收集文章可去:

第一篇:什么是垃圾回收?

第二篇:Java 中的垃圾收集原理解析

第三篇:GC算法基础篇

第四篇:GC 算法实现篇——串行GC

第五篇:GC 算法实现篇——并行GC

第六篇:GC 算法实现篇——并发标记-清除

第七篇:GC 算法实现篇——垃圾优先算法

第八篇:GC 调优基础篇

第九篇:GC 调优工具篇

第十篇:GC调优实战篇—高分配速率(High Allocation Rate)

第11篇:GC 调优的实战篇—过早提升(Premature Promotion)

第12篇:GC 调优的实战篇—Weak, Soft 及 Phantom 引用

技巧TIPS

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

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

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

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

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

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

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

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

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

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

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

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

结论

了解内存是如何工作,可以让你内存资源方面呈现更高质量的代码。

而且,你可以通过提供最适合业务场景的配置来调整正在运行的 JVM。如果使用正确的工具,发现和修复内存泄漏也是分分钟的事情。

点赞收藏
JAY_Y

非专业工程师,能不说就不说协会副会长,闲鱼似的活着!

请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

讲透JAVA Stream的collect用法与原理,远比你想象的更强大

讲透JAVA Stream的collect用法与原理,远比你想象的更强大

没有二十年功力,写不出Thread.sleep(0)这一行“看似无用”的代码!

没有二十年功力,写不出Thread.sleep(0)这一行“看似无用”的代码!

填坑来了!关于“Thread.sleep(0)这一行‘看似无用’的代码”里面留下的坑。

填坑来了!关于“Thread.sleep(0)这一行‘看似无用’的代码”里面留下的坑。

【译】记一次数据库连接泄漏导致的响应迟缓

【译】记一次数据库连接泄漏导致的响应迟缓

【全网首发】微服务10:系统服务熔断、限流

【全网首发】微服务10:系统服务熔断、限流

【全网首发】MQ-消息堆积-JDK Bug导致线程阻塞案例分析

【全网首发】MQ-消息堆积-JDK Bug导致线程阻塞案例分析

6
1