2 垃圾回收
2.1如何判断对象可以回收
2.1.1 引用计数法
什么是引用计数法:
缺点:
2.1.2可达性分析算法
可达分析:
根对象:肯定不能被垃圾回收的对象
jvm会对堆中所有对象进行扫描,看其是否被根对象直接或者间接引用,如果没有被直接或间接引用就会被垃圾回收器回收
在Java语言中,可作为GC Root的对象包括以下几种:
- 虚拟机栈(栈桢中本地变量表)中引用的对象
- 方法区中类静属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Navite方法)中引用的对象
2.1.3 四种引用
一、强引用(StrongReference)
当我们使用 new 这个关键字创建对象时创建出来的对象就是强引用(new出来对象为强引用) 如Object obj = new Object() 这个obj就是一个强引用了,如果一个对象具有强引用。垃圾回收器就不会回收有强引用的对象。如当jvm内存不足时,具备强引用的对象,虚拟机宁可会抛出OutOfMemoryError(内存空间不足),使程序终止,也不会靠垃圾回收器去回收该对象来解决内存。
二、软引用(SoftReference)
如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。可以配合引用队列来释放软引用自身
软引用的作用:软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
public static void soft() {
// list --> SoftReference --> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
三、弱引用(WeakReference)
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。**在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。**不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。可以配合引用队列来释放弱引用自身
private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
// list --> WeakReference --> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
for (WeakReference<byte[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();
}
System.out.println("循环结束:" + list.size());
}
四、虚引用 (PhantomReference)
“虚引用”顾名思义,就是形同虚设,和其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有 虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
ReferenceQueue queue = new ReferenceQueue ();
//虚引用对象
PhantomReference pr = new PhantomReference (object, queue);
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
2.2 垃圾回收算法
2.2.1 标记清除
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
优点:速度快
缺点:容易产生内存碎片,不利于后续较大对象的内存分配
2.2.2 标记整理
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
优点:没有内存碎片
缺点:速度较慢
2.2.3 复制算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的**内存缩减到原来的一半。**很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。
2.3 分代垃圾回收
过程:
-
对象首先分配在伊甸园区域
-
新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
-
minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
-
当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
-
当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
2.3.1相关 VM 参数
GC 分析-大对象直接晋升老年代
public class Code_10_GCTest {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
// -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_6MB]);
list.add(new byte[_512KB]);
list.add(new byte[_6MB]);
list.add(new byte[_512KB]);
list.add(new byte[_6MB]);
new Thread(() -> {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();
System.out.println("sleep....");
Thread.sleep(1000L);
}
}
通过上面的代码,给 list 分配内存,来观察 新生代和老年代的情况,什么时候触发 minor gc,什么时候触发 full gc 等情况,使用前需要设置 jvm 参数。
2.4 垃圾回收器
-
串行
单线程
堆内存较小,适合个人电脑
-
吞吐量优先
多线程
堆内存较大,多核 cpu
让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高
-
响应时间优先
多线程
堆内存较大,多核 cpu
尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
2.4.1 串行
-
单线程
-
堆内存较小,适合个人电脑
- -XX:+UseSerialGC=serial + serialOld
安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象
因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态
Serial 收集器
Serial 收集器是最基本的、发展历史最悠久的收集器
**特点:**单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!
ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本
**特点:**多线程、ParNew 收集器默认开启的收集线程数与CPU的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本
特点:同样是单线程收集器,采用标记-整理算法
2.4.2 吞吐量优先
- 多线程
- 堆内存较大,多核 cpu
- 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4
-XX:+UseParallelGC ~ -XX:+UsePrallerOldGC
-XX:+UseAdaptiveSizePolicy //自适应调整大小新生代堆内存大小策略
-XX:GCTimeRatio=ratio // 1/(1+radio) //调整垃圾回收时间与总运行时间的比例 (radio默认99 一般设置19)↓
-XX:MaxGCPauseMillis=ms // 200ms //每次最大暂停时间 (与ratio取折中,两者有矛盾↑)
-XX:ParallelGCThreads=n //控制ParallelGC运行时线程数
Parallel Scavenge 收集器
与吞吐量关系密切,故也称为吞吐量优先收集器
**特点:**属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与 ParNew 收集器最重要的一个区别)
GC自适应调节策略:
Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy 参数。
当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、
晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。
Parallel Scavenge 收集器使用两个参数控制吞吐量:
XX:MaxGCPauseMillis=ms 控制每次最大的垃圾收集停顿时间(默认200ms)
XX:GCTimeRatio=rario 调整垃圾回收时间与总运行时间的比例(ParallelGC会根据比例取调整对大小 堆越大回收次数越少,时间少比例越小,吞吐量提高)
Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本
特点:多线程,采用标记-整理算法(老年代没有幸存区)
2.4.3 响应时间优先cms
- 多线程
- 堆内存较大,多核 cpu
- 尽可能让 STW 的单次时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld //UseConcMarkSweepGC 使用并发标记扫描 和用户线程并发运行,减少stop the world时间。 在老年代中运行
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads //ParallelGCThreads 并行gc线程数默认4,ConcGCThreads 设置并发线程数一般设置为总线程1/4;
-XX:CMSInitiatingOccupancyFraction=percent //控制CMS垃圾回收的时机,percent:设置执行CMS垃圾回收的内存占比 老年代占比 预留空间给产生的浮动垃圾
-XX:+CMSScavengeBeforeRemark //在重新标记前,对新生代垃圾进行回收(UseParNewGC),减轻重新标记时的压力,防止多做无用功(+CMSScavengeBeforeRemark +打开,-禁用)
CMS 收集器
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器
特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片
**应用场景:**适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务
CMS 收集器的运行过程分为下列4步:
**初始标记:**标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题
并发清除:对标记的对象进行清除回收,清除的过程中,可能任然会有新的垃圾产生,这些垃圾就叫浮动垃圾,如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!
CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 收集器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。
2.4.4 G1(Garbage First)
定义:Garbage First
- 2004 论文发布
- 2009 JDK 6u14 体验
- 2012 JDK 7u4 官方支持
- 2017 JDK 9 默认
适用场景
-
同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
-
超大堆内存,会将堆划分为多个大小相等的 Region
-
整体上是 标记+整理 算法,两个区域之间是 复制 算法
相关 JVM 参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size //设置堆空间大小
-XX:MaxGCPauseMillis=time//时间
1 G1 垃圾回收阶段
Young Collection:对新生代垃圾收集
Young Collection + Concurrent Mark:如果老年代内存到达一定的阈值了,新生代垃圾收集同时会执行一些并发的标记。
Mixed Collection:会对新生代 + 老年代 + 幸存区等进行混合收集,然后收集结束,会重新进入新生代收集。
2 Young Collection 新生代回收
新生代存在 STW:
分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间!
E:eden,S:幸存区,O:老年代
新生代收集会产生 STW !
3 Young Collection + CM
在 Young GC 时会进行 GC Root 的初始化标记
老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的 JVM 参数决定 -```
XX:InitiatingHeapOccupancyPercent=percent (默认45%)
4 Mixed Collection混合回收
会对 E S O 进行全面的回收
- 最终标记会 STW
- 拷贝存活会 STW
-XX:MaxGCPauseMills=xxms 用于指定最长的停顿时间!
问:为什么有的老年代被拷贝了,有的没拷贝?
因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)
5 Full GC
G1 在老年代内存不足时(老年代所占内存超过阈值)
- 如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理
- 如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,然后退化成 serial Old 收集器串行的收集,就会导致停顿的时候长。
6 Young Collection 跨代引用
新生代回收的跨代引用(老年代引用新生代)问题
-
卡表 与 Remembered Set
- Remembered Set 存在于E中,用于保存新生代对象对应的脏卡
- 脏卡:O 被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
- Remembered Set 存在于E中,用于保存新生代对象对应的脏卡
-
在引用变更时通过 post-write barried + dirty card queue (不会立即执行,异步操作)
-
concurrent refinement threads 更新 Remembered Set
7 Remark(重标记)
重新标记阶段
在垃圾回收时,收集器处理对象的过程中
- 黑色:已被处理,需要保留的
- 灰色:正在处理中的
- 白色:还未处理的
但是在并发标记过程中,有可能 A 被处理了以后未引用 C ,但该处理过程还未结束,在处理过程结束之前 A 引用了 C ,这时就会用到 remark 。
有写屏障 pre-write barrier 技术,在对象引用改变前加入一个队列(satb_mark_queue) 最后remark阶段配合这个队列进行进一步判断。
过程如下
- 之前 C 未被引用,这时 A 引用了 C ,就会给 C 加一个写屏障,写屏障的指令会被执行,将 C 放入一个队列当中,并将 C 变为 处理中状态
- 在并发标记阶段结束以后,重新标记阶段会 STW ,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它,由灰色变成黑色。
8 JDK 8u20 字符串去重
过程
- 将所有新分配的字符串(底层是 char[] )放入一个队列
- 当新生代回收时,G1 并发检查是否有重复的字符串
- 如果字符串的值一样,就让他们引用同一个字符串对象
- 注意,其与 String.intern() 的区别
- String.intern() 关注的是字符串对象
- 字符串去重关注的是 char[]
- 在 JVM 内部,使用了不同的字符串标
优点与缺点
- 节省了大量内存—优
- 新生代回收时间略微增加,导致略微多占用 CPU—缺
jvm参数
-XX:+UseStringDeduplication
9 JDK 8u40 并发标记类卸载
在并发标记阶段结束以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类(jdk的类加载器不会卸载,只限于自定义类的类加载器)
-XX:+ClassUnloadingWithConcurrentMark 默认启用
10 JDK 8u60 回收巨型对象
-
一个对象大于 region 的一半时,称之为巨型对象
-
G1 不会对巨型对象进行拷贝
-
回收时被优先考虑
-
G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉
11 JDK 9 并发标记起始时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为 FulGC
- JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
- JDK 9 可以动态调整
- -XX:InitiatingHeapOccupancyPercent 用来设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空挡空间
2.5 垃圾回收调优
查看虚拟机参数命令
D:\JavaJDK1.8\bin\java -XX:+PrintFlagsFinal -version | findstr "GC"
2.5.1 调优领域
-
内存
-
锁竞争
-
cpu 占用
-
io
2.5.2 确定目标
低延迟:互联网项目,响应时间是重要指标
高吞吐量:(科学计算)?
选择合适的GC
-
响应时间优先:CMS ,G1, ZGC
-
高吞吐量选择:ParallelGC
虚拟机: hotspot,Zing
2.5.3 最快的 GC
答案是不发生 GC
-
查看 FullGC 前后的内存占用,考虑下面几个问题
-
数据是不是太多?
- resultSet = statement.executeQuery(“select * from 大表 limit n”)
-
数据表示是否太臃肿?
- 对象图
- 对象大小 16 Integer 24 int 4
-
是否存在内存泄漏?
-
使用集合时static Map map =
可以使用:软引用,弱应用
第三方缓存实现(redis)
-
-
2.5.4
新生代的特点
-
所有的 new 操作分配内存都是非常廉价的
- TLAB thread-lcoal allocation buffer
-
死亡对象回收零代价
-
- 大部分对象用过即死(朝生夕死)
- Minor GC 所用时间远小于 Full GC
-
新生代内存越大越好么?
-
不是
-
新生代内存太小:频繁触发 Minor GC ,会 STW ,会使得吞吐量下降
-
新生代内存太大:老年代内存占比有所降低,会更频繁地触发 Full GC。而且触发 Minor GC 时,清理新生代所花费的时间会更长
-
-
新生代内存设置为内容纳[并发量*(请求-响应)]的数据为宜
-
-
幸存区需要能够保存 当前活跃对象+需要晋升的对象
-
晋升阈值配置得当,让长时间存活的对象尽快晋升
-XX:MaxTenuringThreshold=threshold //调整最大晋升阈值
-XX:+PrintTenuringDistrubution //打印存活对象详情
2.5.5老年代调优
以 CMS 为例:
-
CMS 的老年代内存越大越好
-
先尝试不做调优,如果没有 Full GC 那么已经,否者先尝试调优新生代。
-
观察发现 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent //设置初始占用率 一般20%(可以为0需要有专门的空闲cpu来执行)
2.5.6 案例
案例1:Full GC 和 Minor GC 频繁
方法: GC频繁说明空间紧张,如果Minor GC 频繁 证明新生代空间太小,本来生存周期很短对象到幸存区后。然后幸存区空间不够,晋升阈值会变低,使对象提前到老年代,情况进一步恶化。老年代存放了生命周期很短的对象,触发老年代的Full GC的频繁发生。解决方法:增加新生代空间,幸存区空间,增加晋升阈值。使得晋升变少,触发FullGC次数变低。
案例2:请求高峰期发生 Full GC,单次暂停时间特别长(CMS)
方法:单词暂停长,是在重新标记时暂停,重新标记会扫描整个堆内存对象,耗时较多,。在重新标记之前,先对新生代对象进行垃圾清理,垃圾对象变少,需要重新标记的对象就变少。
-XX:+CMSScavengeBeforeRemark //在重新标记前,对新生代垃圾进行回收(UseParNewGC),减轻重新标记时的压力,防止多做无用功(+CMSScavengeBeforeRemark +打开,-禁用)
案例3:老年代充裕情况下,发生 Full GC(jdk1.7)
方法:jdk1.7 是采用 永久代实现方法区 永久代空间不足就会触发整个堆的Full GC。增大永久代空间可以解决。
我亦无他,惟手熟尔