JVM(四)——垃圾回收

一、GC垃圾回收

当我们在Java中new一个对象时,都会在堆上为该对象开辟一块内存。当该对象的引用作用域失效的时候,该对象就会成为GC垃圾回收器的目标。

GC垃圾回收器是由JVM专门的一个线程(垃圾回收线程)来实现的,其优先级比较低。因此,当有对象不被引用时,GC是不会立刻回收该对象的使用内存的。为了让GC线程执行时更快地知道某对象是否能够被立即回收,可以在该对象不再使用的时候将它的引用置为null,这样可以帮助GC及时判断该对象不再被使用,就可以立即回收掉。

Object类中有一个finalize()方法,可以在Java自定义类中重写finalize()方法。当对象被GC回收之前,会先调用对象的finalize()方法,释放相关资源,然后在GC的下一个回收周期再把该对象的内存回收掉。

二、内存泄漏

(1)static成员变量引用其他对象。static域的成员在方法区,生命周期同Java进程的生命周期。而该static成员对象又引用了其他对象,导致在一个直接引用链或者间接引用链上的所有对象都无法被GC回收。

(2)自定义集合时,如果从逻辑上集合的某个对象不再使用时,应立刻将该对象的引用置为null,否则该对象无法回收。

(3)其他IO流(File、Socket、数据库Connection、Statement、ResultSet等对象)打开使用完却没有关闭,也会造成内存资源泄漏的问题。

(*)排除Java内存泄露的工具:MAT

三、GC怎么检测对象是垃圾?

JVM检测对象是否可以被回收,主要有两种方法:

(1)引用计数法

给每一个创建好的对象分配一个引用计数器,用来存储该对象被引用的个数。当引用个数为零时意味这个对象不会再被使用,可认为该对象“死亡”。

这种方法无法检测“循环引用”。即当两个对象互相引用,即使他俩都不被外界任何东西引用,它们的计数都不为0,所以永远不会被回收。

(2)可达性分析法

可达性分析法是目前主流语言中采用的对象存活判断方案。

基本思路是把所有引用的对象想象成一棵树,从树的根节点GC Roots出发,持续遍历找出所有连接的树枝对象,这些对象为“可达”对象或“存活”对象。其他的对象则被视为“死亡”的、“不可达”的对象,称为“垃圾”。

GC Roots本身一定是“可达”的,这样从它们出发遍历的对象才能保证一定“可达”。

Java中一定“可达”的对象(4种):

(1)虚拟机栈(帧栈的本地变量表)中引用的对象。

(2)方法区中静态属性引用的对象。

(3)方法区中常量引用的对象。

(4)本地方法栈中JNI引用的对象。

四、GC回收垃圾的算法?

(1)标记/清除算法——最基础的收集算法

1、标记/清除算法的基本思想:

分为“标记”和“清除”两个阶段。首先标记出所有需要回收的对象,在标记完成后同一回收所有被标记的对象。

标记阶段:标记的剁成同可达性分析算法的过程。遍历所有的GC Roots对象,从GC Roots对象可达的对象表,都打上一个标识。将其记录为可达对象,一般在对象的header中。

清除阶段:清除的过程是对堆内存进行的遍历,通过读取对象的header信息,如果发现某个对象没有被标记为可达对象,则将其回收。

在垃圾收集器进行GC时,必须停止所有Java执行线程。因为在标记阶段进行可达性分析时不可以出现分析过程中对象的引用关系还在变化的情况,否则可达性分析的结果就无法保证准确。在“标记/清除”结束后,应用线程才会恢复运行。

2、缺点:

<1>效率问题:标记和清除两个阶段的效率都不太高。因为这两个阶段都需要遍历内存中的对象,大多时间内存中的对象实例数目庞大,很耗费时间。且GC时需要停止应用程序,也会导致很差的用户体验。

<2>空间问题:标记清除之后会产生大量的不连续的内存碎片。内存碎片太多可能会导致在之后的程序运行中分配较大对象时,无法找到足够的连续内存,不得不提前触发再一次的垃圾回收动作。

(2)复制算法——解决效率问题

1、复制算法的原理:

将可用的内存容量划分为大小相等的两块,每次使用其中一块。当这一块的内存用完以后,就将还存活的对象复制到另一块内存上,然后把这一块内存中的所有对象一次性全部清理。

复制算法每次都是对整个半区进行内存回收,这样减少了标记对象的遍历时间。在清除使用区域对象时也不用再次遍历,直接清空区域内存。并且在将存活对象复制到保留区域时,也是按地址顺序存储的,这样就解决了内存碎片的问题。在之后的分配对象内存时就不用考虑内存碎片等复杂问题,只按照顺序分配内存即可。

2、缺点:

<1>将内存缩小到原来的一半,浪费了一半的内存空间,代价太高。

<2>当对象的存活率很高时,将存活对象复制的时间代价太大。

(3)标记/整理算法

1、标记/整理算法的原理:

标记/整理算法的标记过程同标记/清除算法,但后续步骤是让存活的对象都向一端移动,然后直接清理掉端边线以外的内存。

回收之后,可回收对象被清理掉,存活的对象按规则排列存放在内存中。这样在给新对象分配内存时,JVM只需要持有内存的起始地址即可。

标记/整理算法不仅弥补了标记/清除算法存在的内存碎片的问题,也消除了复制算法中内存减半的代价。

2、缺点:

效率也不高:不仅要标记存活对象,还要整理存活对象的引用地址,在效率上不如复制算法。

(4)分代收集算法——终极算法

分代收集算法结合了前几种算法的优点,将算法组合使用,进行垃圾回收。

1、分代收集算法的思想:

按对象的存活周期不同,将内存划分为几块,一般是把Java堆分为新生代和老年代,这样根据各个年代的特点采用最合适的收集算法。

新生代:朝生夕灭,存活时间很短。

老年代:经过多次Minor GC而存活下来,存活周期长。

在新生代中每次垃圾回收都有大量的对象死去,只有少量存活,只需要付出少量对象的复制成本就可以完成收集,因此采用复制算法。而老年代中对象的存活率高,不适合采用复制算法,因此必须使用标记/清除或者标记/整理算法收集老年代。

(*总结)以上几种收集算法的共同特点是:

当GC线程启动时,应用程序都要暂停。

五、GC流程

将内存分为一块较大的Eden空间和两块较小的From Survivor空间、To Survivor空间,三者的比例为8:1:1。每次使用Eden区和From Survivor区,To Survivor区作为保留空间。

GC开始时,对象只会存在于Eden区和From区,To Survivor区为空。

GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区。From Survivor区中仍然存活的对象会根据它们的年龄决定去向:

<1>年龄值达到年龄阈值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值+1)的对象会被一袋老年代中。

<2>没有达到阈值的对象会被复制到To Survivor区。

接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。

接着From Survivor区和To Survivor区会交换它们的角色。即上次GC清空的From Survivor区成为新的To Survivor区,上次GC清空的To Survivor区成为新的From Survivor区。

总之,不管怎样都会保证一轮GC后,To Survivor区是空的。当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值