JVM 调优的一些思考和总结
这篇文章不讲基本的 JVM 知识点,需要知道的储备知识有:
- JVM 的内存区域
- JVM 的常见工具
- JVM 的调优参数
首先贴一张 JVM 的架构图方便基础忘记的小伙伴来快速回忆
.png?token=AHCUHWNL6QFJWJO3PQ4ZAPDDMHRHI)
方法区(Method Area)
所有类级别数据将被存储在这里,包括静态变量。每个JVM只有一个方法区,它是一个共享的资源。
堆区(Heap Area)
所有的对象和它们相应的实例变量以及数组将被存储在这里。每个JVM同样只有一个堆区。由于方法区和堆区的内存由多个线程共享,所以存储的数据不是线程安全的。
栈区(Stack Area)如果想详细了解方法栈的底层机构,可以去看我另一篇文章 JVM 内存结构
对每个线程会单独创建一个运行时栈。对每个函数呼叫会在栈内存生成一个栈帧(Stack Frame)。所有的局部变量将在栈内存中创建。栈区是线程安全的,因为它不是一个共享资源。栈帧被分为三个子实体:
- 局部变量数组 – 包含多少个与方法相关的局部变量并且相应的值将被存储在这里。
- 操作数栈 – 如果需要执行任何中间操作,操作数栈作为运行时工作区去执行指令。
- 帧数据 – 方法的所有符号都保存在这里。在任意异常的情况下,catch块的信息将会被保存在帧数据里面。如上是JVM三大核心区域
内存溢出
内存溢出的分类
栈溢出
我们由上面里了解到了栈内存中都包含着哪些数据信息:
一个线程一个方法栈,一个线程中执行的方法形成一个栈帧(栈的一个元素),一个方法中的数据就组成了一个栈帧:局部变量,操作数,返回值等。
栈溢出主要有两个方面:
- 线程太多导致方法栈的分配不够导致栈溢出
- 同一个线程中,栈帧分配太多导致栈溢出,这个主要就是栈帧数量太多(方法调用太深)
明白了这两个远离就能够有效避免栈溢出:
- 避免创建过多线程(一般不会出现)
- 谨慎使用递归,避免递归调用导致栈帧太多造成栈溢出
堆溢出
对内存中的数据,主要是创建的对象数据和数组,所以说,对内存溢出主要是由于大型对象和对象数量过多导致的。
对内存是 JVM 进行 GC 的区域,里面分为 新生代 和 老年代两部分。
避免堆内存溢出就要使用到 JVM 的参数了:
参数名称 含义 默认值 -Xms 初始堆大小 物理内存的1/64(<1GB) 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制. -Xmx 最大堆大小 物理内存的1/4(<1GB) 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制 -Xmn 年轻代大小(1.4or lator) 注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。 整个堆大小=年轻代大小 + 年老代大小 + 持久代大小. 增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8 -XX:NewSize 设置年轻代大小(for 1.3/1.4) -XX:MaxNewSize 年轻代最大值(for 1.3/1.4) -XX:PermSize 设置持久代(perm gen)初始值 物理内存的1/64 -XX:MaxPermSize 设置持久代最大值 物理内存的1/4 -Xss 每个线程的堆栈大小 JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右 一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长) 和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"” -Xss is translated in a VM flag named ThreadStackSize” 一般设置这个值就可以了。 -XX:ThreadStackSize Thread Stack Size (0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.] -XX:NewRatio 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5 Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。 -XX:SurvivorRatio Eden区与Survivor区的大小比值 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10 -XX:LargePageSizeInBytes 内存页的大小不可设置过大, 会影响Perm的大小 =128m -XX:+UseFastAccessorMethods 原始类型的快速优化 -XX:+DisableExplicitGC 关闭System.gc() 这个参数需要严格的测试 -XX:MaxTenuringThreshold 垃圾最大年龄 如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率 该参数只有在串行GC时才有效. -XX:+AggressiveOpts 加快编译 -XX:+UseBiasedLocking 锁机制的性能改善 -Xnoclassgc 禁用垃圾回收 -XX:SoftRefLRUPolicyMSPerMB 每兆堆空闲空间中SoftReference的存活时间 1s softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap -XX:PretenureSizeThreshold 对象超过多大是直接在旧生代分配 0 单位字节 新生代采用Parallel Scavenge GC时无效 另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象. -XX:TLABWasteTargetPercent TLAB占eden区的百分比 1% -XX:+CollectGen0First FullGC时是否先YGC false 并行收集器相关参数
-XX:+UseParallelGC Full GC采用parallel MSC (此项待验证) 选择垃圾收集器为并行收集器.此配置仅对年轻代有效.即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集.(此项待验证) -XX:+UseParNewGC 设置年轻代为并行收集 可与CMS收集同时使用 JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值 -XX:ParallelGCThreads 并行收集器的线程数 此值最好配置与处理器数目相等 同样适用于CMS -XX:+UseParallelOldGC 年老代垃圾收集方式为并行收集(Parallel Compacting) 这个是JAVA 6出现的参数选项 -XX:MaxGCPauseMillis 每次年轻代垃圾回收的最长时间(最大暂停时间) 如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值. -XX:+UseAdaptiveSizePolicy 自动选择年轻代区大小和相应的Survivor区比例 设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开. -XX:GCTimeRatio 设置垃圾回收时间占程序运行时间的百分比 公式为1/(1+n) -XX:+ScavengeBeforeFullGC Full GC前调用YGC true Do young generation GC prior to a full GC. (Introduced in 1.4.1.) CMS相关参数
-XX:+UseConcMarkSweepGC 使用CMS内存收集 测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明.所以,此时年轻代大小最好用-Xmn设置.??? -XX:+AggressiveHeap 试图是使用大量的物理内存 长时间大内存使用的优化,能检查计算资源(内存, 处理器数量) 至少需要256MB内存 大量的CPU/内存, (在1.4.1在4CPU的机器上已经显示有提升) -XX:CMSFullGCsBeforeCompaction 多少次后进行内存压缩 由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理. -XX:+CMSParallelRemarkEnabled 降低标记停顿 -XX+UseCMSCompactAtFullCollection 在FULL GC的时候, 对年老代的压缩 CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。 可能会影响性能,但是可以消除碎片 -XX:+UseCMSInitiatingOccupancyOnly 使用手动定义初始化定义开始CMS收集 禁止hostspot自行触发CMS GC -XX:CMSInitiatingOccupancyFraction=70 使用cms作为垃圾回收 使用70%后开始CMS收集 92 -XX:CMSInitiatingPermOccupancyFraction 设置Perm Gen使用到达多少比率时触发 92 -XX:+CMSIncrementalMode 设置为增量模式 用于单CPU情况 -XX:+CMSClassUnloadingEnabled 辅助信息
-XX:+PrintGC 输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs] -XX:+PrintGCDetails 输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs 121376K->10414K(130112K), 0.0436268 secs] -XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps 可与-XX:+PrintGC -XX:+PrintGCDetails混合使用 输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs] -XX:+PrintGCApplicationStoppedTime 打印垃圾回收期间程序暂停的时间.可与上面混合使用 输出形式:Total time for which application threads were stopped: 0.0468229 seconds -XX:+PrintGCApplicationConcurrentTime 打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用 输出形式:Application time: 0.5291524 seconds -XX:+PrintHeapAtGC 打印GC前后的详细堆栈信息 -Xloggc:filename 把相关日志信息记录到文件以便分析. 与上面几个配合使用 -XX:+PrintClassHistogram garbage collects before printing the histogram. -XX:+PrintTLAB 查看TLAB空间的使用情况 XX:+PrintTenuringDistribution 查看每次minor GC后新的存活周期的阈值 Desired survivor size 1048576 bytes, new threshold 7 (max 15) new threshold 7即标识新的存活周期的阈值为7。 - 方法区溢出
方法区存储的数据是类的元数据信息,常量,静态变量和静态方法
频繁 GC
触发 GC 的时机:
- minor GC 在新生代的 survivor 区的 from 内存达到上限后,会进行 minor GC
- Full GC 在老年代达到内存上限后会进行
频繁发生 GC 的情况:
- minor GC, 证明对象很快就把新生代的 survivor 区中的 from 区域给占满了,此时我们可以调整Elden 和 survivor 的比例
- Full GC 证明有大对象直接放进了老年代,换句话说就是程序中存在大于新生代全部内存的大对象,这样就需要阔大内存并且调整新生代和老年代的比例
常见 GC 算法
对象判断为垃圾的算法
- 引用计数器
给对象打标记,当标记为0的时候,就标记为垃圾
可达性分析
Java并不采用引用计数法来判断对象是否已“死”,而采用“可达性分析”来判断对象是否存活(同样采用此法的还有C#、Lisp-最早的一门采用动态内存分配的语言)。
此算法的核心思想:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。可以作为 GC root 的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中(Native方法)引用的对象
- 标记清除算法
内存中的对象构成一棵树,当有效的内存被耗尽的时候,程序就会停止,做两件事,第一:标记,标记从树根可达的对象(图中水红色),第二:清除(清除不可达的对象)。标记清除的时候程序会停止运行,如果不停止,此时如果存在新产生的对象,这个对象是树根可达的,但是没有被标记(标记已经完成了),会清除掉。
缺点:递归效率低性能低;释放空间不连续容易导致内存碎片;会停止整个程序运行;
- 复制算法
把内存分成两块区域:空闲区域和活动区域,第一还是标记(标记谁是可达的对象),标记之后把可达的对象复制到空闲区,将空闲区变成活动区,同时把以前活动区对象1,4清除掉,变成空闲区。
速度快但耗费空间,假定活动区域全部是活动对象,这个时候进行交换的时候就相当于多占用了一倍空间,但是没啥用。
- 标记整理算法
标记谁是活跃对象,整理,会把内存对象整理成一课树一个连续的空间。
常见的垃圾回收器
名字 新老 线程 算法 优点 缺点 serial 新 串 复制 响应优先 stop the world Parnew 新 并 复制 响应优先适用在多CPU环境Server模式一般采用ParNew和CMS组合多CPU和多Core的环境中高效 Stop the world Parallel 新 并 复制 吞吐量优先有GC自适应的调节策略开关适用在后台运行不需要太多交互的任务 无法与CMS收集器配合使用 Serial old 老 串 标记整理 响应优先单CPU环境下Client模式,CMS的后备预案。无线程交互的开销,简单而高效(与其他收集器的单线程相比) Parallel old 老 并 标记整理 响应优先吞吐量优先适用在后台运行不需要太多交互的任务有GC自适应的调节策略开关 Cms 老 并 标记整理 响应优先集中在互联网站或B/S系统服务、端上的应用。并发收集、低停顿 对CPU资源非常敏感:收集会占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低无法处理浮动垃圾清理阶段新垃圾只能下次回收标记-清除算法导致的空间碎片 G1 新老 并 标记整理 + 复制 面向服务端应用的垃圾收集器分代回收可预测的停顿 这是G1相对CMS的一大优势内存布局变化:将整个Java堆划分为多个大小相等的独立区域(Region)避免全堆扫描
如何对现有系统的内存做预估,防止内存溢出
这是一个完整的对象内存结构,不同的就是数组也作为对象进行存储,所以说,数据对象在对象头中多了一个4 byte 的数组长度
hotspot 的内存结构排序规则
- longs/doubles
- ints
- shorts/chars
- bytes/booleans
- oops(Ordinary Object Pointers)
总结,有长到短,引用在最后
计算规则:
- 静态变量不计算其中
- 父类变量要计算在其中
到此,我们能够通过计算,得到了一个对象的内存大小。但是,事实上,我们漏掉了一些对象。比如,A对象中包含一个属性,B b = new B()。那么,B对象占用的空间,是否也算做A对象的大小呢?严格来说,不应该计算,因为b只是一个引用,而不是对象本身。但是,一般情况下,不会遇到说去估算某个对象在内存中大小的场景,更多的时候,是让我们估计某个程序或某段代码在运行时,占用内存的大小,这个时候,就必须去考虑每个对象的每个引用所指向的具体对象了。
举个例子:
我们来计算 String 对象的大小
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 /** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = -6849794470754667710L; }
默认开启指针压缩:8(对象头) + 4(oop) + 4(int) + 4(char[]引用) + 4(padding) = 24
不开启指针压缩:8 + 8 + 4 + 8 + 4 = 32
所以,不要再去信网上的String 对象计算公式 : 40 + 2*length
下面我们来测试一下
public class TestString{ public static void main(String[] args){ String[] strContainer = new String[4000000]; for(int i = 0; i < 4000000; i++){ strContainer[i] = UUID.randomUUID().toString(); System.out.println(i); } //防止程序退出 while(true){ } } }
默认开启指针压缩:
String 的内存占用:
24 * 4008305 / 1024 = 96199320 byte 由于在监控过程中还存在其他的字符串变量(4008305-4000000),但是所占的内存一丝一毫都对得上,而不是网上的强行估计对齐
字符串数组: (12(对象头) + 4 (数组长度) + 4(引用) + 32*2(字符串实际长度)) = 88
352574920/4008359 = 87.9
通过上面的例子,相信我们在开发中对 对象的内存占用的估计有个大概预估了。