探索堆中的秘密之OQL

3,638 阅读5分钟

前言

坦白的说,有点标题党,一股走进科学的感觉,其实就是与大家分享一下关于OQL的一些内容。

OQL是什么?

它是基于java堆上快照的对象查询语言,语法呢,和我们平常的SQL,HQL等等是近似的,毕竟QL一家亲。

根据它堆上查询的特性,所以自然是可以进行内存诊断,或者出一个你想要的报表等等用途。

笔者本人也比较喜欢使用OQL,毕竟能自己写代码,还是颇为灵活,灵活自然是惹人亲睐的因素。

第二个原因自然是:

今天从介绍,到实际例子,带给还不清楚“OQL”的盆友,多一种调试的思路。

正如《深入理解java虚拟机》所说:“没有什么工具是'秘密武器',拥有了就能‘包治百病’”。 所以多一项技能,也只是多一种调试的思路。

正文

如果你坚持看到了正文,相信你是了解heap dump是何物,

所以内存结构等概念在本文不过多介绍。

heap dump分析工具众多,从JConsole到Jhat,以及广为应用的MAT,它们自带的功能已经很强大了,还用自己写OQL吗?

这个好理解,看Report和写SQL,自然不是一个场景,前者是专注特定领域的结果,后者是灵活的开发语言。

先从简单例子开始,后面笔者会用《Effective Java》中那段经典的“可能存在内存泄漏”示例代码,展示怎样写出“有趣的OQL”。

简单的例子

第一步肯定是搞一个heap dump,既然是先举简单的例子,可以随便找一个heap dump入手。

Tip: 每个人的工具习惯不一样,笔者今天的操作,比如heap dump,读取hprof文件,以及执行OQL,都是在jVisualVm中进行的。

如果是有经验的盆友,当然可以跳过简单的例子,继续看下面的内容~

首先,先从左侧列表中的幸运进程中抽取一位观众,进行heap dump,看名字应该是笔者正在跑的IDEA中内部进程。

不出意外,会跳到heapDump的分析页面,其它功能暂且掠过,将左上角的下拉框选到今天的主题“OQL Console”。

查询所有“字符串实例”,可以算是OQL中的“hello world”了。 本文也不例外:

select 
  s
from 
  java.lang.String s

将以上OQL写到下面的文本框中

从上图看出,已经输出了所有String的实例,是不是和普通的DB可视化工具没什么区别?

输入完OQL运行,上面就显示了查询结果,顺着查询结果,可以找到该对象的值,引用等等信息。

这里还有一个小细节,其实OQL也是有方言一说,只不过是受限于运行工具不同,

比如在有些书籍上是以select * from java.lang.String作为第一个入门语句,

但是在笔者当前的visualVM2.0中运行则会报错,MAT下会正常输出。

虽然大体语法是一致的,但是细节上,还要以你习惯使用工具的官网介绍的语法为准。

Effective Java中的例子

如果你阅读过Effective Java,那么你一定记的有一段经典代码,用于演示存在内存泄漏风险的场景。

如果没有读过也没关系,这段代码很简单,笔者也粘贴了过来:

class Stack {
        private Object[] elements;
        private int size = 0;
        private static final int DEFAULT_INITIAL_CAPACITY = 16;

        public Stack() {
            elements = new Object[DEFAULT_INITIAL_CAPACITY];
        }

        public void push(Object e) {
            ensureCapacity();
            elements[size++] = e;
        }

        public Object pop() {
            if (size == 0) {
                throw new EmptyStackException();
            }
            return elements[--size];
        }

        /**
         * Ensure space for at least one more element, roughly
         * doubling the capacity each time the array needs to grow.
         */
        private void ensureCapacity() {
            if (elements.length == size) {
                elements = Arrays.copyOf(elements, 2 * size + 1);
            }
        }
    }

代码很简单,一个自己实现的栈,出栈操作pop忘记了消除elements数组中的引用,存在内存泄漏的风险。

那么笔者就用这段代码做演示,看看OQL都能做些什么。

笔者测试代码

public static void main(String[] args) throws Exception {
        // stack1
        Stack stack1 = new Stack();
        // stack1入栈30个元素
        addItem(stack1);
        // stack2
        Stack stack2 = new Stack();
        // stack2入栈30个元素
        addItem(stack2);
        // stack2出栈20元素
        for (int i = 0; i < 20; i++) {
            stack2.pop();
        }
        System.out.println("----Over----");
        // 通知full gc, 如果jvm心情不错,可以拿到dump
        System.gc();
        Thread.sleep(5000);
}

笔者的测试很简单,实例化了两个上文的栈,一个没有出栈操作,一个有出栈操作,有出栈操作的实例,自然保存了过期的引用。

为了拿到dump文件,我将启动参数增加了下面两条:

  • -XX:+HeapDumpBeforeFullGC
  • -XX:HeapDumpPath=/Users/vt/logs/jvm

即FullGC之前dump一下。

继续,一起看看可以用OQL做一些什么事情? 下面的内容为了省略篇幅,OQL和输出结果,用文字Input和Output表示。

Input:

select 
  s.elements.length
from instanceof 
  com.vt.example.LeakTest$Stack s

Output:

33
33

可以看出打印出来两个Stack的实例结果,与测试内容一致。 并且其中elements的长度都是被扩容到了33。

因为结果输出,支持json的格式,我们不妨再多查询一些内容。

Input:

 select 
  {
    "elements's length" :s.elements.length, 
    "instance": s, 
    "size attr" : s.size, 
    "count" : (actualCount = count(filter(s.elements, "it != null")))
  }
from instanceof 
  com.vt.example.LeakTest$Stack s

Output:

{
instance = com.vt.example.LeakTest$Stack#1,
count = 30,
size attr = 30,
elements's length = 33
}

{
instance = com.vt.example.LeakTest$Stack#2,
count = 30,
size attr = 10,
elements's length = 33
}

上面笔者的OQL语句分别查询了,实例地址,实例size属性,实例中数组容器的长度,以及真实的size大小

真实的size大小是统计了数组容器中不为null的元素。

也就是说,上文提到的stack问题是:出栈时没有把过期的元算置于null,导致了内存泄漏风险。

那么实际看看上面的OQL结果,是不是知道了什么?

没错,那就是第二个实例实际保存的元素,和size属性大小不符,自然是已经发生问题的实例。

但是看的还不是那么清晰,我们再优化一个版本看看如何:

Input:

select 
  {
    "实例中数组容器当前大小" :s.elements.length, 
    "实例地址": s, 
    "当前size属性" : s.size, 
    "实际保存的引用数量" : (actualCount = count(filter(s.elements, "it != null"))),
    "疑似泄漏" : (actualCount > s.size) ? "<font color='red'>有</font>" : "无" 
  }
from instanceof 
  com.vt.example.LeakTest$Stack s

Output:

最后这版本用了中文描述,并且还有文字高亮,是不是更清晰了一些?

用OQL直接查出我们想要的一个报表,依靠基本功能可不一定能做得到哦。

最后

看到这,大家应该对OQL有一个初步的了解和如何简单“玩转”了,本文也就结束了。

实际中,可不是区区上面的OQL就能查出来内存泄漏,内容是演示功能为主,带给不了解OQL的读者一套新的思路,可不是什么解决方案啊~