JVM调优:带你了解Java虚拟机调优工具、调优过程以及注意事项(有案例)

调优工具与操作

1.jps:jvm process status tool-java虚拟机进程状况工具

jps -l 是输出主类名 列出进程id

jps -m 输出JVM启动时传递给main()的参数

jps -v 显示虚拟机参数配置

      -Xms堆内存最小,-Xmx堆内存最大,-XX:MaxPerSize=256m,永久代大小最大为多少,-Xmn年轻代堆的大小,-Xss栈、线程栈的大小

2.jstat:虚拟机运行时的信息监控

jstat -class 监视类装载,卸载数量,总空间以及类装载所耗费的时间。

jstat -gc 监视类堆状况,包括Eden区,两个survivor区,老年代,永久代的空间和已用的空间。

jstat -gcutil 监视已使用空间占总空间的百分比。

这两个命令里面还包括了新生代GC的次数和所花的时间,以及Full GC的次数和所花的时间。

3.jmap:生成虚拟机的内存转储快照,获取dump文件

可以查询finalize执行队列,java堆和永久代的详细信息。空间的使用率,以及使用的哪种收集器。

jmap -histo pid:

展示java堆中对象的统计信息

instances(实例数)、bytes(大小)、classs name(类名)。它基本是按照使用使用大小逆序排列的。

jmap -dump:format=b,file=lijun pid 生成当前线程的dump文件

jmap -finalizerinfo

打印等待回收对象的信息

jmap -heap pid

查看堆的年轻代年老代的使用情况

jhat lijun 分析dump文件(现在一般不用这个了,因为分析dump文件非常消耗硬件资源,所以一般是复制到其他的机器来进行)现在一般都用visualVM来分析

4.jstack:用来显示虚拟机的线程快照

这里生成线程快照的原因主要是为了定位线程出现长时间停顿的原因,比如线程死锁,死循环,请求外部资源(数据库连接,网络资源,设备资源)导致的长时间等待

对于在java中最简单的死锁情况:
一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。

另外一个原因是默认锁的申请操作是阻塞的

要尽量避免在一个对象的同步方法里面调用其他对象的同步方法或者延时方法。

减小锁的范围,只获对需要的资源加锁,我们锁定了完整的对象资源,但是如果我们只需要其中一个字段,那么我们应该只锁定那个特定的字段而不是完整的对象。

如果两个线程使用 thread join 无限期互相等待也会造成死锁,我们可以设定等待的最大时间来避免这种情况。

死锁的调优:

jstack命令生成线程快照的原因主要是为了定位线程出现长时间停顿的原因,比如线程死锁,死循环,请求外部资源(数据库连接,网络资源,设备资源)导致的长时间等待

死锁的避免:

1.对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会 造成死锁。说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。

5.jinfo实时查看和调整虚拟机的各项参数

对于高性能硬件部署,假如我们-Xms=12g,仍然会出现每隔十几分钟就出现十几秒的停顿?
原因:就是我们在程序设计的时候,访问文档要将文档从磁盘提取到内存中,导致内存中出现很多由文档序列化产生的大对象,这些大对象都直接进入老年代,没有通过minor gc清除,即便有12g的堆,我们的内存也很快被消耗殆尽,最后进行一次full gc,导致等待的时间很长

因此要想减少网站停顿的时间,我们就需要控制full gc的频率,因此就不能有成批的长时间存活的大对象产生,这样才能保证老年代空间的稳定。

堆外内存导致的溢出错误?
即使Eden区,Survivor区,老年代,永久代都正常,但还是会不断抛出内存溢出异常。主要就是direct memory发生内存溢出异常,但是收集器一般只要在fullgc以后才会顺便处理directmemory。只能在catch里面调用System.gc(),这种情况一般要去代码中找IO操作。

外部命令导致的系统缓慢?
假如通过shell脚本获取系统的一些信息,java虚拟机在执行这些命令时一般会克隆一个和当前虚拟机拥有相同环境变量的进程,再用这个新的进程去执行外部命令,频繁执行这个操作,系统的消耗会很大。

服务器jvm进程崩溃?
http请求不能马上处理并返回,就会导致等待的线程和socket连接越来越多,最终会导致虚拟机进程崩溃。

不恰当的数据结构导致内存占用过大?
我们在新生代的垃圾收集器是采用的复制算法,如果在survivor区一直维持这些对象,就会导致gc暂停的时间非常长。可以通过设置参数去掉survivor区,但是仍然没有从根本上解决问题,根本还是在于HashMap存储数据文件效率太低。

Java虚拟机的调优

首先就是编译时间和类加载时间的优化 通过 -Xverify:none来去掉验证码验证过程。

一般我们通过jstat -gc查看java虚拟机中堆的空间使用情况,通过minor gc和full gc的次数来判断我们的堆空间的大小设置是否合理,使用jstat查看FGC发生的频率及FGC所花费的时间,FGC发生的频率越快、花费的时间越高,问题越严重;

频繁gc的原因一般有:

1.程序内调用了System.gc()或Runtime.gc().
2.一些中间件软件调用自己的GC方法,此时需要设置参数禁止这些GC.
3.Java的Heap太小,一般默认的Heap值都很小.
4.频繁实例化对象,Release对象.此时尽量保存并重用对象,例如使用StringBuffer()和String(),也就是尽量少用字符串常量String=“”。

在设置heap的大小的时候也不是越大越好,如果heap特别大,在进行full gc的时候所需要花费的时间也会非常的长。

也可以通过分析gc日志来分析full gc如何产生的。

也可以通过观察cpu资源使用情况来分析选择什么收集器。采用CMS收集器。

总结

1.如果程序内存不足或者频繁GC,很有可能存在内存泄露情况,这时候就要借助Java堆Dump查看对象的情况。
2.要制作堆Dump可以直接使用jvm自带的jmap命令
3.可以先使用jmap -heap命令查看堆的使用情况,看一下各个堆空间的占用情况,不推荐用这个,都用jstat -gc
4.使用jmap -histo:[live]查看堆内存中的对象的情况。如果有大量对象在持续被引用,并没有被释放掉,那就产生了内存泄露,就要结合代码,把不用的对象释放掉。
5.也可以使用 jmap -dump:format=b,file=命令将堆信息保存到一个文件中,再借助jhat命令查看详细内容

一些java应用调优案例

https://tech.meituan.com/2017/12/29/jvm-optimize.html

参考:

https://blog.csdn.net/qq_42914528/article/details/81541760

https://blog.csdn.net/savilors/article/details/81907469

初始调优选择

年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

并发垃圾收集信息

持久代并发收集次数

传统GC信息

花在年轻代和年老代回收上的时间比例

减少年轻代和年老代花费的时间,一般会提高应用的效率

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

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

-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。

-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章