垃圾回收
概述
垃圾收集机制并不是java语言首创的.
但是又是java的招牌, java可以自动垃圾回收.
垃圾回收:
回收哪些区域 频繁回收堆内存,较少回收方法区 栈,本地方法栈,程序计数器没有垃圾回收
什么时候回收
怎么回收
什么是垃圾
垃圾是指在运行程序中没有任何引用指向的对象,这个对象就是需要被回收的垃
圾。
为什么需要 GC?
垃圾如果不及时清理,越积越多,可能会导致内存溢出.
垃圾多了,内存碎片较多 例如数组 需要连续空间
早期是手动回收不被使用的对象 例如 C++
java语言是自动垃圾收集的.
垃圾回收机制
自动内存管理
无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和 内存溢出的风险
以更专心地专注 于业务开发
自动收集的担忧
自动回收方便了程序员的开发,但是降低处理内存问题的能力.
自动虽好,但是还是应该了解并掌握一些相关内存管理的知识.
会弱化 Java 开发人员在程序出现内存溢出时定位问题和解决问题的能力。
Java 堆是垃圾收集器的工作重点
从次数上讲:
频繁收集 Young 区
较少收集 Old 区
基本不收集元空间(方法区)
内存溢出与内存泄漏
溢出: 内存不够用了
泄漏: 有些对象已经在程序不被使用了,但是垃圾回收机制并不能判定其为垃圾对象,不能将其回收掉,
这样的对象越积越多, 长久也会导致内存不够用.
例如: 与数据库连接完之后,需要关闭连接通道,但是没有关闭.
io 读写完成后没有关闭
垃圾收集算法分为两大类:
1.垃圾标记阶段算法
主要是来判定哪些对象已经不再被使用,标记为垃圾对象.
判定对象为垃圾的标准: 不被任何引用所指向的对象. Object obj = new Object();
垃圾回收阶段算法:
引用计数算法(在jvm中不被使用)
如果有一个引用指向此对象,那么计数器加1. 如果没有引用指向,计数器为0, 此时就判定为垃圾.
优点:方便使用,设计简洁.
缺点: 增加了计数器的存储空间,计数需要消耗时间.
导致一个循环引用问题. 好几个对象之间相互引用,但是没有其他引用指向他们,此时垃圾回收不能回收他们,但是也没有引用指向. 这就造成了内存泄漏
Object obj = new Object();
obj=null;
可达性分析算法/根搜素算法(这是java目前所使用的垃圾标记算法)
解决 循环引用问题, 设计简单 ,运行高效,防止内存泄漏
思路:
从一些活跃引用(GCRoots 根)开始, 如果对象被根直接或间接引用,那么此对象不是垃圾, 否则标记为垃圾对象.
哪些引用被用来当做根:
虚拟机栈中引用的对象 (方法中引用的对象)
本地方法栈中引用的对象
静态变量所引用的对象
常量引用指向的对象
被synchronized当做锁的对象
Java 虚拟机内部的引用
总结: 栈中引用的(正在使用的) 方法区,常量池中(生命周期较长的),被synchronized当做锁的对象
final(关键字) finally(代码块) finalize()(方法) 是Object类中的一个方法,在对象被最终回收之前调用,只调用一次.
八股文—>背 理解了背一遍就记住了 加入想法,上升到一个全局思想. 不理解 天天背也不理解
finalize() 方法机制
java允许对象在销毁前去调用finalize(),去处理一些逻辑. 一般不用(不建议用)
public class CanReliveObj {
public static CanReliveObj obj;//类变量,属于 GC Root
//此方法只能被调用一次
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用当前类重写的finalize()方法");
obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
}
public static void main(String[] args) {
try {
obj = new CanReliveObj();
// 对象第一次成功拯救自己
obj = null;
System.gc();//调用垃圾回收器
System.out.println("第1次 gc");
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("第2次 gc");
// 下面这段代码与上面的完全相同,但是这次自救却失败了
obj = null;
System.gc();
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
不要自己显示的去调用finalize()方法,在里面写代码一定要慎重
在 finalize()时可能会导致对象复活。
finalize()由垃圾回收器调用,没有固定的时间.
一个糟糕的 finalize()会严重影响 GC 的性能。比如 finalize 是个死循环。
对象状态:
**可触及的:**从根节点开始,可以到达这个对象 。 (没有被标记为垃圾)
**可复活的:**对象的所有引用都被释放,但是对象有可能在 finalize()中复活。 确定为垃圾了,但没有调用finalize()方法.
**不可触及的:**对象的 finalize()被调用,并且没有复活,那么就会进入不可触及 状态。不可触及的对象不可能被复活,因为 finalize()只会被调用一次.
2.垃圾回收阶段算法
1.标记-清除算法
分为两个阶段:
标记: 标记出从根可达的对象,标记的是被引用的对象.
清除: 此清除并非直接将垃圾对象清除掉.
而是将垃圾对象的地址维护到一个空闲列表中,
之后如果有新的对象产生,判断空闲列表中的对象空间能否存放得下新的对象,如果能放得下,那么就覆盖垃 圾对象.
优点: 简单,容易理解
缺点: 效率低, 会产生STW(在回收时,停止整个应用程序), 会产生内存碎片.
2.复制算法
将内存分为大小相等的两块,每次只使用其中的一块儿区域即可.
当回收时,将不是垃圾的对象,复制到另一块内存中,排放整齐.
然后将原来的内存块清空.
减少内存碎片.
优点: 运行高效,减少内存碎片
缺点: 用到两倍的内存空间 ,对于G1垃圾回收器,将每个区域又可以拆分成更多的小区域,需要维护各区之间的关系.
在新生代中的幸存者0和幸存者1这两个区域使用复制算法.
3.标记压缩算法
背景:
复制算法需要移动对象位置,移动的数量如果多的情况下,效率低. 对于年轻代来讲还是不错的.
对于老年代,大量的对象是存活的. 如果需要移动就比较麻烦效率低.
实现:
将存活对象标记出来,重新在本内存空间中排放位置.
清除其他空间的垃圾对象.
标记-清除 和 标记-压缩对比
标记清除是不移动对象, 不会把垃圾对象清除掉(维护在一个空闲列表中)
标记-压缩是要移动对象的. 要清除掉垃圾对象.
优点: 不会像标记-清除算法那样会产生内存碎片
不会像复制算法那样需要两块内存空间
缺点:
效率相对低, 对象位置移动后需要重新设置对象地址, 也会有STW
分代/分区收集
由于对象的生命周期长短不同,将不同的对象存储在不同的区域.
针对不同的区域进行分区收集,提高收集效率.
垃圾回收中的相关概念
System.gc() 的理解
调用System.gc()方法,会触发FULL GC(整堆收集),
但是不一定调用后会立刻生效,因为垃圾回收是自动的.
一般情况下,不要在项目中显示的去调用.
Stop the World
Stop the World -->STW 在垃圾回收时,会导致整个应用程序停止.
在标记垃圾对象时,需要以某个时间节点上内存中的情况进行分析(拍照 快照)
因为不进行停顿的话,内存中的对象不停的变化,导致分析结果不准确.
停顿是不可避免的,优秀的垃圾回收器尽可能减少停顿的时间.
对象引用
Object obj = new Object();
就是将对象分等级: 强引用(有引用指向的对象) 软引用 弱引用 虚引用(都是垃圾了)
强引用: Object obj = new Object(); obj引用创建的对象 那么此对象就是被强引用的.
这种情况下,即使内存不够用了,报内存溢出,也不会回收.
软引用: 当内存足够使用时,先不回收这类对象,当虚拟机内存不够用时,要回收此类对象.
弱引用: 此类对象只能生存到下次垃圾回收时, 只要发生垃圾回收,就会回收此类对象.
虚引用: 发现即回收.
内存溢出与内存泄漏
垃圾回收器
比较底层,了解垃圾回收器的一些种类及实现.
垃圾回收器(具体实现垃圾回收的收集器名称)
垃圾回收器分类
按线程数分,可以分为串行垃圾回收器和并行垃圾回收器
按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器
按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器
垃圾回收器的性能指标
吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运
行时间+内存回收的时间)
垃圾收集开销:垃圾收集所用时间与总运行时间的比例。
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
收集频率:相对于应用程序的执行,收集操作发生的频率。
内存占用:Java 堆区所占的内存大小。
快速:一个对象从诞生到被回收所经历的时间。
常见的垃圾收集器
CMS 回收器(低延迟)
初始标记:
Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直
接关联的对象进行标记。
并发标记:使用多条标记线程,与用户线程并发执行。此过程进行可达性分析, 标记出所有废弃对象。速度很慢。
重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过 程中新出现的废弃对象标记出来。
并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。 这个过程非常耗时。
并发标记与并发清除过程耗时最长,且可以与用户线程一起工作,因此,总体上 说,CMS 收集器的内存回收过程是与用户线程一起并发执行的
CMS 的优点:
并发收集
低延迟
CMS 的弊端:
- 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配
大对象的青况下,不得不提前触发 Ful1 GC.
2.CMS 收集器对 CPU 资源非常敏感。在并发阶段,它虽然不会导致用户停顿,
但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低
G1(Garbage First)回收器(区域划分代式)
因为 G1 是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)
(物理上不连续的)。使用不同的 Region 来表示 Eden、幸存者 0 区,幸存者
1 区,老年代等。
G1 GC 有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各
个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时
间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收
价值最大的 Region.
由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给 G1 一个名字:垃圾优先(Garbage First)。
G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备
多核 CPU 及大容量内存的机器,以极高概率满足 Gc 停顿时间的同时,还兼具
高吞吐量的性能特征。
G1 收集器可以 “ 建立可预测的停顿时间模型 ”,它维护了一个列表用
于记录每个 Region 回收的价值大小(回收后获得的空间大小以及回收所需时
间的经验值),这样可以保证 G1 收集器在有限的时间内可以获得最大的回收
效率。
如下图所示,G1 收集器收集器收集过程有初始标记、并发标记、最终标记、
筛选回收,和 CMS 收集器前几步的收集过程很相似:
① 初始标记:标记出 GC Roots 直接关联的对象,这个阶段速度较快,需
要停止用户线程,单线程执行。
② 并发标记:从 GC Root 开始对堆中的对象进行可达新分析,找出存活
对象,这个阶段耗时较长,但可以和用户线程并发执行。
③ 最终标记:修正在并发标记阶段引用户程序执行而产生变动的标记记录。
④ 筛选回收:筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划
适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。可以用
-XX:+UseG1GC 使用 G1 收集器,jdk9 默认使用 G1 收集器。