0%

本篇幅只是回顾使用eureka的时候如何钉钉告警。

Eureka钉钉告警

1
2
3
4
sequenceDiagram
eureka ->> eventListener : event
eventListener ->> 钉钉 : alarmNoticeSend
钉钉 ->> 开发人员 : eurekaInstanceInfo
  • 实例图

关键代码

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

@EventListener
public void listen(EurekaInstanceCanceledEvent event) {
log.error("告警通知 [{}] 服务注销 timestamp [{}] serverId [{}]",
event.getAppName(),
event.getTimestamp(),
event.getServerId());
if (!event.isReplication()) {
ServiceNotice serviceNotice = new ServiceNotice();
serviceNotice.setTitle("告警通知 服务下线");
serviceNotice.setAppName(event.getAppName());
serviceNotice.setTimestamp(event.getTimestamp());
serviceNotice.setServerId(event.getServerId());
serviceNotice.setProfiles(active);
alarmNoticeManage.createNotice(serviceNotice, "");
}
}

@EventListener
public void listen(EurekaInstanceRegisteredEvent event) {
log.info("告警通知 [{}] 服务注册 timestamp [{}] id [{}] ipAddr [{}] ",
event.getInstanceInfo().getAppName(),
event.getTimestamp(),
event.getInstanceInfo().getId(),
event.getInstanceInfo().getIPAddr()
);
if (!event.isReplication()) {
ServiceNotice serviceNotice = new ServiceNotice();
serviceNotice.setTitle("告警通知 服务上线");
serviceNotice.setAppName(event.getInstanceInfo().getAppName());
serviceNotice.setTimestamp(event.getTimestamp());
serviceNotice.setServerId(event.getInstanceInfo().getId());
serviceNotice.setProfiles(active);
alarmNoticeManage.createNotice(serviceNotice, "");
}
}
  • 效果如下
1
2
3
告警通知 服务下线
2020-07-16 16:03:20 Z-GATEWAY 192.168.31.7:z-gateway:9000 profiles dev1
@xxx
  • eureka查看源码得知有五个事件。
1
2
3
4
5
EurekaServerStartedEvent - Eureka服务端启动事件
EurekaRegistryAvailableEvent - Eureka服务端可用事件
EurekaInstanceRegisteredEvent - Eureka客户端服务注册事件
EurekaInstanceRenewedEvent - Eureka客户端续约事件
EurekaInstanceCanceledEvent - Eureka客户端下线事件

这里就比较简单了。

本篇幅只是回顾使用钉钉做异常告警需要那些关键业务信息。

为什么要做钉钉通知?

​ 事情要从我入职上家公司说起,进入公司后把线上项目clone下来大致看了下。代码风格过于滞后、编码风格混乱。进入公司第一周就出现了线上故障,嗯。我去线上检查日志,emmmm竟然没有日志输出。这次故障是由客户反馈来的。当时我非常吃惊,大伙好像很淡定的样子,习以为常了?

​ 想到当初面试的时候和总监的谈话,主要是带领团队落地微服务架构,看来必须大刀阔斧了。

​ 首先想到的时候改进日志输出、定义全局异常级别,根据异常级别输出日志。

1
2
3
4
sequenceDiagram
Java应用 ->> SpringAop全局异常拦截 : runtimeException
SpringAop全局异常拦截 ->> 钉钉 : alarmNoticeSend
钉钉 ->> 开发人员 : exceptionInfo
  • 效果图

​ 我们需要从钉钉里面看到那些异常信息呢?这是当时输出到钉钉的信息。通过编写全局拦截器,在公共基础项目里面添加了aop全局拦截。刚开始上线的时候钉钉一天动不动就几千个异常告警。刚开始大伙都很紧张,过了个把月大伙已经又麻木了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
告警信息
工程名:z201-gateway
类路径:cn.z201.cloud.gateway.VlinkFrameworkGatewayApplicationTest
方法名:alarm
异常信息:java.lang.IllegalAccessException
异常追踪:
cn.z201.cloud.gateway.VlinkFrameworkGatewayApplicationTest.alarm(VlinkFrameworkGatewayApplicationTest.java:64)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:498)
org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)

这样的告警信息就够了吗?

​ 明显这样是不够够的,前端有安卓、ios、微信小程序、web、快应用。太多前端项目了,后端需要识别出是哪里的项目出的问题。于是又改进了一次。邀请前端开发人员在HttpHeader里面增加额外参数。为了做流量区分也增加了一些参数。

1
2
3
4
5
6
7
8
9
10
11
 {
"Content-Type":"application/json"
"Authorization":"Bearer xxxxxxxxxx" ## jwt
"Client-Business-Group-Source": "1", ## 业务组来源唯一标号
"Client-Business-Source": "1000", ## 业务来源唯一标号
"Client-Business-Activity-Source": "1", ## 查看介绍、更多 针对特殊业务流量识别
"Client-Env-Source": "1", ##客户端环境来源 1 ios 2 android 3 windows
"Client-Platform-Source": "xxx", ##客户端平台 xxx手机型号、浏览器
"Client-Start-Time": "1", ##请求时间戳
"Client-Version-Source": "1.0.0" ##客户端版本号
}

​ 通过上面的改进告警信息完善很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
告警信息
工程名:z201-gateway
类路径:cn.z201.cloud.gateway.VlinkFrameworkGatewayApplicationTest
方法名:alarm
异常信息:java.lang.IllegalAccessException
异常扩展信息: {
"Client-Business-Source": "1000",
"Client-Business-Activity-Source": "1",
"Client-Env-Source": "1",
"Client-Platform-Source": "xxx",
"Client-Version-Source": "1.0.0"
}
异常追踪:
cn.z201.cloud.gateway.VlinkFrameworkGatewayApplicationTest.alarm(VlinkFrameworkGatewayApplicationTest.java:64)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:498)
org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)

分布式下面临的问题!

1
2
3
4
5
sequenceDiagram
gateway ->> 应用A : httpRequest
应用A ->> 应用B : httpRequest
应用B -->> 应用A : httpResponse
应用A -->> gateway : httpResponse

​ 当调用链多的时候定位问题就有点麻烦,比如应用a调用应用b。应用b执行了异常信息直接抛出了告警信息。但是spring cloud http rpc默认是不会吧请求参数传递到后面的服务中,需要我们做下简单的扩展。

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
/**
* @author z201.coding@gmail.com
**/
public interface HttpApiConstant {

String X_REAL_IP = "x-real-ip";
/**
* 请求头跟踪id名。
*/
String HTTP_HEADER_TRACE_ID = "AppTraceId";

/**
* 请求头
*/
String HTTP_TOKEN_HEADER = "Authorization";

/**
* app租户
*/
String APP_TENANT = "Tenant";

String CLIENT_BUSINESS_GROUP_SOURCE = "Client-Business-Group-Source";

String CLIENT_BUSINESS_SOURCE = "Client-Business-Source";

String CLIENT_BUSINESS_ACTIVITY_SOURCE = "Client-Business-Activity-Source";

String CLIENT_EVN_SOURCE = "Client-Env-Source";

String CLIENT_PLATFORM_SOURCE = "Client-Platform-Source";

String CLIENT_START_TIME = "Client-Start-Time";

String CLIENT_VERSION_SOURCE = "Client-Version-Source";

}


/**
* @author z201.coding@gmail.com
* 自定义restTemplate拦截器
* 这里可以把一些参数从应用层传到内部服务
**/
@Slf4j
@ConditionalOnClass(WebMvcConfigurer.class)
public class MdcFeignInterceptorConfig implements RequestInterceptor, HttpApiConstant {

public MdcFeignInterceptorConfig() {
log.info("Loaded Z-REST-INTERCEPTOR [V1.0.0]");
}

@Override
public void apply(RequestTemplate template) {
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
String xRealIp = request.getHeader(X_REAL_IP);
String authentication = request.getHeader(HTTP_TOKEN_HEADER);
String appTraceId = request.getHeader(HTTP_HEADER_TRACE_ID);
String businessGroupSource = request.getHeader(CLIENT_BUSINESS_GROUP_SOURCE);
String clientBusinessSource = request.getHeader(CLIENT_BUSINESS_SOURCE);
String clientBusinessActivitySource = request.getHeader(CLIENT_BUSINESS_ACTIVITY_SOURCE);
String clientEnvSource = request.getHeader(CLIENT_EVN_SOURCE);
String clientPlatformSource = request.getHeader(CLIENT_PLATFORM_SOURCE);
String clientStartTime = request.getHeader(CLIENT_START_TIME);
String clientVersionSource = request.getHeader(CLIENT_VERSION_SOURCE);
template.header(HttpHeaders.ACCEPT_ENCODING, "gzip");
template.header(X_REAL_IP, xRealIp);
template.header(HTTP_TOKEN_HEADER, authentication);
template.header(CLIENT_BUSINESS_GROUP_SOURCE, businessGroupSource);
template.header(CLIENT_BUSINESS_SOURCE, clientBusinessSource);
template.header(CLIENT_BUSINESS_ACTIVITY_SOURCE, clientBusinessActivitySource);
template.header(CLIENT_EVN_SOURCE, clientEnvSource);
template.header(CLIENT_PLATFORM_SOURCE, clientPlatformSource);
template.header(CLIENT_START_TIME, clientStartTime);
template.header(CLIENT_VERSION_SOURCE, clientVersionSource);
if (log.isDebugEnabled()) {
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = request.getHeader(name);
log.debug("header {} - {}", name, value);
}
}
} catch (Exception e) {
log.error("template exception {}",e.getMessage());
}
}
}

简单的异常钉钉告警就到这里结束了。

本章是整理知识内容,为强化知识长期更新。

JVM

JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。JVM 会翻译执行 Java 字节码,然后调用真正的操作系统函数,这些操作系统函数是与平台息息相关的。

  • 如上图所示,通过JVM,Java实现了跨平台、只要class文件能正常执行,就可以在其他系统上面运行。 JVM 与操作系统之间的关系:JVM 上承开发语言,下接操作系统,它的中间接口就是字节码。

JDK

  • JDK 的全拼,Java Development Kit JVM、JRE、JDK 它们三者之间的关系,可以用一个包含关系表示。

JVM字节码

对于 Java 开发者来说,虚拟机、字节码就是其底层知识。Java Byte 由单字节 byte的指令组成,理论上最多支持256个操作码。实际上只使用了200个左右操作码。

编译java文件

这里采用xxd xx.java

1
2
3
4
5
6
7
8
9

package cn.z201.jvm;

public class Testing {

public static void main(String[] args){
System.out.println("Hello world!");
}
}
  • 通过javac编译成class文件、在用xxd查看编译后的16进制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
➜  jvm git:(master) ✗ javac Testing.java 
➜ jvm git:(master) ✗ xxd Testing.class
00000000: cafe babe 0000 0034 001d 0a00 0600 0f09 .......4........
00000010: 0010 0011 0800 120a 0013 0014 0700 1507 ................
00000020: 0016 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 046d 6169 umberTable...mai
00000050: 6e01 0016 285b 4c6a 6176 612f 6c61 6e67 n...([Ljava/lang
00000060: 2f53 7472 696e 673b 2956 0100 0a53 6f75 /String;)V...Sou
00000070: 7263 6546 696c 6501 000c 5465 7374 696e rceFile...Testin
00000080: 672e 6a61 7661 0c00 0700 0807 0017 0c00 g.java..........
00000090: 1800 1901 000c 4865 6c6c 6f20 776f 726c ......Hello worl
000000a0: 6421 0700 1a0c 001b 001c 0100 1363 6e2f d!...........cn/
000000b0: 7a32 3031 2f6a 766d 2f54 6573 7469 6e67 z201/jvm/Testing
000000c0: 0100 106a 6176 612f 6c61 6e67 2f4f 626a ...java/lang/Obj
000000d0: 6563 7401 0010 6a61 7661 2f6c 616e 672f ect...java/lang/
000000e0: 5379 7374 656d 0100 036f 7574 0100 154c System...out...L
000000f0: 6a61 7661 2f69 6f2f 5072 696e 7453 7472 java/io/PrintStr
00000100: 6561 6d3b 0100 136a 6176 612f 696f 2f50 eam;...java/io/P
00000110: 7269 6e74 5374 7265 616d 0100 0770 7269 rintStream...pri
00000120: 6e74 6c6e 0100 1528 4c6a 6176 612f 6c61 ntln...(Ljava/la
00000130: 6e67 2f53 7472 696e 673b 2956 0021 0005 ng/String;)V.!..
00000140: 0006 0000 0000 0002 0001 0007 0008 0001 ................
00000150: 0009 0000 001d 0001 0001 0000 0005 2ab7 ..............*.
00000160: 0001 b100 0000 0100 0a00 0000 0600 0100 ................
00000170: 0000 0500 0900 0b00 0c00 0100 0900 0000 ................
00000180: 2500 0200 0100 0000 09b2 0002 1203 b600 %...............
00000190: 04b1 0000 0001 000a 0000 000a 0002 0000 ................
000001a0: 0008 0008 0009 0001 000d 0000 0002 000e ................


  • 编译后完全看不懂,jdk实际上提供了专门用来解析类文件的工具javap

javap

javap能对给定的class文件提供的字节代码进行反编译

  • 使用格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
javap <options> <classes>

-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
  • 演示下上面的类
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
javap -c -s -v Testing

Classfile /Users/z201/word/z201.github.io.code/java-learning/learn-jvm/src/test/java/cn/z201/jvm/Testing.class
MD5 checksum b50deb6f6e7df9bb99585bc71b92d39b
Compiled from "Testing.java"
public class cn.z201.jvm.Testing
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello world!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // cn/z201/jvm/Testing
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Testing.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello world!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 cn/z201/jvm/Testing
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public cn.z201.jvm.Testing();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello world!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 8: 0
line 9: 8
}
SourceFile: "Testing.java"

IDEA bytecode viewer

jclasslib is a bytecode viewer for Java class files

  • 在plugins中搜索jclasslib bytecode viewer
  • 安装重启IDEA后,编译项目代码(run 一次)选中代码文件 view 菜单中有 bytecode viewer 选项。

  • IDEA 显示的内容更加容易阅读。

JVM的内存布局

  • 堆(Java Heap) 也叫 Java 堆或者是 GC 堆,它是一个线程共享的内存区域,也是 JVM 中占用内存最大的一块区域,Java 中所有的对象都存储在这里。

    • 存储的是我们new来的对象,不存放基本类型和对象引用。

    • 由于创建了大量的对象,垃圾回收器主要工作在这块区域。

    • 线程共享区域,因此是线程不安全的。

    • 能够发生内存溢出,主要有OutOfMemoryError和StackOverflowError。

    • 那么什么时候发生OutOfMemoryError,什么时候发生StackOverflowError?虚拟机在扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError异常,线程请求的栈深度超过虚拟机所允许的最大深度,将抛出StackOverflowError异常

    • Java堆区还可以划分为新生代和老年代,新生代又可以进一步划分为Eden区、Survivor 1区、Survivor 2区。具体比例参数的话,可以看一下下面这张图。

    • 合理设置老年代和新生代的空间比例对 JVM 垃圾回收的性能有很大影响,JVM 设置老年代新生代比例的参数是 -XX:NewRatio。
  • Minor GC 发生在新生代,而 Full GC 发生在老年代。

  • 大部分新生成的对象都是在 Eden 区,Eden 区满了之后便没有内存给新对象使用,Eden 区便会 Minor GC 回收无用内存,剩下的存活对象便会转移到 Survivor 区。

  • 从 Eden 区存活下来的对象首先会被复制到 From 区,当 From 区满时,此时还存活的对象会被转移到 To 区,经历了多次的 Minor GC 后,还存活的对象就会被复制到老年代,老年代的 GC 一般叫作 FullGC 或者 MajorGC。

新生代

  • 新生代是类的诞生、成长、消亡的区域,一个类在这里产生、应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存区(Survivor pace),绝大多数的类都是在伊甸区被 new 出来的。幸存区有两个:0区(from Survivor 0 space)和 1区(to Survivor 1 space)。两个Survivor 区空间一样大,当Eden 区满了,则会触发普通GC,若 Eden 区中的对象经过垃圾回收没有被回收掉,会将Eden 区中的幸存对象移动到幸存0区。若幸存0区也满了,再对该区域进行垃圾回收,然后移动到1区,然后之前的0区将置为空,没有任何数据。显然,Survivor 区只是增加了对象在年轻代中逗留的时间,增加了被垃圾回收的可能性。那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这时候将产生Full GC,进行养老区的内存清理。若养老区执行Full GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常“OutOfMemoryError”。
  • 每次垃圾回收时都有大量的对象需要被回收。垃圾回收器在新生代采用的收集算法是Copying(复制)方法:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块面对内存用完了,就将还存活的对象复制到另外一块,然后再把已使用的内存空间一次清理掉。由于新生代有每次垃圾回收时都有大量的对象需要被回收的特点,所以采用Copying算法,复制的存活对象较少,性能比较好。但是Copying算法还是有缺陷的,那就是内存的使用率只有一半。

OutOfMemoryError

1
java.lang.OutOfMemoryError: Java heap space
  • Java 虚拟机的堆内存设置不够,可以通过-Xms、-Xmx来调整

  • 代码中创建了大量大对象,并且长时间不能被垃圾回收器收集

  • 内存加载的数据量太大:一次性从数据库取太多数据

  • 集合类中有对对象的引用,使用后未清空,GC不能进行回收

  • 代码中存在循环产生过多的重复对象

老年代

  • 年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代。
  • 每次垃圾回收时只有少量的对象需要被回收。垃圾回收器在老年代采用的收集算法是 Mark-Compac t算法:为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。它先将需要回收的对象进行标记,然后将存活对象都向一端移动,然后清理掉端边界以外的内存。

FULL GC

  • 当老年代空间满了,就会对新生代和老年代空间进行一次全量垃圾回收,即Full GC。

方法区

  • 方法区(Method Area) 也被称为非堆区,用于和“Java 堆”的概念进行区分,它也是线程共享的内存区域,用于存储已经被 JVM 加载的类型信息、常量、静态变量、代码缓存等数据。

运行时常量池

  • Run-Time Constant Pool这是方法区的一部分,版本号、字段、方法、超类、接口等各种信息,还有一项信息就是常量池。Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号引用。

程序计数器

  • 程序计数器(Program Counter Register) 线程独有一块很小的内存区域,保存当前线程所执行字节码的位置,包括正在执行的指令、跳转、分支、循环、异常处理等。

虚拟机栈

  • 虚拟机栈也叫 Java 虚拟机栈(Java Virtual Machine Stack),和程序计数器相同它也是线程独享的,用来描述 Java 方法的执行,在每个方法被执行时就会同步创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。当调用方法时执行入栈,而方法返回时执行出栈。
    • 线程私有区域,每一个线程都有独享一个虚拟机栈,因此这是线程安全的区域。

    • 存放基本数据类型以及对象的引用。

    • 每一个方法执行的时候会在虚拟机栈中创建一个相应栈帧,方法执行完毕后该栈帧就会被销毁。方法栈帧是以先进后出的方式虚拟机栈的。

    • 每一个栈帧又可以划分为局部变量表、操作数栈、动态链接、方法出口以及额外的附加信息。

      • 这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(通常是递归导致的);JVM动态扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

本地方法栈

  • 本地方法栈(Native Method Stacks)与虚拟机栈类似,它是线程独享的,并且作用也和虚拟机栈类似。只不过虚拟机栈是为虚拟机中执行的 Java 方法服务的,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

  • JVM 的执行流程是,首先先把 Java 代码(.java)转化成字节码(.class),然后通过类加载器将字节码加载到内存中,所谓的内存也就是我们上面介绍的运行时数据区,但字节码并不是可以直接交给操作系统执行的机器码,而是一套 JVM 的指令集。这个时候需要使用特定的命令解析器也就是我们俗称的执行引擎(Execution Engine)将字节码翻译成可以被底层操作系统执行的指令再去执行,这样就实现了整个 Java 程序的运行,这也是 JVM 的整体执行流程。

类的加载机制

符号引用和直接引用有一个重要的区别:使用符号引用时被引用的目标不一定已经加载到内存中;而使用直接引用时,引用的目标必定已经存在虚拟机的内存中了。

  • 类的生命周期会经历以下 7 个阶段:

  1. 加载阶段(Loading)
  2. 验证阶段(Verification)
  3. 准备阶段(Preparation)
  4. 解析阶段(Resolution)
  5. 初始化阶段(Initialization)
  6. 使用阶段(Using)
  7. 卸载阶段(Unloading)

加载阶段

此阶段用于查到相应的类(通过类名进行查找)并将此类的字节流转换为方法区运行时的数据结构,然后再在内存中生成一个能代表此类的 java.lang.Class 对象,作为其他数据访问的入口。

  • Java语言的类型可以分为两大类:基本类型、引用类型。基本类型是由虚拟机预先定义好的,所以不会经历单独的类加载过程。而引用类型又分为四种:类、接口、数组类、泛型参数。由于泛型参数会在编译的过程中被擦除(关于类型擦除的知识,大家可以查下资料),所以在Java中只有类、接口、数组类三种类型需要经历JVM对其进行连接和初始化的过程。

验证阶段

此步骤主要是为了验证字节码的安全性,如果不做安全校验的话可能会载入非安全或有错误的字节码,从而导致系统崩溃,它是 JVM 自我保护的一项重要举措。

  • 验证的主要动作大概有以下几个:
    • 文件格式校验包括常量池中的常量类型、Class 文件的各个部分是否被删除或被追加了其他信息等;
    • 元数据校验包括父类正确性校验(检查父类是否有被 final 修饰)、抽象类校验等;
    • 字节码校验,此步骤最为关键和复杂,主要用于校验程序中的语义是否合法且符合逻辑;
    • 符号引用校验,对类自身以外比如常量池中的各种符号引用的信息进行匹配性校验。

准备阶段

此阶段是用来初始化并为类中定义的静态变量分配内存的,这些静态变量会被分配到方法区上。这些变量所使用的内存都将在方法区(<Jdk1.8)元数据区(>=Jdk1.8)中进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化的时候随对象一起分配在Java堆中。

解析阶段

此阶段主要是用来解析类、接口、字段及方法的,解析时会把符号引用替换成直接引用。

  • 所谓的符号引用是指以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可;而直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。

  • 符号引用和直接引用有一个重要的区别:使用符号引用时被引用的目标不一定已经加载到内存中;而使用直接引用时,引用的目标必定已经存在虚拟机的内存中了。

初始化

初始化阶段 JVM 就正式开始执行类中编写的 Java 业务代码了。到这一步骤之后,类的加载过程就算正式完成了。

  • JVM 规范枚举了下述多种触发情况:
    • 当虚拟机启动时,初始化用户指定的主类;
      • 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
      • 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
      • 当遇到访问静态字段的指令时,初始化该静态字段所在的类;子类的初始化会触发父类的初始化;
      • 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
      • 使用反射 API 对某个类进行反射调用时,初始化这个类;
      • 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
阅读全文 »

工作中可能出现一个bug修复多次调教或者开发过程中需要临时保存代码到分支上的操作。但是合并主分支的时候不希望把过多的提交记录信息展示出来。这里就需要用到git rebase 命名。

案例

使用git rebase 合并多条commit

  1. 查看git log 选择要合并的记录
1
git log --oneline

2.选中一个commit id作为合并到对象,从上至下按照最新提交的记录排序。这里可以选中第三条记录。

1
git rebase -i 7893ade

  • 这里会显示可以合并的记录还有git的命令帮助信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 命令:
# p, pick <提交> = 使用提交
# r, reword <提交> = 使用提交,但修改提交说明
# e, edit <提交> = 使用提交,进入 shell 以便进行提交修补
# s, squash <提交> = 使用提交,但融合到前一个提交
# f, fixup <提交> = 类似于 "squash",但丢弃提交说明日志
# x, exec <命令> = 使用 shell 运行命令(此行剩余部分)
# b, break = 在此处停止(使用 'git rebase --continue' 继续变基)
# d, drop <提交> = 删除提交
# l, label <label> = 为当前 HEAD 打上标记
# t, reset <label> = 重置 HEAD 到该标记
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . 创建一个合并提交,并使用原始的合并提交说明(如果没有指定
# . 原始提交,使用注释部分的 oneline 作为提交说明)。使用
# . -c <提交> 可以编辑提交说明。
#
# 可以对这些行重新排序,将从上至下执行。
#
# 如果您在这里删除一行,对应的提交将会丢失。
#
# 然而,如果您删除全部内容,变基操作将会终止。
#

  • 这里将要合并的commit 前缀pick 改成 s 或者 squash 就可以开始合并了。这里使用vim :wq保存
1
2
3
pick a755a10 完善单元测试
s d3f1272 完善代码
s 547947e
  • 正常情况下,不回创建临时空间。直接将代码推送到需要存储分支就可以了。这里不直接推送主分支,建议合并过去。
1
git push -f origin branch
  • 不正常的情况,一般是合并冲突,只需要将合并解决掉, 在将合并的文件添加暂存区,在使用git rebase –continue。或者直接放弃git rebase –abort 或者直接切换到其他分支,然后删除刚才操作的文件退回rm -rf .git/rebase-merge
  • 检查git log --oneline

OpenResty

OpenResty(又称:ngx_openresty) 是一个基于 NGINX 的可伸缩的 Web 平台,由中国人章亦春发起,提供了很多高质量的第三方模块。OpenResty 是一个强大的 Web 应用服务器

安装方法

  • 通过源安装
  • 编译安装

源安装

  • 安装yum-utils
1
yum install yum-utils	
  • 添加openresty源repo
1
yum-config-manager --add-repo https://openresty.org/package/rhel/openresty.repo
  • 安装
1
yum install -y openresty
  • 启动
1
systemctl start openresty
  • 效果

  • 安装openresty命令行工具
1
yum install -y openresty-resty
  • 检查当前安装的openresty
1
/usr/local/openresty/bin/openresty -V

在mac中使用redis-cli。相比rdm我可能更喜欢这个命令行工具。

在mac中之安装redis-cli

按照标准的按照流程,需要先安装redis-server。

1
2
3
4
5
brew tap ringohub/redis-cli

brew update && brew doctor

brew install redis-cli

根据redis常用命令来学习使用redis-cli。