栈
栈内存用于存储堆中对象的引用和八种基本数据类型,每个线程启动都会创建自己的栈内存。
堆
堆内存用于存储创建的对象,每个jvm只有一个堆内存所以多个线程共享一个堆。
引用类型
- 强引用
这是我们惯常使用的最流行的引用类型。通过new
关键字创建的对象。当有强引用指向堆上的对象时,或者可以通过强引用链访问该对象时,不会对此对象进行垃圾收集。 - 简单来说,在下一个垃圾回收过程之后,对堆中对象的弱引用最有可能无法幸存。弱引用的创建方式如下:弱引用的一个很好的用例是缓存场景。想象你检索了一些数据,并且还想要将其存储在内存中——能够再次请求相同的数据。另一方面,你不确定何时或是否会再次请求此数据。因此,你可以对其保持弱引用,万一垃圾收集器运行,从而破坏堆上的对象。所以,过一会儿之后,如果你想检索所引用的对象,可能会突然返回一个null值。缓存场景的一个很好实现是集合WeakHashMap<K,V>。如果我们在Java API中打开WeakHashMap类,则会看到其条目实际上扩展了WeakReference类,并使用其ref字段作为映射的键:
1
WeakReference<StringBuilder> reference = new WeakReference<>(new StringBuilder());
一旦来自WeakHashMap的键被垃圾收集,则整个条目从映射中删除。1
2
3
4
5/**
* 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>{...} - 软引用
此类型的引用用于对内存更敏感的场景,因为只有当应用程序内存不足时,此引用才会被垃圾回收。因此,只要没有必要释放空间,垃圾收集器就不会接触软引用的对象。Java保证在抛出OutOfMemoryError之前清除所有软引用的对象。
与弱引用类似,软引用按如下方式创建:1
SoftReference<StringBuilder> reference = new SoftReference<>(new StringBuilder());
- 虚引用
用于计划事后清理操作,因为我们可以确定对象不再存在。仅与引用队列一起使用,因为此类引用的.get()方法将始终返回null。此类型的引用被认为比终结器(Finalizers)更可取。
字符串
字符串是不可变的,每次对字符串的操作都会创建新的对象,jvm会维护一个串池,尽可能的存储和重用字符串。例如;
1 | String localPrefix = "297"; //1 |
但是对于计算的字符串无效,例如对上述做如下修改:
1 | String localPrefix = new Integer(297).toString(); //1 |
在这种情况下,实际上我们在堆上有两个不同的对象。如果我们认为这是会经常使用的字符串,则可以通过在字符串的末尾添加.intern()方法来强制JVM将其添加到字符串池中:
1 | String localPrefix = new Integer(297).toString().intern(); //1 |
垃圾收集
如前所述,根据栈中变量保存到堆中对象的引用的类型,在某个时间点,该对象将符合垃圾收集器的回收条件。
例如,所有红色的对象都有资格被垃圾收集器回收。你可能会注意到堆上有一个对象,此对象拥有堆上其他对象的强引用(例如可以是一个列表,该列表具有对其项的引用,或者是一个具有两个引用类型字段的对象)。但是,由于来自于栈的引用丢失了,因此无法再对其进行访问,所以它也是垃圾。
为了更深入地讲述细节,我们首先要说明这几件事:
- 此过程由Java自动触发,由Java决定何时以及是否启动此过程。
- 这实际上是一个昂贵的过程。当垃圾收集器运行时,应用程序中的所有线程都将暂停(取决于GC类型,这将在后面讨论)。
- 实际上,这不仅仅是一个垃圾收集和释放内存的过程,而是一个更复杂的过程。
即使Java决定了何时运行垃圾收集器,那么我可以显式调用System.gc()并期望垃圾收集器在执行此行代码时运行,对吗?
这是一个错误的假设。
既然这是一个非常复杂的过程,并且可能会影响性能,那么我们能不能以一种智能的方式来实现呢?为此有了所谓的Mark and Sweep过程。Java分析来自栈的变量,并mark所有需要保持活动状态的对象。然后,清除所有未使用的对象。
因此,实际上,Java不会收集任何垃圾。事实是,垃圾越多,对象被标记为活动的越少,进程就越快。为了使其更加优化,堆内存实际上由多个部分组成。我们可以使用Java JDK附带的工具——JVisualVM——可视化内存使用情况和其他有用的东西。唯一要做的就是安装一款名为Visual GC的插件,该插件允许查看内存的实际结构。让我们放大并分解大图:
创建对象后,将在Eden(1)空间上分配该对象。由于Eden空间并不大,因此会很快占满空间。垃圾收集器在Eden空间上运行,并将对象标记为活动的。
一旦对象在垃圾回收过程中幸存下来,就将其移动到所谓的幸存区S0(2)中。垃圾收集器第二次在Eden空间上运行时,会将所有幸存的对象移动到S1(3)空间中。同样,当前在S0(2)上的所有内容都移至S1(3)空间中。
如果一个对象可以幸存X轮垃圾回收(X取决于JVM实现,本例中是8)可以通过-XX:MaxTenuringThreshold
指定,那么它很可能会永远存在,并转移到Old(4)空间中。
到目前为止,如果你查看垃圾收集器图表(6),你会看到每次运行时,对象切换到幸存区,而Eden空间则获得了空间。就是这样。老的代也可以进行垃圾回收,但是由于与Eden空间相比,它占了内存的更大部分,因此这种情况很少发生。Metaspace(5)用于在JVM中存储有关已加载类的元数据。
显示的图片实际是Java 8应用程序。在Java 8之前,内存的结构有些不同。元空间实际上称为PermGen空间。例如,在Java 6中,这个空间还存储了字符串池的内存。因此,如果Java 6应用程序中的字符串太多,则可能会崩溃。
空间担保: 在新生代发生垃圾回收的时候,JVM会先检查old区中可分配的连续空间是否大于新生代所有对象的总和,如果大于,那么本次垃圾回收就可以安全的执行;如果不大于,那么JVM会先去检查参数HandlePromotionFailure
设置值是否允许空间担保失败,如果允许,JVM会继续检查老年代可分配的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尽管这次Minor GC是有风险的,JVM也会尝试一次Minor GC;如果不允许担保失败,那么JVM直接进行Full GC
垃圾收集器类型
实际上,JVM有三种类型的垃圾收集器,可供程序员选择使用。默认情况下,Java根据基础硬件选择要使用的垃圾收集器类型。
串行GC——单线程收集器。通常适用于数据量少的小型应用程序。可以通过指定命令行选项启用:
-XX:+UseSerialGC
并行GC——从名称上来说,串行和并行之间的区别在于,并行GC使用多个线程来执行垃圾收集过程。这种GC类型也称为吞吐量收集器。可以通过明确指定以下选项启用:
-XX:+UseParallelGC
并发为主GC——如果你还记得,在本文前面,曾提到垃圾收集过程实际上是非常昂贵的,并且在运行时,所有线程都将暂停。但是,我们有这种并发为主的GC类型,它与应用程序并发工作。这里为什么要说它是并发“为主”呢?因为它不是100%并发工作于应用程序。有一段时间内线程被暂停。尽管如此,暂停时间被保持得尽可能短,以实现最佳的GC性能。实际上,并发为主GC也有两种类型:
- 垃圾优先——高吞吐量和合理的应用程序暂停时间。通过以下选项启用:
-XX:+UseG1GC
- 并发标记清理——应用程序暂停时间保持最短。可以通过指定以下选项来使用:
-XX:+UseConcMarkSweepGC
。从JDK 9开始,不建议使用此GC类型。
- 垃圾优先——高吞吐量和合理的应用程序暂停时间。通过以下选项启用:
使用帮助
为了最大程度地减少内存占用,请尽可能限制变量的作用域。谨记,每次弹出栈顶部的作用域时,该作用域中的引用都会丢失,这会使对象有资格进行垃圾收集。
显式引用null过时的引用。这将使那些引用的对象有资格进行垃圾收集。
避免使用终结器(finalizers)。终结器会放慢进程,且不保证任何事情。最好使用虚引用进行清理工作。
不要在使用弱引用或软引用的地方使用强引用。最常见的内存陷阱是缓存方案,也就是即使可能不需要数据,也将其保存在内存中。
JVisualVM还具有在特定点进行堆转储的功能,因此你可以针对每个类分析其占用的内存大小。
根据应用程序需求配置JVM。在运行应用程序时,显式指定JVM的堆大小。内存分配过程也很昂贵,因此要为堆分配合理的初始和最大内存大小。如果你知道从一开始使用较小的初始堆大小是没有意义的,那么JVM将扩展内存空间。
使用以下选项指定内存选项:- 初始堆大小
-Xms512m
——将初始堆大小设置为512 MB。 - 最大堆大小
-Xmx1024m
——将最大堆大小设置为1024 MB。 - 线程栈大小
-Xss1m
——将线程栈大小设置为1 MB。 - 年轻代大小
-Xmn256m
——将年轻代大小设置为256 MB。
- 初始堆大小
如果Java应用程序因OutOfMemoryError而崩溃,并且你需要一些其他信息来检测泄漏,可以使用
–XX:HeapDumpOnOutOfMemory
参数运行此进程,此参数将在下次发生此错误时创建堆转储文件。使用
-verbose:gc
选项获取垃圾回收输出。每次发生垃圾收集时,都会生成输出。jvm常用打印日志的参数:
1
2
3
4
5
6-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径