Hexo

点滴积累 豁达处之

0%

JVM性能调优

JVM性能调优

JVM运行参数

标准参数

1
2
3
4
-help
-server -client ## 模式
-version -showversion
-cp -classpath

非标准

X参数

**java -X ** 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
admindembp-4:tmp admin$ java -X
-Xmixed 混合模式执行 (默认)
-Xint 仅解释模式执行
-Xbootclasspath:<用 : 分隔的目录和 zip/jar 文件>
设置搜索路径以引导类和资源
-Xbootclasspath/a:<用 : 分隔的目录和 zip/jar 文件>
附加在引导类路径末尾
-Xbootclasspath/p:<用 : 分隔的目录和 zip/jar 文件>
置于引导类路径之前
-Xdiag 显示附加诊断消息
-Xnoclassgc 禁用类垃圾收集
-Xincgc 启用增量垃圾收集
-Xloggc:<file> 将 GC 状态记录在文件中 (带时间戳)
-Xbatch 禁用后台编译
-Xms<size> 设置初始 Java 堆大小
-Xmx<size> 设置最大 Java 堆大小
-Xss<size> 设置 Java 线程堆栈大小
-Xprof 输出 cpu 配置文件数据
-Xfuture 启用最严格的检查, 预期将来的默认值
-Xrs 减少 Java/VM 对操作系统信号的使用 (请参阅文档)
-Xcheck:jni 对 JNI 函数执行其他检查
-Xshare:off 不尝试使用共享类数据
-Xshare:auto 在可能的情况下使用共享类数据 (默认)
-Xshare:on 要求使用共享类数据, 否则将失败。
-XshowSettings 显示所有设置并继续
-XshowSettings:all
显示所有设置并继续
-XshowSettings:vm 显示所有与 vm 相关的设置并继续
-XshowSettings:properties
显示所有属性设置并继续
-XshowSettings:locale
显示所有与区域设置相关的设置并继续

-X 选项是非标准选项, 如有更改, 恕不另行通知。

-Xint 完全解释执行

1
2
3
4
admindembp-4:tmp admin$ java -showversion -Xint TestJVM
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, interpreted mode)

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

1
2
3
4
admindembp-4:tmp admin$ java -showversion -Xcomp TestJVM
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, compiled mode)

-Xmixed:混合模式,JVM自己来决定是否编译成本地代码(默认)

1
2
3
4
admindembp-4:tmp admin$ java -version
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)

XX 参数(使用率较高)

Boolean类型

格式:-XX:[+/-] <name> 表示启用或者禁用name属性
比如:
-XX:+UseConcMarkSweepGC 表示启用CMS垃圾回收器
-XX:+UseG1GC 表示启用G1垃圾回收器

非Boolean类型

格式:-XX:<name> = <value> 表示name属性的值为value
比如:
-XX:MaxGCPauseMillis=500 表示GC最大停顿时间是500毫秒
-XX:GCTimeRatio=19 表示新生代和老年代的比值

-Xmx -Xms
  • -Xmx等价于-XX:MaxHeapSize 表示最大堆内存大小,可使用jinfo -flag MaxHeapSize 进程id查看,如下:

    1
    2
    [root@izbp12c0zpe8t4yri0xphiz ~]# jinfo -flag MaxHeapSize 22222
    -XX:MaxHeapSize=392167424
  • -Xms等价于-XX:InitalHeapSize 表示堆内存初始大小

  • -Xss等价于-XX:InitalStackSize 表示线程栈的初始大小,可以使用jinfo -flag ThreadStackSize 进程id查看,如下:

    1
    2
    [root@izbp12c0zpe8t4yri0xphiz ~]# jinfo -flag ThreadStackSize 22222
    -XX:ThreadStackSize=1024

jps

jps:查看Java进程

1
2
3
4
5
6
[root@izbp12c0zpe8t4yri0xphiz ~]# jps
22222 Bootstrap
9407 Jps
[root@izbp12c0zpe8t4yri0xphiz ~]# jps -l
9585 sun.tools.jps.Jps
22222 org.apache.catalina.startup.Bootstrap

jinfo

Jinfo: 查看指定Java进程运行时参数

1
2
3
4
5
6
7
8
[root@izbp12c0zpe8t4yri0xphiz ~]# jinfo -flags 22222
Attaching to process ID 22222, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.51-b03
Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=25165824 -XX:+ManagementServer -XX:MaxHeapSize=392167424 -XX:MaxNewSize=130678784 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=8388608 -XX:OldSize=16777216 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
Command line: -Djava.util.logging.config.file=/usr/local/tomcat-9.0/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote -Djava.rmi.server.hostname=47.96.118.93 -Dcom.sun.management.jmxremote.port=8585 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.net.preferIPv4Stack=true -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dcatalina.base=/usr/local/tomcat-9.0 -Dcatalina.home=/usr/local/tomcat-9.0 -Djava.io.tmpdir=/usr/local/tomcat-9.0/temp
[root@izbp12c0zpe8t4yri0xphiz ~]#

Non-default VM flags表示手动赋值过的参数,其中有些是tomcat设置的

Command line:与Non-default VM flags

1
2
3
#查看某一参数的值,用法:jinfo -flag <参数名> <进程id>
[root@izbp12c0zpe8t4yri0xphiz ~]# jinfo -flag MaxHeapSize 22222
-XX:MaxHeapSize=392167424

2 JVM内存管理

jdk内存模型

jdk1.7的堆内存模型:

JVM_heap01

1.Young (新生代)

新生代 分为三部分。Eden区(new 的对象)和两个大小相同的Survivior区(某一时刻,只有一个被使用),另外一个,当Eden区满了,GC就会将存活的对象移动到空闲的Survivor区,根据JVM的策略,在经过几次垃圾收集后,依然存活在Survivor区的对象,将移动到Tenured区(老年代)

2.Tenured(老年代)

老年代 主要保存生命周期长的对象。(new 的大对象,会直接进入老年代)

3.Perm(永久代)

永久代主要保存class、method、filed对象。这部分的空间一般不会溢出,除非一次性加载很多的类,不过在涉及热部署的应用服务器的时候,有时候会遇到 java.lang.OutOfMemoryError: PermGen space的错误

jdk1.8的堆内存模型:

JVM_heap02

上图表明,jdk1.8的内存模型有2部分:年轻代+老年代

年轻代:Eden + 2*Survivor (Survivor from + Survivor to)

老年代: OldGen

在jdk1.8中变化最大是 Perm(永久区),用 Metaspace(元数据空间)进行替换

注:Metaspace所占用的内存空间不是虚拟机内部的,而是本地内存空间。

JVM_heap03

为什么要废除1.7的永久区?

  1. 在jdk1.8之前的HotSpot实现中,类的元数据 如 方法数据、方法信息(字节码、栈和变量的大小)、运行时常量池等被保存在永久代,32位默认永久代大小为64M,64位默认85M,可以通过参数 -XX:MaxPermSize进行设置,一旦类的元数据超过了永久代的大小,就会抛出OOM(内存过大,虚拟机死掉了)异常。
  2. 对永久代的调优过程非常困难,永久代的大小很难确定,其中涉及到太多因素,如类的总数,常量池大小和方法数量等,而且永久代的数据可能会随着每一次Full GC而发生移动。
  3. 而在jdk1.8中,类的元数据保存在本地内存中,元空间的最大可分配空间就是系统可用内存空间。
  4. 官网给的解释:为了融合HotSpot JVM 与 JRockit VM ,因为JRockit VM没有永久代,不需要配置永久代。

jstat

jstat查看虚拟机统计信息

-class类装载

jstat -class 进程id 每隔多少毫秒 一共输出多少次

1
2
3
4
5
6
7
8
9
10
11
[root@izbp12c0zpe8t4yri0xphiz ~]# jstat -class 22222
Loaded Bytes Unloaded Bytes Time
16574 31245.0 401 614.2 94.22
[root@izbp12c0zpe8t4yri0xphiz ~]# jstat -class 22222 1000 5
Loaded Bytes Unloaded Bytes Time
16574 31245.0 401 614.2 94.22
16574 31245.0 401 614.2 94.22
16574 31245.0 401 614.2 94.22
16574 31245.0 401 614.2 94.22
16574 31245.0 401 614.2 94.22
[root@izbp12c0zpe8t4yri0xphiz ~]#
  • Loaded:表示类加载的个数
  • Bytes:表示类加载的大小,单位为kb
  • UnLoaded:表示类卸载的个数
  • Bytes:表示类卸载的大小,单位为kb
  • Time:表示类加载和卸载的时间

JIT编译

1
2
3
4
5
6
7
8
9
10
11
[root@izbp12c0zpe8t4yri0xphiz ~]# jstat -compiler 22222 
Compiled Failed Invalid Time FailedType FailedMethod
19594 3 0 125.64 1 com/mysql/jdbc/AbandonedConnectionCleanupThread run
[root@izbp12c0zpe8t4yri0xphiz ~]# jstat -compiler 22222 1000 5
Compiled Failed Invalid Time FailedType FailedMethod
19594 3 0 125.64 1 com/mysql/jdbc/AbandonedConnectionCleanupThread run
19594 3 0 125.64 1 com/mysql/jdbc/AbandonedConnectionCleanupThread run
19594 3 0 125.64 1 com/mysql/jdbc/AbandonedConnectionCleanupThread run
19594 3 0 125.64 1 com/mysql/jdbc/AbandonedConnectionCleanupThread run
19594 3 0 125.64 1 com/mysql/jdbc/AbandonedConnectionCleanupThread run
[root@izbp12c0zpe8t4yri0xphiz ~]#
  • Compiled:表示编译成功的方法数量
  • Failed:表示编译失败的方法数量
  • Invalid:表示编译无效的方法数量
  • Time:编译所花费的时间
  • FailedType:编译失败类型
  • FailedMethod:编译失败方法

垃圾收集

1
2
3
4
5
6
7
8
9
10
[root@izbp12c0zpe8t4yri0xphiz ~]# jstat -gc 22222 
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
6144.0 6144.0 1735.3 0.0 49408.0 42161.3 123300.0 75752.0 95920.0 93351.3 11440.0 10883.2 3762 34.124 12 2.742 36.866
[root@izbp12c0zpe8t4yri0xphiz ~]# jstat -gc 22222 1000 4
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
6144.0 6144.0 1735.3 0.0 49408.0 42430.0 123300.0 75752.0 95920.0 93351.3 11440.0 10883.2 3762 34.124 12 2.742 36.866
6144.0 6144.0 1735.3 0.0 49408.0 42430.0 123300.0 75752.0 95920.0 93351.3 11440.0 10883.2 3762 34.124 12 2.742 36.866
6144.0 6144.0 1735.3 0.0 49408.0 42430.0 123300.0 75752.0 95920.0 93351.3 11440.0 10883.2 3762 34.124 12 2.742 36.866
6144.0 6144.0 1735.3 0.0 49408.0 42430.0 123300.0 75752.0 95920.0 93351.3 11440.0 10883.2 3762 34.124 12 2.742 36.866
[root@izbp12c0zpe8t4yri0xphiz ~]#
  • S0C: Current survivor space 0 capacity (kB).表示survivor 0区的总大小
  • S1C: Current survivor space 1 capacity (kB).表示survivor 1区的总大小
  • S0U: Survivor space 0 utilization (kB).表示survivor 0区使用了的大小
  • S1U: Survivor space 1 utilization (kB).表示survivor 1区使用了的大小
  • EC: Current eden space capacity (kB).表示eden区总大小
  • EU: Eden space utilization (kB).表示eden区使用了的大小
  • OC: Current old space capacity (kB).表示old区总大小
  • OU: Old space utilization (kB).表示old区使用了的大小
  • MC: Metaspace capacity (kB).表示Metaspace区总大小
  • MU: Metacspace utilization (kB).表示Metaspace区使用了的大小
  • CCSC: Compressed class space capacity (kB).表示压缩类空间总量
  • CCSU: Compressed class space used (kB).表示压缩类空间使用量
  • YGC: Number of young generation garbage collection events.表示Young GC的次数
  • YGCT: Young generation garbage collection time.表示Young GC的时间
  • FGC: Number of full GC events.表示full GC的次数
  • FGCT: Full garbage collection time.表示full GC的时间
  • GCT: Total garbage collection time.表示总的 GC的时间

jmap使用

查看内存使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
[root@izbp12c0zpe8t4yri0xphiz ~]# jmap -heap 22222
Attaching to process ID 22222, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.51-b03

using thread-local object allocation.
Mark Sweep Compact GC

Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 392167424 (374.0MB)
NewSize = 8388608 (8.0MB)
MaxNewSize = 130678784 (124.625MB)
OldSize = 16777216 (16.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)

Heap Usage: # 堆内存使用情况
New Generation (Eden + 1 Survivor Space):
capacity = 56885248 (54.25MB)
used = 38070792 (36.30713653564453MB)
free = 18814456 (17.94286346435547MB)
66.92559730072725% used
Eden Space:
capacity = 50593792 (48.25MB)
used = 36255304 (34.57575225830078MB)
free = 14338488 (13.674247741699219MB)
71.6595901726441% used
From Space:
capacity = 6291456 (6.0MB)
used = 1815488 (1.73138427734375MB)
free = 4475968 (4.26861572265625MB)
28.856404622395832% used
To Space:
capacity = 6291456 (6.0MB)
used = 0 (0.0MB)
free = 6291456 (6.0MB)
0.0% used
tenured generation:
capacity = 126259200 (120.41015625MB)
used = 77610680 (74.01531219482422MB)
free = 48648520 (46.39484405517578MB)
61.46932659164639% used

31382 interned Strings occupying 3662056 bytes.
[root@izbp12c0zpe8t4yri0xphiz ~]#

查看内存中对象

查看所有对象,包括活跃和非活跃

1
jmap -histo <pid> | more

查看活跃对象

1
jmap -histo:live <pid> | more
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

[root@izbp12c0zpe8t4yri0xphiz ~]# jmap -histo 22222 | more

num #instances #bytes class name
----------------------------------------------
1: 276783 39450176 [C
2: 73182 20722440 [B
3: 268555 6445320 java.lang.Double
4: 159548 6381920 java.util.TreeMap$Entry
5: 230993 5543832 java.lang.String
6: 38000 3344000 java.lang.reflect.Method
7: 134275 3222600 org.jfree.data.xy.XYDataItem
8: 54626 2703784 [Ljava.lang.Object;
9: 24675 2224360 [I
10: 62129 1988128 java.util.concurrent.ConcurrentHashMap$Node
11: 17188 1908880 java.lang.Class
12: 86855 1736240 [Ljava.lang.Class;
13: 17377 1533640 [Ljava.util.HashMap$Node;
14: 47437 1517984 java.util.HashMap$Node
15: 27047 1081880 java.util.LinkedHashMap$Entry
16: 16653 932568 java.util.LinkedHashMap
17: 51470 823520 java.lang.Integer
18: 18179 750448 [Ljava.lang.String;
19: 13853 664944 java.util.HashMap
20: 494 617184 [Ljava.util.concurrent.ConcurrentHashMap$Node;
21: 23696 568704 java.util.ArrayList
23: 30367 485872 java.lang.Object
24: 15016 480512 java.io.File
25: 14317 458144 java.util.Hashtable$Entry
26: 12308 393856 java.lang.ref.WeakReference
27: 9015 360600 java.util.HashMap$ValueIterator
28: 7244 347712 org.apache.tomcat.util.buf.ByteChunk
29: 6707 321936 org.apache.tomcat.util.buf.CharChunk
30: 6575 315600 org.apache.tomcat.util.buf.MessageBytes
31: 7651 306040 java.lang.ref.SoftReference
32: 12396 297504 org.apache.catalina.loader.ResourceEntry
33: 8341 266912 java.util.LinkedList
34: 3265 261200 java.lang.reflect.Constructor
35: 4066 260224 java.util.concurrent.ConcurrentHashMap
36: 2853 240336 [Ljava.util.WeakHashMap$Entry;
37: 3301 237672 java.util.regex.Pattern
--More--

对象说明

对象 说明
B byte
C Char
D Double
F Float
I Int
J Long
Z Boolean
[ 数组 如[I表示int[]
[L + 类名 其它对象

dump到文件

-dump:[live,]format=b,file= 使用hprof二进制形式,输出jvm的heap内容到文件=. live子选项是可选的,假如指定live选项,那么只输出活的对象到文件.

格式:jmap -dump:format=b,file=

示例: jmap -dump:format=b,file=/tmp/dump.dump 2642

1
2
3
4
[root@izbp12c0zpe8t4yri0xphiz bin]# jmap -dump:format=b,file=/tmp/dump.dump 2642
Dumping heap to /tmp/dump.dump ...
Heap dump file created
[root@izbp12c0zpe8t4yri0xphiz bin]#

-finalizerinfo 打印正等候回收的对象的信息

1
2
3
4
5
6
7
8
jmap -finalizerinfo 3772

结果
Attaching to process ID 19570, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 24.80-b11
Number of objects pending for finalization: 0 (等候回收的对象为0个)

jhat分析dump文件

格式 jhat -port

1
2
3
4
5
6
7
8
9
10
[root@izbp12c0zpe8t4yri0xphiz ~]# jhat -port 8485 /tmp/dump.dump
Reading from /tmp/dump.dump...
Dump file created Sun Aug 04 12:11:17 CST 2019
Snapshot read, resolving...
Resolving 480672 objects...
Chasing references, expect 96 dots................................................................................................
Eliminating duplicate references................................................................................................
Snapshot resolved.
Started HTTP server on port 8485
Server is ready.

访问 http://47.96.118.93:8485/

JVM_heap04

jvisualvm分析dump文件

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author : chenqingsong
* @date : 2020-01-31 13:26
* -Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/admin/Desktop/jvm.dump
*/
public class OOMTest {
public static List<Object> list = new ArrayList<>();


public static void main(String[] args) {
List<Object> list = new ArrayList<>();
int i=0;
int j=0;
while (true) {
list.add(new User(i++, UUID.randomUUID().toString()));
new User(j--, UUID.randomUUID().toString());
}
}
}

运行会在桌面生成dump文件,然后使用jvisualvm工具分析

jvisualvm可以监控本地、远程的java进程,实时查看进程的cpu、堆、线程等参数,对java进程生成dump文件,并对dump文件进行分析。

像我这种从服务器上dump下来文件也可以直接扔给jvisualvm来分析。

使用方式:直接双击打开jvisualvm.exe,点击文件->装入,在文件类型那一栏选择堆,选择要分析的dump文件,打开。

JVM_GC03

JVM_GC04

装入之后在界面右侧的概要、类等选项卡可以看到生成dump文件当时的堆信息:

JVM_GC05

jstack 使用

jstack 线程的状态

  • RUNNABLE 线程运行中或 I/O 等待
  • BLOCKED 线程在等待 monitor 锁( synchronized 关键字)
  • TIMED_WAITING 线程在等待唤醒,但设置了时限
  • WAITING 线程在无限等待唤醒

死锁实战

1,构造死锁

启动两个线程,thread1拿到obj1锁,准备去拿obj2锁,obj2已经被thread2锁定,所以发生了死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class TestDeadLock {
private static Object obj1 = new Object();
private static Object obj2 = new Object();

public static void main(String[] args) {
new Thread(new Thread1()).start();
new Thread(new Thread2()).start();
}

private static class Thread1 implements Runnable{
@Override
public void run() {
synchronized (obj1){
System.out.println("Thread1 拿到了obj1的锁!");
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}

synchronized (obj2){
System.out.println("Thread1 拿到了obj2的锁!");
}
}
}
}

private static class Thread2 implements Runnable{
@Override
public void run() {
synchronized (obj2){
System.out.println("Thread2 拿到了obj2的锁!");
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}

synchronized (obj1){
System.out.println("Thread2 拿到了obj1的锁!");
}
}
}
}
}

2,编译运行

1
2
3
4
javac TestDeadLock.java
[root@izbp12c0zpe8t4yri0xphiz local]# java TestDeadLock
Thread1 拿到了obj1的锁!
Thread2 拿到了obj2的锁!

3,使用jstack分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[root@izbp12c0zpe8t4yri0xphiz ~]# jps    # 找到该进程
2642 Bootstrap
17150 Jps
15486 TestDeadLock

[root@izbp12c0zpe8t4yri0xphiz ~]# jstack -l 15486
2019-08-10 14:58:58
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.51-b03 mixed mode):
......
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f3d340062c8 (object 0x00000000e8a58640, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f3d34004e28 (object 0x00000000e8a58650, a java.lang.Object),
which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
at TestDeadLock$Thread2.run(TestDeadLock.java:42)
- waiting to lock <0x00000000e8a58640> (a java.lang.Object)
- locked <0x00000000e8a58650> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:745)
"Thread-0":
at TestDeadLock$Thread1.run(TestDeadLock.java:23)
- waiting to lock <0x00000000e8a58650> (a java.lang.Object)
- locked <0x00000000e8a58640> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:745)

Found 1 deadlock.

从打印的信息中能发现有一个死锁,并知道该问题出现的代码位置TestDeadLock.java:42

JVM内存分配与回收

逃逸分析、标量替换

逃逸分析

当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其它方法中,称为方法逃逸。甚至还有可能被外部线程访问到,例如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。简单的说,逃逸分析指的是分析变量能不能逃出它的作用域。

我们来看代码示例

1
2
3
4
5
public static  SomeClass someClass;
//全局变量赋值逃逸
public void globalVariablePointerEscape(){
someClass=new SomeClass();
}
1
2
3
4
//someClass变量没有逃逸
public void globalVariablePointerEscape(){
SomeClass someClass=new SomeClass();
}

说明:如果方法中new的对象作用于仅限于该方法,则会使用标量替换和逃逸分析(开启了逃逸分析和标量替换),仅在栈上分配。如果方法中的new对象的作用域逃逸了方法,则会在堆中分配

标量

标量(scalar)是指一个无法再分解成更小的数据的数据。Java 中的原始数据类型就是标量。

相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java 中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

在 JIT 阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过 JIT 优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

代码分析

原始代码

1
2
3
4
5
6
7
8
9
10
11
public static void main(String args[]) {
alloc();
}
class Point {
private int x;
private int y;
}
private static void alloc() {
Point point = new Point(1,2);
System.out.println("point.x" + point.x + ";point.y" + point.y);
}

标量替换后的代码

1
2
3
4
5
private static void alloc() {
int x = 1;
int y = 2;
System.out.println("point.x = " + x + "; point.y=" + y);
}

可以看到,Point 这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个标量了。

那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。

标量替换为栈上分配提供了很好的基础。

参数

参数 默认值(JDK)
-XX:+DoEscapeAnalysis 开启 是否开启逃逸分析
-XX:+EliminateAllocations 开启 是否开启标量替换
-XX:+EliminateLocks 开启 是否开启锁消除
-XX:+PrintEscapeAnalysis 开启 开启逃逸分析后,可通过此参数查看分析结果。
-XX:+PrintEliminateAllocations 开启 开启标量替换后,查看标量替换情况

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 标量替换测试
* -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
*/
public class ScalarReplace {
public static class User {
public int id;
public String name;
}

public static void alloc() {
User u = new User(); // 未发生逃逸
u.id = 5;
u.name = "cakin";
}

public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start) + " ms");
}
}

/*
// 一个聚合量分解成两个标量和一个聚合量
class Customer{
String name;
int id;
Account acct;
}
// 一个聚合量分解成一个标量
class Account{
double balance;
}
*/

上述代码在主函数中进行了1亿次 alloc 调用进行对象创建,由于 User 对象实例需要占据约 16 字节的空间,因此累计分配空间达到将近 1.5 GB。如果堆空间小于这个值,就必然会发生 GC。

不进行标量替换

1
-Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations

控制台打印如下

1
2
3
4
5
6
7
8
9
[GC (Allocation Failure)  25600K->1091K(98304K), 0.0010280 secs]
[GC (Allocation Failure) 26691K->976K(98304K), 0.0008025 secs]
[GC (Allocation Failure) 26576K->896K(98304K), 0.0009549 secs]
[GC (Allocation Failure) 26496K->896K(98304K), 0.0006841 secs]
[GC (Allocation Failure) 26496K->896K(98304K), 0.0007844 secs]
[GC (Allocation Failure) 26496K->896K(100864K), 0.0007835 secs]
[GC (Allocation Failure) 31616K->816K(100864K), 0.0007315 secs]
[GC (Allocation Failure) 31536K->816K(100864K), 0.0003173 secs]
花费的时间为: 71 ms

进行标量替换

1
-server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations

控制台打印如下

1
花费的时间为: 6 ms

对象优先在Eden区分配

大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分 配时,虚拟机将发起一次Minor GC。我们来进行实际测试一下。 在测试之前我们先来看看 Minor GC和Full GC 有什么不同呢?

  • Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。

  • Major GC/Full GC:一般会回收老年代,年轻代,方法区的垃圾, Major GC的速度一般会比Minor GC的慢10倍以上。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//添加运行JVM参数: ‐XX:+PrintGCDetails 2 publicclassGCTest{
3 public static void main(String[] args) throws InterruptedException {
4 byte[] allocation1, allocation2/*, allocation3, allocation4, allocation,allocation6*/;
allocation1 = new byte[60000*1024];
//allocation2 = new byte[8000*1024];
/*allocation3 = new byte[1000*1024];
allocation4 = new byte[1000*1024];
allocation5 = new byte[1000*1024];
allocation6 = new byte[1000*1024];*/
}
}

运行结果:
Heap
PSYoungGen total 76288K, used 65536K [0x000000076b400000, 0x00000007709
00000, 0x00000007c0000000)
eden space 65536K, 100% used [0x000000076b400000,0x000000076f400000,0x0
00000076f400000)
from space 10752K, 0% used [0x000000076fe80000,0x000000076fe80000,0x000
0000770900000)
to space 10752K, 0% used [0x000000076f400000,0x000000076f400000,0x00000 0076fe80000)
ParOldGen total 175104K, used 0K [0x00000006c1c00000, 0x00000006cc70000 0, 0x000000076b400000)
object space 175104K, 0% used [0x00000006c1c00000,0x00000006c1c00000,0x 00000006cc700000)
Metaspace used 3342K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 361K, capacity 388K, committed 512K, reserved 1048576K

我们可以看出eden区内存几乎已经被分配完全(即使程序什么也不做,新生代也会使用至少几M内存)。假如我们再为allocation2分配内存会出现什么情况呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//添加运行JVM参数: ‐XX:+PrintGCDetails 2 publicclassGCTest{
3 public static void main(String[] args) throws InterruptedException {
4 byte[] allocation1, allocation2/*, allocation3, allocation4, allocation, allocation6*/;
allocation1 = new byte[60000*1024];
allocation2 = new byte[8000*1024];
/*allocation3 = new byte[1000*1024];
allocation4 = new byte[1000*1024];
allocation5 = new byte[1000*1024];
allocation6 = new byte[1000*1024];*/
}
}

运行结果:
[GC(AllocationFailure)[PSYoungGen:65253K‐>936K(76288K)]65253K‐>6094 4K(251392K), 0.0279083 secs] [Times: user=0.13 sys=0.02, real=0.03 secs]
Heap
PSYoungGen total 76288K, used 9591K [0x000000076b400000, 0x000000077490
0000, 0x00000007c0000000)
eden space 65536K, 13% used [0x000000076b400000,0x000000076bc73ef8,0x00
0000076f400000)
from space 10752K, 8% used [0x000000076f400000,0x000000076f4ea020,0x000
000076fe80000)
to space 10752K, 0% used [0x0000000773e80000,0x0000000773e80000,0x00000
00774900000)
ParOldGen total 175104K, used 60008K [0x00000006c1c00000, 0x00000006cc7 00000, 0x000000076b400000)
object space 175104K, 34% used [0x00000006c1c00000,0x00000006c569a010,0 x00000006cc700000)
Metaspace used 3342K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 361K, capacity 388K, committed 512K, reserved 1048576K

简单解释一下为什么会出现这种情况: 因为给allocation2分配内存的时候eden区内存几乎已经被分配完了,我们刚刚讲了当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,GC期间虚拟机又发现allocation1无法存入Survior空间,所以只好把新生代的对象提前转移到老年代中去,老年代上的空间足够存放allocation1,所以不会出现Full GC。执行Minor GC后,后面分配的对象如果能够存在eden区的话,还是会在eden区分配内存。可以执行如下代

大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 - XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小 会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集 器下有效。

比如设置JVM参数:-XX:PretenureSizeThreshold=1000000 - XX:+UseSerialGC ,再执行下上面的第一个程序会发现大对象直接进了老年代 为什么要这样呢?
为了避免为大对象分配内存时的复制操作而降低效率。

长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 来设置。

对象动态年龄判断

当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%,那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的。

Minor gc后存活的对象Survivor区放不下

这种情况会把存活的对象部分挪到老年代,部分可能还会放在Survivor区

老年代空间分配担保机制

年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间 如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象) 就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是 否设置了 如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。 如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放 新的对象就会发生”OOM”

当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老 年代可用空间,那么也会触发full gc,full gc完之后如果还是没用空间放minor gc之后的存活对象,则也会发生“OOM”

JVM_GC01

Eden与Survivor区默认8:1:1

大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor去垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可JVM默认有这个参数-XX:+UseAdaptiveSizePolicy,会导致这个比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy

如何判断对象可以被回收

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。

###引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。

1
2
3
4
5
6
7
8
9
10
11
12
publicclassReferenceCountingGc{
Object instance = null;

public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}

可达性分析算法

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象

GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等

JVM_GC02

常见引用类型

java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用

强引用:普通的变量引用

1
public static User user = new User();

软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。

1
public static SoftReference<User> user = new SoftReference<User>(new User());

软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出

弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用

1
public static WeakReference<User> user = new WeakReference<User>(new User());

虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用

finalize()方法最终判定对象是否存活

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们 暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。 标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

  1. 第一次标记并进行一次筛选。 筛选的条件是此对象是否有必要执行finalize()方法。 当对象没有覆盖finalize方法,对象将直接被回收。

  2. 第二次标记 如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次 机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一 个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第 二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本 上它就真的被回收了。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
public classOOMTest{ 2
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
inti=0;
intj=0;
while (true) {
list.add(new User(i++, UUID.randomUUID().toString()));
new User(j‐‐, UUID.randomUUID().toString());
}
}
}

如何判断一个类是无用的类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?类需要同时满足下面3个条件才能算是 “无用的类” :

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何 实例。

  • 加载该类的 ClassLoader 已经被回收。

  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何 地方通过反射访问该类的方法。

JVM 之 GC

JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而Java堆区和方法区则不一样、不一样!(怎么不一样说的朗朗上口),这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。

垃圾收集算法

引用计数法

1 算法分析

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

2 优缺点

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。

3 循环引用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class abc_test {
public static void main(String[] args) {
MyObject object1=new MyObject();
MyObject object2=new MyObject();

object1.object=object2;
object2.object=object1;

object1=null;
object2=null;
}
}
class MyObject{
MyObject object;
}

这段代码是用来验证引用计数算法不能检测出循环引用。最后面两句将object1object2赋值为null,也就是说object1object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

标记清除算法

1 分析

在介绍标记清除算法之前,这里要先提一下可达性分析算法,所谓可达性分析就是用来判断对象是否存活,这个算法的基本思路就是以“GC Roots”(在java中,虚拟机栈中的引用对象,方法区中静态属性引用对象,方法区中常量引用对象,本地方法栈中的JNI都可以最为GC Roots)为起始点,从这个节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时,也就是说从GC Roots到这个对象是不可达的,则这个对象就会被判定为可回收对象。

该算法是将垃圾的回收分为两个阶段,分别为标记和清除,首先是标记,如下图,会采用可达性分析算法,找出可用和不可用的对象,将可用的对象的mark值设置为1,不可用的为0。然后就会进行第二个阶段,也就是清除阶段,这里会把mark为0的对象进行垃圾回收,然后将剩余对象的mark设为0,等待下一次标记

JVM_GC01

2 优缺点

优点:由图中也可以看到该算法解决了引用计数算法中循环引用对象的回收问题

缺点:

  1. 效率较低,在标记和清除阶段都需要遍历所有的对象,而且在GC的时候会短暂的停止应用程序,用户体验较差
  2. 通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的

标记压缩算法

1 分析

标记压缩算法是在标记清除算法上做了改进和优化,标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。

JVM_GC02

2 优缺点

优缺点:优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。

复制算法

1 分析

复制算法就是将没存空间一分为二,存储时只使用其中的一块空间,当进行垃圾回收的时候,找出正在使用的对象,并将这些对象复制到另一块内存空间中,然后将该内存清空,交换两个空间的角色,实现垃圾的回收。

在jvm新生代中,Survivor区就是采用复制算法实现的垃圾回收。

  1. 在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
  2. 紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。
  3. 经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。

GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中

2 优缺点

优点:

1.如果垃圾对象较多的情况下,该算法效率比较高

2.垃圾清理之后,内存不会出现碎片化

缺点:

1.并不适用在垃圾较少的情况下适用,例如老年代中

2.分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低

垃圾收集器

Serial垃圾收集器

新生代单线程收集器(复制算法)

一个主要应用于Y-GC的垃圾回收器,采用串行单线程的方式完成GC任务,其中“Stop The World”简称STW,即垃圾回收的某个阶段会暂停整个应用程序的执行
F-GC的时间相对较长,频繁FGC会严重影响应用程序的性能

JVM_GC03

单线程 Stop-The-World 式

JVM_GC04

特点

  • 单线程
    只会使用一个CPU或一条GC线程进行GC,并且在GC过程中暂停其他所有的工作线程,因此用户的请求或图形化界面会出现卡顿

  • 适合Client模式
    一般客户端应用所需内存较小,不会创建太多的对象,而且堆内存不大,因此GC时间比较短,即使在这段时间停止一切用户线程,也不会感到明显停顿

  • 简单高效
    由于Serial收集器只有一条GC线程,避免了线程切换的开销

  • 采用”复制”算法

测试

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TestGC {
public static void main(String[] args) throws InterruptedException {
List<Object> list = new ArrayList<>();
//实现 不断的产生新的数据,随机的废弃对象
while(true){
int sleep = new Random().nextInt(100);
if(System.currentTimeMillis() % 2 == 0){
list.clear();
}else{
//向List中添加10000个对象
for(int i = 0; i < 10000; i++){
Properties properties = new Properties();
properties.put("key_" + i, "value_" + System.currentTimeMillis());
list.add(properties);
}
}
Thread.sleep(sleep);
}
}
}

参数设置:

1
-XX:+UseSerialGC -XX:+PrintGCDetails -Xms16m -Xmx16m

-XX:+UseSerialGC : 指定年轻代和老年代都是用串行垃圾收集器

-XX:+PrintGCDetails: 打印垃圾回收的详细信息

日志打印信息:

1
2
[GC (Allocation Failure) [DefNew: 4416K->512K(4928K), 0.0000210 secs][Tenured: 7659K->2264K(10944K), 0.0074938 secs] 12075K->2264K(15872K), [Metaspace: 3159K->3159K(1056768K)], 0.0075593 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [Tenured: 10943K->779K(10944K), 0.0061130 secs] 15871K->779K(15872K), [Metaspace: 3159K->3159K(1056768K)], 0.0061766 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

GC日志解读:

  • DefNew : 表示使用的是串行垃圾收集器
  • 4416K->512K(4928K): 表示年轻代GC前,占用4416kb内存,GC后占有4416k内存,总大小4928k
  • 0.0000210 secs: 表示GC所用的时间,单位为毫秒
  • 7659K->2264K(10944K): 表示,GC前,堆内存占有7659K,GC后,占有2264K,总大小为10944K
  • Full GC: 表示内存空间全部进行GC

ParNew垃圾收集器

新生代收集器 (停止-复制算法) 

1
2
3
4
5
 # 参数
-XX:+UseParNewGC -XX:+PrintGCDetails -Xms16m -Xmx16m

# 打印出的信息
[GC (Allocation Failure) [ParNew: 4416K->512K(4928K), 0.0022062 secs] 4416K->1345K(15872K), 0.0022293 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

由以上可知,parNew使用的是ParNew收集器,其它信息和串行收集器一致

ParallelGC 垃圾收集器

ParallelGC收集器工作机制和ParNew收集器一样,只是在此基础上,新增了两个和系统吞吐量相关的参数,使其使用起来更加的灵活和高效

相关参数:

  • -XX:+UseParallelGC

    • 年轻代使用的ParallelGC垃圾回收器,老年代使用SerialGC回收器
  • -XX:+UseParallelOldGC

    • 年轻代使用的ParallelGC垃圾回收器,老年代使用ParallelOldGC回收器
  • -XX:MaxGCPauseMillis

    • 设置最大的垃圾收集时的停顿时间,单位为毫秒
    • 需要注意的是,ParallelGC为了达到设置的停顿时间,可能会调整堆大小或其他的参数,如果堆的大小设置的较小,就会导致GC工作变得很频繁,反而可能会影响到性能
    • 该参数使用需谨慎
  • -XX:GCTimeRatio

    • 设置垃圾回收时间占程序时间的百分比,公式为1/(1 + n)
    • 它的值为0-1之间的数字,默认值为99,也就是垃圾回收时间不能超过%
  • -XX:UseAdaptiveSizePolicy

    • 自适应GC模式,垃圾回收器将自动调整新生代,老年代等参数,达到吞吐量,堆大小,停顿时间之间的平衡
    • 一般用于,手动调整参数比较困难的场景,让收集器自动进行整理

测试

1
2
3
4
5
6
# 参数
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -Xms16m -Xmx16m

# 打印日志信息
[GC (Allocation Failure) [PSYoungGen: 4096K->512K(4608K)] 4096K->1207K(15872K), 0.0012154 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 483K->0K(2560K)] [ParOldGen: 9879K->3844K(11264K)] 10362K->3844K(13824K), [Metaspace: 3152K->3152K(1056768K)], 0.0104722 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]

由以上信息可以看出,年轻代和老年代都使用了ParallelGC垃圾回收器

CMS垃圾收集器

Concurrent Mark Sweep Collector : 低延迟为先!

回收停顿时间比较短、目前比较常用的垃圾回收器。它通过初始标记(InitialMark)、并发标记(Concurrent Mark)、重新标记( Remark)、并发清除( Concurrent Sweep )四个步骤完成垃圾回收工作
由于CMS采用的是“标记-清除算法”,因此戸生大量的空间碎片。为了解决这个问题,CMS可以通过配置

-XX:+UseCMSCompactAtFullCollection

参数,强制JVM在FGC完成后対老年代迸行圧縮,执行一次空间碎片整理,但是空间碎片整理阶段也会引发STW。为了减少STW次数,CMS还可以通过配置

-XX:+CMSFullGCsBeforeCompaction=n

垃圾回收过程

  • 初始标记 (Initial Mark): “Stop The World”
    停止一切用户线程,仅使用一条初始标记线程对所有与GC Roots直接相关联的 老年代对象进行标记,速度很快

  • 并发标记 (Concurrent Marking Phase)
    使用多条并发标记线程并行执行,并与用户线程并发执行.此过程进行可达性分析,标记所有这些对象可达的存货对象,速度很慢

  • 重新标记 ( Remark): “Stop The World”
    因为并发标记时有用户线程在执行,标记结果可能有变化
    停止一切用户线程,并使用多条重新标记线程并行执行,重新遍历所有在并发标记期间有变化的对象进行最后的标记.这个过程的运行时间介于初始标记和并发标记之间

  • 并发清除 (Concurrent Sweeping)
    只使用一条并发清除线程,和用户线程们并发执行,清除刚才标记的对象
    这个过程非常耗时

JVM_GC05

缺点:

  • 吞吐量低
    由于CMS在GC过程用户线程和GC线程并行,从而有线程切换的额外开销
    因此CPU吞吐量就不如在GC过程中停止一切用户线程的方式来的高

  • 无法处理浮动垃圾,导致频繁Full GC
    由于垃圾清除过程中,用户线程和GC线程并发执行,也就是用户线程仍在执行,那么在执行过程中会产生垃圾,这些垃圾称为”浮动垃圾”
    如果CMS在GC过程中,用户线程需要在老年代中分配内存时发现空间不足,就需再次发起Full GC,而此时CMS正在进行清除工作,因此此时只能由Serial Old临时对老年代进行一次Full GC

  • 使用”标记-清除”算法产生碎片空间
    由于CMS使用了”标记-清除”算法, 因此清除之后会产生大量的碎片空间,不利于空间利用率.不过CMS提供了应对策略:

    • 开启-XX:+UseCMSCompactAtFullCollection开启该参数后,每次FullGC完成后都会进行一次内存压缩整理,将零散在各处的对象整理到一块儿.但每次都整理效率不高,因此提供了以下参数.
    • 设置参数-XX:CMSFullGCsBeforeCompaction本参数告诉CMS,经过了N次Full GC过后再进行一次内存整理
    • -XX:+UseConcMarkSweepGC:启用cms
    • -XX:ConcGCThreads:并发的GC线程数
    • -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
    • -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
    • -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在remark阶段

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 设置启动参数
-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xms16m -Xmx16m

# 运行日志
[GC (Allocation Failure) [ParNew: 4416K->512K(4928K), 0.0235669 secs] 4416K->1344K(15872K), 0.0236191 secs] [Times: user=0.02 sys=0.01, real=0.02 secs]
# 第一步 初始标记
[GC (CMS Initial Mark) [1 CMS-initial-mark: 7577K(10944K)] 8177K(15872K), 0.0010702 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
# 第二步 并发标记
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.004/0.004 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
# 第三步 预处理
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
# 第四步 重新标记
[GC (CMS Final Remark) [YG occupancy: 2098 K (4928 K)][Rescan (parallel) , 0.0004890 secs][weak refs processing, 0.0000713 secs][class unloading, 0.0002386 secs][scrub symbol table, 0.0002982 secs][scrub string table, 0.0001423 secs][1 CMS-remark: 7577K(10944K)] 9676K(15872K), 0.0012986 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
# 第五步 并发清理
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.004/0.004 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
# 第六步 重置
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

G1垃圾收集器

G1的设计原则就是简单可行的性能调优

G1将新生代,老年代的物理空间划分取消了。

取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。

JVM_GC06

G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。 一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以 用参数”-XX:G1HeapRegionSize”手动指定Region大小,但是推荐默认的计算方式。 G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集 合。 默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200MB左右的内存, 对应大概是100个Region,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统 运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以 通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和Survivor对应的region也跟之前 一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应 100个。 一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是 说Region的区域功能可能会动态变化。

G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象 的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的 Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按 照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且 一个大对象如果太大,可能会横跨多个Region来存放。

Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。
Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。

对象分配策略

说起大对象的分配,我们不得不谈谈对象的分配策略。它分为3个阶段:

  1. TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区
  2. Eden区中分配
  3. Humongous区分配

TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。

对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。

下面我们将分别介绍一下Young GC和Mixed GC,两种都是Stop The World(STW)的。

G1 Young GC

Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

JVM_GC07

JVM_GC08

Remembered Set

这时,我们需要考虑一个问题,如果仅仅GC 新生代对象,我们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。

JVM_GC09

在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。

但在G1中,并没有使用point-out,这是由于一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。

需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。

YoungGC

YoungGC并不是说现有的Eden区放满了就会马上触发,而且G1会计算下现在Eden区回收大 概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代 的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时 间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC

Young GC 阶段:

  • 阶段1:根扫描
    静态和本地对象被扫描

  • 阶段2:更新RS
    处理dirty card队列更新RS

  • 阶段3:处理RS
    检测从年轻代指向年老代的对象

  • 阶段4:对象拷贝
    拷贝存活的对象到survivor/old区域

  • 阶段5:处理引用队列

    软引用,弱引用,虚引用处理

Mix GC

Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。

老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercen)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC

出发条件:由参数-XX:InitiatingHeapOccupancyPercent=n 决定,默认:45%, 该参数的意思是:当老年代大小占整个堆大小百分比到达该阀值时触发。

它的GC步骤分2步:

  1. 全局并发标记(global concurrent marking)
  2. 拷贝存活对象(evacuation)

全局并发标记

在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:

  • 初始标记(initial mark,STW)
    在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。

  • 根区域扫描(root region scan)
    G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。

  • 并发标记(Concurrent Marking)
    G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断

  • 最终标记(Remark,STW)
    该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。

  • 清除垃圾(Cleanup,STW)

    在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。

G1收集相关参数

  • -XX:+UseG1GC

    • 使用G1垃圾收集器
  • -XX:MaxGCPauseMillis

    • 设置期望值达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是200毫秒
  • -XX:G1HeapRegionSize=n

    • 置的 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。
    • 默认是堆的1/2000
  • -XX:ParallelGCThreads=n

    • 设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为 8
  • -XX:ConcGCThreads=n

    • 设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。
  • -XX:InitiatingHeapOccupancyPercent=n

    • 设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
-XX:+UseG1GC -Xmx32m -XX:MaxGCPauseMillis=200 -XX:+PrintGCDetails

# 日志
[GC pause (G1 Evacuation Pause) (young), 0.0058915 secs]
[Parallel Time: 5.4 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 609.3, Avg: 609.4, Max: 609.4, Diff: 0.0]
# 扫描根节点
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.3, Max: 0.5, Diff: 0.4, Sum: 1.0]
# 更新RS区域所消耗的时间
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
# 对象拷贝
[Object Copy (ms): Min: 4.1, Avg: 4.1, Max: 4.3, Diff: 0.2, Sum: 16.6]
[Termination (ms): Min: 0.0, Avg: 0.4, Max: 0.6, Diff: 0.6, Sum: 1.6]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 4.8, Avg: 4.8, Max: 4.9, Diff: 0.0, Sum: 19.4]
[GC Worker End (ms): Min: 614.2, Avg: 614.2, Max: 614.2, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms] # 清空CardTable
[Other: 0.4 ms]
[Choose CSet: 0.0 ms] # 选取CSet
[Ref Proc: 0.3 ms] # 弱引用、软引用的处理耗时
[Ref Enq: 0.0 ms] # 弱引用、软引用的入队耗时
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms] # 大对象区域注册耗时
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 14.0M(14.0M)->0.0B(16.0M) Survivors: 0.0B->2048.0K Heap: 14.0M(32.0M)->3915.0K(32.0M)]
[Times: user=0.01 sys=0.00, real=0.00 secs]

Full GC

Full GC 停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。

GCeasy分析GC日志分析示例

https://gceasy.io

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author : chenqingsong
* @date : 2020-01-31 13:26
* -Xms10M -Xmx10M -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/Users/admin/Desktop/jvm11.log
*/
public class OOMTest {
public static List<Object> list = new ArrayList<>();

public static void main(String[] args) {
List<Object> list = new ArrayList<>();
int i=0;
int j=0;
while (true) {
list.add(new User(i++, UUID.randomUUID().toString()));
new User(j--, UUID.randomUUID().toString());
}
}
}

启动会生成gc日志,使用gceasy工具查看

jvm内存信息:

JVM_GC06

gc文件中的gc信息统计

JVM_GC07

JVM运行情况预估

用 jstat gc -pid 命令可以计算出如下一些关键数据,有了这些数据就可以采用之前介绍过的优化思路,先给自己的系统设置一些初始性的 JVM参数,比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。

年轻代对象增长的速率
可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对 象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不 同的时间分别估算不同情况下对象增长速率。

Young GC的触发频率和每次耗时
知道年轻代对象增长速率我们就能推根据eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC 公式算出,根据结果我们大概就能知道系统大概多久会因为Young GC的执行而卡顿多久。

每次Young GC后有多少对象存活和进入老年代
这个因为之前已经大概知道Young GC的频率,假设是每5分钟一次,那么可以执行命令 jstat -gc pid 300000 10 ,观察每次结果eden, survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次 Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率。

Full GC的触发频率和每次耗时
知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。

优化思路其实简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年 代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。

6 Tomcat 优化

1 jmeter对Tomcat测试

启动jmeter, sh jmeter 不修改任何配置启动1000个线程访问十次结果信息

Tomcat_01

2 禁用AJP服务

注释server.xml中的AJP配置

Tomcat_02

3 设置线程池

Executor 参数解析:

  • name=”tomcatThreadPool” –线程池名
  • namePrefix=”catalina-exec-“ –线程名称前缀 namePrefix+threaNumber
  • maxThreads=”1000” –池中最大线程数
  • minSpareThreads=”100” –活跃线程数 会一直存在
  • maxIdleTime=”60000” –线程空闲时间,超过该时间,线程会被销毁 ms
  • maxQueueSize=”Integer.MAX_VALUE” –被执行前线程的排队数目(队列最大等待数)
  • prestartminSpareThreads=”false” –启动线程池时,是否启用minSpareThreads部分线程
  • threadPriority=”5” –线程池中线程优先级 1~10
  • className=”org.apache.catalina.core.StandardThreadExecutor” –线程实现类 自定义线程需时间 org.apache.catalina.Executor类

线程池配置:

1
2
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="4"/>

启用线程池: executor=”tomcatThreadPool”

1
2
3
4
5
6
7
<Connector executor="tomcatThreadPool"

port="8080" protocol="HTTP/1.1"

connectionTimeout="20000"

redirectPort="8443" />

Tomcat_03

4 设置nio2的运行模式

protocol=”org.apache.coyote.http11.Http11Nio2Protocol”

1
2
3
4
<Connector executor="tomcatThreadPool" port="8080" 
protocol="org.apache.coyote.http11.Http11Nio2Protocol"
connectionTimeout="20000"
redirectPort="8443" />

5 调整JVM参数进行优化

修改bin 目录下的catalina.sh
添加:JAVA_OPTS=’-Xms512m -Xmx1024m ‘。。。