酒后系列:酒后整理的JVM垃圾回收机制和内存分配策略

点击上方蓝色字体,选择“标星公众号”

优质文章,第一时间送达

99套Java企业级实战项目

4000G架构师资料

导语:公众号后续将推出酒后系列技术文,全是干货,希望不要喝醉哦!

在喝酒之前我们应该先来个开场白,想必大家已经看过了上一篇文章,那是我们第一次喝酒,想回顾的大佬们可以继续回顾一番!

接下来让我们再痛痛快快喝一顿吧,那么下酒菜肯定是少不了的,先上两个小菜。

一、引用计数法????

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就+1;当引用失效时,计数器值就-1;任何时候计数器为0的对象就是不可能再被使用的。

二、可达性分析计算????

通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路称为引用链,当一个对象到GC Roots 没有任何引用链时,则证明此对象是不可用的。

有的酒友们可能就不理解了,GC Roots 是什么玩意儿,喝一个我就告诉你????

1、虚拟机栈(栈桢中的本地变量表)中引用的对象 【方法中new() 的对象】

2、方法区中类静态属性引用的对象 【static A a = new A()】

3、方法区中常量引用的对象

4、本地方法栈中JNI(即一般说的Native 方法)引用的对象

来张图让酒友们更好的理解一下

吃完菜得干一杯了????

三、采用可达性分析算法,对象是生存还是毁灭????

Java采用的就是可达性分析算法来判定对象是否存活。

在可达性分析算法中不可达的对象,也并非是“必须毁灭”的。这时候他们处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在可达性分析后没有与GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize() 方法。当对象没有覆盖finalize() 方法,或者finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。如果这个对象有必要执行finalize() 方法,那么这个对象在执行这个方法的时候成功拯救自己——只要重新与引用链上的任意一个对象建立关系即可,例如把自己赋值给某个类变量或者对象的成员变量,那在第二次标记时,他就不会被回收。

垃圾收集算法

一、标记-清除算法????

最基础的收集算法,如同它的名字一样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它的主要不足有亮点:一个是效率问题,标记和清楚的两个过程的效率都不高;一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

二、复制算法????

为了解决效率问题,一种称为“复制”的收集算法出现了,它将可用内存分为大小相等的两块,每次只使用其中的一块。当这一块用完了,就把还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉,这样使得对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了一半,未免太高了一点。

三、标记-整理算法????

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

四、采用分代收集算法????

当前商业虚拟机的垃圾收集都采用“分代收集”算法,根据对象存活周期的不同,将内存分为几块。一般是把java堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理” 或者“标记整理” 算法进行回收。

内存的分配策略

对象的内存分配,往大方向上讲,就是在堆上分配,对象主要分配在新生代的Eden区,少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

一、对象在新生代的Eden 区中分配????

大多数情况下,对象在新生代的Eden 区中分配。当Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。用代码给酒友们演示一下

/**
 * 对象优先在Eden 区分配
 *
 * 虚拟机参数设置:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
 *  -verbose:gc 在控制台输出GC情况
 *  -Xms20M -Xmx20M -Xmn10M 限制Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下10MB分给老年代
 *  -XX:+PrintGCDetails 收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况
 * Created by lxs.
 * 2020/5/22 6:25 PM
 */
public class TestMinorGC {


    private static final int _1MB = 1024 * 1024;


    public static void main(String[] args) {
        byte[] obj1, obj2, obj3;


        obj1 = new byte[2 * _1MB];
        obj2 = new byte[2 * _1MB];
        obj3 = new byte[3 * _1MB];  // 发生Minor GC
    }


}

运行如下图所示:

第一个红框的意思是这次GC 的结果是新生代5650KB变为448KB

第二个红框总的5650KB变为4544KB,并没有减少多少,那是因为obj1,obj2两个对象是存活的,虚拟机几乎没有找到可以回收的对象。

第三个红框表示Eden区和Survivor from区和Survivor to区,内存大小比例为8:1:1,总和正好是10MB。

第四个红框为老年代10MB大小。

这次GC发生的原因是给obj3分配内存的时候,Eden区已经被占用了至少4MB,剩余空间不足矣分配obj3所需要的3MB内存,因此发生了Minor GC。GC期间虚拟机又发现已有的2个2MB大小的对象全部无法放入Survivor 空间,所以只好通过分配担保机制提前转移到老年代中。

这次GC结束后,3MB的obj3 对象分配在Eden 中,因此程序执行完的结果是Eden占用3MB(obj3占用),Survivor 空闲,老年代被占用4MB(obj1,obj2)。通过GC日志可以证明这一点。

二、大对象直接进入老年代????

/**
 * 大对象直接进入老年代
 *
 * 虚拟机参数设置:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:PretenureSizeThreshold=3145728
 *  -verbose:gc 在控制台输出GC情况
 *  -Xms20M -Xmx20M -Xmn10M 限制Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下10MB分给老年代
 *  -XX:+PrintGCDetails 收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况
 *  -XX:PretenureSizeThreshold=3145728 大于这个设置值的对象直接分配在老年代中,即大于3MB
 *  -XX:PretenureSizeThreshold 这个参数只对Serial和ParNew两款收集器有效,我的是用的Parallel Scavenge收集器,所以不顶用,我直接new了大一点的对象,7MB的直接分配到老年代了
 * Created by lxs.
 * 2020/5/22 6:25 PM
 */
public class TestBigObj {


    private static final int _1MB = 1024 * 1024;


    public static void main(String[] args) {
        byte[] obj;


        obj = new byte[7 * _1MB];
    }


}

运行如下图所示:

上述代码,创建的对象直接分配到老年代中去了,看第三个红框,obj对象7MB,老年代10MB,占用79%。但是我们再看前两个红框,Eden区并没有分配对象,可是还是占用了其中一部分内存,这是因为虚拟机本身加载的时候,可能有自己的内部对象,所以占用其中的内存了,说到这儿,第一段代码大家就应该可以理解了吧,三个对象加起来一共7MB,Eden区应该可以放的下呀,当然不是,加上虚拟机本身会占用的内存就不会了,所以就会触发Minor GC。

说到最后,大家一起干一个吧,干之前我要讲几句话,大家经常会听到Minor GC,Major GC,Full GC,这些都是什么意思呢,其实就是新生代GC,被称之为Minor GC;老年代GC,被称之为Major GC;Full GC是指新生代和老年代都进行回收,好了,大家举杯吧????

有热门推荐????

最受欢迎Java数据库访问框架(DAO层)

那些想要替代 C 与 Java的后浪,如今混怎样?

为什么单线程的Redis能够达到百万级的QPS?

干货:一文读懂客户端请求是如何到达服务器的

干货分享:扫码关注下面的公众号后台回复“99”领取99套实战项目+资料

想充电就关注序员闪充宝

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

猜你喜欢

转载自blog.csdn.net/qq_17231297/article/details/106435892