正文
三、垃圾回收机制
什么是垃圾回收
垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中不可达的对象进行清除和回收。垃圾回收是自动进行回收的,不能人为控制。程序员唯一能做的就是通过调用System.gc() 方法来"建议"执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。
MinorGC和MajorGC
新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。
垃圾判断算法
引用计数器法
引用计数法就是给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1。任何时刻计数器值为0的对象就是不可能再被使用的。但是这种方法不能判断对象相互引用的这种情况。
根搜索算法
根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可达的。
GC ROOTS主要回收的区域
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。
垃圾回收算法
标记-清除
标记-清除包含两部分,标记和清除。一部分标记出可达的对象(有的人认为是标记不可达的对象),然后清除掉不可达的对象。
这种算法的缺点是容易产生不连续的空间碎片,而且标记和清除的效率都不是很高。这种算法适合老年代的对象回收。
复制算法
内存会被分为两部分From区和To区。每次只是使用from区,to区则空闲着。当from区内存不够了,开始执行GC操作,这个时候,会把from区存活的对象拷贝到to区,然后直接把from区进行内存清理。
这种算法的虽然避免了标记-清除碎片化的问题,但是如果回收对象较多较大需要花费更长的时间,而且总会有一部分空间是空闲的,浪费内存空间。这种算法适用于新生代的对象 。
标记-整理(标记-压缩)
标记整理和标记清除算法比较相同,也经过标记阶段,然后把可达对象移动到一端,对不可达的对象进行删除。
这种算法也解决了空间碎片化的问题,但是移动对象,需要修改对象的引用地址,而且标记,整理效率也不高。这种算法适合老年代的对象回收。
分代算法
这种算法,根据对象的存活周期的不同将内存划分成几块,新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。新生代对象朝生夕死,对象数量多,只要重点扫描这个区域,那么就可以大大提高垃圾收集的效率。另外老年代对象存储久,无需经常扫描老年代,避免扫描导致的开销。
新生代使用复制算法,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用复制算法进行拷贝时效率比较高。jvm将堆内存划分为新生代与老年代,又将新生代划分为Eden与2块Survivor Space,然后在Eden –>Survivor Space 以及From Survivor Space 与To Survivor Space 之间实行复制算法。
堆空间中新生代和老年代的默认比例是1:2(可以通过参数 –XX:NewRatio)来设定,在新生代中Eden:From:To=8:1:1 (通过参数 –XX:SurvivorRatio )来设定。
复制算法的过程
当Eden区满的时候,会触发第一次MinorGC,把还活着的对象拷贝到Survivor From区。这个时候存活的对象就1岁了。当eden区再次执行MinorGC,就会扫描Eden和From区,把存活的对象复制到To区,然后清空Eden和From区。
当Eden区再次满了之后,再次触发MinorGC,就会扫描Eden和To(新的From区)区,然后将存活的对象复制到From区(新的To区),然后清空Eden和To区。
这样依次往复,在From和To区之间复制来复制去,每熬过一次MinorGC的对象就长大一岁,当对象年满15岁之后,依然存活,则会进入老年代。
可以通过参数设置-XX:MaxTenuringThreshold=15 默认也是15次
注意:这种情况不考虑,破格直接进入老年代的情况。
老年代使用标记清除或者标记整理
老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记-清除-压缩”算法进行回收。
新生代如何进入老年代
创建大对象直接进入老年代 -XX:PretenureSizeThreshold=1M 只对Serial及ParNew收集器管用。
新生代采用的是复制收集算法,S0和S1始终只是用其中一块内存区,当出现MinorGC后大部分对象仍然存活的话,就需要老年代进行空间分配担保,把survior区无法容纳的对象直接晋升到老年代。
长期存活的对象>15岁
当 Survivor 空间中相同年龄(比如10)所有对象的大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄(10)的对象就可以直接进入老年代,而不需要达到MaxTenuringThreshold的分代年龄。
如何触发FullGC
System.gc()方法的调用(大多数的情况下都会进行fullGC,但不能百分之百保证)。
当老年代没有足够空间存放对象时(认为达到92%这个数值仅供参考),会触发一次FullGC。
空间分配担保时,如果剩余空间不足以盛放新生代的对象,这时要进行一次FullGC
如果元空间区域的内存达到了所设定的阈值-XX:MetaspaceSize=,触发FullGC。
内存溢出和内存泄漏
内存溢出(out of memory),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如系统就分给你10M的空间,你要存放20M的东西,这样就会导致内存溢出。
产生原因:
1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
3.代码中存在死循环或循环产生过多重复的对象实体;
4.使用的第三方软件中的BUG;
5.启动参数内存值设定的过小
内存溢出解决方式就是增大jvm的内存
-Xmx3550m -Xms3550m 设置最大内存和初始化内存,两者尽量一致,避免每次垃圾回收完成后JVM重新分配内存。
内存泄露(memory leak),是指程序在申请内存后,无法释放已申请的内存空间。比如一个对象占用了10M的空间,但是它使用完了,一直不释放。如果一次内存泄漏可以容忍,但是有很多的内存泄漏,不管有多少的内存迟早会被占用光,而导致的后果就是,内存溢出。
产生原因:
内存泄露的本质原因是因为代码问题
不使用的对象不能被垃圾回收机制回收。
使用完的资源记得关闭,比如io,数据库等close()。
四、垃圾收集器
垃圾回收器有Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1、ZGC(jdk11之后)。按照新生代和老年代来分,负责新生代的主要是 Serial、ParNew、Parallel Scavenge,负责老年代回收的是Serial Old、Parallel Old、CMS,而G1回收器可以对整个堆进行垃圾回收。
Serial垃圾收集器
特点:单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。使用新生代复制算法。
应用场景:适用于Client模式下的虚拟机。
运行示意图
ParNew垃圾收集器
和Serial完全一致,除了在收集器使用多线程外。
特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
和Serial收集器一样存在Stop The World问题
应用场景:ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。
Parallel Scavenge 垃圾收集器
Parallel Scavenge收集器是一个更关注吞吐量的收集器,与parnew 类似。
特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)。
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)
GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。
Parallel Scavenge收集器使用两个参数控制吞吐量:
XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
XX:GCRatio 直接设置吞吐量的大小。
Serial Old垃圾收集器
Serial Old是Serial收集器的老年代版本。
特点:同样是单线程收集器,采用标记-整理算法。
应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。
Parallel Old垃圾回收器
是Parallel Scavenge收集器的老年代版本。
特点:多线程,采用标记-整理算法。
应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。
CMS收集器
注意:“标记”是指将存活的对象和要回收的对象都给标记出来,而“清除”是指清除掉将要回收的对象。
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。
初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。
并发标记阶段 :并不会阻碍用户线程正常执行任务,与用户线程并发执行进行标记。
重新标记阶段则是为了修正并发标记期间因用户程序继续动作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
并发清除:对标记的对象进行清除回收。
CMS收集器的缺点:
对CPU资源非常敏感。
无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。
因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。
未完待续
性能调优,实战线上问题排查
。。。。。
参考:
https://blog.csdn.net/qzqanzc/article/details/81008598
jvm之java类加载机制和类加载器(ClassLoader)的详解_翻过一座座山-CSDN博客_类加载器
https://www.cnblogs.com/chenpt/p/9803298.html