性能测试JVM问题分析和优化

一,JVM内部结构

JVM内存结构分为:方法区(method),栈内存(stack),堆内存(heap),本地方法栈(java中的jni调用)、程序计数器

JVM中有堆和非堆(我们有的时候称为栈)两个区域:

堆空间(heap)说明:

说明:堆里面主要放对象,变量。非堆中主要放类,方法,线程栈----主要就是方法和栈

年轻代的说明:

1,堆内存中有年轻代(新生代)和老年代(老生代),比例为3/8和5/8(占用比例是可以配置的)

2,年轻代的主要作用就是用来存放java程序新new出来的对象

3,年轻代中又分为2块:年轻代区(年轻代区又称为伊甸园区)和存活区,其中存活区又分s0和s1。伊甸园区占年轻代的8/10,存活区中s0和s1各占1/10。s0和s1都是一样大的

4,年轻代中YoungGC的作用:

  当java执行的时候先new对象,new的对象都放到伊甸园区,当伊甸园区满了以后会触发一个youngGC,因为new的对象都是被引用的,youngGC会判断如果出现没有被引用的对象,就会给回收---称为youngGC的垃圾回收机制(youngGC垃圾回收的时候先进行标记,如没引用的标记为0,有引用的标记为1,第二步就是清扫回收),如果有被引用的对象则移至到存活区中的S0中。

5,存活区有个原则就是大小相等,位置互换,总有一个是空的(保障每一次youngGC的时候能能的不断往存放区放对象,程序不会间断)。当youngGC发生的时候,第一次youngGC的时候只判断伊甸园区,第二次youngGC的时候连s0都要判断,如果s0中有没被引用的对象,就进行标记和清扫,如果有引用的对象,就移至s1中。

6,youngGC触发的2个原则:

1,伊甸园区满了会触发

2,如果配置了fullGC,那么在fullGC之前要先进行一次youngGC

老年代的说明:

伊甸园区和存活区为了不断的存放new对象,那么老年代存放的就有几个原则:

1,age>15的对象进入老年代,简要说明一下什么是age>15:如一个新的对象在第一次遇到youngGC的时候会给他的"age"属性+1,放到s0中,当第二次youngGC的时候,这个对象在s0依然被引用,“age”会再次+1,并且移到s1中,以后每次youngGC的时候,只要该对象被引用都会给"age"+1,直到“age”大于15的时候会被移至到老年代中。当然这个阈值是可以通过参数设置的:XX:MaxTenuringThreshold=15

2,大对象不会进入伊甸园区,而是直接进入老年代,什么是大对象什么是小对象呢?有个大小默认值,超过这个默认值直接进入老年代,这是这个值的配置,-XX:PretenureSizeThreshold,默认值是0,当他是0的时候,表示任何对象都要进入新生代,除非该对象大于Eden的总大小,直接进入到老年代。当这个值被设置后,大于这个值的就直接进入老年代

说明:age的数据和大对象的阈值都可以配置

3,动态分配原则:在存活区有出现相同"age"的对象,大小总和大于或者等于存活区大小的一半时,大于等于该age的对象,全部进入老年代。如:存活区的大小为1M,s0和s1各占一半即为0.5M,现在有a对象和b对象,这两个的age是相同的,大小加起来超过了1M的一半或者等于1M,那么这两个都放到老年代中

4,空间担保原则:老年代中有大对象,age>15的对象,还有一些age小于15的(通过动态原则分配进来的),当再次youngGC的时候,把对象往老年代放的时候,放不下了,这个时候会触发fullGC,对整个堆内存清扫,把失去引用的先标记再清扫,清扫后会把年轻代中存活的对象,直接移入到老年代,相当于把伊甸园区和存活区空置出来,fullGC也会对非堆进行清扫,但是很少,后面基本不用fullGC清扫非堆了(因为比较慢)。如果老年代放不下,就意味着内存溢出了

fullGC触发的原则:

1,老年代或者非堆空间满了

2,空间担保原则

3,代码中写了调用system.gc---一般都是调用fullGC

4,jmap,dump这些工具对堆内清空的时候也会触发

5,监控工具调用fullGC

fullGC要比youngGC慢很多,所以尽量少用

二,非堆

java栈:

栈(Java Virtual Machine Stacks)和线程是紧密联系的,每创建一个线程就会对应创建一个Java栈,所以Java栈也是“线程私有"的内存区域,栈中又对应包含多个 栈帧,每调用一个方法就会往栈中创建并压入一个栈帧,栈帧是用来存储方法数据和部分过程结果的数据结构,每一个方法从调用到最终返回结果的过程,就对应 一个栈帧从入栈到出栈的过程

线程栈就是执行的方法,我们通过工具看到线程执行的状态,其实就是线程栈的快照,他只能通过线程快照的方式来呈现给我们

本地方法栈:

用于支持native方法的执行,存储了每个native方法调用的状态

方法区:

它保存方法代码(编译后的java代码)和符号表。存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用持久代 (Permanet Generation)来存放方法区

程序计数器:

是一个比较小的内存区域,可能是CPU寄存器或者操作系统内存,其主要用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器

三,控制参数:-->堆是给开发人员用的,非堆是给JVM自己用的

-Xmx设置堆的最大空间大小:最大堆内存分配 默认物理内存1/4,当空余堆内存大于70%时,会减小到-Xms的最小限制

-Xms设置堆的最小空间大小:设置JVM最小内存,初始堆内存 默认物理内存1/64,也是最小分配堆内存。当空余堆内存小于40%时,会增加到-Xms的最大限制

此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存

-Xmn设置年轻代内存大小,此值一般建议设置成整个堆的3/8

-XX:NewSize设置新生代最小空间大小:应该小于-Xms的值

-XX:MaxNewSize设置新生代最大空间大小:小于-Xmx的值

-XX:PermSize设置永久代最小空间大小:非堆内存的初始值,默认物理内存的1/64 ,也是最小非堆内存

XX:MaxPermSize设置永久代最大空间大小:非堆内存最大值,默认物理内存的1/4

-Xss设置每个线程的堆栈大小:原则来说这个值越小,创建的线程数越多,但是如果过于小会导致线程无法创建,一般都是默认取1M

没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制,老年代空间大小=堆空间大小-年轻代大空间大小

-MetaspaceSize=256m 初始元数据空间256MB

-XX:MaxMetaspaceSize=512m 最大元数据空间512MB

--Xss1m 启动每个线程分配的空间

-XX:NewRatio=4 :设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4

-XX:SurvivorRatio=4 :设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

-XX:+UseParallelGC :选择垃圾收集器为并行收集器。 此配置仅对年轻代有效

-XX:ParallelGCThreads=20 :配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等

-XX:+UseParallelOldGC :配置年老代垃圾收集方式为并行收集

 

 

典型设置:

  java -Xmx3550m -Xms3550m -Xmn2g -Xss128k   

  java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

    java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20

  java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC

16G内存JDK1.8生产服务器完整配置:

  JAVA_OPTS="-server -Xmx4g -Xms4g -Xmn256m -Xss256k -XX:+DisableExplicitGC  -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled  -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -Duser.timezone=GMT+8"

参数说明:

-server//服务器模式 

-Xmx4g //JVM最大允许分配的堆内存,按需分配

-Xms4g //JVM初始分配的堆内存,一般和Xmx配置成一样以避免每次gc后JVM重新分配内存。 

-Xmn256m //年轻代内存大小,整个JVM内存=年轻代 + 年老代 + 持久代 

-Xss512k //设置每个线程的堆栈大小

-XX:+DisableExplicitGC //忽略手动调用GC, System.gc()的调用就会变成一个空调用,完全不触发GC

-XX:+UseConcMarkSweepGC //并发标记清除(CMS)收集器

-XX:+CMSParallelRemarkEnabled //降低标记停顿

-XX:+UseCMSInitiatingOccupancyOnly //使用手动定义初始化定义开始CMS收集

-XX:CMSInitiatingOccupancyFraction=70 //使用cms作为垃圾回收使用70%后开始CMS收集

-Duser.timezone=GMT+8 //设定GMT区域,避免CentOS坑爹的时区设置

四,有内存溢出问题定位

1,jmap命令---参数说明:

pid: 需要打印配置信息的进程ID

executable: 产生核心dump的Java可执行文件

core: 需要打印配置信息的核心文件

server-id 可选的唯一id,如果相同的远程主机上运行了多台调试服务器,用此选项参数标识服务器

remote server IP or hostname 远程调试服务器的IP地址或主机名

heap: 显示Java堆详细信息

histo[:live]: 显示堆中对象的统计信息

clstats:打印类加载器信息

finalizerinfo: 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象

dump:<dump-options>:生成堆转储快照

F: 当-dump没有响应时,使用-dump或者-histo参数. 在这个模式下,live子参数无效

2,执行命令:

jmap -heap 3829(我的java项目的进程号):

显示Java堆详细信息

 

jmap -histo:live 15849 >jmap_histo.txt 显示堆中存活对象的统计信息

 

 

 

 

 jamp -dump :live,format=b,file=heap.bin 进程号,这样会生成一个bin文件  然后用mat打开分析

jstat命令:jstat的主要作用就是对Java应用程序的资源和性能进行实时监控的命令行工具,主要包括GC情况和Heap Size资源使用情况

jstat -gcutil 15849 1000 5   1000表示间隔1s收集1次,5表示收集5次

 

 So、S1 两个存活区使用占比

E 伊甸园区使用占比

 

 

O老年代使用占比

M元数据区使用比例

CSS:压缩使用比例

YGC、FGC 分别为年轻代和老年代的GC次数,正常情况下 年轻代GC次数大

YGCT、FGCT、GCT 分别为年轻代GC总耗时、老年代GC总耗时、G总耗时

fullGC执行了的次数越多,同时老年代内存没减少,说明无法回收,内存不释放,有溢出 

jstack 定位CPU和内存高的方法:

jstack是java虚拟机自带的一种堆栈跟踪工具,主要分为两个功能:

  a,针对活着的进程做本地的或远程的线程dump

  b,针对core文件做线程dump

执行命令:jstack 15849(PID)

 

在这里我们可以看到每个线程的内容,那么如何定位哪个线程有问题呢?我们看一下nid

nid--->通过这个可以定位到哪个使用内存高.我们看到的是16进制。我们再通过top -H -p PID,可以看到该程序各方法的占用内存情况,通过这个方法看到哪个java进程占用的内存比较高,找到他对应的PID,然后拿着这个PID转成16进制去找nid的值,就可以具体定位到哪个方法占用内存高了。如何转16进制呢?printf %x PID

当我们看到内存或者cpu占用特别高的时候,就可以把对应的PID找出来,转成16进制,然后通过这里jstack PID > D:test\xxx.log 定位,可以看到该线程在做什么操作导致内存比较高

当我们响应时间比较长的时候,也可以通过这个方法去定位,看哪些线程在等待.....

top -H -p 15849(java程序的进程)

 

 我们可以看到这些线程占用内存40%,比较大,然后我们把这些线程号转成16进制

 

 我们执行这个命令jstack 15849 >jstack_test.txt把线程活动情况导出到单独文件中

打开该文件,用3e20去搜索,我们能看到该线程的状态

 

 我们再来看看其他消耗内存的线程

 

 这个时候发现这个这个线程是在执行GC,持续消耗内存,说明GC回收不了,但是又满足触发GC条件,表示内存被占满,有溢出

五,线程栈的状态

一个线程刚开始创建的时候是new的状态,我们看不到因为太快了,创建好了准备就绪的时候是runnable状态,开始运行就是running状态,在运行过程中如果遇到同步块阻塞或者IO阻塞就blocked(死锁),什么是同步阻塞呢?就是在运行的时候可能要用到其他线程,但是该线程被其他程序占用。IO阻塞就是磁盘也没有空闲,被别人占着就成死锁状态。当别人把资源释放以后就又回到runnable状态,如果在等待某个资源的时候就进入waiting状态,直到拿到资源才进入runnable,timed_waiting是有时间限制的,如等10s如果拿不到资源,我就不等了。注意只有runnable状态才能进入running,最后terminated是线程销毁,运行结束或者异常中断就产生这个状态

 

 说明:

NEW,未启动的。不会出现在Dump中

RUNNABLE,在虚拟机内执行的。运行中状态,可能里面还能看到locked字样,表明它获得了某把锁

BLOCKED,受阻塞并等待监视器锁。被某个锁(synchronizers)給block住了

WATING,无限期等待另一个线程执行特定操作。等待某个condition或monitor发生,一般停留在park(), wait(), sleep(),join() 等语句里。

TIMED_WATING,有时限的等待另一个线程的特定操作。和WAITING的区别是wait() 等语句加上了时间限制 wait(timeout)

TERMINATED,已退出的

状态中NEW状态是开始,TERMINATED是销毁,在整个线程对象的运行过程中,这个两个状态只能出现一次。其他任何状态都可以出现多次,彼此之间可以相互转换,处于timed_waiting/waiting状态的线程一定不消耗cpu,处于runnable状态的线程不一定会消耗cpu,要结合当前线程代码的性质判断,是否消耗cpu。如果是纯java运算代码,则消耗cpu。如果线程处于网络io,很少消耗cpu。如果是本地代码,通过查看代码,可以通过pstack获取本地的线程堆栈,如果是纯运算代码,则消耗cpu,如果被挂起,则不消耗,如果是io,则不怎么消耗cpu

runnable:不一定消耗CPU

running:一定消耗Cpu

block,waiting,timed_waiting不消耗CPU

五,垃圾回收算法:

1,标记清除算法

  分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象

  缺点:大的放不下,小的放下有多余空间,因为内存不是连续的,会产生内存碎片:原因就是大的放不下,小的有富余。标记和清除两个过程的效率都不高

  

2,复制算法

  总有一边是空的,不引用的清扫,引用的复制过来----youngGC存活区用的就是这个算法

  

 

   特点:

    每次只需要对整个半区进行内存回收

    内存分配时也不需要考虑内存碎片

    内存缩小为了原来的一半,这样代价太高了

    存活取使用此算法

3,标记整理算法

  先标记上,有用的没用的都标记上,把不用的清扫掉,有用的整理成连续的

  

 

   特点:

    不产生碎片

    内存使用率高

    老年代使用此算法

4,分代算法

    现代商用虚拟机基本都采用分代收集算法来进行垃圾回收。这种算法没什么特别的,无非是上面内容的结合罢了,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法

    

 

六,垃圾收集器:

    垃圾收集器垃圾回收算法的具体实现,没有最好的垃圾收集器,更加没有万能的收集器,只能选择对具体应用最合适的收集器 我们使用的是HotSpot,HotSpot这个虚拟机所包含的所有收集器如图

    

    如果两个收集器之间存在连线,那说明它们可以搭配使用

1,Serial收集器---基本不用 

采用复制算法的单线程的收集器,单线程一方面意味着它只会使用一个CPU或一条线程去完成垃圾收集工作,另一方面也意味着它进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束为止,意味着,在用户不可见的情况下要把用户正常工作的线程全部停掉,这对很多应用是难以接受的。不过实际上到目前为止,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器,因为它简单而高效;分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代停顿时间在几十毫秒最多一百毫秒,只要不是频繁发生,这点停顿是完全可以接受的

  

2,PARNew收集算法---年轻代用这个

其实是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集外,其余行为和Serial收集器完全一样,包括使用的也是复制算法;但是它却是Server模式下的虚拟机首选的新生代收集器,其中有一个很重要的和性能无关的原因是,除了Serial收集器外,只有它能与CMS收集器配合工作; ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,但是多CPU的环境中,效率高;可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数

3,parallel 收集器-----JDK1.6后出现的算法

使用多线程和“标记-整理”算法。这个收集器在JDK 1.6之后的出现; “吞吐量优先收集器”终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器+Parallel Old收集器的组合;  Parallel Scavenge收集器也是一个新生代收集器,也是用复制算法的收集器,也是并行的多线程收集器

4,CMS收集器---jdk1.8后常用的算法--老年代用这个

CMS(Conrrurent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器。使用标记 - 清除算法,收集过程分为如下四步: (1). 初始标记,标记GCRoots能直接关联到的对象,时间很短。 (2). 并发标记,进行GCRoots Tracing(可达性分析)过程,时间很长。 (3). 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。 (4). 并发清除,回收内存空间,时间很长。

5,G1收集器--目前最前沿的技术 

  G1是目前技术发展的最前沿成果之一,它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1收集器有以下特点: (1). 并行和并发。使用多个CPU来缩短停顿时间,与用户线程并发执行 (2). 分代收集。独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果 (3). 空间整合。基于标记 - 整理算法,无内存碎片产生 (4). 可预测的停顿。能简历可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。      在G1之前的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它 将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部 分(可以不连续)Region的集合

GC优化:基本上都是java程序引起的,不是参数引起的

1,首先要明确的是大部分GC问题是java应用引起的,并且基本不是参数设置错误引起的

2,其次在上线前将jvm参数设置到最优,减少创建对象的数量,减少使用全局变量和大对象

3,在实际项目中GC优化是最后不得已采取的手段,分析GC情况优化代码要比调整GC参数更常用

GC优化的两个方法:

一是将转移到老年代的对象数量降低到最小

二是减少fullGC的执行时间

满足以上两种优化方法,具体需要优化事项如下:

减少使用全局变量和大对象

调整新生代大小到最合适

设置老年代大小为最合适

选择合适的GC收集器

-------------------------------------其他的性能监控和分析我们用GUI工具来完成--------------------------------

posted @ 2020-05-02 19:17  老僧观天下  阅读(798)  评论(0编辑  收藏  举报