JVM总结三:垃圾回收策略
确定回收区域
在JVM的内存模型分为:程序计数器,JAVA虚拟机栈,本地方法栈,堆,方法区。
程序计数器,java虚拟机栈和本地方法栈都是每个线程专有的三个区域,但是这三个区域是确定的,会随着线程的创建而创建,会随着线程的消亡而消亡。java虚拟机栈和本地方法栈,他们会随着方法的开始而入栈,方法的结束而出栈,并且每个栈帧的本地变量都是在类被加载时就被确定的。所以对于垃圾收集器会明确的知道什么时候回收这几个区域的垃圾,故这三个区域也不是我们操心的。
关于堆和方法区:堆和方法区在内存空间中,是唯一存在的,并且在多个线程中是共享的,在JVM开始时创建,一直等到JVM停止。因此,他们没有办法想前三个一样可以随着线程的创建而创建,随着线程的消亡而消亡。关于JVM垃圾回收主要考虑的是堆和方法区。
确定回收的对象
堆:确定无效对象的方法
在对堆进行内存回收前,首先判断哪些对象是无效的(因为堆里只存放类对象)。我们知道,当一个对象不被任何变量或者对象引用,那么就是无效对象,需要被回收。一般有两种方法。
- 引用计数法:每个对象都有一个计数器,当这个对象被其他变量或者另外一个对象引用时,那么该计数器就+1,若这个对象的引用被取消就-1;当计数器为0时,那么说明该对象是无效的。
- 可达性分析法:所有和GC Roots直接或间接关联的对象是有效对象,和GC Roots没有关联的对象就是无效对象。
- GC Roots指:
- java虚拟机栈所引用的对象。
- 方法区中的静态变量所引用的对象。
- 方法区中的常量所引用的对象。
- 本地方法栈所引用的对象。
- GC Roots并不包含堆中对象所引用的对象,这样就不会出现循环引用了。
- GC Roots指:
两者对比:引用计数器虽然简单,但存在一个严重的问题,它无法解决循环引用的问题,因此,目前主流的语言都是使用可达性分析来判断对象是否有效。
堆:回收过程
当JVM筛选出失效的对象之后,并不是立即清除,而是给对象一个重生的机会,但是过程如下:
- 判断该对象是否覆盖finalize()方法。①:若已经覆盖了该方法,并该对象还没有执行这个方法,那么将finalize()扔到F-QUEUE队列中。②:若未覆盖,则直接释放对象内存。
- 执行F-QUEUE队列中的finalize()方法。虚拟机会以较低的优先级去执行这些finalize()方法们,也不会确保所有的finalize()方法都会执行结束。如果finalize()方法中出现耗时操作,虚拟机就直接停止执行,将该对象清除。
- 对象重生或死亡。如果在执行finalize()方法时,将this赋值给了某一个引用,那么该对象就重生了。如果没有,那么就被垃圾收集器清除了。
强烈不建议使用finalize()函数进行任何操作,如果需要释放资源,那么使用try-finally,因为finalize不确定太大,无法保证顺利执行。
方法区
如果使用复制算法实现堆的内存回收,堆会被分为新生代和老年代,新生代的对象“朝生夕死”,每次垃圾回收都会清楚大量的对象;而老年代中的对象生命较长,每次垃圾回收只有少量的对象被清除掉。
由于方法区中存放的生命周期较长的类信息,常量,静态变量,因此方法区就像是堆的老年代,每次垃圾回收都只有少量的垃圾被清除。
方法区主要清除两种垃圾:①废弃常量,②废弃的类。
如何判断废弃的常量:
清除废弃的常量和清除废弃的类一样,只要常量池中的常量不被任何常量或者变量引用,那么这个常量就会被清除掉。
如何废弃废弃的类
- 该类的所有对象已经被清除
- 该类的java.lang.Class对象没有被任何对象或者变量引用。
- 加载该类的ClassLoader已经被收回。
垃圾回收算法
- 标记-清除算法:首先确定哪些是要清除的垃圾,先标记,然后清除。这种算法标记和清除过程效率低,而且清除完成后存在大量的碎片空间,导致无法储存大对象,降低了空间利用率。
- 复制算法:将内存分为两块;只将数据存储在其中的一块上。当要回收垃圾时,首先标记废弃的数据,然后将有用的复制到另外一块上,然后将这一块全部删除。这种算法虽然避免了碎片空间,但内存被缩小了一半。而且每次都会将有用的数据复制到另外一块去,效率不高。
解决空间利用率问题:
在新生代中,大量数据:都是“朝生夕死”,也就是一次垃圾回收后只有少量存活,因此我们将内存或分为三块:Eden、Survior1、Survior2,内存占比是8:1:1。分配内存时,只使用Eden和Survior1。当发现Eden+Survior1将要满了时,JVM会发起一次MinorGC,清除掉废弃的对象/数据,并且将所有存活下来的对象复制到Survior2中,那么,接下来就使用Survior2+Eden内存分配。
通过这种方式,只需要浪费10%的空间,实现带有压缩功能的垃圾收集方法,还避免了内存碎片问题。
但是当一个对象要申请内存空间时,发现Eden+Survior剩下的内存不能放置该对象,此时需要进行MinorGc,如果MinorGC过后空闲出来的内存空间仍然无法放置该对象,那么此时就需要将对象,那么将这个Enden+Survior的所有对象转移到老年代中。叫做“分配担保”。
- 标记-整理算法:在回收垃圾之前,首先将所有废弃的对象做出标记,然后将所有未做标记的移到一边,最后清空一边即可。这个算法是一种老年代的算法。老年代的对象一般寿命比较长,因此每次回收都有大量的存活对象,如果使用复制算法,就要复制大量存活对象,导致效率低。而且,在新生代中使用复制算法,当Eden+Survior中都装不下某个对象时,可以使用老年代的“分配担保”,而如果在老年代使用该算法,那么在老年代中如果出现Eden+Survior装不下某个对象时,没有其他区域给他做分配担保。因此老年代一般使用 标记-整理算法。
- 分代收集算法:将内存划分为老年代和新生代。老年代放寿命较长的对象,新生代放“朝生夕死”的对象,然后在不同的区域使用不同的垃圾回收算法。
java中引用的种类
强引用
我们平时所使用的引用就是强引用。
A a = new A();
也就是通过关键字new创建的对象所关联的引用就是强引用。
只要强引用存在,该对象永远也不会被回收。
软引用
只有当堆即将发生OOM异常时,JVM才会回收软引用所指向的对象。
软引用通过SoftReference类实现。
软引用的生命周期比强引用短一些。
弱引用
只要垃圾收集器运行,软引用所指向的对象就会被回收。
弱引用通过WeakReference类实现。
弱引用的生命周期比软引用短
虚引用
虚引用也叫幽灵引用,它和没有引用没有区别,无法通过虚引用访问对象的任何属性或函数。 一个对象关联虚引用唯一的作用就是在该对象被垃圾收集器回收之前会受到一条系统通知