什么是调优?
对于调优这个事情来说,一般就是三个过程:
调优是需要做好准备工作的,毕竟每一个应用的业务目标都不尽相同,性能瓶颈也不会总在同一个点上。在业务应用层面,我们需要:
- 吞吐量:用户代码时间 /(用户代码执行时间 + 垃圾回收时间)
- 响应时间:STW越短,响应时间越好
.所谓调优,首先确定,追求啥?吞吐量优先,还是响应时间优先?还是在满足一定的响应时间的情况下,要求达到多大的吞吐量
- 吞吐量优先的一般选择:PS + PO
- 响应时间优先:网站 GUI API (JDK 1.8 建议使用 G1)
调优步骤:
Java 的自动内存管理依赖 GC, GC 会一遍又一遍地扫描内存区域, 将不使用的对象删除. 简单来说, Java 中的内存泄漏, 就是那些逻辑上不再使用的对象, 却没有被 垃圾收集程序 给干掉. 从而导致垃圾对象继续占用堆内存中, 逐渐堆积, 最后造成java.lang.OutOfMemoryError: Java heap space
错误。
案例:
import java.util.*;
public class KeylessEntry {
static class Key {
Integer id;
Key(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
public static void main(String[] args) {
Map m = new HashMap();
while (true){
for (int i = 0; i < 10000; i++){
if (!m.containsKey(new Key(i))){
m.put(new Key(i), "Number:" + i);
}
}
System.out.println("m.size()=" + m.size());
}
}
}
粗略一看, 可能觉得没什么问题, 因为这最多缓存 10000 个元素嘛! 但仔细审查就会发现, Key 这个类只重写了 hashCode()
方法, 却没有重写equals()
方法, 于是就会一直往 HashMap 中添加更多的 Key。
Key 类没有重写 equals 方法,则默认使用 Object 类中的 equals 方法判断对象是否相同,Object 类中的 equals 判断对象是否相同的依据是对象的地址值是否相同,故每一次 new 出来的 Key 实例都是不相同的对象实例。
随着时间推移, “cached” 的对象会越来越多. 当泄漏的对象占满了所有的堆内存, GC 又清理不了, 就会抛出 java.lang.OutOfMemoryError:Java heap space
错误。
解决办法很简单, 在 Key 类中恰当地实现 equals() 方法即可:
@Override
public boolean equals(Object o) {
boolean response = false;
if (o instanceof Key) {
response = (((Key)o).id).equals(this.id);
}
return response;
}
为了轻易地兼容从 Struts2 迁移到 SpringMVC 的代码, 在 Controller 中直接获取 request.所以在 ControllerBase 类中通过 ThreadLocal 缓存了当前线程所持有的 request 对象:
public abstract class ControllerBase {
private static ThreadLocal<HttpServletRequest> requestThreadLocal = new ThreadLocal<HttpServletRequest>();
public static HttpServletRequest getRequest(){
return requestThreadLocal.get();
}
public static void setRequest(HttpServletRequest request){
if(null == request){
requestThreadLocal.remove(); //这一步可以避免内存泄漏
return;
}
requestThreadLocal.set(request);
}
}
然后在 SpringMVC 的拦截器(Interceptor)实现类中, 在 preHandle 方法里, 将 request 对象保存到 ThreadLocal 中:
/**
* 登录拦截器
*/
public class LoginCheckInterceptor implements HandlerInterceptor {
private List<String> excludeList = new ArrayList<String>();
public void setExcludeList(List<String> excludeList) {
this.excludeList = excludeList;
}
private boolean validURI(HttpServletRequest request){
// 如果在排除列表中
String uri = request.getRequestURI();
Iterator<String> iterator = excludeList.iterator();
while (iterator.hasNext()) {
String exURI = iterator.next();
if(null != exURI && uri.contains(exURI)){
return true;
}
}
// 可以进行登录和权限之类的判断
LoginUser user = ControllerBase.getLoginUser(request);
if(null != user){
return true;
}
// 未登录,不允许
return false;
}
private void initRequestThreadLocal(HttpServletRequest request){
ControllerBase.setRequest(request);
request.setAttribute("basePath", ControllerBase.basePathLessSlash(request));
}
private void removeRequestThreadLocal(){
ControllerBase.setRequest(null);
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
initRequestThreadLocal(request);
// 如果不允许操作,则返回false即可
if (false == validURI(request)) {
// 此处抛出异常,允许进行异常统一处理
throw new NeedLoginException();
}
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler, ModelAndView modelAndView)
throws Exception {
removeRequestThreadLocal();
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
removeRequestThreadLocal();
}
}
在 postHandle 和 afterCompletion 方法中, 清理 ThreadLocal 中的 request 对象。
但在实际使用过程中, 业务开发人员将一个很大的对象(如占用内存 200MB 左右的 List)设置为 request 的 Attributes, 传递到 JSP 中。
JSP 代码中可能发生了异常, 则 SpringMVC 的 postHandle 和 afterCompletion 方法将不会被执行。
Tomcat 中的线程调度, 可能会一直调度不到那个抛出了异常的线程, 于是 ThreadLocal 一直 hold 住 request。 随着运行时间的推移,把可用内存占满, 一直在执行 Full GC, 系统直接卡死。
后续的修正:通过 Filter, 在 finally 语句块中清理 ThreadLocal。
@WebFilter(value="/*", asyncSupported=true)
public class ClearRequestCacheFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
clearControllerBaseThreadLocal();
try {
chain.doFilter(request, response);
} finally {
clearControllerBaseThreadLocal();
}
}
private void clearControllerBaseThreadLocal() {
ControllerBase.setRequest(null);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
}
教训是:可以使用 ThreadLocal, 但必须有受控制的释放措施、一般就是 try-finally 的代码形式。
- 说明: SpringMVC 的 Controller 中, 其实可以通过 @Autowired 注入 request, 实际注入的是一个 HttpServletRequestWrapper 对象, 执行时也是通过 ThreadLocal 机制调用当前的 request。
- 常规方式: 直接在 Controller 方法中接收 request 参数即可。
如果设置的最大内存不满足程序的正常运行, 只需要增大堆内存即可,但很多情况下, 增加堆内存空间并不能解决问题。比如存在内存泄漏, 增加堆内存只会推迟 java.lang.OutOfMemoryError: Java heap space
错误的触发时间。
当然, 增大堆内存, 可能会增加 GC pauses 的时间, 从而影响程序的 吞吐量或延迟。要从根本上解决问题, 则需要排查分配内存的代码. 简单来说, 需要解决这些问题:
要搞清这一点, 可能需要好几天时间。下面是大致的流程:
3. 获得在生产服务器上执行堆转储(heap dump)的权限。“转储”(Dump)是堆内存的快照, 稍后可以用于内存分析. 这些快照中可能含有机密信息, 例如密码、信用卡账号等, 所以有时候, 由于企业的安全限制, 要获得生产环境的堆转储并不容易。
在适当的时间执行堆转储。一般来说,内存分析需要比对多个堆转储文件, 假如获取的时机不对, 那就可能是一个“废”的快照. 另外, 每次执行堆转储, 都会对JVM进行“冻结”, 所以生产环境中,也不能执行太多的Dump操作,否则系统缓慢或者卡死,你的麻烦就大了。
用另一台机器来加载Dump文件。一般来说, 如果出问题的JVM内存是8GB, 那么分析 Heap Dump 的机器内存需要大于 8GB. 打开转储分析软件。如 VisualVM。
检测快照中占用内存最大的 GC roots。 这对新手来说可能有点困难, 但这也会加深你对堆内存结构以及 navigation 机制的理解。
接下来, 找出可能会分配大量对象的代码. 如果对整个系统非常熟悉, 可能很快就能定位了。
java.lang.OutOfMemoryError: GC overhead limit exceeded
这种情况发生的原因是, 程序基本上耗尽了所有的可用内存, GC 也清理不了。
注意,java.lang.OutOfMemoryError: GC overhead limit exceeded
错误只在连续多次 GC 都只回收了不到 2% 的极端情况下才会抛出。假如不抛出 GC overhead limit 错误会发生什么情况呢? 那就是 GC 清理的这么点内存很快会再次填满, 迫使 GC 再次执行. 这样就形成恶性循环, CPU 使用率一直是 100%, 而 GC 却没有任何成果. 系统用户就会看到系统卡死 - 以前只需要几毫秒的操作, 现在需要好几分钟才能完成.
以下代码在无限循环中往 Map 里添加数据。 这会导致GC overhead limit exceeded
错误:
import java.util.Map;
import java.util.Random;
public class TestWrapper {
public static void main(String args[]) throws Exception {
Map map = System.getProperties();
Random r = new Random();
while (true) {
map.put(r.nextInt(), "value");
}
}
}
配置JVM参数: -Xmx12m。执行时产生的错误信息如下所示:
很快就看到了 java.lang.OutOfMemoryError: GC overhead limit exceeded 错误提示消息。但实际上这个示例是有些坑的. 因为配置不同的堆内存大小, 选用不同的 GC 算法, 产生的错误信息也不相同。例如,当 Java 堆内存设置为 10M 时:
读者应该试着修改参数, 执行看看具体。错误提示以及堆栈信息可能不太一样。
这里在 Map 进行 rehash 时抛出了 java.lang.OutOfMemoryError: Java heap space
错误消息. 如果使用其他 垃圾收集算法, 比如 -XX:+UseConcMarkSweepGC, 或者 -XX:+UseG1GC, 错误将被默认的 exception handler 所捕获, 但是没有 stacktrace 信息, 因为在创建 Exception 时 没办法填充 stacktrace 信息。
这些真实的案例表明, 在资源受限的情况下, 无法准确预测程序会死于哪种具体的原因。所以在这类错误面前, 不能绑死某种特定的错误处理顺序。
有一种应付了事的解决方案, 就是不想抛出 java.lang.OutOfMemoryError: GC overhead limit exceeded
错误信息, 则添加下面启动参数:
// 不推荐
-XX:-UseGCOverheadLimit
强烈建议不要指定该选项: 因为这不能真正地解决问题,只能推迟一点 out of memory 错误发生的时间,到最后还得进行其他处理。指定这个选项, 会将原来的 java.lang.OutOfMemoryError: GC overhead limit exceeded
错误掩盖,变成更常见的 java.lang.OutOfMemoryError: Java heap space
错误消息。
有时候触发 GC overhead limit 错误的原因, 是因为分配给 JVM 的堆内存不足。这种情况下只需要增加堆内存大小即可。
在JDK1.7及之前的版本, 永久代(permanent generation) 主要用于存储加载/缓存到内存中的 class 定义, 包括 class 的 名称(name), 字段(fields), 方法(methods)和字节码(method bytecode); 以及常量池(constant pool information); 对象数组(object arrays)/类型数组(type arrays)所关联的 class, 还有 JIT 编译器优化后的class信息等。
PermGen 的使用量和JVM加载到内存中的 class 数量/大小有关。可以说 java.lang.OutOfMemoryError: PermGen space
的主要原因, 是加载到内存中的 class 数量太多或体积太大。
JVM限制了Java程序的最大内存, 修改/指定启动参数可以改变这种限制。Java将堆内存划分为多个部分, 如下图所示:
java.lang.OutOfMemoryError: Metaspace
错误所表达的信息是: 元数据区(Metaspace) 已被用满。
从Java 8开始,内存结构发生重大改变, 不再使用Permgen, 而是引入一个新的空间: Metaspace. 这种改变基于多方面的考虑, 部分原因列举如下:
Permgen空间的具体多大很难预测。指定小了会造成 java.lang.OutOfMemoryError: Permgen size
错误, 设置多了又造成浪费。
为了 GC 性能 的提升, 使得垃圾收集过程中的并发阶段不再 停顿, 另外对 metadata 进行特定的遍历(specific iterators)。
对 G1垃圾收集器 的并发 class unloading 进行深度优化。
Metaspace 的使用量与 JVM 加载到内存中的 class 数量/大小有关。可以说, java.lang.OutOfMemoryError: Metaspace
错误的主要原因, 是加载到内存中的 class 数量太多或者体积太大。
public class Metaspace {
static javassist.ClassPool cp = javassist.ClassPool.getDefault();
public static void main(String[] args) throws Exception{
for (int i = 0; ; i++) {
Class c = cp.makeClass("eu.plumbr.demo.Generated" + i).toClass();
}
}
}
可以看到, 使用 javassist 工具库生成 class 那是非常简单。在 for 循环中, 动态生成很多class, 最终将这些class加载到 Metaspace 中。
执行这段代码, 随着生成的class越来越多, 最后将会占满 Metaspace 空间, 抛出 java.lang.OutOfMemoryError: Metaspace
. 在Mac OS X上, Java 1.8.0_05 环境下, 如果设置了启动参数 -XX:MaxMetaspaceSize=64m, 大约加载 70000 个class后JVM就会挂掉。
如果抛出与 Metaspace 有关的 OutOfMemoryError , 第一解决方案是增加 Metaspace 的大小. 使用下面这样的启动参数:
-XX:MaxMetaspaceSize=512m
这里将 Metaspace 的最大值设置为 512MB, 如果没有用完, 就不会抛出 OutOfMemoryError。
有一种看起来很简单的方案, 是直接去掉 Metaspace 的大小限制。 但需要注意, 不限制 Metaspace 内存的大小, 假若物理内存不足, 有可能会引起内存交换(swapping), 严重拖累系统性能。 此外,还可能造成 native 内存分配失败等问题。
在现代应用集群中,宁可让应用节点挂掉, 也不希望其响应缓慢。
如果不想收到报警, 可以像鸵鸟一样, 把 java.lang.OutOfMemoryError: Metaspace
错误信息隐藏起来。 但这不能真正解决问题, 只会推迟问题爆发的时间。 如果确实存在内存泄露, 按之前的做法处理即可。
JVM 中的线程需要内存空间来执行自己的任务. 如果线程数量太多, 就会引入新的问题。java.lang.OutOfMemoryError: Unable to create new native thread
错误表示: 程序创建的线程数量已达到上限值
JVM 向操作系统申请创建新的 native thread(原生线程)时, 就有可能会碰到 java.lang.OutOfMemoryError: Unable to create new native thread
错误. 如果底层操作系统创建新的 native thread 失败, JVM 就会抛出相应的OutOfMemoryError. 原生线程的数量受到具体环境的限制, 通过一些测试用例可以找出这些限制, 请参考下文的示例. 但总体来说, 导致 java.lang.OutOfMemoryError: Unable to create new native thread
错误的场景大多经历以下这些阶段:
Java 程序向JVM请求创建一个新的Java线程;
JVM本地代码(native code)代理该请求, 尝试创建一个操作系统级别的 native thread(原生线程);
操作系统尝试创建一个新的native thread, 需要同时分配一些内存给该线程;
如果操作系统的虚拟内存已耗尽, 或者是受到32位进程的地址空间限制(约2-4GB), OS就会拒绝本地内存分配;
JVM 抛出 java.lang.OutOfMemoryError: Unable to create new native thread 错误
下面的代码在一个死循环中创建并启动很多新线程。代码执行后, 很快就会达到操作系统的限制, 报出 java.lang.OutOfMemoryError: Unable to create new native thread
错误:
while(true){
new Thread(new Runnable(){
public void run() {
try {
Thread.sleep(10000000);
} catch(InterruptedException e) { }
}
}).start();
}
原生线程的数量由具体环境决定, 比如, 在 Windows, Linux 和 Mac OS X 系统上:
所以如果想知道系统的极限在哪儿, 只需要一个小小的测试用例就够了, 找到触发 java.lang.OutOfMemoryError: Unable to create new native thread 时创建的线程数量即可。
有时可以修改系统限制来避开 Unable to create new native thread 问题. 假如JVM受到用户空间(user space)文件数量的限制, 像下面这样,就应该想办法增大这个值:
[root@dev ~]# ulimit -a
core file size (blocks, -c) 0
...... 省略部分内容 ......
max user processes (-u) 1800
更多的情况, 触发创建 native 线程时的OutOfMemoryError, 表明编程存在BUG. 比如, 程序创建了成千上万的线程, 很可能就是某些地方出大问题了 —— 没有几个程序可以 Hold 住上万个线程的。
一种解决办法是执行线程转储(thread dump) 来分析具体情况。 一般需要花费好几个工作日来处理。
JVM启动参数指定了最大内存限制。如 -Xmx 以及相关的其他启动参数. 假若JVM使用的内存总量超过可用的物理内存, 操作系统就会用到虚拟内存。错误信息 java.lang.OutOfMemoryError: Out of swap space?
表明, 交换空间(swap space,虚拟内存) 不足,是由于物理内存和交换空间都不足所以导致内存分配失败。
如果 native heap 内存耗尽, 内存分配时, JVM 就会抛出 java.lang.OutOfmemoryError: Out of swap space? 错误消息, 这个消息告诉用户, 请求分配内存的操作失败了。
Java进程使用了虚拟内存才会发生这个错误。 对 Java的垃圾收集 来说这是很难应付的场景。即使现代的 GC算法 很先进, 但虚拟内存交换引发的系统延迟, 会让 GC暂停时间 膨胀到令人难以容忍的地步。
通常是操作系统层面的原因导致 java.lang.OutOfMemoryError: Out of swap space? 问题, 例如:
当然也可能是应用程序的本地内存泄漏(native leak)引起的, 例如, 某个程序/库不断地申请本地内存,却不进行释放。
这个问题有多种解决办法。
第一种, 也是最简单的方法, 增加虚拟内存(swap space) 的大小. 各操作系统的设置方法不太一样, 比如Linux,可以使用下面的命令设置:
swapoff -a
dd if=/dev/zero of=swapfile bs=1024 count=655360
mkswap swapfile
swapon swapfile
其中创建了一个大小为 640MB 的 swapfile(交换文件) 并启用该文件。
因为垃圾收集器需要清理整个内存空间, 所以虚拟内存对 Java GC 来说是难以忍受的。存在内存交换时, 执行 垃圾收集 的 暂停时间 会增加上百倍,甚至更多, 所以最好不要增加虚拟内存。
如果程序允许环境还受到 “坏邻居效应” 的干扰, 那么JVM还要和其他程序竞争计算资源, 提高性能的办法就是单独部署到专用的服务器/虚拟机中。
大多数时候, 我们唯一能做的就是升级服务器配置, 增加物理机的内存。当然也可以进行程序优化, 降低内存空间的使用量, 通过堆转储分析器可以检测到哪些方法/代码分配了大量的内存。
Java平台限制了数组的最大长度。各个版本的具体限制可能稍有不同, 但范围都在 1 ~ 21亿 之间。如果程序抛出 java.lang.OutOfMemoryError: Requested array size exceeds VM limit
错误, 就说明想要创建的数组长度超过限制。
这个错误是由JVM中的本地代码抛出的. 在真正为数组分配内存之前, JVM会执行一项检查: 要分配的数据结构在该平台是否可以寻址(addressable). 当然, 这个错误比你所想的还要少见得多。
一般很少看到这个错误, 因为Java使用 int 类型作为数组的下标(index, 索引)。在Java中, int类型的最大值为 2^31 – 1 = 2,147,483,647。大多数平台的限制都约等于这个值 —— 例如在 64位的 MB Pro 上, Java 1.7 平台可以分配长度为 2,147,483,645, 以及 Integer.MAX_VALUE-2) 的数组。
再增加一点点长度, 变成 Integer.MAX_VALUE-1 时, 就会抛出我们所熟知的Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
在有的平台上, 这个最大限制可能还会更小一些, 例如在32位Linux, OpenJDK 6 上面, 数组长度大约在 11亿左右(约2^30) 就会抛出 “java.lang.OutOfMemoryError: Requested array size exceeds VM limit“ 错误。要找出具体的限制值, 可以执行一个小小的测试用例.
以下代码用来演示 java.lang.OutOfMemoryError: Requested array size exceeds VM limit 错误:
for (int i = 3; i >= 0; i--) {
try {
int[] arr = new int[Integer.MAX_VALUE-i];
System.out.format("Successfully initialized an array with %,d elements.\n", Integer.MAX_VALUE-i);
} catch (Throwable t) {
t.printStackTrace();
}
}
其中,for循环迭代4次, 每次都去初始化一个 int 数组, 长度从 Integer.MAX_VALUE-3 开始递增, 到 Integer.MAX_VALUE 为止. 在 64位 Mac OS X 的 Hotspot 7 平台上, 执行这段代码会得到类似下面这样的结果:
请注意, 在后两次迭代抛出 java.lang.OutOfMemoryError: Requested array size exceeds VM limit 错误之前, 先抛出了2次 java.lang.OutOfMemoryError: Java heap space 错误。 这是因为 2^31-1 个 int 数占用的内存超过了JVM默认的8GB堆内存。
此示例也展示了这个错误比较罕见的原因 —— 要取得JVM对数组大小的限制, 要分配长度差不多等于 Integer.MAX_INT 的数组. 这个示例运行在64位的Mac OS X, Hotspot 7平台时, 只有两个长度会抛出这个错误: Integer.MAX_INT-1 和 Integer.MAX_INT。
发生 java.lang.OutOfMemoryError: Requested array size exceeds VM limit 错误的原因可能是:
第一种情况, 需要检查业务代码, 确认是否真的需要那么大的数组。如果可以减小数组长度, 那就万事大吉. 如果不行,可能需要把数据拆分为多个块, 然后根据需要按批次加载。
如果是第二种情况, 请记住, Java 数组用 int 值作为索引。所以数组元素不能超过 2^31-1 个. 实际上, 代码在编译阶段就会报错,提示信息为 “error: integer number too large”。
如果确实需要处理超大数据集, 那就要考虑调整解决方案了. 例如拆分成多个小块,按批次加载; 或者放弃使用标准库,而是自己处理数据结构,比如使用 sun.misc.Unsafe 类, 通过Unsafe工具类可以像C语言一样直接分配内存。
我们知道, 操作系统(operating system)构建在进程(process)的基础上. 进程由内核作业(kernel jobs)进行调度和维护, 其中有一个内核作业称为 “Out of memory killer(OOM终结者)”, 与本节所讲的 OutOfMemoryError 有关。
Out of memory killer 在可用内存极低的情况下会杀死某些进程。只要达到触发条件就会激活, 选中某个进程并杀掉。 通常采用启发式算法, 对所有进程计算评分(heuristics scoring), 得分最低的进程将被 kill 掉。因此 Out of memory: Kill process or sacrifice child 和前面所讲的 OutOfMemoryError 都不同, 因为它既不由JVM触发,也不由JVM代理, 而是系统内核内置的一种安全保护措施。
如果可用内存(含swap)不足, 就有可能会影响系统稳定, 这时候 Out of memory killer 就会设法找出流氓进程并杀死他, 也就是引起 Out of memory: kill process or sacrifice child 错误。
默认情况下, Linux kernels(内核)允许进程申请的量超过系统可用内存. 这是因为,在大多数情况下, 很多进程申请了很多内存, 但实际使用的量并没有那么多.
有个简单的类比, 宽带租赁的服务商, 可能他的总带宽只有 10Gbps, 但却卖出远远超过100份以上的 100Mbps 带宽, 原因是多数时候, 宽带用户之间是错峰的, 而且不可能每个用户都用满服务商所承诺的带宽。
这样的话,可能会有一个问题, 假若某些程序占用了大量的系统内存, 那么可用内存量就会极小, 导致没有内存页面(pages)可以分配给需要的进程。可能这时候会出现极端情况, 就是 root 用户也不能通过 kill 来杀掉流氓进程. 为了防止发生这种情况, 系统会自动激活 killer, 查找流氓进程并将其杀死。
更多关于 ”Out of memory killer“ 的性能调优细节, 请参考: RedHat 官方文档
现在我们知道了为什么会发生这种问题, 那为什么是半夜5点钟触发 “killer” 发报警信息给你呢? 通常触发的原因在于操作系统配置. 例如, /proc/sys/vm/overcommit_memory 配置文件的值, 指定了是否允许所有的 malloc() 调用成功. 请注意, 在各操作系统中, 这个配置对应的 proc 文件路径可能不同。
过量使用(overcommitting)配置, 允许流氓进程申请越来越多的内存, 最终惹得 ”Out of memory killer“ 出来搞事情。
在Linux上(如最新稳定版的Ubuntu)编译并执行以下的示例代码:
package eu.plumbr.demo;
public class OOM {
public static void main(String[] args){
java.util.List<int[]> l = new java.util.ArrayList();
for (int i = 10000; i < 100000; i++) {
try {
l.add(new int[100_000_000]);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
}
将会在系统日志中(如 /var/log/kern.log 文件)看到一个错误, 类似这样:
提示: 可能需要调整 swap 的大小并设置最大堆内存, 例如堆内存配置为 -Xmx2g, swap 配置如下:
swapoff -a
dd if=/dev/zero of=swapfile bs=1024 count=655360
mkswap swapfile
swapon swapfile
有多种处理办法。最简单的办法就是将系统迁移到内存更大的实例中。
另外, 还可以通过 OOM killer 调优, 或者做负载均衡(水平扩展,集群), 或者降低应用对内存的需求。
不太推荐的方案是加大交换空间/虚拟内存(swap space)。 试想一下, Java 包含了自动垃圾回收机制, 增加交换内存的代价会很高昂. 现代GC算法在处理物理内存时性能飞快, 但对交换内存来说,其效率就是硬伤了. 交换内存可能导致GC暂停的时间增长几个数量级, 因此在采用这个方案之前, 看看是否真的有这个必要。
JAVA 线上故障排查完整套路!牛掰!
当程序响应变慢的时候,首先使用 top 、vmstat 、ps 等命令查看系统的 CPU 使用率是否有异常,从而可以判断出是否是 CPU 繁忙造成的性能问题。其中,主要通过 us(用户进程所占的%)这个数据来看异常的进程信息。当 us 接近 100% 甚至更高时,可以确定是 CPU 繁忙造成的响应缓慢。一般说来,CPU 繁忙的原因有以下几个:
top
top -Hp pid
jstack pid
jstack pid |grep 'threadPid' -C5 --color
,表示打印进程堆栈并通过线程id,过滤得到线程堆栈信息`jstat -gcutil pid
vmstat 1 5
-XX:+PrintCompilation
这个参数输出 JIT 编译情况,以排查 JIT 编译引起的 CPU 问题此外,使用多线程的时候,还需要注意以下几点:
对 Java 应用来说,内存主要是由堆外内存和堆内内存组成
堆外内存
堆外内存主要是 JNI 、Deflater/Inflater 、DirectByteBuffer(nio 中会用到)使用的。对于这种堆外内存的分析,还是需要先通过 vmstat 、sar 、top 、pidstat (这里的 sar,pidstat 以及 iostat 都是sysstat软件套件的一部分,需要单独安装)等查看 swap 和物理内存的消耗状况再做判断的。此外,对于 JNI 、Deflater 这种调用可以通过 Google-preftools 来追踪资源使用状况。
堆内内存
此部分内存为 Java 应用主要的内存区域。通常与这部分内存性能相关的有:
以上使用不当很容易造成:
排查堆内存问题的常用工具是 jmap,是 jdk 自带的。一些常用用法如下:
jmap -heap
jmap -histo:live
jmap -dump:format=b,file=xxx.hprof
jmap -dump:format=b,live,file=xxx.hprof
此外,不管是使用 jmap 还是在 OOM 时产生的 dump 文件,可以使用 Eclipse 的 MAT(MEMORY ANALYZER TOOL)来分析,可以看到具体的堆栈和内存中对象的信息。当然 jdk 自带的 jhat 也能够查看 dump 文件(启动 web 端口供开发者使用浏览器浏览堆内对象的信息)。此外,VisualVM 也能够打开 hprof (其实这个后缀名可以是任意字面量) 文件,使用它的 heap walker 查看堆内存信息
内存的调优主要就是对 JVM 的调优
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:[log_path]
,以记录 gc 日志,便于排查问题。其中,对于第一点,具体的还有一点建议:
年轻代大小选择:响应时间优先的应用,尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生 gc 的频率是最小的。同时,也能够减少到达年老代的对象。吞吐量优先的应用,也尽可能的设置大,因为对响应时间没有要求,垃圾收集可以并行进行,建议适合 8 CPU 以上的应用使用
老年代大小选择
响应时间优先的应用,老年代一般都是使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,会造成内存碎片 、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
一般吞吐量优先的应用都应该有一个很大的年轻代和一个较小的年老代。这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代存放长期存活对象
代码上,也需要注意:
避免保存重复的 String 对象,同时也需要小心 String.subString() 与 String.intern() 的使用,尤其是后者其底层数据结构为 StringTable,当字符串大量不重复时,会使得 StringTable 非常大(一个固定大小的 hashmap,可以由参数 -XX:StringTableSize=N
设置大小),从而影响 young gc 的速度。在 jackson 和 fastjson 中使用了此方法,某些场景下会引起 GC 问题: YGC越来越慢,为什么。
尽量不要使用 finalizer
释放不必要的引用:ThreadLocal 使用完记得释放以防止内存泄漏,各种 Stream 使用完也记得 close
使用对象池避免无节制创建对象,造成频繁 gc。但不要随便使用对象池,除非像连接池 、线程池这种初始化/创建资源消耗较大的场景
缓存失效算法,可以考虑使用 SoftReference 、WeakReference 保存缓存对象
谨慎热部署/加载的使用,尤其是动态加载类等
不要用 Log4j 输出文件名 、行号,因为 Log4j 通过打印线程堆栈实现,会生成大量 String。此外,使用 log4j 时,建议此种经典用法:先判断对应级别的日志是否打开,再做操作,否则也会生成大量 String
if (logger.isInfoEnabled()) {
logger.info(msg);
}
有时候部署场景会有线程死锁的问题发生(并不常见),我们采用 jstack 查看一下。比如说我们现在已经有一个线程死锁的程序,导致某些操作 waiting 中
top 或者 jps
jstack -l pid
通常与应用性能相关的包括:文件 IO 和网络 IO
可以使用系统工具 pidstat 、iostat 、vmstat 来查看 io 的状况
使用 vmstat 的结果图:
这里主要注意 bi 和 bo 这两个值,分别表示块设备每秒接收的块数量和块设备每秒发送的块数量,由此可以判定 IO 繁忙状况。进一步的可以通过使用 strace 工具来定位对文件 IO 的系统调用。通常,造成文件 IO 性能差的原因不外乎:
查看网络 IO 状况,一般使用的是 netstat 工具。可以查看所有连接的状况 、数目 、端口信息等。例如:当 time_wait 或者 close_wait 连接过多时,会影响应用的相应速度
使用 netstat -anp
此外,还可以使用 tcpdump 来具体分析网络 IO 的数据。当然,tcpdump 出的文件直接打开是一堆二进制的数据,可以使用 wireshark 阅读具体的连接以及其中数据的内容
使用 tcpdump -i eth0 -w tmp.cap -tnn dst port 8080
监听 8080 端口的网络请求并打印日志到 tmp.cap 中,还可以通过查看 /proc/interrupts
来获取当前系统使用的中断的情况:
各个列依次是:irq的序号, 在各自cpu上发生中断的次数,可编程中断控制器,设备名称(request_irq的dev_name字段)
通过查看网卡设备的终端情况可以判断网络 IO 的状况
文件 IO 上需要注意:
网络 IO 上需要注意: