树
红黑树
B+树
LSM 树(Log-Structured Merge Tree)
LSM 树(Log-Structured Merge Tree)牺牲了一定的读性能来换取写入数据的高性能,Hbase、Cassandra、LevelDB 都是用这种算法作为存储的引擎。
数据首先会写入到一个叫做 MemTable 的内存结构中,在 MemTable 中数据是按照写入的 Key 来排序的。为了防止 MemTable 里面的数据因为机器掉电或者重启而丢失,一般会通过写 Write Ahead Log 的方式将数据备份在磁盘上。
MemTable 在累积到一定规模时,它会被刷新生成一个新的文件,我们把这个文件叫做 SSTable(Sorted String Table)。当 SSTable 达到一定数量时,我们会将这些 SSTable 合并,减少文件的数量,因为 SSTable 都是有序的,所以合并的速度也很快。
当从 LSM 树里面读数据时,我们首先从 MemTable 中查找数据,如果数据没有找到,再从 SSTable 中查找数据。因为存储的数据都是有序的,所以查找的效率是很高的,只是因为数据被拆分成多个 SSTable,所以读取的效率会低于 B+ 树索引。
列表
跳表(skiplist)
压缩列表(ziplist)
问题描述
不停的请求项目中的搜索接口,项目运行一段时间后,出现内存溢出问题。现象是每次YGC后,老年代内存就会增加内存0.02%(2G内存,1G堆内存),而且这些老年代的内存很难被回收,即使手动调用了FGC,依然存在内存中。基本上,只能无奈的看着老年代内存不断增长,却无能为力。
排查
- 不停调用搜索接口,直到内存溢出导出内存情况,使用MAT进行分析。发现内存溢出的原因是存在一个很大的
URLClassLoader
对象。怀疑公司定制的spring boot有问题。
- 因为怀疑是系统原因,所以改调用其他接口,验证是否会有现象。发现确实有现象,并且现象一致。这里走了弯路,因为搜索接口不仅是会完成搜索的工作,同时会把搜索的过程调用kafka发送日志,之后会再接收日志存储进数据库和es。调用其他接口依旧有现象的原因是,之前日志没有处理完依旧在处理日志。这里卡住了很久,一直在研究框架问什么会有
URLClassLoader
大对象。
- 幡然醒悟不是框架原因是,过了一天时间,系统调用其他接口,不会内存溢出了,没有导出想要的内存信息。发现还是和搜索接口有关。这里有另一坑,就是我的内存是导出的本机,但是本机可能本身内存也比较小,所以
URLClassLoader
并不是溢出的根本因素。只有继续调用搜索接口,复现问题,更多的日志进行分析。
- 这次联系了平台,在平台上复现的时候的日志导出了。这里有用到jvm启动参数
-XX:GCTimeLimit
,当发现GC花费时间超过上限时会抛出异常,但保持jvm的运行,方便保留现场。因为用到的是docker,jvm挂了就会重启,无法从平台上导出日志。
- 从平台导出的日志,帮助找到了真凶,老年代驻留了很大
ConnectionPool
对象,顺藤摸瓜找到了对象持有了一堆ClientPreparedStatement
对象没有被close,找到其中的sql语句,是存储日志到数据库的语句。这里用到了sharding-jdbc
做了分表存储,数据库源是tomcat datasource
,orm是mybatis
。于是将这个地方的存储注释掉,继续跑脚本复现问题。
- 依旧会有每次YGC后的老年代占用比增加,但手动调用FGC后内存会被回收。果断移除了
sharding-jdbc
,再试,还是会被回收。继续跑脚本复现。
原因
还没有想明白为什么不关闭ClientPreparedStatement
。猜测因为事务配置直接使用了Spring支持的,但是去掉事务后没有明显的改变,还是有很多这样的对象堆积。
总结
- 对于自己不熟悉的组件一定要研究透彻了再使用,否则可能埋下隐患
- JVM内存问题排查,一定要先收集到足够多的信息,不要急于验证猜想
命令
jstat -gcutil 1 10000 # 每10秒打印一次进程1的gc情况
jmap -histo:live 1 | head -100 # 列出进程1的jvm中当前存活的对象,head是指前100行
jmap -dump:format=b,file=/opt/jvm-06-29 1 # 导出进程1的jvm状态
jvm启动参数
-XX:GCTimeLimit=time-limit
花费在GC上的时间上限,默认是98,当超过上限时,会抛出OutOfMemory(HeapSpace)的异常
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/jvm-06-29
内存溢出时导出堆内存到/opt/jvm-06-29
衍生
虚引用
不同的引用类型,主要体现的是对象不同的可达性(reachable)状态和对垃圾收集的影响。强引用指向一个对象,就表明它一直活着,不能被回收。软引用,可以豁免一些回收,只有在jvm认为内存不足时回收。弱引用不能豁免垃圾回收,仅仅提供一种访问弱引用对象的途径。
对于幻象引用,有时候也翻译成虚引用,你不能通过它访问对象。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制,我在专栏上一讲中介绍的 Java 平台自身 Cleaner 机制等,也有人利用幻象引用监控对象的创建和销毁。
小结:
- 虚引用是最弱的引用
- 虚引用对对象而言是无感知的,对象有虚引用跟没有是完全一样的
- 虚引用不会影响对象的生命周期
- 虚引用可以用来做为对象是否存活的监控