【简述】
垃圾回收GC(Garbage Collection),GC中的垃圾,特指存于内存中不会再使用的对象,回收相当于清除垃圾。
垃圾回收有很多种算法,如:引用计数法、标记压缩法、复制算法、分代分区思想。
[ 引用计数法 ]
是比较古老经典的垃圾收集算法,其核心就是对象在被其引用时计数器+1,而当引用失效时-1,这种方式有一个非常严重的问题:无法处理循环引用的情况,且每次进行操作比较浪费系统性能。
[ 标记清除法 ]
分为标记和清除两个阶段来处理内存中的对象。这种方式也有一个弊端:空间碎片问题,垃圾回收后的空间不是连续的,不连续的内存空间的工作效率要低于连续的内存空间。
[ 复制算法 ]
其核心思想就是将内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存留对象复制到未被使用的内存块中,之后去清除之前正在使用的内存块中的所有对象,反复去交换这两个内存的角色,完成垃圾收集。
Java中新生代的from和to区就是使用这个算法。
[ 标记压缩法 ]
标记压缩法对标记清除法基础上做了优化,把存活的对象压缩到内存一段,然后进行垃圾清除。
Java中老年代使用的就是标记压缩法。
【 分代算法和分区算法 】
[ 分代算法 ]
就是根据对象的特点把内存分成N块,然后根据每个内存的特点使用不同的算法。
对于新生代和老年代来说,新生代回收的频率很高,但是每次回收耗时都很短。
而老年代回收的频率很低,相对耗时会比较长,所以应该尽量减少老年代的GC。
[ 分区算法 ]
主要就是将整个内存分为N多个小的独立空间,每个小空间都可以独立使用,这样细粒度地控制一次回收多少个小空间和哪些小空间,而不是对整个空间进行GC,从而提升性能,并减少GC的停顿时间。
【垃圾回收时的停顿现象】
垃圾回收器的任务是识别和回收垃圾对象进行垃圾清理,为了让垃圾回收器高效地执行,大部分情况下,会要求系统进入一个停顿的状态,停顿的目的是终止所有的应用程序,只有这样系统才不会有新的垃圾产生,同时停顿保证了系统状态在某一瞬间的一致性,也有利于更好地标记垃圾对象。
因此在垃圾回收的时,都会产生应用程序的停顿。
【对象如何进入老年代】
一般对象首次创建会被放在新生代的eden区域,如果没有GC介入,则对象不会离开eden区。
那么对象怎么进入老年代呢?
一般来讲,只要对象的年龄达到一定的大小后,就会自动离开新生代进入老年代,对象的年龄是由对象经历数次GC决定的,在新生代每次GC之后如果对象咩有被回收则年龄+1,虚拟机提供了一个参数来控制新生代对象的最大年龄,当超过这个年龄范围就会晋升到老年代。
-XX:MaxTenuringThreshold,默认情况下是15。
【初始的对象分配在eden区】
package com.jvm.demo01; public class Demo05 { public static void main(String[] args) { //初始的对象在eden区 //参数:-Xmx64M -Xms64M -XX:+PrintGCDetails for(int i=0; i< 5; i++){ byte[] b = new byte[1024*1024]; } } }
【-Xmx64M -Xms64M -XX:+PrintGCDetails 运行结果】
【测试进入老年代的对象】
package com.jvm.demo01; import java.util.HashMap; import java.util.Map; public class Demo05 { public static void main(String[] args) { //测试进入老年代的对象 //参数:-Xmx1024M -Xms1024M -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintGCDetails //每次分配1M内存,执行6000次 for(int k = 0; k<20; k++) { for(int j = 0; j<300; j++){ byte[] b = new byte[1024*1024]; } } } }
【运行结果】
[ 总结 ]
根据设置MaxTenuringThresgold参数,可以指定新生代对象经历多少次回收后进入老年代。
另外,大对象(新生代eden区无法装入时,也会直接进入老年代)。JVM里有个参数可以设置对象的大小超过指定的大小之后,直接进入老年代(针对大对象)。
-XX:PretenureSizeThreshold
【大对象直接进入老年代的例子】
package com.jvm.demo01; import java.util.HashMap; import java.util.Map; public class Demo06 { public static void main(String[] args) { //-XX:PretenureSizeTheshold 可以直接指定进入老年代的对象大小 -XX:PretenureSizeThreshold=1024*1000 < 1024*1024 //-Xmx30m -Xms30m -XX:+UseSerialGC -XX:+PrintGCDetail -XX:PretenureSizeThreshold=1024000 Map<Integer, byte[]> m = new HashMap<Integer, byte[]>(); for(int i=0;i<5;i++){ byte[] b =new byte[1024*1024]; //每次分配的对象大小都大于设定的1024*1000 m.put(i, b); } } }
【大对象直接进入老年代的运行结果】
【-XX:PretenureSizeThreshold 设置的值较小(1024B),生成对象(1024)稍大,但没有存入老年代的例子】
package com.jvm.demo01; import java.util.HashMap; import java.util.Map; public class Demo06 { public static void main(String[] args) { //1.默认使用TLAB区 //参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 //这种现象原因为:虚拟机对于体积不大的对象 会优先把数据分配到TLAB区域中,因此就失去了在老年代分配的机会 //2.禁用TLAB区 //参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 -XX:-UseTLAB Map<Integer, byte[]> m = new HashMap<Integer, byte[]>(); for(int i=0; i< 5*1024; i++){ //5*1024次 byte[] b = new byte[1024]; //每次1024B m.put(i, b); } } }
【使用TLAB区域运行结果】
【禁用TLAB区域运行结果 -XX:-UseTLAB】
[ 总结 ]
使用PretenureSizeTheshold可以直接指定进入老年代的对象大小,但是要注意TLAB区域的优先分配空间。
【TLAB区域】
TLAB区域全称是Thread Local Allocation Buffer ,即线程本地分配缓存,从名字上看是一个线程专用的内存专用分配区域,是为了加速对象的分配而诞生的。每一个线程都会产生一个TLAB,该线程独享的工作区域,Java虚拟机使用这种TLAB区域来避免多线程冲突问题,提高了对象分配的效率。TLAB空间一般不会太大,当大对象无法在TLAB分配时,则会直接分配到堆上。
[ 参数 ]
-XX:+UseTLAB 设置TLAB
-XX:+TLABSize 设置TLAB大小
-XX:TLABRefillWasteFraction 设置维护进入TLAB空间的单个对象的大小,它是一个比例值,默认为64,即如果对象大于整个空间的1/64,则在堆创建对象。
-XX:+PrintTLAB 查看TLAB信息
-XX:ResizeTLAB 自调整TLABRefillWasteFraction阈值
[ 一个对象的创建流程 ]
一个对象创建在什么位置,我们的Jvm会有一个比较细节的流程,根据数据的大小,参数的设置,决定如果创建分配,以及其位置。