JAVA基础篇--JVM--6对象的回收算法

前言:
我们已经知道,类对象都存活在堆中,jvm 通过gc root 可达性分析来判断存活对象,那么对于这些没有存活对象jvm 会进行怎么回收呢?

本文通过以下几点进行探讨:
1 什么时候会垃圾回收 ;
2 怎们回收;
3 堆内存中不同区域使用什么垃圾回收算法;

1 什么时候会垃圾回收:
GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。
当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次垃圾回收,但是具体什么时刻运行也无法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决定。但是不建议手动调用该方法,因为GC消耗的资源比较大;
Gc 的时机:
JAVA基础篇--JVM--6对象的回收算法_第1张图片
1.1 Young 区(young gc):

  • 新产生的对象,分配在eden区,当Eden 空间不够,触发一次young gc 回收对象;
    然后回收后eden 中的内存就不连续了,这个时候将幸存的对象,放入到S0(能放下的情况下);
  • 再有新对象进入eden 中,随着对象增多,触发young gc ,这个时候回收eden 和S0 中的对象,回收完毕后,将eden 和s0 幸存的对象复制到S1(能放下的情况下)中,同时清空S0;
  • 再有新对象进入eden 中,随着对象增多,触发young gc ,这个时候回收eden 和S1 中的对象,回收完毕后,将eden 和s1 幸存的对象复制到S0(能放下的情况下)中,同时清空S1;
    S0 和S1 理论上大小是相等的,但是实际上会动态调整;S区中的某一个对象的计数达到阀值,则晋升至old gen。该阀值通过-XX MaxTenuringThreshold来设置。

1.2 Old 区(old gc/major gc):
Old 区的大小默认是eden 的2倍:
当Young gc 之后存货的对象超过old 区剩余空间 会触发old gc,清除old 区已经不存活的对象;
eg:
如果:Young gc 第一次到old 区20M ;第二次到old区30M;第三次到old 区20M ;
第三次Young gc时,old 区只剩下20M,触发old gc(此时触发young gc 的时候,old 区认为此次要到old区的大小为(20+30+20)/3 = 23.3M 大于20M,虽然实际上只有20M);

扩展:
1.2.1:major gc其实并不等于full gc:
full gc的定义是对young gen,old gen以及metaspace(永久代)的全局范围的一次gc。而major gc只针对于old gen。
但为什么外界大部分都会将major gc和full gc混为一谈呢。大抵是因为hotspot vm所实现的几种GC算法组合中,大部分触发major gc的同时也会带有full gc;
1.2.2:Old 区 担保机制:
内存分配是在JVM在内存分配的时候,新生代内存不足时,把新生代的存活的对象搬到老生代,然后新生代腾出来的空间用于为分配给最新的对象。这里老生代是担保人。在不同的GC机制下,也就是不同垃圾回收器组合下,担保机制也略有不同。在Serial+Serial Old的情况下,发现放不下就直接启动担保机制;在Parallel Scavenge+Serial Old的情况下,却是先要去判断一下要分配的内存是不是>=Eden区大小的一半,如果是那么直接把该对象放入老生代,否则才会启动担保机制。

2 怎们回收:
我们知道不同对象的生命周期长短不一样,大部分对象都是朝生夕死,那么jvm肯定不会一股脑的吧所有的对象使用同一种方式处理,肯定会进行细分工作,使用不同的算法进行垃圾回收,来追求更高的效率;

2.1 标记-清除(Mark-Sweep) 算法:
2.1.1 标记
找出堆中存活的对象,并且把它们存活的对象标记出来,此时堆中所有的对象都会被扫描一遍,比较耗时 :
JAVA基础篇--JVM--6对象的回收算法_第2张图片
2.1.2清除:
清除掉没有被标记为存活的对象,释放出对应的内存空间;
JAVA基础篇--JVM--6对象的回收算法_第3张图片
优点:
(1)法简单、容易实现 ;
(2)与保守式GC 算法兼容 ,清除算法不会移动对象,所以非常适合搭配保守式算法。
缺点:
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程
序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(1)标记和清除两个过程都比较耗时,效率不高 ;
(2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2.2 标记-复制(Mark-Copying)
将内存划分为两块相等的区域,每次只使用其中一块,如下图所示:
JAVA基础篇--JVM--6对象的回收算法_第4张图片
当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。
JAVA基础篇--JVM--6对象的回收算法_第5张图片
缺点: 空间利用率降低。
优点:效率高;
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果
不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都有
100%存活的极端情况,所以老年代一般不能直接选用这种算法。

2.3标记-整理(Mark-Compact)
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
其实上述过程相对"复制算法"来讲,少了一个"保留区":
JAVA基础篇--JVM--6对象的回收算法_第6张图片
让所有存活的对象都向一端移动,清理掉边界意外的内存。
JAVA基础篇--JVM--6对象的回收算法_第7张图片
优点:
(1)消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
(2)消除了复制算法当中,内存减半的高额代价。
缺点:
(1)从效率上来说,标记-整理算法要低于复制算法。
(2)移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。移动过程中,需要全程暂停用户应用程序。即:STW;

3 堆内存中不同区域使用什么垃圾回收算法:
既然上面介绍了3中垃圾收集算法,那么在堆内存中到底用哪一个呢?既然对象的生命周期长短不同,jvm 也将堆进行了进一步的划分:
JAVA基础篇--JVM--6对象的回收算法_第8张图片
划分的目的就是,对于生命周期长的对象分配到old区;对于新产生的对象分配到young区;jvm 对不同的区域使用不同的算法(分代收集算法)更好的回收对象;

  • Young区:对象在被分配之后,可能生命周期比较短,存活下来的对象比较少,Young区就可以使用复制效率比较高复制算法;

  • Old区:Old区对象存活时间比较长,存活的对象也比较多,使用复制算法反而效率低,使用标记清除或标记整理;

扩展:
jvm 中不同分区的内存分配比例:
在默认情况下,新生代 = 1/3的堆空间大小,老生代 = 2/3的堆空间大小;新生代被细分成Eden和两个survivor区域,这两个survivor区分别被命名为from和to。默认的Eden:from:to = 8:1:1

我们常见的配置,如下:
-Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m -XX:MaxTenuringThreshold=15

  • Xms表示jvm启动时的初始堆大小,
  • Xmx为最大堆大小,Xmn为新生代的大小
  • PermSize为永久带的初始大小,
  • MaxPermSize为永久代的最大空间
  • MaxTenuringThreshold为达到这个值,对象就会被移进老年代

你可能感兴趣的:(java基础篇,java)