您可能会认为,如果您使用Java编程,那么您需要了解内存的工作原理?Java具有自动内存管理功能,一个漂亮而安静的垃圾收集器,可在后台运行以清理未使用的对象并释放一些内存。
因此,作为Java程序员,您不需要为破坏对象等问题而烦恼,因为它们不再被使用。但是,即使这个过程在Java中是自动的,它也不能保证任何东西。通过不知道垃圾收集器和Java内存是如何设计的,即使您不再使用它们,也可能有不符合垃圾收集条件的对象。
因此,了解内存在Java中的实际运行方式非常重要,因为它为您提供了编写高性能和优化应用程序的优势,这些应用程序永远不会崩溃 OutOfMemoryError
。另一方面,当您发现自己陷入困境时,您将能够快速找到内存泄漏。
首先,让我们看看内存在Java中的组织方式:
记忆结构
通常,内存分为两大部分:堆栈和堆。请记住,此图片中的内存类型大小与实际内存大小不成比例。与堆栈相比,堆是一个巨大的内存。
堆栈
堆栈内存负责保存对堆对象的引用和存储值类型(在Java中也称为基本类型),它们保存值本身而不是对堆中对象的引用。
此外,堆栈上的变量具有一定的可见性,也称为范围。仅使用活动范围中的对象。例如,假设我们没有任何全局范围变量(字段),并且只有局部变量,如果编译器执行方法的主体,它只能访问方法体内的堆栈中的对象。它不能访问其他局部变量,因为它们超出了范围。方法完成并返回后,堆栈顶部弹出,活动范围发生变化。
也许您注意到在上图中,显示了多个堆栈内存。这是因为Java中的堆栈内存是按Thread分配的。因此,每次创建和启动Thread时,它都有自己的堆栈内存 - 并且无法访问另一个线程的堆栈内存。
堆
这部分内存将实际对象存储在内存中。这些是由堆栈中的变量引用的。例如,让我们分析以下代码行中发生的情况:
StringBuilder builder = new StringBuilder();
该 new
关键字是负责确保有上堆足够的自由空间,在存储器中创建StringBuilder的类型的对象,并通过“助洗剂”的参考,其进入堆叠上提到它。
每个正在运行的JVM进程只存在一个堆内存。因此,无论正在运行多少线程,这都是内存的共享部分。实际上,堆结构与上图中显示的有点不同。堆本身分为几个部分,便于垃圾收集过程。
最大堆栈和堆大小未预定义 - 这取决于正在运行的计算机。但是,在本文后面,我们将研究一些JVM配置,这些配置允许我们为正在运行的应用程序明确指定它们的大小。
参考类型
如果仔细查看内存结构图,您可能会注意到表示堆中对象引用的箭头实际上是不同类型的。这是因为,在Java编程语言中,我们有不同类型的引用:强引用,弱引用,软 引用和幻像引用。引用类型之间的区别在于它们引用的堆上的对象符合不同条件下的垃圾收集条件。让我们仔细看看它们中的每一个。
1.强烈的参考
这些是我们都习惯的最流行的参考类型。在上面的StringBuilder示例中,我们实际上对堆中的对象进行了强引用。堆上的对象不是垃圾收集,而是有强引用指向它,或者它是通过强引用链强烈可达的。
2.弱参考
简单来说,在下一个垃圾收集过程之后,对堆中对象的弱引用很可能无法生存。创建弱引用如下:
WeakReference<StringBuilder> reference = new WeakReference<>(new StringBuilder());
WeakReference<StringBuilder> reference = new WeakReference<>(new StringBuilder());
弱引用的一个很好的用例是缓存场景。想象一下,您检索了一些数据,并希望它也存储在内存中 - 可以再次请求相同的数据。另一方面,您不确定何时或是否会再次请求此数据。所以你可以保留一个弱引用,如果垃圾收集器运行,它可能会破坏堆上的对象。因此,过了一段时间,如果要检索所引用的对象,可能会突然找回一个 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;
一旦来自WeakHashMap的密钥被垃圾收集,整个条目就会从地图中删除。
3.软参考
这些类型的引用用于更多对内存敏感的方案,因为只有在应用程序内存不足时才会对这些引用进行垃圾回收。因此,只要不需要释放一些空间,垃圾收集器就不会触及可轻松访问的对象。Java保证在引发所有软引用对象之前清除它们OutOfMemoryError
。Javadocs声明,“在虚拟机抛出OutOfMemoryError之前,所有对软可访问对象的软引用都保证已被清除。”
与弱引用类似,软引用创建如下:
SoftReference<StringBuilder> reference = new SoftReference<>(new StringBuilder());
4.幽灵参考
用于安排事后清理操作,因为我们确信对象不再存在。仅用于引用队列,因为.get()
此类引用的方法将始终返回null
。这些类型的引用被认为优于终结器。
如何 引用字符串
Java中的类型有点不同。字符串是不可变的,这意味着每次使用字符串执行某些操作时,实际上会在堆上创建另一个对象。对于字符串,Java管理内存中的字符串池。这意味着Java会尽可能地存储和重用字符串。这对于字符串文字大多是正确的。例如: String
String localPrefix = "297"; //1
String prefix = "297"; //2
if (prefix == localPrefix)
{
System.out.println("Strings are equal" );
}
else
{
System.out.println("Strings are different");
}
运行时,会打印出以下内容:
字符串是平等的
因此,事实证明,在比较String类型的两个引用之后,它们实际上指向堆上的相同对象。但是,这对于计算的字符串无效。假设我们在上面代码的第// 1行中进行了以下更改
String localPrefix = new Integer(297).toString(); //1
输出:
字符串是不同的
在这种情况下,我们实际上看到堆上有两个不同的对象。如果我们考虑经常使用计算的String,我们可以通过添加来强制JVM将它添加到字符串池中.intern()
计算字符串末尾的方法:
String localPrefix = new Integer(297).toString().intern(); //1
添加上述更改会创建以下输出:
字符串是平等的
垃圾收集过程
如前所述,根据堆栈中的变量从堆中保存到对象的引用类型,在某个特定时间点,该对象符合垃圾收集器的条件。
符合垃圾资格的对象
例如,所有红色的对象都有资格被垃圾收集器收集。您可能会注意到堆上有一个对象,它对堆上的其他对象也有强引用(例如,可能是包含对其项的引用的列表,或者是具有两个引用类型字段的对象)。但是,由于堆栈中的引用丢失,因此无法再访问它,因此它也是垃圾。
为了更深入地了解细节,我们首先提一下:
-
这个过程是由Java自动触发的,它是由Java决定何时以及是否启动此过程。
-
这实际上是一个昂贵的过程。当垃圾收集器运行时,应用程序中的所有线程都会暂停(取决于GC类型,稍后将对此进行讨论)。
-
这实际上是一个比垃圾收集和释放内存更复杂的过程。
即使Java决定何时运行垃圾收集器,您也可以显式调用System.gc()
并期望垃圾收集器在执行这行代码时运行,对吧?
这是一个错误的假设。
你只是要求Java运行垃圾收集器,但它是否再次取决于它是否这样做。无论如何,System.gc()
不建议明确打电话 。
由于这是一个非常复杂的过程,并且它可能会影响您的性能,因此它以智能方式实现。为此使用所谓的“标记和扫描”过程。Java分析堆栈中的变量并“标记”所有需要保持活动的对象。然后,清除所有未使用的对象。
实际上,Java并没有收集任何垃圾。事实上,垃圾越多,活动标记的对象越少,过程就越快。为了使这更加优化,堆内存实际上由多个部分组成。我们可以使用JVisualVM(Java JDK附带的工具)可视化内存使用情况和其他有用的东西。您唯一需要做的就是安装一个名为Visual GC的插件,它允许您查看内存的实际结构。让我们放大一点,分解大局:
堆内存世代
创建对象时,它将在Eden(1)空间中分配。因为伊甸园的空间并不那么大,所以它的速度非常快。垃圾收集器在Eden空间上运行,并将对象标记为活动。
一旦对象在垃圾收集过程中存活,它就会被移动到所谓的幸存者空间S0(2)中。第二次垃圾收集器在Eden空间上运行时,它会将所有幸存的对象移动到S1(3)空间中。此外,当前在S0(2)上的所有内容都将 移动到S1(3)空间中。
如果一个对象在X轮垃圾收集中存活(X取决于JVM实现,在我的情况下它是8),它很可能永远存在,并且它被移动到Old(4)空间。
到目前为止所说的一切,如果你看一下垃圾收集器图(6),每次运行时,你都可以看到对象切换到幸存者空间并且Eden空间获得了空间。等等等等。老一代也可以被垃圾收集,但由于它与Eden空间相比是内存的更大部分,因此通常不会发生这种情况。在元空间(5)用于元数据存储有关的JVM您加载的类。
提供的图片实际上是Java 8应用程序。在Java 8之前,内存的结构有点不同。元空间实际上称为PermGen。空间。例如,在Java 6中,此空间还存储了字符串池的内存。因此,如果Java 6应用程序中有太多字符串,则可能会崩溃。
垃圾收集器类型
实际上,JVM有三种类型的垃圾收集器,程序员可以选择应该使用哪种垃圾收集器。默认情况下,Java根据底层硬件选择要使用的垃圾收集器类型。
1.串行GC - 单线程收集器。主要适用于数据使用量较小的小型应用程序。可以通过指定命令行选项启用:-XX:+UseSerialGC
2.并行GC - 即使从命名,串行和并行之间的区别在于并行GC使用多个线程来执行垃圾收集过程。此GC类型也称为吞吐量收集器。可以通过显式指定选项来启用它:-XX:+UseParallelGC
3.大多数并发GC - 如果你还记得,在本文前面提到垃圾收集过程实际上相当昂贵,并且当它运行时,所有线程都被暂停。但是,我们有这种大多数并发GC类型,它声明它与应用程序并发工作。但是,它有“大多数”并发的原因。它不能100%同时应用于应用程序。线程暂停一段时间。尽管如此,暂停时间尽可能短,以实现最佳的GC性能。实际上,有两种类型的大多数并发GC:
3.1垃圾优先 - 具有合理应用暂停时间的高吞吐量。启用选项:-XX:+UseG1GC
3.2并发标记扫描 - 应用程序暂停时间保持最短。可以通过指定选项来使用它:-XX:+UseConcMarkSweepGC
。从JDK 9开始,不推荐使用此GC类型。
技巧和窍门
-
要最小化内存占用,请尽可能限制变量的范围。请记住,每次弹出堆栈中的顶级作用域时,该作用域的引用都将丢失,这可能使对象有资格进行垃圾回收。
-
明确引用
null
过时的引用。这将使那些参考的对象有资格进行垃圾收集。 -
避免终结者。他们放慢了流程,他们不保证任何事情。更喜欢幻像引用以进行清理工作。
-
在弱引用或软引用适用的情况下,请勿使用强引用。最常见的内存缺陷是缓存方案,即使可能不需要将数据保存在内存中也是如此。
-
JVisualVM还具有在某一点进行堆转储的功能,因此您可以按类分析它占用的内存量。
-
根据您的应用程序要求配置JVM。运行应用程序时,明确指定JVM的堆大小。内存分配过程也很昂贵,因此为堆分配合理的初始和最大内存量。如果您知道从一开始就使用较小的初始堆大小是没有意义的,JVM将扩展此内存空间。使用以下选项指定内存选项:
-
初始堆大小
-Xms512m
- 将初始堆大小设置为512 MB。 -
最大堆大小
-Xmx1024m
- 将最大堆大小设置为1024兆字节。 -
线程堆栈大小
-Xss128m
- 将线程堆栈大小设置为128兆字节。 -
年轻一代的大小
-Xmn256m
- 将年轻代的大小设置为256兆字节。
-
-
如果Java应用程序崩溃
OutOfMemoryError
并且您需要一些额外的信息来检测泄漏,请运行该过程–XX:HeapDumpOnOutOfMemory
参数,下次发生此错误时将创建堆转储文件。 -
使用该
-verbose:gc
选项获取垃圾收集输出。每次进行垃圾收集时,都会生成一个输出。
结论
了解内存的组织方式可以为您提供在内存资源方面编写优质代码的优势。有利的是,您可以通过提供最适合您正在运行的应用程序的不同配置来调整正在运行的JVM。如果使用正确的工具,查找和修复内存泄漏只是一件容易的事。
另外本人从事在线教育多年,将自己的资料整合建了一个公众号,对于有兴趣一起交流学习java的微信搜索:“程序员文明”,里面有大神会给予解答,也会有许多的资源可以供大家学习分享,欢迎大家前来一起学习进步!