Eric 发布的文章

关于 Java String 拼接

假设我有如下代码:

String result = "!";
for (int i = 0; i < 1000; i++) {
    result += "abc";
}
blackHole.consume(result);

可以看到里面有很多次字符串拼接, 那么这个 String 会被自动优化为 StringBuilder 类型, 使用 append(String) 方法. 具体实现是使用其父类 AbstractStringBuilder 的 append(String) 方法:

    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

这里的 str.getChars() 就是字符串复制的方法, 就是 String.getChars() 方法, 里面就这么一句重要的:

System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);

System.arraycopy 是 native 实现:

    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

这个 native 实现具体就在 openJDK 的 src/share/vm/prims/jvm.cpp 里面:

JVM_ENTRY(void, JVM_ArrayCopy(JNIEnv *env, jclass ignored, jobject src, jint src_pos,
                               jobject dst, jint dst_pos, jint length))
  JVMWrapper("JVM_ArrayCopy");
  // Check if we have null pointers
  if (src == NULL || dst == NULL) {
    THROW(vmSymbols::java_lang_NullPointerException());
  }
  arrayOop s = arrayOop(JNIHandles::resolve_non_null(src));
  arrayOop d = arrayOop(JNIHandles::resolve_non_null(dst));
  assert(s->is_oop(), "JVM_ArrayCopy: src not an oop");
  assert(d->is_oop(), "JVM_ArrayCopy: dst not an oop");
  // Do copy
  s->klass()->copy_array(s, src_pos, d, dst_pos, length, thread);
JVM_END

在具体下去, 就是在 src/share/vm/oops/objArrayKlass.cpp 里面的 copyarray 方法:

void ObjArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d,
            int dst_pos, int length, TRAPS) {...}

上面这个方法调用了 do_copy 方法, 里面会根据具体的类型去选择具体的 copy 方法. 如果我们只是上面例子的字母, 应该会选择到 src/share/vm/utilities/copy.hpp 里面的各种具体的 copy 方法:

// Assembly code for platforms that need it.
extern "C" {
  void _Copy_conjoint_words(HeapWord* from, HeapWord* to, size_t count);
  void _Copy_disjoint_words(HeapWord* from, HeapWord* to, size_t count);

  void _Copy_conjoint_words_atomic(HeapWord* from, HeapWord* to, size_t count);
  void _Copy_disjoint_words_atomic(HeapWord* from, HeapWord* to, size_t count);

  void _Copy_aligned_conjoint_words(HeapWord* from, HeapWord* to, size_t count);
  void _Copy_aligned_disjoint_words(HeapWord* from, HeapWord* to, size_t count);

  void _Copy_conjoint_bytes(void* from, void* to, size_t count);

  void _Copy_conjoint_bytes_atomic  (void*   from, void*   to, size_t count);
  void _Copy_conjoint_jshorts_atomic(jshort* from, jshort* to, size_t count);
  void _Copy_conjoint_jints_atomic  (jint*   from, jint*   to, size_t count);
  void _Copy_conjoint_jlongs_atomic (jlong*  from, jlong*  to, size_t count);
  void _Copy_conjoint_oops_atomic   (oop*    from, oop*    to, size_t count);

  void _Copy_arrayof_conjoint_bytes  (HeapWord* from, HeapWord* to, size_t count);
  void _Copy_arrayof_conjoint_jshorts(HeapWord* from, HeapWord* to, size_t count);
  void _Copy_arrayof_conjoint_jints  (HeapWord* from, HeapWord* to, size_t count);
  void _Copy_arrayof_conjoint_jlongs (HeapWord* from, HeapWord* to, size_t count);
  void _Copy_arrayof_conjoint_oops   (HeapWord* from, HeapWord* to, size_t count);
}

============== 从底向上看 ========
最终这些字符走的是(这里是 X86 架构,也有其它架构的) src/cpu/x86/vm/stubGenerator_x86_64.cpp 的 jshort_disjoint_arraycopy 方法, 那么这些 stub 是定义在:

StubRoutines::_jshort_disjoint_arraycopy

上面这个函数是定义在: src/share/vm/runtime/stubRoutines.hpp 文件里的.

为什么 CPU 不能到达100%

有时候为了让 CPU 贡献最大的能力, 我们会想尽办法让 CPU 在最高频上达到100%使用率, 可是无论怎么优化都达不到100%, 到底问题出在哪里?

  1. 各种 IO 拖慢了 CPU (waiting), 导致 CPU 在等取指令, 从网络到本地, 从磁盘加载到内存, 从内存加载到缓存, 从缓存到寄存器, 在内核态和用户态直接复制;
  2. 各种锁 线程/进程被 block;
  3. 线程数不够多;

Linux namespaces

namespace 表示一种对全局资源使用的隔离方式, 同一 namespace 下的进程能看到一样的资源, 不同的 namespace 不能看到其他 namespace 的变动.
ls -l /proc/<pid>/ns/
UTS IPC PID User Net Cgroup Mount Time

  1. UTS (Unix Time Sharing) namespace
    隔离跟主机名称相关的数据, 其内容包括: hostname, domain name, OS name, OS version, kernel 版本, 处理器信息等信息. 隔离之后使用 uname 这个系统调用就返回不同的值. 参看 uname 系统条用和 Linux uname 命令

    ~$ uname --help
    Usage: uname [OPTION]...
    Print certain system information.  With no OPTION, same as -s.
    
    -a, --all                print all information, in the following order,
    except omit -p and -i if unknown:
    -s, --kernel-name        print the kernel name
    -n, --nodename           print the network node hostname
    -r, --kernel-release     print the kernel release
    -v, --kernel-version     print the kernel version
    -m, --machine            print the machine hardware name
    -p, --processor          print the processor type (non-portable)
    -i, --hardware-platform  print the hardware platform (non-portable)
    -o, --operating-system   print the operating system
        --help     display this help and exit
        --version  output version information and exit
  2. IPC
    隔离跟 IPC 相关的资源: System V IPC objects & POSIX message queue. 每个 IPC namespace 都有自己的 System V IPC identifiers 和它自己的 POSIX message queue filesystem. 系统必须设置 CONFIG_IPC_NS选项.
  3. PID
    用来隔离 PID 数字的命名空间, 不同 PID 的进程号可以重复, 新的 PID namespace 的进程号从 1 开始.
  4. User
  5. Net
  6. Time

参考:

  1. https://www.kernel.org/doc/html/latest/admin-guide/namespaces/compatibility-list.html
  2. https://windsock.io/uts-namespace/
  3. https://www.informit.com/articles/article.aspx?p=23618&seqNum=16#:~:text=The%20uname%20system%20call%20fills,h%3E%20if%20you%20use%20uname.
  4. http://www.linfo.org/uname.html
  5. https://man7.org/linux/man-pages/man7/namespaces.7.html
  6. https://man7.org/linux/man-pages/man7/pid_namespaces.7.html

使用 async-profiler 具体实践步骤

async-profiler 是一个对 Java 应用影响很小的 profiler 工具, 不仅能 sample Java 栈, 还能获取 perf event 的数据.

常见的基本步骤:

  1. 下载最新 async-profiler, 并且复制到远程目标机器

    scp ~/async-profiler-1.7.1-linux-x64.tar.gz  user1@server.tianxiaohui.com:/home/user1
    rsync ~/async-profiler-1.7.1-linux-x64.tar.gz  user1@server.tianxiaohui.com:/home/user1
  2. 解压, 并给运行 Java 应用的用户执行权限

    mkdir /tmp/profiler
    tar -xvf -C /tmp/profiler/  async-profiler-1.7.1-linux-x64.tar.gz
    sudo chown -R app1:app /tmp/profiler/
    sudo chmod -R 755 /tmp/profiler
  3. 执行 profiler

    /tmp/profiler/profiler.sh -e itimer -d 60 -o svg  -f c.log.html 74211
    /tmp/profiler/profiler.sh -e lock  -d 60 -o svg  -f f.log.html --reverse  74211 
    /tmp/profiler/profiler.sh -e com.ebay.configuration.console.CalServlet.service  -d 60 -o svg  -f f.log.html --reverse  74211
    /tmp/profiler/profiler.sh -d 30 -e itimer -o svg -t --reverse -f t.log.html 74211
  4. 展示

    sudo nc -v -4 -l 7070 < f.log.html
    sudo nc -v -l -p 7070 < f.log.html

    http://server.tianxiaohui.com:7070/

https://github.com/jvm-profiling-tools/async-profiler

Apache HttpClient 连接池泄漏诊断思路

经常在线上看到一些应用直接因为连接池无法获得连接, 导致整个应用不在响应任何请求. 常见的有数据库连接池连接泄漏, Http 连接池泄漏. 对于这种连接泄漏的问题, 一般是应用没有考虑到某些特殊情况, 特殊异常的处理导致不能用完之后返回连接到连接池. 这里就针对 Apache HttpClient 连接池泄漏这种清楚, 分析一下基本的求解思路.

- 阅读剩余部分 -