JVM中的各种垃圾回收算法
什么情况下JVM内存中的一个对象被垃圾回收被哪些变量引用的对象是不能回收的JVM使用了一种可达性算法来判断哪些对象可以被回收哪些对象不可以被回收。这个算法的意思就是说对每个对象都分析一下有谁在引用他然后一层一层去判断看是否有一个GC Roots在JVM的规范中静态变量也可以看做一种GCRoots此时只需要一个对象被GC Roots引用了就不会去回收他总结只要你的对象被方法的局部变量类的静态变量给引用了就不会回收他们。Java中对象不同的引用类型Java中不同的引用类型有强引用弱引用软引用虚引用public class Kafka { public static ReplicaManager replicaManager new ReplicaManager(); }一个变量引用一个对象只要是强引用的类型那么垃圾回收的时候绝对不会去回收这个对象的public class Kafka { public static SoftReferenceReplicaManager replicaManager new SoftReferenceReplicaManager(new ReplicaManager()); }正常情况下垃圾回收是不会回收软引用对象的但是如果你进行垃圾回收之后发现内存空间还是不够存放新的对象内存快要溢出此时就会把这些软引用对象给回收掉哪怕他被变量引用但是因为他是软引用所以还是要回收。public class Kafka { public static WeakReferenceReplicaManager replicaManager new WeakReferenceReplicaManager(new ReplicaManager()); }这个软引用就和没有引用类似的如果发生垃圾回收就会把这个对象回收掉。虚引用这个用的很少暂时可以忽略他。这些比较常用的就是强引用和软引用强引用代表绝对不能回收的对象软引用就是说对象可有可无如果内存实在不够了可以回收他。finalize()方法的作用有GC Root用的对象不能被回收没有GC Roots引用的对象可以回收如果有GC Roots引用但是如果是软引用或者弱引用也有可能被回收掉。到回收环节假设没有GC Roots引用的对象是一定立马被回收吗其实不是的这里有一个finalizie()方法可以拯救他自己public class ReplicaManager { public static ReplicaManager instance; Override protected void finalize() throws Throwable { ReplicaManager.instance this; } }如果重新让GC Root变量引用了自己那么就不用被垃圾回收了。JVM中有哪些垃圾回收算法复制算法的背景引入针对新生代的垃圾回收算法他叫做复制算法首先把新生代的内存分为两块public class Play{ public static void main(String[] args) { loadFile(); } public static loadFile() { Manager manager Manager(); } }假设一下代码在不停的运行大量的对象都分布在了新生代内存中的一块区域也只会分布在这块区域而分配过后很快就失去了局部变量或者静态变量的引用成为了垃圾对象这个时候新生代内存那块被分配对象的内存区域基本上都要满了要再次分配对象的时候发现里面内存空间不足 那么此时就会触发Minor GC去回收掉新生代那块被使用的内存空间垃圾对象。一种不好的垃圾回收思路假设我们现在要进行垃圾回收就是直接对上图被使用的那块内存区域中的垃圾对象进行标记也就是标记出来哪些对象是可以被垃圾回收的然后就直接对那块内存区域中的对象进行垃圾回收把内存空出来回收完后的内存区域会如图所示回收掉了大量的垃圾对象但是保留了一些被人引用的存活对象但是存活对象在内存区域里东一个西一个非常的凌乱而且造成了大量的内存碎片那么什么是内存碎片呢这些红色区域就是内存碎片这些内存碎片有的大有的小内存碎片大多会造成什么问题呢内存浪费比如现在打算分配一个新的对象尝试在上图那块被使用的内存区域里去分配可能因为为内存碎片太多的缘故虽然所有的内存碎片加起来其实有很大的一块内存但是因为这些内都是碎片式分散的所有导致没有一块完整的足够的内存空间来分配新的对象所有这种直接对一块内存空间回收掉垃圾对象保留存活对象的方法绝对不可取因为内存碎片太多就是他最大的问题会造成大量内存浪费很多内存碎片压根是没有办法使用的一个合理的垃圾回收思路这种思想不像上面的直接对已经使用的那块区域把垃圾对象全部回收掉然后保留全部存活对象而是先对那块在使用的内存空间标记出里面有哪些对象是不能进行垃圾回收的就是要存活的对象然后先把哪些存活的对象转移到另外一块空白的内存中通过把存活对象先转移到另外一块空白内存区域我们可以把这些对象都比较紧凑的排列在内存里这样就可以让被转移的那块内存区域几乎没有什么内存碎片对象都是按顺序排列在这块内存里的然后那块被转移的内存区域是不是多出来一大块连续的可用的内存空间此时就可以将新对象分配在那块连续内存空间里了这个时候再一次把原来使用的那块内存区域中的垃圾对象全部一扫而空全部给回收掉空出来一块内存区域所谓的复制算法把新生代内存划分分为两块内存区域然后只使用其一块内存待那块内存快满的时候就会把里面的存活对象一次性转移到另外一块内存区域保存没有内存碎片接着一次性回收原来那块内存区域的垃圾对象再次空出来一块内存区域两块内存区域就这么重复循环使用复制算法有什么缺点复制算法的缺点其实非常明显假设我们给新生代1G内存那么只要512MB内存是可以用的另外512MB的内存空间是要一直再那边空着然后512MB内存满了就把存活对象转移到另外512MB内存空间去从始至终就有一半内存可以用这样的算法对内存的使用效率太低了。复制算法的优化Eden区和Survivor区经过一次垃圾回收后新生代99%的对象都被回收掉了就1%的对象存活下来了所有实际上真正的复制算法会做出一下优化把新生代内存区域划分为三块1个Eden区2个Survivor区其中 Eden区占80%内存空间 每块Survivor区各占10%内存空间刚开始的对象都是分配在Eden区如果满了此时会触发垃圾回收这时Eden区所有的存活对象都会一次性转移到一块空着的Survivor区接着Eden区就会被清空然后再次分配再Eden区如果下次Eden区再满再次发生Minor GC就会把Eden区和上次Minior GC后存活对象的Survivor区内的存活对象转移到另一块Survivor区去。年轻代和老年代分别适合什么样的垃圾回收算法年轻代对象进入老年代默认情况下当对象年龄达到15岁的时候也就是躲过15次GC后他会转移到老年代我们可以通过调整JVM参数设置-XX:MaxTenuringThreshold来设置默认是15岁动态对象年龄判断这里跟这个对象有另一个规则可以让对象进入老年代不用等15次GC之后才可以假如说当前对象的Survivor区域里一批对象的总大小大于了这块Survivor区域的内存大小50%那么此时大于等于这批年龄的对象就可以直接进入老年代了说白了就是无论是15岁规则还是动态年龄判断的规则都是希望那些可能长期存活的对象尽早进入老年代大对象进入老年代我们可以使用这个参数-XX:PretenureSizeThreshold可以把他的值设置为字节数。比如1048576字节就是1MB意思就是如果你要创建一个大于这个的对象比如一个超大的数组此时就直接把这个大对象放到老年代去这是为了避免新生代出现那种超大对象然后多次躲过GC还得把他再Survivor区域里来回复制多次才能进入这么大的对象再内存中复制来复制去也浪费时间Minor GC后的对象太多无法进入Survivor区怎么办这个时候就必须将这些对象直接转移到老年代去老年代空间分配担保规则如果新生代里面存活大量对象确实Survivor都存放满了必须转移到老年代去但是 如果老年代里面的内存空间也不够放这些对象呢该怎么办首先在执行一次Minor GC之前JVM会检查一下老年代可用的内存空间是否大于新生代所有对象的总大小因为最极端的情况下可能新生代Minior GC过后所有对象都存活下来新生代所有 对象全部要进入老年代如果老年代的内存大小是新生代所有对象的此时就可以对新生代发生一次Minor GC即使Minor GC后所有对象都存活Survivor区放不下也可以转移到老年代去那如果此时老年代空间也不够了怎么办假如在Minor GC之前发现老年代的可用内存已经小于新生代的全部对象大学里就会 看一个-XX:-HandlePromotionFailure的参数设置了如果有这个参数那么就会进行下一步判断看老年代的内存大小是否大于之前的Minor GC后进入老年代对象的平均大小。如果上面的步骤失败了此时就会发生一次Full GC就是对老年代对象进行垃圾回收尽量腾出空间再执行Minor GC老年代垃圾回收算法在Minor GC之前检查发现可能Minor GC之后要进入老年代的对象太多老年代放不下此时需要提取触发Full GC然后再带着执行Minor GC对老年代采用的是标记整理算法首先标记出来老年代当前存活的对象这些对象一般是东一个西一个的接着会让这些存活对象在内存里面进行移动把存活对象尽量都挪到一边去避免垃圾回收后出现过多的内存碎片然后再一次性把垃圾对象都回收掉老年代的垃圾回收算法速度比新生代垃圾回收算法速度慢10倍如果系统频繁出现老年代Full GC垃圾 回收会导致系统性能被严重影响出现频繁卡顿现象。常见的垃圾回收器有哪些有什么特点背景引入现在我们团队研发了一个数据计算系统日处理数据量上亿规模这个数据计算系统会不停通过SQL语句和其他方式从各种数据存储中提取数据到内存中来计算大致生产负载时每分钟大概需要执行500次数据提取和计算任务我们分配了多台机器所以每台机器负责执行100次数据提取和计算每次会提取1w左右的数据到内存中平均每次计算大概耗费10s时间每台机器是4核8G内存配置JVM内存给了4G其中新生代和老年代分别为1.5G的内存空间每条数据大概有20个字段平均每条数据在1kb左右计算1w条就有10MB左右大小Eden区占80%大概是1.2GBSurvivor为100MB一分钟大概计算100次任务一次任务10MB基本上一分钟后Eden区满了这时候触发Minor GC在进行此操作的时候还要进行以下操作首先会对Minor GC之前进行检查先看老年代的可用内存空间是否大于新生代全部对象这时候再进行一次Minor GC后200MB的对象能放入Survivor区吗不能因为其内存不够就会触发空间担保机制让其200MB对象进入老年代然后再清空Eden区这样计算下来每分钟会有200MB数据进入老年代当第三分钟后再进行Minor GC前会检查新生代对象是否大于老年代对象这是老年代1.1GB新生代1.2GB此时需要查看-XX:HandlePromotionFailure这个参数是否打开默认打开此时进入第二步检查看老年代可用空间是否大于历次Minor GC过后进入老年代的对象平均大小大概执行7次MinorGC后老年代只有100MB内存几乎要满了这时候就需要发生一次Full GC回收全部对象按这样子推算七八分钟就需要进行一次Full GC会十分严重系统性能如何进行JVM调优这个案例下我们可以增加新生代的内存占比3GB左右的堆内存其中2GB分配给新生代1GB给老年代每次Minor GC后200MB存活对象可以放到Survivor下一次Minor GC的时候这个Survivor计算任务应该已经完成了可以回收了。这样就不会有多少对象进入老年代了此时Full GC频率可能从几分钟一次降低到几小时一次。但是需要注意的是前面我们也提到了动态年龄判断升入老年代规则就是S区的同龄对象超过S区的一半就直接升入老年代这里的优化方法只是一个示例意思是要我们增加S区大小为了避免动态年龄判断规则我们可以调整-XX:SurvivorRation8默认Eden区域为80%可以减低Eden区域多给S区域更多空间。Stop the World问题分析如果程序一直运行将Eden区塞满了怎么办这时候必定发生Minor GC针对新生代我们会使用ParNew垃圾回收器进行回收采用的是复制垃圾算法来回收垃圾此时会把Eden区的存活对象标记出来然后转移到S1区去接着一次性清空Eden区垃圾对象系统继续运行会将Eden区放满对象然后发送Minor GC后会将所有存活对象放到S2区中但是我们在发送GC的时候难道就不会创建对象吗如果这些垃圾对象有些还是有引用的存活对象怎么弄现在我们需要考虑如何去让垃圾回收器去持续追踪这些新对象的状态简单介绍垃圾回收器Serial和Serial Old分别用来回收新生代和老年代垃圾对象工作原理就是单线程运行垃圾回收的时候会停止我们写的系统的其他工作线程让我们的系统卡死然后在进行垃圾回收ParNew和CMS垃圾回收器ParNew现在一般是新生代垃圾回收器CMS是用在老年代垃圾回收器他们都是多线程并发执行性能更好G1垃圾回收器统一收集新生代和老年代采用了更优秀的算法和设计机制Stop the World在继续垃圾回收的时候要尽可能让垃圾回收器专心干活不能随便让Java系统创建对象就会进入这个状态如果这个状态持续100ms可能导致我们的系统100ms不会创建新的对象了这是十分不好的事情
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2452483.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!