jvm_垃圾收集算法讲解(一)

GC本质上就是垃圾回收的概念,而是JVM帮你去做这个事情,刚才我们看的代码是rabbitmq内部帮你实现的主从选举包括投票,包括分发,包括连接重新建,但是如果后期我们讲RocketMQ,或者是KAFKA的时候,那个事情咱们就不需要做了,KAFKA是用zookeeper帮你去协调了,然后rocketmq是用nameserver的机制自动帮你协调了,当然每种MQ都有自己的优势,市面上最好的是之前讲过的activemq,然后性能比较好的我们之前也研究过ZeroMQ,虽说他的性能比较好,断点就挂了,KAFKA也是一样的,但是都是基于内存的,其实要保证高可用,数据不丢失,那无非是rabbitmq和RocketMQ这种方案,由于RocketMQ已经闭源了,如果你想实现数据不丢失,恢复能力也特别强,只要你整个集群别爆炸了,就算是恢复不了了,也照样可用,所以保证可靠性,尤其是我们做支付这块,所以要选的话只能选Rabbitmq,这是一个技术选型,垃圾回收有很多种算法,最古老的引用技术法,标记压缩法,复制算法,分代,分区的思想
引用计数法: 这是比较古老而经典的垃圾收集算法,其核心就是对象被其他所引用时计数器加1,而引用失效的时候减1,但是这种碰到循环引用的时候就无法处理了,这个对象被引用就加1,失去引用就减1,但是有一种是循环引用递归,父类引用子类对象,JAVA基础的API操作的时候,包括有一种继承,引用来引用去的,这种计数法就有bug,而且每次加减的时候要保证对象的原子性,内部肯定有算法的保证,性能也不是特别好,有的时候加加减减就成负数了,然后在古老一点的就是标记清除法.标记清除法:就是把标记和清除分为两个阶段去做,子啊内存中是两个阶段去操作这个对象,当然这种方式也是有非常大的弊端的,就是存在空间碎片的问题,垃圾回收后空间不是连续的,不连续的内存空间的工作效率低于连续的内存空间,这种方式叫做标记清除法,比如在内存中有好多好多的内存对象,然后现在我就采用标记清除法去做,我把所有被标记的元素,这四块东西统一都被我标记,我的GC就去找被标记的对象,去进行一个clear的操作,但是会有一个问题,这种JVM垃圾收集包括打印这种东西,它不是非常非常准确的,标记清除法是什么概念呢,正常的时候清除的时候会把这4M内容给释放,但是GC的时候不会百分百的去清空这1M的空间,它可能清空0.9999M,这个咱们有目共睹的,有很多东西并不是那么绝对的,尤其是清理内存这一块,所以它会存在一个内存的碎片,这里就会有一个盲点,没法去存数据,就造成内存不连续,所以这是比较早期的两种方式,一种是计数法,还有一种就是标记清除法,这两种方式都不是特别好的,由于说有这么两种方式,只有之前的不好的,所以你才会有一种更好的方案,之后就有两套比较不错的方案,第一种不错的方案是复制算法,第二种不错的方案是标记压缩法,这两种方案它分别应用在JVM的新生代和老年代中
复制算法:核心思想就是将内存分为两块,每次只使用其中一块,在垃圾回收的时候,将正在使用的内存对象复制到未被使用的内存块中去,然后把第一块给清除,然后反复去交换两个角色,去完成垃圾的收集,这种复制算法是非常好的,JAVA中新生代的from和to空间就是使用这种算法,或者叫s0和s1区,相互转换角色.标记压缩法:它是在标记清除的方法之上,做了一个优化,他叫做标记压缩法,其实更全面的说叫做标记压缩清除法,把存活的对象压缩到内存的一端,然后进行垃圾清理,这样就好玩了,这样就解决了之前的问题了,原先有两块对象,把所有被标记的对象压缩到另一端,然后我就划清界限,然后我把这块整体的去clear,这样就不会产生空间碎片的问题,标记完了压缩到一端,然后再整体清空,为什么新生代和老年代使用不同的算法,为什么新生代使用复制算法老年代使用标记清除压缩法,你想一想它为什么要这么去做,为什么新生代使用复制算法,而老年代使用标记压缩法,为什么JVM分成两块,为什么新生代和老年代不都使用复制算法,或者都是用标记压缩法,原因是新生代它太频繁了,新生代它里面的对象死的快,存活的少,比如100个对象只有3,4个存活的,剩下的都是要被GC的,所以要大块的空间转换,整体的clear,并不是一条条的delete,所以性能是比较高的,而老年代的大部分都经过了老年代的GC了,老年代比新生代要高出太多了,至少是15个等级的,经历过15次GC的,所以老年代要经过清空的时候,一般都是很少很少的一部分,100个可能清空一两个,内存操作尽量最少化一些,来提高整个JVM的性能,这个就是关于垃圾回收算法比较经典的
其实刚才所说的就是一个分代的算法,把不同的算法去提升我们整体的内存的性能了,对于新生代和老年代来讲,他就是一个分代的算法,新生代回收的频率比较高,每次耗时都很短,老年代回收的频繁非常低,但是耗时很长,所以我们要尽量的去减少老年代的GC,老年代的GC会严重的影响你程序的性能,我们新生代和老年代一划分,是很重要很关键的一个问题,新生代怎么划分,新生代和老年代怎么区划分,这个是和JVM调优,和系统的性能是有很大的影响的,所以所很重要,并不是你代码写好了应用程序不调优,代码优化多好怎样怎样,还是和整个的硬件,整个的JVM,整个的软件,跟你的访问量,跟等等一系列的因数,所以你如果想做一个负责人的话,但是现在其实分的很细,咱们有运维,有架构师,各司其职,对于分区算法,ORACLE收购JAVA之后,去做了一个尝试分区算法:主要就是将整个内存分为N多个独立的空间,每个空间都可以独立的使用,这样细粒度的控制一次GC的时候回收一小块空间,就好像一个网格一样,如果你数据都存在在这块区域,我就整体的去清理这块区域,然后其他的不变,这样的好处是减少GC的停顿,比如新生代的话,你把数据copy到另一块的空间,GC的停顿的意思就是系统停顿,GC介入系统停顿,你就说程序在这里跑的好好的,我系统级的JVM去垃圾收集,垃圾收集通知系统你得停一会,我这边把垃圾都收集玩之后自己去玩,如果GC只是介入内存中的一小块的话,那对于其他模块不影响的话,也让我们的系统性能也是比较好的,它是这么一种做法,这有点想Map中的分段锁的概念,所以这块包括现在的JDK1.8来讲,他这种分区算法是应用了,但是性能并不见得特别特别好,都是不断的在改进,JDK1.9,JDK2.0的时候也许就成熟了这种算法,我们以后垃圾回收的事什么都不用管了,基本上什么也不用管了,而且JVM参数什么都可以去调整,JVM的架构整体都采用分区的话,你就不用考虑调优的事了,人家内部就帮你完成了
GC的停顿,是GC介入我们的系统,要理解这个停顿是什么概念,垃圾收集器的任务是识别和回收垃圾对象进行内存清理,大部分的情况下,会要求系统进入一个停顿的状态,停顿的目的是终止所有的应用线程,只有这样才不会有新的垃圾产生,同时停顿也保证了系统状态在某一瞬间的一致性,也有益于更好的标记垃圾对象,在这个时间点GC的时候,都会产生一个停顿的现象,这个就是垃圾收集会产生一个停顿现象.
新生代和老年代默认是经过15次会晋级,到底有没有什么阀值,对象首次创建会被放置在新生代的eden区,就是我第一次new的时候我会把它放在这里,如果没有GC的介入的话,对象就不会离开Eden区,那么Eden的对象如何进入老年代呢,就是eden区直接进入老年代,一般来讲,只要对象的年龄达到一定的大小,经过一次GC就会进入s0或者s1区,这样进入之后他的年龄也会增长,年龄达到一定的大小,就会自动的离开年轻代,就直接进入老年代,对象的年龄是由对象经历GC的次数决定的,新生代每次GC之后,如果对象没有被垃圾回收器回收,年龄就加1,新生代每次GC的时候,每次还被别的对象引用着,那就是没有被GC回收,那我就把对象的年龄加1,然后虚拟机也提供一些参数来控制GC的大小,超过这个阀值就会晋升到老年代,默认有一个参数 -XX:MaxTenuringThreshold,就是一个对象从出生到最后尽力过15次GC还没有被垃圾收集回去,默认情况下会进入到老年代,当然这个值你就会去设置,它这个参数就是 -XX:PretenureSizeThreshold,通过-XX也就知道它是一个系统级别的参数,然后另外还有几点你是需要知道的,大对象刚实例化的时候,无法装入Eden区的时候,也会直接进入到老年代,同理如果老年代也装不进去呢,超出内存会报exception,会报OOM内存溢出,JVM里有个参数可以设置对象的大小超过在指定的大小之后,直接进入到老年代,这些参数也是都有设置的, -XX:PretenureSizeThreshold,所以JVM的调优参数是很多的

对象初始化的时候要在Eden区,最大的内存分配64M,打印一下情况,for循环是5次,每次申请1M的内存

之前我们都是加了-XX:UseSerialGC,默认都是使用串行垃圾收集器,这种是PSYoungGen,这种是JDK默认自带的垃圾收集器,我们的数据是新生代,老年代,基本上我们used是0%,也就是我们的对象都用到了新生代

测试对象进入老年代

实例化出来一个hashMap,循环5次,每次申请1M的内存,内存不释放,直接放在map里面,接着每次申请好多好多M,6000次的循环,6000乘以1M,6个G,我要做6个G的内存,这样的话我要配置这个参数之后,这个参数我初始化是一个G,最大也是一个G,然后使用的是串行的垃圾收集器,然后我进入老年代的次数是15,对象进入老年代的次数是15,然后我去打印一下详细信息,后来这个程序的目的是让程序直接停

我们在走完这5次之后,我把数据都放在map里了,map一直都是被引用着的,map是一直都被数据引用着的,除非后面的map等于空,手动的去把Map去清空,才会有一些效果,我在这里不清空,就表示我这个对象一直被引用着,可能一开始的时候就进入到新生代,它不经历GC,下面我模拟了很大的6个G的不断的生产和消亡,我现在是6个G,但是现在我的上限才是1G,也就是它会经历n多次不断的GC,我们要看的结果是15次之后发生变化,既然这块代码读懂了之后

上面这块和下面这块完全不同,DefNew是新生代存的吗,然后16次的时候新生代就直接降为0了,也就是新生代一直引用的5M就直接扔到老年代了,新生代到16次的时候才降到0

5次放到老年代,

第5次的时候新生代就清空了,我们怎么去升级怎么去晋级,根据这个参数做调整就可以,正常来讲这个参数很少去配置,采用它默认的15次就挺好,不是你会这个就瞎配置,除非你有那种特殊的需求

 


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部