JVM调优基础

JVM参数调整

标配参数

各版本之间很稳定,没有变化。

-version

-help

java -showversion

X参数

HotSpot VM 默认为混合模式

-Xint:解释执行

-Xcomp:第一次使用就编译成本地代码

-Xmixed:混合模式

XX参数

Boolean类型

-XX:+或-某个属性值+表示开启 -表示关闭

Case:

是否打印GC收集细节-XX:+PrintGCDetails 开启 -XX:-PrintGCDetails 关闭

是否使用串行垃圾回收器-XX:-UseSerialGC

p.s. 查看一个正在运行的Java程序的某个JVM参数是否开启,具体值为多少?

jps -l 可以得到正在运行的Java进程编号

jinfo -flag PrintGCDetails 13632 查看是否添加了某参数

jinfo -flags 13632 查看修改的所有的参数值

Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:MaxNewSize=3145728 -XX:Mi
nHeapDeltaBytes=524288 -XX:NewSize=3145728 -XX:OldSize=7340032 -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseComp
ressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
Command line:  -Xms10m -Xmx10m -XX:+PrintGCDetails -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2018.2\lib\idea_rt.jar=1
2035:C:\Program Files\JetBrains\IntelliJ IDEA 2018.2\bin -Dfile.encoding=UTF-8

KV设值类型

-XX:属性key=属性值value

Case:

-XX:MetaspaceSize=128m

p.s.元空间默认初始值为21m

-XX:MaxTenuringThreshold=15 设定年龄阈值

-Xms 等同于 -XX:InitialHeapSize

-Xmx 等同于 -XX:MaxHeapSize

Java7:

图片名称

Java8:

Java8以后的元空间并不在虚拟机中而是使用本地物理内存,这样可以加载多少类的元数据就不再由MaxPerSize控制,而是由系统的实际可用空间来控制。

图片名称
参数 意义
-Xms 设置初始分配大小,默认为物理内存的1 / 64
-Xmx 最大分配内存,默认为物理内存的1 / 4
-XX:+PrintGCDetails 输出详细的GC处理日志
-Xss 为JVM启动的每个线程分配的栈内存大小

测试案例:

public class JVMDemo {
    public static void main(String[] args) {
        //返回Java虚拟机试图使用的最大内存量。
        long maxMemory = Runtime.getRuntime().maxMemory();

        //返回Java虚拟机中的内存总量
        long totalMemory = Runtime.getRuntime().totalMemory();

        System.out.println("MAX_MEMORY = " + maxMemory + "(字节)、" + (maxMemory / (double) 1024 / 1024) + "MB");
        System.out.println("TOTAL_MEMORY = " + totalMemory + "(字节)、" + (totalMemory / (double) 1024 / 1024) + "MB");
    }
}

//输出:
//MAX_MEMORY = 1886912512(字节)、1799.5MB 8G的四分之一
//TOTAL_MEMORY = 128974848(字节)、123.0MB

在实际工程中,必须将Xms与Xmx设置为相同,避免内存忽高忽低导致停顿,避免JVM在运行时向OS申请内存。

设置JVM参数-Xms1024m -Xmx1024m -XX:+PrintGCDetails

再次执行上述代码:

查看参数

-XX:+PrintFlagsInitial 查看初始默认值(未运行)

-XX:+PrintFlagsFinal查看修改后的参数 :=说明是修改过的

-XX:+PrintCommandLineFlags 查看使用的垃圾回收器

常用基本配置参数

-Xms

初始大小内存,默认为物理内存1/64,等价于-XX:InitialHeapSize

-Xmx

最大分配内存,默认物理内存1/4,等价于-XX:MaxHeapSize

-Xss

设置单个线程的大小,默认542K~1024K ,等价于-XX:ThreadStackSize

-Xmn

设值新生代大小,一般用默认值

-XX:MetaspaceSize

设值元空间大小,默认只有21m,可以把元空间配置的大一些(512m)。

-XX:+PrintGCDetails

输出详细GC收集日志信息

-XX:SurvivorRatio

设置新生代中Eden和S0/S1空间的比例

默认-XX:SurvivorRatio=8,Eden:S0:S1=8:1:1

-XX:NewRatio

设置年轻代与老年代在堆结构的占比,一般用默认值

默认-XX:NewRatio=2 新生代占1,老年代占2,年轻代占整个堆的1/3

-XX:MaxTenuringThreshold

设置新生代垃圾的最大年龄

默认-XX:MaxTenuringThreshold=15

如果设置为0,年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大的值,则年轻代对象回在Survivor区进行多次复制,这样可以增加对对象在年轻代的存活时间,增加在年轻代即被回收的概率

垃圾收集器

垃圾回收方式

四种主要垃圾回收方式:

图片名称
  1. Serial 串行回收

    为单线程换进该设计且只是用过一个线程进行垃圾回收,会暂停所有的用户线程,不适合服务器环境;

  2. Paralle 并行回收

    多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理后台台处理等弱交互场景;垃圾收集速度比Serial快;

  3. CMS 并发标记清除

    用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,互联网公司多用它,适用对响应时间有要求的场景(强交互);

  4. G1 Garbage First

    将堆内存分割成不同的区域然后并发的对其进行垃圾回收。

  5. ZGC

注:STW(Stop The World) VS Concurrent(并发)

概述

Java的垃圾收集器共有七种

UseSerialGC UseParallelGC UseConcMarkSweepGC UseParNewGC UseParallelOldGC UseG1GC

(有一种SerialOld已淘汰)

各收集器所对应的收集区域:

七种垃圾收集器之间的对应关系:

部分参数说明:

  1. DefNew:Default New Generation
  2. Tenured:Old
  3. ParNew:Parallel New Generation
  4. PSYoungGen:Parallel Scavenge
  5. ParOldGen:Parallel Old Generation

Serial

一个单线程的收集器,在进行垃圾收集的时候,必须暂停其它所有的工作线程知道它收集结束。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N1uSPC0Q-1585122361316)(http://picture.tjtulong.top/%E5%8D%95%E7%BA%BF%E7%A8%8B%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.JPG)]

串行收集器是最古老,最稳定以及效率最高的收集器,只使用一个线程去垃圾回收。但其在进行垃圾收集过程中可能会产生较长的停顿(Stop-The-World状态)。虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。

对应的JVM参数是:-XX:+UseSerialGC

开启后会使用:Serial(Young区)+Serial Old(Old区)的收集器组合,日志为DefNew +Tenured。表示:新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记整理算法。

ParNew

使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-world暂停其他所有的工作线程直到它收集结束。

图片名称

ParNew收集器其实就是Serial收集器新生代的并行多线程版本,最常见的应用场景时配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程成中同样也要暂停所有其他的工作线程,他是很多Java虚拟机运行在Server模式下新生代的默认垃圾收集器。

对应的JVM参数:-XX:+UseParNewGC 启用ParNew收集器,只影响新生代的收集,不影响老年代。

开启后会使用:ParNew(Young区)+Serial Old(Old 区)的收集器组合,日志为ParNew+Tenured,现在已不建议使用,新生代使用复制算法,老年代采用标记-整理算法。

注:-XX:ParallelGCThreads 限制线程数量,默认开启和CPU数目相同的线程数

Parallel Scavenge

类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。串行收集器在新生代和老年代的并行化。

它重点关注的是:

  • 可控制的吞吐量(Thoughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99%)。高吞吐量意味着高效利用CPU时间,它多用于在后台运算而不需要太多交互的任务
  • 自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。(虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量)。

-XX:MaxGCPauseMillis 垃圾收集器最大停顿的时间,但最大停顿时间过短必然会导致新生代的内存大小变小,垃圾回收频率变高,效率可能降低。
-XX:CGTIMERatio 吞吐量大小(0-100),默认为99。

-XX:+UseParallelGC -XX:+UseParallelOldGC效果相同,都是Parallel Scavenge + Parallel Old的组合

Parallel Old

Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6开始提供

1.6之前,新生代使用的Parallel Scavenge收集器只能搭配年老代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。

Parallel Old正是为了在老年代同样提供吞吐量优先而产生的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可以考虑新生代Parallel Scavenge和年老代Parallel Old收集器的搭配策略。

CMS

CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。

适合在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短(也有两个短时间的Stop The World)。CMS非常适合堆内存大、CPU核数多的服务区端应用,也是G1出现之大型应用的首选收集器。

enter description here

4步过程:

  • 初始标记(CMS initial mark)

    只是标记一下GC Roots能够直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

  • 并发标记(CMS concurrent mark)

    进行GC Roots跟踪过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象。

  • 重新标记(CMS remark)

    为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。

  • 并发清除(CMS concurrent sweep)

    清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程,基于标记结果,直接清理对象。

    由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上看来CMS收集器的内存回收和用户线程是一起并发的执行

JVM参数:-XX:+UseConcMarkSweepGC,开启该参数后会自动将-XX:UseParNewGC打开。

开启该参数后,使用ParNew+CMS+Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器。

优缺点:

  • 并发收集停顿低

  • 并发执行,cpu资源压力大

    由于并发进行,CMS在收集时与应用线程会同时增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将出发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间。

  • 采用的标记清除算法会导致大量的碎片

    标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来制定多少次CMS收集之后,进行一次压缩的FullGC。

Serial Old

Serial Old是Serial垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记整理算法,这个收集器主要运行在Client的JVM。

G1收集器

G1(Garbage First)垃圾收集器是当今垃圾回收技术最前沿的成果之一,早在JDK7就已加入JVM的收集器大家庭中,成为HotSpot重点发展的垃圾回收技术。用于服务端。

主要应用在多CPU和大内存服务器环境下,极大的减少了垃圾收集的停顿时间,全面提升服务器的性能,逐步替代Java8以前的CMS收集器。主要改变是Eden,Survivor和Tenured等内存区域不再是连续的,而是变成了一个一个的Region,这种化整为零的方法可以将每次扫描的空间变小,提高效率。

优势:并行(多核CPU)与并发;
   分代收集(新生代和老年代区分不明显);
   空间整合;
   限制收集范围,可预测的停顿。
步骤:初始标记、并发标记、最终标记和筛选回收。
enter description here

选择垃圾收集器

  • 单CPU或小内存:SerialNew + SerialOld
  • 多CPU,需要大吞吐量,如后台计算型应用:ParallelNew+ParallelOld
  • 多CPU,追求低停顿时间,需快速响应,如互联网应用:CMS+ParNew

GC日志信息

运行一个会导致OOM的程序:-Xms10m -Xmx10m -XX:+PrintGCDetails

public class JVMDemo {
    public static void main(String[] args) {
        String str = "tjtulong";
        while (true) {
            str += str + new Random().nextInt(88888888)
                    + new Random().nextInt(99999999);
        }
    }
}

输出日志:

[GC (Allocation Failure) [PSYoungGen: 2045K->504K(2560K)] 2045K->822K(9728K), 0.0019536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2443K->504K(2560K)] 2761K->1222K(9728K), 0.0013311 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2484K->504K(2560K)] 3202K->2189K(9728K), 0.0010760 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2094K->504K(2560K)] 5315K->4109K(9728K), 0.0009304 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2089K->504K(2560K)] 7230K->6437K(9728K), 0.0015372 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 504K->504K(1536K)] 6437K->6437K(8704K), 0.0004704 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 5933K->3116K(7168K)] 6437K->3116K(8704K), [Metaspace: 3516K->3516K(1056768K)], 0.0059659 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 47K->32K(2048K)] 6235K->6219K(9216K), 0.0005355 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 32K->0K(2048K)] [ParOldGen: 6187K->4650K(7168K)] 6219K->4650K(9216K), [Metaspace: 3524K->3524K(1056768K)], 0.0066428 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 23K->64K(2048K)] 6209K->6250K(9216K), 0.0004144 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 64K->0K(2048K)] [ParOldGen: 6186K->2347K(7168K)] 6250K->2347K(9216K), [Metaspace: 3529K->3529K(1056768K)], 0.0065336 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 47K->80K(2048K)] 7002K->7034K(9216K), 0.0006274 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 80K->0K(2048K)] [ParOldGen: 6954K->5442K(7168K)] 7034K->5442K(9216K), [Metaspace: 3530K->3530K(1056768K)], 0.0027675 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 5442K->5442K(9216K), 0.0002907 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
[PSYoungGen: 0K->0K(2048K)] [ParOldGen: 5442K->5421K(7168K)] 5442K->5421K(9216K), [Metaspace: 3530K->3530K(1056768K)], 0.0078808 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
	at java.lang.StringBuilder.append(StringBuilder.java:208)
	at com.activemq.demo.JVMDemo.main(JVMDemo.java:9)
Heap
 PSYoungGen      total 2048K, used 50K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 4% used [0x00000000ffd00000,0x00000000ffd0cbd8,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 7168K, used 5421K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 75% used [0x00000000ff600000,0x00000000ffb4b7c0,0x00000000ffd00000)
 Metaspace       used 3562K, capacity 4502K, committed 4864K, reserved 1056768K
  class space    used 391K, capacity 394K, committed 512K, reserved 1048576K

Process finished with exit code 1

YoungGC日志

// 表示发生了Young GC
[GC (Allocation Failure) 
// 新生代内存变化,新生代占总内存1/3 
[PSYoungGen: 2045K->504K(2560K)] 2045K->822K(9728K), 0.0019536 secs] 
// 耗时
[Times: user=0.00 sys=0.00, real=0.00 secs] 

Full GC日志

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GJHWOFcP-1585122361328)(http://picture.tjtulong.top/FullGC%E6%97%A5%E7%A7%9F.jpg)]

// Full GC标志
[Full GC (Ergonomics) 
// 新生代内存变化
[PSYoungGen: 80K->0K(2048K)] 
// 老年代内存变化
[ParOldGen: 6954K->5442K(7168K)] 7034K->5442K(9216K), 
// 元空间内存变化
[Metaspace: 3530K->3530K(1056768K)], 0.0027675 secs] 
[Times: user=0.00 sys=0.00, real=0.00 secs] 

OOM异常

都是Error错误

栈溢出

java.lang.StackOverflowError

栈空间溢出 ,递归调用时可能出现

堆溢出

java.lang.OutOfMemoryError:Java heap space

堆内存溢出,对象过多过大

GC回收时间过长

java.lang.OutOfMemoryError:GC overhead limit exceeded

过长的定义是超过**98%的时间用来做GC并且回收了而不倒2%**的堆内存,连续多次GC,都回收了不到2%的极端情况下才会抛出。如果不抛出,那就是GC清理的一点内存很快会被再次填满,迫使GC再次执行,这样就恶性循环,CPU使用率一直是100%,而GC却没有任何成果。

public class JVMDemo {
    public static void main(String[] args) throws Exception {
        int i = 0;
        List<String> list = new ArrayList<>();
        try {
            while (true) {
                list.add(String.valueOf(++i).intern());
            }
        } catch (Throwable e) {
            System.out.println("***i = " + i);
            e.printStackTrace();
            throw e;
        }
    }
}

直接内存溢出

java.lang.OutOfMemoryError:Direct buffer memory

NIO程序经常使用ByteBuffer来读取或写入数据,这是一种基于通道(Channel)与缓存区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因次避免了在Java堆和native堆中来回复制数据。

ByteBuffer.allocate(capability)第一种方式是分配JVM堆内存,属于GC管辖,由于需要拷贝所以速度较慢

ByteBuffer.alloctedDirect(capability)分配OS本地内存,不属于GC管辖,不需要拷贝,速度较快

但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现oom,程序崩溃。

可以设置直接内存大小:-XX:MaxDirectMemorySize=50m

线程问题

java.lang.OutOfMemoryError:unable to create new native thread

产生的原因:

  • 应用创建了太多线程,一个应用进程创建了多个线程,超过系统承载极限
  • 服务器并不允许你的应用程序创建这么多线程,linux系统默认允许单个进程可以创建的线程数是1024,超过这个数量,就会报错。

解决办法:

  • 降低应用程序创建线程的数量,分析应用是否需要创建这么多线程,如果不是,将线程数减到最低;

  • 如果确实需要创建较多的线程,可以修改linux服务器配置。

元空间溢出

java.lang.OutOfMemoryError:Metaspace

元空间主要存放了虚拟机加载的类的信息、常量池、静态变量、即时编译后的代码

向元空间中不断的生产类就可能溢出,例如使用cglib不断生产静态类:

static class OOMTest{}
public static void main(String[] args){
    int i = 0;
    try{
       while(true){
           i++;
           Enhancer enhancer = new Enhancer();
           enhancer.setSuperclass(OOMTest.class);
           enhancer.setUseCache(false);
           enhancer.setCallback(new MethodInterceptor(){
               @Override
               public Object intercept(Object o,Method method,Object[] objects,
                                      MethodProxy methodProxy)throws Throwable{
                   return methodProxy.invokeSuper(o,args);
               }
           });
           enhancer.create();
       } 
    } catch(Throwable e){
        System.out.println(i+"次后发生了异常");
        e.printStackTrace();
    }
}

猜你喜欢

转载自blog.csdn.net/TJtulong/article/details/105096910