需要了解的问题:
1. 什么时候回收?
在cpu空闲的时候
堆内存满的时候
程序员主动调用System.gc();的时候
2. 如何判断该不该回收?
有两种方式:
① 引用计数法
简单的来说就是判断对象的引用数量。实现方式:给对象共添加一个引用计数器,每当有引用对他进行引用时,计数器的值就加1,当引用失效,也就是不在执行此对象时,他的计数器的值就减1,若某一个对象的计数器的值为0,那么表示这个对象没有人对他进行引用,也就是意味着是一个失效的垃圾对象,就会被gc进行回收。
但是这种简单的算法在当前的jvm中并没有采用,原因是他并不能解决对象之间循环引用的问题。
假设有A和B两个对象之间互相引用,也就是说A对象中的一个属性是B,B中的一个属性时A,这种情况下由于他们的相互引用,从而是垃圾回收机制无法识别。
②可达性算法
因为引用计数法的缺点有引入了可达性分析算法,通过判断对象的引用链是否可达来决定对象是否可以被回收。可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,通过一系列的名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连(就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。
对象不可达的时候一定被回收么?(对象被回收前的最后挣扎)
通过可达性分析:不可达的对象并不是立即被销毁,它还要经历两次标记,第一次标记判断对象是否覆盖了finalize()方法,如果没有,则直接进行第二此标记,进行回收
如果对象在finalize()方法中重新与它的引用建立了连接,那么将会逃离本次回收
那么问题又来了:什么是finalize()方法?
它是Object类提供的修饰符为protected(只给子类使用)的方法
源码 protected void finalize() throws Throwable { }
它的作用是,可以在类中覆盖,用于清理资源(但最好不要,因为finalize()方法不是100%被调用的)
缺点:不安全的,容易造成内存泄露OutOfMemoryErroy()
原因是:Finalizer线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐。所以最后会发生OutOfMemoryError异常。
用finalize的情况:
1不希望对象被jvm杀死;这么说是因为jvm在回收一个对象时,会先宣判对象死刑,然后finalize会被调用一次,仅有一次, 在这一次的机会里,对象可以完成救赎,救赎通过与GCRoot产生引用而继续存活下去。但是你真的有这样的需求么??(噗)
2当一个对象被回收时,但是一些文件类的流没有正确关闭,这时候可以在finalize中做出一道最后的防线,即验证是否有特殊的资源未关闭的情况。
**最终结论:**别用它,容易内存泄露,因为Finalizer线程永远追不上主线程(优先级低)
3. 如何回收
一共有四个算法:
① :标记-清除算法
② :复制算法
③ :标记整理算法
④ :分代收集算法
a. 标记-清除算法:
特点是简单但是容易造成很多内存碎片
原理是: 第一个步骤就是标记,也就是标记处所有需要回收的对象,标记完成后就进行统一的回收掉哪些带有标记的对象(对存活对象不做处理)
b. 复制算法:
特点是:不会有内存碎片,但是最主要缺点是会牺牲一半的内存,复制算法适用于存活对象不多的场景下,因为存活对象太多复制的就越多,效率降低
原理是: 复制将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
c. 标记-整理算法
特点是,不同于标记清除,它会对已存在的对象进行整理,使得不会有内存碎片生成
原理: 标记整理算法与标记清除算法很相似,但最显著的区别是:标记清除算法仅对不存活的对象进行处理,剩余存活对象不做任何处理,造成内存碎片;而标记整理算法不仅对不存活对象进行处理清除,还对剩余的存活对象进行整理,重新整理,因此其不会产生内存碎片。
d. 分代收集算法
特点是:集以上三种算法于一身,分情况调用这三种算法
原理:在java1.7之前,分为:①新生代②老年代③永久代(存放静态的一些东西,只在HotSpot虚拟机里面,其他虚拟机里是没有的)
根据这三代的特点来选择用什么算法
一. 新生代特点是存活对象的数量高,就选用复制算法
其次介绍一下新生代:新生代的目标就是快速回收那些生命周期短的对象,一般情况下:所有新生成的对象都是放在新生代的
二. 老年代中因为对象的存活率高,没有额外空间进行分配担保,所以选择标记-清除或者标记-整理算法
其次介绍下老年代:那些经历过N次垃圾回收仍然存活的对象就放在老年代中
三. 从jdk8开始,永久代替换为元空间,原因是:
- 字符串存在永久代中,容易出现性能问题和内存溢出
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
- 便于将 HotSpot 与 JRockit 合二为一(JRockit 中并没有永久代)
4. 要回收什么(哪些回收,哪些不回收)?
在jvm内存模型中,有三个不需要进行垃圾回收:程序计数器,JVM栈,本地方法栈.因为他们的声明周期和线程同步,随着线程销毁而销毁,内存自动释放,所以不需要回收,需要回收的是方法区和堆
那么问题又来了?
方法区如何判断是否需要回收
方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面3个条件:
① 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
② 加载该类的ClassLoader(类加载器)已经被回收;
③ 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。