jvm之5垃圾回收
参考
https://www.cnblogs.com/czwbig/p/11127124.html JVM JMM
https://zhuanlan.zhihu.com/p/402225242
http://blog.csdn.net/java2000_wl/article/details/8042010 jvm参数
http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html jvm参数
如何确定对象是垃圾
引用计数法:
堆中每个对象实例都有⼀个引⽤用计数。当⼀个对象被创建时,且将该对象实例分配给⼀个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引⽤用时,计数加1(a = b,则b引⽤的对象实例的计数器+1),但当一个对象实例的某个引⽤超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引⽤计数器减1。
缺点:
无法处理循环引用。
可达性分析算法:
根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引⽤用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点(垃圾)。
可以做为GC ROOT的节点有:
- Java虚拟机栈中被引用的对象,各个线程调用的参数、局部变量、临时变量等。
- 方法区中类静态属性引用的对象,比如引用类型的静态变量。
- 方法区中常量引用的对象。
- 本地方法栈中所引用的对象。
- Java虚拟机内部的引用,基本数据类型对应的Class对象,一些常驻的异常对象。
- 被同步锁(synchronized)持有的对象。
三色标记算法
不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
分为三个颜色:白色、灰色和黑色:
白色:这个对象尚示被垃圾收集器访问过,在初始阶段,所有对象都是白色,如果在可达性分析结束后,仍然是白色的对象,即代表不可达。
灰色:这个对象已经被垃圾收集器访问过,但是这个对象上至少存在一个直接引用还没有被扫描过。
黑色:对象和它所直接引用的所有对象都被访问过。这里只要访问过就行,比如A只引用了B,B引用了C、D,那么只要A和B都被访问过,A就是黑色,即使B所引用的C或D还没有被访问到,此时B就是灰色。
根据这些定义,我们可以得出:
在可达性分析的初始阶段,所有对象都是白色,一旦访问了这个对象,那么就变成灰色,一旦这个对象所有直接引用的对象都访问过(或者没有引用其它对象),那么就变成黑色。
初始标记之后(标记GC Roots 能直接关联的对象),GC Root节点变为黑色(GC Root不会是垃圾),GC Root直接引用的对象变为灰色。
正常情况下,一个对象如果是黑色,那么其直接引用的对象要么是黑色,要么是灰色,不可能是白色(如果出现了黑色对象直接引用白色对象的情况,就说明漏标了,就会导致对象误删,后面会介绍如何解决),这个特性也可以说是三色标记算法正确性保障的前提条件。
并发标记带来的问题
如果整个标记过程是STW的,那么没有任何问题,但是并发标记的过程中,用户线程也在运行,那么对象引用关系就可能发生改变,进而导致两个问题出现。
2.1 浮动垃圾:
把垃圾也标黑了
浮动垃圾你觉得没啥所谓,即使本次不清理,下一次GC也会被清理,而且并发清理阶段也会产生所谓的浮动垃圾,影响不大。
2.2 非垃圾变成了垃圾
在1994年Wilson在理论上证明了,当且仅当以下两个条件同时满足时,会产生“对象消失”的问题,即:应该是黑色的对象被误标记为白色。
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
解决非垃圾标记为垃圾
出现这个问题的主要原因是,一个对象从被B引用,变更为了被A引用。那么对于A来说就是多了一个直接引用,对于B来说就是少了一个直接引用。
CMS 增量更新(Incremental Update)
增量更新是站在新增引用的对象(也就是例子中的A对象)的角度来解决问题。所谓增量更新,就是在赋值操作之前添加一个写屏障,在写屏障中记录新增的引用。
比如,用户线程要执行:A.f = F;那么在写屏障中将新增的这个引用关系记录下来。
标准的描述就是,当黑色对象新增一个白色对象的引用时,就通过写屏障将这个引用关系记录下来。然后在重新标记阶段,再以这些引用关系中的黑色对象为根,再扫描一次,以此保证不会漏标。
要实现也很简单,在重新标记阶段直接把A对象(和其它有相同情况发生的对象)变为灰色,放入队列中,再来一次枚举过程。要注意,在重新标记阶段如果用户线程还是继续执行,那么这个GC永远可能也做不完了,所以重新标记需要STW,但是这个时间消耗不会太夸张。
G1 原始快照(SATB, Snapshot At The Beginning)
原始快照是站在减少引用的对象(也就是例子中的B对象)的角度来解决问题。所谓原始快照,简单的讲,就是在赋值操作(这里是置空)执行之前添加一个写屏障,在写屏障中记录被置空的对象引用。
比如,用户线程要执行:B.f=null;那么在写屏障中,首先会把B.f记录下来,然后再进行置空操作。记录下来的这个对象就可以称为原始快照。
那么记录下来之后呢?很简单,之后直接把它变为黑色。意思就是默认认为它不是垃圾,不需要将其清理。当然,这样处理有两种情况,一种情况是,F的确不是垃圾,直到清理的那一刻,都仍然有至少一个引用链能访问到它,这没有什么问题;另一种情况就是F又变成了垃圾。在上述的例子中,就是A到F的引用链也断了,或者直接A都成垃圾了,那F对象就成了浮动垃圾。对于浮动垃圾,前面不止一次就提到了,直接不用理会,如果到下一次GC时它仍然是垃圾,自然会被清理掉。
方案抉择
从增量更新和原始快照的实现(理论上)就可以发现,原始快照相比于增量更新来说效率会更高,因为不用在重新标记阶段再去做枚举遍历,但是也就可能会导致有更多的浮动垃圾。G1使用的就是原始快照,CMS使用的是增量更新。
既然原始快照可能会有更严重的浮动垃圾问题,那么为什么不使用增量更新呢?原因可能很简单,就是因为简单。想象一下,G1虽然也是基于年轻代和老年代的分代收集算法,但是年轻代和老年代被弱化为了逻辑上,其所管理的内存被划分为了很多region,对象跨代引用带来的问题在G1中要比传统的分代收集器更加突出,虽然有Remember Set方案缓解,但是相对来说在重新标记阶段进行再次遍历枚举的代价会大很多。最重要的是,重新标记(最终标记)阶段是会STW的,如果这个阶段花费太多的时间去做可达性分析,那么就违背了G1低延时的理念。
垃圾回收算法
标记清除算法
算法分为标记和清除两个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足
1.效率问题,标记和清除两个过程的效率都不高;
2.另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后在程序运行过程中需要分配大对象时 ,无法找到足够的连续内存而不得不提前触发别一次垃圾收集动作。
复制算法
拥有两块大小相等的内存,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片问题,只要移动堆指针,按顺序分配内存即可,实现简单,运行高效。
复制算法一般用在回收新生代。研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden区和两块较小的survivor区,每次使用eden和其中一块survivor。survivor from 和 survivor to,内存默认比例8:1:1。默认会浪费10%的内存空间,当survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。
标记整理算法
对象存活率较高时复制操作耗时,有可能还需要分配担保,一般老年代不使复制算法。老年代使用标记整理算法。
首先标记出所有需要回收的对象,让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
GC 分代收集算法 VS 分区收集算法
分代收集算法
当前主流 VM 垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为几块, 如 JVM 中的 新生代、老年代、永久代,这样就可以根据各年代特点分别采用最适当的 GC 算法
在新生代
每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集.
在老年代
因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记-清理”或“标记-整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存.
分区收集算法
分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次 GC 所产生的停顿。
串行、并发、并行
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
所以我认为它们最关键的点就是:是否是『同时』。
垃圾回收器
收集器 | 收集线程和stop the world | 新生代、老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 单线程收集线程,整个收集过程stop the world | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的client模式 |
parNew | 多线程收集线程,整个收集过程stop the world | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在server模式下与CMS配合 |
Parallel scanvenge | 多线程收集线程,整个收集过程stop the world | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Serial old | 单线程收集线程,整个收集过程stop the world | 老年代 | 标记整理 | 响应速度优先 | 单CPU环境下的client模式、CMS的后备方案 |
Parallel old | 多线程收集线程,整个收集过程stop the world | 老年代 | 标记整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 分段,多线程,初始标记、重新标记时stop the world | 老年代 | 标记清除 | 响应速度优先 | 集中在网站或B/S系统服务端上的应用 |
G1 | 分段,多线程,初始标记时stop the world,其他阶段可设置 | 逻辑分代,分区 | 标记整理 + 复制 | 响应速度优先 | 面向服务端应用,大内存时使用 |
ParNew 垃圾收集器
ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。
parallel scavenge收集器
可以通过指定垃圾收集器最大停顿时间(-XX:MaxGCPauseMillis),来达到我们预期设定的吞吐量大小(-XX:GCTimeRatio)。
吞吐量 = 执行用户代码时间 / (执行用户代码时间 + 垃圾回收占用时间)
吞吐量即CPU用于运行用户代码的时间与CPU消耗的总时间的比值。
CMS 收集器
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。
最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。
CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:
- 初始标记
只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
- 并发标记
进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
- 重新标记
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
- 并发清除
清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看
CMS 收集器的内存回收和用户线程是一起并发地执行。
CMS 收集器工作过程:
G1收集器(Garbage First)
在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么为整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1垃圾收集器使用Mixed GC模式可以面向堆内存任何部分来组成回收集(Collection Set,一般简称为Cset)进行回收,衡量标准不再是它属于哪个年代,而是哪块内存中存放的垃圾数最多,回收收益最大。
G1基于Region的堆布局时它能够实现这个目标的关键。虽然G1仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为大小相等的独立区域(Region),且每一个Region都可以根据需要扮演新生代的Eden空间,Survivor空间或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活一段时间,熬过多次收集的旧对象都能获取很好的收集效果。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且为2的N次幂。而对于那些超过整个Region容量的超级大对象,将会被存放N个连续的Humongous Region中,G1的大多数行为都把HumonGous Region作为老年代的一部分来进行看待。
虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列无序连续区域的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单词回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
更具体的思路为让G1收集器去跟踪各个Region中的垃圾堆积的”价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后再后台维护一个有限级列表,每次根据用户设定的收集停顿时间(通过-XX:MaxGCPauseMillis指定,默认值为200毫秒),优先处理回收价值收益最大的Region,这也是”Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式保证了G1收集器在有限的时间内获取尽可能高的收集效率。
工作步骤:
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一个阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时比较短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记(Concurrent Marking):从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时比较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收(Live Data Counting and Evacuation): 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
GC类型
- Minor GC/Young GC:针对新生代的垃圾收集;(耗时较短、发生频率高)
- Major GC/Old GC:针对老年代的垃圾收集。(耗时较长、发生频率低)
- Full GC:针对整个Java堆以及方法区的垃圾收集。
Minor GC工作原理
通常情况下,初次被创建的对象存放在新生代的Eden区,当第一次触发Minor GC,Eden区存活的对象被转移到Survivor区的某一块区域。以后再次触发Minor GC的时候,Eden区的对象连同一块Survivor区的对象一起,被转移到了另一块Survivor区。可以看到,这两块Survivor区我们每一次只使用其中的一块,这样也仅仅是浪费了一块Survivor区。
需要注意的2点:
- 每经历过一次垃圾回收的对象,它的分代年龄就加1,当分代年龄达到15以后,就直接被存放到老年代中。
- 给大对象分配内存的时候,大对象就会直接进入老年代。
Full GC工作原理
老年代是存储长期存活的对象的,占满时就会触发我们最常听说的Full GC,期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。
需要注意的几点:
- Full GC耗时较长,发生次数远没有Minor GC频繁,太频繁意味着性能出现问题。
- 标记-清除算法会产生大量内存碎片,以后如果需要为大对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次GC回收操作。
无论是Minor GC,还是Full GC,都会产生停顿现象,即Stop-The-World。Minor GC停顿时间较短,而Full GC耗时较长将导致长时间停顿、系统无响应,极大影响系统的性能。因此,Full GC日志的监控和性能分析在性能优化中极为重要。
FullGC触发条件
调用 System.gc()
此方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存。可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC
GC日志
开启GC日志
使用-verbose:gc或-XX:+PrintGC这两个标志中的任意一个能创建基本的GC日志。 默认为关闭。
使用-XX:+PrintGCDetails标志会创建更详细的GC日志
使用-XX:+PrintGCTimeStamps或者-XX:+PrintGCDateStamps 便于我们更精确地判断几次GC操作之间的时间。
默认情况下GC日志直接输出到标准输出,不过使用-Xloggc:filename标志也能修改输出到某个文件。
通过-XX:+UseGCLogfileRotation -XX:NumberOfGCLogfiles=N -XX:GCLogfileSize=N标志可以控制日志文件的循环。
默认情况下,UseGCLogfileRotation标志是关闭的。它负责打开或关闭GC日志滚动记录功能的。要求必须设置 -Xloggc参数
开启UseGCLogfileRotation标志后,默认的文件数目是0(意味着不作任何限制),默认的日志文件大小是0(同样也是不作任何限制)。
1 |
|
日志详解
JDK提供的工具
jps
JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
命令格式:
1 |
|
option参数:
- -l : 输出主类全名或jar路径
- -q : 只输出LVMID
- -m : 输出JVM启动时传递给main()的参数
- -v : 输出JVM启动时显示指定的JVM参数
jstat
jstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
https://docs.oracle.com/javase/7/docs/technotes/tools/share/jstat.html#gcnew_option
1 |
|
1 |
|
示例 1:jstat -gc 15 5000 5
每5秒一次显示进程号为15的java进程的GC情况,每5S生成异常,一共生成5次。
1 |
|
jstat -gccapacity 15
同-gc,不过还会输出Java堆各区域使用到的最大、最小空间
jstat -gcutil 15
同-gc,不过输出的是已使用空间占总空间的百分比
jstat -gccause 15
垃圾收集统计概述(同-gcutil),附加最近两次垃圾回收事件的原因
1 |
|
jstat -gcnew 15
统计新生代的行为
jstat -gcold 15
统计老年代的行为
jstat -class 15
监视类装载、卸载数量、总空间以及耗费的时间。
1 |
|
jstat -compiler 15
1 |
|
jinfo
jinfo (Configuration Info for Java)的作用是实时地查看和调整虚拟机各项参数。使用 jps 命令的-v 参数可以查看虚拟机启动时显式指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,除了去找资料外,就只能使用 info 的-flag 选项进行查询了
1 |
|
jmap
Jmap (Memory Map for Java)命令用于生成堆转储快照。如果不使用 jmap 命令,要想获取 Java 堆转储快照,还有一些比较“暴力”的手段:-XX: +HeapDumpOnOutOfMemoryError 参数,可以让虚拟机在 OOM 异常出现之后自动生成 dump 文件,用于系统复盘环节
和 info 命令一样,jmap 有不少功能在 Windows 平台下都是受限的,除了生成 dump 文件的- dump 选项和用于查看每个类的实例、空间占用统计的-histo选项在所有操作系统都提供之外,其余选项都只能在Linux/Solaris 下使用。
jmap常用命令
-dump
生成 Java 堆转储快照。格式为:-dump: format=b, file=
1 |
|
-histo more分页去查看
显示堆中对象统计信息,包括类、实例数量、合计容量
B :byte
C : char
I :Int
jmap -finalizerinfo 15
打印等待回收对象的信息
jmap -heap 15
打印heap的概要信息,GC使用的算法,heap的配置及wise heap的使用情况,可以用此来判断内存目前的使用情况以及垃圾回收情况,感觉这个非常使用!
jmap -histo:live 15 | more
打印堆的对象统计,包括对象数、内存大小等等 (因为在dump:live前会进行full gc,如果带上live则只统计活对象,因此不加live的堆大小要大于加live堆的大小 ),仅打印前15行。
jhat
jhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。
示例:jhat dump.hprof
当执行完毕后:
可以通过Http://localhost:7000访问:
具体排查时需要结合代码,观察是否大量应该被回收的对象在一直被引用或者是否有占用内存特别大的对象无法被回收。一般情况,会down到客户端用工具来分析。
jstack
stack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。
另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。
命令格式:
1 |
|
option参数:
1 |
|
图形化工具
JConsole
JConsole (Java Monitoring and Management Console)是一种基于 JMX 的可视化监视、管理工具,它管理部分的功能是针对 JMXMBean 进行管理,由于 MBean 可以使用代码、中间件服务器的管理控制台或者所有符合 JMX 规范的软件进行访问。
通过JDK/bin目录下的“jconsole.exe”启动JConsole 后,将自动搜索出本机运行的所有虚拟机进程,不需要用户自己再使用 jps 来查询了
开启远程连接
1 |
|
VisualVM
VisualVM是一个集成命令行JDK工具和轻量级分析功能的可视化工具
在IDEA安装VisualVM插件,File-> Setting-> Plugins -> Browers Repositrories 搜索VisualVM Launcher安装并重启IDEA
arthas
查看arthas相关的文章
其他三方工具
- MAT:Java 堆内存分析工具。
- GChisto:GC 日志分析工具。
- GCViewer:GC 日志分析工具。
- JProfiler:商用的性能分析利器。
- async:Java 应用性能分析工具,开源、火焰图、跨平台。
JVM参数
JVM的命令行参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
HotSpot参数分类
标准: - 开头,所有的HotSpot都支持
非标准:-X 开头,特定版本HotSpot支持特定命令
不稳定:-XX 开头,下个版本可能取消
Java -version 标准参数
java -Xloggc: var/log/xx.log 非标准
-XX:+PrintGCDetails 不稳定
GC常用参数
-Xmn -Xms -Xmx -Xss
年轻代 最小堆 最大堆 栈空间-XX:+UseTLAB
使用TLAB,默认打开-XX:+PrintTLAB
打印TLAB的使用情况-XX:TLABSize
设置TLAB大小-XX:+DisableExplictGC
System.gc()不管用 ,FGC-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintHeapAtGC
-XX:+PrintGCTimeStamps
-XX:+PrintGCApplicationConcurrentTime (低)
打印应用程序时间-XX:+PrintGCApplicationStoppedTime (低)
打印暂停时长-XX:+PrintReferenceGC (重要性低)
记录回收了多少种不同引用类型的引用-verbose:class
类加载详细过程-XX:+PrintVMOptions
XX:+PrintFlagsInitial 是打印所有的默认参数设置
-XX:+PrintFlagsFinal 是打印最终值,如果某个默认值被新值覆盖,显示新值
-XX:+PrintCommandLineFlags 是打印那些被新值覆盖的项必须会用
-Xloggc:opt/log/gc.log
-XX:MaxTenuringThreshold
升代年龄,最大值15锁自旋次数 -XX:PreBlockSpin 热点代码检测参数-XX:CompileThreshold 逃逸分析 标量替换 …
这些不建议设置-XX:PermSize
指非堆区初始化内存分配大小。(非堆区配置)-XX:MaxPermSize
指对非堆区分配内存的最大上限。(非堆区配置)
Parallel常用参数
- -XX:SurvivorRatio
- -XX:PreTenureSizeThreshold
大对象到底多大 - -XX:MaxTenuringThreshold
- -XX:+ParallelGCThreads
并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同 - -XX:+UseAdaptiveSizePolicy
自动选择各区大小比例
CMS常用参数
- -XX:+UseConcMarkSweepGC
- -XX:ParallelCMSThreads
CMS线程数量 - -XX:CMSInitiatingOccupancyFraction
使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收) - -XX:+UseCMSCompactAtFullCollection
在FGC时进行压缩 - -XX:CMSFullGCsBeforeCompaction
多少次FGC之后进行压缩 - -XX:+CMSClassUnloadingEnabled
- -XX:CMSInitiatingPermOccupancyFraction
达到什么比例时进行Perm回收 - GCTimeRatio
设置GC时间占用程序运行时间的百分比 - -XX:MaxGCPauseMillis
停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代
G1常用参数
- -XX:+UseG1GC
- -XX:MaxGCPauseMillis
建议值,G1会尝试调整Young区的块数来达到这个值 - -XX:GCPauseIntervalMillis
?GC的间隔时间 - -XX:+G1HeapRegionSize
分区大小,建议逐渐增大该值,1 2 4 8 16 32。
随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长
ZGC做了改进(动态区块大小) - G1NewSizePercent
新生代最小比例,默认为5% - G1MaxNewSizePercent
新生代最大比例,默认为60% - GCTimeRatio
GC时间建议比例,G1会根据这个值调整堆空间 - ConcGCThreads
线程数量 - InitiatingHeapOccupancyPercent
启动G1的堆空间占用比例
CMS 调优最佳参数
-server
-Xms6144m 指定应用程序可用的最小堆大小。
-Xmx6144m 指定应用程序可用的最大堆大小。 最好和最小一样,扩容会产生内存抖动,服务停顿。
-XX:NewSize=2048m 新生代初始化内存的大小(该值需要小于-Xms的值)
-XX:MaxNewSize=2048m 新生代可被分配的内存的最大上限(该值需要小于-Xmx的值)
-XX:MetaspaceSize=350m 设置元空间初始大小
-XX:MaxMetaspaceSize=512m 设置元空间最大可分配大小。
-Xss256k 设置栈内存的大小,设置栈的大小决定了函数调用的最大深度。
-XX:+unlockExperimentalVMOptions 解锁实验参数
-XX:+UseParNewGC 新生代用parNew收集器
-XX:ParallelGCThreads=4 这个参数是指定并行GC线程的数量,一般最好和CPU核心数量相当。同时这个参数只要是并行GC都可以使用。
-XX:+UseConcMarkSweepGC 使用CMS收集器。
-XX:CMSInitiatingOccupancyFraction=75 指定回收阈值
-XX:+UseCMSInitiatingOccupancyOnly 开启回收阈值
-XX:MaxTenuringThreshold=6 设置对象进入老年代的年龄 最大15
-XX:+ExplicitGCInvokesConcurrent System.gc()是正常Full GC, 会STW。打开此参数后,在做system.gc()时会做background模式CMS GC,即并行FUll GC,可提高full GC效率。
-XX:+CMSParallelRemarkEnabled 通过CMSScavengeBeforeRemark参数可以强制在重新标记阶段之前强制进行一次Young GC,通过设置 CMSParallelRemarkEnabled参数可以开启并行的Remark,加快remark的速度。
-XX:+UseCMSCompactAtFullCollection 配置在进行了Full GC时,对老年代进行压缩整理,处理掉内存碎片。
-XX:CMSFullGCsBeforeCompaction=1 配置进行了多少次 full GC之后执行一次内存压缩。
-XX:-OmitStackTraceInFastThrow 关闭省略异常栈信息从而快速抛出(开启时如果一个地方多次抛出异常将清空异常堆栈信息,快速抛出)
调优
JVM调优应该是Java性能优化的最后一颗子弹。JVM调优不是常规手段,性能问题一般第一选择是优化程序,最后的选择才是进行JVM调优。
什么是调优?
- 根据需求进行JVM规划和预调优
- 优化运行JVM运行环境(慢,卡顿)
- 解决JVM运行过程中出现的各种问题
调优前环境
1.如果是规划
这时要了解业务,选择合适的垃圾收集器
2.如果是压测
这时是出现响应慢还是直接OOM等问题。
3.线上监控
线上有监控发现达到阈值,或出现错误
调优步骤
购买主机,CPU、内存的选择
CPU 核数,与多线程的垃圾收集器
内存大小,如果内存太大,应使用G1
选择合适的垃圾回收器
- CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。
- CPU多核,关注吞吐量 ,那么选择PS+PO组合。
- CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,那么选择CMS。
- CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。
1 |
|
如何查看默认的垃圾回收器
查看:
方法一:java -XX:+PrintCommandLineFlags -version
参数 | 描述 |
---|---|
UseSerialGC | 虚拟机运行在Client模式下的默认值,打开此开关后,使用serial + serial old的收集器组合进行内存回收 |
UseParNewGC | 打开此开关后,使用ParNew + Serial Old 的收集器组合进行内存回收 |
UseConcMarkSweepGC | 打开此开关后,使用ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old收集器将作为CMS收集器出现 concurrent mode failure 失败后的后备收集器使用 |
UseParallelGC | 虚拟机运行在Server模式下默认值,打开此开关后,使用Parallel scavenge + serial Old (PS markSweep) 的收集器组合进行内存回收 |
UseParallelOldGC | 打开此开关后,使用Parallel Scavenge + parallel old 的收集器组合进行内存回收 |
UseG1GC | 打开此开关后,使用G1收集器 |
方法二:
打开GC日志,通过打印的GC日志的新生代、老年代名称判断
调整内存大小
现象:垃圾收集频率非常频繁。
原因:如果内存太小,就会导致频繁的需要进行垃圾收集才能释放出足够的空间来创建新的对象,所以增加堆内存大小的效果是非常显而易见的。
注意:如果垃圾收集次数非常频繁,但是每次能回收的对象非常少,那么这个时候并非内存太小,而可能是内存泄露导致对象无法回收,从而造成频繁GC。
参数配置:
1 |
|
设置符合预期的停顿时间
现象:程序间接性的卡顿
原因:如果没有确切的停顿时间设定,垃圾收集器以吞吐量为主,那么垃圾收集时间就会不稳定。
注意:不要设置不切实际的停顿时间,单次时间越短也意味着需要更多的GC次数才能回收完原有数量的垃圾.
参数配置:
1 |
|
调整内存区域大小比率
现象:某一个区域的GC频繁,其他都正常。
原因:如果对应区域空间不足,导致需要频繁GC来释放空间,在JVM堆内存无法增加的情况下,可以调整对应区域的大小比率。
注意:也许并非空间不足,而是因为内存泄造成内存无法回收。从而导致GC频繁。
参数配置:
1 |
|
调整对象升老年代的年龄
现象:老年代频繁GC,每次回收的对象很多。
原因:如果升代年龄小,新生代的对象很快就进入老年代了,导致老年代对象变多,而这些对象其实在随后的很短时间内就可以回收,这时候可以调整对象的升级代年龄,让对象不那么容易进入老年代解决老年代空间不足频繁GC问题。
注意:增加了年龄之后,这些对象在新生代的时间会变长可能导致新生代的GC频率增加,并且频繁复制这些对象新生的GC时间也可能变长。
配置参数:
1 |
|
调整大对象的标准
现象:老年代频繁GC,每次回收的对象很多,而且单个对象的体积都比较大。
原因:如果大量的大对象直接分配到老年代,导致老年代容易被填满而造成频繁GC,可设置对象直接进入老年代的标准。
注意:这些大对象进入新生代后可能会使新生代的GC频率和时间增加。
配置参数:
1 |
|
调整GC的触发时机
现象:CMS,G1 经常 Full GC,程序卡顿严重。
原因:G1和CMS 部分GC阶段是并发进行的,业务线程和垃圾收集线程一起工作,也就说明垃圾收集的过程中业务线程会生成新的对象,所以在GC的时候需要预留一部分内存空间来容纳新产生的对象,如果这个时候内存空间不足以容纳新产生的对象,那么JVM就会停止并发收集暂停所有业务线程(STW)来保证垃圾收集的正常运行。这个时候可以调整GC触发的时机(比如在老年代占用60%就触发GC),这样就可以预留足够的空间来让业务线程创建的对象有足够的空间分配。
注意:提早触发GC会增加老年代GC的频率。
配置参数:
1 |
|
调整 JVM本地内存大小
现象:GC的次数、时间和回收的对象都正常,堆内存空间充足,但是报OOM
原因: JVM除了堆内存之外还有一块堆外内存,这片内存也叫本地内存,可是这块内存区域不足了并不会主动触发GC,只有在堆内存区域触发的时候顺带会把本地内存回收了,而一旦本地内存分配不足就会直接报OOM异常。
注意: 本地内存异常的时候除了上面的现象之外,异常信息可能是OutOfMemoryError:Direct buffer memory。 解决方式除了调整本地内存大小之外,也可以在出现此异常时进行捕获,手动触发GC(System.gc())。
配置参数:
1 |
|
打开GC日志并观察
-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause
或者每天产生一个日志文件
3.观察日志
调优案例
案例一:升级内存后,更慢
原服务器32位,1.5G的堆,用户反馈网站比较缓慢,因此公司决定升级,新的服务器为64位,16G的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了。
响应慢为什么?
1.请求多,这时会造成,minor gc 和 full gc。
2.内存变大后,GC的时间也相应的变长
解决方案:
1.使用合适的垃圾回收器(cms 、 G1)
2.使用集群提供服务
案例二: 网站流量浏览量暴增后,网站反应页面响很慢
1、问题推测:在测试环境测速度比较快,但是一到生产就变慢,所以推测可能是因为垃圾收集导致的业务线程停顿。
2、定位:为了确认推测的正确性,在线上通过jstat -gc 指令 看到JVM进行GC 次数频率非常高,GC所占用的时间非常长,所以基本推断就是因为GC频率非常高,所以导致业务线程经常停顿,从而造成网页反应很慢。
3、解决方案:因为网页访问量很高,所以对象创建速度非常快,导致堆内存容易填满从而频繁GC,所以这里问题在于新生代内存太小,所以这里可以增加JVM内存就行了,所以初步从原来的2G内存增加到16G内存。
4、第二个问题:增加内存后的确平常的请求比较快了,但是又出现了另外一个问题,就是不定期的会间断性的卡顿,而且单次卡顿的时间要比之前要长很多。
5、问题推测:练习到是之前的优化加大了内存,所以推测可能是因为内存加大了,从而导致单次GC的时间变长从而导致间接性的卡顿。
6、定位:还是通过jstat -gc 指令 查看到 的确FGC次数并不是很高,但是花费在FGC上的时间是非常高的,根据GC日志 查看到单次FGC的时间有达到几十秒的。
7、解决方案: 因为JVM默认使用的是PS+PO的组合,PS+PO垃圾标记和收集阶段都是STW,所以内存加大了之后,需要进行垃圾回收的时间就变长了,所以这里要想避免单次GC时间过长,所以需要更换并发类的收集器,因为当前的JDK版本为1.7,所以最后选择CMS垃圾收集器,根据之前垃圾收集情况设置了一个预期的停顿的时间,上线后网站再也没有了卡顿问题。
案例三:后台导出数据引发的OOM 或其他接口响应速度变慢
查找问题点:
1.从堆内存信息下手,通过开启了-XX:+HeapDumpOnOutOfMemoryError参数 获得堆内存的dump文件。
2.VisualVM 对 堆dump文件进行分析,通过VisualVM查看到占用内存最大的对象是String对象,本来想跟踪着String对象找到其引用的地方,但dump文件太大,跟踪进去的时候总是卡死,而String对象占用比较多也比较正常,最开始也没有认定就是这里的问题。
3.从线程信息里面找突破点。通过线程进行分析,先找到了几个正在运行的业务线程,然后逐一跟进业务线程看了下代码,发现有个引起我注意的方法,导出订单信息。
4.因为订单信息导出这个方法可能会有几万的数据量,首先要从数据库里面查询出来订单信息,然后把订单信息生成excel,这个过程会产生大量的String对象。
问题可能出现点:
1.无重复提交限制
2.功能使用频繁,数据量大,生成大对象,直接进入老年代,从而触发full gc
解决方案:
1.重复提交限制
2.业务代码获取数据 循环获取,转成小对象,配合JVM的大对象进入老年代参数
案例四:单个缓存数据过大导致的系统CPU飚高
同导出。从redis 获取一个大对象,这个大对象到 java堆中,有可能是个大对象,直接进入老年代。
解决方案:
把大对象变小。业务调整。
案例五:数据分析平台系统频繁 Full GC
平台主要对用户在 App 中行为进行定时分析统计,并支持报表导出,使用 CMS GC 算法。
数据分析师在使用中发现系统页面打开经常卡顿,通过 jstat 命令发现系统每次 Young GC 后大约有 10% 的存活对象进入老年代。
原来是因为 Survivor 区空间设置过小,每次 Young GC 后存活对象在 Survivor 区域放不下,提前进入老年代。
通过调大 Survivor 区,使得 Survivor 区可以容纳 Young GC 后存活对象,对象在 Survivor 区经历多次 Young GC 达到年龄阈值才进入老年代。
调整之后每次 Young GC 后进入老年代的存活对象稳定运行时仅几百 Kb,Full GC 频率大大降低。
案例六:业务对接网关 OOM
网关主要消费 Kafka 数据,进行数据处理计算然后转发到另外的 Kafka 队列,系统运行几个小时候出现 OOM,重启系统几个小时之后又 OOM。
通过 jmap 导出堆内存,在 eclipse MAT 工具分析才找出原因:代码中将某个业务 Kafka 的 topic 数据进行日志异步打印,该业务数据量较大,大量对象堆积在内存中等待被打印,导致 OOM。
案例七:鉴权系统频繁长时间 Full GC
系统对外提供各种账号鉴权服务,使用时发现系统经常服务不可用,通过 Zabbix 的监控平台监控发现系统频繁发生长时间 Full GC,且触发时老年代的堆内存通常并没有占满,发现原来是业务代码中调用了 System.gc()。
案例八:内存泄露
堆内存泄漏问题:
现象:出现OOM或者Full GC,heap使用率明显上升,经常达到Xmx
Full GC出现的正常频率是大概一天一到两次
看内存飚高问题定位
堆外内存泄漏:
现象:heap使用率很低,但是出现了OOM或者Full GC
解决方案: 可以用btrace跟踪DirectByteBuffer的构造函数来定位
案例九: 因为对事务理解不足,造成死循环,导致CPU高
用户量不多,并发不多
使用可重复读事务级别
1 |
|
用户请求后,一直死循环,造成服务所在CPU高,造成mysql查询变慢。
CPU经常100% 问题定位
问题分析:CPU高一定是某个程序长期占用了CPU资源。
1、所以先需要找出那个进行占用CPU高。
1 |
|
2、然后根据找到对应进行里哪个线程占用CPU高。
1 |
|
3、找到对应线程ID后,再打印出对应线程的堆栈信息
1 |
|
4、最后根据线程的堆栈信息定位到具体业务方法,从代码逻辑中找到问题所在。
1 |
|
内存飚高问题定位
分析: 内存飚高如果是发生在java进程上,一般是因为创建了大量对象所导致,持续飚高说明垃圾回收跟不上对象创建的速度,或者内存泄露导致对象无法回收。
1、先观察垃圾回收的情况
1 |
|
如果每次GC次数频繁,而且每次回收的内存空间也正常,那说明是因为对象创建速度快导致内存一直占用很高;如果每次回收的内存非常少,那么很可能是因为内存泄露导致内存一直无法被回收。
2、导出堆内存文件快照
1 |
|
3、使用visualVM对dump文件进行离线分析,找到占用内存高的对象,再找到创建该对象的业务代码位置,从代码和业务场景中定位具体问题。