查看Java8的默认GC
1. cmd命令行查看Java8的GC:
java -XX:+PrintCommandLineFlags -version
结果如下:
-XX:InitialHeapSize=132397312 // JVM默认初始化堆大小
-XX:MaxHeapSize=2118356992 //JVM堆的默认最大值
-XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:-UseLargePagesIndividualAllocation
-XX:+UseParallelGC //Java8使用的GC类型
java version "1.8.0_20" //使用的java版本
Java(TM) SE Runtime Environment (build 1.8.0_20-b26)
Java HotSpot(TM) 64-Bit Server VM (build 25.20-b23, mixed mode)
结果分析:由结果可以看出Java8的GC情况是:-XX:+UseParallelGC,即Parallel Scavenge(新生代) + Parallel Old(老生代),实际上几个主流Java版本的GC情况如下:
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代【标记-复制算法】)+Parallel Old(老年代【标记整理算法】)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代【标记-复制算法】)+Parallel Old(老年代【标记整理算法】)
jdk1.9 默认垃圾收集器G1【从局部(两个Region之间)来看是基于"标记—复制"算法实现,从整体来看是基于"标记-整理"算法实现】
Parallel Scavenge收集器-标记-复制算法(Copy)
复制算法是针对标记—清除算法的缺点,在其基础上进行改进而得到的,它将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。复制算法有如下优点:
每次只对一块内存进行回收,运行高效
只需移动栈顶指针,按顺序分配内存即可,实现简单
内存回收时不用考虑内存碎片的出现
它的缺点是:可一次性分配的最大内存缩小了一半
复制算法的执行情况如下图所示:
- 回收前状态
- ** 回收后状态**
现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块比较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的空间会被浪费。
当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。
Parallel Old收集器 - 标记—整理算法(Mark-Compact)
复制算法比较适合于新生代,在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。标记—整理算法的回收情况如下所示:
-
回收前状态:
-
回收后状态:
G1 GC
G1 GC全称为Garbage First GC,也就是垃圾优先GC。从Java 7中就开始引入了G1,以用于支持更大的(超过4GB)的堆内存。G1收集器也是一种并发收集器。它会利用多个后台线程来扫描堆,但和其他GC不同的是,它将堆内存划分为多个大小相等的region。每个region可以在 1M - 32M,region的具体大小取决于Heap的大小。我们可以通过:
-XX:G1HeapRegionSize=n
指定。
和它的名字一样,G1 GC会优先扫描包含最多垃圾对象的区域。
默认,G1 GC是不开启的,我们来看下JVM默认参数:
bool UseG1GC = false
要开启G1,可以使用:
-XX:+UseG1GC
G1 GC尽量减少不再使用的对象耗尽堆内存的机会。因为堆内存一旦耗尽,就不得不执行Major GC而导致STW。另外,它还有另外一个优势,它可以对堆内存进行整理。G1将对象从堆中的一个或多个region复制到单个region中,在该过程中进行整理并释放内存。所以,G1的每一次回收,都会减少内存碎片。
相比于CMS,它仅在Major GC时,才会进行整理。
这是Oracle官方提供的一个G1的堆内存示意图。我们可以明显地看到,使用G1 GC,堆内存被分为了一个个的region(上图中的一个个灰色的格子)。而分代的概念是逻辑性的。
上图,浅蓝色的表示是年轻代。当我们创建对象时,会分配到逻辑年轻代的region中。当逻辑年轻代满了时,逻辑年轻代的region会进行GC。G1允许,同时收集逻辑年轻代和逻辑老年代。上图中,用红色表示正在gc的region。G1进行GC时,会将存活的对象复制到空的region中,然后根据存活对象的age,决定是复制到Survior(标有s的格子)或者复制到老年代的region中。标有H的region是1.5倍以上大小,并经过特殊处理,可以用于存放更大的对象。
我们可以通过对G1GC指定最大的暂停时间:
-XX:MaxGCPauseMillis=200
G1 GC会自动调整其参数以达到我们预期的目标。
优先使用G1
HotSpot官方将G1作为CMS的替代品,也就是说,我们应该优先使用G1 GC。G1对内存管理更友好、而且能够保证更稳定的STW时间。
关于大堆
尽管这些年,大家一直在避免使用大堆,很多Java开发人员从单个JVM迁移到多个JVM,例如:微服务。微服务可以隔离不同的应用程序模块,简化部署、并且避免将应用程序类加载到内存需要更多的成本。
但即便如此,大家还是在尽量避免较大的Heap长时间STW。Java8 Upate 20做了一次重要的优化,G1 GC可以进行字符串重复删除。字符串占用了大量的JVM内存,这一次优化可以让G1 GC可以识别在整个堆中重复多次的字符串,让它们应用同样的一个Char[],避免一样的字符串在内存中有太多副本。这项配置默认是关闭的:
bool UseStringDeduplication = false