拒绝纸上谈兵,从JVM原理到实例分析内存溢出 – Javaer必备技巧

内存溢出问题需要怎么分析?如果每天内存只泄露几十M,测试环境和本地开发环境根本难察觉到,但是最终的后果就是每隔几周生产环境就宕机一次。无脑的增加环境内存只是延长了宕机周期,从根本解决问题才能一劳永逸。

首先说下JAVA的对象回收机制 (内容引用《深入理解JAVA虚拟机》一书,结尾附下载地址)

JAVA的对象回收是根据可达性分析来判断对象是否存活,这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所 走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连 (用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如 图3-1所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达 的,所以它们将会被判定为是可回收的对象。

在Java语言中,可作为GC Roots的对象包括下面几种: 

虚拟机栈(栈帧中的本地变量表)中引用的对象。 

方法区中类静态属性引用的对象。 

方法区中常量引用的对象。 

本地方法栈中JNI(即一般说的Native方法)引用的对象。

根据这个基本原理,我们可以确定一点:内存泄露的原因是被GC Roots引用的对象一直在增加。 

内存泄露的DEMO(一个真实案例的简化版)

真实案例是这样:程序启动后每隔一个星期就会发生一次内存溢出,因为每次内存泄露很小,导致本地基本无法复现。最后分析完发现是一个第三方JAR中存在内存泄露的问题。

Demo代码代码如下: 

public class JvmOOM {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new BugObject();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public static class BugObject {
        private byte[] buff = new byte[50 * 1024 * 1024];
        public BugObject() {
            Runtime.getRuntime().addShutdownHook(new Thread(() -> BugObject.this.destroy()));
        }
        public void destroy() {
            System.out.println("destroy");
        }
    }
}

每隔1秒,新建一个BugObject对象,BugObject对象构造函数里会有个程序退出的钩子,为了加速内存泄露,BugObject里面放了个50M的字节数组。

我们运行Demo后,隔一会可以获取到下面的异常信息

OOM具体分析过程

我们整个分析过程只用了一个JDK默认的工具:jvisualvm,默认在JDK安装目录的bin目录下;JDK加入环境变量后,在终端直接运行jvisualvm就能打开。

选择需要分析的JAVA进程,双击后出现监控界面,因为DEMO中每隔1秒内存泄露50M,所以可以很明显的看到堆内存一直的增加;每次泄露很少的内存短时间内很难从这里看出来有内存泄露。

下面是定位问题的具体步骤:

第一步:执行垃圾回收后,堆Dump

第二步:隔10秒(这个时间一般需要根据内存泄露发生时间来设定,如果1个星期泄露一次,可以隔几个小时)重复第一步

第三步:重复第二步(一般是根据内存泄露爆发时长来,时间越长,就多重复几次第二步),这样可以减少干扰

第四步:执行完成上面几步后,我们就会有多个heapdump,选中一个heapdump,切换到类视图,选择“与另一个堆转储进行比较”。

然后根据类的大小排序,很快就可以发现byte[]占用的内存,在6秒内增加了200M,这时候基本可以判断就是有byte[]对象没有被回收,既然没有被回收肯定是被GC Roots引用了。

双击类名进入实例视图,并且展开对象引用关系

根据引用关系可以定位到最顶层的是被 ApplicationShutdownHooks 类的静态字段 hooks 字段引用。根据JAVA的对象回收机制的原理,明细可以知道这是个GC Root。

所以定位到DEMO代码中的这行代码有问题

Runtime.getRuntime().addShutdownHook(new Thread(() -> BugObject.this.destroy()));

这行代码会使BugObject对象被GC Root对象一直引用,导致对象无法被回收。 这里的分析步骤直接使用了jvisualvm 来获取堆dump,并不适合在线上环境处理。

生产环境的分析方式

线上可以使用jmap命令导出堆dump,下载到本地后再使用jvisualvm来分析

  1. 使用jps -l 获取到JAVA程序进程
  2. 通过 jmap -dump:format=b,file=[file] [pid] 导出dump
  1. 将服务器上生成的dump文件下载到本地,使用jvisualvm对比dump文件来分析问题

载入堆,再对比堆

和使用之前的方式分析结果一致

这里用到的命令行工具都是JDK自带的,一般无需额外安装。

关注公众号,输入“JAVA虚拟机”获取《深入理解JAVA虚拟机》一书的电子版。 

树莓派安装MYSQL记录

因为自己经常用多台电脑进行进行开发,开始每台开发的电脑都安装单独的数据库,后来发现个问题,有的项目如果测试数据不同,自己开发测试起来不是很方便,就打算在家里放一个树莓派,安装个MYSQL作为自己的测试数据库。

第一步:sudo apt-get update 更新软件包列表

第二步:执行sudo apt-get install mysql-server

得到如下信息:

没有可用的软件包 mysql-server,但是它被其它的软件包引用了。
这可能意味着这个缺失的软件包可能已被废弃,
或者只能在其他发布源中找到
然而下列软件包会取代它:
mariadb-server-10.0

意思就是软件源中没有mysql-server,可以用mariadb-server-10.0代替,我们知道mariadb-server-10.0是一个分支版本,凑合用是没什么问题的。

第三步:执行sudo apt-get install mariadb-server-10.0

安装完成,开始尝试了用 mysql -uroot -p 登录,但是出现下面错误:

ERROR 1698 (28000): Access denied for user ‘root’@’localhost’

加上sudo后登录mysql成功,sudo mysql -uroot

新增个root用户

grant all privileges on *.* to root@'%' identified by '';
flush privileges;

在自己电脑上用 DataGrip链接发现无法连上,使用 telnet ip 3306 提示:

telnet 192.168.1.5 3306
Trying 192.168.1.5…
telnet: connect to address 192.168.1.5: Connection refused
telnet: Unable to connect to remote host

猜测是MYSQL的bind-address配置有问题,修改配置/etc/mysql/mariadb.conf.d/50-server.cnf

bind-address		= 127.0.0.1
改成
bind-address		= 0.0.0.0

重启MYSQL,重新尝试DataGrip,搞定。

sudo systemctl restart mysql

MacOS 将树莓派SD卡生成img

第一步:

将树莓派的SD卡插入到电脑,先执行 diskutil list

可以确定树莓派的SD卡对应/dev/disk2

第二步

执行下面命令

sudo dd if=/dev/rdisk2 of=1.img bs=4m

这里有个坑,最好不要用 sudo dd if=/dev/disk2 of=1.img 这个命令,因为会慢到你吐血。对比下截图就知道了。

将img写入SD卡,可以使用balenaEtcher工具,也可以使用dd命令

pip安装模块,提示 ld: library not found for -lssl 问题解决方式

今天在电脑中安装airflow的mysql扩展时,安装失败,错误信息如下:

    205 warnings generated.
    clang -bundle -undefined dynamic_lookup -L/usr/local/opt/readline/lib -L/usr/local/opt/readline/lib -L/Users/farmer/.pyenv/versions/3.7.9/lib -L/usr/local/opt/readline/lib -L/usr/local/opt/readline/lib -L/Users/farmer/.pyenv/versions/3.7.9/lib build/temp.macosx-10.15-x86_64-3.7/_mysql.o -L/usr/local/Cellar/mysql/8.0.22_1/lib -lmysqlclient -lssl -lcrypto -lresolv -o build/lib.macosx-10.15-x86_64-3.7/_mysql.cpython-37m-darwin.so
    ld: library not found for -lssl
    clang: error: linker command failed with exit code 1 (use -v to see invocation)
    error: command 'clang' failed with exit status 1

大致意思就是编译时候没找到ssl依赖库。只需要在执行命令前指定下ssl库的目录

env LDFLAGS="-I/usr/local/opt/openssl/include -L/usr/local/opt/openssl/lib" pip install 'apache-airflow[mysql]'

重新安装,成功!

MAC RVM 安装 Ruby编译报错问题解决

之前在自己的老MAC上通过RVM安装Ruby报错,查看错误信息:mjit_compile.inc:3385:73: error: expected ‘)’ ; https://github.com/rvm/rvm/issues/4927 和这个issues描述的一样。 前几天在自己的新MAC上安装一切正常。想起来以前在/usr/local/include 目录下ln过MacOSX.sdk的头文件到该目录下,尝试删除这些链接过来的头文件。

ls -la | grep CommandLine | awk -F " " '{print $9}' | xargs rm

重新执行 :

rvm install "ruby-2.6.5" --with-openssl-dir=`brew --prefix openssl`

安装成功。

同时发现;Golang test 时候 Golang test error: macro expansion producing ‘defined’ has undefined behavior [-Werror,-Wexpansion-to-defined] 报错也消失。

Mac 安装 RVM

RVM 用于管理 Ruby 版本的工具,类似pyenv

官网: https://rvm.io/

根据官网的介绍,先执行:

gpg2 --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB

执行报错:zsh: command not found: gpg2

发现这个gpg2可以不同配置,直接执行下一步

\curl -sSL https://get.rvm.io | bash
source /Users/farmer/.rvm/scripts/rvm

这样rvm就安装好了。

rvm list # 查看当前系统已经安装的ruby版本
rvm list known # 查看可以安装的ruby版本

通过RVM安装ruby

rvm install 2.6.6

Golang test error: macro expansion producing ‘defined’ has undefined behavior [-Werror,-Wexpansion-to-defined]

今天用Go语言开发过程中,执行测试用例发现如下错误:

go test -v ./services/

# runtime/cgo
In file included from gcc_darwin_amd64.c:6:
/usr/local/include/pthread.h:331:6: error: macro expansion producing 'defined' has undefined behavior [-Werror,-Wexpansion-to-defined]
/usr/local/include/pthread.h:200:2: note: expanded from macro '_PTHREAD_SWIFT_IMPORTER_NULLABILITY_COMPAT'
/usr/local/include/pthread.h:331:6: error: macro expansion producing 'defined' has undefined behavior [-Werror,-Wexpansion-to-defined]
/usr/local/include/pthread.h:200:34: note: expanded from macro '_PTHREAD_SWIFT_IMPORTER_NULLABILITY_COMPAT'
/usr/local/include/pthread.h:540:6: error: macro expansion producing 'defined' has undefined behavior [-Werror,-Wexpansion-to-defined]
/usr/local/include/pthread.h:200:2: note: expanded from macro '_PTHREAD_SWIFT_IMPORTER_NULLABILITY_COMPAT'
/usr/local/include/pthread.h:540:6: error: macro expansion producing 'defined' has undefined behavior [-Werror,-Wexpansion-to-defined]
/usr/local/include/pthread.h:200:34: note: expanded from macro '_PTHREAD_SWIFT_IMPORTER_NULLABILITY_COMPAT'

https://github.com/golang/go/issues/38876 在这个上面找到了类似的问题

sudo rm -rf /Library/Developer/CommandLineTools
xcode-select --install

尝试里面说的 重新安装 xcode 命令行工具,并没有解决问题。

export CGO_CPPFLAGS="-Wno-error -Wno-nullability-completeness -Wno-expansion-to-defined -Wno-builtin-requires-header"

尝试修改CGO_CPPFLAGS变量后问题解决。

猜想可能是哪次 link 过一些头文件到 /usr/local/include/目录引起的。具体原因也不知道,不改再尝试了,可能把其它环境弄坏,暂时解决就好。

Python SSLError(“Can’t connect to HTTPS URL because the SSL module is not available.”,)

Mac 升级 openssl 后,Python可能就会出现这个错误;原因是升级openssl过程中导致库文件的链接路径发生变化

在Python 命令行输入

import ssl
Traceback (most recent call last):
   File "", line 1, in 
   File "/Users/farmer/.pyenv/versions/3.6.2/lib/python3.6/ssl.py", line 101, in 
     import _ssl             # if we can't import it, let the error propagate
 ImportError: dlopen(/Users/farmer/.pyenv/versions/3.6.2/lib/python3.6/lib-dynload/_ssl.cpython-36m-darwin.so, 2): Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib
   Referenced from: /Users/farmer/.pyenv/versions/3.6.2/lib/python3.6/lib-dynload/_ssl.cpython-36m-darwin.so
   Reason: image not found

可以发现是因为/usr/local/opt/openssl/lib/libssl.1.0.0.dylib无法加载导致的,修复这个问题只需要让Python能够正常加载到这个库,因此需要重新创建libssl.1.0.0.dylib链接。

cd /usr/local/opt/openssl/lib/


发现目录下并没有 libssl.1.0.0.dylib,只有 1.1 的库,如果已经安装了openssl1.0则直接link到这个目录,否则需要先安装openssl 1.0

我的电脑上已经安装了openssl1.0 并且也有 libssl.1.0.0.dylib库文件,只是目录在:/usr/local/opt/openssl@1.0/lib

因此只需要执行:

ln -s /usr/local/opt/openssl@1.0/lib/libssl.1.0.0.dylib /usr/local/opt/openssl/lib/libssl.1.0.0.dylib

import ssl
Traceback (most recent call last):
File “”, line 1, in
File “/Users/farmer/.pyenv/versions/3.6.2/lib/python3.6/ssl.py”, line 101, in
import _ssl # if we can’t import it, let the error propagate
ImportError: dlopen(/Users/farmer/.pyenv/versions/3.6.2/lib/python3.6/lib-dynload/_ssl.cpython-36m-darwin.so, 2): Library not loaded: /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib
Referenced from: /Users/farmer/.pyenv/versions/3.6.2/lib/python3.6/lib-dynload/_ssl.cpython-36m-darwin.so
Reason: image not found

重新执行import ssl 测试出现了另外一个错误,可以看出来还是相同问题,继续执行

ln -s /usr/local/opt/openssl@1.0/lib/libcrypto.1.0.0.dylib
 /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib

再次验证,解决。

PHP curl 设置请求头

正确做法

        $header = array(
            'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
            'Connection: keep-alive',
            'Upgrade-Insecure-Requests: 1'
        );

错误做法

        $header = array(
            'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-Language' => 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
            'Connection' => 'keep-alive',
            'Upgrade-Insecure-Requests' => '1'
        );

LNMP 升级与多版本PHP

今天在部署opencart3.6的时候提示:PH7.3+ Required,为了下次不用在去百度,特意把LNMP的升级与多版本PHP的步骤记录下来。

0、升级到指定的LNMP,执行下列指令将自动升级 lnmp

wget -c http://soft.vpser.net/lnmp/lnmp1.7.tar.gz &amp;&amp; tar zxf lnmp1.7.tar.gz &amp;&amp; cd lnmp1.7 &amp;&amp; ./upgrade1.x-1.7.sh

1、添加多个版本的PHP,输入指定的PHP版本后完成安装

./install.sh mphp

2、安装完成后可以在nginx的conf目录下看到 enable-phpX.X.conf

3、在需要其它版本的网址的nginx引入对应的 include enable-phpX.X.conf