0%

加密与解密

AES

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Scanner;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

/*
* AES对称加密和解密
*/
public class SymmetricEncoder {
/*
* 加密
* 1.构造密钥生成器
* 2.根据ecnodeRules规则初始化密钥生成器
* 3.产生密钥
* 4.创建和初始化密码器
* 5.内容加密
* 6.返回字符串
*/
public static String AESEncode(String encodeRules,String content){
try {
//1.构造密钥生成器,指定为AES算法,不区分大小写
KeyGenerator keygen=KeyGenerator.getInstance("AES");
//2.根据ecnodeRules规则初始化密钥生成器
//生成一个128位的随机源,根据传入的字节数组
keygen.init(128, new SecureRandom(encodeRules.getBytes()));
//3.产生原始对称密钥
SecretKey original_key=keygen.generateKey();
//4.获得原始对称密钥的字节数组
byte [] raw=original_key.getEncoded();
//5.根据字节数组生成AES密钥
SecretKey key=new SecretKeySpec(raw, "AES");
//6.根据指定算法AES自成密码器
Cipher cipher=Cipher.getInstance("AES");
//7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密解密(Decrypt_mode)操作,第二个参数为使用的KEY
cipher.init(Cipher.ENCRYPT_MODE, key);
//8.获取加密内容的字节数组(这里要设置为utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码
byte [] byte_encode=content.getBytes("utf-8");
//9.根据密码器的初始化方式--加密:将数据加密
byte [] byte_AES=cipher.doFinal(byte_encode);
//10.将加密后的数据转换为字符串
//这里用Base64Encoder中会找不到包
//解决办法:
//在项目的Build path中先移除JRE System Library,再添加库JRE System Library,重新编译后就一切正常了。
String AES_encode=new String(new BASE64Encoder().encode(byte_AES));
//11.将字符串返回
return AES_encode;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//如果有错就返加nulll
return null;
}
/*
* 解密
* 解密过程:
* 1.同加密1-4步
* 2.将加密后的字符串反纺成byte[]数组
* 3.将加密内容解密
*/
public static String AESDncode(String encodeRules,String content){
try {
//1.构造密钥生成器,指定为AES算法,不区分大小写
KeyGenerator keygen=KeyGenerator.getInstance("AES");
//2.根据ecnodeRules规则初始化密钥生成器
//生成一个128位的随机源,根据传入的字节数组
keygen.init(128, new SecureRandom(encodeRules.getBytes()));
//3.产生原始对称密钥
SecretKey original_key=keygen.generateKey();
//4.获得原始对称密钥的字节数组
byte [] raw=original_key.getEncoded();
//5.根据字节数组生成AES密钥
SecretKey key=new SecretKeySpec(raw, "AES");
//6.根据指定算法AES自成密码器
Cipher cipher=Cipher.getInstance("AES");
//7.初始化密码器,第一个参数为加密(Encrypt_mode)或者解密(Decrypt_mode)操作,第二个参数为使用的KEY
cipher.init(Cipher.DECRYPT_MODE, key);
//8.将加密并编码后的内容解码成字节数组
byte [] byte_content= new BASE64Decoder().decodeBuffer(content);
/*
* 解密
*/
byte [] byte_decode=cipher.doFinal(byte_content);
String AES_decode=new String(byte_decode,"utf-8");
return AES_decode;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
//如果有错就返加nulll
return null;
}
public static void main(String[] args) {
SymmetricEncoder se=new SymmetricEncoder();
Scanner scanner=new Scanner(System.in);
/*
* 加密
*/
System.out.println("使用AES对称加密,请输入加密的规则");
String encodeRules=scanner.next();
System.out.println("请输入要加密的内容:");
String content = scanner.next();
System.out.println("根据输入的规则"+encodeRules+"加密后的密文是:"+se.AESEncode(encodeRules, content));
/*
* 解密
*/
System.out.println("使用AES对称解密,请输入加密的规则:(须与加密相同)");
encodeRules=scanner.next();
System.out.println("请输入要解密的内容(密文):");
content = scanner.next();
System.out.println("根据输入的规则"+encodeRules+"解密后的明文是:"+se.AESDncode(encodeRules, content));
}
}

Apache commons codec

1
2
3
4
5
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//1、MD5加密
String md5Str = DigestUtils.md5Hex(str);
System.out.println("MD5-->" + md5Str);

//SHA1加密
String sha1Str = DigestUtils.sha1Hex(str);
System.out.println("SHA1-->" + sha1Str);

//Base64加密
String base64Str = Base64.encodeBase64String(str.getBytes());
System.out.println("base64加密-->" + base64Str);

//Base64解密
String base64DecodeStr = new String(Base64.decodeBase64(base64Str));
System.out.println("base64解密-->" + base64DecodeStr);

typesafe.config

Typesafe Config介绍

Why Typesafe Config

THE config library 非常好用的API,用过之后不会再想使用其他配置工具

  • 纯java实现,无任何依赖
  • 支持各种格式配置的融合: Java properties, JSON, and a human-friendly JSON superset
  • 可以通过文件、urls、classpath加载配置
  • 支持多层嵌套的配置方式:树形配置
  • 识别Java system properties, 如java -Dmyapp.foo.bar=10
  • 可以转换时间,大小等单位。如 “512k”、”10 seconds”
  • 类型转换,比如yes可以转换为true,数字之间也可以在内部做转换
  • JSON superset features:
    • comments
    • includes
    • substitutions (“foo” : ${bar}, “foo” : Hello ${who})
    • properties-like notation (a.b=c)
    • less noisy, more lenient syntax
    • substitute environment variables (logdir=${HOME}/logs)
  • 基于不可变对象,不用担心多线程问题

获取

使用gradle

1
2
compile 'com.typesafe:config:1.2.1'

HOCON (Human-Optimized Config Object Notation)

config使用的是HOCON的文件格式,这种文件格式类似于json,很灵活但没有歧义,能引用,能替换,能注释,能兼容老的properties等格式

The following features are desirable, to support human usage:

  • less noisy / less pedantic syntax
  • ability to refer to another part of the configuration (set a value to another value)
  • import/include another configuration file into the current file
  • a mapping to a flat properties list such as Java’s system properties
  • ability to get values from environment variables
  • ability to write comments

约束:utf8编码

注释

1
2
3
4
5
# 单行注释
a = 1 // 单行注释
b = 2 # 单行注释
c = [3, # 行内注释 # 4]

赋值与嵌套

普通赋值
1
2
3
a : 1
a = 1

(对象)嵌套赋值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"foo" : { "a" : 42 },
"foo" : { "b" : 43 }
}

{
"foo" : { "a" : 42, "b" : 43 }
}

foo.bar : 42
foo { bar : 42 }

foo.bar.baz : 42
foo { bar { baz : 42 } }

a.x : 42, a.y : 43
a { x : 42, y : 43 }

多种格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"foo" : {
"bar" : 10,
"baz" : 12
}
}

foo : {
bar : 10,
baz : 12
}

foo {
bar = 10
baz = 12
}

foo.bar=10
foo.baz=12

foo.bar=10, foo.baz=12

多行赋值
1
2
3
logo = """hello
world"""

数组与对象合并
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// one object
a : { b : 1, c : 2 }
// two objects that are merged via concatenation rules
a : { b : 1 } { c : 2 }
// two fields that are merged
a : { b : 1 }
a : { c : 2 }

// one array
a : [ 1, 2, 3, 4 ]
// two arrays that are concatenated
a : [ 1, 2 ] [ 3, 4 ]
// a later definition referring to an earlier
// (see "self-referential substitutions" below)
a : [ 1, 2 ]
a : ${a} [ 3, 4 ]

path = [ /bin ]
path = ${path} [ /usr/bin ]

对象的继承
1
2
3
data-center-generic = { cluster-size = 6 }
data-center-east = ${data-center-generic} { name = "east" }

合并还是覆盖
1
2
3
4
5
6
7
8
9
10
{ a : { x : 1 } } (first priority)
{ a : 42 } (fallback)
{ a : { y : 2 } } (another fallback)
# result in { a : { x : 1 } }

{ a : { x : 1 } } (first priority)
{ a : { y : 2 } } (fallback)
{ a : 42 } (another fallback)
# result in { a : { x : 1, y : 2 } }

替换

使用这样的语法 ${pathexpression} or ${?pathexpression}

1
2
3
animal.favorite = dog
key : ${animal.favorite} is my favorite animal

pathexpression是绝对路径,替换是config对象构建的最后一步。

Include

1
2
3
4
5
6
include "foo"

include "foo.properties"
include "foo.json"
include "foo.conf"

内置转换API

自动转换,时间单位(ns、ms、s、m、h etc),文件大小(kB、MB etc)

文件载入顺序

依次载入合并

  • 引用 jar 包中的 reference.conf 库引用配置
  • 引用 jar 包中的 application.{conf,json,properties} 应用配置
  • 本地 reference.conf 库引用配置
  • 本地 application.{conf,json,properties} 应用配置
  • system properties

在java代码中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 配置内容
foo=42
dev.foo=57
prod.foo=10

import com.typesafe.config.ConfigFactory

// 载入根配置
Config conf = ConfigFactory.load();
int foo1 = conf.getInt("dev.foo");

// 载入某个对象
Config prod = conf.getConfig("prod");
int foo2 = foo.getInt("foo");

Config devConfig = conf
.getConfig("dev")
.withFallback(originalConfig)

// handle default
// boolean getBoolean(String path, boolean fallback)

调试

使用下面的方法,会把最终所有的配置信息打印出来

1
logger.debug(myConfig.root().render())

输出结果中包含所有载入的配置,如果使用了conf文件,会将对应文件的行信息也打印出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
# system properties
"awt" : {
# system properties
"toolkit" : "sun.awt.windows.WToolkit"
},
# complex1.conf: 2
# these are our own config values defined by the app
"complex-app" : {
# complex1.conf: 3
"something" : "This value comes from complex-app's complex1.conf"
},
...
}

源代码

Docker构建Java调试环境

网易镜像仓库

Docker动态给容器Container暴露端口

https://blog.csdn.net/lsziri/article/details/69396990

Docker container

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
docker pull hub.c.163.com/public/centos:6.7-tools
docker tag hub.c.163.com/public/centos:6.7-tools centos


# 增加参数解决不能获取ptrace的问题
# https://blog.csdn.net/russle/article/details/99708261
# 这种不可用 docker run --name centos-one --cap-add=SYS_PTRACE -d -P centos
docker run --cap-add=SYS_PTRACE --security-opt seccomp:unconfined --name centos-two -d -P centos
➜ ~ docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3ae403bf463c centos "/usr/bin/supervisord" About a minute ago Up About a minute 0.0.0.0:32772->22/tcp centos-one

# 进入容器
➜ ~ docker exec -it centos-one su deploy



Java环境

1
2
3
# 安装全部 yum install -y java-1.8.0-openjdk*
# 只安装需要的
yum install -y java-1.8.0-openjdk.x86_64 java-1.8.0-openjdk-devel.x86_64

install git

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
yum install gcc gcc-c++ autoconf make automake -y
yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel -y
yum install perl docbook2X texinfo sgml2xml openjade perl-ExtUtils-MakeMaker -y
yum install asciidoc xmlto cpio expat-devel gettext-devel -y
yum install perl-ExtUtils-MakeMaker

yum install -y tk zlib-devel openssl-devel perl cpio expat-devel gettext-devel asciidoc xmlto autoconf gcc
wget https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.9.5.tar.gz

tar zxvf git-2.9.5.tar.gz
cd git-2.9.5

make configure
./configure --prefix=/usr/local/git --with-iconv=/usr/local/libiconv
# 配置安装路径
make all doc
# 编译
make install install-doc install-html
# 安装

修改环境变量

# echo -e "# git\nexport PATH=/usr/local/git/bin:\$PATH"> /etc/profile.d/git.sh
# cat /etc/profile.d/git.sh
\# git // 文件内容
export PATH=/usr/local/git/bin:$PATH
# source /etc/profile

安装oh-my-zsh

1
2
3
4
5
6
7
8
9
10
11
12
13


yum install -y zsh
chsh -s /bin/zsh



sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"


git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions


编辑~/.zshrc文件

找到plugins=(git)这一行,然后再添加autosuggestions,最后为:

1
plugins=(git zsh-autosuggestions)
1
docker container cp target/demo-0.0.1-SNAPSHOT.jar centos-one:/home/deploy/

增加端口,做远程debug

1
2
3
4
5
6
docker commit 95c6d4eed5f0 jdk8-debug

docker run --cap-add=SYS_PTRACE --security-opt seccomp:unconfined --name centos-jdk8-debug -d -p 8000:8000 -p 8001:8001 -p 8002:8002 -P jdk8-debug


docker exec -it centos-jdk8-debug zsh

调优

TODO

jhat
vmstat

测试样例

新docker container,全新的系统,以纯净的系统Centos为例

安装java

1
2
3
4
5
6
# 安装java8
[root@centos ~]# yum install -y java-1.8.0-openjdk*
# 创建deploy用户,用于折腾
[root@centos ~]# useradd deploy
[root@centos ~]# su deploy
[root@centos ~]# cd

创建工作目录

1
2
3
# 创建并切换到工作目录
[deploy@centos ~]$ mkdir -p lk-optimization
[deploy@centos ~]$ cd !$

创建package,用于存放java源文件和编译后的class文件

1
2
[deploy@centos ~]$ mkdir -p src/com/lk/optimization/demo target
[deploy@centos ~]$ vim !$/DemoTest.java

新建Java文件

// Thread thread = Thread.currentThread();
// System.out.println(thread.getName());
// thread.join();
System.out.println(“join over”);

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
package com.lk.optimization.demo;
import java.util.List;
import java.util.ArrayList;
public class DemoTest {
public static void main(String[] atgs) throws InterruptedException {
new Handler().doHandle();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.out.println(this.getName() + " is shutting down ....");
}
});
}
}

class Handler {
public void doHandle() {
for (int i = 0; i < 10; i++) {
String workerName="worker-"+(i+1);
new Thread(new Worker(workerName), "handler-" + i).start();
}
}
}


class Worker implements Runnable {
private String workerName;
private List<String> list;
public Worker(String workerName){
this.workerName=workerName;
this.list=new ArrayList<>(1000);
}
@Override
public void run() {
System.out.println(workerName+" start!");
while (true) {
for(int i=0;i<100;i++){
list.add(""+i);
}
if(list.size()>1000){
list.clear();
}
try {
Thread.sleep(100);
System.out.println(workerName+": sleep over");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[deploy@centos ~]$ find src -name "*.java" | xargs javac -d target

[deploy@centos ~]$ tree
.
├── src
│ └── com
│ └── lk
│ └── optimization
│ └── demo
│ └── DemoTest.java
└── target
└── com
└── lk
└── optimization
└── demo
├── DemoTest$1.class
├── DemoTest.class
├── Handler.class
└── Worker.class

10 directories, 5 files

运行

1
2
[deploy@centos ~]$ nohup java -cp target  -Xcomp -XX:MaxNewSize=1000000  -XX:MaxHeapSize=2000000 com.lk.optimization.demo.DemoTest &
main

ps 查看进程和线程信息

ps命令选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
********* simple selection *********  ********* selection by list *********
-A all processes -C by command name
-N negate selection -G by real group ID (supports names)
-a all w/ tty except session leaders -U by real user ID (supports names)
-d all except session leaders -g by session OR by effective group name
-e all processes -p by process ID
T all processes on this terminal -s processes in the sessions given
a all w/ tty, including other users -t by tty
g OBSOLETE -- DO NOT USE -u by effective user ID (supports names)
r only running processes U processes for specified users
x processes w/o controlling ttys t by tty
*********** output format ********** *********** long options ***********
-o,o user-defined -f full --Group --User --pid --cols --ppid
-j,j job control s signal --group --user --sid --rows --info
-O,O preloaded -o v virtual memory --cumulative --format --deselect
-l,l long u user-oriented --sort --tty --forest --version
-F extra full X registers --heading --no-heading --context
********* misc options *********
-V,V show version L list format codes f ASCII art forest
-m,m,-L,-T,H threads S children in sum -y change -l format
-M,Z security data c true command name -c scheduling class
-w,w wide output n numeric WCHAN,UID -H process hierarchy

root用户查看进程信息

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@centos ~]# ps -ef  | grep -v ps
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 13:16 ? 00:00:00 /usr/sbin/sshd -D
root 14 0 0 13:23 pts/0 00:00:00 /bin/bash
root 421 1 0 13:58 ? 00:00:00 sshd: root@pts/1
root 423 421 0 13:58 pts/1 00:00:00 -bash
root 461 1 0 14:15 ? 00:00:00 sshd: root@pts/2
root 463 461 0 14:15 pts/2 00:00:00 -bash
root 519 1 0 14:16 ? 00:00:00 sshd: root@pts/3
root 521 519 0 14:16 pts/3 00:00:00 -bash
root 785 423 0 14:59 pts/1 00:00:00 su deploy
deploy 786 785 0 14:59 pts/1 00:00:00 bash
deploy 1129 786 0 15:32 pts/1 00:00:02 java -cp target com.lk.optimization.demo.DemoTest

ps 命令

  • -e 显示所有进程信息
  • -f 显示所有的字段
  • -l long format
  • -L 显示NLWP (number of threads) and LWP (thread ID) 显示线程信息
  • -p 只显示指定的PID

字段的解释可以在man页的STANDARD FORMAT SPECIFIERS中查找

默认情况下,ps显示与当前用户相同EUID以及调用了同一个终端的进程。

  • PID是进程ID
  • TTY为与进程关联的终端名称
  • TIME为以[dd-]hh:mm:ss格式显示的CPU时间
  • CMD为执行的名称, 默认不排序
  • PPID为父进程ID,根据ID可以找到进程的调用关系

样例中的进程信息:

进程id 解释
0 0号进程是根进程
1 通过sshd发起的ssh连接
421 ssh连接,用户为root,终端为pts/1,子进程全部这个终端
423 bash shell
785 切换deploy账户
786 deploy账户的bash shell
1129 java启动进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@centos ~]# ps -flL -p 1129
F S UID PID PPID LWP C NLWP PRI NI ADDR SZ WCHAN STIME TTY TIME CMD
0 S deploy 1129 786 1129 0 27 80 0 - 934507 - 15:32 pts/1 00:00:00 java -cp target com.lk.optimization.demo.DemoTest
5 S deploy 1129 786 1130 0 27 80 0 - 934507 - 15:32 pts/1 00:00:00 java -cp target com.lk.optimization.demo.DemoTest
1 S deploy 1129 786 1131 0 27 80 0 - 934507 - 15:32 pts/1 00:00:00 java -cp target com.lk.optimization.demo.DemoTest
1 S deploy 1129 786 1132 0 27 80 0 - 934507 - 15:32 pts/1 00:00:00 java -cp target com.lk.optimization.demo.DemoTest
1 S deploy 1129 786 1133 0 27 80 0 - 934507 - 15:32 pts/1 00:00:00 java -cp target com.lk.optimization.demo.DemoTest
1 S deploy 1129 786 1134 0 27 80 0 - 934507 - 15:32 pts/1 00:00:00 java -cp target com.lk.optimization.demo.DemoTest
1 S deploy 1129 786 1135 0 27 80 0 - 934507 - 15:32 pts/1 00:00:00 java -cp target com.lk.optimization.demo.DemoTest
1 S deploy 1129 786 1136 0 27 80 0 - 934507 - 15:32 pts/1 00:00:00 java -cp target com.lk.optimization.demo.DemoTest
1 S deploy 1129 786 1137 0 27 80 0 - 934507 - 15:32 pts/1 00:00:00 java -cp target com.lk.optimization.demo.DemoTest
1 S deploy 1129 786 1138 0 27 80 0 - 934507 - 15:32 pts/1 00:00:00 java -cp target com.lk.optimization.demo.DemoTest
1 S deploy 1129 786 1139 0 27 80 0 - 934507 - 15:32 pts/1 00:00:00 java -cp target com.lk.optimization.demo.DemoTest
1 S deploy 1129 786 1140 0 27 80 0 - 934507 - 15:32 pts/1 00:00:00 java -cp target com.lk.optimization.demo.DemoTest

Process Format

  1. F PROCESS FLAGS 1 forked但没有执行 5超级用户权限
  2. S PROCESS STATE CODES
    D Uninterruptible sleep (usually IO)
    R Running or runnable (on run queue)
    S Interruptible sleep (waiting for an event to complete)
    T Stopped, either by a job control signal or because it is being traced.
    W paging (not valid since the 2.6.xx kernel)
    X dead (should never be seen)
    Z Defunct (“zombie”) process, terminated but not reaped by its parent.
  3. lwp (light weight process, or thread) ID 线程ID
  4. C processor utilization. Currently, this is the integer value of the percent usage over the lifetime of the process.
  5. NLWP 轻量级进程的个数
  6. ni NI nice value. This ranges from 19 (nicest) to -20 (not nice to others)
  7. size SZ approximate amount of swap space that would be required if the process were to dirty all writable pages and then be swapped out. This number is very rough!
  8. wchan WCHAN name of the kernel function in which the process is sleeping, a “-“ if the process is running, or a “*” if the process is multi-threaded and ps is not displaying threads.

线程信息

线程线程的选项:

  • H Show threads as if they were processes 显示正在处理的线程
  • -L Show threads, possibly with LWP and NLWP columns 显示线程
  • -T Show threads, possibly with SPID column 显示线程
  • m Show threads after processes
  • -m Show threads after processes

通过上面的例子,可以看出

线程id是1129~1154, 共27个线程
实际handler线程和main线程加起来是11个,为什么是27个线程呢

top 查看资源

以线程模式查看下进程31951的所有线程情况

1
top -Hp 31951

top-thread

1
2
3
4
5
6
-H : Threads toggle
Starts top with the last remembered ’H’ state reversed. When this toggle is On, all individual threads will be displayed. Otherwise, top displays a summation of all threads in a process. 当此开关打开时,将显示所有单个线程。否则,top显示进程中所有线程的总和。

-p : Monitor PIDs as: -pN1 -pN2 ... or -pN1, N2 [,...]
Monitor only processes with specified process IDs. This option can be given up to 20 times, or you can provide a comma delimited list with up to 20 pids. Co-mingling both approaches is permitted. This is a command-line option only. And should you wish to return to normal operation, it is not necessary to quit and and restart top -- just issue the ’=’ interactive command.
打开top后,= 可以切换回全部进程

jstack Java线程栈信息

jstack中的线程id为16进制,需要将从top或ps获取的线程id转换为16进制,

1
printf %x <tid>

注意:

  1. jstack必须和运行的JVM进程是同一个用户
1
2
3
4
5
Options:
-F to force a thread dump. Use when jstack <pid> does not respond (process is hung)
-m to print both java and native frames (mixed mode)
-l long listing. Prints additional information about locks
-h or -help to print this help message

虚拟机信息flag

打印虚拟机所有的参数

java 命令文档

1
2
3
4
5
6
7
8
9
java -XX:+PrintFlagsFinal -version | grep :
java -XX:+PrintFlagsInitial -version


java -XX:+PrintFlagsFinal -version | grep -v :=


The -XX:+PrintFlagsFinal (emphasis on "Final") option displays what options HotSpot ended up using for running Java code while
-XX:+PrintFlagsInitial (emphasis on "Initial") displays what options were provided to HotSpot initially, before HotSpot has made its own tweaks.

jinfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
jinfo -flags $PID

Attaching to process ID 29893, please wait...
Debugger attached successfully.
Server compiler detected. # 服务器版本的编译器
JVM version is 25.232-b09 # jvm版本
Non-default VM flags:
-XX:CICompilerCount=2
-XX:InitialHeapSize=33554432 # 初始堆大小 33M
-XX:MaxHeapSize=524288000 # 最大堆大小 524M
-XX:MaxNewSize=174718976 #年轻代最大大小 174M
-XX:MinHeapDeltaBytes=196608 #堆自动增加步长最小196K
-XX:NewSize=11141120 #年轻代大小11M
-XX:OldSize=22413312 #老年代大小22M
-XX:+UseCompressedClassPointers #压缩类指针
-XX:+UseCompressedOops #压缩
-XX:+UseParallelGC # 使用并行回收器

Command line:

jstat查看jvm统计信息和垃圾回收信息

JVM statistics

jstat docs

jstat 命令使用样例

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
➜ jstat -options
-class # classLoader行为的统计信息
-compiler # Java HotSpot VM Just-in-Time compiler行为的统计信息
-gc # 垃圾回收堆的统计信息
-gccapacity # 每代的大小和容量
-gccause # 垃圾收集统计及回收原因
-gcmetacapacity
-gcnew
-gcnewcapacity
-gcold
-gcoldcapacity
-gcutil # gc统计信息汇总,展示gc次数、耗时和各个分区的大小
-printcompilation


class: Displays statistics about the behavior of the class loader.
compiler: Displays statistics about the behavior of the Java HotSpot VM Just-in-Time compiler.
gc: Displays statistics about the behavior of the garbage collected heap.
gccapacity: Displays statistics about the capacities of the generations and their corresponding spaces.
gccause: Displays a summary about garbage collection statistics (same as -gcutil), with the cause of the last and current (when applicable) garbage collection events.
gcnew: Displays statistics of the behavior of the new generation.
gcnewcapacity: Displays statistics about the sizes of the new generations and its corresponding spaces.
gcold: Displays statistics about the behavior of the old generation and metaspace statistics.
gcoldcapacity: Displays statistics about the sizes of the old generation.
gcmetacapacity: Displays statistics about the sizes of the metaspace.
gcutil: Displays a summary about garbage collection statistics.
printcompilation: Displays Java HotSpot VM compilation method statistics.

GC堆统计

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
jstat -gc 12196
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
512.0 1024.0 496.0 0.0 67584.0 38337.9 437248.0 75268.8 83992.0 80411.0 9496.0 8854.4 388 5.643 3 0.390 6.033



-gc option
Garbage-collected heap statistics.

S0C: Current survivor space 0 capacity (kB). #s0区当前容量
S1C: Current survivor space 1 capacity (kB). #s1区当前容量
S0U: Survivor space 0 utilization (kB). #s0区使用量
S1U: Survivor space 1 utilization (kB). #s1区使用量
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. # YGC次数
YGCT: Young generation garbage collection time. #YGC耗时
FGC: Number of full GC events. # Full GC次数
FGCT: Full garbage collection time. # Full GC耗时
GCT: Total garbage collection time. # GC总耗时

GC统计

1
2
3
jstat -gcutil 12196
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
96.88 0.00 89.96 17.21 95.74 93.24 388 5.643 3 0.390 6.033
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-gcutil option
Summary of garbage collection statistics.

S0: Survivor space 0 utilization as a percentage of the space's current capacity. # s0区使用率
S1: Survivor space 1 utilization as a percentage of the space's current capacity. # S1区使用率
E: Eden space utilization as a percentage of the space's current capacity. # eden区使用率
O: Old space utilization as a percentage of the space's current capacity. # old区使用率
M: Metaspace utilization as a percentage of the space's current capacity. # Metaspace使用率
CCS: Compressed class space utilization as a percentage. # 压缩类空间使用率
YGC: Number of young generation GC events. # YGC次数
YGCT: Young generation garbage collection time. # YGC耗时
FGC: Number of full GC events. # Full GC次数
FGCT: Full garbage collection time. # Full GC耗时
GCT: Total garbage collection time. # GC总耗时

堆dump

full gc前后dump

1
java -XX:HeapDumpPath=./heap/ -XX:+HeapDumpOnOutOfMemoryError -XX:+HeapDumpAfterFullGC

HeapDumpPath: dump path
HeapDumpOnOutOfMemoryError 内存溢出dump
HeapDumpAfterFullGCHeapDumpBeforeFullGC :Full GC前后dump

jmap

Prints shared object memory maps or heap memory details for a process, core file, or remote debug server. This command is experimental and unsupported.
打印进程、核心文件或远程调试服务器的共享对象内存映射或堆内存详细信息

jmap docs

jmap -heap <pid>

堆的各个分区大小

堆内存分析-GC日志解读

展示堆满的情况下的各个分区大小

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
> jmap -heap 16186
Attaching to process ID 16186, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.242-b08

using thread-local object allocation.
Parallel GC with 6 thread(s)

Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 522190848 (498.0MB)
NewSize = 11010048 (10.5MB)
MaxNewSize = 174063616 (166.0MB)
OldSize = 22544384 (21.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
capacity = 58720256 (56.0MB)
used = 49193976 (46.91503143310547MB)
free = 9526280 (9.084968566894531MB)
83.7768418448312% used
From Space:
capacity = 57147392 (54.5MB)
used = 0 (0.0MB)
free = 57147392 (54.5MB)
0.0% used
To Space:
capacity = 57671680 (55.0MB)
used = 0 (0.0MB)
free = 57671680 (55.0MB)
0.0% used
PS Old Generation
capacity = 348127232 (332.0MB)
used = 347644232 (331.5393753051758MB)
free = 483000 (0.46062469482421875MB)
99.8612576220409% used

10381 interned Strings occupying 935984 bytes.

远程监控

JVM开启远程JMX

1
2
3
4
5
6
7
8
9
# 开启远程调试端口
-Dcom.sun.management.jmxremote.port=8777
# 开启本地调试端口
-Dcom.sun.management.jmxremote.rmi.port=8777
# jmx远程服务默认是开启ssl和认证功能功能的,也可以通过jvm选项把这两个功能关闭
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
# jmx默认是通过localhost的ip地址提供RMI服务的,如果要明确指定RMI服务地址或主机名(比如主机有多个接口,想使用非hostname关联的接口)
-Djava.rmi.server.hostname=10.242.93.40

例子:

1
2
3
4
5
6
7
8
java \
-Djava.rmi.server.hostname=0.0.0.0 \
-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.rmi.port=8000 \
-Dcom.sun.management.jmxremote.port=8001 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
-jar demo-0.0.1-SNAPSHOT.jar

Tomcat 开启远程

修改catalina.sh,

1
2
3
4
5
6
JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=0.0.0.0 
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.rmi.port=8000
-Dcom.sun.management.jmxremote.port=8001
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false "

jvisualvm

插件中心

  • 新建远程主机0.0.0.0
  • 新建JMX链接: 端口是8001

中文文档

开启jstatd连接

BTrace

官网

JDWP与tomcat远程调试

Tomcat 监控

JDWP

Tomcat-manager

psi-probe监控

Nginx

ngx_http_stub_status监控连接信息

ngxtop监控请求信息

字节码

常量池官方文档

深入解析String#intern

深入解析String.intern
原文出处: 深入解析String.intern

字符串的常量池

常量池在哪里?

  • Jdk1.6及之前: 有永久代, 常量池在方法区
  • Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池在堆
  • Jdk1.8及之后: 无永久代,常量池在元空间

String去重

String Deduplication in G1

-XX:+UseStringDeduplication 开启String去重
-XX:+PrintStringDeduplicationStatistics 打印详细的去重统计
-XX:StringDeduplicationAgeThreshold=15 达到这个年龄的String对象才会去重

重用代码优化方法

  • 尽量重用对象,不要循环创建对象,比如:for循环字符串拼接
  • 容器类初始化的时候指定长度
  • ArrayList随机遍历快,LinkedList添加删除快
  • 集合遍历尽量减少重复计算
    1
    for(int i=0;i<collection.size();i++) // collection.size() 提取成常量
  • 使用Entry遍历Map
    1
    2
    3
    4
    for(Map.Entry<String,String> entry:map.entrySet()){
    String key=entry.getKey();
    String value=entry.getValue();
    }
  • 大数组复制用System.arrayCopy()
  • 尽量使用基本类型而不是包装类型
    1
    2
    Integer i=100;
    System.out.println(i);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Code:
    stack=2, locals=2, args_size=1
    0: bipush 100 // 将100压入栈
    2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    5: astore_1
    6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
    9: aload_1
    10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
    13: return
    1
    2
    3
    Integer i1=100;
    Integer i2=100;
    System.out.println(i1==i2); // true
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Code:
    stack=3, locals=3, args_size=1
    0: bipush 100
    2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    5: astore_1
    6: bipush 100
    8: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    11: astore_2
    12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
    15: aload_1
    16: aload_2
    17: if_acmpne 24
    20: iconst_1
    21: goto 25
    24: iconst_0
    25: invokevirtual #5 // Method java/io/PrintStream.println:(Z)V
    28: return
    1
    2
    3
    Integer i1=1000;
    Integer i2=1000;
    System.out.println(i1==i2); // false
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Code:
    stack=3, locals=3, args_size=1
    0: sipush 1000
    3: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    6: astore_1
    7: sipush 1000
    10: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    13: astore_2
    14: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
    17: aload_1
    18: aload_2
    19: if_acmpne 26
    22: iconst_1
    23: goto 27
    26: iconst_0
    27: invokevirtual #5 // Method java/io/PrintStream.println:(Z)V
    30: return
    Integer.valueOf 内部有缓存,如果大于-128且小于java.lang.Integer.IntegerCache.high(默认值是127),则返回缓存
  • 尽量使用非同步的容器,vector和ArrayList
  • 尽量减少同步作用范围,synchronized方法vs代码块
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public synchronized void f1() {
    System.out.println("f1");
    }

    public void f2() {
    synchronized (this) {
    System.out.println("f4");
    }

    }

    public static synchronized void f3() {
    System.out.println("f3");
    }

    public static void f4() {
    synchronized (TestPrimaryType.class) {
    System.out.println("f4");
    }
    }
  • 使用ThreadLocal缓存线程不安全对象,simpleDateFormat
  • 尽量使用延迟加载,懒惰单例模式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class TestPrimaryType{
    private TestPrimaryType() {
    }

    private static class TestPrimaryTypeHolder {
    private static TestPrimaryType instance = new TestPrimaryType();
    }
    /**
    ** 调用这个方法时,发生对TestPrimaryTypeHolder的引用,才创建instance对象
    */
    public static TestPrimaryType getInstance() {
    return TestPrimaryTypeHolder.instance;
    }
    }
  • 尽量减少使用反射,加缓存
  • 尽量使用连接池、线程池、对象池、缓存
  • 及时释放资源,I/O流、socket、数据库连接
  • 慎用异常,不要用抛异常来标示正常的业务逻辑
  • String操作尽量少用正则表达式
    1
    2
    replace VS replaceAll: 尽量用replace
    split
  • 日志输出注意使用不同的级别
  • 日志中参数拼接使用占位符
    1
    2
    log.info(“orderId:”+orgerId); // 不推荐
    log.info(“orderId:{}”,orgerId); //推荐

03.大堆问题定位

  1. 首先确定是内存问题,还是内存泄露。如果是内存问题,则可以通过JVM监控等手段,判断内存持续增长的区域;假设确定了是内存泄露,也可能是堆内或者堆外
  2. 堆内内存泄露的情况,最有效的手段莫过于Heap Dump,使用jmap -heap $PIDjcmd或者HeapDumpOnOutOfMemoryError等等手段都可以获取,使用MAT找超级powerful的机器分析;MAT 其实提供了分析脚本可以在不用 IDE 加载整个 HEAP 获取到需要的信息, 也就是通过脚本解析 HEAP 逐步分析, 具体可参考 How to Analyse Large Heap Dumps. PS: 曾经分析过 108G 的堆
  3. 实际生产中,这么大的堆,不管是dump对生产系统的影响,还是dump本身的难度,都往往不切实际,相对低成本的手段:
  4. jmap -histo $PIDjmap -histo:live $PID获得当前堆内对象的个数统计,并采样多次,查看一下里面object的分布,看哪类对象比较多,一般来说200G的堆,做一次jmap -histo可能要几十秒到一分钟,可能会触发full gc,如果使用 CMSG1 的话, 可加入 -XX:+ExplicitGCInvokesConcurrent 使用并发收集器显式;如果不能探明的话, 只能使用 jmap 将整个堆 dump 下来。
    所以如果有类似timeout killer的守护线程,要注意不要让它把进程kill掉
  5. 详细的GC日志等,比如判断引用堆积情况等,看看没有没什么异常, 比如有没有因为metaspace 满了而导致的GC。这种情况,可以看看是不是打开XX:+TraceClassLoading -XX:+TraceClassUnloading 分析下类加载情况。
  6. 使用Tencent JDK,可以利用old object sampling技术,不做Heap dump定位相当一部分memory leak
  7. 堆外内存: 确认下是否是 Java 的 direct bytebuffer 泄露, 由于采用 reference 机制回收, 如果一直没有触发 JVM GC 或回收线程偏少也会导致堆外内存回收缓慢导致泄露
    如果是 Native 或者使用 Unsafe 方式直接向 OS 申请内存, 可通过 NMT以及 pmap 等查看, JDK 团队分享的 http://km.oa.com/group/42239/articles/show/404478?ts=1574932416 可谓是面面俱到.

使用MAT命令行分析

How to Analyse Large Heap Dumps

  1. 下载MAT

  2. 到MAT的安装目录下,打开MemoryAnalyzer.ini,调整MAT的启动jvm参数

    1
    2
    3
    4
    5
    6
    7
    -Xms6144m
    -Xmx8192m
    -XX:+UseConcMarkSweepGC
    -XX:+UseParNewGC
    -XX:+CMSParallelRemarkEnabled
    -XX:+CMSClassUnloadingEnabled
    -XX:+UseCMSInitiatingOccupancyOnly

    堆最大大小调整为机器内存大小

  3. dump文件所在文件夹,确保有dump文件两倍的空间

  4. 到MAT的安装目录下,使用root账户运行命令
    For UNIX:

    1
    2
    3
    ./ParseHeapDump.sh /opt/heap_dump/jvm.hprof org.eclipse.mat.api:suspects
    ./ParseHeapDump.sh /opt/heap_dump/jvm.hprof org.eclipse.mat.api:overview
    ./ParseHeapDump.sh /opt/heap_dump/jvm.hprof org.eclipse.mat.api:top_components

    For Windows:

    1
    2
    3
    ParseHeapDump.bat D:\heap_dump\jvm.hprof org.eclipse.mat.api:suspects
    ParseHeapDump.bat D:\heap_dump\jvm.hprof org.eclipse.mat.api:overview
    ParseHeapDump.bat D:\heap_dump\jvm.hprof org.eclipse.mat.api:top_components
  5. 使用MAT打开生成的分析结果文件

CMS GC

垃圾回收器组合

Young 年轻代 Tenured 老生代 JVM options 备注
Serial Serial -XX:+UseSerialGC 单线程回收,全程STW
Parallel Scavenge Serial -XX:+UseParallelGC -XX:-UseParallelOldGC 年轻代并行,老年代串行,全程STW
Parallel Scavenge Parallel Old -XX:+UseParallelGC -XX:+UseParallelOldGC 多线程回收,全程STW
Parallel New或Serial CMS -XX:+UseParNewGC -XX:+UseConcMarkSweepGC 年轻代并行或串行,老年代并发,只有某个阶段会STW
G1 G1 -XX:+UseG1GC 并发回收, 某个阶段会STW

垃圾回收器从线程运行情况分类有三种:

  • 串行回收: Serial回收器,单线程回收,全程STW;
  • 并行回收: 名称以Parallel开头的回收器,多线程回收,全程STW;
  • 并发回收: CMS与G1,多线程分阶段回收,只有某阶段会STW;

Minor GC、Major GC与Full GC

分代回收中:

Minor GC清理年轻代(Young GC),除了G1 GC外,都会STW
Major GC清理老年代(Tenured GC)
Full GC清理整个堆

Minor GC触发条件:

Major GC触发条件:

Full GC触发条件:

  • 调用System.gc时,系统建议执行Full GC,不是必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后,进入老年代的平均大小 > 老年代的可用内存
  • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。即老年代无法存放新年代过度到老年代的对象的时候,会触发Full GC
  • 手动触发Full GC: jmap -histo:live <pid> 或者 jmap -dump:live,file=dump_001.bin PID,然后删掉dump_001.bin文件

CMS垃圾收集器 Concurrent Mark Sweep(CMS) Collector

并发,低停顿
特别是拥有大量长期数据(大老年代),多核心,低停顿

启用-XX:+UseConcMarkSweepGC

CMS收集器是分代的。 因此,minor GC和major GC都会发生。 CMS收集器尝试通过使用单独的垃圾收集器线程在执行应用程序线程的同时跟踪可访问对象,来减少由于major GC而导致的暂停时间。 在每个major收集周期中,CMS收集器会在收集开始时暂停所有应用程序线程一小段时间,然后收集中间再暂停一次。 第二次停顿往往是两个停顿中较长的一个。 在两个暂停期间都使用多个线程来执行收集工作。 收集的其余部分(包括大部分活动对象的跟踪和无法访问对象的清除)是通过与应用程序同时运行的一个或多个垃圾收集器线程来完成的。minor GC可以与正在进行的主要周期交错,并在一个 类似于并行收集器的方式(特别是在次要收集期间停止了应用程序线程)。

并发模式失效Concurrent Mode Failure

  1. 如果CMS收集器在老年代填满之前无法完成回收无法访问的对象,
  2. 如果老年代的可用空闲空间块(出现了碎片)无法满足分配,则暂停应用程序,并使所有应用程序线程已停止。 无法同时完成收集的情况称为并发模式失败,代表需要调整CMS收集器参数。
  3. 如果并发收集被显式垃圾收集(System.gc())中断
  4. 为提供诊断工具信息所需的垃圾收集中断了,则将报告并发模式中断。

if the CMS collector is unable to finish reclaiming the unreachable objects before the tenured generation fills up, or if an allocation cannot be satisfied with the available free space blocks in the tenured generation, then the application is paused and the collection is completed with all the application threads stopped. The inability to complete a collection concurrently is referred to as concurrent mode failure and indicates the need to adjust the CMS collector parameters. If a concurrent collection is interrupted by an explicit garbage collection (System.gc()) or for a garbage collection needed to provide information for diagnostic tools, then a concurrent mode interruption is reported.

Excessive GC Time and OutOfMemoryError

太多的时间花在gc上: 如果总时间的98%花在GC上,并且回收不到2%的堆空间,将抛出OutOfMemoryError

禁用命令行: -XX:-UseGCOverheadLimit

浮动垃圾Floating Garbage

边收集边运行,出现浮动垃圾

CMS垃圾回收特点

CMS只会回收老年代和永久代(1.8开始为元数据区,需要设置CMSClassUnloadingEnabled),不会收集年轻代;

CMS是一种预处理垃圾回收器,它不能等到老年代内存用尽时回收,需要在内存用尽前,完成回收操作,否则会导致并发回收失败(并发回收降级);
所以CMS垃圾回收器开始执行回收操作,有一个触发阈值(参数名称),默认是老年代或永久代达到92%;

CMS垃圾收集器步骤

CMS 处理过程有七个步骤:

步骤 是否STW 详情
初始标记(CMS-initial-mark) 会导致STW 标记GCRoot和被年轻代引用的老年代对象
并发标记(CMS-concurrent-mark) 与用户线程同时运行; 扫描整个老年代,将引用关系变化的对象置为dirty
预清理(CMS-concurrent-preclean) 与用户线程同时运行;
可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
重新标记(CMS-remark) 会导致STW
并发清除(CMS-concurrent-sweep) 与用户线程同时运行;
并发重置状态等待下次CMS的触发(CMS-concurrent-reset) 与用户线程同时运行;

CMS运行流程图如下所示:

Phase 1: Initial Mark(初始化标记)

这是CMS中两次stop-the-world事件中的一次。这一步的作用是标记存活的对象,有两部分:

  1. 从GC Roots遍历可直达的老年代对象,下图中1;
  2. 遍历被新生代存活对象所引用的老年代对象,如下图节点2、3;
  • 支持单线程或并发标记
  • 发生STW

在Java语言里,可作为GC Roots对象的包括如下几种:

  1. 虚拟机栈(栈桢中的本地变量表)中的引用的对象 ;
  2. 方法区中的类静态属性引用的对象 ;
  3. 方法区中的常量引用的对象 ;
  4. 本地方法栈中JNI的引用的对象;

ps:为了加快此阶段处理速度,减少停顿时间:

  • 开启并行化初始标记: -XX:+CMSParallelInitialMarkEnabled
  • 同时调大并行标记的线程数,线程数不要超过cpu的核数: -XX:ConcGCThreads=4

Phase 2: Concurrent Mark(并发标记)

通过遍历第一个阶段(Initial Mark)标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象。

由于应用线程和GC线程是并发执行的,因此可能产生新的对象或对象关系发生变化,例如:

  • 新生代的对象晋升到老年代;
  • 直接在老年代分配对象;
  • 老年代对象的引用关系发生变更;
  • 等等。

对于这些对象,需要重新标记以防止被遗漏。为了提高重新标记的效率,本阶段只会把发生变化的对象所在的Card标识为Dirty,这样后续就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。

并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;

如下图所示,也就是节点1、2、3,最终找到了节点4和5。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。

这个阶段因为是并发的, 容易导致concurrent mode failure

Phase 3: Concurrent Preclean(并发预清理)

在并发预清洗阶段,将会重新扫描前一个阶段标记的Dirty对象,并标记被Dirty对象直接或间接引用的对象,然后清除Card标识。

前一个阶段已经说明,不能标记出老年代全部的存活对象,是因为标记的同时应用程序会改变一些对象引用,这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Direty的Card

如下图所示,在并发清理阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty;

最后将6标记为存活,如下图所示:

Phase 4: Concurrent Abortable Preclean(可中止的并发预清理)

本阶段尽可能承担更多的并发预处理工作,从而减轻在Final Remark阶段的stop-the-world。

这个阶段尝试着去承担下一个阶段Final Remark阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生abort的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。

ps:此阶段最大持续时间为5秒,之所以可以持续5秒,另外一个原因也是为了期待这5秒内能够发生一次ygc,清理年轻代的引用,是的下个阶段的重新标记阶段,扫描年轻代指向老年代的引用的时间减少;

在该阶段,主要循环的做两件事:

  • 处理 From 和 To 区的对象,标记可达的老年代对象;
  • 和上一个阶段一样,扫描处理Dirty Card中的对象。

具体执行多久,取决于许多因素,满足其中一个条件将会中止运行:

  • 执行循环次数达到了阈值;
  • 执行时间达到了阈值;
  • 新生代Eden区的内存使用率达到了阈值。

Phase 5: Final Remark(重新标记)

预清理阶段也是并发执行的,并不一定是所有存活对象都会被标记,因为在并发标记的过程中对象及其引用关系还在不断变化中。

因此,需要有一个stop-the-world的阶段来完成最后的标记工作,这就是重新标记阶段(CMS标记阶段的最后一个阶段)。主要目的是重新扫描之前并发处理阶段的所有残留更新对象。

主要工作:

遍历新生代对象,重新标记;(新生代会被分块,多线程扫描)
根据GC Roots,重新标记;
遍历老年代的Dirty Card,重新标记。这里的Dirty Card,大部分已经在Preclean阶段被处理过了。

这个阶段会导致第二次stop the world,该阶段的任务是完成标记整个年老代的所有的存活对象。

这个阶段,重新标记的内存范围是整个堆,包含young_gen和old_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做CMS的“gc root”,来扫描老年代; 因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”:
当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc,回收掉年轻代的对象无用的对象,并将对象放入survivor区或晋升到老年代,这样再进行年轻代扫描时,只需要扫描幸存区的对象即可,一般survivor区非常小,这大大减少了扫描时间

由于之前的预处理阶段是与用户线程并发执行的,这时候可能年轻代的对象对老年代的引用已经发生了很多改变,这个时候,remark阶段要花很多时间处理这些改变,会导致很长stop the word,所以通常CMS尽量运行Final Remark阶段在年轻代是足够干净的时候。

另外,还可以开启并行收集:-XX:+CMSParallelRemarkEnabled

Phase 6: Concurrent Sweep(并发清理

并发清理阶段,主要工作是清理所有未被标记的死亡对象,回收被占用的空间。

通过以上5个阶段的标记,老年代所有存活的对象已经被标记并且现在要通过Garbage Collector采用清扫的方式回收那些不能用的对象了。

这个阶段主要是清除那些没有标记的对象并且回收空间;

由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

步骤7: 并发重置

并发重置阶段,将清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构,为下一个垃圾收集周期做好准备。

这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。

CMS日志分析

下面就是该参数设置打印出来的gc信息,一些非关键的信息已经去掉,如时间:

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
//第一步 初始标记 这一步会停顿*
[GC (CMS Initial Mark) [1 CMS-initial-mark: 299570K(307200K)] 323315K(491520K), 0.0026208 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
0.345: CMS_Initial_Mark [ 10 0 1 ] [ 0 0 0 0 2 ] 0
Total time for which application threads were stopped: 0.0028494 seconds

//第二步 并发标记
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.012/0.012 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

//第三步 并发预清理
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

//第四步 可被终止的并发预清理
[CMS-concurrent-abortable-preclean-start]
[CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

//第五步 最终重新标记
[GC (CMS Final Remark) [YG occupancy: 72704 K (184320 K)][Rescan (parallel) , 0.0009069 secs][weak refs processing, 0.0000083 secs][class unloading, 0.0002626 secs][scrub symbol table, 0.0003789 secs][scrub string table, 0.0001326 secs][1 CMS-remark: 299570K(307200K)] 372275K(491520K), 0.0017842 secs] [Times: user=0.05 sys=0.00, real=0.00 secs]
vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count
0.360: CMS_Final_Remark [ 10 0 1 ] [ 0 0 0 0 1 ] 0
Total time for which application threads were stopped: 0.0018800 seconds

//第六步 并发清理
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.007/0.007 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

//第七步 并发重置
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

输出GC详情,需要添加 -verbose:gc-XX:+PrintGCDetails 参数

CMS-initial-mark标示着并发收集周期的开始
CMS-concurrent-mark标示着并发标记阶段的结束
CMS-concurrent-sweep标志着并发清理阶段的结束
CMS-concurrent-preclean标志着预清理阶段,预清理代表着在准备CMS-remark阶段可以并发处理的工作
CMS-concurrent-reset是最后阶段,为下一次并发收集做准备

CMS-initial-mark indicates the start of the concurrent collection cycle,
CMS-concurrent-mark indicates the end of the concurrent marking phase,
and CMS-concurrent-sweep marks the end of the concurrent sweeping phase.
Not discussed previously is the precleaning phase indicated by CMS-concurrent-preclean.
Precleaning represents work that can be done concurrently in preparation for the remark phase CMS-remark.
The final phase is indicated by CMS-concurrent-reset and is in preparation for the next concurrent collection.

调优参数与启用参数

下面抓取一下gc信息,来进行详细分析,首先将jvm中加入以下运行参数:

  • -XX:+PrintCommandLineFlags [0]
  • -XX:+UseConcMarkSweepGC [1]
  • -XX:+UseCMSInitiatingOccupancyOnly [2]
  • -XX:CMSInitiatingOccupancyFraction=80 [3]
  • -XX:+CMSClassUnloadingEnabled [4]
  • -XX:+UseParNewGC [5]
  • -XX:+CMSParallelRemarkEnabled [6]
  • -XX:+CMSScavengeBeforeRemark [7]
  • -XX:+UseCMSCompactAtFullCollection [8]
  • -XX:CMSFullGCsBeforeCompaction=0 [9]
  • -XX:+CMSConcurrentMTEnabled [10]
  • -XX:ConcGCThreads=4 [11]
  • -XX:+ExplicitGCInvokesConcurrent [12]
  • -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses [13]
  • -XX:+CMSParallelInitialMarkEnabled [14]
  • -XX:+PrintGCDetails [15]
  • -XX:+PrintGCCause [16]
  • -XX:+PrintGCTimeStamps [17]
  • -XX:+PrintGCDateStamps [18]
  • -Xloggc:../logs/gc.log [19]
  • -XX:+HeapDumpOnOutOfMemoryError [20]
  • -XX:HeapDumpPath=../dump [21]

先来介绍下下面几个参数的作用:

[0] 打印出启动参数行

[1] 参数指定使用CMS垃圾回收器;

[2]、[3] 参数指定CMS垃圾回收器在老年代达到80%的时候开始工作,如果不指定那么默认的值为92%;

[4] 开启永久代(jdk1.8以下版本)或元数据区(jdk1.8及其以上版本)收集,如果没有设置这个标志,一旦永久代或元数据区间也会尝试进行垃圾回收,但是收集不会是并行的,而再一次进行Full GC;

[5] 使用CMS时默认这个参数就是打开的,不需要配置,CMS只回收老年代,年轻代只能配合Parallel New或Serial回收器;

[6] 减少Remark阶段暂停的时间,启用并行Remark,如果Remark阶段暂停时间长,可以启用这个参数

[7] 如果Remark阶段暂停时间太长,可以启用这个参数,在Remark执行之前,先做一次ygc。因为这个阶段,年轻代也是CMS的gcroot,CMS会扫描年轻代指向老年代对象的引用,如果年轻代有大量引用需要被扫描,会让Remark阶段耗时增加;

[8]、[9]两个参数是针对CMS垃圾回收器碎片做优化的,CMS是不会移动内存的, 运行时间长了,会产生很多内存碎片, 导致没有一段连续区域可以存放大对象,出现”promotion failed”、”concurrent mode failure”, 导致fullgc,启用UseCMSCompactAtFullCollection 在FULL GC的时候, 对年老代的内存进行压缩。-XX:CMSFullGCsBeforeCompaction=0 则是代表多少次FGC后对老年代做压缩操作,默认值为0,代表每次都压缩, 把对象移动到内存的最左边,可能会影响性能,但是可以消除碎片;

1
2
3
106.641: [GC 106.641: [ParNew (promotion failed): 14784K->14784K(14784K), 0.0370328 secs]106.678: [CMS106.715: [CMS-concurrent-mark: 0.065/0.103 secs] [Times: user=0.17 sys=0.00, real=0.11 secs]

(concurrent mode failure): 41568K->27787K(49152K), 0.2128504 secs] 52402K->27787K(63936K), [CMS Perm : 2086K->2086K(12288K)], 0.2499776 secs] [Times: user=0.28 sys=0.00, real=0.25 secs]

[11] 定义并发CMS过程运行时的线程数。比如value=4意味着CMS周期的所有阶段都以4个线程来执行。尽管更多的线程会加快并发CMS过程,但其也会带来额外的同步开销。因此,对于特定的应用程序,应该通过测试来判断增加CMS线程数是否真的能够带来性能的提升。如果未设置这个参数,JVM会根据并行收集器中的-XX:ParallelGCThreads参数的值来计算出默认的并行CMS线程数:

1
2
ParallelGCThreads = (ncpus <=8 ? ncpus : 8+(ncpus-8)*5/8) ,ncpus为cpu个数,
ConcGCThreads =(ParallelGCThreads + 3)/4

这个参数一般不要自己设置,使用默认就好,除非发现默认的参数有调整的必要;
[12]、[13]开启foreground CMS GC,CMS gc 有两种模式,background和foreground,正常的CMS gc使用background模式,就是我们平时说的CMS gc;当并发收集失败或者调用了System.gc()的时候,就会导致一次full gc,这个fullgc是不是CMS回收,而是Serial单线程回收器,加入了参数[12]后,执行full gc的时候,就变成了CMS foreground gc,它是并行full gc,只会执行CMS中stop the world阶段的操作,效率比单线程Serial full GC要高;需要注意的是它只会回收old,因为CMS收集器是老年代收集器;而正常的Serial收集是包含整个堆的,加入了参数[13],代表永久代也会被CMS收集;

[14] 开启初始标记过程中的并行化,进一步提升初始化标记效率;

[15]、[16]、[17]、[18] 、[19]是打印gc日志,其中[16]在jdk1.8之后无需设置

[20]、[21]则是内存溢出时dump堆

CMS需要注意的问题

CMS不是full GC

有一点需要注意的是:CMS并发GC不是“full GC”。HotSpot VM里对concurrent collectionfull collection有明确的区分。所有带有“FullCollection”字样的VM参数都是跟真正的full GC相关,而跟CMS并发GC无关的,CMS收集算法只是清理老年代。

减少remark阶段停顿

一般CMS的GC耗时 80%都在remark阶段,如果发现remark阶段停顿时间很长,可以尝试添加该参数:

-XX:+CMSScavengeBeforeRemark

在执行remark操作之前先做一次ygc,目的在于减少ygen对oldgen的无效引用,降低remark时的开销,如果添加该参数后 ”ygc停顿时间+remark时间<添加该参数之前的remark时间“,说明该参数是有效的;

内存碎片

CMS是基于标记-清除算法的,只会将标记为为存活的对象删除,并不会移动对象整理内存空间,会造成内存碎片,这时候我们需要用到这个参数;

-XX:CMSFullGCsBeforeCompaction=n

这个参数大部分人的使用方式都是错误的,往往会导致设置后问题更大。

CMSFullGCsBeforeCompaction这个参数在HotSpot VM里是这样声明的:

product(bool, UseCMSCompactAtFullCollection, true, \

“Use mark sweep compact at full collections”) \

\

product(uintx, CMSFullGCsBeforeCompaction, 0, \

“Number of CMS full collection done before compaction if > 0”) \

然后这样使用的:

*should_compact =

UseCMSCompactAtFullCollection &&

((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction) ||

GCCause::is_user_requested_gc(gch->gc_cause()) ||

gch->incremental_collection_will_fail(true */* consult_young /));

CMS GC要决定是否在full GC时做压缩,会依赖几个条件。其中,

  1. UseCMSCompactAtFullCollection 与 CMSFullGCsBeforeCompaction 是搭配使用的;前者目前默认就是true了,也就是关键在后者上。

  2. 用户调用了System.gc(),而且DisableExplicitGC没有开启。

  3. young gen报告接下来如果做增量收集会失败;简单来说也就是young gen预计old gen没有足够空间来容纳下次young GC晋升的对象。

上述三种条件的任意一种成立都会让CMS决定这次做full GC时要做压缩。

CMSFullGCsBeforeCompaction 说的是,在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 如果把CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件变成每隔10次真正的full GC才做一次压缩(而不是每10次CMS并发GC就做一次压缩,目前VM里没有这样的参数)。这会使full GC更少做压缩,也就更容易使CMS的old gen受碎片化问题的困扰。 本来这个参数就是用来配置降低full GC压缩的频率,以期减少某些full GC的暂停时间。CMS回退到full GC时用的算法是mark-sweep-compact,但compaction是可选的,不做的话碎片化会严重些但这次full GC的暂停时间会短些;这是个取舍。

concurrent mode failure

这个异常发生在CMS正在回收的时候。执行CMS GC的过程中,同时业务线程也在运行,当年轻代空间满了,执行ygc时,需要将存活的对象放入到老年代,而此时老年代空间不足,这时CMS还没有机会回收老年带产生的,或者在做Minor GC的时候,新生代救助空间放不下,需要放入老年代,而老年代也放不下而产生的。

设置CMS触发时机有两个参数:

-XX:+UseCMSInitiatingOccupancyOnly

-XX:CMSInitiatingOccupancyFraction=70

-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC。

-XX:+UseCMSInitiatingOccupancyOnly如果不指定, 只是用设定的回收阈值CMSInitiatingOccupancyFraction,则JVM仅在第一次使用设定值,后续则自动调整会导致上面的那个参数不起作用。

为什么要有这两个参数?

由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。

CMS前五个阶段都是标记存活对象的,除了”初始标记”和”重新标记”阶段会stop the word ,其它三个阶段都是与用户线程一起跑的,就会出现这样的情况gc线程正在标记存活对象,用户线程同时向老年代提升新的对象,清理工作还没有开始,old gen已经没有空间容纳更多对象了,这时候就会导致concurrent mode failure, 然后就会使用串行收集器回收老年代的垃圾,导致停顿的时间非常长。

CMSInitiatingOccupancyFraction参数要设置一个合理的值,设置大了,会增加concurrent mode failure发生的频率,设置的小了,又会增加CMS频率,所以要根据应用的运行情况来选取一个合理的值。

如果发现这两个参数设置大了会导致fullgc,设置小了会导致频繁的CMSgc,说明你的老年代空间过小,应该增加老年代空间的大小了;

promotion failed

这个异常发生在年轻代回收的时候;

在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的,多数是由于老年带有足够的空闲空间,但是由于碎片较多,新生代要转移到老年带的对象比较大,找不到一段连续区域存放这个对象导致的,以下是一段promotion failed的日志:

106.641: [GC 106.641: [ParNew (promotion failed): 14784K->14784K(14784K), 0.0370328 secs]106.678: [CMS106.715: [CMS-concurrent-mark: 0.065/0.103 secs] [Times: user=0.17 sys=0.00, real=0.11 secs]

(concurrent mode failure): 41568K->27787K(49152K), 0.2128504 secs] 52402K->27787K(63936K), [CMS Perm : 2086K->2086K(12288K)], 0.2499776 secs] [Times: user=0.28 sys=0.00, real=0.25 secs]

过早提升与提升失败

在 Minor GC 过程中,Survivor Unused 可能不足以容纳 Eden 和另一个 Survivor 中的存活对象, 那么多余的将被移到老年代, 称为过早提升(Premature Promotion),这会导致老年代中短期存活对象的增长, 可能会引发严重的性能问题。 再进一步, 如果老年代满了, Minor GC 后会进行 Full GC, 这将导致遍历整个堆, 称为提升失败(Promotion Failure)。

早提升的原因

  1. Survivor空间太小,容纳不下全部的运行时短生命周期的对象,如果是这个原因,可以尝试将Survivor调大,否则端生命周期的对象提升过快,导致老年代很快就被占满,从而引起频繁的full gc;

  2. 对象太大,Survivor和Eden没有足够大的空间来存放这些大象;

提升失败原因

当提升的时候,发现老年代也没有足够的连续空间来容纳该对象。

为什么是没有足够的连续空间而不是空闲空间呢?

老年代容纳不下提升的对象有两种情况:

  1. 老年代空闲空间不够用了;

  2. 老年代虽然空闲空间很多,但是碎片太多,没有连续的空闲空间存放该对象;

解决方法

  1. 如果是因为内存碎片导致的大对象提升失败,CMS需要进行空间整理压缩;

  2. 如果是因为提升过快导致的,说明Survivor 空闲空间不足,那么可以尝试调大 Survivor;

  3. 如果是因为老年代空间不够导致的,尝试将CMS触发的阈值调低;

其它导致回收停顿时间变长原因

linux使用了swap,内存换入换出(vmstat),尤其是开启了大内存页的时候,因为swap只支持4k的内存页,大内存页的大小为2M,大内存页在swap的交换的时候需要先将swap中4k内存页合并成一个大内存页再放入内存或将大内存页切分为4k的内存页放入swap,合并和切分的操作会导致操作系统占用cup飙高,用户cpu占用反而很低;

除了swap交换外,网络io(netstat)、磁盘I/O (iostat)在 GC 过程中发生会使 GC 时间变长。

如果是以上原因,就要去查看gc日志中的Times耗时:

[Times: user=0.00 sys=0.00, real=0.00 secs]

user是用户线程占用的时间,sys是系统线程占用的时间,如果是io导致的问题,会有两种情况

  1. user与sys时间都非常小,但是real却很长,如下:

[ Times: user=0.51 sys=0.10, real=5.00 secs ]

user+sys的时间远远小于real的值,这种情况说明停顿的时间并不是消耗在cup执行上了,不是cup肯定就是io导致的了,所以这时候要去检查系统的io情况。

sys时间很长,user时间很短,real几乎等于sys的时间,如下:

[ Times: user=0.11 sys=31.10, real=33.12 secs ]

这时候其中一种原因是开启了大内存页,还开启了swap,大内存进行swap交换时会有这种现象;

增加线程数

CMS默认启动的回收线程数目是 (ParallelGCThreads + 3)/4) ,这里的ParallelGCThreads是年轻代的并行收集线程数,感觉有点怪怪的;

年轻代的并行收集线程数默认是(ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8),可以通过-XX:ParallelGCThreads= N 来调整;

如果要直接设定CMS回收线程数,可以通过-XX:ParallelCMSThreads=n,注意这个n不能超过cpu线程数,需要注意的是增加gc线程数,就会和应用争抢资源;

参考

https://plumbr.eu/handbook/garbage-collection-algorithms-implementations#concurrent-mark-and-sweep

http://www.infoq.com/cn/presentations/a-long-period-of-atypical-jvm-gc-caused-by-os/

GC Cause

Heap Inspection Initiated GC

因为执行了jmap -histo:live 触发的gc

参考文献

JMeter与性能压测

jmeter是一款纯java的性能测试工具,跨平台运行方便、提供图形化界面设置、简单易用。

在性能测试方法论中,很典型的方法就是二八原则,量化业务需求。

二八原则:指80%的业务量在20%的时间里完成。

如何理解,下面我们来个例子吧

用户登录场景:早高峰时段,8:50—9:10,5000坐席上线登陆。

  业务量:5000个 

  时间:20x60=1200秒

吞吐量=80%x业务量/(20%*时间)=4000/240=16.7/秒

而并非5000/1200=4.1/秒

实际上,登录请求数分布是一个正态分布,最高峰时肯定比4.1/秒更高,高峰段实际上完成了80%的业务量,却只花了20%的时间。

温馨提示:

1.二八原则计算的结果并非在线并发用户数,是系统要达到的处理能力(吞吐量),初学者容易被误导,那这这个数据就去设置并发数,这是错误滴。

2.如果你的系统性能要求更高,也可以选择一九原则或更严格的算法,二八原则比较通用,一般系统性能比较接近这个算法而已,大家应该活用。

3.tps、响应时间、在线并发数三者关系详解:点击打开链接

三者关系图

  1. 结论
  • 小并发数区间测试,找拐点(如:100-300并发持续5分钟,可以发现上图中200并发时出现拐点)
  • 大并发数区间测试,找符合需求的最大并发数(如:1800-2200并发持续5分钟,可以找到满足响应时间在3秒内的最大并发数2000)
  • 利用最大并发数,压测环境在极限时的资源消耗(压测时间1小时以内)
  • 80%最大并发数,进行稳定性测试(压测时间1小时以上)

注:执行机资源消耗必须监控上,保证能提供稳定的并发负载。

注:这里的响应时间是90%响应时间

tps:

每秒事务处理量 - 性能测试的术语介绍

TPS(Transaction Per Second)

每秒钟系统能够处理的交易或事务的数量。它是衡量系统处理能力的重要指标。TPS是LoadRunner中重要的性能参数指标。

1.下载安装

仅仅需要从apache的网站找到下载包,解压到本地文件目录即可。

http://jmeter.apache.org/download_jmeter.cgi

2.启动

解压目录中存在一个bin的目录,里面有很多批处理文件和脚本文件,window系统运行jmeter.bat即可。需要关注的是bin目录中的jmeter.properties文件,这是运行相关的配置文件. 特别是TCP Sampler configuration部分几个配置会和后面内容相关

3.建立一种类型测试

这里只描述简单的tcp测试建立步骤,因为目前支持的测试类型很多,无法一一陈述,功能细节部分可以参考JMeter文档

1)创建测试线程组

1. 启动测试用接口
首先我们写一段 php 代码,通过 PHP 内置的 Server 启动它。

1
2
3
4
$user_id = $_GET['user_id'];
file_put_contents('/tmp/1.log', $user_id.PHP_EOL, FILE_APPEND);
echo $user_id;

以上代码保存为 index.php

命令中执行 php -S 127.0.0.1:8080

在浏览器访问 http://127.0.0.1:8080/index.php?user_id=1 , 输出 1 说明服务接口正常

2. 创建线程组
使用 JMeter 测试应用性能首先要创建一个线程组
右键 “Text Plan”, 在弹出的菜单栏选择 “Add->Threads(Users)->Thread Group”

就创建了一个线程组:

“Number of Threads (users): ” 即并发用户数,相当于 ab 命令的 -c 参数
“Loop Count:” 循环请求次数, 即每个线程请求多少次, 这个数据乘以线程数相当于 ab 命令的 -n 参数

我们设置了 “Number of Threads (users)” 为 5 , “Loop Count” 为 60 , 相当于ab 命令

1
2
ab -c 5 -n 300 http://xxx.com

2. 创建测试请求
右键我们刚刚创建的线程组“Thread Group”, 选择 “Add-> Sampler-> HTTP Request”

这一步相当于通过多个参数拼出要测试的接口地址。

注意Path中, ${__counter(false)} 为 JMeter 内置的函数, 它的返回值为当前请求次数
**这样保证了我们每次向服务器请求的 user_id 的值都不一样 **

此时我们将要进行的测试等同于 ab 测试命令:

1
ab -c 5 -n 300 http://127.0.0.1/index.php?user_id=1

、_Counter 函数

每次调用计数器函数都会产生一个新值,从1开始每次加1。计数器既可以被配置成针对每个虚拟用户是独立的,也可以被配置成所有虚拟用户公用的。如果每个虚拟用户的计数器是独立增长的,那么通常被用于记录测试计划运行了多少遍。全局计数器通常被用于记录发送了多少次请求。

计数器使用一个整数值来记录,允许的最大值为2,147,483,647。

功能:这个函数是一个计数器,用于统计函数的使用次数,它从1开始,每调用这个函数一次它就会自动加1,它有两个参数,第一个参数是布尔型的,只能设置成“TRUE”或者“FALSE”,如果是TRUE,那么每个用户有自己的计数器,可以用于统计每个线程歌执行了多少次。如果是FALSE,那就使用全局计数器,可以统计出这次测试共运行了多少次。第二个参数是“函数名称”

格式:${__counter(FALSE,test)}

**使用:**我们将“_counter”函数生成的参数复制到某个参数下面,如果为TRUE格式,则每个线程各自统计,最大数为循环数,如果为FALSE,则所有线程一起统计,最大数为线程数乘以循环数

参数:

第一个参数:True,如果测试人员希望每个虚拟用户的计数器保持独立,与其他用户的计数器相区别。False,全局计数器

第二个参数:重用计数器函数创建值的引用名。测试人员可以这样引用计数器的值:${test}。这样一来,测试人员就可以创建一个计数器后,在多个地方引用它的值。

以上,摘自网络(不知道怎么用,只好摘抄,记录下来等灵感~~~~(>_<)~~~~ )。

目前,我测试_Counter函数,就是在参数列表加一个参数,值填写为${__counter(FALSE,test)}

3.开始测试
右键线程组 “Thread Group”, 选择 “Add-> Listener->Summary Report “, 创建一个结果报表

然后点击, 菜单栏中的绿色按钮, 开始测试:

结果如图:

打开 ‘/tmp/1.log’ 可以看到,每次请求的 user_id的值都是不同的。

Thread Group(线程组)

1.线程组,或者可以叫用户组,进行性能测试时的用户资源池。

2.是任何一个测试计划执行的开始点。

3.上一篇提到的“控制器”和“HTTP请求”(采集器)必须在线程组内;监听器等其他组件,可以直接放在测试计划下。

https://www.cnblogs.com/linglingyuese/archive/2013/03/06/linglingyuese-three.html

https://www.cnblogs.com/hait1234/p/6767212.html

二、Thread Group线程组功能分区

总的来说,一个线程组有三个功能分区,这里分别标注为区域1、区域2、区域3。

1.区域1:在取样器错误后要执行的动作,这个区域的主要作用很明显,在线程内的采样器失败后,接下来做什么。

 (1)继续:选择此项,将继续执行接下来的操作。

 (2)Start Next Loop:忽略错误,执行下一个循环。

 (3)停止线程:退出该线程(不再进行此线程的任何操作)。

 (4)停止测试:等待当前执行的采样器结束后,结束整个测试。

 (5)Stop Test Now:直接停止整个测试。(注意与4的“停止测试”进行区分)。

2.区域2:线程属性,这里可以设置线程数(模拟的用户数)和循环次数。含义如下图所示:

ramp up:斜坡上升; [动词短语] 加强,加大;

相当于warm up的一个词,包含准备,热身,加速的意思,可用在生产中小批量的试制中, 也可以指人初入公司的锻炼. 在项目初始阶段要做许多准备工作。

3.区域3:调度器配置(全部都在调度器复选框被选中的前提下,下面的选项才会生效。)

最重要的Tcp Sampler:tcp取样器

TCPClient classname

TCP Sampler提供了3个Sampler的实现,分别是

org.apache.jmeter.protocol.tcp.sampler.TCPClientImpl

org.apache.jmeter.protocol.tcp.sampler.BinaryTCPClientImpl和
org.apache.jmeter.protocol.tcp.sampler.LengthPrefixedBinaryTCPClientImpl。

其中TCPClientImpl实现了以文本编辑器中所编辑的纯文本为内容进行发送,BinaryTCPClientImpl则以文本编辑器中所编辑的16进制字符(hex)内容为基础转换为二进制的字节内容进行发送,LengthPrefixedBinaryTCPClientImpl则会在BinaryTCPClientImpl基础上默认以发送内容的长度以字节前缀进行填充。

我们可以通过配置jmeter.properties文件中tcp.handler属性来设置默认的TCPClient。

测试基于文本套接字应用

被测应用的源码请参见这里. 如果想运行该程序,请点击该链接下载socket_echo-0.0.1-SNAPSHOT.jar,并且在命令行下执行:

https://github.com/XMeterSaaSService/Blog\_sample\_project/tree/master/socket_echo

(javac 和java可以去掉包名后再在命令行执行)

1
java -cp socket_echo-0.0.1-SNAPSHOT.jar net.xmeter.echo.TextServer这个程序源码:

复制代码

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; public class TextServer { public static AtomicInteger sessions = new AtomicInteger(0); public void handleRequest(final Socket socket) {
ExecutorService executor = Executors.newSingleThreadExecutor();

    executor.submit(new Runnable() {
        @Override public void run() { try {
                BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                PrintWriter os = new PrintWriter(socket.getOutputStream()); while(true) {
                    String line = is.readLine(); if(line == null) {
                        System.out.println("Probably the client side closed the connection, now close me as well.");
                        socket.close(); break;
                    }
                    System.out.println("Received message: " + line);
                    os.println("Echo: " + line);
                    os.flush(); if("bye".equals(line)) { break;
                    }
                }
            } catch(Exception ex) {
                ex.printStackTrace();
            } finally { try {
                    socket.close(); int num = sessions.decrementAndGet();
                    System.out.println("Now totally has " + num + " of conn.");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        
    });
    
} public static void main(String\[\] args) { try {
        ServerSocket server = new ServerSocket(4700); while(true) {
            Socket socket = server.accept();
            TextServer srv = new TextServer();
            srv.handleRequest(socket); int num = sessions.incrementAndGet();
            System.out.println("Received new conn, now totally has " + num + " of conn.");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

}

复制代码

1
(这个程序测试:
1
注意几图:hello后面有个换行, ENDof line Byte value 填写的是10.LF (NL line feed, new line) 换行键 ,ascill是10.os.println("Echo: " + line); 用的是println,服务端返回的最后是一个换行符。如果不填写EOF byte value,那么客户端将会一直阻塞没有返回。

我们发现EOL原来是与读数据相关的,就是设定来自于服务器数据流的一个结束标识字节。没有设置EOL将会一直读到输入流结束为止。

这里值得注意的是,这是个十进制的值(千万不要写成hex),比如你可以查询ASCII表,来确认一个表示结束字符的十进制值,我们以$作为案例,改造一下Mock TCP Server,输出结尾为$,如下面代码:

)

1

(请确保您的机器上已经安装了Java)。 该程序会在4700端口建立一个ServerSocket,等待来自客户端的请求,客户端如果发送了一个字符串,服务器端返回“Echo: “ + 客户端发送的字符串。如下图所示,如果我们使用telnet连接到服务器端的套接字应用,双方就可以直接进行通信了。

TCPClientImpl

我们使用TCPClientImpl对Mock TCP Server进行测试,配置参考下图:

点击运行测试,你会发现测试发生了阻塞,原因是服务器使用了readLine获取客户端的发送数据,需要根据发送数据中的CRLF(\r或\n)判断一行的结束。而我们制作的发送内容并不包括CRLF标识内容,因此,服务器阻塞在了读数据,测试客户端得不到服务器响应,同样也阻塞在了读数据,正确的配置需要添加一个“回车”(不能是”\r”或”\n”,因为TCPClientImpl会自动将其转换为对应的两个字符而不是CRLF标识)参考下图

TCP 取样器通过TCP/IP来连接特定服务器,连上服务器之后发送消息,然后等待服务器回复。

如果“Re-use connection”(重复使用连接) 复选框被选中了,在同一个线程中Samplers(取样器)共享连接,包含相同主机名和端口,不同主机/端口合并将会使用不同线程。如果“Re-use connection” 和 “Close connection”(关闭连接)同时被选中,这个套接字在运行完当前Samplers将会关闭。再下一个Sampler将会另外创建一个新套接字。你可能想要在每次线程循环结束之后关闭套接字。

如果一个错误被检测到或者“Re-use connection” 没有被选中,这个套接字将会关闭,另外套接字将会在接下Samplers被再一次打开。

详细看这篇文章:

Apache JMeter TCPSampler的使用及自定义

还有这篇文章:https://www.jianshu.com/p/63e08071075e

JMeter—–TCP Sampler(TCP 取样器)

jmeter报告结果中会出现三个时间

  1. Elapsed time 经过的时间(= Sample time = Load time = Response time )

    这个时间是我们测试常用的时间,也是整个请求的消耗时间,从发送到接收完成全程消耗的时间

  2. Latency time 延迟时间

    不常用,表示请求发送到刚开始接收响应时,这个时间<Elapsed time

3. Connection time 建立连接时间 (2.13新增参数)

   不常用,请求连接建立的时间,这个时间 < Latency time < Elapsed time

Java生产环境下性能监控与调优详解笔记

另一个整理http://alanhou.org/java-optimization/

1:JVM字节码指令与 javap

1
2
3
javap <options> <classes>
cd monitor\_tuning/target/classes/org/alanhou/monitor\_tuning/chapter8/
javap -verbose Test1.class > Test1.txt

即可保存字节码文件
会有三个部分组成
操作数栈
LineNumberTable
LocalVariableTable

i++和++i 的执行效果完全相同 多了一个压入栈顶操作
for(int i=0;i<10;i++) {}
for(int i=0;i<10;++i) {} 执行效果一样

2:

public static void f1() {
String src = “”;
for(int i=0;i<10;i++) {
//每一次循环都会new一个StringBuilder 然后在src.append(“A”);
src = src + “A”;
}
System.out.println(src);
}
public static void f2() {
//只要一个StringBuilder
StringBuilder src = new StringBuilder();
for(int i=0;i<10;i++) {
src.append(“A”);
}
System.out.println(src);
}

3:

public static String f1() {
String str = “hello”;
try{
return str;
}
finally{
str = “imooc”;
}
} 返回 hello 但会执行finally 中的代码

4:字符串拼接都会在编译阶段转换成stringbuilder

5:字符串去重

字符串在任何应用中都占用了大量的内存。尤其数包含独立UTF-16字符的char[]数组对JVM内存的消耗贡献最多——因为每个字符占用2位。

内存的30%被字符串消耗其实是很常见的,不仅是因为字符串是与我们互动的最好的格式,而且是由于流行的HTTP API使用了大量的字符串。使用Java 8 Update 20,我们现在可以接触到一个新特性,叫做字符串去重,该特性需要G1垃圾回收器,该垃圾回收器默认是被关闭的。

字符串去重利用了字符串内部实际是char数组,并且是final的特性,所以JVM可以任意的操纵他们。

对于字符串去重,开发者考虑了大量的策略,但最终的实现采用了下面的方式:

无论何时垃圾回收器访问了String对象,它会对char数组进行一个标记。它获取char数组的hash value并把它和一个对数组的弱引用存在一起。只要垃圾回收器发现另一个字符串,而这个字符串和char数组具有相同的hash code,那么就会对两者进行一个字符一个字符的比对。

如果他们恰好匹配,那么一个字符串就会被修改,指向第二个字符串的char数组。第一个char数组就不再被引用,也就可以被回收了。

这整个过程当然带来了一些开销,但是被很紧实的上限控制了。例如,如果一个字符未发现有重复,那么一段时间之内,它会不再被检查。

那么该特性实际上是怎么工作的呢?首先,你需要刚刚发布的Java 8 Update 20,然后按照这个配置: -Xmx256m -XX:+UseG1GC 去运行下列的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LotsOfStrings {
private static final LinkedList LOTS_OF_STRINGS = new LinkedList<>();

public static void main(String[] args) throws Exception {
int iteration = 0;
while (true) {
for (int i = 0; i < 100; i++) {
for (int j = 0; j < 1000; j++) {
LOTS_OF_STRINGS.add(new String("String " + j));
}
}
iteration++;
System.out.println("Survived Iteration: " + iteration);
Thread.sleep(100);
}
}
}

这段代码会执行30个迭代之后报OutOfMemoryError。

现在,开启字符串去重,使用如下配置去跑上述代码:

-Xmx256m -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics

此时它已经可以运行更长的时间,而且在50个迭代之后才终止。

6:

ArrayLIst 底层是数组 扩容会拷贝
hashmap 底层也是数组+ 链表 扩容 重新计算key 负载因子是 0.75

linklist底层是双向链表
1. 尽量重用对象,不要循环创建对象,比如:for 循环字符串拼接(不在 for中使用+拼接,先new 一个StringBuilder再在 for 里 append)

2. 容器类初始化的地时候指定长度

List collection = new ArrayLIst(5);

Map<String, String> map = new HashMap<String, String>(32);

3. ArrayList(底层数组)随机遍历快,LinkedList(底层双向链表)添加删除快

4. 集合遍历尽量减少重复计算

5. 使用 Entry 遍历 Map可以同时取出key和value

6. 大数组复制使用System.arraycopy 底层是native实现的

7. 尽量使用基本类型而不是包装类型

public class Test03 {

public static void main(String[] args) {
Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;

System.out.println(f1 == f2);
System.out.println(f3 == f4);
}
}

如果不明就里很容易认为两个输出要么都是true要么都是false。首先需要注意的是f1、f2、f3、f4四个变量都是Integer对象引用,所以下面的==运算比较的不是值而是引用。装箱的本质是什么呢?当我们给一个Integer对象赋一个int值的时候,会调用Integer类的静态方法valueOf,如果看看valueOf的源代码就知道发生了什么。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
简单的说,如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象,所以上面的面试题中f1==f2的结果是true,而f3==f4的结果是false。
8. 不要手动调用 System.gc()

9. 及时消除过期对象的引用,防止内存泄漏
public string pop()
{
string currentValue=object[size];
//object[size]=null;如果不添加这句话就会造成内存泄漏
size–;
return currentValue;
}

10. 尽量使用局部变量,减小变量的作用域 方便出了作用域尽快垃圾回收

11. 尽量使用非同步的容器ArraryList vs. Vector

12. 尽量减小同步作用范围, synchronized 方法 vs. 代码块

public class SynchronizedTest {
public static void main(String[] args) {
}
public synchronized void f1() {//在this對象上加鎖
System.out.println(“f1”);
}
public void f2() {//在this對象上加鎖
synchronized(this) {
System.out.println(“f2”);
}
}
public static synchronized void f3() {//在类上加鎖
System.out.println(“f3”);
}
public static void f4() {//在类上加鎖
synchronized(SynchronizedTest.class) {
System.out.println(“f4”);
}
}
}

13. 用ThreadLocal 缓存线程不安全的对象,SimpleDateFormat 缓存重量的对象避免重新构造
@SuppressWarnings(“rawtypes”)
private static ThreadLocal threadLocal = new ThreadLocal() {
protected synchronized Object initialValue() {
return new SimpleDateFormat(DATE_FORMAT);
}
};

14. 尽量使用延迟加载

15. 尽量减少使用反射,必须用加缓存,反射比较影响性能

16. 尽量使用连接池、线程池、对象池、缓存

17. 及时释放资源, I/O 流、Socket、数据库连接

18. 慎用异常,不要用抛异常来表示正常的业务逻辑,异常也是比较重的对象要记录堆栈信息

19. String 操作尽量少用正则表达式 比如replaceAll是用正则 比较耗费性能 replace就不是用正则

20. 日志输出注意使用不同的级别

21. 日志中参数拼接使用占位符
log.info(“orderId:” + orderId); 不推荐 会用字符串拼接
log.info(“orderId:{}”, orderId); 推荐 用占位符 不会进行字符串拼接

7:JVM的参数类型

标准参数(各版本中保持稳定)

-help

-server -client

-version -showversion

-cp -classpath

X 参数(非标准化参数)

-Xint:解释执行

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

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

示例:

java -version(默认是混合模式)

Java HotSpot(TM) 64-Bit Server VM (build 25.40-b25, mixed mode)

java -Xint -version

Java HotSpot(TM) 64-Bit Server VM (build 25.40-b25, interpreted mode)

XX 参数(非标准化参数)

主要用于 JVM调优和 debug

  • Boolean类型

格式:-XX:[+-]表示启用或禁用 name 属性
如:-XX:+UseConcMarkSweepGC
-XX:+UseG1GC

  • 非Boolean类型

格式:-XX:=表示 name 属性的值是 value
如:-XX:MaxGCPauseMillis=500
-xx:GCTimeRatio=19
-Xmx -Xms属于 XX 参数
-Xms 等价于-XX:InitialHeapSize
-Xmx 等价于-XX:MaxHeapSize
-xss 等价于-XX:ThreadStackSize

查看

-XX:+PrintFlagsInitial 查看jvm初始值

-XX:+PrintFlagsFinal 查看jvm最终值

-XX:+UnlockExperimentalVMOptions 解锁实验参数

-XX:+UnlockDiagnosticVMOptions 解锁诊断参数

-XX:+PrintCommandLineFlags 打印命令行参数

输出结果中=表示默认值,:=表示被用户或 JVM 修改后的值

示例:java -XX:+PrintFlagsFinal -version

补充:测试中需要用到 Tomcat,CentOS 7安装示例如下

sudo yum -y ``install java-1.8.0-openjdk*
wget http://mirror.bit.edu.cn/apache/tomcat/tomcat-8/v8.5.32/bin/apache-tomcat-8.5.32.tar.gz
tar -zxvf apache-tomcat-8.5.32.tar.gz
mv apache-tomcat-8.5.32 tomcat
cd tomcat/bin/sh startup.sh

pid 可通过类似 ps -ef|grep tomcat或 jps来进行查看

jps

查看java进程 -l 可以知道完全类名

jinfo

jinfo -flag MaxHeapSize

jinfo -flags 手动赋过值的参数

jstat

可以查看jvm的统计信息 如类加载。垃圾回收信息,jit编译信息

详情参考 jstat 官方文档

jstat 使用示例

类加载

以下1000表每隔1000ms 即1秒,共输出10次

jstat -class 1000 10

垃圾收集

-gc, -gcutil, -gccause, -gcnew, -gcold

jstat -gc 1000 10

以下大小的单位均为 KB

S0C, S1C, S0U, S1U: S0和 S1的总量和使用量

EC, EU: Eden区总量与使用量

OC, OU: Old区总量与使用量

MC, MU: Metacspace区(jdk1.8前为 PermGen)总量与使用量

CCSC, CCSU: 压缩类区总量与使用量

YGC, YGCT: YoungGC 的次数与时间

FGC, FGCT: FullGC 的次数与时间

GCT: 总的 GC 时间

JIT 编译

-compiler, -printcompilation

一个对象默认分配在堆上面 但是有个指针指向class默认是64位长指针,可以设置为用32位存储在压缩类空间

非堆区 即对应于虚拟机规范中的方法区 是操作系统本地内存 独立于jvm堆区之外 jdk8后面叫metaspace jdk8前面叫performancespace

codecache 存储的是jit即时编译的代码 以及native代码

jmap+MAT

详情参考jmap 官方文档

内存溢出演示:

https://start.spring.io/生成初始代码

最终代码:monitor_tuning

为快速产生内存溢出,右击 Run As>Run Configurations, Arguments 标签VM arguments 中填入

-Xmx32M -Xms32M

访问 http://localhost:8080/heap

Exception in thread “http-nio-8080-exec-2” Exception in thread “http-nio-8080-exec-1” java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: Java heap space

-XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M(同时在 pom.xml 中加入 asm 的依赖)

访问 http://localhost:8080/nonheap

Exception in thread “main” java.lang.OutOfMemoryError: Metaspace
Exception in thread “ContainerBackgroundProcessor[StandardEngine[Tomcat]]“ java.lang.OutOfMemoryError: Metaspace

内存溢出自动导出

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=./

右击 Run As>Run Configurations, Arguments 标签VM arguments 中填入

-Xmx32M -Xms32M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./

可以看到自动在当前目录中生成了一个java_pid660.hprof文件

java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to ./java_pid660.hprof …

另一种导出溢出也更推荐的方式是jmap

option: -heap, -clstats, -dump:, -F

jmap -dump:format=b,file=heap.hprof

jmap 导出溢出文件

MAT下载地址:http://www.eclipse.org/mat/

找开上述导出的内存溢出文件即可进行分析,如下图的溢出源头分析:

Memory Analyzer 内存溢出分析

  1. Histogram可以列出内存中的对象,对象的个数以及大小。
  2. Dominator Tree可以列出那个线程,以及线程下面的那些对象占用的空间。

Histogram

[![](_v_images/20200117100109236_4753.png)](http://static.oschina.net/uploads/space/2014/0702/120039_qSi5_1767531.png)
  • Class Name : 类名称,java类名

  • Objects : 类的对象的数量,这个对象被创建了多少个

  • Shallow Heap :一个对象内存的消耗大小,不包含对其他对象的引用

  • Retained Heap :是shallow Heap的总和,也就是该对象被GC之后所能回收到内存的总和

Dominator Tree

我们可以看到ibatis占了较多内存

快速找出某个实例没被释放的原因,可以右健 Path to GC Roots–>exclue all phantom/weak/soft etc. reference :

得到的结果是:

从表中可以看出 PreferenceManager -> … ->HomePage这条线路就引用着这个 HomePage实例。用这个方法可以快速找到某个对象的 GC Root,一个存在 GC Root的对象是不会被 GC回收掉的.

jstack

详情参考 jstack 官方文档

jstack 打印jvm内部所有的线程

jstack 15672 >15673.txt 导出当前进程文件

可查看其中包含java.lang.Thread.State: WAITING (parking),JAVA 线程包含的状态有:

NEW:线程尚未启动

RUNNABLE:线程正在 JVM 中执行

BLOCKED:线程在等待监控锁(monitor lock)

WAITING:线程在等待另一个线程进行特定操作(时间不确定)

TIMED_WAITING:线程等待另一个线程进行限时操作

TERMINATED:线程已退出

此时会生成一个monitor_tuning-0.0.1-SNAPSHOT.jar的 jar包,为避免本地的 CPU 消耗过多导致死机,建议上传上传到虚拟机进行测试

nohup java -jar monitor_tuning-0.0.1-SNAPSHOT.jar &

访问 http://xx.xx.xx.xx:12345/loop(端口12345在application.properties文件中定义)

top 是查询所有进程的cpu 占用率
top还可以用来显示一个进程中各个线程CPU的占用率:top -p -H
top命令如下

top -p -H 命令如下 看的是7930的进程

使用 jstack 可以导出追踪文件,文件中 PID 在 jstack 中显示的对应 nid 为十六进制(命令行可执行 print ‘%x’ 可以进行转化,如1640对应的十六进制为668)

“http-nio-12345-exec-3” #18 daemon prio=5 os_prio=0 tid=0x00007f10003fb000 nid=0x668 runnable [0x00007f0fcf8f9000]
java.lang.Thread.State: RUNNABLE
at org.alanhou.monitor_tuning.chapter2.CpuController.getPartneridsFromJson(CpuController.java:77)

访问http://xx.xx.xx.xx:12345/deadlock(如上jstack 导出追踪记录会发现如下这样的记录)

Java stack information for the threads listed above:

“Thread-5”:
at org.alanhou.monitor_tuning.chapter2.CpuController.lambda$deadlock$1(CpuController.java:41)
- waiting to lock <0x00000000edcf3470> (a java.lang.Object)
- locked <0x00000000edcf3480> (a java.lang.Object)
at org.alanhou.monitor_tuning.chapter2.CpuController$$Lambda$337/547045985.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
“Thread-4”:
at org.alanhou.monitor_tuning.chapter2.CpuController.lambda$deadlock$0(CpuController.java:33)
- waiting to lock <0x00000000edcf3480> (a java.lang.Object)
- locked <0x00000000edcf3470> (a java.lang.Object)
at org.alanhou.monitor_tuning.chapter2.CpuController$$Lambda$336/1704575158.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

查看后台日志,都是使用tail -f catalina.out命令来查看

jvisualvm 图形化工具
插件安装Tools>Plugins>Settings根据自身版本(java -version)更新插件中心地址,各版本查询地址:
http://visualvm.github.io/pluginscenters.html
建议安装:Visual GC, BTrace Workbench
概述 监控可以堆dump 线程可以线程dump 抽样器可以对cpu和内存进行抽样调查

以上是本地的JAVA进程监控,还可以进行远程的监控,在上图左侧导航的 Applications 下的 Remote 处右击Add Remote Host…,输入主机 IP 即可添加,在 IP 上右击会发现有两种连接 JAVA 进程进行监控的方式:JMX, jstatd

bin/catalina.sh(以192.168.0.5为例)

JAVA_OPTS=”$JAVA_OPTS -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9004 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.net.preferIPv4Stack=true -Djava.rmi.server.hostname=192.168.0.5”

启动tomcat,

启动tomcat服务
方式一:直接启动 ./startup.sh
方式二:作为服务启动 nohup ./startup.sh &
查看tomcat运行日志
tail -f catalina.out

tomcat设置jvm参数
修改文件 apache-tomcat-9.0.10/bin下catalina.bat文件

以 JMX 为例,在 IP 上右击点击Add JMX Connection…,输入 IP:PORT

Add JMX Connection

以上为 Tomcat,其它 JAVA 进程也是类似的,如:

nohup java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9005 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.net.preferIPv4Stack=true -Djava.rmi.server.hostname=192.168.0.5 -jar monitor_tuning-0.0.1-SNAPSHOT.jar &

BTrace

BTrace 可以动态地向目标应用程序的字节码注入追踪代码,使用的技术有 JavaCompilerApi, JVMTI, Agent, Instrumentation+ASM

使用方法:JVisualVM中添加 BTrace 插件

方法二:btrace

btrace只能调试本地进程
btrace修改后的字节码不能被还原

pom.xml 中添加 btrace-agent, btrace-boot, btrace-client的依赖

拦截构造方法

拦截同名方法

拦截返回值

拦截行号

拦截异常信息

拦截复杂类型

拦截正则表达式

拦截环境参数信息

常用参数:

-Xms -Xmx

-XX:NewSize -XX:MaxNewSize

-XX:NewRatio -XX:SurvivorRatio

-XX:MetaspaceSize -XX:MaxMetaspaceSize 以下几个参数通常这样只设置这个值即可

-XX:+UseCompressedClassPointers

-XX:CompressedClassSpaceSize

-XX:InitialCodeCacheSize

-XX:ReservedCodeCacheSize

Tomcat 远程 Debug

JDWP

bin/startup.sh 修改最后一行(添加 jpda)

exec “$PRGDIR”/“$EXECUTABLE” jpda start “$@”

bin/catalina.sh 为便于远程调试进行如下修改

JPDA_ADDRESS=”localhost:8000”

修改为

JPDA_ADDRESS=”54321”

若发现54321端口启动存在问题可尝试bin/catalina.sh jpda start

使用 Eclipse 远程调试,右击 Debug As > Debug Configurations… > Remote Java Application > 右击 New 新建

普通java进程可以这样配置
java -jar -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=10001 access-10000.jar

tomcat-manager 监控

1.conf/tomcat-users.xml添加用户

2.conf/Catalina/localhost/manager.xml配置允许的远程连接



远程连接将allow=”127\.0\.0\.1”修改为allow=”^.*$”,浏览器中输入http://127.0.0.1:8080/manage或对应的 IP,用户名密码为tomcat-users.xml中所设置的

3.重启 Tomcat 服务

Tomcat Manager

psi-probe 监控

下载地址:https://github.com/psi-probe/psi-probe,

下载后进入psi-probe-master目录,执行:

mvn clean package -Dmaven.test.skip

将 web/target/probe.war放到 Tomcat 的 webapps 目录下,同样需要conf/tomcat-users.xml和conf/Catalina/localhost/manager.xml中的配置(可保持不变),启动 Tomcat 服务

浏览器中输入http://127.0.0.1:8080/probe或对应的 IP,用户名密码为tomcat-users.xml中所设置的

PSI Probe演示

Tomcat 调优

线程优化(webapps/docs/config/http.html):

maxConnections

acceptCount

maxThreads

minSpareThreads

配置优化(webapps/docs/config/host.html):

autoDeploy

enableLookups(http.html)

reloadable(context.html)

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

Session 优化:

如果是 JSP, 可以禁用 Session

Nginx 性能监控与调优

Nginx 安装

添加 yum 源(/etc/yum.repos.d/nginx.repo)

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/$basesearch/
gpgcheck=0
enabled=1

安装及常用命令

yum install -y nginx

systemctl status|start|stop|reload|restart nginx
nginx -s stop|reload|quit|reopen nginx 启动nginx
cat default.conf | grep -v “#’ > default2.conf 移除配置文件中的注释 并生成新的配置文件
nginx -V
nginx -t

配置反向代理 setenforce 0

ngx_http_stub_status 监控连接信息

location = /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}

可通过curl http://127.0.0.1/nginx_status 进行查看或注释掉 allow 和 deny 两行使用 IP 进行访问

ngxtop监控请求信息

查看官方使用方法:https://github.com/lebinh/ngxtop

安装 python-pip

yum install epel-release
yum install python-pip

安装 ngxtop

pip install ngxtop

使用示例

指定配置文件:ngxtop -c /etc/nginx/nginx.conf

查询状态是200:ngxtop -c /etc/nginx/nginx.conf -i ‘status == 200’

查询访问最多 ip:ngxtop -c /etc/nginx/nginx.conf -g remote_addr

ngxtop查询访问最多 ip

Nginx 优化

增加工作线程数和并发连接数

worker_processes 4; # 一般CPU 是几核就设置为几
events {
worker_connections 1024; # 每个进程打开的最大连接数,包含了 Nginx 与客户端和 Nginx 与 upstream 之间的连接
multi_accept on; # 可以一次建立多个连接
use epoll;
}

启用长连接

upstream server_pool{
server localhost:8080 weight=1 max_fails=2 fail_timeout=30s;
server localhost:8081 weight=1 max_fails=2 fail_timeout=30s;
keepalive 300; # 300个长连接
}
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection “upgrade”;
proxy_pass http://server\_pool;
}

启用缓存压缩

gzip on;
gzip_http_version 1.1;
gzip_disable “MSIE [1-6]\.(?!.*SV1)”;
gzip_proxied any;
gzip_types text/plain text/css application/javascript application/x-javascript application/json application/xml application/vnd.ms-fontobject application/x-font-ttf application/svg+xml application/x-icon;
gzip_vary on;
gzip_static on;

操作系统优化

配置文件/etc/sysctl.conf

sysctl -w net.ipv4.tcp_syncookies=1 # 防止一个套接字在有过多试图连接到时引起过载
sysctl -w net.core.somaxconn=1024 # 默认128,连接队列
sysctl -w net.ipv4.tcp_fin_timeout=10 # timewait 的超时时间
sysctl -w net.ipv4.tcp_tw_reuse=1 # os 直接使用 timewait的连接
sysctl -w net.ipv4.tcp_tw_recycle=0 # 回收禁用

/etc/security/limits.conf

  •           hard    nofile            204800
    
  •           soft    nofile             204800
    
  •           soft    core             unlimited
    
  •           soft    stack             204800
    

其它优化

sendfile on; # 减少文件在应用和内核之间拷贝
tcp_nopush on; # 当数据包达到一定大小再发送
tcp_nodelay off; # 有数据随时发送

Flink Data Types & Serialization

使用case class的坑

1
2
3
case class Event(id: Int) {
val lb = new ListBuffer[Int]
}
1
2
13:00:43,342 INFO  org.apache.flink.api.java.typeutils.TypeExtractor             - class org.myorg.quickstart.Event does not contain a setter for field id
13:00:43,343 INFO org.apache.flink.api.java.typeutils.TypeExtractor - Class class org.myorg.quickstart.Event cannot be used as a POJO type because not all fields are valid POJO fields, and must be processed as GenericType. Please read the Flink documentation on "Data Types & Serialization" for details of the effect on performance.

提示信息:找不到setter,对于POJO类型必须所有的字段必须要有setter和getter
命名是case class啊

再看生产环境的例子: 折腾了一下午

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
53
54
55
56
case class Event(uin: String,
sPid: String,
applyId: String,
bankType: Long,
transactionId: String,
amount: Long,
createTime: Long,
bizType: Long,
modifyTime: String,
equ: ListBuffer[Int]
) {

def this(uin: String,
sPid: String,
applyId: String,
bankType: Long,
transactionId: String,
amount: Long,
createTime: Long,
bizType: Long,
modifyTime: String,
equ: List[Int]
) = {
this(uin, sPid, applyId, bankType, transactionId, amount, createTime, bizType, modifyTime)
if (equ != null) {
equities.appendAll(equ)
}
}


private var _active = false
val equities: ListBuffer[Int] = new ListBuffer[Int]

def addEquity(id: Int): Event = {
equities.append(id)
this
}

def equityString(separator: String): String = {
equities.mkString(separator)
}

def setActive(): Event = {
_active = true
this
}

def setActive(active: String): Event = {
_active = "1".equals(active)
this
}

def isActive() = {
_active
}
}

使用的是flink 1.6版本的,case class识别出来了,但是equities没有传递到下一个算子中,始终没有值

老老实实的修改成普通类

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
class Event(_uin: String,
_sPid: String,
_applyId: String,
_bankType: Long,
_transactionId: String,
_amount: Long,
_createTime: Long,
_bizType: Long,
_modifyTime: String
) extends Serializable {


private var equities: ListBuffer[Int] = new ListBuffer[Int]
private var uin: String = _uin
private var sPid: String = _sPid
private var applyId: String = _applyId
private var bankType: Long = _bankType
private var transactionId: String = _transactionId
private var amount: Long = _amount
private var createTime: Long = _createTime
private var bizType: Long = _bizType
private var modifyTime: String = _modifyTime
private var _active = false

def this(uin: String,
sPid: String,
applyId: String,
bankType: Long,
transactionId: String,
amount: Long,
createTime: Long,
bizType: Long,
modifyTime: String,
equ: List[Int]
) = {
this(uin, sPid, applyId, bankType, transactionId, amount, createTime, bizType, modifyTime)
if (equ != null) {
equities.appendAll(equ)
}
}

def getUin: String = uin

def setUin(Uin: String): Unit = {
this.uin = Uin
}

def getSPid: String = sPid

def setSPid(SPid: String): Unit = {
this.sPid = SPid
}

def getApplyId: String = applyId

def setApplyId(ApplyId: String): Unit = {
this.applyId = ApplyId
}

def getBankType: Long = bankType

def setBankType(BankType: Long): Unit = {
this.bankType = BankType
}

def getTransactionId: String = transactionId

def setTransactionId(TransactionId: String): Unit = {
this.transactionId = TransactionId
}

def getAmount: Long = amount

def setAmount(Amount: Long): Unit = {
this.amount = Amount
}

def getCreateTime: Long = createTime

def setCreateTime(CreateTime: Long): Unit = {
this.createTime = CreateTime
}

def getBizType: Long = bizType

def setBizType(BizType: Long): Unit = {
this.bizType = BizType
}

def getModifyTime: String = modifyTime

def setModifyTime(ModifyTime: String): Unit = {
this.modifyTime = ModifyTime
}

def addEquity(id: Int): Event = {
equities.append(id)
this
}

def equityString(separator: String): String = {
equities.mkString(separator)
}

def setActive(): Event = {
_active = true
this
}

def setActive(active: String): Event = {
_active = "1".equals(active)
this
}

def isActive() = {
_active
}

def rights(split: String) = {
RightEvent(this, equities.mkString(split))
}

def getEquities = equities

def setEquities(equities: ListBuffer[Int]) = {
this.equities = equities
}
}

可以了

初步估计,序列化除了问题

flink 类型和序列化机制
flink 支持的数据类型

Java Tuples 跟 Scala Case 类
Java POJOs
基础类型(Primitive Types : int/long/string/char/short/boolean 等)
普通的类(非POJO)
Values
Hadoop Writable
特殊类型(Scala : Either, Option, Try; Java : List, Map)
flink 支持的序列化

Tuple
Row
Pojo
Avro
Protobuf (via Kryo)
Thrift (via Kryo)
Kryo


flink 序列化性能
flink serialization performance results

可以看到 flink 内置的 Tuple 跟 Row 性能最好, POJO 次之,
一般 Tuple 跟 POJO使用的频率最高, 但是只有POJO 跟 Avro 支持 Schema 升级
POJO 一不小心就可能回退到 Kryo
POJO 第一个要求是符合 Java Bean 规范, 但是目前(1.12.2) 还不支持特殊的类型(List, Map等)
为了避免 POJO序列化回退, 开发过程中可以开启

env.getConfig().disableGenericTypes();
当POJO 中包含List/Map 处理方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Pojo1 {
public int id;
public List<String> names
}
public static void main(String[] args) {
ExecutionConfig config = new ExecutionConfig();
config.disableGenericTypes();

TypeInformation<Pojo1> information = Types.POJO(Pojo1.class);
// TypeInformation<Pojo1> information = TypeInformation.of(Pojo1.class);

//Generic types have been disabled in the ExecutionConfig and type java.util.List is treated as a generic type.
TypeSerializer<Pojo1> serializer = information.createSerializer(config);

}

因为POJO包含了不支持的List该序列化, names 字段序列化会回退到 Kryo序列化

解决方案

1: 把 List 换成 Array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  public static class Pojo1 {
public int id;
// public List<String> names;
public String[] names;
}

public static void main(String[] args) {
ExecutionConfig config = new ExecutionConfig();
config.disableGenericTypes();

TypeInformation<Pojo1> information = Types.POJO(Pojo1.class);

TypeSerializer<Pojo1> serializer = information.createSerializer(config);

}

2: 指定 POJO 字段 TypeInformation

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
  public static void main(String[] args) {
ExecutionConfig config = new ExecutionConfig();
config.disableGenericTypes();

Map<String, TypeInformation<?>> map = new HashMap<>();
map.put("id", Types.INT);
map.put("names", Types.LIST(Types.STRING));
TypeInformation<Pojo1> information = Types.POJO(Pojo1.class, map);

TypeSerializer<Pojo1> serializer = information.createSerializer(config);
}
```
3: 自定义 TypeInfoFactory

```java
@TypeInfo(MyPojo1Factory.class)
public static class Pojo1 {
public int id;
public List<String> names;
}

public static class MyPojo1Factory extends TypeInfoFactory<Pojo1> {

@Override
public TypeInformation<Pojo1> createTypeInfo(Type t, Map<String, TypeInformation<?>> genericParameters) {
Map<String, TypeInformation<?>> map = new HashMap<>();
map.put("id", Types.INT);
map.put("names", Types.LIST(Types.STRING));
TypeInformation<Pojo1> information = Types.POJO(Pojo1.class, map);
return information;
}
}

public static void main(String[] args) {
ExecutionConfig config = new ExecutionConfig();
config.disableGenericTypes();

TypeInformation<Pojo1> information = Types.POJO(Pojo1.class);
// TypeInformation<Pojo1> information = TypeInformation.of(Pojo1.class);
TypeSerializer<Pojo1> serializer = information.createSerializer(config);

}

引用
https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/dev/types_serialization.html
https://flink.apache.org/news/2020/04/15/flink-serialization-tuning-vol-1.html

Flink的序列化
Flink实现了自己的序列化框架,并结合自身的内存模型,实现了对象的密集存储也高效操作。

Flink序列化框架

可以看出这种序列化方式存储密度是相当紧凑的。其中 int 占4字节,double 占8字节,POJO多个一个字节的header,PojoSerializer只负责将header序列化进去,并委托每个字段对应的serializer对字段进行序列化。
memory pool 内存池 memorySegment的数据结构,由两部分组成,一部分是存储key+pointer(完整二进制数据的指针以及定长的序列化后的key),第二部分是对象的二进制数据
如下图:

使用内存池管理内存和使用二进制存储数据的的好处:
避免oom,所有的运行时数据结构和算法只能通过内存池申请内存,保证了其使用的内存大小是固定的,不会因为运行时数据结构和算法而发生OOM。在内存吃紧的情况下,算法(sort/join等)会高效地将一大批内存块写到磁盘,之后再读回来。因此,OutOfMemoryErrors可以有效地被避免。
节省内存空间,Java 对象在存储上有很多额外的消耗,使用二进制可以避免。
高效的二进制操作 & 缓存友好的计算,第一,交换定长块(key+pointer)更高效,不用交换真实的数据也不用移动其他key和pointer。第二,这样做是缓存友好的,因为key都是连续存储在内存中的,可以大大减少 cache miss(cpu读取L1,L2,L3高速缓存速度高于读取主内存速度几个数量级,使用key+pointer极大提高缓存L1,L2,L3命中率)
注意:Flink 中,排序会先用 key 比大小,这样就可以直接用二进制的key比较而不需要反序列化出整个对象。因为key是定长的,如果key相同(或者没有提供二进制key),那就必须将真实的二进制数据反序列化出来,然后再做比较。之后,只需要交换key+pointer就可以达到排序的效果,真实的数据不用移动。

1)该类型系统与SQL的兼容性不好。
2)无法控制Decimal类型的精度。
3)无法区分char和varchar类型。
4)物理类型和逻辑类型紧耦合。
5)物理类型是类型描述,而不是类型的序列化/反序列化器。

Flink SQL引入了新的LogicalTypes类型系统
TypeInformation类型系统是为DataStream/DataSet API设计的

DataType有两个职责:
1)声明逻辑类型LogicalType。
2)运行时逻辑转换类,允许为空。

类型推断:

Java的Flink应用使用反射机制获取Function的输入和输出类型。Scala使用Scala Macro类提取类型。

类型提取:

泛型的类型推断:

Java的泛型机制是在编译级别实现的。编译器生成的字节码在运行期间并不包含泛型的类型信息
使用TypeHint的匿名类来获取泛型的类型信息

Lambda函数的类型提取

Eclipse的JDT编译器会把Lambda函数的泛型签名等信息写入编译后的字节码中,而对于javac等常见的其他编译器,则不会这样做,因而Flink就无法获取具体类型信息了

(1)Java类型擦除的原因

1)避免JVM的重构。如果JVM将泛型类型延续到运行期,那么到运行期时JVM就需要进行大量的重构工作,提高了运行期的效率。
2)版本兼容。在编译期擦除可以更好地支持原生类型(Raw Type)。

(2)Java泛型类型擦除规则
1)如果是继承基类而来的泛型,就用getGenericSuperclass(), 转型为ParameterizedType来获得实际类型。
2)如果是实现接口而来的泛型,就用getGenericInterfaces(), 针对其中的元素转型为ParameterizedType来获得实际类型。
3)Java泛型在字节码中会被擦除,并不总是擦除为Object类型,而是擦除到上限类型。

显示类型:

Flink提供了等价的Types类
org.apache.flink.api.common.typeinfo.Types),Types作为类型声明的统一入口,基本涵盖了常用类型。

类型擦除带来的问题

  1. Lambda函数的类型提取因为类型擦除导致Lambda函数的类型提取并不能总是有效的,有时候需要手动指定类型。
  2. Kryo的JavaSerializer在Flink下存在Bug,可能导致ClassNotFound异常推荐使用org.apache.flink.api.java.typeutils.runtime.kryo.JavaSerializer,而非com.esot-ericsoftware.kryo.serializers.JavaSerializer,以防止与Flink不兼容

SQL类型系统

Flink SQL中则使用DataType中的LogicalType类型系统来描述类型信息
LogicalType类型系统与SQL标准基本保持一致,同时增加了一些额外的信息,如是否可以为null等,目的是提高scala expression(标量表达式)的处理效率。

Flink SQL执行时,最终转换为了FlinkDataStream/DataSet应用,此时就需要TypeInfomation类型信息来实现序列化/反序列化,所以SQL逻辑类型LogicalType需要转换为TypeInfomation

1)org.apache.flink.types.Row:在Flink Planner中使用,是1.9版本之前FlinkSQL使用的Row结构,在SQL相关的算子、UDF函数、代码生成中都是使用该套Row结构。
2)org.apache.flink.table.dataformat.BaseRow及其子类:是在Blink Runtime和Blink Planner中使用的新的Row类型数据结构,在Blink算子、UDF函数和代码生成中使用此结构。

Blink Row总览

ColumnarRow

ColumnarRow是一种内存列式存储结构,每一列的抽象结构为ColumnVector。在当前的实现中,只支持堆上ColumnVector,堆外的ColumnVector尚不被支持。堆上ColumnVector本质上是使用Java原始类型数据保存一列的数据。Orc类型的列式存储使用了ColumnarRow。对于查询类的请求,使用列式存储能够提高CPU缓存命中率。CPU的数据预读取策略总是尝试将相邻的数据预读取到缓存中,因为列式存储形式中一列数据总是紧邻的,与行式数据相比,访问同一个字段的时候,CPU缓存命中率更高,因此CPU就无须浪费宝贵的事件周期去等待数据从内存加载,从而提高计算效率,如图5-8所示。

序列化

MapFunction使用了匿名内部类的方式实现,默认内部类会持有一个外部对象的引用this$0,如果外部对象不实现序列化接口,内部类的序列化会失败,所在Flink中使用ASM操作字节码将匿名内部类中的this$0设置为null。在FlinkDataStreamp的map、filter、keyBy等接口中都使用了ClosureCleaner#clean方法来设置this$0。

如果开发者在编写Flink应用过程中使用了自定义类型,并且又没有提供类型的注册和序列化/反序列化方法,Flink就无法对该类型进行该自定义序列化/反序列化。此时为了Flink的正常运行,对于这一类的数据类型,无法识别的类型就会交给Kryo进行序列化。Kryo可以对任意类型的Java对象进行序列化,是一种Java中的通用序列化方式,缺点是序列化/反序列化效率相对较低。

Flink HBase connectors

flighting

奇怪,没有依赖HBase-Client,而是依赖了HBase-server

1
2
3
4
5
6
7
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-hadoop2-compat</artifactId>
<version>${hbase.version}</version>
<scope>test</scope>
<type>test-jar</type>
</dependency>
1
2
3
4
5
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-server</artifactId>
<version>${hbase.version}</version>
</dependency>

添加文件到ClassLoader的classpath

1
2
3
4
5
6
7
8
9
10
11
12
// Get the classloader actually used by HBaseConfiguration
ClassLoader classLoader = HBaseConfiguration.create().getClassLoader();
if (!(classLoader instanceof URLClassLoader)) {
fail("We should get a URLClassLoader");
}

// Make the addURL method accessible
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);

// Add the directory where we put the hbase-site.xml to the classpath
method.invoke(classLoader, directory.toURI().toURL());