jvm调优与能 JVM性能调优
一、jvm的理解
我们在编写程序时,经常会遇到OOM(out of Memory)以及内存泄漏等问题。为了避免出现这些问题,我们首先必须对JVM的内存划分有个具体的认识。JVM将内存主要划分为:方法区、虚拟机栈、本地方法栈、堆、程序计数器。JVM运行时数据区如下:
程序计数器是线程私有的区域,很好理解嘛~,每个线程当然得有个计数器记录当前执行到那个指令。占用的内存空间小,可以把它看成是当前线程所执行的字节码的行号指示器。如果线程在执行Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的是Native方法,这个计数器的值为空(Undefined)。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
与程序计数器一样,Java虚拟机栈也是线程私有的,其生命周期与线程相同。
本质上来讲,就是个栈。里面存放的元素叫栈帧,栈帧好像很复杂的样子,其实它很简单!它里面存放的是一个函数的上下文,具体存放的是执行的函数的一些数据。执行的函数需要的数据无非就是局部变量表(保存函数内部的变量)、*作数栈(执行引擎计算时需要),方法出口等等。
执行引擎每调用一个函数时,就为这个函数创建一个栈帧,并加入虚拟机栈。换个角度理解,每个函数从调用到执行结束,其实是对应一个栈帧的入栈和出栈。
注意这个区域可能出现的两种异常:
一种是StackOverflowError,当前线程请求的栈深度大于虚拟机所允许的深度时,会抛出这个异常。制造这种异常很简单:将一个函数反复递归自己,终会出现栈溢出错误(StackOverflowError)。
另一种异常是OutOfMemoryError异常,当虚拟机栈可以动态扩展时(当前大部分虚拟机都可以),如果无法申请足够多的内存就会抛出OutOfMemoryError,如何制作虚拟机栈OOM呢,参考一下代码:
这段代码有风险,可能会导致*作系统假死,请谨慎使用~~~
本地方法栈与虚拟机所发挥的作用很相似,他们的区别在于虚拟机栈为执行Java代码方法服务,而本地方法栈是为Native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。
Java堆可以说是虚拟机中大一块内存了。它是所有线程所共享的内存区域,几乎所有的实例对象都是在这块区域中存放。当然,随着JIT编译器的发展,所有对象在堆上分配渐渐变得不那么“绝对”了。
Java堆是垃圾收集器管理的主要区域。由于现在的收集器基本上采用的都是分代收集算法,所有Java堆可以细分为:新生代和老年代。在细致分就是把新生代分为:Eden空间、From Survivor空间、To Survivor空间。当堆无法再扩展时,会抛出OutOfMemoryError异常。
方法区存放的是类信息、常量、静态变量等。方法区是各个线程共享区域,很容易理解,我们在写Java代码时,每个线程度可以访问同一个类的静态变量对象。由于使用反射机制的原因,虚拟机很难推测那个类信息不再使用,因此这块区域的回收很难。另外,对这块区域主要是针对常量池回收,值得注意的是JDK1.7已经把常量池转移到堆里面了。同样,当方法区无法满足内存分配需求时,会抛出OutOfMemoryError。
制造方法区内存溢出,注意,必须在JDK1.6及之前版本才会导致方法区溢出,原因后面解释,执行之前,可以把虚拟机的参数-XXpermSize和-XX:MaxPermSize限制方法区大小。
运行后会抛出java.lang.OutOfMemoryError:PermGen space异常。
解释一下,String的intern()函数作用是如果当前的字符串在常量池中不存在,则放入到常量池中。上面的代码不断将字符串添加到常量池,终肯定会导致内存不足,抛出方法区的OOM。
下面解释一下,为什么必须将上面的代码在JDK1.6之前运行。我们前面提到,JDK1.7后,把常量池放入到堆空间中,这导致intern()函数的功能不同,具体怎么个不同法,且看看下面代码:
这段代码在JDK1.6和JDK1.7运行的结果不同。
JDK1.6结果是:false,false,JDK1.7结果是true, false。
原因是:JDK1.6中,intern()方**吧首次遇到的字符串实例**到常量池中,返回的也是常量池中的字符串的引用,而StringBuilder创建的字符串实例是在堆上面,所以必然不是同一个引用,返回false。
在JDK1.7中,intern不再**实例,常量池中只保存首次出现的实例的引用,因此intern()返回的引用和由StringBuilder创建的字符串实例是同一个。为什么对str2比较返回的是false呢?这是因为,JVM中内部在加载类的时候,就已经有"java"这个字符串,不符合“首次出现”的原则,因此返回false。
二、jvm调优如何做
1、如果没有必要,请不要做调优。没有万能的调优,只有根据使用场景选择合适的手段,初始默认指定堆大小,元空间大小(jdk8)即可
2、确认性能问题由JVM再考虑调优,如fullGC频繁,GC时间较长,内存使用不正常,OOM等。开启JVM监控,记录GC日志,分析GC情况
3、JVM调优的目标是减少/避免老年代GC
4、对于追求响应时间的如web系统使用并发垃圾回收器(jdk8开启G1,低版本使用CMS)
5、根据JVM内存使用情况,可以考虑手动设置年轻代大小,survivor区大小,减少/避免垃圾进入老年代(注意jdk8默认开启自适应调节,需关闭)
6、影响GC时间的还有GC线程数等等,需要结合GC日志分析GC过程可能存在的问题
三、JVM性能调优***G1
1、本篇是对Java**G1收集器调优的精简版。针对G1垃圾的收集阶段可能出现的问题,非合理内存分配,大对象占用,Full GC等问题作出解决方式和*作参数。
2、 G1是一个吞吐量和时间延迟之间相互平衡的收集器。目标是高吞吐量下提供相对较小、统一的暂停。
3、所以如果是交互性强的应用程序,使用G1时需要基于时延优先进行考虑。
4、虚拟机从*作系统内存中分配或归还内存可能会导致不必要的延迟。通过使用选项-Xms和-Xmx将小和大堆大小设置为相同的值,并使用- XX:+AlwaysPreTouch预触摸所有内存,以将这项工作移到VM启动阶段,从而避免延迟。
5、并行处理 Reference对象,ParallelRefProcEnabled默认值false,若 GC log里出现 Reference处理时间较长的日志,可以开启此参数- XX:+ParalleRefProcEnabled。开启后会使用jvm可用的线程数进行处理,但**上提到的-XX:ReferencesPerThread参数在jdk17的版本中没有找到,猜测可能是jvm内部控制不再作可调试的参数。
6、年轻代收集所花费的时间大致与年轻代的大小成正比。**给出的两个参数- XX:G1NewSizePercent,-XX: G1MaxNewSizePercent在jdk17中没有找到,在没有固定年轻代大小时,G1会进行动态调整,所以这个调优的参考性不大,可以忽略。
7、 RS是一个抽象的数据结构,具体的实现由table card完成。一般会把记忆集和卡表放在一起讨论。简单来讲就是所有对象引用关系的一个**,GCRoot时扫描的不是去实际的内存区域,否则跨代引用时从新生代到老年代会是一个漫长的过程。RS很好的解决了跨代引用的问题。由于RS会动态更新,垃圾收集必须先等RS更新完毕后才去执行。所以RS更新如果耗时过长则会影响回收时间。
8、扫描RS时间也由G1为保持低存储容量而执行的压缩量决定。记忆的**存储在内存中越紧凑,在垃圾收集期间检索存储的值所花费的时间就越多。G1自动执行这种压缩,称为记忆集粗化,同时根据该区域记忆集的当前大小更新记忆集。特别是在高压缩级别时,检索实际数据可能非常慢。
9、使用选项- XX:G1SummarizeRSetStatsPeriod结合gc+remset=trace级别日志显示是否发生粗化。