欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JVM调优基础

程序员文章站 2022-06-01 11:58:12
...

1. JVM命令功能

  • jcmd help
  • jhat: 读取内存堆转存
  • jmap: 提供转存和内存使用信息
  • jinfo:查看jvm的系统属性,也可以是设置系统属性,可用于脚本
  • jstack: 转存java进程信息,可用于脚本
  • jstat: 提供GC和类装载活动的信息,可用于脚本
  • jvisualvm: 可视化工具
  • jstat -J-Djstat.showUnspported=true -snap 5376 -- 获得监控信息

-XX:ReservedCodeCacheSize=N 标志可以设置代码缓存最大值
-XX:InitialCodeCacheSize=N 代码缓存从初始大小开始分配

1.1 常用信息

  • 1.错误信息:C: C帧, j: java帧 V: VM帧 v:VM生成的stub帧,J:其它帧,含java帧(编译)
  • 2:
  • TPS:每秒事物数,比如执行了dml操作,那么相应的tps会增加
  • RPS: 每秒请求数,并发数/平均响应时间
  • OPS:每秒操作次数
  • QPS是指每秒内查询次数,比如执行了select操作,相应的qps会增加。
  • PV 是指页面被浏览的次数,比如你打开一网页,那么这个网站的pv就算加了一次;
  • 3 vmstate 1: 单个线程,CPU内存等监控, iostat -xm 5 , nicstat 5: 监控网络信息
  • 4
JVM内部线程状态: 
 _thread_uninitialized
 _thread_new
 _thread_in_native
 _thread_in_vm
 _thread_in_java
 _thread_block
 _<thread_state_type>_trans

5.JVM状态

  1. not at a safepoint: 正常执行的状态
  2. at safepoint: 所有线程阻塞,等待VM完成专门的VM操作(VM operation)
  3. synchronizing : VM接到一个专门的VM操作请求,等待VM中所有现场阻塞

2. JVM保留内存和分配内存之间的差别:

编译是基于JVM计数器: 方法调用计数器和方法中的循环回边计数器,

  • 当方法或循环 编译的时候,就会进入编译队列,队列有一个或多个后台线程处理.也就是异步的,
    并不是严格先进先出,调用计数次数多的方法有更高优先级,也就是PrintCompilation输出中ID乱序的原因.

OSR(on-stack Replacement): 在循环进行的时候还能编译循环,在循环代码编译结束后,JVM会替换在栈上的代码,循环的下一次就行快的多的代码

  • -XX:CompliThreshold=Nclient默认1500,server默认10000,一般不用修改),这个数值是回边计数器和方法调用次数的总和
  • -XX:PrintCompilation(默认false):每编译一个循环和方法就输出编译内容
  • -XX:CICompilerCount=N :JVM 处理队列的线程总数
  • -XX:+BackgroundCompilation(默认为true,即异步处理,也可以改为false,一个方法适合编译就一直等到他确实被编译)

  • jstat -compiler 8895 输出这个pid的编译过的方法,有时也会出现编译失败的方法
  • jstat -printcompilation 8895 1000 (每秒钟输出一次编译的方法)

-XX:-Inline (默认开启,关闭的话严重影响性能)
-XX:PrintInlining 生成内联信息

  • 方法是否内联取决于: 多热和它的大小,只有字节码小于325字节才会或者小于35字节
    -XX:MaxFreqInlineSize=N

-XX:MaxInLineSize=N

  • 逃逸分析
    -XX:+DoEscapeAnalysis(默认为true)
  • 逆优化 (意味着编译器不得不撤销之前的优化):
    (1) 代码状态为"made not entrant"(代码被丢弃) -原因: 可能和类与接口的工作方式有关,也可能与分层编译的实现有关

(2) "made zombie"(产生僵尸代码) client编译的结果后来server编译更优化的结果

编译级别: C1编译器有3种级别,所以总有5种编译级别

  • 0: 解释代码
  • 1: 简单的C1编译代码
  • 2: 受限的C1编译代码
  • 3: 完全的C1编译代码
  • 4: C2编译代码

目前主流的4个GC算法:

  • 1: Serial : -XX:+UseSerialGC
  • 2: Throughput(Parallel): 7之后默认,使用多线程收集Eden区, -XX:+UseParalleGC -XX:+UseParallelOldGC
  • 3: Concurrent (CMS): 修改之前UseParalleGC算法收集Eden区,改用 -XX:UseParNewGC收集Eden, -XX:+UseConcMarkSweepGC 后台线程扫描Old区的垃圾对象,停顿时间少, 问题: (1)占用CPU线程资源,(2)内存碎片化严重,最后变成Serial(间隔时间长),整理好内存之后重复之前
  • 4: G1: -XX:+UseG1GC, 属于Concurrent收集器,老年代的收集工作由后台线程完成,大多数不需要暂停应用线程,由于老年代被划分到不同的
    区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成对象的清理工作,这也意味着G1实现了堆的压缩整理(至少是部分整理)
System.gc() 会触发Full GC
  • -Xms4096m -Xmx4096m 设置一样大就不需要估算堆是否需要调整大小了
  • -XX:NewRatio=N(默认2): 新生代与老年代的空间占用比率
    Initial Yong Gen size = Initial Heap Size / (1+NewRatio) --> 默认新生代占 1/3
  • -XX:NewSize=N 直接设置新生代初始的大小
  • -XX:MaxNewSize: 设置新生代最大大小
  • -XX:PermSize=N, -XX:MaxPermSize=N 设置永久区的大小
  • -XX:MetaspaceSize=N -XX:MaxMatespaceSize=N 调整

除了SerialGC之外几乎所有的GC都是使用多线程,启动线程数由 -XX:ParalleGCThreads=N控制
-XX:+UseParallelGC -XX:+UseParNewGC -XX:+UseG1GC 收集新生代空间
-XX:+UseParallelOldGC 收集老年代空间
估算公式: ParalleGCThreads = 8 + ((N-8)*5/8), 每超出5/8个CPU启动一个新的线程(可能会略高,需适当调整)

  • -XX:-UseAdaptiveSizePolicy(默认开启) 可以在全局范围内关闭自适应调整功能,如果堆容量最大值和最小值设置同样的值,与此同时,新生代的
    最大值和最小值也设置为同样的值,自适应功能将会被关闭
  • -XX:PrintAdaptiveSizePolicy: 一旦发生GC会在日志中包含GC不同空间调整的信息

-verbose:gc 或 -XX:+PrintGC -XX:+PrintGCDetails(默认关闭) -XX:+PrintGCTimeStamps -XX:+PirntGCDateStamps

-XX:+UseGCLogfileRotation -XX:NumberOfGCLogfiles=N -XX:GCLogfileSize=N 组合使用可以控制循环输出日志
文件大小不足8kb按照8kb算

查看日志可以使用 gchisto 或者jstat -gcutil pid 1000 每秒输出日志信息

GC回收算法

1. Throughput 收集器

  • 回收新生代的垃圾 Minor GC
  • 回收老年代垃圾 Full GC
  • -XX:MaxGCPauseMillis=N 设定应用可承受最大停顿时间
  • -XX:GCTimeRatio=N 设置你希望应用程序在垃圾回收上花费多少时间(与应用线程的运行时间相比) 公式: ThroughputGoal=1-1/(1+GCTimeRatio) 默认是99,也就是运行程序占99%,只有1%的时间消耗在垃圾回收上

2. CMS收集器

  • CMS会对新生代的对象进行回收 ParNew(所有线程都会被暂停)
  • CMS会启动一个并发线程对老年代空间进行垃圾回收,(不会内存整理)
  • 如有必要,CMS会发起 Full GC
CMS收集步骤:
  • 1: Init-mark: 找到堆中所有垃圾回收根节点对象
  • 2: current-mark: 和应用程序并行,标记,(可能还会产生垃圾)
  • 3: preclean-start: 预处理,与应用程序并行
  • 4: remark(多个阶段),不是并发,STW
  • 5: concurrent-sweep: 与应用程序并行
  • 6: concurrent-reset: 并发重置
CMS可能会产生的问题:
  • concurrent mode failure:新生代发生GC,同时老年代又没有足够的空间容纳晋升的对象时,CMS GC就会退化成Full GC,所有的应用线程都会暂停,老年代对象进行回收,这个操作是单线程的,所以非常耗时
  • 老年代有足够的空间容纳晋升的对象,但是空间非常碎片化,导致晋升失败. 这时新生代暂停了所有的应用线程,专门在老年代整理碎片内存,好消息是内存碎片整理完了,但是会有很长时间停顿
    ,这比并发模式失效停顿的时间还长.(因为并发模式失效时只需要整理堆里无效的对象)
针对并发模式失效的调优:
  • 增大老年代空间
  • 以更高的频率运行后台回收线程
  • 使用更多的后台线程
注意点:1. CMS与其他回收算法的显著不同就是:除非发生Full GC,否则CMS的新生代大小不会调整.CMS的目标就是尽量避免Full GC, 可以使用: MaxPauseMllis=N和GCTimeRatio=N来确定使用多大的堆和多大的空间
2. 给后台线程更多的运行机会:
  • -XX:CMSInitiatingOccupancyFraction=N和-XX+UseCMSInitiatingOccupanyOnly(默认70,即CMS在老年代空间占70%时启动并发收集周期调整)
  • 调整后台线程: -XX:ConcGCThreads=N增加后台线程,ConcGCThreads=(3+ParallelGCThreads) / 4
3. CMS不会处理永久代的垃圾,如果耗尽会产生一次Full GC来回收垃圾,可以设置: -XX:+CMSPermGenSweepingEnable(默认false)标志开启和老年代同样的回收方式,后台开启一组线程并发回收永久代中的垃圾,同时配合:+XX:CMSInitiatingPermOccupancyFrantion=N指定在永久代占用达到比例值时开启回收线程. java8 CMSPermGenSweepingEnable标志默认开启
4: 增量式CMS,(java8种不推荐使用)
  • 当CPU资源有限,后台线程会间歇性暂停,让出一部分CPU给应用线程
  • 开启: -XX:+CMSIncrementalMode, 设置: -XX:CMSIncrementalSaftyFactor=N,-XX:CMSIncrementalDutyCycleMin=N,-XX:CMSIncrementalPacing可以控制GC线程为应用线程让出多少CPU

G1收集器

  • 分区(Region)一般分为2048个区域,专注于垃圾最多的分区,用最少的时间回收最多的垃圾
  • 新生代进行回收时,要么被回收,要么被晋升,新生代采用分区机制的部分原因是采用预定义的分区能够便于代的大小调整
  • 主要包括4种操作:

    • 1.新生代垃圾收集
    • 2.后台收集,并发周期
    • 3.混合式垃圾回收(不仅进行新生代的GC同时也回收部分后台扫描线程标记的分区)
    • 4.以及必要的Full GC
  • 操作步骤: 1:initial-mark(STW) 2: root-scan(并发) 3: conc-mark 4:remark 5: conc -clean
  • G1注意:

    1: 并发模式失效:,在G1启动标记周期,但老年代在周期完成之前就被填完,G1会放弃标记周期,(GC-concurrent-mark-abort)
    2:晋升失败: G1完成了标记阶段,开始启动混合式垃圾回收,清理老年代分区,不过,老年代空间在垃圾回收释放出足够内存之前就会被耗尽. 一般都是混合式回收之后就是Full GC

    3:疏散失败:进行新生代垃圾回收时,S区和老年代中没有足够的空间容纳所有的幸运对象.这表明堆已经几乎用完或者碎片化了.转用Full GC,性能下降,最简单的解决办法就是增加堆

    4:巨型对象分配失败也可能会导致Full GC

G1 调优:

  • G1调优的主要目标就是避免发生并发模式失败或者疏散失败 -XX:MaxGCPauseMillis=N(默认200ms,这个CMS有所不同)
  • 1:调整G1的后台线程数,可以使用ParallelGCThreads设置运行的线程数,计算方式ConcGCThreads=(ParallelGCThreads+2)/4 这个和CMS也不同
  • 2: -XX:InitiatingHeapOccupancyPercent=N(默认45),堆占用比例,如果设置过高会陷入Full GC泥潭中,因为并发阶段没有足够的时间在剩下的堆空间被填满之前完成垃圾回收,如果设置过小又会以超过实际的节奏在后台大量处理
  • 3:调整G1收集器混合式垃圾收集周期:混合式垃圾收集取决于三个因素(1)有多少分区被发现大部分是垃圾对象,默认超过35%就会标记为可进行垃圾回收,实验版标志: -XX:G1MixedGCLiveThresholdPercent=N (2)G1垃圾回收分区时最大混合式GC周期数,参数 -XX:G1MixedGCCountTarget=N(默认8),减少该值可以解决晋升失败的问题(代价是混合式GC周期的停顿时间会更长) (3)GC停顿可忍受的最大时常,参数-XX:MaxGCPauseMillis
  • 4:2种情况下会被移动到老年区: (1):S区实在太小,S区被填满之后,Eden区剩下的活跃的对象直接放到Old区,(2)在S区经历的GC周期的个数上限(Tenuring Threshold)
  • 5: S区是新生代的一部分,跟堆内存的其他区域一样,JVM可以对他动态调节,参数: -XX:InitialSurvivorRatio=N(默认8)决定, survivor_space_size=new_size / (initial_survivor_ratio+2) JVM可以增大S区空间直至最大上限, -XX:MinSurvivorRatio(默认3)设置(注意参数在分母,名字不直观),maximum_survivor_space=new_size / (min_survivor_ratio + 2), -XX:TargetSurvivorRatio=N可以在Survivor空间调整之后能保证垃圾回收之后有50%的空间是空闲的.
  • 6: -XX:InitialTenuringThreshold=N可以设置初始晋升阈值(Throughput和G1的默认值7,CMS默认值是7),JVM最终会在1和最大阈值(-XX:MaxTenuringThreshold=N)之间选择一个合适的值, 对于Throughput和G1,最大值默认15,CMS最大默认6)
  • 7:-XX:PrintTenringDistribution标志可以在GC中增加这些信息
  • 8: TLAB的大小由三个因素决定: (1)应用线程的线程数(2)Eden空间的大小(3)线程分配率,默认开启,-XX:-UseTLAB可以关闭. -XX:TLABSize=N可以显示的指定TLAB的大小(默认为0,由Eden区的大小动态计算出),这个标志只能设置TLAB的初始大小,为了避免每次GC时都调整TLAB的大小可以使用 -XX:-ResizeTLAB,TLAB调整时,最小容量可以使用:-XX:MinTLABSize=N设置(默认2kb),最大容量略小于1GB
  • 9: -XX:+PrintTLAB输出信息
  • G1分区大小: size=1<<log(初始堆的大小/2048), 分区最小1MB,最大不能超过32MB, 堆的大小:
堆的大小 Region Size
< 4GB 1MB
4GB - 8GB 2MB
8GB - 16GB 4MB
16GB - 32GB 8MB
32GB - 64GB 16MB
> 64GB 32MB
  • G1分区的大小可以通过-XX:G1HeapRegionSize=N设置(默认0,动态计算出),选择合适的大小,使分区数量接近于: 2048个

堆内存

  • 1.jmap -dump:live,file=/path/heap.dump.hprof pid, jmap有live选项会在堆转储之前强制执行一次Full GC, jcmd默认就会这么做 jcmd pid GC.heap_dump /path/heap_dump.hprof
    1. -XX:+HeapDumpOnOutOfMemoryError(默认false)会在抛出OutofMemoryError时转储 -XX:HeapDumpPath=/path 指定堆转储位置,默认java_pid.hprof -XX:+HeapDumpAfterFullGC:在运行一次Full GC之后生成一个堆转储 -XX:+HeapDumpBeforeullGC:会在一次Full GC之前生成一个堆转储文件
  • 3: Exception in thread 'main' java.lang.OutOfMemoryError: GC overhead limit exceeded:原因: (1) 花在Full GC上的时间超出了-XX:GCTimeLimit=N(默认98,也就是98%的时间花在了GC上了) (2):一次Full GC回收的内存量少于-XX:GCHeapFreeLimit=N设置的值,默认值2,即意味着如果Full GC期间释放的内存不足堆的2%,(3)上面2个条件连续5次Full GC都成立(这个值无法调整) (4):-XX:+UseGCOverHeadLimit标志的值为true(默认true) 请注意:所有四个条件都满足的时候才会报错,如果连续4次Full GC都成立,作为释放内存的最后一搏,JVM中所有的软引用都会在第五次Full GC之前被释放
减少内存使用(减少对象大小,对象延迟初始化,规范化对象)
  • 1.对普通对象,对象头字段在32位JVM上占8字节,在64位JVM上占16字节,对于数组,对象头在32位JVM以及堆小于32GB的64位JVM上占16字节,其他情况是64字节
  • 2.不可变对象:基本的数据类型Double,Boolean,以及一些基于数值的类型,BigDeciaml,String,像这类不可变对象的单一化表示,就被称为标准化(canonical)版本,类似有String.intern
  • 例如: 创建一个Map来保存该对象的标准化版本,为防止内存泄漏,采用WeakHashMap
 class ImmutableObject{
    WeakHashMap<ImmutableObject,ImmutableObject> map = new WeakHashMap<>();
    public ImmutableObject canonicalVersion(ImmutableObject io){
        synchronized (map){
            ImmutableObject cv = map.get(io);
            if(cv == null){
                map.put(io,io);
                cv = io;
            }
            return cv;
        }
    }
}
  • String提供了自己的标准化方法,intern()方法.保留字符串的表是保留在原生内存中的,他是一个大小固定的HashTable,在Java 7u40之前默认是1009个桶,之后的版本中,默认改为60013个 可以通过-XX:StringTableSize=N设置,输出信息: -XX:+PrintStringTableStatistics(默认false)
  • 对象重用的方式: 对象池和ThreadLocals,注意:被重用的对象会在堆中停留很长时间,如果有大量对象存于堆中,那用来创建新对象的空间就少了.并且会通过Eden S0 S1最终才会进入Old区,执行一次Full GC所化的时间与老年代中仍然存活的对象数量成正比,存活对象的数量甚至比堆的大小更重要.
    • 线程池: 线程初始化成本高
  • JDBC池: 数据库连接初始化的成本高
  • EJB池: EJB初始化成本高
  • 大数组:java要求,一个数组在分配的时候,其中的每个元素都必须初始化为某个默认初始值(null,0,false)
  • 原生NIO缓冲区:不管缓冲区多大,分配一个直接java.nio.Buffer(即调用allocateDirector()方法返回的缓冲区)
  • 安全相关的类: MessageDigest,Signature以及其他安全算法的实例
  • 字符串编解码对象,大多数都是软引用
  • StringBuilder协助者:BigDecimal类在计算中间结果时会重用一个StringBuilder对象
  • 随机数生成器: Random和SecureRandom类,生成这些类代价较高
  • 从DNS查询到的名字:网络查询代价很高
  • ZIP编解码器:初始化开销不是特别高,但是释放成本很高,因为这些对象要依赖对象终结操作(finalization)来确保释放掉所用的原生内存.
  • 对象池: (1)GC影响,降低GC效率 (2)同步: 对象池必然是同步 (3)限流(Throttling):对于稀缺的资源访问,线程池可以起到限流的作用.同时也意味着,超出的部分会等待资源.
  • 线程局部变量:(1)生命周期管理 (2)基数性(Cardinality):线程局部变量通常会伴生线程数与保存的可重用对象数之间一一对应的关系.(3)不需要同步
  • 注意: 初始化一个Random对象的开销非常大,而且持续创建这个类的实例,与再多个线程间共享一个类实例的同步瓶颈相比,性能可能更差.使用TreadLocalRandom类性能会更好.
  • -XX:+PrintReferenceGC(默认false),可以输出调试信息
  • 软引用的释放:
  • (1):所引用的对象不能有其它的强引用,如果软引用是指向其所引用对象的唯一引用,而且该引用最近没有被访问过,则所引用对象会在下一次GC周期前释放.

     long ms = SoftRefLRUPolicyMSPerMB * AmountOfFreeMemoryInMb 
     if(now - last_access_to_reference > ms)
     free the reference
  • (2) 堆空间空闲内存的数量,例如堆空间4GB,再一次Full GC之后,堆可能被占用了50%,因此空闲堆是2GB,SoftRefLRUPolicyMSPerMB的默认值1000,意味着 过去的 2048s内没有访问到的任何软引用都会被清理. 如果4GB的堆占了75%,则过去的1024s没有访问到的软引用会被回收.

    log ms = 2048000;//2048*1000
    if(System.currentTimeMillis()- last_access_to_reference_in_ms  ms)
         free the reference 
  • 注意: 对于长期运行的应用,如果满足以下2个条件,可以考虑增加SoftRefLRUPolicyMSPerMB的值:
    (1)有很多空闲堆可用 (2)软引用会频繁访问 ,(不过情况罕见,增加软引用的策略值就是告诉JVM不到万不得已不要释放软引用)
  • 软引用:当问题中的所引用对象会同时被几个线程使用时,考虑弱引用,否则入弱引用很可能会被GC掉,只有弱引用的对象在每个GC周期都可以回收

原生内存的使用
  • 1.从Java8开始: -XX:NativeMemoryTracking=off|summary|detail 标志来跟踪JVM如何分配原生内存. 也可以使用-XX:+PrintNMTStatistics(默认false)启动 ,前提需要设置-XX:+UnlockDiagnosticVMOptions
  • 2: 开启压缩指针: -XX:+UseCompressedOops(默认启用的) Ordinary object pointer

tables Area Cool
cold 2 is right $1800
col is 2 left $0099