JVM学习(垃圾收集相关)
在上一个JVM学习中我们学习了JVM的内存区域,其中的程序计数器、虚拟机栈、本地方法栈随线程灭和生,还有栈中的栈帧随着方法的进入和退出有条不紊的出栈入栈,但每一个栈帧中分配多少内存基本上可以说是在类结构确定就已知的,所以这几个区域的垃圾回收暂且先不需要考虑。我们需要考虑的就是JVM堆和方法区的内容了。
我们只有在程序运行期间才知道会创建那些对象,而这些对象内存的分配和回收都是动态的,我们之后要讨论的垃圾回收也是指的这的。
1、对象已死吗?
这是我们要考虑的第一个问题,在JVM回收垃圾之前,我们必须也是不得不考虑这个问题,对象已死吗?(不会再被引用指到以及用到的对象)那么我们该如何确定这个问题呢?
接下来是两种判断对象是否还存活的方法,以及其他相关内容
①引用计数算法
有许多算法判断对象是否存活是这样的,给对象添加一个引用计数器,每当有一个地方引用它时,计数器值+1,每当一个引用失效时,计数器-1.当对象的计数器为0的时候,就表示对象是没有在被引用的了,这也是大多数人的看法。
但是这个算法也是有一个弊端,当两个对象进行相互引用的时候,那无论如何他们最终的计数器的值都为1,就比如Obja.value = objb ; Objb.value = obja;
但其实他们两已经是失去作用的对象了,那这个时候他们两又该怎么消除呢?从虚拟机的侧面垃圾收集可以看出,虚拟机可以对这两个对象进行回收,那也表示虚拟机不是使用这个算法,那让我们接着往下说
②可达性分析算法
这时候我们就接触到了可达性分析算法,这个算法的基本思路就是通过一个叫"GC Root根"的对象引用作为起点,一直往下进行搜索引用,所走过的路称为引用链,当一个对象GC Root根是没有任何引用到它的时候,则证明这个对象是废弃的,也就是可以回收的。这就是可达性分析算法。
就类似于上图的情况,GC root根左边有连接到对象1和对象2的对象链,但是对象3和对象4实现了上个情况的互相引用,可这时引用链并没有引用到他们,对象3和对象4就会被当做垃圾回收掉。这就是可达性分析算法。
③引用
引用在理解方面有那么一些困难,在这我就直接摘抄书上的内容。
强引用:强引用就是指在程序代码之中普遍存在的,类似直接的引用“Object obj = new Object();”这样的强引用,只要这样的强引用还在,垃圾收集器就不会回收掉被引用的对象
软引用:软引用描述一些还有用但是并非必需的对象。只有在系统将要发出内存溢出异常之前,才会将这些对象列入回收范围之中进行第二次回收,如果回收还没有足够的空间,才会抛出内存溢出异常。在JDK1.2之后提供了SoftReference类实现软引用
弱引用:弱引用也是描述一些非必须对象,但是强度比软引用更弱,被弱引用引用的对象只能生存到下一次垃圾收集之前,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后提供了WeakReference类实现软引用
虚引用:虚引用也称为幽灵引用或幻影引用,是最弱的引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象实例。唯一关联就是对象被收集器回收时会收到一个系统通知。
④生存还是死亡
在上文提到可达性分析算法,也并非是“非死不可”的,只能说这个时候处于一种即将执行死刑的时候,但要真正宣告一个对象死亡,还得经历两次标记过程,如果对象在被可达性分析进行“死亡标记”之后,这是第一次标记,并且再进行一次筛选,筛选的条件是这个对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法后,或者这个方法已经被调用过,JVM就不会管finalize方法直接二次标记了。
那么对象怎么在这第二次标记的finalize中拯救自己呢?在JVM判断对象有必要执行finalize这个算法,会将对象放入一个F-Queue的队列中,如果一个对象在finalize方法中执行缓慢,或者发生了死循环,将会导致其他的对象处于永久等待。finalize是用户逃脱死亡命运的最后一次机会,稍后GC将会对F-Queue中的对象进行第二次标记,如果对象想要拯救自己,就只能重新将自己与引用链中的对象建立关联,这样就会在第二次标记时候移除被回收的集合,逃离死亡的命运。
2、方法区需要回收吗?
很多人认为方法区是没有垃圾收集的,是不需要垃圾收集的,虽然JVM规范中确实说可以不要求JVM实现方法区的垃圾收集,并且效率以及性价比不是很高,但其实里面还是有回收的简单内容的。
永久代的垃圾回收主要分为两部分:废弃变量和无用的类,
废弃常量怎么判断呢?当常量池的字面量在当前系统中没有被任何对象引用的时候就代表这个常量是废弃的,就会当做废弃常量被清理出。
无用的类?需要同时满足下面三种条件才算无用类
①该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例
②加载该类的ClassLoader已经被回收
③该类对应的Class对象,已经没有在任何地方被引用,该类已经不可能被任何方法访问到。也就是无引用了。
当然在大量使用反射、动态代理、CGLib等ByteCode框架等以及自定义ClassLoader的场景都需要虚拟机具备类写在的功能,以保证永久代不会抛出溢出错误。
3、垃圾收集算法
3.1 标记-清除算法
最基础的收集算法是“标记-清除”算法,这个算法理解起来很简单,在内存区域中,在垃圾搜索的时候,将需要回收的垃圾统一进行标记,然后回收的时候对标记的垃圾进行清除,没标记的就是剩下的存活对象。
这个算法有两个缺点:
第一个是效率问题,标记和清除效率都不很高
第二个是空间问题,这样标记清除导致之后会产生大量的不连续的内存碎片,会在以后分配大对象的时候出现提前垃圾收集动作的隐患。
后来的算法大都是基于标记清除算法做出的改进
3.2 标记-整理算法
标记整理算法,大致过程大概与标记清除算法类似,但后续与它不一样,它是让标记与未标记的内容通过整理后分成两端将标记那一端直接全部清除,然后存活的都是在另一端,这样效率低低在标记整理,但是好处就是没有碎片化的空间内存了。
3.3 复制算法
复制算法,复制算法会将内存分成可用的大小相等的两块内存,但是每次只会用到其中一块,当一块内存垃圾处理完了,就把存活的移到另一块去,再完全清理另外一块。这样实现的算法有好有坏,好是效率高实现简单,并且不会出现碎片化。坏处是这样太浪费空间,可能执行的频率会非常高,基于效果以及效率,现在很多虚拟机都是采用这种方法来回收新生代。这点我们之后细讲。
3.4 分代收集算法
当前的虚拟机都采用的是“分代收集”算法,根据对象生存的周期或者是对象的大小将内存划分为不同的几块。一般的Java堆分为新生代和老年代。根据各个年代的特点采用最适当的算法。举个例子比如在新生代每次垃圾收集都有大量的对象死去,这样的话频率较高也需要高效率,我们就选用复制算法。而老年代因为对象存活率高,没有额外的空间进行担保,我们就根据情况选用标记清除或者标记整理算法来对老年代进行回收。分代收集算法只是一种思想不是具体的算法实现。
今天我们说的是对垃圾收集其中的算法机制以及如何判断对象是否还存活做了一定的介绍,后续我会给大家继续深入搬运垃圾收集器以及如何查看垃圾日志和小型的优化调整做出讲解。有哪里讲的不对或者有错误请大家指出,一起学习。