解决 Non-numeric value found - int expected 问题

在使用 btrace, 远程 debug 工具, JDK 自带小工具通过 agent 去连目标 Java 进程的时候, 有时候会遇到这个错误: Non-numeric value found - int expected. 我们明明给了一个目标进程的 int ID, 却报这个错误.

这里的原因是当时 client 使用的 Java 版本和目标进程的 Java 版本不一致造成的.

通过修改其中一个 Java 版本或者设置其中一个的 JAVA_HOME 变量, 使他们版本一致, 这个问题就解决了.

诊断由 modelmapper 导致的内存泄漏

发现一个 Java 应用 GC overhead 非常高, 查看 verbose GC log, 发现 heap 基本用完. 做了一个 heap dump, 发现其中一个 ClassLoader 管理着 327680 个类, 占用了 1.2G 的空间.

The classloader/component "org.springframework.boot.loader.LaunchedURLClassLoader @ 0x768800000" occupies 1,296,913,416 (77.87%) bytes. The memory is accumulated in one instance of "java.lang.Object[]" loaded by "<system class loader>".

仔细检查最新加入的这些类(一般Java 应用运行在稳定状态,很少有新的类被载入), 都有一些通用的模式:

  1. 类名类似于: com.tianxiaohui.MyClass$ByteBudddy$Ag2xax0, 前面的到 ByteBuddy 都是一样的, 后面全是类似 Hash 码的字符串, 看上去是一些代理子类;
  2. 类名类似于: org.modelmapper.internal.TypeMapImpl$Property@3f59d6c7, 都是基于 TypeMapImpl$Property的一些类.

通过以下 btrace 脚本, 在加入的 ClassLoader 的 classes 之前, 可以截获这些类, 并能查看到底哪个地方新加的这些类:

package test;

import static org.openjdk.btrace.core.BTraceUtils.println;

import org.openjdk.btrace.core.BTraceUtils;
import org.openjdk.btrace.core.BTraceUtils.Strings;
import org.openjdk.btrace.core.annotations.BTrace;
import org.openjdk.btrace.core.annotations.OnMethod;
import org.openjdk.btrace.core.annotations.ProbeClassName;
import org.openjdk.btrace.core.annotations.ProbeMethodName;

@BTrace
public class NewClassTracer {

    @OnMethod( clazz="/java\\.util\\.Vector/", method="/addElement/")
    public static void m(@ProbeClassName String probeClass, @ProbeMethodName String probeMethod, Object obj) {
            //print(Strings.strcat("entered ", probeClass));
            //println(Strings.strcat(".", probeMethod));
            println(Strings.strcat("new Class: ", Strings.str(obj)));
            BTraceUtils.jstack();
    }
}

通过上面的 btrace 脚本, 可以看到如下的 stacktrace:

java.util.Vector.addElement(Vector.java)
java.lang.ClassLoader.addClass(ClassLoader.java:263)
java.lang.ClassLoader.defineClass1(Native Method)
java.lang.ClassLoader.defineClass(ClassLoader.java:763)
sun.reflect.GeneratedMethodAccessor10.invoke(Unknown Source)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:498)
org.modelmapper.internal.bytebuddy.dynamic.loading.ClassInjector$UsingReflection$Dispatcher$Direct.defineClass(ClassInjector.java:604)
org.modelmapper.internal.bytebuddy.dynamic.loading.ClassInjector$UsingReflection.injectRaw(ClassInjector.java:235)
org.modelmapper.internal.bytebuddy.dynamic.loading.ClassInjector$AbstractBase.inject(ClassInjector.java:111)
org.modelmapper.internal.bytebuddy.dynamic.loading.ClassLoadingStrategy$Default$InjectionDispatcher.load(ClassLoadingStrategy.java:232)
org.modelmapper.internal.bytebuddy.dynamic.loading.ClassLoadingStrategy$Default.load(ClassLoadingStrategy.java:143)
org.modelmapper.internal.bytebuddy.dynamic.TypeResolutionStrategy$Passive.initialize(TypeResolutionStrategy.java:100)
org.modelmapper.internal.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:5623)
org.modelmapper.internal.ProxyFactory.proxyFor(ProxyFactory.java:97)
org.modelmapper.internal.ProxyFactory.proxyFor(ProxyFactory.java:72)
org.modelmapper.internal.ReferenceMapExpressionImpl.map(ReferenceMapExpressionImpl.java:67)
org.modelmapper.internal.ConfigurableConditionExpressionImpl.map(ConfigurableConditionExpressionImpl.java:65)

那么诊断下来, 就是每次使用下面的代码的时候, 就创建一些新的类 (例子代码中 Man 和 Person 都是一个只有 name 字段的 POJO):

People p = new People();
p.setName("eric");

//type 1
ModelMapper modelMapper = new ModelMapper();
Man man = modelMapper.map(p, Man.class);
System.out.println(man);

//type 2
ModelMapper modelMapper2 = new ModelMapper();
modelMapper2.getConfiguration().setAmbiguityIgnored(true);

TypeMap<People, Man> typeMap = modelMapper2.createTypeMap(People.class, Man.class);
typeMap.addMappings(mapper -> {
    mapper.map(source -> source.getName(), Man::setName);
});

System.out.println(modelMapper2.map(p, Man.class));

原因在于每个 ModelMapper 实例都会管理自己的 Model, 每次都会创建一些新的类. 所以官方站点上明确说明:

Unless you need different mappings between the same types, then it’s
best to re-use the same ModelMapper instance.

一些其他人遇到类似的问题: https://github.com/modelmapper/modelmapper/issues/375

所以, 最好是这些 ModelMapper 都是 static final 的, 保证尽最大可能重用, 否则就会出现内存溢出问题.

确诊线程池没有 shutdown 引起的内存泄漏

遇到好几次 Java 应用程序因为不正确的使用线程池, 没有 shutdown 导致内存泄漏, 最终不断 GC, 引起 CPU 100% 的问题. 这里举个例子来说明如何确认这种问题.

首先, 如果不是有活动线程数量的时序数据(active thread number trend metrics), 很难一开始就发现这种问题, 一般都是到最后引起 GC overhead 或者 CPU 100%的时候, 才开始查起. 当然, 如果有持续的活动线程数量统计信息, 那么一开始就能看到应用的线程数量不断的增加, 这就是问题的体现(正常的应用程序活动线程数能保持在一个稳定状态).

当我们通过 GC overhead 或者 CPU 100% 最终确定线程数量增加引起的内存泄漏的时候, 我们就能发现原来是某些线程不断的被创建, 却没有销毁.
当我们通过活动线程数持续增加判断出来之后, 我们可以通过查看线程的 thread dump (jcmd <pid> Thread.print > /tmp/thread.log) 来查看不断增加的线程.

当我们确定是线程不断增加的问题之后, 那么就需要确诊到底是哪里不断的添加新线程? 这里, 我们可以使用 btrace (btrace <pid> <script_file>) 来打出创建线程的 stacktrace.

btrace script

package com.ilikecopy.btrace;

import org.openjdk.btrace.core.annotations.*;
import static org.openjdk.btrace.core.BTraceUtils.*;

@BTrace 
public class Trace {
    
    @OnMethod( clazz="/java\\.util\\.concurrent\\.Executors/", method="/.*/" )
    public static void createThreadPool(@ProbeClassName String probeClass, @ProbeMethodName String probeMethod) {
        print(Strings.strcat("trace ... entered ", probeClass));
        println(Strings.strcat(".", probeMethod));
        println(jstackStr());
    }
    
    @OnMethod( clazz="/java\\.lang\\.Thread/", method="/init/" )
    public static void createThread(@ProbeClassName String probeClass, @ProbeMethodName String probeMethod) {
        print(Strings.strcat("trace ... entered ", probeClass));
        println(Strings.strcat(".", probeMethod));
        println(jstackStr());
    }
}

-------------------- 例子 --------------------
今天遇到的问题: 不断的看到线程在增加, 且线程名字的模式是: pool-xxx-thread-1. 这个很明显是使用只有一个线程的线程池, 不断的创建新的线程池, 忘记的 shutdown 导致的问题. 使用上面的脚步就很容易确认是哪里创建了这个线程池.

Java 8 Iterable 接口新增加的 forEach() 函数

公司的 web 应用基础框架几年前升级到 Java 8 之后, 很多开发人员很开心地使用起了很多 Java 8 提供的API新功能. 可是也遇到了不少问题. 其中一个便是使用非常广泛的 Iterable 的 forEach(Consumer<? super T> action) API.

在 Java 8 之前, 我们通常这样循环遍历一个可以 iterable 的对象:

    public void oldWay() {
        List<String> lines = Arrays.asList("test");
        
        Iterator<String> iterator = lines.iterator();
        while (iterator.hasNext()) {
            //do something
            System.out.println(iterator.next());
        }
        
        for (String line : lines) {
            //do something
            System.out.println(line);
        }
    }

使用 Java 8 的 forEach() API 我们可以这么遍历对象

    public void newWay() {
        List<String> lines = Arrays.asList("test");
        
        lines.forEach(new Consumer<String>() {
            @Override
            public void accept(String line) {
                //do something
                System.out.println(line);
            }
        });
        
        // or use Lambda
        lines.forEach(line -> {
            //do something
            System.out.println(line);
        });
        
        // or just println
        lines.forEach(System.out::println);
    }

那么这个新的 API 到底有什么好处呢?

  1. 这个简洁的语法, 深受一些熟悉 JavaScript/Node.js 或者 其它脚本语言的开发者欢迎, 看上去简洁;
  2. 能够使用一些并行流的 API, 省去了自己维护 ExecutorService 的开销;

不过从实际应用来看, 遇到的问题更多.

  1. 无法抛出 checked exception, 从 Consumer 的 accept 方法可以看到, 它没有声明任何异常, 所以一旦里面执行的代码中需要跑出 checked exception, 直接无法使用这个 API;
  2. 无法中途退出. 如果我们执行一个10000次的循环, 如果其中某次出错, 我们不想执行后面的, 可是 forEach 无法做到; 无法像一般的 loop 一样使用 continue, return 等关键字达到的效果.
  3. forEach 里面必须使用外层传入的 final 变量. 那么一旦是 final, 内层则无法直接赋值. 除非使用 container 类(如 一个对象的属性, map, list 等), 可以把值传出.
    public void newWayMustFinal () {
        List<String> lines = Arrays.asList("test");
        
        final Object finalTmp = "test";
        Object tmp = null;
        lines.forEach(line -> {
            //do something
            if (finalTmp.equals(line)) {
                System.out.println(line);
                
                //tmp = line; // can not do this, as it is not final
            }
        });
    }
  1. 对于一般的 for loop, JVM 是可以进行优化的, 比如 for 循环有10000次, JVM 可以每次做100次, 这些 CPU 在某些指令( fetch, load, save) 等操作上可以实现流水线; forEach 暂时无法使用这些优化;
  2. 可读性差. 除了读 lambda 表达式感觉不直接, 另外读 stack 也不直接, 比如下面的 stack:
java.lang.Exception: Stack trace
    at java.lang.Thread.dumpStack(Thread.java:1336)
    at com.ilikecopy.basic.IterableForEach.lambda$0(IterableForEach.java:46)
    at java.util.Arrays$ArrayList.forEach(Arrays.java:3880)
    at com.ilikecopy.basic.IterableForEach.newWay(IterableForEach.java:43)
    at com.ilikecopy.basic.IterableForEach.main(IterableForEach.java:12)

修复 vnc viewer 不记住密码

最近升级 Mac 到 Catalina 之后, VNC viewer 总是不在记住密码了, 每次都要输入 VNC account 的密码.

如何修复:

  1. 删除之前记住的密码文件
 rm ~/.vnc/config.d/vncviewer.d/passwords.json

如果是 windows , 请删除这个文件夹下的 passwords.json

C:\Users\<user>\AppData\Local\RealVNC\vncviewer.d
  1. 在 preferences 里面这个选项是勾选的
    vnc.png
  2. 重新登录, 记住密码, 下次就 ok 了.