3.1 如何判断对象可以垃圾回收
两种算法:引用计数法和可达性分析算法
-
引用计数法(Python解释器早期使用的垃圾回收策略)
描述:只要一个对象被其他变量所引用,就让它的计数加1;如果某个变量不再引用它,就让它的计数减1。当它的引用数量为0时,记为垃圾并等待回收弊端:循环引用问题。也就是A对象引用B对象,B对象引用A对象,引用永不为0,永远无法回收
-
可达性分析算法(Java虚拟机采用的垃圾回收策略)
描述:首先要确定一系列根对象(肯定不能被当成垃圾回收的对象,即GC Root对象),也就是先对堆内存中的所有对象进行一遍扫描,之后查看对象是否被根对象直接或间接引用(引用链),如果是则该对象不能被回收;如果不是则该对象可作为垃圾被回收
GC Root对象:通过工具Memory Analyzer(专业内存分析工具)进行分析得知,GC Root对象主要有:
- System Class(java.lang.Class)
- Native Stack(本地方法栈)
- Thread(活动线程)
- Busy Monitor(锁对象)
概括起来GCRoot对象就是:
- 虚拟机栈中的局部变量表引用的对象
- 方法区中类静态属性引用和常量引用对象
- 本地方法栈中引用的对象
JVM中的几种引用:
-
强引用
特点:GC时永远不会被回收
描述:强引用是指创建一个对象并把这个对象赋给一个引用变量。比如:
Object object = new Object(); String str = "hello";
强引用有引用变量指向时永远不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。强引用也是 导致内存泄露的主要原因
-
软引用
特点:内存不足时(自动触发GC)会被回收
描述:如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。在java中,用java.lang.ref.SoftReference类来表示。可以配合引用队列来释放软引用自身
-
弱引用
特点:无论内存是否充足,只要进行GC都会被回收
描述:弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以配合引用队列来释放弱引用自身
-
虚引用
特点:如同虚设,和没有引用没什么区别描述:虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。使用时必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放内存。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收
-
终结器引用
特点:无需手动编码,其内部配合引用队列使用
描述:它用于实现对象的
finalize()
方法,也可以称为终结器引用。在GC时, 终结器引用入队。由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize()
方法,第二次GC时才能回收被引用对象
3.2 垃圾回收算法
常见回收算法:
-
标记清除(Mark Sweep)
- 先标记:沿着GCRoot引用链扫描,把没有引用到的对象作为待回收的垃圾标记起来
- 后清除:释放标记对象的内存,也就是将对象起止内存编号存入空闲地址列表,以便再次内存分配
- 优势:垃圾回收速度快
- 弊端:空间不连续,容易产生内存碎片
-
标记整理(Mark Compact)
- 先标记:同上
- 后整理:将存活着的对象整理在一起,覆盖掉之前标记的内存,使内存空间更紧凑
- 优势:不会产生内存碎片
- 弊端:工作量大,速度慢
-
复制(Copy)
-
先分区:将内存区划成两块大小相等的区域(FROM区域和TO区域)
-
再标记:同上
-
复制并交换:将FROM区域的存活对象整理在一起复制到TO区域,并将两个区域交换(即FROM区域变成TO区域,TO区域变成FROM区域)
-
优势:不会产生内存碎片
-
弊端:会占用双倍的内存空间
-
小结:在JVM中以上三种回收算法其实都会涉及到,由JVM根据内存占用的情况进行决策具体使用哪些算法
3.3 分代垃圾回收
分代垃圾回收策略将堆内存划分为两个区域,即新生代和老年代。其中新生代又由伊甸园、幸存区FROM和幸存区TO构成
划分理由:Java中有的对象需要长时间使用,所以就放在老年代;而那些用完之后就可以丢弃的对象就放在新生代。老年代的垃圾回收很久才发生一次,新生代的垃圾回收则比较频繁。这样一来,可以根据对象生命周期的不同特点实施不同的垃圾回收策略
分配与回收的具体过程:
- 对象首先分配在伊甸园区域,如果对象所占空间大于伊甸园的空间小于老年代的空间,则直接将该对象晋升到老年代
- 新生代空间不足时触发minor gc,伊甸园和from存活的对象使用copy算法复制到to中,存活的对象年龄加1,其余对象被回收,之后交换from和to
- minor gc会引发stop the world,也就是会暂停所有其他用户线程等垃圾回收结束,用户线程才恢复运行。但minor gc速度很快,停顿时间较短
- 当对象寿命超过阈值(最大15)时,会晋升到老年代
- 当老年代空间不足时,会先触发一次minor gc,如果之后空间仍不足,则触发full gc(老年代存活的对象更多,且老年代不再采用复制算法作为垃圾回收算法,所以停顿时间更长)
- 如果空间仍不足,则抛出OutOfMemoryException
附加知识:堆内存相关VM参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx或-XX:MaxHeapSize=size |
新生代大小 | -Xmn或(-XX:NewSize=size + -XX:MaxNewSize=size) |
幸存区比例(动态) | -XX:InitalSurvivorRatio=ratio和 -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:InitalSurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC前MinorGC | -XX:+ScavengeBeforeFullGC |
注意:一个线程内的堆内存溢出并不会使整个程序崩溃,只会抛出一个OOM异常
3.4 垃圾回收器
-
串行
-
底层是一个单线程的垃圾回收器
-
应用场景是堆内存较小,个人电脑(CPU个数较少)
-
开启串行垃圾回收器的VM参数:
-XX:+UserSerialGC=Serical+SerialOld
-
-
吞吐量优先(JDK1.8默认)
- 多线程
- 应用场景是堆内存较大,多核CPU
- 注重让单位时间内STW的时间最短,即清理更彻底
- 开启该回收器的参数为:
-XX:+UseParallelGC
- 优化参数:
-
-XX:ParallelGCThreads=n
指定垃圾回收器的线程数 -
-XX:+UseAdaptiveSizePolicy
动态调整新生代内伊甸园的大小 -
-XX:GCTimeRatio=ratio
垃圾回收时间在总时间中的占比默认1%,开启此参数VM将会动态调整堆的大小来适应这个比值 -
-XX:MaxGCPauseMillis=ms
设置单次最长垃圾回收时间,默认不超过200ms
-
-
响应时间优先
-
多线程
-
应用场景是堆内存较大,多核CPU
-
注重尽可能让单次STW的时间最短,即清理更快
-
是工作在老年代的垃圾回收器,能与用户线程并发进行,STW的时间短。但并发失败时会退化成一个单线程的串行垃圾回收器
-
由于采用Mark Sweep算法进行垃圾清除,当产生的内存碎片过多无法再分配对象时,会导致并发失败(最大的问题)
-
开启该回收器的VM参数为:
-XX:UseConcMarkSweepGC
-
优化参数:
-
-XX:ConcGCThreads=n
指定垃圾回收器的线程数,一般为总CPU数 $\div$4 -
-XX:CMSInitiatingOccupancyFraction=percent
指定为浮动垃圾预留的空间占比,超过这个比值将触发该垃圾回收器。这个值越小越早触发垃圾回收器 -
-XX:+CMSScavengeBeforeRemark
在重新标记(第二次STW)之前先执行一次清理,避免重复标记,缩短STW的时间
-
-
-
G1(JDK9及以上默认):
- 新一代垃圾收集器,取代了CMS垃圾回收器
- 适用场景:
- 同时注重吞吐量和低延迟,默认的暂停目标是200ms
- 适合超大堆内存,会将堆划分为多个大小相等的Region,每个区域都有可能成为伊甸园(Eden)、From区域、To区域和老年代
- 整体上是标记+整理算法,两个区域之间是复制算法
- 相关VM参数:
-XX:+UseG1GC
启用G1回收器-XX:G1HeapRegionSize=size
设置划分堆内存的Region大小-XX:MaxGCPauseMillis=time
设置最大的垃圾回收时间,默认200ms-XX:InitiatingHeapOccupancyPercent=percent
设置老年代占用堆空间比例的阈值,当达到这一阈值时触发并发标记(不STW),默认45%
3.5 垃圾回收调优
- 预备知识
- 掌握GC相关VM参数,会基本的空间调整
- 掌握相关工具
- 调优跟应用、环境有关,没有放之四海而皆准的法则
-
调优领域
- 内存
- 锁竞争
- cpu占用
- io
-
确定目标
- 【低延迟】还是【高吞吐量】,选择合适的回收器
- CMS,G1,ZGC
- ParallelGC
注意:不同的垃圾回收器有不同的应用场景,低延迟适用于互联网项目(类似分时系统),高吞吐量适用于科学计算(类似批处理系统)
-
最快的GC是不发生GC
查看FullGC前后的内存占用,要考虑的问题主要有:- 数据是不是太多?
resultSet = statement.executeQuery("select * from 大表");
这种情况就需要考虑加limit或者使用分页查询,否则当多个线程同时访问大批量数据时就会造成OOM异常
- 数据表示是不是太臃肿?
- 拒绝对象图,用到哪个查哪个
- 要了解对象大小对内存的影响:
- 最简单的Object对象都要占16字节的内存
- 包装类型Integer占24字节内存,而int类型只占4字节,所以能用基本类型就尽可能少用包装类型
- 是否存在内存泄漏?
- 反面教材:不断向静态的Map对象中添加对象
- 解决方法:使用软引用或弱引用,或使用第三方缓存(如redis)
- 数据是不是太多?
-
新生代调优
- 所有的new操作的内存分配都非常廉价
- TLAB(thread-local allocation buffer)线程局部分配缓冲区
- 死亡对象的回收代价是0
- 大部分对象用过即死
- Minor GC的时间远小于Full GC
- 调优策略:
- 新生代并非越大越好,Oracle官方建议新生代大小应该占整个堆空间的25% ~ 50%。太小则频繁触发Minor GC,太大又会导致频繁触发Full GC
- 新生代能容纳所有【并发量 * (请求与响应)】的数据
- 幸存区大到能保留【当前活跃的对象 + 需要晋升对象】
- 晋升阈值配置得当,让长时间存活对象尽快晋升,而不应该长期保留在幸存区
两个常用VM指令:-XX:MaxTenuringThreshold=threshold
调整最大晋升阈值,推荐适当调小-XX:+PrintTenuringDistribution
打印晋升对象的详细信息
- 所有的new操作的内存分配都非常廉价
-
老年代调优
以CMS为例:- CMS的老年代内存越大越好
- 先尝试不调优,如果没有Full GC那么已经很OK了,应该先调优新生代
- 观察发生Full GC时老年代内存的占用,将老年代内存预设调大1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent
指定为浮动垃圾预留的空间占比,超过这个比值将触发该垃圾回收器。一般设为70%~80%最佳