一 jvm 内存分析
作为一个 Java 程序员,如果不了解 jvm 内存模型和内存分配,就不能称之为一个真正的程序员。
要想知道 jvm 内存是怎么分配的,首先需要知道 java 程序是怎么运行的,只有这样才能结合 java 程序运行的各个阶段掌握 jvm 内存的分配。
如上图所示,首先 Java 源代码文件(.java 后缀)会被 Java 编译器编译为字节码文件(.class 后缀),然后由 JVM 中的类加载器加载各个类的字节码文件,加载完毕之后,交由 JVM 执行引擎执行。
在整个程序执行过程中,JVM 会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为 Runtime Data Area(运行时数据区),也就是我们常说的 JVM 内存。因此,在 Java 中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。
1.1 运行时数据区
根据《Java 虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java 栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。
如上图所示,JVM 中的运行时数据区应该包括这些部分。在 JVM 规范中虽然规定了程序在执行期间运行时数据区应该包括这几部分,但是至于具体如何实现并没有做出规定,不同的虚拟机厂商可以有不同的实现方式
1.1.1 程序计数器
程序计数器(Program Counter Register),也有称作为 PC 寄存器。想必学过汇编语言的朋友对程序计数器这个概念并不陌生,在汇编语言中,程序计数器是指 CPU 中的寄存器,它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当 CPU 需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加 1 或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。
虽然 JVM 中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的 CPU 寄存器,但是 JVM 中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示 执行哪条指令的。
由于在 JVM 中,多线程是通过线程轮流切换来获得 CPU 执行时间的,因此,在任一具体时刻,一个 CPU 的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。
在 JVM 规范中规定,如果线程执行的是非 native 方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是 native 方法,则程序计数器中的值是 undefined。
由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
1.1.2 Java 栈
Java 栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈,跟 C 语言的数据段中的栈类似。事实上,Java 栈是 Java 方法执行的内存模型。为什么这么说呢?下面就来解释一下其中的原因。
栈帧
Java 栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于 Java 栈的顶部。讲到这里,大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了以及为什么栈区的空间不用程序员去管理了(当然在 Java 中,程序员基本不用关系到内存分配和释放的事情,因为 Java 有自己的垃圾回收机制),这部分空间的分配和释放都是由系统自动实施的。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。下图表示了一个 Java 栈的模型:
局部变量表
局部变量表,顾名思义,想必不用解释大家应该明白它的作用了吧。就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
操作数栈
操作数栈,想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
指向运行时常量池的引用
指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
方法返回地址
方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的 Java 栈,互不干扰。
1.1.3 本地方法栈
本地方法栈与 Java 栈的作用和原理非常相似。区别只不过是 Java 栈是为执行 Java 方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在 JVM 规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在 HotSopt 虚拟机中直接就把本地方法栈和 Java 栈合二为一。
1.1.4 堆
在 C 语言中,堆这部分空间是唯一一个程序员可以管理的内存区域。程序员可以通过 malloc 函数和 free 函数在堆上申请和释放空间。那么在 Java 中是怎么样的呢?
Java 中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在 Java 栈中的)。只不过和 C 语言中的不同,在 Java 中,程序员基本不用去关心空间释放的问题,Java 的垃圾回收机制会自动进行处理。因此这部分空间也是 Java 垃圾收集器管理的主要区域。另外,堆是被所有线程共享的,在 JVM 中只有一个堆。
1.1.5 方法区
方法区在 JVM 中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。
在 Class 文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到 JVM 后,对应的运行时常量池就被创建出来。当然并非 Class 文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如 String 的 intern 方法。
在 JVM 规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为 HotSpot 虚拟机以永久代来实现方法区,从而 JVM 的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从 JDK7 之后,Hotspot 虚拟机便将运行时常量池从永久代移除了。
1.2 JVM 内存模型
1.2.1 为什么需要把堆分代
为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化 GC 性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC 的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当 GC 的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来
1.2.2 年轻代中的 GC
新生代大小(PSYoungGen total 9216K)=eden 大小(eden space 8192K)+1 个 survivor 大小(from space 1024K)
HotSpot JVM 把年轻代分为了三部分:1 个 Eden 区和 2 个 Survivor 区(分别叫 from 和 to)。默认比例为 8(Eden):1(一个 survivor)。
一般情况下,新创建的对象都会被分配到 Eden 区(一些大对象特殊处理),这些对象经过第一次 Minor GC 后,如果仍然存活,将会被移到 Survivor 区。对象在 Survivor 区中每熬过一次 Minor GC,年龄就会增加 1 岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在 GC 开始的时候,对象只会存在于 Eden 区和名为“From”的 Survivor 区,Survivor 区“To”是空的。紧接着进行 GC,Eden 区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。
年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold 来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次 GC 后,Eden 区和 From 区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次 GC 前的“From”,新的“From”就是上次 GC 前的“To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的。
Minor GC 会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
1.2.3 堆内存模型
如果没有 Survivor,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发 Major GC(因为 Major GC 一般伴随着 Minor GC,也可以看做触发了 Full GC)。老年代的内存空间远大于新生代,进行一次 Full GC 消耗的时间比 Minor GC 长得多。你也许会问,执行时间长有什么坏处?频发的 Full GC 消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度,更不要说某些连接会因为超时发生连接错误了。
什么样的对象进入老年代
大对象直接进入老年代(大对象大小可配置,-XX:PretenureSizeThreshold 参数配置)
长期存活的对象进入老年代(默认 age=15,一次 ygc 的周期 age+1)
对象动态分配原则,ygc 时计算
空间担保原则
空间担保原则
每次 ygc 时预估这次 ygc 要往老年代放多少东西,如果要往老年代存放的对象大小大于老年代大小,则不进行 ygc,改为进行 full gc
触发条件:ygc
1.2.4 为什么要设置两个 Survivor 区
设置两个 Survivor 区最大的好处就是解决了碎片化。
为什么一个 Survivor 区不行?第一部分中,我们知道了必须设置 Survivor 区。假设现在只有一个 survivor 区,我们来模拟一下流程:
刚刚新建的对象在 Eden 中,一旦 Eden 满了,触发一次 Minor GC,Eden 中的存活对象就会被移动到 Survivor 区。这样继续循环下去,下一次 Eden 满了的时候,问题来了,此时进行 Minor GC,Eden 和 Survivor 各有一些存活对象,如果此时把 Eden 区的存活对象硬放到 Survivor 区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。
那么,顺理成章的,应该建立两块 Survivor 区,刚刚新建的对象在 Eden 中,经历一次 Minor GC,Eden 中的存活对象就会被移动到第一块 survivor space S0,Eden 被清空;等 Eden 区再满了,就再触发一次 Minor GC,Eden 和 S0 中的存活对象又会被复制送入第二块 survivor space S1(这个过程非常重要,因为这种复制算法保证了 S1 中来自 S0 和 Eden 两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0 和 Eden 被清空,然后下一轮 S0 与 S1 交换角色,如此循环往复。如果对象的复制次数达到 16 次,该对象就会被送到老年代中。
上述机制最大的好处就是,整个过程中,永远有一个 survivor space 是空的,另一个非空的 survivor space 无碎片。
那么,Survivor 为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果 Survivor 区再细分下去,每一块的空间就会比较小,很容易导致 Survivor 区满,因此,我认为两块 Survivor 区是经过权衡之后的最佳方案。
二 jvm 参数详解
2.1 常用参数介绍
在 java8 中
新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。
老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即 90% )的新生代空间。
Metaspace:如果启动后 GC 过于频繁,请将该值设置得大一些。更多 Meatspace 内容见《Java 8: 从永久代(PermGen)到元空间(Metaspace)》
一些常见的 jvm 参数的用法和含义如下:
-XX:NewSize 和-XX:MaxNewSize
-XX:NewSize 和-XX:MaxNewSize(jdk1.3or1.4)
用于设置年轻代的大小,建议设为整个堆大小的 1/3 或者 1/4,两个值设为一样大。
-Xmn
-Xmn(jdk1.4or lator)
用于设置年轻代大小。例如:-Xmn10m,设置新生代大小为 10m。此处的大小是(eden+ 2 survivor space).与 jmap -heap 中显示的 New gen 是(eden+1 survivor space)不同的。
-XX:PretenureSizeThreshold
-XX:PretenureSizeThreshold 的意思是超过这个值的时候,对象直接在 old 区分配内存
默认值是 0,意思是不管多大都是先在 eden 中分配内存
-XX:MaxTenuringThreshold
设置晋升到老年代的对象年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。如果将此值设置为一个较大值,则年轻代对象会在 Survivor 区进行多次复制。
-XX:SurvivorRatio
-XX:SurvivorRatio
用于设置 Eden 和其中一个 Survivor 的比值,默认比例为 8(Eden):1(一个 survivor),这个值也比较重要。
例如:-XX:SurvivorRatio=4:设置年轻代中 Eden 区与 Survivor 区的大小比值。设置为 4,则两个 Survivor 区与一个 Eden 区的比值为 2:4,一个 Survivor 区占整个年轻代的 1/6。
例子:-XX:SurvivorRatio=8,则两个 Survivor 区与一个 Eden 区的比值为 2:8,一个 Survivor 区占整个年轻代的 1/10。
-XX:+UseParallelGC
如果你有一个双核的 CPU,也许可以尝试这个参数:
-XX:+UseParallelGC
让 GC 可以更快的执行。(只是 JDK 5 里对 GC 新增加的参数)
-Xms 和-Xmx
JVM 初始分配的堆内存由-Xms 指定,默认是物理内存的 1/64。
JVM 最大分配的堆内存由-Xmx 指定,默认是物理内存的 1/4。
默认空余堆内存小于 40%时,JVM 就会增大堆直到-Xmx 的最大限制。
空余堆内存大于 70%时,JVM 会减少堆直到-Xms 的最小限制。因此服务器一般设置-Xms、-Xmx 相等以避免在每次 GC 后调整堆的大小。
如果-Xmx 不指定或者指定偏小,应用可能会导致 java.lang.OutOfMemory 错误,此错误来自 JVM,不是 Throwable 的,无法用 try…catch 捕捉。
-XX:PermSize
JVM 使用-XX:PermSize 设置非堆内存初始值,默认是物理内存的 1/64;由 XX:MaxPermSize 设置最大非堆内存的大小,默认是物理内存的 1/4。
XX:MaxPermSize 设置过小会导致 java.lang.OutOfMemoryError: PermGen space 就是内存益出。
- 这一部分内存用于存放 Class 和 Meta 的信息,Class 在被 Load 的时候被放入 PermGen space 区域,它和存放 Instance 的 Heap 区域不同。
- GC(Garbage Collection)不会在主程序运行期对 PermGen space 进行清理,所以如果你的 APP 会 LOAD 很多 CLASS 的话,就很可能出现 PermGen space 错误。
这种错误常见在 web 服务器对 JSP 进行 pre compile 的时候。
2.2 参数举例说明
参数说明
- -Xmx500m:设置 JVM 最大堆内存为 500M。
- -Xms500m:Xms500m。此值可以设置与-Xmx 相同,以避免每次垃圾回收完成后 JVM 重新分配内存。
- -Xss128k:设置每个线程的栈大小。JDK5.0 以后每个线程栈大小为 1M,之前每个线程栈大小为 256K。应当根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000 左右。需要注意的是:当这个值被设置的较大(例如>2MB)时将会在很大程度上降低系统的性能。
- -Xmn2g:设置年轻代大小为 2G。在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然。此值关系到 JVM 垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的 3/8。
- -XX:NewSize=1024m:设置年轻代初始值为 1024M。
- -XX:MaxNewSize=1024m:设置年轻代最大值为 1024M。
- -XX:PermSize=256m:设置持久代初始值为 256M。
- -XX:MaxPermSize=256m:设置持久代最大值为 256M。
- -XX:NewRatio=4:设置年轻代(包括 1 个 Eden 和 2 个 Survivor 区)与年老代的比值。表示年轻代比年老代为 1:4。
- -XX:SurvivorRatio=4:设置年轻代中 Eden 区与 Survivor 区的比值。表示 2 个 Survivor 区(JVM 堆内存年轻代中默认有 2 个大小相等的 Survivor 区)与 1 个 Eden 区的比值为 2:4,即 1 个 Survivor 区占整个年轻代大小的 1/6。
- -XX:MaxTenuringThreshold=7:表示一个对象如果在 Survivor 区(救助空间)移动了 7 次还没有被垃圾回收就进入年老代。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代,对于需要大量常驻内存的应用,这样做可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在 Survivor 区进行多次复制,这样可以增加对象在年轻代存活时间,增加对象在年轻代被垃圾回收的概率,减少 Full GC 的频率,这样做可以在某种程度上提高服务稳定性。
2.2.1 参数优先级
-Xmn,-XX:NewSize/-XX:MaxNewSize,-XX:NewRatio 3 组参数都可以影响年轻代的大小,混合使用的情况下,优先级如下:
- 高优先级:-XX:NewSize/-XX:MaxNewSize
- 中优先级:-Xmn(默认等效 -Xmn=-XX:NewSize=-XX:MaxNewSize=?)
- 低优先级:-XX:NewRatio
推荐使用-Xmn 参数,原因是这个参数简洁,相当于一次设定 NewSize/MaxNewSIze,而且两者相等,适用于生产环境。-Xmn 配合 -Xms/-Xmx,即可将堆内存布局完成。
-Xmn 参数是在 JDK 1.4 开始支持
举例说明 1
1 | -vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M |
举例说明 2
1 | java -Xmx3550m -Xms3550m -Xmn2g -Xss128k |
举例说明 3
1 | java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 |
2.3 回收器选择
JVM 给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0 以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0 以后,JVM 会根据当前系统配置进行判断。
垃圾回收不可预知,不同的 jvm 采用不同的垃圾回收机制和算法,有可能定时发生,有可能 CPU 空闲时发生,也有可能内存耗尽时发生
2.3.1 GC 类型
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC 有两种类型:Minor GC 和 Full GC。
Minor GC
一般情况下,当新对象生成,并且在 Eden 申请空间失败时,就会触发 Minor GC,对 Eden 区域进行 GC,清除非存活对象,并且把尚且存活的对象移动到 Survivor 区。然后整理 Survivor 的两个区。这种方式的 GC 是对年轻代的 Eden 区进行,不会影响到年老代。因为大部分对象都是从 Eden 区开始的,同时 Eden 区不会分配的很大,所以 Eden 区的 GC 会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使 Eden 去能尽快空闲出来。
Minor GC 触发条件:
- eden 区满
Full GC
对整个堆进行整理,包括 Young、Tenured 和 Perm。Full GC 因为需要对整个对进行回收,所以比 Full GC 要慢,因此应该尽可能减少 Full GC 的次数。在对 JVM 调优的过程中,很大一部分工作就是对于 Full GC 的调节。
Full GC 触发条件
- 老年代满
- 空间担保原则生效时
- 永久带满
- 代码显示调用(system.gc(),getRuntime.gc())
- 执行 jmap -dump
2.3.2 配置收集器
吞吐量优先的并行收集器
并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
典型配置:
1 | java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 |
响应时间优先的并发收集器
并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
典型配置:
1 | java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC |
2.4 常见配置汇总
堆设置
- -Xms:初始堆大小
- -Xmx:最大堆大小
- -XX:NewSize=n:设置年轻代大小
- -XX:NewRatio=n:设置年轻代和年老代的比值。如:为 3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代年老代和的 1/4
- -XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。如:3,表示 Eden:Survivor=3:2,一个 Survivor 区占整个年轻代的 1/5
- -XX:MaxPermSize=n:设置持久代大小
收集器设置
- -XX:+UseSerialGC:设置串行收集器
- -XX:+UseParallelGC:设置并行收集器
- -XX:+UseParalledlOldGC:设置并行年老代收集器
- -XX:+UseConcMarkSweepGC:设置并发收集器
垃圾回收统计信息
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:filename
并行收集器设置
- -XX:ParallelGCThreads=n:设置并行收集器收集时使用的 CPU 数。并行收集线程数。
- -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
- -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为 1/(1+n)
- 并发收集器设置
- -XX:+CMSIncrementalMode:设置为增量模式。适用于单 CPU 情况。
- -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的 CPU 数。并行收集线程数。
如果你的 WEB APP 下都用了大量的第三方 jar,其大小超过了服务器 jvm 默认的大小,那么就会产生内存益出问题了。
解决方法: 设置 MaxPermSize 大小
三 jvm 性能分析
输出 gc 日志得到文件
命令如下:
1 | -verbose:gc : 开启gc日志 |
例子
1 | -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gclogs |
对于新生代回收的一行日志,其基本内容如下
2019-06-18T16:02:17.606+0800: 611.633: [GC 611.633: [DefNew: 843458K->2K(948864K), 0.0059180 secs] 2186589K->1343132K(3057292K), 0.0059490 secs][times: user=0.00 sys=0.00, real=0.00 secs]
其含义大概如下:
2019-06-18T16:02:17.606+0800(当前时间戳): 611.633(时间戳): [GC(表示 Young GC) 611.633: [DefNew(单线程 Serial 年轻代 GC): 843458K(年轻代垃圾回收前的大小)->2K(年轻代回收后的大小)(948864K(年轻代总大小)), 0.0059180 secs(本次回收的时间)] 2186589K(整个堆回收前的大小)->1343132K(整个堆回收后的大小)(3057292K(堆总大小)), 0.0059490 secs(回收时间)][times: user=0.00(用户耗时) sys=0.00(系统耗时), real=0.00 secs(实际耗时)]
3.1 GC 活动分析
1、查找 java 进程 pid,ps -ef |grep java
1 | [root@zczc ~]# ps -ef|grep java |
- 查看 GC 活动,jstat -gcutil 进程 pid
1 | [root@zczc ~]# jstat -gcutil 30340 |
参数说明如下:
s0:s0 区使用率
S1:s1 区使用率
E:eden 区使用率
O:老年代使用率
M:方法区使用率
P:永久区使用率
YGC:YGC 次数
YGCT:总 YGC 时间,单位 s
FGC:Full GC 次数
FGCT:Full GC 总时间
GCT:总共 GC 时间(包含 YGC 和 Full GC)
3.2 典型 GC 分析
例 1
首先,老年带满(100%),进行 full gc
其次,eden 区满,进行 ygc,对象要往存活区放,长期存活对象往老年代放,但老年代满,触发空间担保原则,改 ygc 为 full gc
例 2
eden 区满,进行 ygc,eden 区被引用的对象往存活区放,大对象或长期存活的对象往老年代放,但老年代放不下,触发空间担保原则,改 ygc 为 full gc
例 3
存活区占用很小,但一直 full gc
eden 区满,进行 ygc,大对象或长期存活的对象往老年代放,但老年代放不下,触发空间担保原则,改 ygc 为 full gc
例 4
老年代没满,但一直 full gc
eden 区满,进行 ygc,大对象或长期存活的对象往老年代放,但老年代放不下,触发空间担保原则,改 ygc 为 full gc
例 5
老年代满,直接进行 full gc
四 jvm 问题排查
4.1 GC 日志的格式分析
在讲述 GC 日志之前,我们先来运行下面这段代码
1 | package com.yishuifengxiao.jvm; |
配置如下的虚拟机参数运行上述程序
1 | vm option: -Xms50M -Xmx50M -Xmn30M -verbose:gc -XX:+PrintGCDetails -XX:SurvivorRatio=8 |
注意
-XX:+PrintGCDetails 参数用于告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前内存的各区域分配情况。
得到程序日志如下
1 | [GC (Allocation Failure) [PSYoungGen: 23728K->3048K(27648K)] 23728K->16413K(48128K), 0.0324328 secs] [Times: user=0.09 sys=0.01, real=0.03 secs] |
对于上述结果,解释如下:
1 | (1)GC, Full GC说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC。如果有"Full",则表示这次GC发生了"Stop-The-World"。 |
CPU 时间与墙钟时间的区别是:墙钟时间包括各种非运算的等待耗时,例如等待磁盘 I/O、等待线程阻塞等;而 CPU 时间不包括这些耗时。
当系统有多 cpu 或者多核的话,多线程操作会叠加这些 CPU 时间,所以有时看到 user 或 sys 时间超过 real 时间是完全正常的。
4.2 常见溢出问题
1) java.lang.OutOfMemoryError:PermGen space 永久代溢出
优化:通过 MaxPermSize 参数设置 PermGen space 大小;
2) java.lang.OutOfMemoryError:java heap space 堆内存溢出
优化:-Xmn(最小值)–Xms(初始值) -Xmx(最大值),手动设置 Heap(堆)的大小;
3) java.lang.StackOverFlowError:栈溢出 栈溢出
优化:通过 Xss 参数调整;
性能出现问题时该看堆还是栈
- CPU 使用过高:方法导致,此时应该从栈中看
- 磁盘 IO 高:看栈
- 内存过高:看堆
- GC:看堆
4.3 定位内存溢出原因
不要重启,jmap -histo 查看
1)ps -ef |grep java 查找 java 程序 pid
2)将 jmap -histo 中文件重定向到 1.txt 中,找 1.txt 中 top20 中开发写的类
1 | jmap -histo 2573 > 1.txt |
1.txt 中将堆内存里的方法按占内存大小进行倒叙排序,占用最大的排在第一位。
查看 top 20 中找到自己公司 package 的方法,如果有,那就恭喜你,找到了,这个就是引起内存溢出的类。
但如果没有,top 20 全是 Char、String、Object 则需要进一步分析。
把堆内存 dump 下来,用工具分析
命令如下
1 | jmap -dump:live,format=b,file=heap.bin 2573 |
分析软件 MAT 链接:https://pan.baidu.com/s/1dl0D2gmnXeDvXg-OfRtnXg 密码:s663
4.4 栈内存分析
线程堆栈也称线程调用堆栈,是虚拟机中线程(包括锁)状态的一个瞬间状态的快照,即系统在某一个时刻所有线程的运行状态,包括每一个线程的调用堆栈,锁的持有情况。
打印出的线程堆栈的信息包括内容:
1)线程名字,id,线程的数量等;
2)线程的运行状态,锁的状态(锁被哪个线程持有,哪个线程在等待锁等);
3)调用堆栈(即函数的调用层次关系)调用堆栈包含完整的类名,所执行的方法,源代码的行数;
命令如下:
1 | jstack 2593 > 2.txt |
五 一些常用的工具
5.1 虚拟机进程状况工具 jps
jps 命令类似与 linux 的 ps 命令,但是它只列出系统中所有的 Java 应用程序。 通过 jps 命令可以方便地查看 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息。
如果在 linux 中想查看 java 的进程,一般我们都需要 ps -ef | grep java 来获取进程 ID。
如果只想获取 Java 程序的进程,可以直接使用 jps 命令来直接查看。
参数说明
1 | -q:只输出进程 ID |
5.1.1 显示进程的 ID 和类的名称
无参数:显示进程的 ID 和 类的名称
jps 不带参数,默认显示 进程 ID 和 启动类的名称。
1 | C:\cmder |
5.1.2 只输出进程 ID
参数 -q 只输出进程 ID,而不显示出类的名称
1 | C:\cmder |
5.1.3 输出传递给 java 进程的参数
参数 -m 可以输出传递给 Java 进程(main 方法)的参数。
1 | C:\cmder |
5.1.4 输出主函数的完整路径
参数 -l 可以输出主函数的完整路径(类的全路径)
1 | C:\cmder |
5.1.5 显示传递给 Java 虚拟机的参数
参数 -v 可以显示传递给 Java 虚拟机的参数。
1 | C:\cmder |
5.2 虚拟机统计信息监控工具 jstat
Jstat 用于监控基于 HotSpot 的 JVM,对其堆的使用情况进行实时的命令行的统计,使用 jstat 我们可以对指定的 JVM 做如下监控:
类的加载及卸载情况
查看新生代、老生代及持久代的容量及使用情况
查看新生代、老生代及持久代的垃圾收集情况,包括垃圾回收的次数及垃圾回收所占用的时间
查看新生代中 Eden 区及 Survior 区中容量及分配情况等
jstat 命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:
1 | jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数] |
5.2.1 垃圾回收统计
1 | jstat -gc pid |
结果如下
1 | C:\cmder |
各个输出参数的解释如下:
1 | - S0C:第一个幸存区的大小 |
5.2.2 总结垃圾回收统计
1 | jstat -gcutil pid |
结果如下
1 | C:\cmder |
各个输出参数的解释如下:
1 | S0:幸存1区当前使用比例 |
5.2.3 新生代垃圾回收统计
1 | jstat -gcnew pid |
结果如下:
1 | C:\cmder |
各个输出参数的解释如下:
1 | - S0C:第一个幸存区大小 |
5.2.4 堆内存统计
1 | jstat -gccapacity pid |
结果如下
1 | C:\cmder |
各个输出参数的解释如下:
1 | NGCMN:新生代最小容量 |
5.2.5 元数据空间统计
1 | jstat -gcmetacapacity pid |
结果如下
1 | C:\cmder |
各个输出参数的解释如下:
1 | MCMN:最小元数据容量 |
5.2.6 新生代内存空间统计
1 | jstat -gcnewcapacity pid |
结果如下
1 | C:\cmder |
各个输出参数的解释如下:
1 | NGCMN:新生代最小容量 |
5.2.7 老年代内存空间统计
1 | jstat -gcoldcapacity pid |
结果如下
1 | C:\cmder |
各个输出参数的解释如下:
1 | OGCMN:老年代最小容量 |
5.3 配置信息工具 jinfo
jinfo 是 JDK 自带的命令,可以用来查看正在运行的 java 应用程序的扩展参数,包括 Java System 属性和 JVM 命令行参数;也可以动态的修改正在运行的 JVM 一些参数。当系统崩溃时,jinfo 可以从 core 文件里面知道崩溃的 Java 应用程序的配置信息
使用命令
1 | C:\cmder |
参数说明
1 | pid 对应jvm的进程id |
option
1 | no option 输出全部的参数和系统属性 |
5.3.1 输出当前进程的全部参数和系统属性
1 | 命令:jinfo pid |
5.3.2 输出对应名称的参数
1 | 命令:jinfo -flag name pid |
使用该命令,可以查看指定的 jvm 参数的值。如:查看当前 jvm 进程是否开启打印 GC 日志。
5.3.3 开启或者关闭对应名称的参数
示例三:-flag [+|-]name
1 | 命令:jinfo -flag [+|-]name pid |
使用 jinfo 可以在不重启虚拟机的情况下,可以动态的修改 jvm 的参数。尤其在线上的环境特别有用。
使用如下:
5.3.4 修改指定参数的值
1 | 命令:jinfo -flag name=value pid |
同示例三,但示例三主要是针对 boolean 值的参数设置的。
如果是设置 value 值,则需要使用 name=value 的形式。
使用如下:
注意事项 :
jinfo 虽然可以在 java 程序运行时动态地修改虚拟机参数,但并不是所有的参数都支持动态修改
5.3.5 输出全部的参数
1 | 命令:jinfo -flags pid |
5.3.6 输出全部的系统属性
1 | 命令:jinfo -sysprops pid |
5.3.7 运行时开启 GC 日志
我们经常会遇到 JVM 运行时出错的情况。若能在启动时加入一些启动选项(startup option),便可以获取与 bug 相关的重要线索,从而有希望根治它们。但在实际操作时,我们总是忘记添加-XX:+HeapDumpOnOutOfMemoryError 或 -XX:+PrintGCDetails 这样必要的 flag。
每当面对如此窘境,我们只能关闭 JVM,修改启动参数(startup parameter),然后默默祈祷,希望问题场景(problematic situation)能在重启之后得以重现。这种方法偶尔奏效,在场景重现后你或许还能收集到足够的证据,以便动手根治潜在的问题。
不难看出,前文所述的方法问题显著——你必须执行一次额外的重启才能加入那烦人的 debug 选项,而不能借助中断(outage)实现
通过以下的命令你便能看到 JVM 中哪些 flag 可以被 jinfo 动态修改
1 | C:\cmder |
通过选项-XX:+PrintFlagsFinal 可以列出所有的 JVM flag,而其中的标注为 manageable 的 flag 则是值得我们关注的部分。这些 flag 可通过 JDK management interface(-XX:+PrintFlagsFinal)动态修改。虽然在 JConsole 中也能查到与其十分相似的 MBean。但在我看来,以命令行的方式查看它们更加的便捷。
1 | C:\cmder |
在 jinfo 中需要打开-XX:+PrintGC 和 -XX:+PrintGCDetails 两个选项才能开启 GC 日志,这与用命令行参数的方式实现有着细微的差别——如果你通过启动脚本(startup script)来设置参数,仅需-XX:+PrintGCDetails 即可,因为-XX:+PrintGC 会被自动打开。
5.4 内存映射工具
jmap 用于生成堆快照(heapdump)。当然我们有很多方法可以取到对应的 dump 信息,如我们通过 JVM 启动时加入启动参数 –XX:HeapDumpOnOutOfMemoryError 参数,可以让 JVM 在出现内存溢出错误的时候自动生成 dump 文件,亦可以通过-XX:HeapDumpOnCtrlBreak 参数,在运行时使用 ctrl+break 按键生成 dump 文件,当然我们也可以使用 kill -3
pid 的方式去恐吓 JVM 生成 dump 文件。
jmap 的作用不仅仅是为了获取 dump 文件,还可以用于查询 finalize 执行队列、Java 堆和永久带的详细信息,如空间使用率、垃圾回收器等。
其运行格式如下:
1 | λ jmap --h |
参数:
1 | option: 选项参数。 |
option
1 | no option: 查看进程的内存映像信息,类似 Solaris pmap 命令。 |
5.4.1 查看进程的内存映像信息
1 | 命令:jmap pid |
使用不带选项参数的 jmap 打印共享对象映射,将会打印目标虚拟机中加载的每个共享对象的起始地址、映射大小以及共享对象文件的路径全称。这与 Solaris 的 pmap 工具比较相似。
1 | C:\cmder |
5.4.2 显示 Java 堆详细信息
1 | 命令:jmap -heap pid |
打印一个堆的摘要信息,包括使用的 GC 算法、堆配置信息和各内存区域内存使用信息
1 | C:\cmder |
5.4.3 显示堆中对象的统计信息
1 | 命令:jmap -histo:live pid |
其中包括每个 Java 类、对象数量、内存大小(单位:字节)、完全限定的类名。打印的虚拟机内部的类名称将会带有一个’*’前缀。如果指定了 live 子选项,则只计算活动的对象。
1 | C:\cmder |
5.4.4 打印类加载器信息
1 | 命令:jmap -clstats pid |
打印 Java 堆内存的永久保存区域的类加载器的智能统计信息。对于每个类加载器而言,它的名称、活跃度、地址、父类加载器、它所加载的类的数量和大小都会被打印。此外,包含的字符串数量和大小也会被打印。
5.4.5 打印等待终结的对象信息
1 | 命令:jmap -finalizerinfo pid |
Number of objects pending for finalization: 0 说明当前 F-QUEUE 队列中并没有等待 Fializer 线程执行 final
5.4.6 生成堆转储快照 dump 文件
1 | 命令:jmap -dump:format=b,file=heapdump.phrof pid |
以 hprof 二进制格式(后缀名也可以是 bin)转储 Java 堆到指定 filename 的文件中。live 子选项是可选的。如果指定了 live 子选项,堆中只有活动的对象会被转储。想要浏览 heap dump,你可以使用 jhat(Java 堆分析工具)读取生成的文件。
这个命令执行,JVM 会将整个 heap 的信息 dump 写入到一个文件,heap 如果比较大的话,就会导致这个过程比较耗时,并且执行的过程中为了保证 dump 的信息是可靠的,所以会暂停应用, 线上系统慎用。
5.5 虚拟机堆转储快照分析工具
jhat 是用来分析 dump 文件的一个微型的 HTTP/HTML 服务器,它能将生成的 dump 文件生成在线的 HTML 文件,让我们可以通过浏览器进行查阅,然而实际中我们很少使用这个工具,因为一般服务器上设置的堆、栈内存都比较大,生成的 dump 也比较大,直接用 jhat 容易造成内存溢出,而是我们大部分会将对应的文件拷贝下来,通过其他可视化的工具进行分析。启用法如下:
1 | jhat {dump_file} |
执行命令后,我们看到系统开始读取这段 dump 信息,当系统提示 Server is ready 的时候,用户可以通过在浏览器键入http://ip:7000进行查询。
5.6 堆栈跟踪工具 jstack
jstack 用于 JVM 当前时刻的线程快照,又称 threaddump 文件,它是 JVM 当前每一条线程正在执行的堆栈信息的集合。生成线程快照的主要目的是为了定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部时长过长导致线程停顿的原因
1 | C:\cmder |
参数说明
1 | -l 长列表. 打印关于锁的附加信息,例如属于java.util.concurrent 的 ownable synchronizers列表. |
pid 需要被打印配置信息的 java 进程 id,可以用 jps 查询.
Jstack 使用
1 | C:\cmder |
5.7 jcmd 工具
在 JDK1.7 以后,新增了一个命令行工具 jcmd。这是一个多功能的工具,可以用它来导出堆、查看 Java 进程、导出线程信息、执行 GC、还可以进行采样分析(jmc 工具的飞行记录器)
命令格式
1 | jcmd <pid | main class> <command ... | PerfCounter.print | -f file> |
描述
- pid:接收诊断命令请求的进程 ID。
main class :接收诊断命令请求的进程的 main 类。匹配进程时,main 类名称中包含指定子字符串的任何进程均是匹配的。如果多个正在运行的 Java 进程共享同一个 main 类,诊断命令请求将会发送到所有的这些进程中。
command:接收诊断命令请求的进程的 main 类。匹配进程时,main 类名称中包含指定子字符串的任何进程均是匹配的。如果多个正在运行的 Java 进程共享同一个 main 类,诊断命令请求将会发送到所有的这些进程中。
注意: 如果任何参数含有空格,你必须使用英文的单引号或双引号将其包围起来。 此外,你必须使用转义字符来转移参数中的单引号或双引号,以阻止操作系统 shell 处理这些引用标记。当然,你也可以在参数两侧加上单引号,然后在参数内使用双引号(或者,在参数两侧加上双引号,在参数中使用单引号)。
- Perfcounter.print:打印目标 Java 进程上可用的性能计数器。性能计数器的列表可能会随着 Java 进程的不同而产生变化
- -f file:从文件 file 中读取命令,然后在目标 Java 进程上调用这些命令。在 file 中,每个命令必须写在单独的一行。以”#”开头的行会被忽略。当所有行的命令被调用完毕后,或者读取到含有 stop 关键字的命令,将会终止对 file 的处理。
- -l:查看所有的进程列表信息。
- -h:查看帮助信息。(同 -help)
5.7.1 查看所有的 jvm 进程信息
查看进程 jcmd -l
1 | 命令:jcmd -l |
执行结果
1 | C:\cmder |
5.7.2 查看性能统计
1 | 命令:jcmd pid PerfCounter.print |
执行结果
1 | C:\cmder |
参考文章