一个正则表达式导致 CPU 高的问题排查过程

这篇文章记录一个正则表达是导致 CPU 高的问题排查。由于无法直接使用线上的代码测试,所以我自己把代码整理了下来,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class AppMain {
public static void main(String[] args) throws InterruptedException {
final String regex="^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$";
final String email="blog.laofu.online.fuweilao@vip.qq.com#";
for (int i = 0; i < 1000000; i++) {
Matcher matcher = RegexUtils.matcher(regex, email);
matcher.find();
Thread.sleep(10);
// matcher.group();
}
}
}

当运行程序的时候,我们可以看到 java 的进程占用了 CPU 了 82.1%,由于我使用的服务器是 1核+2G, 所以 load avg 占用也很高。

阅读更多

字节码判断方法参数的个数

Jvm 如何确定方法的参数的个数

找到 Method 的 DescriptionIndex 的属性,找到对应的描述,例如:

1
2
3
4
5
6
7
8
9
10
11
public class AddMain {
public static void main(String[] args) {

int c = add(100,200);
System.out.println(c);
}

private static int add(int a, int b) {
return a + b;
}
}

这个例子中的 java 代码,add 方法对应的代码是 (II)I,最后一个 I 代表返回值,这个代表两个整型的参数.

1
2
3
private static int add(int a, int b,String c,boolean d) {
return a + b;
}

​ 同样,(IILjava/lang/String;Z)I 代表有4个参数,字符串的表示是:Ljava/lang/String;,解析比较特殊。

阅读更多

手写一个简单的JVM--01. 解析Class文件

java的运行过程

在运行一段 java 代码的时候需要经过编译,验证,加载运行,具体如下图:

运行过程

对于 Java 源码变成字节码的编译过程,我们暂且跳过不讨论。

想弄清楚 java 代码的运行原理,其实本质就是 java 字节码如何被 jvm 执行。

阅读更多

JVM指令的速记

在学习的JVM的时候,最重要的是认识JVM的指令,JVM指令很多,为了方便记忆,可以根据前缀和功能进行分类:

例如:nop指令代表是一个空指令,JVM收到指令后,什么都不用做,等待下一个指令。

阅读更多

Java如何实现零拷贝

什么是零拷贝

在操作系统中,从内核的形态区分,可以分为内核态(Kernel Space)和用户态(User Space)。

在传统的IO中,如果把数据通过网络发送到指定端的时候,数据需要经历下面的几个过程:

IO

  1. 当调用系统函数的时候,CPU执行一系列准备工作,然后把请求发送给DMA处理(DMA可以理解为专门处理IO的组件),DMA将硬盘数据通过总线传输到内存中。

  2. 当程序需要读取内存的时候,这个时候会执行CPU Copy,内存会有内核态写入用户的缓存区。

  3. 系统调用write()方法时,数据从用户态缓冲区写入到网络缓冲区(Socket Buffer), 由用户态编程内核态。

  4. 最后由DMA写入网卡驱动中,传输到网卡的驱动。

可以看到,传统的IO的读写,数据会经历4次内存的拷贝,这种拷贝拷贝会带来资源的浪费和效率的底下。


如何实现零拷贝


内存映射方式I/O

阅读更多

Java中实现顺序IO

顺序IO和随机IO


对于磁盘的读写分为两种模式,顺序IO和随机IO。 随机IO存在一个寻址的过程,所以效率比较低。而顺序IO,相当于有一个物理索引,在读取的时候不需要寻找地址,效率很高。

网上盗了一个图(侵权删)
IO


Java中的随机读写


在Java中读写文件的方式有很多种,先总结以下3种方法:

  1. FileWriter和FileReader

    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
    public static void fileWrite(String filePath, String content) {
    File file = new File(filePath);
    //创建FileWriter对象
    FileWriter writer = null;
    try {
    //如果文件不存在,创建文件
    if (!file.exists())
    file.createNewFile();
    writer = new FileWriter(file);
    writer.write(content);//写入内容
    writer.flush();
    writer.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    public static void fileRead(String filePath) {
    File file = new File(filePath);
    if (file.exists()) {
    try {
    //创建FileReader对象,读取文件中的内容
    FileReader reader = new FileReader(file);
    char[] ch = new char[1];
    while (reader.read(ch) != -1) {
    System.out.print(ch);
    }
    reader.close();
    } catch (IOException ex) {
    ex.printStackTrace();
    }

    }
    }

阅读更多

ServiceLoader的使用

获得接口的实现类有点困难

在Java中,由于反射的局限性,无法直接获取一个接口的所有实现子类,所以为了能够实现一个接口动态的注入实现的子类对象,需要借助ServiceLoader

简单的Demo使用

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

public interface IService {
void doSomeThing();
}

public class DefalutService implements IService{
@Override
public void doSomeThing() {
RzLogger.info("默认服务");
}
}

public class LogService implements IService {
@Override
public void doSomeThing() {
RzLogger.info("日志服务");
}
}


public static void main(String[] args) {
ServiceLoader<IService> loader = ServiceLoader.load(IService.class);
for (IService service : loader) {
service.doSomeThing();
}
}

如果直接运行,可以发现没有任何结果,需要在META-INF\services创建一个文件xxx.xxx.IService(是接口类的全类名) ,内容是两个子类的全类名:

1
2
learnJava.rz08.DefalutService
learnJava.rz08.LogService

再次运行结果:

1
2
21:55:48,873  INFO [main] (RzLogger.java:12) - 默认服务
21:55:48,877 INFO [main] (RzLogger.java:12) - 日志服务
阅读更多

一个有效的收拾程序运行残局的方法--ShutdownHook

发现addShutdownHook

在阅读QMQ的源码的时候,在Server端启动的时候,注册了一个shutdown的代码,具体的代码如下:

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
 Runtime.getRuntime().addShutdownHook(new Thread(wrapper::destroy));

```

#### addShutdownHook作用
`addShutdownHook`方法可以添加一个指定的线程来在Java程序退出的时候做一些事情,在以下几个场景会被调用:

1. 程序运行完成后退出
2. 使用Ctrl+C时终端退出
3. 调用系统退出的方法, `System.exit(0)`

具体的使用Demo如下:

``` java
public class AppMain {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(AppMain::exit01));
Runtime.getRuntime().addShutdownHook(new Thread(AppMain::exit02));
Runtime.getRuntime().addShutdownHook(new Thread(AppMain::exit03));
while (true){
System.out.println(".....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
public static void exit01(){
System.out.println("exit01");
}
public static void exit02(){
System.out.println("exit02");
}
public static void exit03(){
System.out.println("exit03");
}
}

```

执行当执行退出的时候:
![程序退出](/img/assets/67/01.png)




#### addShutdownHook的使用场景

addShutdownHook的使用场景很多, 尤其是在通信的模块功能,比如Netty为例,当服务端或者客户端异常断开的时候,需要告诉对方释放资源,从而保证不会触发异常机制。

例如分布式队列,RPC框架都需要在异常后,释放相关资源。


#### addShutdownHook使用注意几点

1. 在多次使用addShutdownHook增加退出的方法是,由于是开一个线程,所以不能保证方法的执行是按顺序的。 所以尽量保证方法在一个方法里面完成。

2. 在释放的时候不应该增加阻塞的方法,否者会导致推出时阻塞,导致无法退出

``` java
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
while (true) {
//
}
}));

hooks相关源码

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
 public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}

public boolean removeShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
return ApplicationShutdownHooks.remove(hook);
}

```

``` java

class ApplicationShutdownHooks {
/* The set of registered hooks */
private static IdentityHashMap<Thread, Thread> hooks;
static {
try {
Shutdown.add(1 /* shutdown hook invocation order */,
false /* not registered if shutdown in progress */,
new Runnable() {
public void run() {
runHooks();
}
}
);
hooks = new IdentityHashMap<>();
} catch (IllegalStateException e) {
// application shutdown hooks cannot be added if
// shutdown is in progress.
hooks = null;
}
}


private ApplicationShutdownHooks() {}

/* Add a new shutdown hook. Checks the shutdown state and the hook itself,
* but does not do any security checks.
*/
static synchronized void add(Thread hook) {
if(hooks == null)
throw new IllegalStateException("Shutdown in progress");

if (hook.isAlive())
throw new IllegalArgumentException("Hook already running");

if (hooks.containsKey(hook))
throw new IllegalArgumentException("Hook previously registered");

hooks.put(hook, hook);
}

/* Remove a previously-registered hook. Like the add method, this method
* does not do any security checks.
*/
static synchronized boolean remove(Thread hook) {
if(hooks == null)
throw new IllegalStateException("Shutdown in progress");

if (hook == null)
throw new NullPointerException();

return hooks.remove(hook) != null;
}

/* Iterates over all application hooks creating a new thread for each
* to run in. Hooks are run concurrently and this method waits for
* them to finish.
*/
static void runHooks() {
Collection<Thread> threads;
synchronized(ApplicationShutdownHooks.class) {
threads = hooks.keySet();
hooks = null;
}

for (Thread hook : threads) {
hook.start();
}
for (Thread hook : threads) {
while (true) {
try {
hook.join();
break;
} catch (InterruptedException ignored) {
}
}
}
}
}


```

#### Hooks管理问题

在很多开源的框架中,有的Hooks反而会起到反作用,我们怎么样能够移除我们不需要的Hooks呢?

在看了上面的代码,我们发现`ApplicationShutdownHooks` 不是一个`public`的类,JDK也没有提供对外管理的方法,只有`add`和`remove`方法。

通过分析`ApplicationShutdownHooks`类源码的结构,我们可以发现,用来存放hooks的文件的是一个`IdentityHashMap`容器,而且是一个静态的属性。

**从反射入手**

既然是一个变量,可以通过反射来获得对应的Hooks的值,代码如下:

``` java
String className = "java.lang.ApplicationShutdownHooks";
Class<?> clazz = Class.forName(className);
Field field = clazz.getDeclaredField("hooks");
field.setAccessible(true);
IdentityHashMap<Thread, Thread> map = (IdentityHashMap<Thread, Thread>) field.get(clazz);
for (Thread thread : map.keySet()) {
RzLogger.info("found shutdownHook: " + thread.getName());
}
```

增加三个Hook验证:

``` java

Runtime.getRuntime().addShutdownHook(new Thread(AppMain::exit01));
Runtime.getRuntime().addShutdownHook(new Thread(AppMain::exit02));
Runtime.getRuntime().addShutdownHook(new Thread(AppMain::exit03));


```

``` cte

23:33:11,102 INFO [main] (RzLogger.java:12) - found shutdownHook: Thread-2
23:33:11,106 INFO [main] (RzLogger.java:12) - found shutdownHook: Thread-0
23:33:11,106 INFO [main] (RzLogger.java:12) - found shutdownHook: Thread-1
exit03
exit01
exit02

收获

  1. ShutDown的使用,在程序退出后清理现场
  2. 一步一步去理解代码的原理和特性,细节很重要
阅读更多

CAS的性能问题

昨天写了一个计数器的类,性能高于JDK,思考了很久,后来被同学点破。

 public void increase() {
    long before = unsafe.getLongVolatile(this, offset);
    while (!unsafe.compareAndSwapLong(this, offset, before, before + 1))
    {
        before = unsafe.getLongVolatile(this, offset);
        Thread.yield();
    }
}

有人怀疑是测试的代码问题,后来发现并不是,真正的原因是:在高并发的环境下,CAS修改旧值时经常被其他线程中断,就会进行重试,不断的重试的代价就很高。yield操作能够缓解这个情况,但是也会带来多次上下文切换,在并发没那么高的情况下,反而更浪费资源

所以JDK在写的Atomic的类型的时候,应该是考虑到重试一次的代价,小于线程的上下文切换,所以并没有采用yield操作。

阅读更多