Java基础之刨根问底第3集——GC的优化

原文转自我自己的个人公众号:Java基础之刨根问底第3集——GC的优化(由于是拷贝过来的,如果排版有问题,请看公众号文章)

  • 本系列不适合初学者,读者应具备一定的Java基础。

  • 考虑到目前行业中使用最广的版本,本系列依据Java8编写。

GC的优化

        

        在开始之前,我想纠正一下上一集中关于client和server模式下heap默认大小的描述。在上一集中说:

client模式下:初始堆内存(-Xms)被设置为4MB。最大堆内存(-Xmx)被设置为64MB。默认的垃圾回收器为Serial。server模式下:初始堆内存(-Xms)会被设置为物理内存的1/64,最大为1GB(由于server模式至少有2GB的物理内存,因此-Xms最小为32MB)。最大堆内存(-Xmx)会被设置为物理内存的1/4,最大为1GB。默认的垃圾回收器为Parallel。

樊超的赞赏账户,公众号:超神说Java基础之刨根问底第2集——垃圾回收器

        上面这部分的内容来源是Java5的内存管理白皮书(https://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf)中的内容,如下所示:

图片

        在Java8中,默认值已经发生了变化,下图是Java8官方文档中的描述:

图片

        按照Java8文档中的意思来看,client模式下,如果物理内存小于192mb则默认最大堆内存是物理内存的一半,否则就是物理内存的1/4,最大是1GB物理内存的1/4。文档中还举了一个例子,如果主机物理内存是128mb,那么最大堆内存是64mb(一半),如果物理内存超过1GB,则最大堆内存是256mb(1G的1/4)。

        文档中说在server模式下和client模式类似,只是server模式在32位系统上默认最大堆内存为1GB,而在64位系统上则默认最大堆内存能到32GB,这些都是物理内存的1/4。

        然而,在我实际测试的时候,发现并不是这样的。我申请了3台云主机,配置分别位1核1G,2核4G,2核8G。然后我统一使用1.8.0_301版本的JDK运行同一个程序。首先是1核1G上分别使用-server和-client的效果:

图片

        最上面是物理内存数量,这里total现实总物理内存位990MB,左侧是client模式,右侧是server模式,可以看到两个模式的最大堆内存是一摸一样的都是248MB,是990MB的1/4。垃圾收集器显示的都是Mark Sweep Compact,即:串行垃圾回收器,编译器也使用的是server模式(Server compiler detected)。下面是2核4G的效果:

图片

        与1核1G相比,垃圾回收器变味了Paraller(Parallel GC with 2 thread(s)),最大堆内存变为了948MB,是总物理内存3.7GB的1/4。client和server也无任何区别。下来再看2核8G的效果:

图片

        除了最大堆内存都变为了1956mb(是总物理内存7.6G的1/4)外,与上一个测试没有任何变化,server和client依然没有区别。

        难道文档有误吗?经过一些思考和查证,似乎事情是这样的,首先我在2核4G核1核2G的主机上用java -h看了下命令的参数,发现两者还真是有差别的,如下图所示:

图片

        左侧是2核4G的,右侧是1核1G的,可以看到,当符合文档中说的2核2G以上的配置就是server模式时,-server的说明中显示当前默认的vm是server模式,因为运行在server级别的机器上,而右侧不符合server级的1核1G上,也显示了默认VM是server。这是因为server模式核client模式是两个单独的程序,有的发布包中包含,有的不包含,我使用的"Linux x64 Compressed Archive"(jdk-8u301-linux-x64.tar.gz)版本似乎并不包含client模式,在java -h中,没有-client的参数。官方问答中有如下回答:

图片

        也就是说,64位的发布包中只包含server模式。

        在Java8的官方文档中说,在64位的linux中,编译器也只会是server的,文档中还说了目前各个垃圾回收器默认的最大堆内存的算法都是和Parallel回收器一样的。

        总结一下,从实际角度来看,目前java更多的是用来开发web应用程序,这些程序几乎都会发布在64位linux系统之上,因此不会运行在client模式下的,而主机配置至少都是2核4G以上的,因此默认设置的最大堆内存为物理内存的1/4,默认最高可达32GB。

        默认的初始堆内存在jmap中并未出现,因此无法直观的看到,但Java8的文档中说是物理内存的1/64,Parallel回收器至少8mb,不过初始堆内存貌似也跟运行的主机平台有关,文档中还有个地方说在64位的Solaris操作系统上,Xms的默认值为6656k。虽然无法直观的看到初始堆内存,但可以通过在启动时加入“-verbose:gc”这个参数,这会在日志中显示gc日志,以此来查看初始堆内存,如下图所示:

图片

        通过分析第一次gc的日志就可以得出初始堆内存的大小了,箭头前的数字是回收之前年轻代的大小,箭头后是回收后年轻代的大小,括号中就是堆内存的总大小。从第一行就可以看出,初始堆内存是58880K,而我的物理内存是3.7G,大约就是1/64的比例,与文档中的描述符合。

        在上面的截图中我们可以看到,默认会设置一些堆内存的参数,下面我们将对这些参数进行一些介绍,另外,也可以从上面的截图中发现,当主机配置不符合server级别时,垃圾回收器用的是Serial,否则会使用Parallel,并且这两个回收器的MinHeapFreeRatio和MaxHeapFreeRatio是不一样的,如下图所示:

图片

  • MinHeapFreeRatio和MaxHeapFreeRatio

        当初始堆内存和最大堆内存设置的不同时,JVM会确保堆内存保持在一个合理的大小,会根据当前空间的使用情况自动的扩大或缩小堆内存的大小,这两个参数就是用来控制扩大和缩小时机的。以左侧Serial回收器的默认配置为例,MinHeapFreeRatio的值是40,MaxHeapFreeRatio的值是70。也就是说,如果剩余的空闲内存量小于总容量的40%,就会自动扩容以保证至少有40%的空闲容量;如果空闲的容量超过70%,就会执行缩容,保证空闲不会超过70%(上一节讲过,扩容每次按照20%,缩容每次按照5%)。

        Parallel回收器的MinHeapFreeRatio和MaxHeapFreeRatio设置为0和100,意味着当可用内存为0时才会执行扩容,并且不会进行缩容。这里我并不是很理解这样做的原因,我又对比了CMS和G1回收器,它们都是按照40,70来设置的。还是上方查看gc日志的方法,我把Parallel的比例改成40,70,来看看有什么区别,如下图所示:

图片

        左侧是40,70,右侧是0,100。可以看到右侧的总容量递增的,从最初的58880K增长到最后的155648K,而左侧则是先扩容再缩容的,从58880K增长到59904K,最后又缩容到35328K,并且由于右侧只会在没有空间的时候GC,而左侧会频繁GC以满足可用空间的符合40,70的配置,因此gc次数会更多。从这个例子中也看出,Parallel的默认配置只会在内存满后扩容,而不会缩容。        

        补充一点上一集漏说的,上图中,Parallel回收器显示使用了2个线程,是因为我的主机cpu是2核的,当核数小于8个时,Parallel使用的线程数等于核数,超过8个时,线程数等于核数的5/8(有的平台会是5/16)。线程数量可以用“-XX:ParallelGCThreads”参数设置。

  • Xms和Xmx

        图中的MaxHeapSize对应了Xmx参数设置的最大堆内存,Xms则可以设置初始堆内存,我使用-Xms512m -Xmx2048m后,如下图所示:

图片

        可以看到,MaxHeapSize是2048MB,初始堆内存大约是500MB左右。

  • NewSize和MaxNewSize

        这两个参数是年轻代的最小和最大值,默认情况下,年轻代和老年代的比例是1:2(NewRatio参数设置),以上一个图为例,Xms512m,newSize就是其1/3,大约170MB,Xmx2048,MaxNewSize也是其1/3,大约682MB。

  • OldSize

        这是老年代的大小,因为和年轻代是1:2的关系,年轻代的初始大小是170MB,因此这里是341MB。我查了一下,貌似并没有OldSize这个参数能够直接设置老年代的大小,应该只是根据比例计算后的值,这里仅仅是显示。

  • NewRatio

        年轻代和老年代的比例,下面我修改成3,大家可以看看效果:

图片

  • SurvivorRatio

        这个参数用来设置生存区与Eden区的比例,需要注意的是,这里是一个生存区所占的比例,总共有2个生存区,如下图所示:

图片

        From和To是2个生存区,每个生存区与Eden的比例都是1:8,因此每个生存区占年轻代1/10,Eden占年轻代8/10,当前年轻代的总容量是170MB,生存区大概就是17MB左右,这里21MB略大一点点,按照比例计算,Eden大概是136MB,这里略小一点点,不过基本上都是符合比例的。

  • MetaspaceSize和MaxMetaspaceSize

        这两个参数用来设置Metaspace(可以理解为方法区)的初始大小和上限,默认MetaspaceSize的值是和平台有关的,一般在12MB到20MB之间,上图中可以看到,在我这里(64位Linux,2核4G)是20MB。MaxMetaspaceSize的默认值非常大,含义是没有上限(Metaspace的空间是直接在物理内存上开的)。和堆内存类似,Metaspace也会动态的扩缩容,同样可以使用MaxMetaspaceFreeRatio和MinMetaspaceFreeRatio来设置扩缩容得时机。使用-XX:+PrintGCDetails参数后,终止程序的时候,日志中会输出Metaspace的用量,也可以使用jstat工具查看,如下所示:

图片

        used表示目前已经使用的容量,capacity表示目前的chunk(JVM向操作系统请求内存时是按照chunk申请的)中的可用的容量,committed表示可用于chunk的容量。因此当前metaspace的总大小是三者之和,我算了一下是93338KB,约为91mb,我们再用jstat工具查看一下:

图片

        这里的M列就是Metaspace,可以看到这里显示的是94MB,和上面计算的基本相等(本来计算就会有些误差,而且我这里还是同一个程序的两次运行)。

  • CompressedClassSpaceSize

        可压缩的类指针占据的空间,默认是1G。这个主要是为了更好的兼容32位和64位系统的,因为32位系统迁移到64位系统后,因为对象指针的位数增加了一倍,因此再64位系统上会需要更多的内存,因此JVM使用了压缩技术,可以在64位系统上使用32位的指针。这里需要注意的是,这个区也是在Metaspace中的,因此Metaspace的大小等于压缩的和非压缩的之和。可以再看一下jstat上面的那个截图,Metaspace后面reserved的值是1077248K,表示预留给Metaspace的空间,这个大约等于压缩区的1G+used的数量,如果我将压缩区的大小改成2G,如下图所示:

图片

        可以看到reserved的值也相应的变大了。

  • G1HeapRegionSize

        上一集我介绍过G1垃圾回收器,它将整个堆划分成若干个大小相等的小区域,这个参数就是控制每个区域的大小的。现在看到它的值是0,是因为我们目前的垃圾回收器不是G1,我们换成G1后,如下图所示:

图片

        可以看到,G1HeapRegionSize默认值是1MB,因为最大堆内存是948MB,因此将整个堆划分成了948个区域。

        到此,截图中的参数就都介绍完了。JVM的内存参数根据回收器的不同还有很多,但这里不想多讲,原因是我并不建议花费大量的精力进行JVM内存的调优,原因在下面的优化方法中。

        最后,谈一下JVM内存的优化方法

        虽然JVM提供了很多内存参数,但我个人认为,对于一个新的应用程序不应使用任何参数,而是交给JVM的Ergonomics自动管理。内存也不是越大越好,内存越大,垃圾回收的时间就会越长,一切需要在吞吐率和暂停时间之间进行权衡,内存越大吞吐率就越大,但暂停时间也会越长,为了降低暂停时间,则需要选择适当的垃圾回收器。不建议随意的调整各种内存参数,以我的经验来看,大概率会越跳越差。

        总的来说,有如下几条原则:

  • 优先使用Ergonomics(不加任何参数),通过暂停时间和吞吐率目标来给Ergonomics提供自动优化建议。

  • 对程序进行测试,看Ergonomics的自动调整是否能够满足对吞吐率和暂停时间的要求(使用工具看gc的频率和暂停时间),若满足则无需调整。

  • 如果主机的cpu是单核的并且程序堆内存的需求在100mb以内,那么最好使用Serial。

  • 如果是单核,但内存超过100mb,可以考虑使用增量模式的CMS。

  • 如果多核,关注程序的性能,可以允许偶尔的暂停时间超过1秒,选择Parallel。

  • 如果持续快速的响应(暂停时间短)比吞吐率更重要,那么可以考虑CMS和G1(我个人倾向优先考虑G1)。

  • 如果对吞吐率和暂停时间都有要求,选择G1。

  • 如果堆内存非常大(数G以上),或者CPU的核数很多,选择G1。

  • 堆中超过50%的对象都是活着的,选择G1。

  • 对程序进行压力测试,判断默认最大堆内存是否够用,若不够,使用Xmx参数设置,观察程序的内存变化趋势,使用Xms调整初始堆内存。也可以倒着来,直接将初始内存和最大堆内存设置为能够设置的最大值(比物理内存要小一些,并且小于32G),然后观察暂停时间是否能够容忍。

  • 当程序所需的最大内存相对确定后,若对暂停时间可容忍,可将初始堆内存和最大堆内存设置为一样的值,避免Ergonomics自动扩缩容,减少损耗。

  • 通常,调整SurvivorRatio并不会对性能有太大的影响。

  • 若确实在有限内存下进一步优化,使用工具对堆中各个区域进行监控,适当改变年轻代和老年代的比例,或用NewSize和MaxNewSize设置具体的值。逐步增加年轻代的大小,但要保证老年代的Full GC不会太频繁。

  • 当增加处理器的核数后,适当的增大堆内存,如果内存无法增大,可适当增大年轻代的大小。

  • 对于G1回收器来说:1)避免显式的用Xmn参数或者相关参数(如:NewRatio)设置年轻代的固定大小,这样会阻碍达成暂停时间目标;2)G1会让应用程序的吞吐率下降10%左右,而Parallel只会下降1%,如果堆吞吐率有更高要求,可以放宽暂停时间目标。

        如果要进一步优化G1回收器,那我需要再补充点内容——G1的混合收集执行过程(G1的年轻代收集是用拷贝算法并行执行的)

  • 初始标记阶段(Initial marking):该阶段是在满足混合收集条件后,伴随年轻代收集的一个Stop-the-world过程,用于标记所有的根(roots)。

  • 根区域扫描阶段(Root region scanning):在上一个阶段标记的生存区中扫描对老年代的引用,然后标记被引用的对象。这个过程是并发执行的,但只有该阶段能结束,下一次年轻代回收才能开始。

  • 并发标记阶段(Concurrent marking):在整个堆中寻找可达的(reachable)活对象。这个过程是并发执行的,但会被年轻代回收所打断。

  • 重标记阶段(Remark):这个阶段是Stop-the-world的,使用SATB(snapshot-at-the-beginning(开始时的快照))算法,追踪没有访问到的活对象,处理他们的引用。

  • 清理阶段(Cleanup):首先会执行一个Stop-the-world的核算和RSet(Remembered Sets(就是上一集说的卡表))划分,这个过程用于识别空区域和识别混合收集的候选区域。清理阶段中一部分是并发的,用于重置和返回空区域。

        了解了混合收集过程后,来看看怎么优化混合收集过程

  • -XX:InitiatingHeapOccupancyPercent:这个参数用于设置混合收集的触发时机,上一集介绍过,默认值是45,表示当内存占用率达到整个堆内存的45%时触发混合收集,也就是开始执行初始标记阶段。

  • -XX:G1MixedGCLiveThresholdPercent和-XX:G1HeapWastePercent:这两个参数会影响混合收集中的一些决策。G1MixedGCLiveThresholdPercent默认值是85,表示当老年代的某个区域中的存活对象的比率低于85%,才会被作为清理的候选。G1HeapWastePercent的默认值是5,表示当发现可回收的对象超过整个堆内存的5%时,才会触发混合收集。

  • -XX:G1MixedGCCountTarget和-XX:G1OldCSetRegionThresholdPercent:这两个参数用于调整老年代的CSet(Collection Set),CSet是实际上就是每次G1增量的、并发的将活着的对象拷贝到另一个区域中时,这些对象原来所在区域的几何。G1MixedGCCountTarget默认值是8,表示清理阶段执行的次数。G1OldCSetRegionThresholdPercent默认是10,表示每次清理最多选取总堆内存的10%的老年代区域。

  • 当GC日志中出现“to-space overflow”或“to-space exhausted”时:1)增加-XX:G1ReservePercent的值,默认是10,表示预留总堆内存的10%的空间;2)减少-XX:InitiatingHeapOccupancyPercent参数,更早的触发混合收集;3)增加-XX:ConcGCThreads,用于设置并行标记的线程数量,默认是1/4的ParallelGCThreads参数值(ParallelGCThreads的默认值和Parallel收集器一样)。

        总结一下G1的优化:优化的参数很多,难度很大,不建议修改过多的默认值,建议当需求不满足或者出现问题时再针对性的优化。

        以上就是本集的全部内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值