1、前言

在没有接触微服务之前,我们的java程序一般都部署在WebLogic、Tomcat这类应用服务器上,这些应用服务器本身也是基于Jvm虚拟机的。一般我们统一对应用服务器做Jvm参数调优(分配多大内存,线程池限制等),而不用考虑每个部署在服务器上的java程序。

但是微服务和Spring的兴起,让这一切都变了。一个简单的SpringBoot应用在运行时,都有一个独立并且完整的Tomcat服务器。这对开发人员来说很开心,不用考虑上线后程序运行环境问题。但是对于运维人员就要很谨慎,每个SpringBoot程序都要考虑资源分配和Jvm调优问题。
下面我们来看看我轻身经历的两个案例,具体的案例分析在本文的后面再开始:

1.1、案例一:本地Idea运行SpringBoot

本地windows电脑16G内存(15.7G可用)。现在打开 Idea,运行任意一个普通的,没有主动配置 Jvm参数的 SpringBoot 程序。
打开jdk自带的监控工具 - jvisualvm.exe ,找到刚刚运行的SpringBoot 进程。可用看到堆最大内存是4223664128字节,约等于3.933G,正好是电脑可用内存15.7G的1/4。

1.2、案例二:微服务部署SpringBoot

将一个普通的 SpringBoot 程序生成docker镜像,部署在kubernetes服务器上,jdk的版本是 Java 8u111,Dockerfile中同样没有配置 Jvm参数限制。但我们在pod的yaml文件中有设置资源限制 - 最大内存1G,除去pod中的pause容器,意味着这个SpringBoot所在容器的内存是小于1G的。

当这个SpringBoot 的微服务运行起来后,我们进入容器内部,通过 jmap -heap pid 命令查询到,该应用的堆最大内存是 8G多,都超过docker 容器本身的内存。这样造成的结果是,可能会在某一时刻应用的实际内存超过了docker容器的内存,程序就会报 OutOfMemory,然后pod就会重启。

2、Jvm调优

Jvm调优的学问很深,如果感兴趣可以找一本有关Jvm虚拟机的书先看看,本章就只讲一些简单的内存限制和垃圾收集器选择。

2.1、基础概念

Jvm(Java Virtual Machine)即Java虚拟机,java代码编译后的字节码(class)文件,就通过Jvm解释成平台能识别的机器码。JVM把内存区分为堆区(heap)、栈区(stack)和方法区(method)。本文主要讲解JVM调优,而JVM中只有堆区中存放的对象是需要被GC的,我们侧重讲堆区。JVM实质上分为三大块,年轻代(YoungGen),年老代(Old Memory),及永生代(Perm,在Java8中被取消,我们不做深入介绍)。

JDK 1.7及以前,Java类信息、常量池、静态变量都存储在永久代(Perm)里。类的元数据和静态变量在类加载的时候分配到Perm,当类被卸载的时候垃圾收集器从Perm处理掉。
JDK 1.8 的对 JVM 架构的改造将类元数据放到本地内存中,另外,将常量池和静态变量放到Java堆里。HotSopt VM 将会为类的元数据明确分配和释放本地内存。这样就从一定程度上解决了原来在运行时生成大量类的造成经常Full GC问题,如运行时使用反射、代理等。
jvm分代.jpg

2.2、方法区

方法区(Method Area),与java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类型信息,常量,静态变量,及时编译后的代码缓存等数据

java虚拟机规范中明确说明:"尽管所有的方法区在逻辑上是属于堆的一部分,但是一些简单的实现可能不会选择去进行垃圾回收或进行压缩",但是对于HotSpot JVM 而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆区分开来

所以,方法区看作是一块独立于堆的内存空间。

方法区,元空间,永久代三者关系是什么呢?方法区是java虚拟机规范的一部分,而元空间和永久代是一个具体的实现,元空间的本质和永久代类似,都是对JVM规范方法区的实现,不过两者最大的区别就是:元空间不在虚拟机中设置内存,而是直接使用本地内存

方法区的演进细节:

  • jdk1.6即之前:有永久代(permanent generation),静态变量存放在永久代上
  • jdk1.7: 有永久代,但是已经逐步"去永久代",字符串常量池,静态变量移除,存放在堆空间中
  • jdk1.8及以后:无永久代,类型信息,字段,方法保存在本地内存的元空间,但字符串常量池,静态变量仍在堆空间

永久代为什么要替换为元空间:

  • 永久代设置空间大小难以确定,如果设置比较小容易发生FullGC影响程序性能,而且容易出现OOM,如果过大又占用内存
  • 对永久代的调优是很困难的

2.2、内存调优

通过上面的Jvm堆区的图,我们可以看到几个参数,下面来介绍一下。

  • -Xms(-XX:InitialHeapSize):JVM启动时申请的初始Heap值,默认为操作系统物理内存的1/64但小于1G。默认当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过-XX:MaxHeapFreeRation=来指定这个比列。Server端JVM最好将-Xms和-Xmx设为相同值,避免每次垃圾回收完成后JVM重新分配内存
  • -Xmx(-XX:MaxHeapSize):JVM可申请的最大Heap值,默认值为物理内存的1/4,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列。最佳设值应该视物理内存大小及计算机内其他内存开销而定。
  • -Xmn :Java Heap Young区大小。整个堆大小=年轻代大小 + 年老代大小 + 永生代大小(Java8移除)。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

这就很好理解案例一的情况了,在没有设置 -Xmx的前提下,Jvm默认最大堆内存是机器物理内存的1/4。

2.3、垃圾收集器

image.png

图中实际上是分了三类,年轻代的收集器、老年代的收集器和G1收集器,G1收集器比较特殊,本身支持新的分代收集算法,逻辑上并没有年轻代和老年代的区分。

  • jdk1.7/1.8,默认垃圾收集器的搭配,一般都是:Parallel Scavenge + Parallel Old
  • jdk1.9之后,默认垃圾收集器是:G1

有关垃圾收集器的详细说明,可以参考这篇文章 《垃圾回收器的种类及优缺点》

3、微服务资源分配

3.1、基础概念

现在很多企业都在上微服务,也就有很多基于kubernetes做容器化部署的产品被推出了,像rancher、openshift,还有阿里的云效平台。我们项目上用的是daocloud的产品,在部署服务时会要求设置资源限制,内存多少G?CPU多少核?初学者很容易就被误解。

kubernetes中创建或部署的最小单位就是pod,每个pod都可以设置能使用服务器上资源量,可以有Requests(该资源最小申请量)和Limits(该资源最大允许使用量),资源内容包含内存Memory和CPU。内存限额大家基本都了解,可是CPU限额是怎么回事?如果我给这个服务所在的pod分配CPU限额是1个单位,那是不是就意味着我的服务只能单线程运行了?

在这里CPU的资源单位为CPU(Core)的数量,是一个绝对值而非相对值。在Kubernetes里,通常以千分之一的CPU配额为最小单位,用m来表示。例如给容器限制了300m,即占用0.3个CPU。由于CPU配额是绝对值,所以无论服务器是单核CPU,还是32核CPU,300m代表的CPU使用量都是一样的,只不过在32核CPU的机器上多线程的使用效率更高而已。

3.2、Jvm容器感知

在刚接触docker的时候,网上都说把它可以直接理解成一个虚拟的服务器。那么在分析案例二的时候就会觉得奇怪,我现在这个“服务器”最大内存只有1G,默认Jvm可获取的最大堆空间是1G的1/4啊,为什么会变成8G多?很明显Jvm虚拟机并没有意识到自己处于这个一个虚拟的“服务器”中,没有感知到外部的容器。

这个其实也可以理解,我们常用的Java 8 早期版本,在2014年就出来了,甚至于有些项目还在用Java 6,而docker在近些年才出来,要想让Jvm自己能支持docker的各种特性的确不太可能。大家可以记住下面几个Jdk的版本节点,尤其是Java 8u191 和Java 10以后的版本,就已经默认开启UseContainerSupport。

  1. Java 5/6/7/8u131 -:不支持容器感知,只能通过设置 -Xmx 来控制内存。
  2. Java 8u131 +/Java 9 +:支持容器感知,通过 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap 参数来开启。通过 -XX:MaxRAMFraction 来设置Jvm最大内存,值1/2/3,分别表示占容器最大内存的100%/50%/25%。
  3. Java 8u191 +/Java 10 +:容器感知默认开启。-XX:{Min|Max}RAMFraction 被弃用,引入了-XX:MaxRAMPercentage来代替,其值介于0到100之间,默认值为25,即占容器最大内存的25%。

这样一来我们就可以理解案例二的情况了,Java 8u111版本低于Java 8u191,并没有开启容器感知,所以Jvm的最大堆内存才会有那么大。现在Java 8的使用比较多,你可以去 Java发行版官网 ,查询你正在使用的Java版本是否已开启容器感知。


KerryWu
558 声望149 粉丝

保持饥饿