JVM调优
java虚拟机内存模型
- 程序计数器:用于存放下一条运行的指令
- 虚拟机栈,本地方法栈:用于存放函数用堆栈信息
- java堆:存放程序运行时候需要的对象等数据方法区
- 方法区:用于存放程序的类元数据信息
程序计数器
- 是一块很小的空间,当我们用多线程时候,其实一个CPU一个时刻只能为一个现场提供服务,所以线程数超过CPU个数的时候,线程质检轮询争夺CPU资源。此时当一个线程被切换出去,为此需要程序计数器来记录这个独立线程运行到哪一个步骤指令,以便下一次轮询到继续执行的时候从这个步骤指令开始往下执行。
- 各个线程质检的计数器互补影响,独立工作,是一块线程私有的内存空间。
- 如果一个线程在执行java方法,程序计数器就在记录正在执行java字节码的地址,如果是一个Native方法,程序计数器为空。
java虚拟机栈
-
也是线程私有的内存空间,同java线程统一时刻创建,保存:局部变量,部分结果,并参与方法调用放回
-
java虚拟机运行栈空间动态边,如果java线程计算栈深度大于最大可用深度异常StackOverFlowException,如果java动态扩容到内存不够异常:OutOfMamoryException
-
可用-Xss参数控制栈大小
-
栈存储结构是栈帧,每个栈帧存放:
- 方法局部变量表
- 操作数栈
- 动态连接方法
- 返回地址
-
每个方法调用就是一次入栈,出栈过程,参数多,局部变量表就大,需要内存就大。如下图:
-
如上可看出,调用函数参数,局部变量越多,占用栈内存越大,导致调用次数比无参数时候下降5544–>4167
-
我们用idea中jclasslib插件看下class文件中的recursion方法
本地方法栈
- 本地方法栈和虚拟机栈类似,只不过是用来管理本地方法调用,本地方法并不是用java实现,而是C实现,SUN的HotSpot虚拟机中不区分本地方法栈和虚拟机栈。因此和虚拟机栈一样会抛出StackOverFlowError,OutOfMemoryError
java堆
-
java堆运行时内存,几乎所有对象和数组都在堆空间分配内存,堆内存分为新生代存放刚产生对象和年轻对象,老年代,存放新生代中一定时间内一直没有被回收的对象。如下
-
如上图,新生代分为三个部分:
- eden:对象刚建立时候存放的位置
- s0(surivivor space0 或者 from space),s1(survivor space1 或者通space):servivor意为幸存者空间,也就是存放其中的对象至少经历一次GC并新村。并如果幸存区到指定年龄还没被回收,则有机会进入老年代
方法区
- 方法区最重要的是类信息,常量池,域信息,方法信息。
- 类信息:类的名称,父类名称,类型修饰符(public/private/protected),类型的直接接口类表
- 常量池:类方法,域等信息引用的常量
- 域信息:域名称,域类型,域修饰符
- 方法信息:方法名,返回类型,方法参数,方法修饰符,方法字节码,操作数栈,方法栈帧的局部变量区大小以及异常表。
- HotSpot中方法去也成为永久区,GC对此部分也能回收,两点:
- GC对永久区常量池的回收
- 永久区对类元数据的回收
JVM内存分配参数
设置最大堆内存
- 用-Xmx指定最大堆内存,最大堆指的是新生代+ 老年代大小之和的最大值。
设置最小堆内存
- 用-Xms设置最小对内存
- 意义在于:Java启动优先满足-Xms的指定大小,当指定内存不够才向操作系统申请,直到触及Xmx导致OOM,
- -Xms过小,JMV为保证系统尽量在指定范围内存工作,只能频繁的GC操作来释放内存,间接导致MinorGC和FUllCc的次数,我们将-Xms与-Xmx设置成一样可以在系统运行初期减少GC次数和耗时。
设置新生代
- 用-Xmn设置新生代的大小,新生代也会对GC有比较大影响,新生代内存不够JVM会尝试GC清理,因此不合理的大小也有影响
- HotSpot虚拟机中,-XX:NewSize设置新生代初始大小,-XX:MaxNewSize设置新生代最大值,只设置-Xmn等效同时设置这两个参数。
设置永久代,方法区,元空间
- jdk1.7 中用-XX:MaxPermSize,-XX:PermSize,分别设置永久代最大,最小值,
- jdk1.8 中用-XX:MetaspaceSize,-XX:MaxMetaspaceSize=256m 设置永久代的最小,最大值
- 永久代直接决定未来系统可以支持多少个类的定义,以及多少个常量,当不指定时候最大方法去的内存就是当前系统最大可用内存
- 当XX:MetaspaceSize 设置过小(64位机器默认21M),会造成频繁的FullGC来卸载方法去中的无用类来腾出空间
- 我们一般设置一个较大的XX:MetaspaceSize(方法区初始值),不设置最大值
- 元空间与永久代区别是其内存空间直接使用的是本地内存,而metaspace没有了字符串常量池,而在jdk7的时候已经被移动到了堆中,MetaSpace其他存储的东西,包括类文件,在JAVA虚拟机运行时的数据结构,以及class相关的内容,如Method,Field道理上都与永久代一样,只是划分上更趋于合理,比如说类及相关的元数据的生命周期与类加载器一致,每个加载器就是我们常说的classloader,都会分配一个单独的存储空间。
设置线程栈
- 使用-Xss参数设置线程栈大小
- 栈过小,将会导致线程运行时候没有足够空间分配句柄变量,或者达不到足够深度的栈深度调用,导致StackOverFlowError。栈空间过大,那么将导致开启线程所需要的内存成本上升,系统所能支持线程总数下降。
堆比例分配
- 之前提了设置堆最大最小,新生代大小JVM参数配置,实际生产环境,希望能对对空间进行比例分配,有如下参数:
- 用-XX:SurivorRatio用来设置新生代中eden空间和from space,to space空间的比例关系,from空间和to空间大小相同,只能一样,并且在MinorGC后互换角色。
JVM参数总结
- -XX:SurvivorRatio=2 设置Eden与survivor区的比例
- -XX:NewRatio=2 设置老年代与新生代比例
- -Xms:堆内存初始大小
- -Xmx:堆内存最大大小
- -Xss:线程栈大小
- -XX:MinHeapFreeRatio:设置堆空间最小空闲比例,堆空间内存小于这个JVM会扩展堆空间
- -XX:MasHeapFreeRatio:设置堆空间最大空闲比例,当对空间空闲内存大于这个,会压缩堆空间,得到一个较小的堆
- -XX:NewSize :设置新生代大小
- -XX:MaxPermSize:设置最大持久区大小(方法区)
- -XX:PermSize : 设置永久区初始值(方法区)
- -XX:TargetSurvivorRatio:设置survivior区可使用率,当survivor区使用率达到这个数值,会将对象送入老年代。
//线上案例. zhenai-advertising-api
#!/bin/bash
/usr/bin/java -cp /data/dubbo/zhenai-advertising-api/classes:/data/dubbo/zhenai-advertising-api/libs-zhenai/*:/data/dubbo/zhenai-advertising-api/libs/*
-Xms2329m //最小堆 2.27G
-Xmx2329m //最大堆 2.27G
-Xss512k //线程栈大小512k
-XX:MetaspaceSize=256m //方法区初始值
-XX:MaxMetaspaceSize=256m //方法区最大值
-XX:+UnlockExperimentalVMOptions //docker配置
-XX:+UseCGroupMemoryLimitForHeap //docker配置
-Xmn1164m //新生代大小
-XX:+UseConcMarkSweepGC // 新生代使用并行收集器,老年代使用CMS收集器
-XX:+CMSParallelRemarkEnabled //采用并行标记方式降低停顿(默认开启)。
-XX:+UseCMSInitiatingOccupancyOnly //让下一个老年代比例配置生效
-XX:CMSInitiatingOccupancyFraction=70 //代表老年代堆空间的使用率,默认值为68,此处70表示达到70%使用率开始GC
-verbose:gc
-XX:PrintCMSStatistics=2
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintGCDateStamps
-XX:+PrintGCDetails
-XX:+PrintHeapAtGC
-XX:-OmitStackTraceInFastThrow
-Xloggc:/data/dubbo/logs/common/zhenai-advertising-api/jvm.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dubbo/logs/common/zhenai-advertising-api
-XX:ErrorFile=/data/dubbo/logs/common/zhenai-advertising-api/hs_err_pid%p.log -Dserver.port=8080
-Dmanagement.server.port=8088
-Dmanagement.server.address=127.0.0.1
-Dmanagement.endpoints.web.exposure.include=*
-Djava.security.egd=file:/dev/./urandom
-Dhttp.maxConnections=200
-Dfile.encoding=UTF-8
-Ddubbo.protocol.port=9090
-Ddubbo.application.logger=slf4j
-Ddubbo.application.qos.enable=true
-Ddubbo.application.qos.port=9099
-Ddubbo.application.qos.accept.foreign.ip=false
-Dlog4j2.formatMsgNoLookups=true
-Dpinpoint.statlog.center=tx-bj
-Dpinpoint.statlog.namespace=common
-Dpinpoint.statlog.project=zhenai-advertising-api
-Dapp=zhenai-advertising-api
-javaagent:/data/dubbo/pinpoint-agent-1.8.5/pinpoint-bootstrap-1.8.5.jar
-Dpinpoint.agentId=advertising-a-6-115
-Dpinpoint.applicationName=advertising-api
-Dpinpoint.container com.zhenai.advertising.Application
--spring.profiles.active=online
//线上案例 zhenai-mobile-api
#!/bin/bash
/usr/bin/java -cp /data/dubbo/zhenai-mobile-api/classes:/data/dubbo/zhenai-mobile-api/libs-zhenai/*:/data/dubbo/zhenai-mobile-api/libs/*
-Xms5734m // 最小堆内存5G+
-Xmx5734m // 最大堆内存5G+
-Xss512k // 线程栈,一个线程分配512k
-XX:MetaspaceSize=512m //方法区最小512
-XX:MaxMetaspaceSize=512m // 方法区域最大 512
-XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap
-XX:+UseG1GC // 使用G1 垃圾收集器
-XX:MaxGCPauseMillis=100 // 设置GC暂停等待时间,单位为毫秒
-XX:+ParallelRefProcEnabled
-XX:+PrintClassHistogramBeforeFullGC
-XX:+PrintClassHistogramAfterFullGC
-XX:+PrintGCApplicationConcurrentTime
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintTenuringDistribution
-XX:+PrintHeapAtGC
-XX:+PrintGCDateStamps
-XX:-OmitStackTraceInFastThrow
-Xloggc:/data/dubbo/logs/zhenai/zhenai-mobile-api/jvm.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dubbo/logs/zhenai/zhenai-mobile-api
-XX:ErrorFile=/data/dubbo/logs/zhenai/zhenai-mobile-api/hs_
err_pid%p.log -Dserver.port=8080
-Dmanagement.server.port=8088
-Dmanagement.server.address=127.0.0.1
-Dmanagement.endpoints.web.exposure.include=*
-Djava.security.egd=file:/dev/./urandom
-Dhttp.maxConnections=200
-Dfile.encoding=UTF-8
-Ddubbo.protocol.port=9090
-Ddubbo.application.logger=slf4j
-Ddubbo.application.qos.enable=true
-Ddubbo.application.qos.port=9099
-Ddubbo.application.qo
s.accept.foreign.ip=false
-Dlog4j2.formatMsgNoLookups=true
-Dpinpoint.statlog.center=tx-bj
-Dpinpoint.statlog.namespace=zhenai
-Dpinpoint.statlog.pro
ject=zhenai-mobile-api -Dapp=zhenai-mobile-api
-javaagent:/data/dubbo/pinpoint-agent-1.8.5/pinpoint-bootstrap-1.8.5.jar
-Dpinpoint.agentId=mobile-a-12-121
-Dpinpoint.applicationName=mobile-api
-Dpinpoint.container com.zhenai.mobile.Application
--spring.profiles.active=online
垃圾收集基础
垃圾收集作用
- Java和C++最大区别就是C++需要手动回收分配的内存,但是Java使用了垃圾收集器替代C++的手工管理方式减少程序员负担,减少出错几率。因此GC需解决一下问题:
- 那些对象需要回收
- 什么时候回收
- 怎么回收
GC回收算法与思想
引用计算算法
- 最古老的的GC算法,对象A,只要有任何对象引用了A就计数器+1,任何取消A引用-1,当引用为0,A就可以被清除
- 弊端在于无法解决相互引用的死局,例:A,B相互引用,但是没有任何第三方引用,根据算法是不可被回收,这种情况导致内存泄露。
- 不适合作为JVM的GC策略
标记清除算法(Mark-Sweep)
- 分两个步骤:
- 标记:通过根节点(java虚拟机栈中对象的引用)标记从根节点开始可达对象,未被标记则是可回收对象
- 清除:清除未被标记的对象
- 弊端:清理后产生空间碎片,空间是非连续空间,大对象分配时候,不连续内存空间工作效率更低。
复制算法(Copying)
-
基于标记算法将原内存分两块,每次用其中一块,GC时候,将存活对象复制到另一块内存中,清楚原来内存块中所有对象,然后交换角色。
-
优势:如果系统中垃圾对象多,复制算法需要复制的就少,此时复制算法效率最高,并且统一复制到某一块内存,不会有内存碎片。
-
弊端:内存折半,负载自然降低。
-
用处: Java新生代串行垃圾回收器使用复制算法,Eden空间,from,to空间三部分,from = to空间,天生就有区域划分,并且from与to空间角色互换。因此GC时候,eden空间存活对象复制到survivor空间(假设是to),正在使用的survivor(from)中的年前对象复制到to,大对象to放不下直接进入old区域,to满了,其他放不下的直接去old区域,之后清理eden与from去。
-
体现复制算法优势在新生代的合理性,新生代存活对象比例小,复制算法效果更佳
标记压缩算法(Mark-Compact)
- 一个老年代的回收算法
- 标记:和之前一样的通过根节点可达性分析获取被引用的对象,做标记
- 压缩:清理没标记对象并且将存活对象压缩到内存的一端,接着清理边界外所有空间,避免碎片产生
- 标记压缩,避免碎片化的同时不需要进行内存空间减半的风险,因此性价比高
- 老年代中存活对象多,不使用与复制算法,因此可以用标记压缩方式。
增量算法(Incremental Collecting)
- 为解决Stop the World状态提出的一种思想,GC时间长影响用户体验,
- 增量算法思想:一次性完成GC需要更多时间,我们让GC收集线程与应用线程交替执行,每次收集一部分区域,将对系统影响降到最低
- 弊端:线程切换和上下文切换的消耗一定程度影响应用程序性能是系统吞吐量下降,使GC总体成本上升
分代收集(Generational Collecting)
- 主要思想:按每个GC算法不同给合适的区域使用对应的GC算法。
- HotSpot案例:
- 年轻代:对象存活度低,使用复制算法,经过N次后,存活对象进入old区
- 老年代:对象存活度高,使用标记压缩算法,提高GC效率
堆外内存泄漏排查
-
对外内存,是指Java应用成通过直接方式从操作系统申请内存,也叫堆外内存,因为这些对象分配在java虚拟机的堆以外。
-
直接内存有哪些:
- 元数据区域,存储了类信息,方法信息,常量池
- Java NIO中的DirectByteBuffer 直接分配的堆外内存,可以通过XX:MaxDirectMemorySize 设置最大可分配空间,没有设置的话就是 -Xmx 减去一个Survivor的大小
- 使用java 中 Unsafe 类做的一些内存分配操作:unsafe.allocateMemory 这种不受任何参数控制,可能造成OOM
- JNI或者JNA程序,直接操作了本地内存,比如一些加密库,压缩解压缩等(GZIPInputStream,GZIPOutputStream)
G1 的缺点
- 相对比与CMS,G1 不具备全方位优势。与CMS对比,G1 无论是在内存占用,还是额外执行负载都要比CMS高
- 在使用经验上来说,小内存应用上CMS表现大概率会优于G1,而G1 在大内存上更发挥优势,平衡点在6~8G之间。
什么时候用G1
- 存活对象占用到堆内存空间的50%(G1 CSet 与RSet)
- 对象分配晋升速度变化大(GC频繁)
- 垃圾回收时间长(0.5 ~ 1s)