JVM 垃圾回收
- 如何判断哪些对象需要进行回收
- 垃圾收集算法
Java自动内存管路主要是针对对象的内存回收和对象的内存分配,其中Java自动内存管理中最核心的就是堆内存中对象的分配和回收
如何判断哪些对象需要进行回收
1.引用计数法
给对象中添加以恶搞引用计数器
- 每当有一个地方引用它时,计数器就加1
- 当引用失效的时候,计数器就减1
- 任何时候计数器为0的对象就是不可能在被使用的
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,最主要的原因就是它很难解决对象之间互相循环引用的问题。
像这样两个对象之间相互引用,对象的计数器都不会是0,就会造成内存泄漏。
- 任何时候计数器为0的对象就是不可能在被使用的
- 当引用失效的时候,计数器就减1
2.可达性分析算法
这个算法的基本思想就是通过一系列的“GC Roots”的对象作为起点,从这些加点开始向下进行搜索,节点所走过的路径被称为引用链,当一个对象到“GC Roots”没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
哪些对象可以作为 GC Roots 呢?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
即使不可达,对象也不一定会被垃圾收集器回收,
1)先判断对象是否有必要执行 finalize()方法,对象必须重写 finalize()方法且没有被运行过。
2)若有必要执行,会把对象放到一个队列中,JVM 会开一个线程去回收它们,这是对象最后一次可以逃逸清理的机会。
3.引用
无论是通过桔树算法判断对象的引用数量,还是通过可达性分析算法判断是否引用链可达,判断对象是否存活都和“引用”离不开关系。
在JDK1.2之前,Java中引用的定义很传统:如果reference类型的数据储存的数字数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。
JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种
- 强引用是最传统的“引用”定义,是指程序代码中普遍存在的引用赋值,即类似Object obj = new Object() 这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收点被引用的对象
- 软引用是用来描述一些还有用,但是非必须的对象,只要被软引用关联着的对象,在系统将要发生内存溢出的异常前,会把这些对象列入回收范围之中进行二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出的异常,在JDK1.2之后提供了SoftReference类来实现软引用。
- 弱引用也是用来描述那些非必须对象的,但是它的强度要比软应用更低一些,被弱引用关联的对象只能生存到下一次垃圾回收发生为止,当垃圾收集器开始工作时,无论内存空间是否足够,都只会回收掉被弱引用关联的对象。在JDK1.2之后提供了WeakReference类来实现软引用。
- 虚引用也称为“幽灵引用”或者“幻影引用”,他是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对线其生存时间造成影响,也无法通过虚引用来获得一个对象实例,为一个对象设置虚引用的唯一目的就是能够在这个对象被回收器回收时收到一个系统通知。在JDK1.2之后提供了PhantomReference类来实现软引用。
4.回收方法区
有些人认为方法区(如HotSpot虚拟机种的元空间或者永久代)是没有垃圾收集行为的,《Java虚拟机规范》种提到过可以不要求虚拟机在方法去中实现垃圾回收,事实上也确实为实现或者未能完整实现方法区类型卸载的收集器的存在(如JDK11时的ZGC收集器就不支持类卸载),方法区垃圾收集通常可以回收70%至99%的内存空间,相比之下,方法区回收拘于苛刻的判定条件其区域内的垃圾回收成果远低于此,
方法区的垃圾回收主要共分为两种:废弃的常量和不再使用的类型。回收废弃常量与回收Java种的对象非常类似,举个常量池的例子,假如一个字符串“java”曾进入常量池,但是系统中没有任何一个字符串对象的值是’java“,如果此时发生内存回收,而且垃圾回收器判断确有必要的话,这个java常量就会被系统清理出常量池,常量池中的其他类、接口、方法、字段符号的引用也与此类似。
如何判断一个类是无用的类
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
- 虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样使用了就会必然被回收。
垃圾收集算法
1.分代收集理论
当前商业虚拟机的垃圾收集器,大多都遵循了分代收集的理论进行设计,分代收集名为理论,实际是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上::
1)弱分代假说:绝大多数对象都是朝生夕灭的
2)强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
这两个分代收集假说奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收的对象依据其年龄(即熬过垃圾收集过程的次数)分配到不同的区域之中储存,显而易见,如果一个区域中的大多数对象都是朝生夕灭,难以熬过垃圾收集的过程的话,那么九八他们集中在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量的将要被回收的对象,就能以较低的代价回收到大量的空间。
2.标记清除算法
该算法分为标记和清除两部分:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象,它是最基础的收集算法,后续的算法都是对其不足进行改进得到的。这种垃圾收集算法会带来两个明显的问题:
1)效率问题
2)空间问题(标记后会产生大量不连续的碎片)
标记前
如果Java队中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量的标记和清除的动作,导致标记和清除的效率都随对象数量的增长而降低。
标记删除后
碎片就是多余的白色部分,明明可以放一个很大的对象,但是因为内存空间的不连续,导致内存不够。
解决办法
1.标记整理算法
根据老年代的特点提出来的一种标记算法,标记过程仍然与”标记清除“算法一样,但是后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象向一端移动,然后直接清理到边界以外的内存。
2.标记-复制算法
为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
这种算法实现简单、运行高效,不过其缺陷也显而易见,这种复制回收算法的d代价是将可用内存缩小为了原来的一半,空间浪费很大。
3.分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活的周期的不同将内存分为几块。一般将Java堆分为新生代和老年代这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
例如,在新生代中,每次收集都会有大量的对象死去,所以可以选择标记复制的算法,只需要付出少量对象的复制成本就可以完成每次的垃圾收集,而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择标记清除或者标记整理的算法吖来进行垃圾回收。