WangBooth's Logbook

王布斯的网络日志

0%

JVM限制了Java程序的最大内存, 修改/指定启动参数可以改变这种限制。Java将堆内存划分为多个部分, 如下图所示:

04_01_OOM-example-metaspace.png

【Java8及以上】这些内存池的最大值, 由 -Xmx-XX:MaxMetaspaceSize 等JVM启动参数指定. 如果没有明确指定, 则根据平台类型(OS版本+JVM版本)和物理内存的大小来确定。

java.lang.OutOfMemoryError: Metaspace 错误所表达的信息是: 元数据区(Metaspace) 已被用满

原因分析

如果你是Java老司机, 应该对 PermGen 比较熟悉. 但从Java 8开始,内存结构发生重大改变, 不再使用Permgen, 而是引入一个新的空间: Metaspace. 这种改变基于多方面的考虑, 部分原因列举如下:

  • Permgen空间的具体多大很难预测。指定小了会造成 java.lang.OutOfMemoryError: Permgen size 错误, 设置多了又造成浪费。
  • 为了 GC 性能 的提升, 使得垃圾收集过程中的并发阶段不再 停顿, 另外对 metadata 进行特定的遍历(specific iterators)。
  • G1垃圾收集器 的并发 class unloading 进行深度优化。

在Java8中,将之前 PermGen 中的所有内容, 都移到了 Metaspace 空间。例如: class 名称, 字段, 方法, 字节码, 常量池, JIT优化代码, 等等。

Metaspace 的使用量与JVM加载到内存中的 class 数量/大小有关。可以说, java.lang.OutOfMemoryError: Metaspace\ 错误的主要原因, 是加载到内存中的 class 数量太多或者体积太大

示例

和 PermGen 类似, Metaspace 空间的使用量, 与JVM加载的 class 数量有很大关系。下面是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
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();
}
}
}
12345678910

可以看到, 使用 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 的大小. 使用下面这样的启动参数:

1
-XX:MaxMetaspaceSize=512m

这里将 Metaspace 的最大值设置为 512MB, 如果没有用完, 就不会抛出 OutOfMemoryError

有一种看起来很简单的方案, 是直接去掉 Metaspace 的大小限制。 但需要注意, 不限制Metaspace内存的大小, 假若物理内存不足, 有可能会引起内存交换(swapping), 严重拖累系统性能。 此外,还可能造成native内存分配失败等问题。

在现代应用集群中,宁可让应用节点挂掉, 也不希望其响应缓慢。

如果不想收到报警, 可以像鸵鸟一样, 把 java.lang.OutOfMemoryError: Metaspace 错误信息隐藏起来。 但这不能真正解决问题, 只会推迟问题爆发的时间。 如果确实存在内存泄露, 请参考前面的文章, 认真寻找解决方案。

JVM限制了Java程序的最大内存使用量, 可以通过启动参数来配置。而Java的堆内存被划分为多个区域, 如下图所示:

java.lang.outofmemoryerror: Permgen space

这些区域的最大值, 由JVM启动参数 -Xmx-XX:MaxPermSize 指定. 如果没有明确指定, 则根据操作系统平台和物理内存的大小来确定。

java.lang.OutOfMemoryError: PermGen space 错误信息所表达的意思是: 永久代(Permanent Generation) 内存区域已满

原因分析

我们先看看 PermGen 是用来干什么的。

在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 数量太多或体积太大。

示例

最简单的例子

我们知道, PermGen 空间的使用量, 与JVM加载的 class 数量有很大关系。下面的代码演示了这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import javassist.ClassPool;

public class MicroGenerator {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100_000_000; i++) {
generate("eu.plumbr.demo.Generated" + i);
}
}

public static Class generate(String name) throws Exception {
ClassPool pool = ClassPool.getDefault();
return pool.makeClass(name).toClass();
}
}

这段代码在 for 循环中, 动态生成了很多class。可以看到, 使用 javassist 工具类生成 class 是非常简单的。

执行这段代码, 会生成很多新的 class 并将其加载到内存中, 随着生成的class越来越多,将会占满Permgen空间, 然后抛出 java.lang.OutOfMemoryError: Permgen space 错误, 当然, 也有可能会抛出其他类型的 OutOfMemoryError。

要快速看到效果, 可以加上适当的JVM启动参数, 如: -Xmx200M -XX:MaxPermSize=16M 等等。

Redeploy 时产生的 OutOfMemoryError

说明: 如果在开发时Tomcat产生警告,可以忽略。 生产环境建议不要 redploy,直接关闭/或Kill相关的JVM,然后从头开始启动即可。

下面的情形更常见,在重新部署web应用时, 很可能会引起 java.lang.OutOfMemoryError: Permgen space 错误. 按道理说, redeploy 时, Tomcat之类的容器会使用新的 classloader 来加载新的 class, 让垃圾收集器 将之前的 classloader (连同加载的class一起)清理掉,。

但实际情况可能并不乐观, 很多第三方库, 以及某些受限的共享资源, 如 thread, JDBC驱动, 以及文件系统句柄(handles), 都会导致不能彻底卸载之前的 classloader. 那么在 redeploy 时, 之前的class仍然驻留在PermGen中, 每次重新部署都会产生几十MB,甚至上百MB的垃圾

假设某个应用在启动时, 通过初始化代码加载JDBC驱动连接数据库. 根据JDBC规范, 驱动会将自身注册到 java.sql.DriverManager, 也就是将自身的一个实例(instance) 添加到 DriverManager 中的一个 static 域。

那么, 当应用从容器中卸载时, java.sql.DriverManager 依然持有 JDBC实例(Tomcat经常会发出警告), 而JDBC驱动实例又持有 java.lang.Classloader 实例, 那么 垃圾收集器 也就没办法回收对应的内存空间。

java.lang.ClassLoader 实例持有着其加载的所有 class, 通常是几十/上百 MB的内存。可以看到, redeploy时会占用另一块差不多大小的 PermGen 空间, 多次 redeploy 之后, 就会造成 java.lang.OutOfMemoryError: PermGen space 错误, 在日志文件中, 你应该会看到相关的错误信息。

解决方案

1. 解决程序启动时产生的 OutOfMemoryError

在程序启动时, 如果 PermGen 耗尽而产生 OutOfMemoryError 错误, 那很容易解决. 增加 PermGen 的大小, 让程序拥有更多的内存来加载 class 即可. 修改 -XX:MaxPermSize 启动参数, 类似下面这样:

1
java -XX:MaxPermSize=512m com.yourcompany.YourClass

以上配置允许JVM使用的最大 PermGen 空间为 512MB, 如果还不够, 就会抛出 OutOfMemoryError

2. 解决 redeploy 时产生的 OutOfMemoryError

我们可以进行堆转储分析(heap dump analysis) —— 在 redeploy 之后, 执行堆转储, 类似下面这样:

1
jmap -dump:format=b,file=dump.hprof <process-id>

然后通过堆转储分析器(如强悍的 Eclipse MAT)加载 dump 得到的文件。找出重复的类, 特别是类加载器(classloader)对应的 class. 你可能需要比对所有的 classloader, 来找出当前正在使用的那个。

Eclipse MAT 在各个平台都有独立安装包. 大约50MB左右, 官网下载地址: http://www.eclipse.org/mat/downloads.php

对于不使用的类加载器(inactive classloader), 需要先确定最短路径的 GC root , 看看是哪一个阻止其被 垃圾收集器 所回收. 这样才能找到问题的根源. 如果是第三方库的原因, 那么可以搜索 Google/StackOverflow 来查找解决方案. 如果是自己的代码问题, 则需要在恰当的时机来解除相关引用。

3. 解决运行时产生的 OutOfMemoryError

如果在运行的过程中发生 OutOfMemoryError, 首先需要确认 GC是否能从PermGen中卸载class。 官方的JVM在这方面是相当的保守(在加载class之后,就一直让其驻留在内存中,即使这个类不再被使用). 但是, 现代的应用程序在运行过程中, 会动态创建大量的class, 而这些class的生命周期基本上都很短暂, 旧版本的JVM 不能很好地处理这些问题。那么我们就需要允许JVM卸载class。使用下面的启动参数:

1
-XX:+CMSClassUnloadingEnabled

默认情况下 CMSClassUnloadingEnabled 的值为false, 所以需要明确指定。 启用以后, GC 将会清理 PermGen, 卸载无用的 class. 当然, 这个选项只有在设置 UseConcMarkSweepGC 时生效。 如果使用了 ParallelGC, 或者 Serial GC 时, 那么需要切换为CMS:

1
-XX:+UseConcMarkSweepGC

如果确定 class 可以被卸载, 假若还存在 OutOfMemoryError, 那就需要进行堆转储分析了, 类似下面这种命令:

1
jmap -dump:file=dump.hprof,format=b <process-id>

然后通过堆转储分析器(如 Eclipse MAT) 加载 heap dump。找出最重的 classloader, 也就是加载 class 数量最多的那个. 通过加载的 class 及对应的实例数量, 比对类加载器, 找出最靠前的部分, 挨个进行分析。

对于每个有嫌疑的类, 都需要手动跟踪到生成这些类的代码中, 以定位问题。

转载自: https://blog.csdn.net/renfufei/article/details/77585294

Java运行时环境内置了 垃圾收集(GC) 模块. 上一代的很多编程语言中并没有自动内存回收机制, 需要程序员手工编写代码来进行内存分配和释放, 以重复利用堆内存。

在Java程序中, 只需要关心内存分配就行。如果某块内存不再使用, 垃圾收集(Garbage Collection) 模块会自动执行清理。GC的详细原理请参考 GC性能优化 系列文章, 一般来说, JVM内置的垃圾收集算法就能够应对绝大多数的业务场景。

java.lang.OutOfMemoryError: GC overhead limit exceeded 这种情况发生的原因是, 程序基本上耗尽了所有的可用内存, GC也清理不了

原因分析

JVM抛出 java.lang.OutOfMemoryError: GC overhead limit exceeded 错误就是发出了这样的信号: 执行垃圾收集的时间比例太大, 有效的运算量太小. 默认情况下, 如果GC花费的时间超过 98%, 并且GC回收的内存少于 2%, JVM就会抛出这个错误。

java.lang.OutOfMemoryError: GC overhead limit exceeded

注意, java.lang.OutOfMemoryError: GC overhead limit exceeded 错误只在连续多次 GC 都只回收了不到2%的极端情况下才会抛出。假如不抛出 GC overhead limit 错误会发生什么情况呢? 那就是GC清理的这么点内存很快会再次填满, 迫使GC再次执行. 这样就形成恶性循环, CPU使用率一直是100%, 而GC却没有任何成果. 系统用户就会看到系统卡死 - 以前只需要几毫秒的操作, 现在需要好几分钟才能完成。

这也是一个很好的 快速失败原则 的案例。

示例

以下代码在无限循环中往 Map 里添加数据。 这会导致 “GC overhead limit exceeded” 错误:

1
2
3
4
5
6
7
8
9
10
11
12
package com.cncounter.rtime;
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。执行时产生的错误信息如下所示:

1
2
3
4
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.Hashtable.addEntry(Hashtable.java:435)
at java.util.Hashtable.put(Hashtable.java:476)
at com.cncounter.rtime.TestWrapper.main(TestWrapper.java:11)

你碰到的错误信息不一定就是这个。确实, 我们执行的JVM参数为:

1
java -Xmx12m -XX:+UseParallelGC TestWrapper

很快就看到了 java.lang.OutOfMemoryError: GC overhead limit exceeded 错误提示消息。但实际上这个示例是有些坑的. 因为配置不同的堆内存大小, 选用不同的GC算法, 产生的错误信息也不相同。例如,当Java堆内存设置为10M时:

1
java -Xmx10m -XX:+UseParallelGC TestWrapper

DEBUG模式下错误信息如下所示:

1
2
3
4
5
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Hashtable.rehash(Hashtable.java:401)
at java.util.Hashtable.addEntry(Hashtable.java:425)
at java.util.Hashtable.put(Hashtable.java:476)
at com.cncounter.rtime.TestWrapper.main(TestWrapper.java:11)

读者应该试着修改参数, 执行看看具体。错误提示以及堆栈信息可能不太一样。

这里在 Map 进行 rehash 时抛出了 java.lang.OutOfMemoryError: Java heap space 错误消息. 如果使用其他 垃圾收集算法, 比如 -XX:+UseConcMarkSweepGC, 或者 -XX:+UseG1GC, 错误将被默认的 exception handler 所捕获, 但是没有 stacktrace 信息, 因为在创建 Exception 时 没办法填充stacktrace信息

例如配置:

1
-Xmx12m -XX:+UseG1GC

在Win7x64, Java8环境运行, 产生的错误信息为:

1
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

建议读者修改内存配置, 以及垃圾收集算法进行测试。

这些真实的案例表明, 在资源受限的情况下, 无法准确预测程序会死于哪种具体的原因。所以在这类错误面前, 不能绑死某种特定的错误处理顺序。

解决方案

有一种应付了事的解决方案, 就是不想抛出 “java.lang.OutOfMemoryError: GC overhead limit exceeded” 错误信息, 则添加下面启动参数:

1
2
// 不推荐
-XX:-UseGCOverheadLimit

我们强烈建议不要指定该选项: 因为这不能真正地解决问题,只能推迟一点 out of memory 错误发生的时间,到最后还得进行其他处理。指定这个选项, 会将原来的 java.lang.OutOfMemoryError: GC overhead limit exceeded 错误掩盖,变成更常见的 java.lang.OutOfMemoryError: Java heap space 错误消息。

需要注意: 有时候触发 GC overhead limit 错误的原因, 是因为分配给JVM的堆内存不足。这种情况下只需要增加堆内存大小即可。

在大多数情况下, 增加堆内存并不能解决问题。例如程序中存在内存泄漏, 增加堆内存只能推迟产生 java.lang.OutOfMemoryError: Java heap space 错误的时间。

当然, 增大堆内存, 还有可能会增加 GC pauses 的时间, 从而影响程序的 吞吐量或延迟

如果想从根本上解决问题, 则需要排查内存分配相关的代码. 简单来说, 需要回答以下问题:

  1. 哪类对象占用了最多内存?
  2. 这些对象是在哪部分代码中分配的。

要搞清这一点, 可能需要好几天时间。下面是大致的流程:

  • 获得在生产服务器上执行堆转储(heap dump)的权限。“转储”(Dump)是堆内存的快照, 可用于后续的内存分析. 这些快照中可能含有机密信息, 例如密码、信用卡账号等, 所以有时候, 由于企业的安全限制, 要获得生产环境的堆转储并不容易。
  • 在适当的时间执行堆转储。一般来说,内存分析需要比对多个堆转储文件, 假如获取的时机不对, 那就可能是一个“废”的快照. 另外, 每执行一次堆转储, 就会对JVM进行一次“冻结”, 所以生产环境中,不能执行太多的Dump操作,否则系统缓慢或者卡死,你的麻烦就大了。
  • 用另一台机器来加载Dump文件。如果出问题的JVM内存是8GB, 那么分析 Heap Dump 的机器内存一般需要大于 8GB. 然后打开转储分析软件(我们推荐Eclipse MAT , 当然你也可以使用其他工具)。
  • 检测快照中占用内存最大的 GC roots。详情请参考: Solving OutOfMemoryError (part 6) – Dump is not a waste。 这对新手来说可能有点困难, 但这也会加深你对堆内存结构以及 navigation 机制的理解。
  • 接下来, 找出可能会分配大量对象的代码. 如果对整个系统非常熟悉, 可能很快就能定位问题。运气不好的话,就只有加班加点来进行排查了。

打个广告, 我们推荐 Plumbr, the only Java monitoring solution with automatic root cause detection。 Plumbr 能捕获所有的 java.lang.OutOfMemoryError , 并找出其他的性能问题, 例如最消耗内存的数据结构等等。

Plumbr 在后台负责收集数据 —— 包括堆内存使用情况(只统计对象分布图, 不涉及实际数据),以及在堆转储中不容易发现的各种问题。 如果发生 java.lang.OutOfMemoryError , 还能在不停机的情况下, 做必要的数据处理. 下面是Plumbr 对一个 java.lang.OutOfMemoryError 的提醒:

Plumbr OutOfMemoryError incident alert

强大吧, 不需要其他工具和分析, 就能直接看到:

  • 哪类对象占用了最多的内存(此处是 271 个 com.example.map.impl.PartitionContainer 实例, 消耗了 173MB 内存, 而堆内存只有 248MB)
  • 这些对象在何处创建(大部分是在 MetricManagerImpl 类中,第304行处)
  • 当前是谁在引用这些对象(从 GC root 开始的完整引用链)

得知这些信息, 就可以定位到问题的根源, 例如是当地精简数据结构/模型, 只占用必要的内存即可。

当然, 根据内存分析的结果, 以及Plumbr生成的报告, 如果发现对象占用的内存很合理, 也不需要修改源代码的话, 那就增大堆内存吧。在这种情况下,修改JVM启动参数, (按比例)增加下面的值:

1
java -Xmx1024m com.yourcompany.YourClass`

这里配置了最大堆内存为 1GB。请根据实际情况修改这个值. 如果 JVM 还是会抛出 OutOfMemoryError, 那么你可能还需要查询手册, 或者借助工具再次进行分析和诊断。

转载自: https://blog.csdn.net/renfufei/article/details/76350794

每个Java程序都只能使用一定量的内存, 这种限制是由JVM的启动参数决定的。而更复杂的情况在于, Java程序的内存分为两部分: 堆内存(Heap space)和 永久代(Permanent Generation, 简称 Permgen):

01_01_java-heap-space.png

这两个区域的最大内存大小, 由JVM启动参数 -Xmx-XX:MaxPermSize 指定. 如果没有明确指定, 则根据平台类型(OS版本+ JVM版本)和物理内存的大小来确定。

假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发java.lang.OutOfMemoryError: Java heap space 错误。

不管机器上还没有空闲的物理内存, 只要堆内存使用量达到最大内存限制,就会抛出 java.lang.OutOfMemoryError: Java heap space 错误。

原因分析

产生 java.lang.OutOfMemoryError: Java heap space 错误的原因, 很多时候, 就类似于将 XXL 号的对象,往 S 号的 Java heap space 里面塞。其实清楚了原因, 就很容易解决对不对? 只要增加堆内存的大小, 程序就能正常运行. 另外还有一些比较复杂的情况, 主要是由代码问题导致的:

  • 超出预期的访问量/数据量。 应用系统设计时,一般是有 “容量” 定义的, 部署这么多机器, 用来处理一定量的数据/业务。 如果访问量突然飙升, 超过预期的阈值, 类似于时间坐标系中针尖形状的图谱, 那么在峰值所在的时间段, 程序很可能就会卡死、并触发 java.lang.OutOfMemoryError: Java heap space 错误。
  • 内存泄露(Memory leak). 这也是一种经常出现的情形。由于代码中的某些错误, 导致系统占用的内存越来越多. 如果某个方法/某段代码存在内存泄漏的, 每执行一次, 就会(有更多的垃圾对象)占用更多的内存. 随着运行时间的推移, 泄漏的对象耗光了堆中的所有内存, 那么 java.lang.OutOfMemoryError: Java heap space 错误就爆发了。

具体示例

一个非常简单的示例

以下代码非常简单, 程序试图分配容量为 2M 的 int 数组. 如果指定启动参数 -Xmx12m, 那么就会发生 java.lang.OutOfMemoryError: Java heap space 错误。而只要将参数稍微修改一下, 变成 -Xmx13m, 错误就不再发生。

1
2
3
4
5
6
public class OOM {
static final int SIZE=2*1024*1024;
public static void main(String[] a) {
int[] i = new int[SIZE];
}
}

内存泄漏示例

这个示例更真实一些。在Java中, 创建一个新对象时, 例如 Integer num = new Integer(5); , 并不需要手动分配内存。因为 JVM 自动封装并处理了内存分配. 在程序执行过程中, JVM 会在必要时检查内存中还有哪些对象仍在使用, 而不再使用的那些对象则会被丢弃, 并将其占用的内存回收和重用。这个过程称为 垃圾收集. JVM中负责垃圾回收的模块叫做 垃圾收集器(GC)

Java的自动内存管理依赖 GC, GC会一遍又一遍地扫描内存区域, 将不使用的对象删除. 简单来说, Java中的内存泄漏, 就是那些逻辑上不再使用的对象, 却没有被 垃圾收集程序 给干掉. 从而导致垃圾对象继续占用堆内存中, 逐渐堆积, 最后造成 java.lang.OutOfMemoryError: Java heap space 错误。

很容易写个BUG程序, 来模拟内存泄漏:

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
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。

请参考: Java中hashCode与equals方法的约定及重写原则

随着时间推移, “cached” 的对象会越来越多. 当泄漏的对象占满了所有的堆内存, GC 又清理不了, 就会抛出 java.lang.OutOfMemoryError:Java heap space 错误。

解决办法很简单, 在 Key 类中恰当地实现 equals() 方法即可:

1
2
3
4
5
6
7
8
@Override
public boolean equals(Object o) {
boolean response = false;
if (o instanceof Key) {
response = (((Key)o).id).equals(this.id);
}
return response;
}

说实话, 在寻找真正的内存泄漏原因时, 你可能会死掉很多很多的脑细胞。

一个SpringMVC中的场景

译者曾经碰到过这样一种场景:

为了轻易地兼容从 Struts2 迁移到 SpringMVC 的代码, 在 Controller 中直接获取 request.

所以在 ControllerBase 类中通过 ThreadLocal 缓存了当前线程所持有的 request 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 中:

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
/**
* 登录拦截器
*/
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();
}
}

postHandleafterCompletion 方法中, 清理 ThreadLocal 中的 request 对象。

但在实际使用过程中, 业务开发人员将一个很大的对象(如占用内存200MB左右的List)设置为 request 的 Attributes, 传递到 JSP 中。

JSP代码中可能发生了异常, 则SpringMVC的postHandleafterCompletion 方法不会被执行。

Tomcat 中的线程调度, 可能会一直调度不到那个抛出了异常的线程, 于是 ThreadLocal 一直 hold 住 request。 随着运行时间的推移,把可用内存占满, 一直在执行 Full GC, 系统直接卡死。

后续的修正: 通过 Filter, 在 finally 语句块中清理 ThreadLocal。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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 的时间, 从而影响程序的 吞吐量或延迟

要从根本上解决问题, 则需要排查分配内存的代码. 简单来说, 需要解决这些问题:

  1. 哪类对象占用了最多内存?
  2. 这些对象是在哪部分代码中分配的。

要搞清这一点, 可能需要好几天时间。下面是大致的流程:

  • 获得在生产服务器上执行堆转储(heap dump)的权限。“转储”(Dump)是堆内存的快照, 稍后可以用于内存分析. 这些快照中可能含有机密信息, 例如密码、信用卡账号等, 所以有时候, 由于企业的安全限制, 要获得生产环境的堆转储并不容易。
  • 在适当的时间执行堆转储。一般来说,内存分析需要比对多个堆转储文件, 假如获取的时机不对, 那就可能是一个“废”的快照. 另外, 每次执行堆转储, 都会对JVM进行“冻结”, 所以生产环境中,也不能执行太多的Dump操作,否则系统缓慢或者卡死,你的麻烦就大了。
  • 用另一台机器来加载Dump文件。一般来说, 如果出问题的JVM内存是8GB, 那么分析 Heap Dump 的机器内存需要大于 8GB. 打开转储分析软件(我们推荐Eclipse MAT , 当然你也可以使用其他工具)。
  • 检测快照中占用内存最大的 GC roots。详情请参考: Solving OutOfMemoryError (part 6) – Dump is not a waste。 这对新手来说可能有点困难, 但这也会加深你对堆内存结构以及navigation机制的理解。
  • 接下来, 找出可能会分配大量对象的代码. 如果对整个系统非常熟悉, 可能很快就能定位了。

打个广告, 我们推荐 Plumbr, the only Java monitoring solution with automatic root cause detection。 Plumbr 能捕获所有的 java.lang.OutOfMemoryError , 并找出其他的性能问题, 例如最消耗内存的数据结构等等。

Plumbr 在后台负责收集数据 —— 包括堆内存使用情况(只统计对象分布图, 不涉及实际数据),以及在堆转储中不容易发现的各种问题。 如果发生 java.lang.OutOfMemoryError , 还能在不停机的情况下, 做必要的数据处理. 下面是Plumbr 对一个 java.lang.OutOfMemoryError 的提醒:

01_02_outofmemoryerror-analyzed.png

强大吧, 不需要其他工具和分析, 就能直接看到:

  • 哪类对象占用了最多的内存(此处是 271 个 com.example.map.impl.PartitionContainer 实例, 消耗了 173MB 内存, 而堆内存只有 248MB)
  • 这些对象在何处创建(大部分是在 MetricManagerImpl 类中,第304行处)
  • 当前是谁在引用这些对象(从 GC root 开始的完整引用链)

得知这些信息, 就可以定位到问题的根源, 例如是当地精简数据结构/模型, 只占用必要的内存即可。

当然, 根据内存分析的结果, 以及Plumbr生成的报告, 如果发现对象占用的内存很合理, 也不需要修改源代码的话, 那就增大堆内存吧。在这种情况下,修改JVM启动参数, (按比例)增加下面的值:

1
-Xmx1024m

这里配置Java堆内存最大为 1024MB。可以使用 g/G 表示 GB, m/M 代表 MB, k/K 表示 KB.

下面的这些形式都是等价的, 设置Java堆的最大空间为 1GB:

1
2
3
4
5
# 等价形式: 最大1GB内存
java -Xmx1073741824 com.mycompany.MyClass
java -Xmx1048576k com.mycompany.MyClass
java -Xmx1024m com.mycompany.MyClass
java -Xmx1g com.mycompany.MyClass

图 | 刚毕业那年在车库的工作情景

翻出来刚毕业那会儿工作时被派到地下车库干活儿的照片,当时是在做无线 AP 的测试,由于写字楼无线网络干扰严重,公司又没有专业的隔离实验室,就只好在地下车库这种干扰少一些的地方做无线测试了。当时觉得把自己派到地下真是憋屈,还安慰自己说在车库里工作过的后来都牛逼了…

正式开工

受2019冠状病毒疫情影响,这周一才正式复工,之前两周其实是在家里远程办公,效率一般,复工后的效率要高一些。

听说杭州很多企业受疫情影响都过得不太好,裁员、降薪之类的公司有不少,我们也降薪了,全员降10%,可自愿多降一些,但公司不白降你薪,会给你换双倍价值的期权。期权这东西,我认为信则有,不信则无。有意思的现象是:由于不能聚集到一起开会,老板在钉钉上视频直播讲这次降薪的决定,底下的人在群里纷纷表示自己愿意自降20%、自降30%等等,像极了斗鱼上美女主播唱歌表演时粉丝们刷弹幕刷礼物的样子,只不过这个主播不说谢谢。

关于软件测试

国内技术型的创业公司,不建议设专职的软件测试岗位。

这是我这两年的感受,技术型的创业公司要求先活下来再活得好,专职的软件测试岗位在创业公司比较尴尬:干的外包的活儿,拿着正式员工的钱。自己觉着不爽,公司也觉着不划算。

关于裁员

今天听说一个公司元老级的人物合同到期了,这个时间点正是公司困难的时候,就没有再跟他续签合同了,这个应该不算是被裁掉,只是觉得有些心寒。在我的印象里,他原先是个交互设计师,后面由于公司发展需要还兼任了产品经理,加班出差之类的事情也没见抱怨过,属于任劳任怨而且有一定的能力能产出成绩,对公司特别忠诚的那种人(老板经常讲对公司要忠诚)。

不续签合同,我想他也很无奈吧,这个时间点除了几个大厂在招人,其他公司基本都只出不进了,他能顺利进入大厂么?或许他当初就是被老板画的大饼吸引了才从大厂出来加入这个创业公司的吧。

如何在公司里不被裁,我有几个感受:

  1. 一定要跟对一个好老板

    这里的「老板」不是指 CEO,而是你的某个上司,这个上司必须得在公司有一定的地位,一但有人要裁你,他有能力保住你;

  2. 做跟公司方向一致的事情

    公司的最终目标都是赚钱,要么帮公司赚钱,要么帮公司省钱。每个月的个人收入如果是10000元,那公司在你身上的支出可能是15000~20000,你一年能帮公司赚到 (15000~20000)*12这么多钱吗?公司都是按钱算账的;

  3. 高调做事

    工作上做出来的成果,要高调宣扬,给自己打广告,告诉老板们你又帮公司实现了什么。一年只需有那么两三次高调的事情让老板们听到,并且没有非常大的黑点,你在老板心里的地位基本上就稳了;

  4. 保持好心态

    工作,其实是个人与企业做的一次长达 N 年的交易,这 N 年期间,企业为个人提供薪资,个人为企业提供产品(代码、服务等等)。个人随时可能出现不合规范的交易(比如交不出好的产品,或者突然提离职),企业也随时可能出现不合规范的交易(比如单方面解除合同,延期发工资等),双方都有能力对对方造成损失。企业为此做了充足的准备,比如一些关键岗位必须设置 backup,防止关键人物突然离职造成的业务停滞,将意外造成的损失最小化;而个人往往没有对此做足够的准备,一方面可能被企业宣导的「大家庭」理念误导了。所以,认清「人与企业之间是单纯的利益关系,而不是亲戚朋友的关系」这个事实,保持好心态,与企业公平交易即可。

ToB 的企业往往会有 私有化交付 的需求,也就是将企业内部的应用打包部署到客户的服务器上使用。最近在交付过程中遇到 传包 的问题,由于客户方的服务器不是随便能传文件的,必须经过一级一级领导审批才行,而且过程中对传入服务器的文件大小还有限制,整个流程大致是:

image-20190726171454923

而我们当前的 jar 包又特别大,一个大的可能有几百兆,往客户服务器上传文件比较坎坷,客户也经常抱怨:不是说就改几行代码么,干嘛传这么大的包?

jar 包为什么这么大呢?解压出来一个 jar 包看看:

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
.
├── BOOT-INF
│   ├── classes
│   │   ├── application-dubbox-provider.xml
│   │   ├── application.properties
│   │   ├── application-root.xml
│   │   ├── com
│   │   │   └── dtwave
│   │   │   └── dsource
│   │   │   └── biz
│   │   │   ├── config
│   │   │   │   └── SecurityConfig.class
│   │   ├── dsource.properties
│   │   ├── git.properties
│   │   ├── logback
│   │   │   └── default.xml
│   │   └── logback.xml
│   └── lib
│   ├── activation-1.1.jar
│   ├── dsource-common-4.4.0-SNAPSHOT.jar
├── dsource-common-4.4.0-SNAPSHOT.jar
├── dsource-service-provider-d-4.4.1.jar
├── META-INF
│   ├── dubbo
│   │   └── com.alibaba.dubbo.rpc.Filter
│   ├── MANIFEST.MF
│   └── maven
│   └── com.dtwave.dsource
│   └── dsource-service-provider
│   ├── pom.properties
│   └── pom.xml
└── org
└── springframework
└── boot
└── loader
├── archive
│   ├── Archive.class

待续…

basic_logstash_pipeline

需求

公司里目前各个服务部署在不同的服务器上,每个服务有自己的日志文件,日志缺乏统一查看的地方,平时看日志得去 SSH 到服务器上去才能看,不方便排查问题:

image-20190724205836242

另外,有些服务器对不同岗位的人权限是分开的,比如测试服务器上,只有测试人员才能通过 SSH 登录到服务器,开发人员是没权限登录的,开发人员想查问题就得找测试人员去一个个要日志了,测试人员也得陪着开发去一个个找,大家都难受。

所以,就想着通过 Logstash 将日志汇聚到 ES 里,让大家通过同一个口子—— kibana 来查看服务日志。

image-20190725163751321

测试环境

  • CentOS 7.5
  • Java 1.8
  • Logstash 7.2.0
  • ES 7.2.0
  • Filebeat 7.2.0
  • Kibana 7.2.0

安装

直接从官网下载压缩包解压即可,参考官网

Filebeat

监控文件的变化以及读文件,是通过 Filebeat 实现的,下载好 Filebeat 后,修改 Filebeat 的配置文件:

1
2
3
4
5
6
7
8
filebeat.inputs:
- type: log
enabled: true
paths:
- /data/dubhe-master/logs/dubhe-master.info.log
- /data/dubhe-daemon/logs/dubhe-daemon e.info.log
output.logstash:
hosts: ["logstash服务器IP:5044"]

上述配置表明,Filebeat 将监控并读取两个日志文件,然后将日志内容传给 Logstash。

启动 Filebeat: nohup ./filebeat -e -c filebeat.yml -d "publish" >> filebeat.log 2>&1 &

Logstash pipeline

Logstash 是通过 pipeline 的方式来完成整个日志接收、处理和传送过程的,先创建一个空的 pipeline 文件 test.conf:

1
2
3
4
5
6
7
8
input {
}

filter {

}
output {
}

pipeline 中的 input 用于接收 Filebeat 传来的内容,Logstash 通过自带的 Beats input 插件来接收,在 input 中添加 beats :

1
2
3
input {
beats { port => 5044 }
}

filter 用于内容转换,暂时先放着不管。

output 为输出,这里将日志内容输出到 ES 中,在 output 中添加 ES 信息(ES 需提前安装好):

1
2
3
output {
elasticsearch { hosts => ["ES服务器IP:9200"] }
}

检查 pipeline 文件格式是否正确:

1
bin/logstash -f test.conf --config.test_and_exit

如果返回内容显示 Config Validation Result: OK. 则表名 pipeline 文件配置正确,否则需要检查 pipeline 文件内容。

启动 logstash:

1
nohup bin/logstash -f test.conf --config.reload.automatic >> logstash.log 2>&1 &

Kibana

打开 kibana,过滤查看字段 log.file.pathmessage 来查看日志内容:

image-20190725163443511

总结

通过简单的配置,练习了从 日志文件 -> Filebeat -> logstash -> ES -> Kibana 的过程,简单实现了最原始的需求:不同地方的日志从统一一个口子查看。

参考资料

ES 官方文档: https://www.elastic.co/guide/en/logstash/current/advanced-pipeline.html

(全文完)

图 | 2019年1月27日 杭州 梦想小镇


技术

  1. 测试金字塔

    测试金字塔,越靠近金字塔底层的测试越重要,也应该做得越多,但我们目前大部分还是倒过来的「冰淇淋」型,上层的测试反而更多。

  2. 产品和项目的区别

    可以理解为,产品是一个长远的目标,一个产品会有很大的需求池,而项目是为了实现这个长远目标而做的迭代,每次在需求池里取出一部分需求去实现。

  3. 密码学入门

    免费的密码学入门教程。

图 | 2019年1月15日 北京 某烤鸭饭店

有很多年没有正经吃过冰糖葫芦了,无籽,中间还有豆沙,很好吃~

资源

  1. The 2019 DevOps RoadMap

    DevOps 应该是 ToB 类企业里的程序员必备的技能,如果开发人员只专注于代码,而不关心产品的交付过程,感觉只有在大型的 ToB 类企业里才会活得舒服,最近半年深有感触!

  2. 2019,业绩就是尊严,其他都是扯淡!

    一篇鸡汤,今年很多公司年终奖都打了折扣,似乎大家都缺钱了,喝点鸡汤吧~

    挑了一段我喜欢的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    1、请你来是解决问题而不是制造问题。 

    2、如果你不能发现问题或解决不了问题,你本人就是一个问题。

    3、你能解决多大的问题,你就坐多高的位子。

    4、你能解决多少问题,你就能拿多少薪水。

    5、让解决问题的人高升,让制造问题的人让位,让抱怨问题的人下课。
  3. CES 2019: A Show Report

    CES:Consumer Electronics Show, 消费电子展。

    我当漫画的看~

  4. 技术团队效能动力模型如何搭建

    阿里大神的演说,学习下

  5. 内网穿透工具

    frpngrok,最近用 ngrok 比较多,省了 TeamViewer 的订阅费,frp 有时间也研究下~

技术

  1. Git 凭证

    前些天从 gitlab 上拉代码的时候,提示 remote: Not Found:

    1
    2
    3
    > git fetch origin
    remote: Not Found
    fatal: repository 'http://git.xxx.com/xxx.git/' not found

    字面意思是:远程仓库不存在。但是我对比了 URL, 远程仓库真的存在,而且在其他人电脑上是可以拉到代码的,我思来想去,应该跟我最近改了 gitlab 密码有关。

    Git 在使用 HTTP 协议时,每次与远程仓库交互,都需要输入用户名密码,很麻烦,于是 Git 便使用一套 凭证系统 来解决这个麻烦,凭证系统 有几种模式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    1. 默认所有都不缓存。 每一次连接都会询问你的用户名和密码。

    2. “cache” 模式会将凭证存放在内存中一段时间。 密码永远不会被存储在磁盘中,并且在15分钟后从内存中清除。

    3. “store” 模式会将凭证用明文的形式存放在磁盘中,并且永不过期。 这意味着除非你修改了你在 Git 服务器上的密码,否则你永远不需要再次输入你的凭证信息。 这种方式的缺点是你的密码是用明文的方式存放在你的 home 目录下。

    4. 如果你使用的是 Mac,Git 还有一种 “osxkeychain” 模式,它会将凭证缓存到你系统用户的钥匙串中。 这种方式将凭证存放在磁盘中,并且永不过期,但是是被加密的,这种加密方式与存放 HTTPS 凭证以及 Safari 的自动填写是相同的。

    5. 如果你使用的是 Windows,你可以安装一个叫做 “winstore” 的辅助工具。 这和上面说的 “osxkeychain” 十分类似,但是是使用 Windows Credential Store 来控制敏感信息。 可以在 https://gitcredentialstore.codeplex.com 下载。

    我用的 MacOS,所以使用了 osxkeychain 模式,当我在 gitlab 上的密码修改之后,系统 钥匙串 中的凭证是没有变的,所以我应该是认证失败了( git 的错误提示也很让人憔悴),删掉 钥匙串 之后再试一下,输入新的密码,这次 osxkeychain 里就有新的凭证了。

  2. NetCat 网络工具中的「瑞士军刀」

    最近在客户现场出差,生成环境的机器只能通过堡垒机访问,而且各个服务器直接的网络也限制得很严格,ssh_config 中限制了所有服务器都只允许堡垒机的 root 账户访问,想要在服务器直接传文件不能直接用 scp ,这个时候,nc 就能大显神威了!我常用的两个场景:

    1
    2
    3
    4
    5
    6
    1. 传文件
    [server] $nc -l 1567 > file.txt
    [client] $nc server_ip 1567 < file.txt

    2. 端口扫描
    $nc -z -v -n 172.31.100.7 21-25

图 | 2019年1月12日 北京 北海公园

作为一个北方人,见过结冰,但没见过河里结冰还能一群人上去玩的,这次开眼了~

北海公园,80块钱,不限时随意玩,可以:冰上碰碰车、亲子小板凳(我不知道该叫啥)、自己溜冰。

这次路过,下次有机会玩一下!

资源

  1. nullschool

    这个网站通过超级计算机的预告,可以在这个交互的动画地图上查看现在的风,天气,海洋和污染状况。每三个小时更新一次。

  2. SSH Examples, Tips & Tunnels

    有关 SSH 的各种用法示例,我们平时可能只是简单用一下 SSH,其实它还有很多能实现的功能,不一定用得上,但起码得知道,用的时候能想到。

生活知识

  1. 异地购车上牌的政策差异

    由于我在杭州工作,但户口还在老家,于是在杭州买了车要开回老家上牌,踩了个地区政策差异的坑,分享下:

    1.1 买车是要交「车辆购置税」的,在哪里上牌就在哪里交税;

    1.2 交税时,需要一个「车辆购置税申报表」,这张表,按地区不同,政策有所不同:要不要「表」?有些地区要,有些地区不要;「表」由谁提供?有些地区必须由4S店提供,有些地区可以到交税的地方领取;「表」的格式,有些地区要求必须有二维码,有些地区允许手写;

    1.3 到哪里交税?有些地区是去国税局交,有些是在车管所。

    上面的问题,需要提前问清楚,要有所准备,不要完全相信「官网」上的信息(几年不更新的)。另外,在我朝,花钱找黄牛,总是「靠谱」的。

  2. 超声心动图

    平时体检时,一般会作彩超,但没有做过心脏的彩超,超声心动图 可以检测心脏瓣膜区域功能、左右侧心脏不正常联系、瓣膜返流、以及心脏输出量的计算等。其他测量的参数包括心脏尺寸(管腔直径和室间隔厚度)和E / A比值。

    心脏作为人体的核心部件,必须好好照顾下,以后每年体检可以加上这一项。

  3. 如何跟压力做朋友

    「工作压力大」会导致各种疾病啥的,看人。用积极的态度对待压力,可能就没那么多毛病了。

技术

  1. docker tag的一次踩坑

    这次用 gitlab-ci + k8s 来打包,用了官方的 maven 镜像,一开始,ci 里直接用了 maven:latest ,前两天还能正常打包,后来就出问题了,一查才发现是 JDK 版本变了,maven:latest3-jdk-8 这个 tag 挪动到 3-jdk-11 了。

    所以,以后latest这个 tag 得慎重用,我觉得,有些保持向前兼容的,是可以一直用latest的。