深入理解Java虚拟机(实战篇)

深入理解Java虚拟机(实战篇)

1.JVM常用参数

通用参数

-Xms 初始堆大小。如:-Xms256m
-Xmx 最大堆大小。如:-Xmx512m
-Xmn 新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90%
-Xss JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。
-XX:NewRatio 新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3
-XX:SurvivorRatio 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10
-XX:+PrintGCDetails 打印 GC 信息
-XX:+HeapDumpOnOutOfMemoryError 让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用

Serial 收集器参数

串行收集器,client 的默认收集器,分为年轻代 Serial 和老年代 Serial Old 收集器。

-XX:+UseSerialGC 这个参数就是可以指定使用新生代串行收集器和老年代串行收集器, “+” 号的意思是ture,开启,反之,如果是 “-”号,则是关闭。

ParNew 收集器参数

并行收集器是 Serial 的多线程版本,在 CPU 并行能力强大的计算机上有很大优势。

-XX:+UseParNewGC :新生代使用 ParNew 收集器,老年代使用串行收集器。
-XX:+UseConcMarkSweepGC: 新生代使用 ParNew 回收器,老年代使用 CMS。
-XX:ParallelGCThreads={value} 这个参数是指定并行 GC 线程的数量,一般最好和 CPU 核心数量相当。默认情况下,当 CPU 数量小于8,ParallelGCThreads 的值等于 CPU 数量,当 CPU 数量大于 8 时,则使用公式:3+((5*CPU)/ 8);同时这个参数只要是并行 GC 都可以使用,不只是 ParNew。

Parallel 收集器参数

全称 Parallel Scavenge 收集器,该收集器是 Java 8 的默认收集器,因为它能够根据系统当前状态给出吞吐量最高的GC 配置。所以,在一些手工调优复杂的场合或者对实时性要求不高的场合,可以使用该处理器。

-XX:MaxGCPauseMillis 设置最大垃圾收集停顿时间,他的值是一个大于0的整数。ParallelGC 工作时,会调整 Java 堆大小或者其他的一些参数,尽可能的把停顿时间控制在 MaxGCPauseMillis 以内。如果为了将停顿时间设置的很小,将此值也设置的很小,那么 PS 将会把堆设置的也很小,这将会到值频繁 GC ,虽然系统停顿时间小了,但总吞吐量下降了。
-XX:GCTimeRatio 设置吞吐量大小,他的值是一个0 到100之间的整数,假设 GCTimeRatio 的值是 n ,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集,默认 n 是99,即不超过1% 的时间用于垃圾收集。
-XX:+UseParallelGC 新生代使用 ParallelGC 回收器,老年代使用串行回收器。
-XX:+UseParallelOldGC 新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器。
-XX:UseAdaptiveSizePolicy: 打开自适应策略。在这种模式下,新生代的大小,eden 和 Survivor 的比例,晋升老年代的对象年龄等参数会被自动调整。以达到堆大小,吞吐量,停顿时间的平衡点。
-XX:ParallelGCGThreads 同时可以指定该参数设置并行线程数量。

1 和 2 两个参数是矛盾的。因为吞吐量和停顿时间就是矛盾的。所以,要根据应用的特性来进行设置,以达到最优水平。

CMS 收集器参数

CMS 处理器关注的是停顿时间。全称 Concurrent Mark Sweep。因为该处理器较为复杂,因此可以使用较多参数。

-XX:-CMSPrecleaningEnabled 不进行预清理,,CMS 在并发标记和重新标记的这段时间内,会有一个预清理的工作,而这个通过会尝试5秒之内等待来一次 YGC。以免在后面的重新标记阶段耗费大量时间来标记新生代的对象。

-XX:+UseConcMarkSweepGC 此参数将启动 CMS 回收器。默认新生代是 ParNew,也可以设置 Serial 为新生代收集器。该参数等价于 -Xconcgc。

-XX:ParallelGCThreads 由于是并行处理器,当然也可以指定线程数。默认并发线程数是:(ParallelGCThreads + 3)/ 4)。

-XX:ConcGCThreads 或者 -XX:ParallelCMSThreads ;除了上面设置线程的方式,你也可以通过这个两个参数任意一个手工设定 CMS 并发线程数。

-XX:CMSInitiatingOccupancyFraction 由于 CMS 回收器不是独占式的,在垃圾回收的时候应用程序仍在工作,所以需要留出足够的内存给应用程序,否则会触发 FGC。而什么时候运行 CMS GC 呢?通过该参数即可设置,该参数表示的是老年代的内存使用百分比。当达到这个阈值就会执行 CMS。默认是68。 如果老年代内存增长很快,建议降低阈值,避免 FGC,如果增长慢,则可以加大阈值,减少 CMS GC 次数。提高吞吐量。

-XX:+UseCMSCompactAtFullCollection 由于 CMS 使用标记清理算法,内存碎片无法避免。该参数指定每次 CMS 后进行一次碎片整理

-XX:CMSFullGCsBeforeCompaction 由于每次进行碎片整理将会影响性能,你可以使用该参数设定多少次 CMS 后才进行一次碎片整理,也就是内存压缩。

-XX:+CMSClassUnloadingEnabled 允许对类元数据进行回收。

-XX:CMSInitiatingPermOccupancyFraction 当永久区占用率达到这一百分比时,启动 CMS 回收(前提是 -XX:+CMSClassUnloadingEnabled 激活了)。

-XX:UseCMSInitiatingOccupancyOnly 表示只在到达阈值的时候才进行 CMS 回收

XX:CMSWaitDuration=2000 由于CMS GC 条件比较简单,JVM有一个线程定时扫描Old区,时间间隔可以通过该参数指定(毫秒单位),默认是2s。

G1 收集器参数

作为 Java 9 的默认垃圾收集器,该收集器和之前的收集器大不相同,该收集器可以工作在young 区,也可以工作在 old 区。有哪些参数呢?

-XX:+UseG1GC 开启 G1 收集器。

-XX:MaxGCPauseMillis 用于指定最大停顿时间,如果任何一次停顿超过这个设置值时,G1 就会尝试调整新生代和老年代的比例,调整堆大小,调整晋升年龄的手段,试图达到目标。和 PS 一样,停顿时间小了,对应的吞吐量也会变小。这点值得注意。

-XX:ParallelGCThreads 由于是并行并发的,可以指定GC 工作线程数量。

-XX:InitiatingHeapOccupancyPercent 该参数可以指定**当整个堆使用率达到多少时,触发并发标记周期的执行。**默认值时45,即当堆的使用率达到45%,执行并发标记周期,该值一旦设置,始终都不会被 G1修改。也就是说,G1 就算为了满足 MaxGCPauseMillis 也不会修改此值。如果该值设置的很大,导致并发周期迟迟得不到启动,那么引起 FGC 的几率将会变大。如果过小,则会频繁标记,GC 线程抢占应用程序CPU 资源,性能将会下降。

-XX:GCPauseIntervalMillis 设置停顿时间间隔。

其他通用参数

在 GC 调优中,还有一些通用的参数。通常是我们的好帮手。

-XX:-+DisableExplicitGC 禁用 System.gc(),由于该方法默认会触发 FGC,并且忽略参数中的 UseG1GC 和 UseConcMarkSweepGC,因此必要时可以禁用该方法。

-XX:+ExplicitGCInvokesConcurrent 该参数可以改变上面的行为,也就是说,System.gc() 后不使用 FGC ,而是使用配置的并发收集器进行并发收集。注意:使用此选项就不要 使用 上面的选项。

-XX:-ScavengeBeforeFullGC 由于大部分 FGC 之前都会 YGC,减轻了 FGC 的压力,缩短了 FGC 的停顿时间,但也可能你不需要这个特性,那么你可以使用这个参数关闭,默认是 ture 开启。

-XX:MaxTenuringThreshold={value} 新生代 to 区的对象在经过多次 GC 后,如果还没有死亡,则认为他是一个老对象,则可以晋升到老年代,而这个年龄(GC 次数)是可以设置的,有就是这个参数。默认值时15。超过15 则认为是无限大(因为age变量时4个 bit,超过15无法表达)。但该参数不是唯一决定对象晋升的条件。当 to 区不够或者改对象年龄已经达到了平均晋升值或者大对象等等条件。

-XX:TargetSurvivorRatio={value} :如果在 Survivor 空间中相同年龄所有对象大小的总和大鱼 Survivor 空间的一半(默认50%),年龄大于或等于该年龄的对象就可以直接进入老年代。 无需在乎 XX:MaxTenuringThreshold参数。因此,MaxTenuringThreshold 只是对象晋升的最大年龄。如果将 TargetSurvivorRatio 设置的很小,对象将晋升的很快。

-XX:PretenureSizeThresholds={value} 除了年龄外,对象的体积也是影响晋升的一个关键,也就是大对象。如果一个对象新生代放不下,只能直接通过分配担保机制进入老年代。 该参数是设置对象直接晋升到老年代的阈值,单位是字节。只要对象的大小大于此阈值,就会直接绕过新生代,直接进入老年代。注意:这个参数只对 Serial 和 ParNew 有效,ParallelGC 无效,默认情况下该值为0,也就是不指定最大的晋升大小,一切有运行情况决定。

-XX:-UseTLAB 禁用线程本地分配缓存。TLAB 的全称是 Thread LocalAllocation Buffer ,即线程本地线程分配缓存,是一个线程私有的内存区域。该设计是为了加速对象分配速度。由于对象一般都是分配在堆上,而对是线程共享的。因此肯定有锁,虽然使用 CAS 的操作,但性能仍有优化空间。通过为每一个线程分配一个 TLAB 的空间(在 eden 区),可以消除多个线程同步的开销。默认开启。

-XX:TLABSize 指定 TLAB 的大小。

-XX:+PrintTLAB 跟踪 TLAB 的使用情况。用以确定是用多大的 TLABSize。

-XX:+ResizeTLAB 自动调整 TLAB 大小。

2.性能分析

对于调优这个事情来说,一般就是三个过程:

  • 性能监控:问题没有发生,你并不知道你需要调优什么。此时需要一些系统、应用的监控工具来发现问题。
  • 性能分析:问题已经发生,但是你并不知道问题到底出在哪里。此时就需要使用工具、经验对系统、应用进行瓶颈分析,以求定位到问题原因。
  • 性能调优:经过上一步的分析定位到了问题所在,需要对问题进行解决,使用代码、配置等手段进行优化。

在系统层面能够影响应用性能的一般包括三个因素:CPU、内存和IO,可以从这三方面进行程序的性能瓶颈分析。

CPU分析

当程序响应变慢的时候,首先使用top、vmstat、ps等命令查看系统的cpu使用率是否有异常,从而可以判断出是否是cpu繁忙造成的性能问题。其中,主要通过us(用户进程所占的%)这个数据来看异常的进程信息。当us接近100%甚至更高时,可以确定是cpu繁忙造成的响应缓慢。一般说来,cpu繁忙的原因有以下几个:

  • 线程中有无限空循环、无阻塞、正则匹配或者单纯的计算
  • 发生了频繁的gc
  • 多线程的上下文切换

确定好cpu使用率最高的进程之后就可以使用jstack来打印出异常进程的堆栈信息:
jstack [pid]
在这里插入图片描述
接下来需要注意的一点是,Linux下所有线程最终还是以轻量级进程的形式存在系统中的,而使用jstack只能打印出进程的信息,这些信息里面包含了此进程下面所有线程(轻量级进程-LWP)的堆栈信息。因此,进一步的需要确定是哪一个线程耗费了大量CPU,此时可以使用top -p [processId] -H来查看,也可以直接通过ps -Le来显示所有进程,包括LWP的资源耗费信息。最后,通过在jstack的输出文件中查找对应的LWP的id即可以定位到相应的堆栈信息。其中需要注意的是线程的状态:RUNNABLE、WAITING等。对于Runnable的进程需要注意是否有耗费cpu的计算。对于Waiting的线程一般是锁的等待操作。

也可以使用jstat来查看对应进程的gc信息,以判断是否是gc造成了cpu繁忙。
jstat -gcutil [pid]
jstat
在这里插入图片描述
还可以通过vmstat,通过观察内核状态的上下文切换(cs)次数,来判断是否是上下文切换造成的cpu繁忙。

vmstat 1 5jstat
此外,有时候可能会由jit引起一些cpu飚高的情形,如大量方法编译等。这里可以使用-XX:+PrintCompilation这个参数输出jit编译情况,以排查jit编译引起的cpu问题。

内存分析

对Java应用来说,内存主要是由堆外内存和堆内内存组成。

  • 堆外内存

堆外内存主要是JNI、Deflater/Inflater、DirectByteBuffer(nio中会用到)使用的。对于这种堆外内存的分析,还是需要先通过vmstat、sar、top、pidstat(这里的sar,pidstat以及iostat都是sysstat软件套件的一部分,需要单独安装)等查看swap和物理内存的消耗状况再做判断的。此外,对于JNI、Deflater这种调用可以通过Google-preftools来追踪资源使用状况。

  • 堆内内存

此部分内存为Java应用主要的内存区域。通常与这部分内存性能相关的有:

  1. 创建的对象:这个是存储在堆中的,需要控制好对象的数量和大小,尤其是大的对象很容易进入老年代
  2. 全局集合:全局集合通常是生命周期比较长的,因此需要特别注意全局集合的使用
  3. 缓存:缓存选用的数据结构不同,会很大程序影响内存的大小和gc
  4. ClassLoader:主要是动态加载类容易造成永久代内存不足
  5. 多线程:线程分配会占用本地内存,过多的线程也会造成内存不足

以上使用不当很容易造成:

  1. 频繁GC -> Stop the world,使你的应用响应变慢
  2. OOM,直接造成内存溢出错误使得程序退出。OOM又可以分为以下几种:
  • Heap space:堆内存不足
  • PermGen space:永久代内存不足
  • Native thread:本地线程没有足够内存可分配
    排查堆内存问题的常用工具是jmap,是jdk自带的。一些常用用法如下:

查看jvm内存使用状况:jmap -heap
查看jvm内存存活的对象:jmap -histo:live
把heap里所有对象都dump下来,无论对象是死是活:jmap -dump:format=b,file=xxx.hprof
先做一次full GC,再dump,只包含仍然存活的对象信息:jmap -dump:format=b,live,file=xxx.hprof
此外,不管是使用jmap还是在OOM时产生的dump文件,可以使用**Eclipse的MAT(MEMORY ANALYZER TOOL)来分析,可以看到具体的堆栈和内存中对象的信息。**当然jdk自带的jhat也能够查看dump文件(启动web端口供开发者使用浏览器浏览堆内对象的信息)。此外,VisualVM也能够打开hprof文件,使用它的heap walker查看堆内存信息。

IO分析

通常与应用性能相关的包括:文件IO和网络IO。

  • 文件IO

可以使用系统工具pidstat、iostat、vmstat来查看io的状况。这里可以看一张使用vmstat的结果图。
在这里插入图片描述
这里主要注意bi和bo这两个值,分别表示块设备每秒接收的块数量和块设备每秒发送的块数量,由此可以判定io繁忙状况。进一步的可以通过使用strace工具定位对文件io的系统调用。通常,造成文件io性能差的原因不外乎:

  • 大量的随机读写

  • 设备慢

  • 文件太大

  • 网络IO

查看网络io状况,一般使用的是netstat工具。可以查看所有连接的状况、数目、端口信息等。例如:当time_wait或者close_wait连接过多时,会影响应用的相应速度。
在这里插入图片描述
此外,还可以使用tcpdump来具体分析网络io的数据。当然,tcpdump出的文件直接打开是一堆二进制的数据,可以使用wireshark阅读具体的连接以及其中数据的内容。

3.JVM性能调优

CPU调优
  • 不要存在一直运行的线程(无限while循环),可以使用sleep休眠一段时间。这种情况普遍存在于一些pull方式消费数据的场景下,当一次pull没有拿到数据的时候建议sleep一下,再做下一次pull。
  • 轮询的时候可以使用wait/notify机制
  • 避免循环、正则表达式匹配、计算过多,包括使用String的format、split、replace方法(可以使用apache的commons-lang里的StringUtils对应的方法),使用正则去判断邮箱格式(有时候会造成死循环)、序列/反序列化等。
  • 结合jvm和代码,避免产生频繁的gc,尤其是full GC。

此外,使用多线程的时候,还需要注意以下几点:

  • 使用线程池,减少线程数以及线程的切换
  • 多线程对于锁的竞争可以考虑减小锁的粒度(使用ReetrantLock)、拆分锁(类似ConcurrentHashMap分bucket上锁), 或者使用CAS、ThreadLocal、不可变对象等无锁技术。此外,多线程代码的编写最好使用jdk提供的并发包、Executors框架以及ForkJoin等,此外Discuptor和Actor在合适的场景也可以使用。
内存调优

内存的调优主要就是对jvm的调优。

  • 合理设置各个代的大小。避免新生代设置过小(不够用,经常minor gc并进入老年代)以及过大(会产生碎片),同样也要避免Survivor设置过大和过小。
  • 选择合适的GC策略。需要根据不同的场景选择合适的gc策略。这里需要说的是,cms并非全能的。除非特别需要再设置,毕竟cms的新生代回收策略parnew并非最快的,且cms会产生碎片。此外,G1直到jdk8的出现也并没有得到广泛应用,并不建议使用。
  • jvm启动参数配置-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:[log_path],以记录gc日志,便于排查问题。
    其中,

对于第一点,具体的还有一点建议:

  • 年轻代大小选择:响应时间优先的应用,尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生gc的频率是最小的。同时,也能够减少到达年老代的对象。吞吐量优先的应用,也尽可能的设置大,因为对响应时间没有要求,垃圾收集可以并行进行,建议适合8CPU以上的应用使用。
  • 年老代大小选择:响应时间优先的应用,年老代一般都是使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。**如果堆设置小了,会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。**最优化的方案,一般需要参考以下数据获得:
  1. 并发垃圾收集信息
  2. 持久代并发收集次数
  3. 传统GC信息
  4. 花在年轻代和年老代回收上的时间比例

一般吞吐量优先的应用都应该有一个很大的年轻代和一个较小的年老代。这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代存放长期存活对象。

此外,较小堆引起的碎片问题:因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:-XX:+UseCMSCompactAtFullCollection,使用并发收集器时,开启对年老代的压缩。同时使用-XX:CMSFullGCsBeforeCompaction=xx设置多少次Full GC后,对年老代进行压缩。

其余对于jvm的优化问题可见后面JVM参数进阶一节。

代码上,也需要注意:

  • 避免保存重复的String对象,同时也需要小心String.subString()与String.intern()的使用,尤其是后者其底层数据结构为StringTable,当字符串大量不重复时,会使得StringTable非常大(一个固定大小的hashmap,可以由参数-XX:StringTableSize=N设置大小),从而影响young gc的速度。在jackson和fastjson中使用了此方法,
  • 尽量不要使用finalizer
  • 释放不必要的引用:ThreadLocal使用完记得释放以防止内存泄漏,各种stream使用完也记得close。
  • 使用对象池避免无节制创建对象,造成频繁gc。但不要随便使用对象池,除非像连接池、线程池这种初始化/创建资源消耗较大的场景,
  • 缓存失效算法,可以考虑使用SoftReference、WeakReference保存缓存对象
  • 谨慎热部署/加载的使用,尤其是动态加载类等
  • 不要用Log4j输出文件名、行号,因为Log4j通过打印线程堆栈实现,生成大量String。此外,使用log4j时,建议此种经典用法,先判断对应级别的日志是否打开,再做操作,否则也会生成大量String。
IO调优
  • 文件IO上需要注意:

考虑使用异步写入代替同步写入,可以借鉴redis的aof机制。
利用缓存,减少随机读
尽量批量写入,减少io次数和寻址
使用数据库代替文件存储

  • 网络IO上需要注意:

和文件IO类似,使用异步IO、多路复用IO/事件驱动IO代替同步阻塞IO
批量进行网络IO,减少IO次数
使用缓存,减少对网络数据的读取
使用协程: Quasar

此外,jdk7、8在jvm的性能上做了一些增强:

  • 通过-XX:+TieredCompilation开启JDK7的多层编译(tiered compilation)支持。多层编译结合了客户端C1编译器和服务端C2编译器的优点(客户端编译能够快速启动和及时优化,服务器端编译可以提供更多的高级优化),是一个非常高效利用资源的切面方案。在开始时先进行低层次的编译,同时收集信息,在后期再进一步进行高层次的编译进行高级优化。需要注意的一点:这个参数会消耗比较多的内存资源,因为同一个方法被编译了多次,存在多份native内存拷贝,建议把code cache调大一点儿(-XX:+ReservedCodeCacheSize,InitialCodeCacheSize)。否则有可能由于code cache不足,jit编译的时候不停的尝试清理code cache,丢弃无用方法,消耗大量资源在jit线程上。
JVM参数进阶

jvm的参数设置一直是比较理不清的地方,很多时候都搞不清都有哪些参数可以配置,参数是什么意思,为什么要这么配置等。这里主要针对这些做一些常识性的说明以及对一些容易让人进入陷阱的参数做一些解释。

  • 启动参数默认值

Java有很多的启动参数,而且很多版本都并不一样。但是现在网上充斥着各种资料,如果不加辨别的全部使用,很多是没有效果或者本来就是默认值的。一般的,我们可以通过使用java -XX:+PrintFlagsInitial来查看所有可以设置的参数以及其默认值。也可以在程序启动的时候加入-XX:+PrintCommandLineFlags来查看与默认值不相同的启动参数。如果想查看所有启动参数(包括和默认值相同的),可以使用-XX:+PrintFlagsFinal。

此外,还可以使用jinfo命令显示启动的参数。

jinfo -flags [pid] #查看目前启动使用的有效参数
jinfo -flag [flagName] [pid] #查看对应参数的值

这里需要指出的是,当你配置jvm参数时,最好是先通过以上命令查看对应参数的默认值再确定是否需要设置。也最好不要配置你搞不清用途的参数,毕竟默认值的设置是有它的合理之处的。

  • 动态设置参数

当Java应用启动后,定位到了是GC造成的性能问题,但是你启动的时候并没有加入打印gc的参数,很多时候的做法就是重新加参数然后重启应用。但这样会造成一定时间的服务不可用。最佳的做法是能够在不重启应用的情况下,动态设置参数。使用jinfo可以做到这一点(本质上还是基于jmx的)。

jinfo -flag [+/-][flagName] [pid] #启用/禁止某个参数
jinfo -flag [flagName=value] [pid] #设置某个参数

  • -verbose:gc 与 -XX:+PrintGCDetails

很多gc推荐设置都同时设置了这两个参数,其实,只要打开了-XX:+PrintGCDetails,前面的选项也会同时打开,无须重复设置。

  • -XX:+DisableExplicitGC

这个参数的作用就是使得system.gc变为空调用,很多推荐设置里面都是建议开启的。但是,如果你用到了NIO或者其他使用到堆外内存的情况,使用此选项会造成oom。可以用XX:+ExplicitGCInvokesConcurrent或XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses(配合CMS使用,使得system.gc触发一次并发gc)代替。

此外,还有一个比较有意思的地方。如果你不设置此选项的话,当你使用了RMI的时候,会周期性地来一次full gc。这个现象是由于分布式gc造成的,为RMI服务。具体的可见此链接内容中与dgc相关的:http://docs.oracle.com/javase/6/docs/technotes/guides/rmi/sunrmiproperties.html

  • MaxDirectMemorySize

此参数是设置的堆外内存的上限值。当不设置的时候为-1,此值为-Xmx减去一个survivor space的预留大小。

  • 由于遗留原因,作用相同的参数

-Xss 与 -XX:ThreadStackSize
-Xmn 与 -XX:NewSize,此外这里需要注意的是设置了-Xmn的话,NewRatio就没作用了。

  • -XX:MaxTenuringThreshold

使用工具查看此值默认值为15,但是选择了CMS的时候,此值会变成4。当此值设置为0时,所有eden里的活对象在经历第一次minor GC的时候就会直接晋升到old gen,survivor space直接就没用。还有值得注意的一点,当使用并行回收器时,此值是没有作用的,并行回收器默认是自动调整这些参数以求达到吞吐量最大的。此外,即使是使用CMS等回收器,晋升到老年代的age也不是不变的,当某一age的对象的大小达到年轻代的50%时,这个age会被动态调整为晋升年龄。

  • -XX:HeapDumpPath

使用此参数可以指定-XX:+HeapDumpBeforeFullGC、-XX:+HeapDumpAfterFullGC、-XX:+HeapDumpOnOutOfMemoryError触发heap dump文件的存储位置。

  • -XX:+UseAdaptiveSizePolicy

此参数在并行回收器时是默认开启的,会根据应用运行状况做自我调整,如MaxTenuringThreshold、survivor区大小等。其中第一次晋升老年代的年龄以InitialTenuringThreshold(默认为7)开始,后续会自动调整。如果希望跟踪每次minor GC后新的存活周期的阈值,可在启动参数上增加:-XX:+PrintTenuringDistribution。如果想要可以配置这些参数,可以关闭此选项,但paralle的性能很难达到最佳。其他垃圾回收期则慎重开启此开关。

4.内存异常

  • 内存泄露:代码中的某个对象本应该被虚拟机回收,但因为拥有GCRoot引用而没有被回收。关于GCRoot概念,下一篇文章讲解。
  • 内存溢出: 虚拟机由于堆中拥有太多不可回收对象没有回收,导致无法继续创建新对象。

在分析问题之前先给大家讲一讲排查内存溢出问题的方法,内存溢出时JVM虚拟机会退出,那么我们怎么知道JVM运行时的各种信息呢,Dump机制会帮助我们,可以通过加上VM参数-XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现内存溢出异常时生成dump文件,然后通过外部工具(作者使用的是VisualVM)来具体分析异常的原因。

内存溢出

内存溢出包括:

  • Java堆内存异常(用VisualVM工具分析heapDump文件)
  • Java栈内存异常(一般不会出现,在做多线程开发时,当创建很多线程时,容易出现OOM(OutOfMemoryError),这时可以通过具体情况,减少最大堆容量,或者栈容量来解决问题)
  • 方法区内存异常(在经常动态生成大量Class的应用中,需要特别注意类的回收状况,这类场景除了例子中的CGLib技术,常见的还有,大量JSP,反射,OSGI等。)
内存泄漏

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

下面给出了一个简单的内存泄露的例子。在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个Vector中,如果我们仅仅释放引用本身,那么Vector仍然引用该对象,所以这个对象对GC来说是不可回收的。因此,如果对象加入到Vector后,还必须从Vector中删除,最简单的方法就是将Vector对象设置为null。

Vector v=new Vector(10);
for (int i=1;i<100; i++)
{
    Object o=new Object();
    v.add(o);
    o=null;
}

此时,所有的Object对象都没有被释放,因为变量v引用这些对象。

常见等内存泄漏原因:
1、静态集合类引起内存泄露:
像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
例:
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}//
在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。

2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
例:
public static void main(String[] args)
{
Set set = new HashSet();
Person p1 = new Person(“唐僧”,“pwd1”,25);
Person p2 = new Person(“孙悟空”,“pwd2”,26);
Person p3 = new Person(“猪八戒”,“pwd3”,27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println(“总共有:”+set.size()+" 个元素!"); //结果:总共有:3 个元素!
p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变

set.remove(p3); //此时remove不掉,造成内存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println(“总共有:”+set.size()+" 个元素!"); //结果:总共有:4 个元素!
for (Person person : set)
{
System.out.println(person);
}
}

3、监听器
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
4、各种连接
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接
5、内部类和外部模块等的引用
内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如:
public void registerMsg(Object b);
这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用
6、单例模式
不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露,考虑下面的例子:
class A{
public A(){
B.getInstance().setA(this);
}

}
//B类采用单例模式
class B{
private A a;
private static B instance=new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a=a;
}
//getter…
}
显然B采用singleton模式,它持有一个A对象的引用,而这个A类的对象将不能被回收。 想象下如果A是个比较复杂的对象或者集合类型会发生什么情况

5.VisualVM应用

https://blog.csdn.net/a724888/article/details/79220184

6.JVM调优目标

GC调优(Tuning Garbage Collection)和其他性能调优是同样的原理。初学者可能会被 200 多个 GC参数弄得一头雾水, 然后随便调整几个来试试结果,又或者修改几行代码来测试。其实只要参照下面的步骤,就能保证你的调优方向正确:

  1. 列出性能调优指标(State your performance goals)
  2. 执行测试(Run tests)
  3. 检查结果(Measure the results)
  4. 与目标进行对比(Compare the results with the goals)
  5. 如果达不到指标, 修改配置参数, 然后继续测试(go back to running tests)

第一步, 我们需要做的事情就是: 制定明确的GC性能指标。对所有性能监控和管理来说, 有三个维度是通用的:

  • Latency(延迟)
  • Throughput(吞吐量)
  • Capacity(系统容量)
Latency(延迟)

GC的延迟指标由一般的延迟需求决定。延迟指标通常如下所述:

  • 所有交易必须在10秒内得到响应
  • 90%的订单付款操作必须在3秒以内处理完成
  • 推荐商品必须在 100 ms 内展示到用户面前

面对这类性能指标时, 需要确保在交易过程中, GC暂停不能占用太多时间,否则就满足不了指标。“不能占用太多” 的意思需要视具体情况而定, 还要考虑到其他因素, 比如外部数据源的交互时间(round-trips), 锁竞争(lock contention), 以及其他的安全点等等。

假设性能需求为: 90%的交易要在 1000ms 以内完成, 每次交易最长不能超过 10秒。 根据经验, 假设GC暂停时间比例不能超过10%。 也就是说, 90%的GC暂停必须在 100ms 内结束, 也不能有超过 1000ms 的GC暂停。为简单起见, 我们忽略在同一次交易过程中发生多次GC停顿的可能性。

有了正式的需求,下一步就是检查暂停时间。通过查看GC日志, 检查一下GC暂停的时间。相关的信息散落在不同的日志片段中, 看下面的数据:

2015-06-04T13:34:16.974-0200: 2.578: [Full GC (Ergonomics)
        [PSYoungGen: 93677K->70109K(254976K)] 
        [ParOldGen: 499597K->511230K(761856K)] 
        593275K->581339K(1016832K),
        [Metaspace: 2936K->2936K(1056768K)]
    , 0.0713174 secs]
    [Times: user=0.21 sys=0.02, real=0.07 secs

此事件将应用线程暂停了 0.0713174 秒。虽然花费的总时间为 210 ms, 但因为是多核CPU机器, 所以最重要的数字是应用线程被暂停的总时间, 这里使用的是并行GC, 所以暂停时间大约为 70ms 。 这次GC的暂停时间小于 100ms 的阈值,满足需求。

Throughput(吞吐量)

吞吐量和延迟指标有很大区别。当然两者都是根据一般吞吐量需求而得出的。一般吞吐量需求(Generic requirements for throughput) 类似这样:

  • 解决方案每天必须处理 100万个订单
  • 解决方案必须支持1000个登录用户,同时在5-10秒内执行某个操作: A、B或C
  • 每周对所有客户进行统计, 时间不能超过6小时,时间窗口为每周日晚12点到次日6点之间。

可以看出,吞吐量需求不是针对单个操作的, 而是在给定的时间内, 系统必须完成多少个操作。和延迟需求类似, GC调优也需要确定GC行为所消耗的总时间。每个系统能接受的时间不同, 一般来说, GC占用的总时间比不能超过 10%。

现在假设需求为: 每分钟处理 1000 笔交易。同时, 每分钟GC暂停的总时间不能超过6秒(即10%)。

有了正式的需求, 下一步就是获取相关的信息。依然是从GC日志中提取数据, 可以看到类似这样的信息:

2015-06-04T13:34:16.974-0200: 2.578: [Full GC (Ergonomics)
        [PSYoungGen: 93677K->70109K(254976K)] 
        [ParOldGen: 499597K->511230K(761856K)] 
        593275K->581339K(1016832K), 
        [Metaspace: 2936K->2936K(1056768K)], 
     0.0713174 secs] 
     [Times: user=0.21 sys=0.02, real=0.07 secs

此时我们对 用户耗时(user)和系统耗时(sys)感兴趣, 而不关心实际耗时(real)。在这里, 我们关心的时间为 0.23s(user + sys = 0.21 + 0.02 s), 这段时间内, GC暂停占用了 cpu 资源。 重要的是, 系统运行在多核机器上, 转换为实际的停顿时间(stop-the-world)为 0.0713174秒, 下面的计算会用到这个数字。

提取出有用的信息后, 剩下要做的就是统计每分钟内GC暂停的总时间。看看是否满足需求: 每分钟内总的暂停时间不得超过6000毫秒(6秒)。

Capacity(系统容量)

系统容量(Capacity)需求,是在达成吞吐量和延迟指标的情况下,对硬件环境的额外约束。这类需求大多是来源于计算资源或者预算方面的原因。例如:

  • 系统必须能部署到小于512 MB内存的Android设备上
  • 系统必须部署在Amazon EC2实例上, 配置不得超过 c3.xlarge(4核8GB)。
  • 每月的 Amazon EC2 账单不得超过 $12,000
    因此, 在满足延迟和吞吐量需求的基础上必须考虑系统容量。可以说, 假若有无限的计算资源可供挥霍, 那么任何 延迟和吞吐量指标 都不成问题, 但现实情况是, 预算(budget)和其他约束限制了可用的资源。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值