JVM垃圾回收机制

JVM垃圾回收介绍

在 Java 开发中,开发人员是无需过度关注对象的回收与释放的,JVM 的垃圾回收机制可以减轻不少工作量。但完全交由 JVM 回收对象,也会增加回收性能的不确定性。在一些特殊的业务场景下,不合适的垃圾回收算法以及策略,都有可能导致系统性能下降。

垃圾回收机制

1. 回收发生区域

JVM 的内存区域中,程序计数器、虚拟机栈和本地方法栈这 3 个区域是线程私有的,随着线程的创建而创建,销毁而销毁;栈中的栈帧随着方法的进入和退出进行入栈和出栈操作,每个栈帧中分配多少内存基本是在类结构确定下来的时候就已知的,因此这三个区域的内存分配和回收都具有确定性。

那么垃圾回收的重点就是关注堆和方法区中的内存了,堆中的回收主要是对象的回收,方法区的回收主要是废弃常量和无用的类的回收

2. 对象被回收时机

一般一个对象不再被引用,就代表该对象可以被回收。目前有以下两种算法可以判断该对象是否可以被回收。

引用计数算法:这种算法是通过一个对象的引用计数器来判断该对象是否被引用了。每当对象被引用,引用计数器就会加 1;每当引用失效,计数器就会减 1。当对象的引用计数器的值为 0 时,就说明该对象不再被引用,可以被回收了。这里强调一点,虽然引用计数算法的实现简单,判断效率也很高,但它存在着对象之间相互循环引用的问题。

可达性分析算法:GC Roots 是该算法的基础,GC Roots 是所有对象的根对象,在 JVM 加载时,会创建一些普通对象引用正常对象。这些对象作为正常对象的起始点,在垃圾回收时,会从这些 GC Roots 开始向下搜索,当一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可用的。目前 HotSpot 虚拟机采用的就是这种算法。
Java中可以作为GC Root的包括下面几种:

  • 虚拟机栈中的引用对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

以上两种算法都是通过引用来判断对象是否可以被回收。在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为了以下四种:
在这里插入图片描述

GC算法

1.标记-清除算法

标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行垃圾回收

这种算法实现起来比较容易,但是会造成内存碎片

2.标记-复制算法

复制算法是为了解决标记-清除算法的缺陷而提出的。

它将内存划分为大小相等的两块,每次只使用其中的一块。当这A快内存用完了,就将还存活的对象复制到B块上面,然后把A块的内存空间一次性清理掉

这种算法虽然实现简单,运行高效且不易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能使用的空间缩减为原来的一半。很显然,复制算法的效率跟存活对象的数量有很大关联,若存活对象很多,那么效率将大大降低

3.标记-整理算法

该算法是为了解决复制算法的缺陷,充分利用内存空间而提出的。

该算法与标记-清除算法一样,但是在完成标记后,不直接清理可回收对象,而是将存活对象全部向一端移动,接着清理掉边界以外的内存。

4.分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。其核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。

将其分为年轻代、老年代和永久代。然后根据不同的区域采用合适的收集算法。

Java一般将堆区分为年轻代和老年代,将方法区划为永久代。

年龄代

下面对不同的年龄代进行简单说明

年轻代:新创建的对象都存放在这里。因为年轻代会频繁的进行GC清理,JVM在年轻代采用的是标记-复制算法,先标记出存活的实例,然后清除掉无用实例,将存活的实例根据年龄(每个实例被经历一次GC后年龄会加1)拷贝到不同的年龄代。

老年代:老年代中是经历了N此垃圾祸首后仍然存活的对象,其中的N由JVM的参数决定。这块内存区域一般大于年轻代。GC发生的次数也比年轻代要少。

永久代:用于存放静态文件,如Java类、方法等。为方法区。

方法区主要回收的内容有:废弃的常量、无用的类,对与废弃常量可以同过引用的可达性判断,但是对于无用类需要同时满足以下3个条件:

该类的所有实例都已经被回收了
加载该类的 ClassLoader 已经被回收了
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

GC在什么时候触发

GC在优先级最低的线程中运行,一般在应用程序空闲时被调用。当内存不足时才会主动调用

因为对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有如下两种:

1.Scavenge GC

一般情况下,当新对象生成,并且在年轻代申请空间失败时,会触发Scavenge GC, 对年轻代进行垃圾回收。这种方式的GC不会影响到老年代。因为大部分对象都是年轻代开始的,同时年轻代内存不会分配的很大,所有年轻代的GC会频繁的进行。所以在这里要使用速度快、效率高的算法,使其空间尽快空出来。

若GC一次后仍不能满足内存分配,JVM会进行二次GC,若仍无法满足,则报“out of memory"的错误,Java应用将停止

2.Full GC

对整个内存进行整理,包括年轻代、老年代和永久代,所以Full GC比Scavenge GC要慢, 因此应该尽量减少Full GC的次数。以下可能引发Full GC的原因:

老年代被写满
永久代被写满
System.gc()被显示调用
上一次GC后堆的各域分配策略动态变化。

GC调优建议

  1. 不要显式调用System.gc()。此函数建议JVM进行GC,虽然只是建议,但是大多数 情况下会触发GC,增加了间歇性停顿的次数,大大影响系统的性能
  2. 尽量减少临时对象的使用。也就是减少Scavenge GC执行的机会
  3. 对象不用时最好显式置为null。将不用的对象置为null,有利于GC收集器判定,从而提高GC的效率
  4. 尽量减少静态对象变量。静态变量属于全局变量,不会被GC祸首。
  5. 能有基本类型的就不要用包装类。基本类型变量栈用的内存资源比对应的包装类要少的多
  6. 使用StringBuffer 而不是String类累加字符串。因为堆String类型进行加的时候,会创建新的String对象,而StringBuffer是可变长的,在原有基础上进行扩增,不会产生中间对象
  7. 分散对象创建或删除的时间。集中在短时间内大量创建新对象,特别是大对象,会突然需要大量内存,JVM在面临这种情况时只能进行GC,以回收内存或整合内存碎片,从而增加GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值