jvm内存结构/java运行时数据区
JVM调优
调优流程:
- 压测,可以增加代码或者使用工具,比如LoadRunner、jmeter
- 使用工具,监控jvm虚拟机运行,生成堆快照和日志,awr 报告,分析结果
- 性能瓶颈定位,比如内存瓶颈、cpu瓶颈、代码缺陷、内存泄漏、JVM频繁gc
- 优化代码(线程池、集合对象使用不对、连接池)或者调整内存/gc配置。
GC优化
jvm调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量或用户体验。
指标定义:
- 吞吐量
- 延迟
- 内存占用
目的:
- MinorGC回收原则: 每次minor GC 都要尽可能多的收集垃圾对象。以减少应用程序发生Full GC的频率。
- 减少垃圾回收次数
2、将转移到老年代的对象数量降低到最小;
3、减少full GC的执行时间;
一般程序优化
1、减少使用全局变量和大对象;
2、调整新生代的大小到最合适;
3、设置老年代的大小为最合适;
4、选择合适的GC收集器;
参考数字
注:如果满足下面的指标,则一般不需要进行GC:
Minor GC执行时间不到50ms;
Minor GC执行不频繁,约10秒一次;
Full GC执行时间不到1s;
Full GC执行频率不算频繁,不低于10分钟1次;
场景1
现象:系统报错java.lang.OutOfMemoryError:GC overhead limit exceeded异常
原因:堆太小
解决方案:把-Xmx设置大点
场景2
现象:系统经常卡顿
原因:新生代和老生代大小之比为1:9,新生代太小,导致对象提前进入老年代,触发老年代发生Full GC,而且老年代较大,进行Full GC时耗时较大
解决方案:优化的方法是调整NewRatio的值,调整到4,这就是把对象控制在新生代就清理掉,没有进入老年代
场景3
现象:一应用在性能测试过程中,发现内存占用率很高,Full GC频繁
原因:使用jmap生成dump文件,使用mat分析,发现使用了队列LinkedBlockingQueue,所引用的大量对象并未释放,导致整个线程占用内存高达378m
解决方案:代码优化,将相关对象手动释放掉即可。
- 垃圾回收器调优
- 内存分配参数调优
- 内存泄漏解决
- 内存溢出解决
- 虚拟机监控工具
- 程序设计调优
- 系统架构调整,负载均衡
- 硬件资源升级
垃圾回收
如何查看JVM GC情况
- 打印gc日志
-XX:+PrintGCDetails 打印GC信息
-XX:+PrintGCDateStamps 打印GC时间
- jstat可以查看本地或远程虚拟机ID对于的gc情况
//jvm gc查看
jstat -gcutil pid interval(ms)
- visualVM工具监控
- jstack获取线程转储文件
- jmap获取堆转储文件
问题
- 垃圾回收过于频繁,很多时候是因为堆内存分配不合理导致,比如没有设置-Xmx,每次内存不够,老年代太小引发full gc,进行内存拓展。
- 停顿时间过长,假如cpu资源富余,可以使用cms来减低停顿时间,增加用户体验。
直接内存溢出
可以通过-XX:MaxDirectMemorySize指定直接内存大小。
如果不指定,且操作系统内存大小允许,直接内存大小与堆最大内存大小(-Xmx)一样。
一般情况下,如果是直接内存导致的OutOfMemoryError,一个明显的特征是Heap Dump文件很小,看不出异常,这样大致可以推断是直接内存溢出。
方法区溢出
可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小。
OutOfMemoryError:PermGen space一般有两种情况
- 常量池满,比如String.intern()方法可以将大量字符串复制到方法区字符常量池
- 加载太多Class,比如很多动态类加载技术,jdk动态代理、CGLib、Spring、Hibernate、JSP文件、字节码生成技术会动态生成class加载到方法区
方法区只会在full gc时才去回收,回收的主要目标是常量池和类型卸载,但是类型卸载条件太苛刻,所以方法区的回收成绩很差。
jdk1.8已经把方法区废掉。
栈溢出两种情况
- -Xss参数设置每个栈的内存大小, JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。如果线程请求的栈深度大于虚拟机允许的最大深度,将抛出StackOverflowError异常;
- 如果虚拟机在拓展栈时无法申请足够的内存空间,则抛出OutOfMemoryError异常,就针对虚拟机运行而言,栈可申请的内存大小 = 操作系统内存 - 堆 - 方法区 - 直接内存;
在相同物理内存下,减小-Xss这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
内存溢出
jmap或-XX:+HeapDumpOnOutOfMemoryError在获取内存堆转储快照文
使用visualVM或者MAT工具分析堆转储快照
分析GC Roots的引用链,排查弱/软引用,如果存在不需要的对象没有被回收,则定位是内存泄漏
如果不存在内存泄漏,检查堆参数(-Xmx与-Xms),结合物理内存大小,看是否可以调大堆大小。
各种内存/堆/栈/方法区溢出案例参考:
https://www.cnblogs.com/dingyingsi/p/3760447.html
垃圾回收
判断哪些对象需要回收:
- 引用计数算法
- 可达性分析算法
垃圾回收算法:
- 标记和清除(mark-and-sweep)算法:标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间,这个算法问题就是容易产生内存碎片。
- 复制算法:复制算法的提出是为了克服句柄的开销和解决内存碎片的问题,缺点是浪费了一般的内存空间,还有当存活对象较多时效率低。
- 标记-整理(Mark-Compact)算法:标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
- 分代收集(Generation)算法:它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域,一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
垃圾回收器:
JVM参数
- 堆内存
//堆内存 = 新生代(eden+survivor1+survivor2)+老年代
//默认 eden : survivor1 : survivor2 = 8 : 1 : 1
//官方推荐 新生代 : 老年代 = 3 :5
-Xms:堆初始化内存,默认是物理内存的1/64,空余堆内存小于40%时扩充到Xmx。
-Xmx:堆最大内存,默认是物理内存的1/4,空余堆内存大于70%时缩小到Xms。
-Xmn:年轻代内存大小
-XX:NewSize:年轻代初始大小
-XX:MaxNewSize:年轻代最大大小
-XX:NewRatio:年轻代与老年代的比值,4代表年轻代与老年代1:4
-XX:SurvivorRatio:年轻代中Eden区与Survivor区的比值,8代表eden和survivor比例8:1。
####疑问解答
-Xmn,-XX:NewSize/-XX:MaxNewSize,-XX:NewRatio 3组参数都可以影响年轻代的大小,混合使用的情况下,优先级是什么?
如下:
高优先级:-XX:NewSize/-XX:MaxNewSize
中优先级:-Xmn(默认等效 -Xmn=-XX:NewSize=-XX:MaxNewSize=?)
低优先级:-XX:NewRatio
推荐使用-Xmn参数,原因是这个参数简洁,相当于一次设定 NewSize/MaxNewSIze,而且两者相等,适用于生产环境。-Xmn配合 -Xms/-Xmx,即可将堆内存布局完成。
-Xmn参数是在JDK 1.4 开始支持。
- 虚拟机栈内存
//设置虚拟机栈内存:是每个线程使用栈的内存最大大小
-Xss:设置每个栈的内存大小, 默认JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。
在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。需要注意的是:当这个值被设置的较大(例如>2MB)时将会在很大程度上降低系统的性能。
- 持久代(方法区)
-XX:PermSize:持久代的初始值,默认为物理内存的1/64。
-XX:MaxPermSize:持久代的最大值,默认为物理内存的1/4。
- 元空间
jdk8取消了方法区,使用元空间代替,元空间存储在本地内存。
-XX:MetaspaceSize=128m:元空间的初始内存大小
-XX:MaxMetaspaceSize=512m:元空间最大内存大小
- 直接内存
-XX:MaxDirectMemorySize:设置最大直接内存
- GC参数
//串行收集器
-XX:+UseSerialGC:使用Serial + Serial Old,这是Client模式下默认值。
-XX:+UseParNewGC:使用ParNew + Serial Old收集器。
//并行收集器(吞吐量优先)
-XX:+UseParallelGC:使用Parallel Scavenge + Serial Old,这是Server模式下默认值。
-XX:+UseParallelOldGC:使用Parallel Scavenge + Parallel Old收集器。
-XX:ParallelGCThreads=20:并行收集器的线程数,即:同时有多少个线程一起进行垃圾回收。此值建议配置与CPU数目相等。
-XX:MaxGCPauseMillis=100:每次年轻代垃圾回收的最长时间(单位毫秒)。如果无法满足此时间,JVM会自动调整年轻代大小,以满足此时间。
-XX:GCTimeRatio=99:GC时间占总时间的比率,默认值为99,即允许1%的GC时间。
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动调整年轻代Eden区大小和Survivor区大小的比例,以达成目标系统规定的最低响应时间或者收集频率等指标。此参数建议在使用并行收集器时,一直打开。
//并发收集器(响应时间优先)
-XX:+UseConcMarkSweepGC:使用ParNew + CMS + SerialOld收集器。
-XX:+UseCMSCompactAtFullCollection:打开内存空间的压缩和整理,在Full GC后执行。可能会影响性能,但可以消除内存碎片。
-XX:CMSFullGCsBeforeCompaction=0:多少次Full GC后对内存空间进行整理来清除内存碎片,设置为0次,即每次Full GC后立刻开始压缩和整理内存。
-XX:CMSInitiatingOccupancyFraction=70:表示年老代内存空间使用到70%时就开始执行CMS收集,让年老代有空间接纳年轻代的对象,避免full gc。
//G1收集器(取代CMS)
-XX:+UseG1GC:使用Garbage First (G1)收集器
//其它
-XX:+MaxTenuringThreshold=15:代表对象在Survivor区经过15次复制以后才进入老年代。如果设置为0,则年轻代对象不经过Survivor区,直接进入老年代。
-XX:PretenureSizeThreshold=3145728:直接晋升老年代的对象大小阈值,大于这个参数值的对象将直接在老年代分配,这里设置为3MB,默认值是0,意思是不管多大都是先在eden中分配内存。
- 日志打印
-XX:-HeapDumpOnOutOfMemoryError:当首次遭遇内存溢出时Dump出此时的堆内存。
-XX:-PrintCompilation:当一个方法被编译时打印相关信息。
-XX:-PrintGC:每次GC时打印相关信息。
-XX:-PrintGCDetails:每次GC时打印详细信息。
-XX:-PrintGCTimeStamps:打印每次GC的时间戳。
-XX:-TraceClassLoading:跟踪类的加载信息。
-XX:-TraceClassLoadingPreorder:跟踪被引用到的所有类的加载信息。
-XX:-TraceClassResolution:跟踪常量池。
-XX:-TraceClassUnloading:跟踪类的卸载信息。
G1和CMS收集器的比较
网上有测试称,在相同的应用程序测试,G1和CMS相比,吞吐量下降了3.5%,停顿时间减少20%。
CMS仍然是最佳的默认选择,G1的吞吐量实在是太差,和它所减少的那点时延相比并不划算。
年轻代和年老代设置多大才合理?
- 更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间,小的年老代会导致更频繁的Full GC;
- 更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短,大的年老代会减少Full GC的频率
如何选择应该依赖应用程序对象生命周期的分布情况:
- 如果应用存在大量的临时对象,应该选择更大的年轻代;
- 如果存在相对较多的持久对象,年老代应该适当增大。
但很多应用都没有这样明显的特性,在抉择时应该根据以下两点:
(A)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理
(B)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间
操作系统影响堆大小
jvm内存大小大小受操作系统的限制,32位操作系统,理论上堆最大可用内存为4G,但实际上最多只能达到1.4G至1.6G。
1、在32位solaris的机器上,堆最大可以达到2G
2、在64位的操作系统上,32位的JVM,堆大小可以达到4G
MAT
Memory Analyzer tool(MAT)是eclipse的一个内存分析插件
https://www.cnblogs.com/cnmenglang/p/6261435.html
分析原因
- 内存泄漏(Memory Leak),如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。
- 内存溢出(Memory Overflow),如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。