Buffer
是java NIO
操作的基础,Java NIO的Buffer
用于与Channel
进行交互,数据是从Channel
读入Buffer
(缓冲区),从Buffer
写入到Channel
中的。从本质上来说,NIO主要是将数据从Buffer
中移入和移出。
DMA控制器
直接复制到进程缓存区,这样岂不是更好?这样会有两个问题
DMA控制器
通常不能直接与运行JVM进程的用户空间
通信DMA控制器
这种面向块的设备,与固定大小的数据块一起工作,JVM进程
可能请求的数据大小不是块大小的倍数或者未对齐JVM进程
与DMA控制器
之间切换时,操作系统会撕裂和重新组合数据Buffer
从两方面提高IO的效率
Buffer
对象,并提供一组方法用来方便的访问这块内存Buffer
读写数据一般遵循以下四个步骤
Buffer
flip()
,切换读写模式Buffer
中读取数据clear()
方法或者compact()
Buffer
的一些特征
Buffer
类不是线程安全的,避免多线程同时读写同一个Buffer
Buffer
都是可读的,但是并非所有的都可写,可通过isReadOnly()
判断,对只读Buffer
的修改会导致ReadOnlyBufferException
Buffer
是双向的capacity(容量)
Buffer
能容纳的数据元素的最大数量(不能为负值),在缓冲区创建时指定,并且永远不能更改。capacity(容量)
指的是具体实现类中写入的数据对象的数量,比如实现类IntBuffer
初始d额capacity是10,则最多可以写入10个int类型的数据。position(位置)
Buffer
中时,position
表示当前的位置,当一个数据被写入到Buffer
后,position
移动到下一个可插入数据的Buffer
单元。position
初始值为0,最大值为capacity-1
Buffer
从写模式切换到读模式,position
会被重置为0,当从Buffer
的position
处读取数据时,position
向前移动到下一个可读的位置limit(界限)
Buffer
位置索引,位于limit
后的数据既不能读也不能写Buffer
的limit
表示你最多能向Buffer
写入多少数据,写模式下,limit
等于capacity
limit
表示你最多能从Buffer
读取多少数据,因此,当Buffer
从写模式切换到读模式下,此时limit
被设置为写模式下的position
。即能读取到之前写入的所有数据mark(标记)
mark()
来设定mark=position
,调用reset()
设定postion=mark
。作用就是临时保存position
的值,当需要恢复时可以通过reset()
恢复Buffer
属性的关系
0 <= mark <= position <= limit <= capacity
ByteBuffer
Buffer
是一个抽象类,它具有7个直接抽象子类(即缓冲区中存储的数据类型并不像I/O流只能存储byte或char数据类型),继承关系图如下,从下图可以看出没有BooleanBuffer
这个子类。java.lang.StringBuffer
是在lang包下,在nio包下没有提供java.nio.StringBuffer缓冲区,在NIO中存储字符的缓冲区使用CharBuffer
类
Buffer
创建
allocate()或者allocateDirect()
//分配一个captacity为48字节的HeapByteBuffer
ByteBuffer byBuffer = ByteBuffer.allocate(48);
//分配一个captacity为48字节的DirectByteBuffer
ByteBuffer byBufferDirect = ByteBuffer.allocateDirect(48);
//分配一个可存储1024个字符的CharBuffer
CharBuffer charBuffer = CharBuffer.allocate(1024);
wrap()
:包装已有数组,wrap()
类似于静态工厂方法
@Test
public void testWrapArray() {
int[] bytes = new int[]{1, 2, 3, 4};
//包装一个已有的数组
IntBuffer ib = IntBuffer.wrap(bytes);
/**
* capacity:4,limit:4,position:0
* 通过wrap创建的Buffer,容量与数组的length一样
*/
logger.info("capacity:{},limit:{},position:{}", ib.capacity(), ib.limit(), ib.position());
for (int i = 0; i < bytes.length; i++) {
//依次输出1234
logger.info("{}", ib.get(i));
}
for (int i = 0; i < bytes.length; i++) {
if (i == 0) {
//改变数组,同时会改变缓冲区中的值
bytes[i] = 5;
}
//5234
logger.info("{}", ib.get(i));
}
int[] intArray = new int[]{1, 2, 3, 4, 5};
/**
* wrap(array,offset,length)
* 1. 创建一个capacity等于inArray.length的buffer
* 2. 并且position等于offset,此处是1
* 3. limit为length,此时是2
*/
IntBuffer intBuffer = IntBuffer.wrap(intArray, 1, 2);
//capacity:5,limit:3,position:1
logger.info("capacity:{},limit:{},position:{}",
intBuffer.capacity(),
intBuffer.limit(),
intBuffer.position());
while (intBuffer.hasRemaining()) {
//23
logger.info("{}",intBuffer.get());
}
}
map()
:内存映射方式
String filePath = BufferTest.class.getClassLoader().getResource("book.txt").getFile();
File file = new File(filePath);
FileChannel fc = new RandomAccessFile(filePath, "rw").getChannel();
MappedByteBuffer out = fc.map(FileChannel.MapMode.READ_WRITE, 0, file.length());
向Buffer
中写入数据
从Channel
中写入Buffer
//只是演示,所以不做异常处理
@Test
public void testBuffer() throws IOException {
String filePath = BufferTest.class.getClassLoader().getResource("book.txt").getFile();
RandomAccessFile accessFile = new RandomAccessFile(filePath, "rw");
FileChannel channel = accessFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
/**
* 将FileChannel通道中的数据读取到Buffer中
* 返回值表示有多少字节被读到Buffer中,返回-1表示读到文件末尾
*/
int readByte = channel.read(buf);
while (readByte != -1) {
//将写模式切换到读模式
buf.flip();
//hasRemaining 判断是否已经达到缓冲区的limit
while (buf.hasRemaining()) {
//remaining从position到limit还剩余的元素个数
int count = buf.remaining();
System.out.println("----" + count + "------");
System.out.println(new String(new byte[]{buf.get()}, "utf-8"));
}
//读完清空缓冲区,让缓冲区继续可写
buf.clear();
readByte = channel.read(buf);
}
accessFile.close();
}
通过Buffer的put方法
@Test
public void testWriteBuffer() {
CharBuffer charBuffer = CharBuffer.allocate(10);
charBuffer.put('a');
charBuffer.put('b');
charBuffer.put('c');
charBuffer.flip();
while (charBuffer.hasRemaining()) {
//abc
System.out.print(charBuffer.get());
}
}
从Buffer
中读取数据
从Buffer
读取数据到Channel
int bytesWritten = inChannel.write(buf);
使用get()
System.out.print(charBuffer.get());
flip()
:翻转,可以理解为模式切换(比如写模式切换到读模式)
JDK中Buffer抽象类
,可以byteBuffer.limit(byteBuffer.position()).position(0);
即limit
指明了Buffer
有效内容的末端,将limit
设置为当前位置,position
设置为0
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
案例
@Test
public void testFlip() {
IntBuffer intBuffer = IntBuffer.allocate(20);
//position:0,limit:20,capacity:20
logger.info("position:{},limit:{},capacity:{}",
intBuffer.position(),
intBuffer.limit(),
intBuffer.capacity());
Stream.of(1, 2, 3, 4, 5).forEach(intBuffer::put);
//position:5,limit:20,capacity:20
logger.info("position:{},limit:{},capacity:{}",
intBuffer.position(),
intBuffer.limit(),
intBuffer.capacity());
intBuffer.flip();
//position:0,limit:5,capacity:20
logger.info("position:{},limit:{},capacity:{}",
intBuffer.position(),
intBuffer.limit(),
intBuffer.capacity());
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
//将读模式切换为写模式
intBuffer.clear();
//position:0,limit:20,capacity:20
logger.info("position:{},limit:{},capacity:{}",
intBuffer.position(),
intBuffer.limit(),
intBuffer.capacity());
}
rewind()
:倒带(重新播放),已经读完的数据如果需要再读一遍,可以调用此方法。rewind
与flip
类似,但是rewind
不影响limit
(表示仍然能读多少元素),它只是将position设置为0,mark标记被清理
1. rewind源码
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
2. 测试rewind
@Test
public void testRewind() {
IntBuffer intBuffer = IntBuffer.allocate(20);
//写入数据
Stream.of(1, 2, 3, 4, 5).forEach(intBuffer::put);
//切换到读模式
intBuffer.flip();
//position:0,limit:5,capacity:20
logger.info("position:{},limit:{},capacity:{}",
intBuffer.position(),
intBuffer.limit(),
intBuffer.capacity());
while (intBuffer.hasRemaining()) {
System.out.print(intBuffer.get());
}
System.out.println();
//position:5,limit:5,capacity:20
logger.info("position:{},limit:{},capacity:{}",
intBuffer.position(),
intBuffer.limit(),
intBuffer.capacity());
intBuffer.rewind();
//position:0,limit:5,capacity:20
logger.info("position:{},limit:{},capacity:{}",
intBuffer.position(),
intBuffer.limit(),
intBuffer.capacity());
while (intBuffer.hasRemaining()) {
System.out.print(intBuffer.get());
}
}
clear()
: 一旦缓冲区完成填充并释放,它就可以被重新使用了,clear()
函数将缓冲区重置为空状态,即切换到写模式。它不改变缓冲区中的任何元素(不清空数据),而是仅仅将limit
设为capacity
,并把position
设置为0
public class BufferClearTest {
// 释放缓冲区
public static void drainBuffer(CharBuffer buffer) {
// hasRemaining会在释放缓冲区时告诉您是否已经达到缓冲区的limit
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
System.out.println("=====释放缓冲区结束=====");
}
// 释放缓冲区 第二种方式,此方法比较高效,因为limit不会在每次循环重复时都被检查
public static void drain2Buffer(CharBuffer buffer) {
// remaining()将返回从当前position到limit还剩余的元素数目。
int count = buffer.remaining();
for (int i = 0; i < count; i++) {
System.out.println(buffer.get());
}
}
// 填充缓冲区
private static void fillBuffer(CharBuffer charBuffer) {
String str = "this is buffer data";
for (int i = 0; i < str.length(); i++) {
charBuffer.put(str.charAt(i));
}
System.out.println("=====填充缓冲区结束=====");
}
public static void main(String[] args) {
CharBuffer buffer = CharBuffer.allocate(100);
// 写入数据
fillBuffer(buffer);
// 翻转(写模式转读模式)
buffer.flip();
// 读取数据
drainBuffer(buffer);
// 重置缓冲区,让缓冲区可以写入
buffer.clear();
//此时缓冲区中还是有内容的
System.out.println("clear缓冲区后,缓冲区中的内容是" + buffer.get());
}
}
compact()
:有时只想从缓冲区中释放一部分数据,而不是全部,然后重新填充,为了实现这一点,未读的数据元素需要下移以使第一个元素索引为 0,尽管重复这样做效率很低,但有时非常必要,API有一个compact()
方法,此方法在复制数据的时候要比使用get()
和put()
函数高效的多.(读取一部分Buffer
,将剩下的部分整体移动到Buffer
的头部)
mark()
:使Buffer
能够记住一个位置并在之后将其返回。缓冲区的标记在mark()
函数被调用之前都是未定义的,调用时标记被设为当前位置的值。reset()
将位置设为当前的标记值。如果标记值未定义,调用reset()
将导致InvalidMarkException异常
。一些Buffer
函数会抛弃已经设定的标记(rewind()
clear
flip
总是抛弃标记).如果新设置的值比当前的标记小,调用limit()
或position()
带有索引参数的版本会抛弃标记。
equals()
:所有的缓冲区都提供了equals来测试两个缓冲区是否相等。如果每个缓冲区中剩余的内容相同,则equals返回true,否则返回false。两个缓冲区被认为相等的充要条件
Buffer
的容量不需要相同,缓冲区剩余数据的索引也不必相同,但是每个缓冲区中剩余元素的数目(从position
到limit
)必须相同get()
函数返回的剩余数据元素序列必须一致duplicate()与Slice()
:复制缓冲区,两个缓冲区对象实际上指向了同一个内部数组,但分别管理各自的属性。slice()
方法获取的是原ByteBuffer的position-limit之间的内容,和原内容相互影响,原内容的position和limit不受影响。duplicate()
方法获取的是原ByteBuffer所有的内容,包括原ByteBuffer的mark,position,limit,capacity值,和原内容相互影响,源内容的position和limit不受影响
public class BufferDuplicateTest {
private static final Logger logger = LoggerFactory.getLogger(BufferDuplicateTest.class);
@Test
public void testDuplicate() {
CharBuffer buffer = CharBuffer.allocate(10);
buffer.put("abcde");
CharBuffer buffer1 = buffer.duplicate();
buffer1.clear();
buffer1.put("efghijk");
//position=5, limit=10, capacity=10,content=efghijk...
showBuffer(buffer);
//position=7, limit=10, capacity=10,content=efghijk...
showBuffer(buffer1);
}
@Test
public void testSlice() {
CharBuffer buffer = CharBuffer.allocate(10);
buffer.put("abcde");
CharBuffer buffer1 = buffer.slice();
buffer1.clear();
//通过slice创建的新缓冲区只能操作原始缓冲区中数组剩余的数据
//即索引为调用slice方法时原始缓冲区的position到limit索引之间的数据,
// 超出这个范围的数据通过slice创建的新缓冲区无法操作到。
buffer1.put("efghi");
//position=5, limit=10, capacity=10,content=abcdeefghi
showBuffer(buffer);
//position=5, limit=5, capacity=5,content=efghi
showBuffer(buffer1);
}
private static void showBuffer(CharBuffer buffer) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < buffer.limit(); i++) {
char c = buffer.get(i);
if (c == 0) {
c = '.';
}
sb.append(c);
}
System.out.printf("position=%d, limit=%d, capacity=%d,content=%s\n",
buffer.position(), buffer.limit(), buffer.capacity(), sb.toString());
}
}
分散(scatter)
从Channel
中读取:在读操作时将读取的数据写入多个Buffer
中聚集(gather)
写入Channel
:在写操作时将多个Buffer
的数据写入同一个Channel
中JVM进程
在单个系统调用中向操作系统
传递一个缓冲区地址列表,可以使数据汇编/反汇编任务更加高效。然后,操作系统按顺序填充或排出(fills or drains)
,在读取操作期间将数据分散(scattering)
到多个缓存区,或者写入操作期间从多个缓冲区收集(gathering)
数据。这种分散(scatter)/收集(gather)
活动减少了JVM进程
必须进行的(可能昂贵的)系统调用的数量,并允许操作系统优化数据处理,因为它知道缓冲区空间的总量。此外当多个处理器或核可用时,操作系统可以允许缓冲区同时填充(filled)
或排出(drained)
术语
虚拟内存与物理内存
什么是虚拟内存地址和物理内存地址?
假设你的计算机是32位,那么它的地址总线是32位的,也就是它可以寻址00xFFFFFFFF(4G)的地址空间,但如果你的计算机只有256M的物理内存0x0x0FFFFFFF(256M),同时你的进程产生了一个不在这256M地址空间中的地址,那么计算机该如何处理呢?回答这个问题前,先说明计算机的内存分页机制。
计算机会对虚拟内存地址空间(32位为4G)进行分页产生页(page),对物理内存地址空间(假设256M)进行分页产生页帧(page frame),页和页帧的大小一样,所以虚拟内存页的个数势必要大于物理内存页帧的个数。在计算机上有一个页表(page table),就是映射虚拟内存页到物理内存页的,更确切的说是页号到页帧号的映射,而且是一对一的映射。问题来了,虚拟内存页的个数 > 物理内存页帧的个数,岂不是有些虚拟内存页的地址永远没有对应的物理内存地址空间?不是的,操作系统是这样处理的。操作系统有个页面失效(page fault 也叫页面中断)功能。操作系统找到一个最少使用的页帧,使之失效,并把它写入磁盘,随后把需要访问的页放到页帧中,并修改页表中的映射,保证了所有的页都会被调度。
现在来看看什么是虚拟内存地址和物理内存地址:
虚拟内存地址:由页号(与页表中的页号关联)和偏移量(页的小大,即这个页能存多少数据)组成。
举个例子,有一个虚拟地址它的页号是4,偏移量是20,那么他的寻址过程是这样的:首先到页表中找到页号4对应的页帧号(比如为8),如果页不在内存中,则用失效机制调入页,接着把页帧号和偏移量传给MMC组成一个物理上真正存在的地址,最后就是访问物理内存的数据了。
MappedByteBuffer
将文件直接映射到内存(这里的内存指的是虚拟内存,并不是物理内存)
FileChannel
提供了map()方法把文件映射到虚拟内存,通常情况可以映射整个文件,如果文件比较大,可以进行分段映射。当通过map()方法建立映射关系之后,就不依赖于用于创建映射的文件通道(Channel)。 特别是,关闭通道(Channel)对映射的有效性没有影响(map方法的文档说明: A mapping, once established, is not dependent upon the file channel that was used to create it. Closing the channel, in particular, has no effect upon the validity of the mapping. )。即映射之后MappedByteBuffer
访问的是一块内存,什么时候从将内存的修改同步到磁盘上是不确定的
mmap出来的MappedByteBuffer
会作为page cache的一部分,MappedByteBuffer
本质是一个抽象类,map()方法返回的是一个DirectByteBuffer
实例
MapMode mode
:内存映像文件访问的方式:
MapMode.READ_ONLY
:只读,试图修改得到的缓冲区将导致抛出ReadOnlyBufferException
异常。MapMode.READ_WRITE
:读/写,对得到的缓冲区的更改最终将写入文件;但该更改对映射到同一文件的其他程序不一定是可见的。MapMode.PRIVATE
:私用,可读可写,所做的任何修改都会产生一个私有的数据副本并且只有当前MappedByteBuffer
实例才能看到,不会对底层文件作任何修改,这种能力称之为copy on write(写时复制)
。FileChannel没有提供公开的unmap()方法(私有)释放内存,如果想要释放内存需要如下方式:
// 第一种方式: 在关闭资源时执行以下代码释放内存
Method m = FileChannelImpl.class.getDeclaredMethod("unmap", MappedByteBuffer.class);
m.setAccessible(true);
m.invoke(FileChannelImpl.class, buffer);
//第二种方式:让MappedByteBuffer自己释放本身持有的内存
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
try {
Method getCleanerMethod = buffer.getClass().getMethod("cleaner", new Class[0]);
getCleanerMethod.setAccessible(true);
sun.misc.Cleaner cleaner = (sun.misc.Cleaner)
getCleanerMethod.invoke(byteBuffer, new Object[0]);
cleaner.clean();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
});
两种方式的本质都是调用
private static void unmap(MappedByteBuffer bb) {
Cleaner cl = ((DirectBuffer)bb).cleaner();
if (cl != null)
cl.clean();
}
DirectByteBuffer
是Java用于实现堆外内存的一个重要类,我们可以通过该类实现堆外内存的创建、使用和销毁
DirectByteBuffer
的父类Buffer
有个address
属性。address
只会被直接缓存给使用到。之所以将address
属性升级放在Buffer
中,是为了在JNI调用GetDirectBufferAddress
时提升它调用的速率,
address
表示分配的堆外内存的地址。java通过Unsafe类
的native allocateMemory()
本地方法创建直接缓冲区,此方法会返回堆外内存基地址(long)
,并赋值给address
//ByteBuffer类
// Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
new DirectByteBuffer(capacity)通过 Unsafe类的本地方法 public native long allocateMemory(long bytes)创建直接内存缓冲区
直接内存不属于GC管辖范围,DirectByteBuffer
属于java类,适当的时候会被GC回收,当它回收前会调用native方法
把直接内存释放,即本地内存可以随DirectByteBuffer
对象被GC回收而自动被操作系统回收。但是如果不断分配本地内存,堆内存很少使用,此时JVM并不会执行GC,此时DirectByteBuffer
对象就不会被GC回收,此时会出现堆内存充足,但是本地内存不足的情况,会导致OutOfMemoryError
DirectByteBuffer
私有构造
// Primary constructor
//
DirectByteBuffer(int cap) { // package-private
// mark, pos, lim, cap
super(-1, 0, cap, cap);
// 内存是否按页分配对齐
boolean pa = VM.isDirectMemoryPageAligned();
// 获取每页内存大小
int ps = Bits.pageSize();
// 分配内存的大小,如果是按页对齐方式,需要再加一页内存的容量
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
// 用Bits类保存总分配内存(按页分配)的大小和实际内存的大小
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 分配堆外内存,并返回堆外内存的基地址,指定内存大小
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
// 计算堆外内存的基地址
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
//在Cleaner内部中通过一个列表,维护了一个针对每一个 directBuffer 的一个回收堆外内存的线程对象(Runnable thunk),回收操作是发生在Cleaner的clean()中。当DirectByteBuffer被回收时,堆外内存也会被释放
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
Clean的clean方法
/**
* Runs this cleaner, if it has not been run before.
*/
public void clean() {
if (!remove(this))
return;
try {
thunk.run();
} catch (final Throwable x) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null)
new Error("Cleaner terminated abnormally", x)
.printStackTrace();
System.exit(1);
return null;
}});
}
}
DirectByteBuffer
通过full gc来回收内存的,DirectByteBuffer
会自己检测情况而调用system.gc(),但是如果参数中使用了-XX:+DisableExplicitGC
那么就无法回收该块内存了,-XX:+DisableExplicitGC
标志自动将System.gc()
调用转换成一个空操作,就是应用中调用System.gc()
会变成一个空操作。那么如果设置了就需要我们手动来回收内存了。还有一种情况,CMS GC会回收DirectByteBuffer
的内存,CMS主要是针对old space空间的垃圾回收
@Test
public void testAllocateDirector() throws Exception{
ByteBuffer buffer=ByteBuffer.allocateDirect(1024);
Field cleanerField = buffer.getClass().getDeclaredField("cleaner");
cleanerField.setAccessible(true);
Cleaner cleaner = (Cleaner) cleanerField.get(buffer);
cleaner.clean();
}
mac安装与使用
# brew install google-perftools
在项目的启动之前配置两个环境变量
# export DYLD_INSERT_LIBRARIES=/usr/local/Cellar/gperftools/2.7/lib//usr/local/Cellar/gperftools/2.7/lib/libtcmalloc.dylib 指定动态链接库(mac环境的)
# export HEAPPROFILE=/Volumes/O/a.log 指定导出的文件目录
# pprof -text $JAVA_HOME/bin/java a.log.0001.heap >> result.txt
Centos安装
# yum install autoconf automake libtool libunwind -y
# wget https://github.com/gperftools/gperftools/releases/download/gperftools-2.7/gperftools-2.7.tar.gz
# tar -zxvf gperftools-2.7.tar.gz -C /usr/local
# c -2.7
# ./configure --prefix=/usr/local/gperftools/ && make && make install
# rpm -qa| grep libunwind
libunwind-1.2-2.el7.x86_64
# rpm -ql libunwind-1.2-2.el7.x86_64
/usr/lib64/libunwind-coredump.so.0
/usr/lib64/libunwind-coredump.so.0.0.0
/usr/lib64/libunwind-x86_64.so.8
/usr/lib64/libunwind-x86_64.so.8.0.1
/usr/lib64/libunwind.so.8
/usr/lib64/libunwind.so.8.0.1
/usr/share/doc/libunwind-1.2
/usr/share/doc/libunwind-1.2/COPYING
/usr/share/doc/libunwind-1.2/NEWS
/usr/share/doc/libunwind-1.2/README
# vim /etc/ld.so.conf.d/usr_local_lib.conf 加入/usr/lib64/(libunwind的lib所在目录)
# sudo /sbin/ldconfig 使libunwind生效
# export LD_PRELOAD=/usr/local/gperftools/lib/libtcmalloc.so
# export HEAPPROFILE=/root/Desktop/a.log
# pprof -text $JAVA_HOME/bin/java a.log.0001.heap >> result.txt