简单来讲,垃圾回收 分红两步, 第一步找出垃圾,第二步进行回收,而标记阶段使用的算法,就是 为了找出谁是垃圾html
优势:java
缺点:算法
由于第三点的严重性,JAVA 垃圾回收器中没有使用这类算法。数据库
什么是 循环引用缓存
上面的图中 , 对象一引用了对象二 , 对象二引用了对象三, 而对象三又从新指向了对象一,并发
而对象一是被外部引用的,因此它的计数器是2,eclipse
可是当外部的引用断掉时, 计数器减1,仍然是1, 不会被清除,致使这三个对象 没法清除,形成内存泄漏jvm
使用代码证实JAVA 中没有使用引用计数算法ide
public class RefCountGC { //这个成员属性惟一的做用就是占用一点内存 private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB Object reference = null; public static void main(String[] args) { RefCountGC obj1 = new RefCountGC(); RefCountGC obj2 = new RefCountGC(); obj1.reference = obj2; obj2.reference = obj1; obj1 = null; obj2 = null; //显式的执行垃圾回收行为,这里发生GC,obj1和obj2可否被回收? // System.gc(); } }复制代码
上面代码的内存示意图:函数
下面运行验证一下
jvm参数: -XX:+PrintGCDetails 打印GC日志
不进行垃圾回收时:使用了 16798k
手动进行GC: 只剩下了655k , 说明 这两个对象确实被回收了
引用计数小结
可达性分析算法:也能够称为根搜索算法、追踪性垃圾收集
基本思路以下:
示意图:
GC Roots 能够是哪些元素
列举:
总结一句话就是,除了堆空间外的一些结构,好比:虚拟机栈、本地方法栈、方法区、字符串常量池等地方对堆空间进行引用的,均可以做为GC Roots进行可达性分析
扩展:
除了这些固定的GC Roots集合之外,根据用户所选用的垃圾收集器以及当前回收的内存区域不一样,还能够有其余对象“临时性”地加入,共同构成完整GC Roots集合。好比:在进行分代收集和局部回收时(PartialGC)。
若是只针对Java堆中的某一块区域进行垃圾回收(好比:典型的只针对新生代),必须考虑到这个区域的对象彻底有可能被其余堆区域的对象所引用,例如老年代等,这时候就须要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。
总结:
可达性分析注意事项
注意 finaliza() 方法并非一定是销毁前调用的, 它也是肯定此对象可不能够被销毁的一个判断因素,在标记阶段调用
Object 类中 finalize() 源码
// 等待被重写 protected void finalize() throws Throwable { }复制代码
永远不要主动调用某个对象的finalize()方法应该交给垃圾回收机制调用。
一个糟糕的finalize()会严重影响GC的性能(写个多重循环, 每一个对象在标记时调用时,均可能执行)。
从功能上来讲,finalize()方法与C++中的析构函数比较类似,可是Java采用的是基于垃圾回收器的自动内存管理机制,因此finalize()方法在本质上不一样于C++中的析构函数。
因为finalize()方法的存在,可能会将对象复活,因此虚拟机中的对象通常处于三种可能的状态。
若是从全部的根节点都没法访问到某个对象,说明对象己经再也不使用了。通常来讲,此对象须要被回收。
但事实上,也并不是是“非死不可”的,这时候它们暂时处于“缓刑”阶段(关入大牢)。一个没法触及的对象有可能在某一个条件下“复活”本身,若是这样,那么对它当即进行回收就是不合理的
为此,定义虚拟机中的对象可能的三种状态。以下:
以上3种状态中,是因为finalize()方法的存在,进行的区分。只有在对象两次标记,不可触及时才能够被回收。
上一节已经说到, 断定一个对象objA是否可回收,至少要经历两次标记过程:
使用代码证实上述观点
下面的代码中, 使用类变量做为 GC Roots ,而且在对象回收时,在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(); } } }复制代码
打印:
第1次 gc obj is dead 第2次 gc obj is dead复制代码
说明此对象在第一次 gc时直接就回收了
打印:
调用当前类重写的finalize()方法 第1次 gc obj is still alive 第2次 gc obj is dead复制代码
在第一次gc时 ,调用了 finalize 方法,并又从新使类变量指向本身,复活
可是在第二次gc 时,发现finalize 方法 就没有再执行了,直接被回收
本节将介绍使用各个工具查看 GC Roots 集合
获取 dump 文件
jvm的一个内存快照,能够被各个软件分析,
下面将 演示如何将正在运行的 程序导出 dump文件
public class GCRootsTest { public static void main(String[] args) { List<Object> numList = new ArrayList<>(); Date birth = new Date(); for (int i = 0; i < 100; i++) { numList.add(String.valueOf(i)); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("数据添加完毕,请操做:"); new Scanner(System.in).next(); numList = null; birth = null; System.out.println("numList、birth已置空,请操做:"); new Scanner(System.in).next(); System.out.println("结束"); } }复制代码
先将程序跑起来,将阻塞
方式一:命令行使用jmap
方式二:使用JVisualVM
第一步: 选中监视tab, 点击堆 Dump
第二步: 右击另存为
第三步: 将上面程序 键盘输入,继续执行, 捕获第二个快照
这样咱们就获取到了两个 内存快照, 一个是被局部变量引用的,一个是释放掉的
如何使用MAT 查看堆内存快照
打开 MAT ,选择 File --> Open Heap Dump, 选择 须要查看的Dump文件
选择 Java Basics --> GC Roots
前后查看两个快照, 因为 局部变量再也不引用对象, 因此不在是GC Roots
释 放后:
不用 dump文件, 查看实时的 运行时程序
查看当前程序中堆中最多的对象类型,并查看其GC Roots
点击 :Live Memory --> All Object ,查看 堆中最多的对象, 并右击 ,点击Show Selection In Heap Walker
在显示界面 选择 References tab,查看堆中该类型的全部实例, 而后能够选中某一个对象,选择 Incoming References 选项, 再点击 Show Paths To FC Roots 按钮,弹出框点击确认
而后就能够看到 选中的对象的GC Roots , 例以下面的案例中, 字符串 "添加完毕,请操做" 对象 的 GC Roots 就是 out 对象, 由于被 System.out.println("添加完毕,请操做")打印
使用JProfiler 分析OOM
代码:
public class HeapOOM { byte[] buffer = new byte[1 * 1024 * 1024];//1MB public static void main(String[] args) { ArrayList<HeapOOM> list = new ArrayList<>(); int count = 0; try{ while(true){ list.add(new HeapOOM()); count++; } }catch (Throwable e){ System.out.println("count = " + count); e.printStackTrace(); } } }复制代码
上面的代码 将会致使OOM, 能够开启jvm指令,在出现OOM 时 自动生成 dump文件
运行程序,jvm指令: -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
输出日志: 出现了OOM,生成的dump文件在 工程目录下
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid14608.hprof ... java.lang.OutOfMemoryError: Java heap space at com.atguigu.java.HeapOOM.<init>(HeapOOM.java:12) at com.atguigu.java.HeapOOM.main(HeapOOM.java:20) Heap dump file created [7797849 bytes in 0.010 secs] count = 6复制代码
打开JProfiler, 能够在 超大对象 里面找到它
也能够 查看出现OOM的线程:
上面第一节中, 说到了如何标记垃圾,那么下面就开始清除垃圾,关于清除垃圾,也有不一样的算法
目前在JVM中比较常见的三种垃圾收集算法是
标记-清除算法(Mark-Sweep)是一种很是基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并并应用于Lisp语言。
执行过程
当堆中的有效内存空间(available memory)被耗尽的时候,就会中止整个程序(也被称为stop the world),而后进行两项工做,第一项则是标记,第二项则是清除
**标记:**垃圾收集器从引用根节点开始遍历,标记全部被引用的对象。(这里是标记不是垃圾的对象)
**清除:**垃圾收集器对堆内存从头至尾进行线性的遍历,若是发现某个对象在其Header中没有标记为可达对象,则将其回收
流程示意图, 先从跟节点 找出全部的 可达对象, 标记为"绿色",再遍历整个对象列表,将没有标记为绿色的清除
何为清除?
这里所谓的清除并非真的置空,而是把须要清除的对象地址回收,保存在空闲的地址列表里。下次有新对象须要加载时,判断垃圾的位置空间是否够,若是够,就覆盖原有的地址。 (跟电脑硬盘的删除同样)
关于空闲列表是在为对象分配内存的时候提过:
若是内存规整
若是内存不规整
标记-清除算法的缺点
标记清除算法的优势很明显, 简单 易理解 易于实现,可是缺点也很明显
核心思路:
将存放对象的内存空间分为两块,每次只使用其中一块,在垃圾回收时,垃圾回收器也从跟节点开始遍历,找到全部的可达对象,可是此时不标记, 而是直接将此对象复制到未被使用的内存块中,以后全盘清除正在使用的内存块中的全部对象,交换两个内存的角色,最后完成垃圾回收
示意图:
把可达的对象,直接复制到另一个区域中复制完成后,from区里面的对象就没有用了,新生代里面就用到了复制算法
复制算法的优缺点
优势
缺点
注意事项
若是系统中的垃圾对象不少,复制算法须要复制的存活对象数量并不会太大,效率较高,可是若是垃圾对象很是少的状况, 每次拷贝都几乎所有拷贝了,而后清除也就清除了个寂寞,
因此在jvm 中新生代中, 因为垃圾回收频率高,数量多,一次一般能够回收70% - 99% 的内存空间 ,回收性价比很高。因此如今的商业虚拟机都是用这种收集算法回收新生代。
复制算法的高效性是创建在存活对象少、垃圾对象多的前提下的。这种状况在新生代常常发生,可是在老年代,更常见的状况是大部分对象都是存活对象。
若是依然使用复制算法,因为存活对象较多,复制的成本也将很高。所以,基于老年代垃圾回收的特性,须要使用其余的算法。
标记-清除算法的确能够应用在老年代中,可是该算法不只执行效率低下,并且在执行完内存回收后还会产生内存碎片,因此JVM的设计者须要在此基础之上进行改进。标记-压缩(Mark-Compact)算法由此诞生。
1970年先后,G.L.Steele、C.J.Chene和D.s.Wise等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。
执行流程:
示意图:
标记-压缩算法与标记-清除算法的比较
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,所以,也能够把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
两者的本质差别在于
并且在对象分配内存时能够看到,若内存区域是零散的,须要访问空闲列表(标记-清除算法回收地址到空闲列表)
可是若是使用标记-压缩算法, 标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当咱们须要给新对象分配内存时,JVM只须要持有一个内存的起始地址便可,这比维护一个空闲列表显然少了许多开销。
是否移动回收后的存活对象是一项优缺点并存的风险决策。
标记-压缩算法的优缺点
优势
缺点
三种算法的横纵对比:
标记清除
标记整理
复制
速率
中等
最慢
最快
空间开销
少(但会堆积碎片)
少(不堆积碎片)
一般须要活对象的2倍空间(不堆积碎片)
移动对象
否
是
是
总结: 没有最好的算法,只有最适合的算法
前面全部这些算法中,并无一种算法能够彻底替代其余算法,它们都具备本身独特的优点和特色。
分代收集算法应运而生。他的目标不是替换上面的算法,而是具体问题 具体对待
分代收集算法,是基于这样一个事实:不一样的对象的生命周期是不同的。所以,不一样生命周期的对象能够采起不一样的收集方式,以便提升回收效率。
通常是把Java堆分为新生代和老年代,这样就能够根据各个年代的特色使用不一样的回收算法,以提升垃圾回收的效率。
在Java程序运行的过程当中,会产生大量的对象,其中有些对象是与业务信息相关:
目前几乎全部的GC都采用分代收集算法执行垃圾回收的
每一个代的各个特色和适合的回收算法
年轻代(Young Gen)
老年代(Tenured Gen)
简单介绍CMS 回收器
上述现有的算法,在垃圾回收过程当中,应用软件将处于一种Stop the World的状态。在Stop the World状态下,应用程序全部的线程都会挂起,暂停一切正常的工做,等待垃圾回收的完成。
若是垃圾回收时间过长,应用程序会被挂起好久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接致使了增量收集(Incremental Collecting)算法的诞生。
基本思路:
使用这种方式,因为在垃圾回收过程当中,间断性地还执行了应用程序代码,因此能减小系统的停顿时间。
缺点:
由于线程切换和上下文转换的消耗,会使得垃圾回收的整体成本上升,形成系统吞吐量的降低。
注意,这些只是基本的算法思路,实际GC回收器过程要复杂的多,目前还在发展中的前沿GC都是复合算法,而且并行和并发兼备。 因此这里以为模糊的,到后面把各个GC 回收器的实现说明完,就清晰了.