JVM调优

来源:互联网 发布:人工蜂群算法 无人机 编辑:程序博客网 时间:2024/05/16 18:06

绪论

jvm调优涉及到很多方面。

  1. 当发生内存溢出时,如何打印出内存堆快照
    -XX:+HeapDumpOnOutOfMemoryError
  2. 使用-XX:+PrintFlagsFinal参数可以输出所有参数的名称及默认值
  3. 参数的使用方式:
    -XX:+option开启option参数
    -XX:-option 关闭option参数
    -XX:option=value将option参数的值设置为value

GC 日志

  1. -XX:+PrinGC
    开启简单的GC日志模式(新生代GC和每一次的Full GC打印一行信息)
  2. -XX:PrintGCDetails
    开启详细的GC日志模式。[Times]中包含GC所使用的CPU时间信息。
    显示的出发Full GC的gc日志开头Full Gc(System),而不是Full Gc
  3. 位GC的每一行添加绝对的日期和时间
    -XX:+PrintGCDateStamps
  4. 打印GC停顿时间及其他的stop时间(如取消偏向锁)
    -XX:+PrintGCApplicationStoppedTime
  5. 规定GC文件的大小
    -XX:GCLogFileSize=1M

  6. 输出GC日志
    -Xloggc,缺省输出到终端
    也可以使用-Xloggc=指定输出文件(tempfs是Linux的一种基于内存的文件系统)

  7. 在排查问题时可以打开安全点日志
    -XX:+PrintSafepointStatistis
    -XX:+PrintSafepointStatistisCount=1

    安全点:是指一些特定的位置,当线程运行到这些位置时,线程的一些状态可以被确定。特定位置有:(1)循环的末尾;(2)方法返回前(3)调用方法的call之后(4)抛出异常的位置在JIT模式执行下:JIT编译的时候直接把safepoint的检查代码加入到生成的本地代码中。当JVM需要让java线程进入safepoint的时候,只需要设置一个标志位,让java程序运行到safepoint的时候主动检查这个标志位,如果被设置,则线程停顿。在解释器执行的时候,JVM会设置一个2字节的dispatch tables,解释器执行的时候经常去检查这个dispatch tables,当有safepoint请求的时候,就会让线程去进行safepoint检查。

1.内存调优

一:堆内存(新生代和老年代)

由于对象数量达到最大堆的容量限制后就会产生内存溢出。
(1)堆的最小值:-Xms和最大值-Xmx(助记:m可以代表memory)
(2)新生代大小设置:-Xmn(n代表new)
一般把最小值设置为==最大值(避免堆自动扩展)
堆内存的回收算法:复制算法
Jvm新生代分为:一个Eden区和两个Survivor。
(2.1)调整Eden区和Survivor区的比例(默认,8:1,即Eden区80%,两个Survivor区分别为10%):-XX:SurvivorRatio

 如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。 在大多数情况下,对象在新生代Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC 当老年代没有足够空间进行分配时,虚拟机将发起一次Full GC。 Full GC的速度一般会比Minor GC慢10倍以上。

(2.2)调整当对象大小超过多少时直接进入老年代
-XX:PretenureSizeThreshold=3124(设置这个值得目的是避免在Eden 区及两个Survivor区之间发生大量的内存复制)

注意:PretenureSizeThreshold只对Serial和ParNew两个收集器有效,Parallel Scavenge收集器不认识这个参数。     

(2.3)调整进入老年代的年龄阀值(长期存活的对象将进入老年代)
-XX:MaxTenuringThreshold=15(默认值)

每个对象有关对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1。当它的年龄达到一定值时,将晋升到老年代。**动态对象年龄判定** 为了更好适应不同程序内存状况,虚拟机并不是永远的要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

(2.4)空间分配担保
在JDK 1.6后,HandlePromotionFailure 参数不会影响到虚拟机的空间分配担保策略的影响。
之后的规则变为在Minor GC之前,检查老年代的连续空间是否大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

发生Full GC的条件: 1. 老年代的连续空间小于历次晋升的平均大小

解决办法:

  1. 通过内存映像工具先分清楚是出现了内存泄漏还是内存溢出。
  2. 如果是内存泄漏 ,通过工具查看泄漏对象到GC Root的引用链,是什么原因导致对象无法被回收。
  3. 如果不存在泄漏,查看虚拟机的堆参数-Xms和Xmx是否可以再调整。
  4. 从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况。

二:栈内存

栈内存的调优参数:-Xss(Stack Space)
栈内存会抛出两种异常:
1. StackOverflowError(栈深度异常)
2. OutOfMemoryError(虚拟机在扩展栈时无法申请到足够的内存空间)

三:方法区和运行时常量池(java 1.7及以前)

运行时常量池是永久代的一部分:
通过-XX:PermSize和-XX:MaxPermSise来限制方法区的大小
常量池分配在永久代内。

三:元空间(java 1.8)

永久代中的数据可能会随着每一次Full GC的发生而进行移动,并且为永久代设置空间大小也是很难确定的(如类的总数,常量池的大小和方法数量等)
同时,虚拟机的每种类型的垃圾回收器都需要对永久代中的数据做特殊处理。
移除永久代,简化Full GC及对元空间的管理。
元空间放到本地内存中;元空间的最大分配空间就是系统可用内存空间。
元空间的内存管理由元空间虚拟机来完成。
在元空间中,类和其元数据的生命周期和其对应的类加载器是相同的。
元空间虚拟机负责元空间的分配,其采用的形式为组块分配。组块的大小因类加载器的类型而异。
在元空间虚拟机中存在一个全局的空闲组块列表。
当一个类加载器需要组块时,它就会从这个全局的组块列表中获取并维持一个自己的组块列表。
当类加载器不再存活,那么持有的组块将会被释放,并返回全局组块列表。
组块中的块时线性分配(指针碰撞)。
组块分配自内存映射局域。
一旦某个虚拟机内存映射区域清空,这部分内存就返回给操作系统。

在Java 1.7之后,在Java堆中开辟了一块区域存放运行时常量池。

四:直接内存

调优参数:-XX:MaxDirectMemeorySize
如:DirectByteBuffer类

2.多线程调优(Java 1.5后锁的优化)

(1)调整自旋次数:-XX:PreBlockSpin=10(默认)
-XX:UseSpinning(开启自旋锁以避免线程频繁挂起和唤醒,默认开启)

自旋也称为忙循环自旋等待不能代替阻塞,自旋等待虽然避免了线程切换的开销,但它是要占用处理器时间的。如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会消耗处理器的资源,而不会做任何有用的工作,反而会带来性能上的浪费。 自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数,仍然没有成功获得锁,就应该使用传统的方式挂起线程。自适应自旋(JDK 1.6之后)自适应自旋意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免处理器资源被浪费   

(2)禁用偏向锁(jdk 1.6默认开启)
-XX:+UseBiasedLocking
(2.1)轻量级锁
轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
实现轻量级锁的关键是对象头部。即Mark Word

存储内容 标志位 状态 对象哈希码、对象分代年龄 01 未锁定 指向锁记录的指针 00 轻量级锁定 指向重量级锁的指针 10 膨胀(重量级锁定) 空,不需要记录信息 11 GC标记 偏向线程ID,偏向时间戳,对象分代年龄 01 可偏向

在代码进入同步块的时候,如果此同步对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Mark Word的拷贝。
然后虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。
如果这个更新成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后2bit)将转变为“00”,即表示此对象处于轻量级锁定状态。
如果这个更新失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是说明当前线程已经拥有这个对象的锁,那就可以直接进入同步块继续执行。
否则,说明这个锁对象已经被其他线程抢占了。
如果有两条以上的线程抢用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,对象Mark Word的锁标志位最后两位转变为“10”。Mark Word中存储的就是指向重量级(互斥锁)的指针,后面等待锁的线程也要进入阻塞状态。
解锁:是通过CAS操作进行的,如果对象的Mark Word仍然指向这线程的锁记录,那就使用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来,如果成功,则整个同步过程就完成。
如果替换失败,说明有其他线程尝试获取过锁,那么在释放锁的同时,唤醒被挂起的线程。
(2.1.1)轻量级锁提升程序同步性能的依据
对于绝大部分的锁,在整个同步周期内都是不存在竞争的。

 如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

(2.2)偏向锁(1.6默认启用)
偏向锁中的偏是偏心。这个锁会偏向于第一个获得它的线程。如果再接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步
原理:当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。
同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机不再进行任何同步操作。
当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束,根据锁对象目前是否处于被锁定的状态,撤销偏向锁后恢复到未锁定(“01”)或轻量级锁(“00”)。
(2.2)偏向锁和轻量级锁的区别
轻量级锁是在无竞争的情况下,使用CAS操作去消除同步使用的互斥量。
偏向锁就是在无竞争的情况下,把整个同步都消除掉,连CAS操作都不作了(进一步优化)

3.设置int和long的缓存范围

-XX:AutoBoxCacheMax=2000
JDK默认只缓存-128—+127的int和long,超出范围的数字就要即时构建新的Integer对象。
为了节省内存,对下列包装对象的两个实例,当它们的基本值相同时,它们总是==
+ Boolean(全部缓存)
+ Byte(全部缓存)
+ Character(<=127缓存)
+ Short(-128—+127缓存)
+ Long(-128—+127缓存)
Float 和Double 没有缓存

4.即时编译

当JVM发现某个方法或代码执行特别频繁时,就将其认为“热点代码”,在程序运行期间。JVM将这些热点代码编译为本地平台相关的机器码。并进行各层次的优化,从而提升热点代码的执行效率。
分层编译:
第0层:程序解释执行,解释器不开启性能监控功能,可触发第一层编译。
第1层:也称为C1编译,将字节码编译为机器码,进行局部性的优化。
第二层:C2编译,是一个经过充分优化过的编译器。它会执行所有经典的优化动作:无用代码消除、循环展开,范围检查消除,空值检查消除等。
如何检测热点代码:
1.基于采样的热点检测:检查各个线程的栈顶
2.基于计数器的热点
3.回边计数器:统计每个方法中循环体代码的执行次数

设置多层编译(Java 8默认开启)
即程序启动时用C1编译,样本足够后使用C2编译
-XX:+TieredCompilation
调整方法计数器达到多少时进行即时编译:
-XX:CompileThreshold=10000(server默认值)
调整计数器衰减:
每次GC,计数器衰减一半。可以关闭计数器衰减。这样,只要系统运行时间足够长绝大部分代码都会编译成本地代码:
-XX:-UseCounterDelay
调整半衰周期的时间
-XX:CounterHalfLifeTime=100000ms
调整回边计数器值
-XX:OnStackReplacePercentage=10700
计算公式:方法调用计数器阀值(CompileThreshold)* (OSR比率(OnStackReplacePercentage)-解释器监控比率(InterpreterProfilePercentage)/100
调整代码缓存
-XX:InitialCodeCacheSize and -XX:ReservedCodeCacheSize
它是用来存储已编译方法生成的本地代码,如果代码缓存被占满,JVM会打印出一条警告信息,并切换到Interpreted-Only模式,JIT编译被停用,字节码将不再编译成机器码。

选择合适的垃圾收集器

  1. Serial收集器(单线程收集器)(与CMS和Serial old搭配使用)
    简单高效,对于限定单个CPU的环境来说,没有线程交互开销。复制算法
  2. ParNew收集器(与CMS,Serial old搭配使用)
    多线程进行垃圾收集,其余跟Serial收集器相同,复制算法
    在java1.5中,当老年代使用CMS收集器时,新生代只能使用ParNew及Serial收集器
    调优:-XX:ParallelGCThreads参数来限制垃圾收集器的线程数

  3. Parallel Scavenge收集器(与Parllel Old和Serial old搭配使用)
    新生代收集,复制算法,吞吐量优先收集器:
    吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
    主要适合在后台运算而不需要太多交互的认为
    -XX:MaxGCPauseMills 控制最大垃圾收集时间
    时间越小,垃圾收集越频繁,吞吐量下降。找到平衡点。
    -XX:GCTimeRatio直接设置吞吐量大小
    是个吞吐量的倒数。是一个大于0小于100的整数
    如果对收集器运作不太了解。可以使用打开自适应调节策略
    -XX:+UseAdaptiveSizePolicy