源码时代Java干货分享|Jvm优化指南

Posted IT-source

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码时代Java干货分享|Jvm优化指南相关的知识,希望对你有一定的参考价值。

Jvm简介

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

Java Hotspot vm参数配置

-client -server -classpath / -cp
-verbose :class 输出jvm载入类的相关信息,当jvm报告说找不到类或者类冲突时可此进行诊断。
-verbose:gc 输出每次GC的相关情况
-verbose:jni 输出native方法调用的相关情况,一般用于诊断jni调用错误信息。

jvm参数

Heapsize 堆栈大小
-Xms 定义最小值 -Xmx 定义最大值 建议设置相同,防止垃圾收集器在最小、最大之间收缩堆而产生额外开销。
GC垃圾回收--新生代New配置
-XX:newSize
-XX:MaxNewSize
建议设置相同,防止年轻代的堆收缩。
GC垃圾回收—旧生代Old配置 -XX:NewRadio=2表示新生代:老年代 = 1:2

Jit编译器:c1/c2

在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器和C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:

 

C1编译器会对字节码进行简单和可靠的优化,以达到更快的编译速度;而C2编译器会启动一些编译耗时更长的优化,以获取更好的编译质量。

垃圾回收


Java的堆内存模型

新生代(Young generation)

绝大多数被新建的对象会分配到这里,由于大部分对象在创建后会很快变得不可达,所以很多对象被创建在新生代,然后消失,对象消失的过程称为”minor GC”
新生代存在1个eden区和2个survivor区,新对象会首先分配到eden区,当然如果对象过大会直接分配到老年代,在gc中,eden中的对象会被移动到survivor中,直到对象满足一定年纪(熬过gc的次数),会被移动到老年代
可以设置新生代和老年代的相对大小,这种方式的优点是新生代的大小随着堆内存的动态扩展,设置方式为-XX:NewRatio,例如-XX:NewRatio=8代表老年代/新生代为8/1,老年代占堆内存的7/8,新生代占1/8

老年代(Old generation)

对象没有变的不可达,在新生代中存活下来的对象会被拷贝到这里,其占用的空间比新生代多,因为其占用空间大,所以发生在老年代上的gc比新生代少,对象从老年代消失的过程称之为”major GC”或者”full GC”

永久代(permanent generation)

像一些类级的层次信息,方法数据和方法信息,运行时常量(jdk1.7之后移除永久代),已确定的符号引用和虚方法表等等,它几乎是静态的很少被回收,在jdk8之前的hotspot虚拟机中,类的这些”永久的”数据存放在一个叫做永久代的区域,在jvm启动时可以设置-XX:MaxPermsize的值来控制永久代的大小,但是jdk1.8之后取消了永久代,这些元素被移到了与堆不相连的本地内存区域

Gc参数

hotspot实现垃圾回收细节

一致性

一致性要求gc进行时必须停顿所有java执行线程

安全点

程序只有达到安全点才能暂停,安全点的标准就是是否让程序长时间执行的特征,比如方法调用和循环跳转这种,具有这些才会产生安全点

程序暂停的2种方式

抢先式中断:在gc发生时,主动中断所有线程
主动式中断:设一个标志,各线程去轮询这个标志,遇到中断则暂停,轮询地方与安全点重合

垃圾收集器

Serial收集器

Serial收集器是最基本,发展历史最悠久的收集器,曾经(在jdk1.3.1之前)是虚拟机新生代收集的唯一选择
这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。
它的优势在于简单而高效,对于限定单个CPU来说,由于serial收集器没有线程交互的开销,专心做垃圾收集自然能获得最高的单线程收集效率

 

ParNew 收集器

ParNew收集器其实就是serial收集器的多线程版本,除了使用多线程执行垃圾回收外,其余行为跟serial收集器一模一样,例如在回收算法和回收策略,分配规则都是一样的,在实现上这2种收集器共用了很多代码
Parnew的应用场景:是运行在server模式下的虚拟机的首选新生代垃圾回收器,因为它是除serial收集器外唯一能与cms收集器配合工作

 

Serial收集器与parnew收集器比较

Parnew收集器在单cpu环境下绝对不会比serial收集器有更好的效果,因为它存在线程的交互开销,但是随着可以使用的CPU数量增加,它对于gc时系统资源的有效利用还是很好的

Parallel Scavenge 收集器

是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。应用场景主要是针对那些需要频繁与用户交互的程序,良好的响应速度能提升用户体验

Serial Old 收集器

Serial收集器的老年代版本,单线程,使用 标记 —整理算法。
应用场景:
Client模式:serial old收集器主要意义在于给client模式下的虚拟机使用
Server模式:在server模式下有2大用途:一种用途是在jdk1.5之前与Parallel Scavenge 收集器配合使用,另一种用途是作为cms收集器的备用预案,在并发收集失败时使用
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记 — 整理算法

 

应用场景:在注重吞吐量以及CPU资源敏感的场合,可以优先考虑Parallel Old 和 Parallel Scavenge 收集器
这个收集器是在jdk1.6中才提供的

CMS 收集器

CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —清除算法实现。

运作步骤:

初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象,需要”stop the world”
并发标记(CMS concurrent mark):进行 GC Roots Tracing
重新标记(CMS remark):修正并发标记期间的变动部分,仍然需要”stop the world”
并发清除(CMS concurrent sweep):会清除对象

 


缺点:

1,cms收集器对CPU资源比较敏感,cms默认启动的回收线程数是(CPU数量+3)/4,也就是CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并随着CPU数量的增加而下降,但是,如果CPU不足4个时,cms对用户程序的影响变得很大
2,cms收集器无法处理浮动垃圾,可能出现”concurrent mode failure”失败而导致另一次full gc产生
3,cms收集器会产生大量空间碎片,因为它是基于标记-清除算法的收集器,故在收集结束时会产生大量空间碎片,空间碎片过多会给大对象分配带来很大麻烦,比如老年代还有很大空间剩余,但是无法找到更大的连续空间来分配对象从而不得不提前触发一次full gc

G1 收集器

G1(garbage first)是一款面向服务端应用的垃圾收集器,Hotspot开发团队是希望用它替换掉jdk1.5中发布的cms收集器
G1的优点:并行与并发、分代收集、空间整合、可预测停顿。
运作步骤:

初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(Final Marking)
筛选回收(Live Data Counting and Evacuation)

 


 

实例分析

实例1

java.lang.OutOfMemoryError: GC overhead limit exceeded
通过Linux查看进程的命令ps -ef|grep java发现
-xms768m -xmx768m -xx:NewSize=320m, -xx:MaxNewSize=320m
通过分析应用堆区内存设置只有768m,机器内存有2g,机器上只跑了一个java应用,没有其他需要占内存的地方,另外,这个应用比较大,需要占得内存比较比较多所以修改配置如下
-xms1280m -xmx1280m -xx:NewSize=500m, -xx:MaxNewSize=500m
跟踪运行情况发现,异常没有再出现,问题解决

实例2

一个服务系统,经常出现卡顿,分析原因,发现full gc时间太长
Jstat -gcutil:
S0 S1 E O P YGC YGCT FGC FGCT GCT
12.16 0.00 5.18 63.78 20.32 54 2.047 5 6.946 8.993
分析上面的数据,发现YGC执行54次,耗时2.047秒,每次YGC耗时37ms,在正常范围,而FGC执行了5次,耗时6.946秒,每次耗时1.389秒数据显示问题
Full gc耗时较长,分析该系统发现-XX:NewRadio=9,也就是新生代和老年代的比例是1:9
这会造成:
1,新生代内存太小,导致对象提前进入老年代,从而触发full gc
2,老年代较大,进行full gc耗时较长
解决方法是将-XX:NewRadio=4,意思是将对象控制在新生代就清理掉,没有进入老年代(这种做法针对一些应用有效,并不是所有应用都有效)

总结

1,多数的Java应用不需要在服务器上进行GC优化;
2,多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;
3,在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合)
4,减少创建对象的数量
5,减少使用全局变量和大对象;
6,GC优化是到最后不得已才采用的手段
7,在实际使用中,分析GC情况优化代码比优化GC参数要多得多
GC优化的目的有2个
1,将转移到老年代的对象数量降低到最少
2,减少full gc的执行时间
为达目的可以这样做:
1,减少使用全局变量和大对象
2,调整新生代的大小到最合适
3,设置老年代的大小为最合适
4,选择合适的GC收集器

以上是关于源码时代Java干货分享|Jvm优化指南的主要内容,如果未能解决你的问题,请参考以下文章

JVM故障问题排查心得「内存优化技术」Java虚拟机内存优化实战案例分析指南

源码时代前端干货分享| JavaScript编程优化,超强入门必备技能!

源码时代软件测试干货分享|什么是探索式测试?

源码时代Java干货分享|SpringSecurity快速上手秘籍

源码时代Java干货分享|带你了解原型模式_prototype

源码时代Java干货分享|带你认识Zookeeper 分布式服务框架