0%

定位JVM相关的问题时,知识和经验是关键基础,数据是依据,工具是运用知识处理数据的手段。JVM数据包括运行日志、异常堆栈、GC日志、线程快照(threaddump/javacore文件)、堆转储快照(heapdump/hprof文件)等。使用JVM监控和分析工具可以提高分析数据、定位解决问题的效率,常用监控工具如下表所示,具体各工具的用法可以Google对应JDK版本的文档。

名称 用途
jps JVM Process Status Tool显示系统内所有Hotspot虚拟机进程
jstat JVM Statistics Monitoring Tool,收集Hotspot虚拟机各方面运行数据
jinfo Configuration Info for Java,显示虚拟机配置信息
jmap Memory Map for Java,生成虚拟机内存转储快照(heapdump文件)
jhat JVM Statistics Heap ,用于分析heapdump文件,它会建立一个HTTP/HTML服务器,可以在浏览器中查看分析结果
jstack Stack Trace for Java,显示虚拟机的线程快照

JDK可视化工具

Java虚拟机规范详细描述了虚拟机指令级中每条指令的执行过程,执行前后对操作数栈、局部变量表的影响等细节。但随着技术的发展,虚拟机规范中的描述逐渐成为虚拟机实现的概念模型,即实际实现的虚拟机只能保证与规范实现等效。在分析程序执行语义时(虚拟机做了什么)时,在字节码层面上分析完全可行,但分析程序的执行行为时(虚拟机是怎么做的、性能如何)时,在字节码层面上分析就没有什么意义了。需要使用HSDIS这类反汇编插件,把动态生成的本地代码还原为汇编代码输出。

方法区溢出

方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收掉,判定条件是非常苛刻的。在经常动态生成大量 Class 的应用中,需要特别注意类的回收状况。这类场景除了上面提到的程序使用了 GCLib 字节码增强外,常见的还有:大量 JSP 或动态产生 JSP 文件的应用( JSP 第一次运行时需要编译为Java 类)、基于 OSGi 的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。

参考资料

1.Java虚拟机OOM之方法区溢出

阅读全文 »

Starters

Starters是Spring Boot提供的一系列开箱即用的依赖描述符,每个starter包含所需的所有相关依赖。

阅读全文 »

注解又叫元数据,可以提供代码难以描述的信息,有助于减轻编写“样板”代码的负担,编写更加简洁易读的代码。

注解定义

注解的定义类似接口的定义,除了@符号外,注解很像一个空的接口,并且也会被编译成class文件。定义注解时需要一些元注解(meta-annotation),例如@Target和@Retention,@Target用来定义注解用于什么地方(例如一个方法还是一个域),@Retention用来定义注解在哪一个级别可用(源代码中、类文件中还是运行时)。

1
2
3
4
5
6
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserCase {
int id();
String description() default "Default description";
}

注解中一般会包含一些元素表示一些值。分析处理注解时,可以利用这些值。注解的元素看起来就像接口的方法,唯一的区别是可以指定默认值。注解元素的可用类型包括:

  1. 所有基本类型(int, float, boolean等),不能使用包装类
  2. String
  3. Class
  4. enum
  5. Annotation
  6. 以上类型的数组
    对于非基本类型元素,无论在源代码声明时,还是在注解接口定义默认值时,都不能以null作为值。如果需要表示对应值不存在,可以用特殊的值表示, 例如空字符串、负数等。

参考资料

《Java编程思想》

阅读全文 »

从头搭建一个完善的后端服务并不像看上去那么容易,一般都需要用到众多组件,并且要让这些组件相互协调。不仅能搭起来,还能明白为什么这么搭,就需要更多的脑容量了。

从降低开发和测试成本、快速定位错误的角度,需要版本控制、单元测试、日志监控;从项目可扩展、可维护和高性能的角度,采用成熟、优秀的框架不仅可以缩短开发周期,而且还能避免自己造轮子的过程中踩太多坑。在Java Web邻域,最常用的组件如下表所示:

|模块| 常用组件|
|—|—|—|
|版本控制| Git|
|单元测试| Junit|
|日志| Log4j|
|数据库| MySQL|
|持久化 |MyBatis|
|Bean容器| SpringMVC|

根据实际搭建经验,由于组件太多,不是这个出问题就是那个不听话。所谓按下葫芦浮起瓢说得就是这种情况。为了避免重复踩坑,本文把搭建过程和各组件之间的配置整理了一下,并提供了一个demo项目。

Maven

Maven用来管理包依赖想必是极好的,由于一直以来没惹太多麻烦,这次就暂且按下不表了。本文重点整顿的组件包括及版本号如下pom所示。

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.jeffzzf</groupId>
<artifactId>basic</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>4.1.6.RELEASE</spring.version>
<mybatis.version>3.3.1</mybatis.version>
<log4j.version>2.7</log4j.version>
<junit.version>4.12</junit.version>
</properties>

<dependencies>

<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.29</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.2</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- 阿里巴巴数据源包 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.2</version>
</dependency>

<!--把项目中使用commons-logging输出的日志统一桥接到log4j2-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jcl</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-web</artifactId>
<version>${log4j.version}</version>
</dependency>

</dependencies>
</project>

Spring MVC

使用Spring作为Java Web框架的常见配置是在web.xml文件中注册加载Spring ApplicationContext的Listener——ContextLoaderListener,并通过contextConfigLocation参数指定ApplicationContext的配置文件。如果没有配置contextConfigLocation参数,ContextLoaderListener将默认使用/WEB-INF/applicationContext.xml作为配置文件。

1
2
3
4
5
6
7
8
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/*.xml</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
阅读全文 »

MySQL日志

MySQL中的日志文件记录了数据库中各种类型的活动,是DBA发现和定位错误、优化性能的重要依据。MySQL常见的日志文件有:

  • 错误日志(error log)
  • 二进制日志(binlog)
  • 慢查询日志(slow query log)
  • 查询日志(log)

错误日志

错误日志文件对MySQL的启动、运行、关闭过程进行了记录。MySQL DBA在遇到问题时应该首先查看该文件以便定位问题。该文件不仅记录了所有的错误信息,也记录一些警告信息或正确的信息。用户可以通过命令SHOW VARIABLES LIKE’log_error’来定位该文件, 或者使用SHOW VARIABLES LIKE’%error%’来查询与所有与error相关的参数(包括log_error变量的值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> show variables like '%error%';
+---------------------+----------------------------------------+
| Variable_name | Value |
+---------------------+----------------------------------------+
| binlog_error_action | ABORT_SERVER |
| error_count | 0 |
| log_error | /usr/local/mysql/data/mysqld.local.err |
| log_error_verbosity | 3 |
| max_connect_errors | 100 |
| max_error_count | 64 |
| slave_skip_errors | OFF |
+---------------------+----------------------------------------+
7 rows in set (0.00 sec)
阅读全文 »

git merge 和 git rebase 这两个命令都可以用来合并分支,两者的区别主要体现在 commit log 的处理方式和解决冲突的方式不同。和 git merge 不同的是,需要在 feature 分支上进行 git rebase master 的操作,意味着让当前分支 feature 相对于 分支 master 进行变基。解决了冲突之后,需要执行 git rebase --continue 来继续变基的操作。

merge 命令合并完分支后,如果解决过冲突,会产生一个 commit log 记录

rebase 命令合并分支后不会产生额外的commit。这样的好处是,‘干净’,分支上不会有无意义的解决分支的commit;坏处,如果合并的分支中存在多个commit,需要重复处理多次冲突。

当需要保留详细的合并信息的时候建议使用git merge,特别是需要将分支合并进入master分支时;当发现自己修改某个功能时,频繁进行了git commit提交时,发现其实过多的提交信息没有必要时,可以尝试git rebase

参考资料

  1. Git merge和rebase分支合并命令的区别
  2. [git]merge和rebase的区别
阅读全文 »

不追踪某些文件的变化

项目中生成的一些临时文件往往不需要添加到版本管理中,这些文件可以通过在.git/info/exclude文件中设置,例如在该文件中添加以下内容的话,以.iml结尾的文件和target、.idea、test目录下的文件就不会被git监控:

1
2
3
4
*.iml
target/
.idea/
test/

如果这些文件已经被添加到git中,可以通过git rm --cached 文件全路径名将文件从git索引中删除(不删除文件本身),然后在配置到exclude中

merge分支时不merge特定文件

在一些场景下不同分支的一些文件不应该合并,例如测试环境和生产环境的数据库配置文件。这种情况下即使在.git/info/exclude配置了不追踪这些文件,merge的时候还是会被merge。一种很好的办法是在.git/info/attributes中配置不被merge的文件(不包含文件路径),例如:

1
2
SpringDispatcher-servlet.xml merge=ours
log4j.xml merge=ours

然后执行git config --global merge.ours.driver true定义一个merge策略ours,再执行merge命令时,定义在attributes中的文件就不会参与merge了。

参考资料

Git Documentation

阅读全文 »

使用Mac的终端(Terminal)时,如果主机名或者用户名过长,命令行的很大一部分都会被这些无关信息占据。作为一个有强迫症的boy,是无法忍受这种情况发生的,好在这些信息都可以非常方便地进行定制。

OSX系统的Terminal命令行前面显示的信息保存在PS1变量中,可以通过echo $PS!命令查看当前显示格式。各字段含义如下:

\a ASCII 响铃字符(也可以键入 \007)
\d “Wed Sep 06” 格式的日期
\e ASCII 转义字符(也可以键入 \033)
\h 主机名的第一部分(如 “mybox”)
\H 主机的全称(如 “mybox.mydomain.com”)
\j 在此 shell 中通过按 ^Z 挂起的进程数
\l 此 shell 的终端设备名(如 “ttyp4”)
\n 换行符
\r 回车符
\s shell 的名称(如 “bash”)
\t 24 小时制时间(如 “23:01:01”)
\T 12 小时制时间(如 “11:01:01”)
@ 带有 am/pm 的 12 小时制时间
\u 用户名
\v bash 的版本(如 2.04)
\V Bash 版本(包括补丁级别) ?/td>
\w 当前工作目录(如 “/home/drobbins”)
\W 当前工作目录的“基名 (basename)”(如 “drobbins”)
! 当前命令在历史缓冲区中的位置
# 命令编号(只要您键入内容,它就会在每次提示时累加)
$ 如果您不是超级用户 (root),则插入一个 “$”;如果您是超级用户,则显示一个 “#”
\xxx 插入一个用三位数 xxx(用零代替未使用的数字,如 “\007”)表示的 ASCII 字符
\ 反斜杠
[ 这个序列应该出现在不移动光标的字符序列(如颜色转义序列)之前。它使 bash 能够正确计算自动换行。
] 这个序列应该出现在非打印字符序列之后。

例如PS1=”\W $ “就表示命令行前面显示的信息为相对路径 $,该变量信息可以保存在/.bash_profile文件中,例如export PS1=”\W $ “。
值得一提的是,个人其它一些定制化命令和信息都可以保存在
/.bash_profile文件中,例如:

1
2
3
4
5
# 快捷执行mysql命令
alias mysql='/usr/local/mysql/bin/mysql'
# cd ..和cd ../..的简记形式
alias ..="cd .."
alias ...="cd ../.."
阅读全文 »

在一个项目中需要获取数据库中某天0时0分0秒到第二天0时0分0秒之间的记录,一开始想到的是之间使用Date对象的getTime()方法获得当前的微秒数,然后截掉小时、分钟和微秒得到该日期的0时0分0秒,代码如下:

1
2
3
4
5
6
final long MICROSECONDS_PER_DAY = 24 * 60 * 60 * 1000;
long curMillis = System.currentTimeMillis();
//当天的0时0分0秒
Date today = new Date(curMillis - curMillis % MICROSECONDS_PER_DAY);
//昨天的0时0分0秒
Date yesterday = new Date(today.getTime() - MICROSECONDS_PER_DAY);

一切看上去都很reasonable,日期也是对的,不过时间并不是0时0分0秒,而是8时0分0秒

1
2
System.out.println(today);  //Wed Jun 01 08:00:00 CST 2016
System.out.println(yesterday); //Tue May 31 08:00:00 CST 2016

为了搞清楚问题出在哪里,对Java中与date、time相关概念做了一次梳理。

时区(TimeZone)

时区(Time Zone)是地球上的区域使用同一个时间的定义,1884年在华盛顿召开国际经度会议时,为了克服时间上的混乱,规定将全球划分为24个时区(东、西各12个时区)。规定英国(格林尼治天文台旧址)为中时区(零时区)、东1-12区,西1-12区。每个时区横跨经度15度,时间正好是1小时。最后的东、西第12区各跨经度7.5度,以东、西经180度为界。每个时区的中央经线上的时间就是这个时区内统一采用的时间,称为区时,相邻两个时区的时间相差1小时。例如,中国东8区的时间总比泰国东7区的时间早1小时,而比日本东9区的时间迟1小时。因此,出国旅行的人,必须随时调整自己的手表,才能和当地时间相一致。凡向西走,每过一个时区,就要把表拨慢1小时(比如2点拨到1点);凡向东走,每过一个时区,就要把表拨快1小时(比如1点拨到2点)。并且规定英国(格林尼治天文台旧址)为本初子午线,即零时(24时)经线。在中国采用首都北京所在地东八区的时间为全国统一使用时间。
Java中的TimeZone对应现实世界中的时区,可以使用getTimeZone(String ID)获得某个时区,例如使用TimeZone.getTimeZone(“Asia/Shanghai”)可以得到中国标准时间的时区,使用TimeZone.getDefault()可以得到系统标准时区。

Date

在Java 7中,Date类除了Date(long milliseconds)和Date()两个构造器外,其它构造器都废弃了。这两个构造器都基于从1970年1月1日00:00:00GMT算起的微秒数,默认构造器以系统当前的微秒数构造对象。

1
2
3
4
5
6
7
//Date类的构造器
public Date(long date) {
fastTime = date;
}
public Date() {
this(System.currentTimeMillis());
}
阅读全文 »