JVM 调优的一些思考和总结

简介: 这篇文章不讲基本的 JVM 知识点,需要知道的储备知识有:

JVM 调优的一些思考和总结

这篇文章不讲基本的 JVM 知识点,需要知道的储备知识有:

  1. JVM 的内存区域
  2. JVM 的常见工具
  3. JVM 的调优参数

首先贴一张 JVM 的架构图方便基础忘记的小伙伴来快速回忆

20170610165140237 (1).png?token=AHCUHWNL6QFJWJO3PQ4ZAPDDMHRHI)

方法区(Method Area)

所有类级别数据将被存储在这里,包括静态变量。每个JVM只有一个方法区,它是一个共享的资源。

堆区(Heap Area)

所有的对象和它们相应的实例变量以及数组将被存储在这里。每个JVM同样只有一个堆区。由于方法区和堆区的内存由多个线程共享,所以存储的数据不是线程安全的。

栈区(Stack Area)如果想详细了解方法栈的底层机构,可以去看我另一篇文章 JVM 内存结构

对每个线程会单独创建一个运行时栈。对每个函数呼叫会在栈内存生成一个栈帧(Stack Frame)。所有的局部变量将在栈内存中创建。栈区是线程安全的,因为它不是一个共享资源。栈帧被分为三个子实体:

  • 局部变量数组 – 包含多少个与方法相关的局部变量并且相应的值将被存储在这里。
  • 操作数栈 – 如果需要执行任何中间操作,操作数栈作为运行时工作区去执行指令。
  • 帧数据 – 方法的所有符号都保存在这里。在任意异常的情况下,catch块的信息将会被保存在帧数据里面。如上是JVM三大核心区域

内存溢出

内存溢出的分类

  1. 栈溢出

    我们由上面里了解到了栈内存中都包含着哪些数据信息:

    ​ 一个线程一个方法栈,一个线程中执行的方法形成一个栈帧(栈的一个元素),一个方法中的数据就组成了一个栈帧:局部变量,操作数,返回值等。

    栈溢出主要有两个方面:

    • 线程太多导致方法栈的分配不够导致栈溢出
    • 同一个线程中,栈帧分配太多导致栈溢出,这个主要就是栈帧数量太多(方法调用太深)

    明白了这两个远离就能够有效避免栈溢出:

    • 避免创建过多线程(一般不会出现)
    • 谨慎使用递归,避免递归调用导致栈帧太多造成栈溢出
  2. 堆溢出

    对内存中的数据,主要是创建的对象数据和数组,所以说,对内存溢出主要是由于大型对象和对象数量过多导致的。

    对内存是 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。
  3. 方法区溢出

    方法区存储的数据是类的元数据信息,常量,静态变量和静态方法

频繁 GC

触发 GC 的时机:

  1. minor GC 在新生代的 survivor 区的 from 内存达到上限后,会进行 minor GC
  2. Full GC 在老年代达到内存上限后会进行

频繁发生 GC 的情况:

  • minor GC, 证明对象很快就把新生代的 survivor 区中的 from 区域给占满了,此时我们可以调整Elden 和 survivor 的比例
  • Full GC 证明有大对象直接放进了老年代,换句话说就是程序中存在大于新生代全部内存的大对象,这样就需要阔大内存并且调整新生代和老年代的比例

常见 GC 算法

对象判断为垃圾的算法

  • 引用计数器

    给对象打标记,当标记为0的时候,就标记为垃圾

  • 可达性分析

    Java并不采用引用计数法来判断对象是否已“死”,而采用“可达性分析”来判断对象是否存活(同样采用此法的还有C#、Lisp-最早的一门采用动态内存分配的语言)。
    此算法的核心思想:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。

    可以作为 GC root 的对象

    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    2. 方法区中静态属性引用的对象
    3. 方法区中常量引用的对象
    4. 本地方法栈中(Native方法)引用的对象
  1. 标记清除算法

    内存中的对象构成一棵树,当有效的内存被耗尽的时候,程序就会停止,做两件事,第一:标记,标记从树根可达的对象(图中水红色),第二:清除(清除不可达的对象)。标记清除的时候程序会停止运行,如果不停止,此时如果存在新产生的对象,这个对象是树根可达的,但是没有被标记(标记已经完成了),会清除掉。

    缺点:递归效率低性能低;释放空间不连续容易导致内存碎片;会停止整个程序运行;

  2. 复制算法

    把内存分成两块区域:空闲区域和活动区域,第一还是标记(标记谁是可达的对象),标记之后把可达的对象复制到空闲区,将空闲区变成活动区,同时把以前活动区对象1,4清除掉,变成空闲区。

    速度快但耗费空间,假定活动区域全部是活动对象,这个时候进行交换的时候就相当于多占用了一倍空间,但是没啥用。

  3. 标记整理算法

    标记谁是活跃对象,整理,会把内存对象整理成一课树一个连续的空间。

常见的垃圾回收器

名字 新老 线程 算法 优点 缺点
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)避免全堆扫描

如何对现有系统的内存做预估,防止内存溢出

image-20220308152131335

这是一个完整的对象内存结构,不同的就是数组也作为对象进行存储,所以说,数据对象在对象头中多了一个4 byte 的数组长度

hotspot 的内存结构排序规则

  • longs/doubles
  • ints
  • shorts/chars
  • bytes/booleans
  • oops(Ordinary Object Pointers)

总结,有长到短,引用在最后

计算规则:

  1. 静态变量不计算其中
  2. 父类变量要计算在其中

到此,我们能够通过计算,得到了一个对象的内存大小。但是,事实上,我们漏掉了一些对象。比如,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

image-20220308163231708

不开启指针压缩:8 + 8 + 4 + 8 + 4 = 32

image-20220308163344119

所以,不要再去信网上的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){

      }
  }
}

image-20220308170129274

默认开启指针压缩:

String 的内存占用:

24 * 4008305 / 1024 = 96199320 byte 由于在监控过程中还存在其他的字符串变量(4008305-4000000),但是所占的内存一丝一毫都对得上,而不是网上的强行估计对齐

字符串数组: (12(对象头) + 4 (数组长度) + 4(引用) + 32*2(字符串实际长度)) = 88

352574920/4008359 = 87.9

通过上面的例子,相信我们在开发中对 对象的内存占用的估计有个大概预估了。

相关文章
|
2月前
|
存储 监控 算法
jvm-性能调优(二)
jvm-性能调优(二)
|
4月前
|
Arthas 监控 Java
(十一)JVM成神路之性能调优篇:GC调优、Arthas工具详解及各场景下线上最佳配置推荐
“在当前的互联网开发模式下,系统访问量日涨、并发暴增、线上瓶颈等各种性能问题纷涌而至,性能优化成为了现时代开发过程中炙手可热的名词,无论是在开发、面试过程中,性能优化都是一个常谈常新的话题”。
430 3
|
4月前
|
监控 Java 测试技术
JVM 性能调优 及 为什么要减少 Full GC
JVM 性能调优 及 为什么要减少 Full GC
121 4
|
12天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
10天前
|
监控 Java 编译器
Java虚拟机调优实战指南####
本文深入探讨了Java虚拟机(JVM)的调优策略,旨在帮助开发者和系统管理员通过具体、实用的技巧提升Java应用的性能与稳定性。不同于传统摘要的概括性描述,本文摘要将直接列出五大核心调优要点,为读者提供快速预览: 1. **初始堆内存设置**:合理配置-Xms和-Xmx参数,避免频繁的内存分配与回收。 2. **垃圾收集器选择**:根据应用特性选择合适的GC策略,如G1 GC、ZGC等。 3. **线程优化**:调整线程栈大小及并发线程数,平衡资源利用率与响应速度。 4. **JIT编译器优化**:利用-XX:CompileThreshold等参数优化即时编译性能。 5. **监控与诊断工
|
21天前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
29天前
|
Arthas 监控 数据可视化
JVM进阶调优系列(7)JVM调优监控必备命令、工具集合|实用干货
本文介绍了JVM调优监控命令及其应用,包括JDK自带工具如jps、jinfo、jstat、jstack、jmap、jhat等,以及第三方工具如Arthas、GCeasy、MAT、GCViewer等。通过这些工具,可以有效监控和优化JVM性能,解决内存泄漏、线程死锁等问题,提高系统稳定性。文章还提供了详细的命令示例和应用场景,帮助读者更好地理解和使用这些工具。
|
1月前
|
监控 架构师 Java
JVM进阶调优系列(6)一文详解JVM参数与大厂实战调优模板推荐
本文详述了JVM参数的分类及使用方法,包括标准参数、非标准参数和不稳定参数的定义及其应用场景。特别介绍了JVM调优中的关键参数,如堆内存、垃圾回收器和GC日志等配置,并提供了大厂生产环境中常用的调优模板,帮助开发者优化Java应用程序的性能。
|
1月前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
46 3
|
1月前
|
Java API 对象存储
JVM进阶调优系列(2)字节面试:JVM内存区域怎么划分,分别有什么用?
本文详细解析了JVM类加载过程的关键步骤,包括加载验证、准备、解析和初始化等阶段,并介绍了元数据区、程序计数器、虚拟机栈、堆内存及本地方法栈的作用。通过本文,读者可以深入了解JVM的工作原理,理解类加载器的类型及其机制,并掌握类加载过程中各阶段的具体操作。