1.如何判断对象可以回收
1.1 引用计数法
Reference Counting
记录每个对象被引用的次数,当对象被引用时,引用计数器加1,当引用被释放时,引用计数器减1。当引用计数器为0时,说明该对象没有任何引用,可以被回收。
引用计数法的优点是实现简单,回收对象的时间可以很快。但是,引用计数法也存在一些问题,例如无法处理循环引用的情况。如果两个对象相互引用,它们的引用计数器会一直不为0,因此这两个对象将永远不会被回收,从而导致内存泄漏。Java虚拟机(JVM)没有使用引用计数算法来进行垃圾回收。
1.2 可达性分析算法
它的基本思想是通过一系列称为“根集”(Root Set)的对象作为起点,递归地遍历对象图,标记所有可达的对象,然后清除所有不可达的对象。
可以使用Eclipse Memory Analyzer Tool(MAT)来查看分析Java堆转储文件中的内存使用情况和对象引用关系
1.3 五种引用
强引用:强引用是最为常见的引用类型,它是指通过一个普通的Java对象变量引用另一个Java对象。只要强引用存在,垃圾回收器就不会回收被引用的对象。只有所有GC Roots对象都不通过[强引用]引用该对象,该对象才能被垃圾回收
Object obj = new Object();
软引用:软引用是一种比强引用弱一些的引用类型,它可以让对象存活一段时间,直到系统内存不足时才会回收。当JVM需要回收内存时,会先回收所有的弱引用对象,然后再回收所有的软引用对象。软引用可以通过java.lang.ref.SoftReference
类来实现。仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象可以配合引用队列来释放软引用自身
Object obj = new Object();
SoftReference<Object> softRef = new SoftReference<>(obj);
弱引用:弱引用是一种比软引用更加弱的引用类型,它可以让对象存活直到没有任何强引用指向它为止,即只有弱引用指向它时垃圾回收器会回收它。弱引用可以通过java.lang.ref.WeakReference
类来实现。仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象,可以配合引用队列来释放弱引用自身
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
虚引用:虚引用是一种最弱的引用类型,它几乎没有引用价值,主要用于在对象被回收之前,做一些必要的清理工作。当垃圾回收器决定回收一个对象时,如果它存在虚引用,就会在回收对象之前将虚引用加入到一个队列中,程序可以通过这个队列来获取对象被回收的通知。虚引用可以通过java.lang.ref.PhantomReference
类来实现。必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存
Object obj = new Object();
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, referenceQueue);
终结器引用:终结器引用是一种特殊的引用类型,它与对象的终结器相关联。在Java中,对象的终结器是一个方法,当垃圾回收器准备回收对象时,会先调用对象的终结器方法。终结器引用可以通过java.lang.ref.FinalizerReference
类来实现,但是由于终结器引用的实现过于复杂,因此在Java 9中已经被弃用。无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器弓|用找到被弓|用对象并调用它的finalize方法,第二次GC时才能回收被引用对象
2.垃圾回收算法
2.1 标记清除算法
清除时,并不需要将垃圾内存置零,而是记录下起始和结束位置,以提供给接下来的程序使用。然而,这样的做法可能会导致内存空间的碎片化,从而影响程序的性能。
2.2 标记整理算法
标记-整理算法则会先标记出所有需要回收的对象,然后将所有存活的对象向一端移动,最后清理掉不需要的对象。这个过程中,整理出来的存活对象会被移动到一端,释放出来的空闲内存空间会被整理成一块连续的内存区域,以便后续程序使用。这样的做法可以减少内存空间的碎片化,提高程序的性能。
2.3 复制算法
复制算法是一种基于空闲列表的垃圾回收算法,它将堆空间分成两块大小相同的区域,每次只使用其中一块,称为“From Space”,另一块则保持空闲,称为“To Space”。当From Space中的对象需要进行垃圾回收时,复制算法会将其中存活的对象复制到To Space中,并将From Space中的所有对象全部清除。这样一来,To Space中就会有一块连续的内存空间,可以供程序使用。
需要注意的是,Java虚拟机的垃圾回收算法是动态选择的,根据垃圾回收器的实现和运行时情况来选择合适的算法。因此,在实际应用中,可能会使用不同的垃圾回收器和不同的垃圾回收算法,以达到最优的垃圾回收效果。
3.分代垃圾回收
JVM中的内存被分为不同的区域,其中包括新生代和老年代。
新生代是JVM内存的一部分,它是用于存放新创建的对象的区域。当创建一个新的对象时,JVM将其分配到新生代中的Eden空间。如果Eden空间没有足够的空间来存放新对象,JVM将会启动垃圾回收机制,将不再被引用的对象清理出内存。如果存活下来的对象足够多,它们将会被移动到新生代中的Survivor空间。经过多次回收后仍然存活下来的对象会被晋升到老年代中。
老年代是JVM内存的另一部分,它是用于存放已经存活了一段时间的对象的区域。当一个对象在新生代经历了多次垃圾回收仍然存活下来,它将被移动到老年代中。老年代是相对稳定的,垃圾回收的频率比新生代要低。
- 对象首先分配在Eden(伊甸园)区域
- 新生代空间不足时,触发 minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄+1且交换 from to
- minor gc 会引发 stop the world,即暂停其他用户的线程,待垃圾回收结束,用户线程才恢复运行
当对象寿命超过阈值(最大是15->4bit)时,会晋升到老年代
- 当需要分配一个大对象时,JVM会先检查老年代的剩余空间是否足够,如果足够,则将该对象直接分配到老年代中。
- 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍然不足,那么触发 full gc,STW(stop the world)的时间更长
相关VM参数
参数 | 含义 |
---|---|
-Xms | 堆初始大小 |
-Xmx 或 -XX:MaxHeapSize=size | 堆最大大小 |
-Xmn 或 -XX:NewSize=size + -XX:MaxNewSize=size | 新生代大小 |
-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptivePolicy | 幸存区比例(动态) |
-XX:SurvivorRatio=ratio | 幸存区比例 |
-XX:MaxTenuringThreshold=threshold | 晋升阈值 |
-XX:+PrintTenuringDistribution | 晋升详细 |
-XX:+PrintGCDetails -verbose:gc | GC详细 |
-XX:+ScavengeBeforeFullGC | FullGC 前 MinorGC |
4. 垃圾回收器
- 串行
- 单线程
- 堆内存较小,适合个人电脑
- 吞吐量优先
- 多线程
- 堆内存较大,多核CPU
- 使得单位时间内的STW的时间最短
- 响应时间优先
- 多线程
- 对此内存较大,多核CPU
- 尽可能缩短单次STW时间
4.1 串行
-XX:+UseSerialGC=Serial + SerialOld
Serial->复制算法;SerialOld->标记整理算法
4.2 吞吐量优先
-XX:+UseParallelGC ~ -XX:+UseParallerOldGC
-XX:+UseAdapticeSizePolicy(动态调整)
-XX:GCTimeRatio=ratio(1/1+ratio -> 垃圾回收占总时间的比例)
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n(线程数)
4.3 响应时间优先
-XX:+UseConCMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent(老年代堆空间使用率的百分比阈值,默认是68%)
-XX:+CMSScavengeBeforeRemark
4.4 G1
定义: Garbage First
- 2004论文发布
- 2009 JDK 6u14体验
- 2012 JDK 7u4
官方支持
- 2017JDK9默认
适翻场景
- 同时注重吞吐量(Throughput) 和低延迟(Low latency),默认的暂停目标是200 ms
- 超大堆内存,会将堆划分为多个大小相等的Region
- 整体上是标记+整理算法,两个区域之间是复制算法
相关JVM参数
- -XX:+UseG1GC
- -XX:G1HeapRegionSize=size
- -XX:MaxGCPauseMillis=time
1)G1垃圾回收阶段
3)Young Collection + CM
- 在Young GC 时会进行 GC Root的初始标记
老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定:
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
4)Mixed GC
会对E、S、O进行全面垃圾回收
- 最终标记(Remark)会STW
- 拷贝存活(Evacuation)会STW
-XX:MaxGCPauseMillis=ms
优先收集较大的老年代内存区
5)Full GC
SerialGC
- 新生代内存不足发生的垃圾回收 - minor gc
- 老生代内存不足发生的垃圾回收 - full gc
ParallelGC
- 新生代内存不足发生的垃圾回收 - minor gc
- 老生代内存不足发生的垃圾回收 - full gc
CMS
- 新生代内存不足发生的垃圾回收 - minor gc
老生代内存不足发生的垃圾回收
- 并发收集失败 -> full gc
G1
- 新生代内存不足发生的垃圾回收 - minor gc
老生代内存不足发生的垃圾回收
- 新生代内存不足 -> Mixed GC
- 老生代内存不足时 -> full gc
6)Young Collection 跨代引用
新生代回收的跨代引用(老年代引用新生代)问题
- 卡表与Remembered Set
- 在引用变更时通过post-write barrier + dirty card queue
- concurrent refinement threads更新Remembered Set
7)Remark
pre-write barrier
+ satb_mark_queue
8)JDK 8u20 字符串去重
- 优点:节省大量内存.
- 缺点:略微多占用了cpu时间,新生代回收时间略微增加
-XX: +UseStringDeduplication
String s1 = new String("hello"); // char[]{'h','e','l','1','o'}
String s2 = new String("hello"); // char[]{'h','e','1','l','o'}
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否有字符串重复
- 如果它们值-样,让它们引用同一个char[]|
注意,与String. intern()不一样
- String. intern()关注的是字符串对象
- 而字符串去重关注的是char[]
- 在JVM内部,使用了不同的字符串表
9)JDK 8u40并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
条件包括:
- 该类所有的实例都已被回收。
- 该类的 ClassLoader 已经被回收。
- 该类没有在其他地方被引用。
-XX:+ClassUnloadingWithConcurrentMark (默认启用)
10)JDK 8u60回收巨型对象
- 一个对象大于region的一半时,称之为巨型对象
- G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
11)JDK 9并发标记起始时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为FullGC
- JDK 9之前需要使用-XX: InitiatingHeapOccupancyPercent
JDK 9可以动态调整
- -XX: InitiatingHeap0ccupancyPercent用来设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空档空间
5.垃圾回收调优
java -XX:+PrintFlagsFinal -version | findstr "GC"
5.1 新生代调优
新生代的特点:
所有new操作的内存分配非常廉价
- TLAB thread-local allocation buffer
- 死亡对象的回收代价是0
- 大部分对象用过即死
- Minor GC 的时间远远低于Full GC
调优原则:
- 晋升阈值配置得当,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistribution
5.2 老年代调优
以CMS为例
- CMS的老年代内存越大越好
- 先尝试不做调优,如果没有Full GC说明老年代OK,否则先尝试调优新生代
观察发生Full GC时老年代的内存占用,将老年代内存预设调大1/4 ~ 1/3
- -XX:CMSInitiatingOccupancyFraction=percent
评论