GC(JVM垃圾的垃圾回收机制)

JVM垃圾回收GC(Garbage Collection)

JVM的GC主要是对堆内存的回收,而栈是线程的,线程已结束它就没了,所以不用管。

按代实现垃圾回收

新生代(Young generation):

绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可到达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为”minor GC“。

minor GC触发的条件:当Eden区满时

JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例?一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。参数 -XX:NewRatio 设置老年代与新生代的比例。例如 -XX:NewRatio=8 指定老年代/新生代为8/1. 老年代占堆大小的 7/8 ,新生代占 1/8 .(默认即使1/8)
例如:-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8

老年代(Old generation):

对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中消失的过程,可以称之为”major GC“(或者”full GC“)

Full GC触发条件:

  1. 调用System.gc时,系统建议执行Full GC,但是不必然执行
  2. 老年代空间不足
  3. 方法区空间不足
  4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  5. 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

Full GC是清理整个堆空间的,包括新生代和老年代

GC时,程序都需要停顿的,这样系统才不会有新的垃圾产生,也可以更好的进行清除操作。不过停顿的时间不会长。

如果GC的效率不高,JVM会抛出OOM:GC overhead limit exceeded

一般虚机会检查几项:

  1. 花在GC上的时间是否超过了98%
  2. 老年代释放的内存是否小于2%
  3. eden区释放的内存是否小于2%
  4. 是否连续最近5次GC都出现了上述几种情况(注意是同时出现)

只有满足所有条件,虚机才会抛出OOM:GC overhead limit exceeded

永久代(permanent generation):

像一些类的层级信息,方法数据和方法信息(如字节码,栈和变量大小),运行时常量池(jdk7之后移出永久代),已确定的符号引用和虚方法表等等,它们几乎都是静态的并且很少被卸载和回收,在JDK8之前的HotSpot虚拟机中,类的这些“永久的”数据存放在一个叫做永久代的区域。永久代一段连续的内存空间,我们在JVM启动之前可以通过设置-XX:MaxPermSize的值来控制永久代的大小。但是jdk8之后取消了永久代,这些元数据被移到了一个与堆不相连的本地内存区域 。

思考:为什么要有新生代和老年代呢?
为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化GC性能。
你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

怎样判断对象是否已经死亡

垃圾收集的目的在于清除不再使用的对象。gc通过确定对象是否被活动对象引用来确定是否收集该对象。两种常用的方法是引用计数和对象引用遍历。

引用计数
引用计数存储对特定对象的所有引用数,也就是说,当应用程序创建引用以及引用超出范围时,jvm必须适当增减引用数。当某对象的引用数为0时,便可以进行垃圾收集。
当一个对象被创建时,且将该对象分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象+1),但当一个对象的某个引用超过了生命周期或者被设置为一个新值时,对象的引用计数减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。

这种方法要慢很多。

对象引用遍历
早期的jvm使用引用计数,现在大多数jvm采用对象引用遍历。通过一系列称为”GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(从GC Roots到此对象不可达),则证明此对象是不可用的。

GC Roots都有哪些:

  • 栈中所引用的对象(线程的本地变量表里存的对象)
  • 静态存储区(方法区)中的对象(包括常量、全局变量、静态数据)
  • 本地方法栈中JNI引用的对象(Native对象)

这种方法会快一些

垃圾回收的算法

基本的回收算法:
空间维度:标记-清除、标记-压缩、标记-复制、增量回收、分代回收
时间维度:串行回收、并发回收、并行回收

标记-清除算法:
标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

停止-复制算法:
这种收集算法将堆栈分为两个域,常称为半空间。每次仅使用一半的空间,JVM生成的新对象则放在另一半空间中。GC运行时,它把可到达对象复制到另一半空间,从而压缩了堆栈。这种方法适用于短生存期的对象,持续复制长生存期的对象则导致效率降低。并且对于指定大小堆来说,需要两倍大小的内存,因为任何时候都只使用其中的一半。

标记整理算法:
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往一端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。

分代收集算法:
在上边三种收集思想中加入了分代的思想。

JVM到底选择了哪种方法?
实际上,JVM采用了“自适应的、分代的、停止-复制、标记-清除”的算法。
重点在与自适应,JVM会跟踪标记-清除的算法,如果堆中出现许多碎片,会自动切换至停止-复制的算法;如果所有对象都很稳定,但是效率降低的话,JVM会自动切换至标记-清除的算法。

参考:https://www.jianshu.com/p/99772ad092d3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值