简介
Garbage collection is the JVM’s process of freeing up unused Java objects in the Java heap.The Java heap is where the objects of a Java program live. It is a repository for live objects, dead objects, and free memory. When an object can no longer be reached from any pointer in the running program, it is considered “garbage” and ready for collection.
内存回收
所谓gc 就是一个内存管理、分配与回收的问题,因此可以跟操作系统的 内存分配回收 做一个对比。
回收分两步:
- 查找内存中不再使用的对象。引用计数、根搜索
-
释放这些对象占用的内存
- 标记-清理,找到无用对象并清理,符合人的直观反应,但容易有碎片
- 标记-复制,有用的复制到一边去,另一边直接清理掉。占用两倍的空间
- 标记-整理,找到无用对象并清理,把有用的复制出去,并更新引用其对象的指针
后两者的具体区别暂不说,总之回收思路就是:把无用的清理掉,有用的挪位置。
-
java垃圾收集器的历史
- Serial(串行)收集器
- Parallel(并行)收集器
- CMS(并发)收集器
- G1(并发)收集器,G1将新生代,老年代的物理空间划分取消了。是的,我们掌握的所有知识都在快速变化
gc 执行的时候,涉及到串行、并行、是否按分代分别回收,回收时是否暂停所有应用线程等
- 分代。gc 是有成本的,你挪了对象的位置, 在gc 期间,应用线程是不能工作的(因为引用指向的值都不对了)。因此要降低gc 的粒度。
- Java垃圾回收器是一种“自适应的、分代的、停止—复制、标记-清扫”式的垃圾回收器。
- 在G1中没有物理上的Yong(Eden/Survivor)/Old Generation,它们是逻辑的,使用一些非连续的区域(Region)组成的。上文说 gc 在分代上 降低粒度。在这里, 回收的过程多个回收线程并发收集,划分region 在物理空间上降低了并发的粒度。
堆内存分代
内容 | gc | ||
---|---|---|---|
新生代 | |||
eden | 不定时,主要是eden空间不够时,Scavenge GC | ||
survivor0 | gc后,eden存活的对象 | ||
survivor1 | 大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。 | ||
空间不够时,老年代 | survivor1空间不够时转移的数据 | full gc | |
持久代 | 用于存放静态文件,如Java类、方法等 |
所以,我们讲一个对象的生存期,关键就是看谁在引用它,背靠的大佬有多深。
堆外内存
分配 | 回收 | |
---|---|---|
堆内存 | new | gc机制 |
直接内存 | 显式分配 | 代码主动回收或基于system.gc回收 |
内存泄漏
一个最简单的C的内存泄漏的例子:
char *ptr1 = (char *)malloc(10);
char *ptr2 = (char *)malloc(10);
ptr2 = ptr1;
free(ptr1)
一开始ptr2指向的那块内存发生了泄漏,没人用了,因为没有指针指向,用不了,却又回收不掉(内存管理数据结构,一直记录此块内存是被分配的)。
What is a PermGen leak?a memory leak in Java is a situation where some objects are no longer used by an application, but the Garbage Collector fails to recognize them as unused. This leads to the OutOfMemoryError if those unused objects contribute to the heap usage significantly enough that the next memory allocation request by the application cannot be fulfilled.
如何分析内存泄漏,精确到对象?用弱引用堵住内存泄漏 关注hprof工具, google 分析程序内存和cpu 使用的工具:gperftools
内存泄漏既可以是堆内也可以是堆外内存,还可以是PermGen/MetaspaceWhat is a PermGen leak?
如上图所示,如果一个对象 被另一个有效对象引用,则其Class 对象、Class 对象引用的ClassLoader对象、ClassLoader对象引用的所有归其加载的Class对象也将“可达”,可能导致不需要的Class无法被“卸载”。
引用和内存回收
比较netty 引用 + arena 一套和java 四种引用 + gc一套的关系。
未完成:netty 引用计数和arena
- 程序有内存泄漏的第一个迹象通常是它抛出一个 OutOfMemoryError,或者因为频繁的垃圾收集而表现出糟糕的性能。
- 用一个普通的(强)引用拷贝一个对象引用时,限制 referent 的生命周期至少与被拷贝的引用的生命周期一样长。如果不小心,将一个对象放入一个全局集合中的话,那么它可能就与程序的生命周期一样(也就是对对象引用的操作会影响对象的生命周期)。另一方面,在创建对一个对象的弱引用时,完全没有扩展 referent 的生命周期,只是在对象仍然存活的时候,保持另一种到达它的方法。
- 引用队列是垃圾收集器向应用程序返回关于对象生命周期的信息的主要方法。如果创建弱引用时将弱引用与引用队列关联,则当referent被回收时,gc会将弱引用加入到引用队列中。
使用 VisualVM 进行性能分析及调优
可以将jvm日志导出来,有专门的线下、线上工具帮你分析日志,生成图表。也可以配置tomcat等打开jmx端口,jvisualvm 连接远程 ip 和 jmx 端口也可进行分析。
heap dump
heap dump文件是一个二进制文件,它保存了某一时刻JVM堆中对象使用情况。HeapDump文件是指定时刻的Java堆栈的快照,是一种镜像文件。需要使用专门工具分析。
java程序性能分析之thread dump和heap dump
jmap 生成dump文件
jhat -port 5000 dump文件
在浏览器中,通过http://localhost:5000/进行访问 页面底部点击 Show heap histogram
可以观察到所有类的实例数,笔者就曾观察到
Class | Instance Count | Total size |
---|---|---|
class [B | 18600 | 4153766165 |
class [B
表示byte数组, 很多实例都没有释放,有助于辅助查找内存泄露的原因。
除了jhat之外,还可以使用eclipse 插件memory analyer。参见使用 Eclipse Memory Analyzer 进行堆转储文件分析
thread dump
java程序性能分析之thread dump和heap dump
thread dump文件主要保存的是java应用中各线程在某一时刻的运行的位置,即执行到哪一个类的哪一个方法哪一个行上。thread dump是一个文本文件,打开后可以看到每一个线程的执行栈,以stacktrace的方式显示。通过对thread dump的分析可以得到应用是否“卡”在某一点上,即在某一点运行的时间太长,如数据库查询,长期得不到响应,最终导致系统崩溃。单个的thread dump文件一般来说是没有什么用处的,因为它只是记录了某一个绝对时间点的情况。比较有用的是,线程在一个时间段内的执行情况。
两个thread dump文件在分析时特别有效,困为它可以看出在先后两个时间点上,线程执行的位置,如果发现先后两组数据中同一线程都执行在同一位置,则说明此处可能有问题,因为程序运行是极快的,如果两次均在某一点上,说明这一点的耗时是很大的。通过对这两个文件进行分析,查出原因,进而解决问题。
jstack 2576 > thread.txt
Java内存泄漏分析系列之四:jstack生成的Thread Dump日志线程状态 是一个系列,建议结合java线程的状态转换图 一起看。一个thread dump文件部分示例如下:
"resin-22129" daemon prio=10 tid=0x00007fbe5c34e000 nid=0x4cb1 waiting on condition [0x00007fbe4ff7c000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:315)
at com.caucho.env.thread2.ResinThread2.park(ResinThread2.java:196)
at com.caucho.env.thread2.ResinThread2.runTasks(ResinThread2.java:147)
at com.caucho.env.thread2.ResinThread2.run(ResinThread2.java:118)
一次fullgc 的处理过程
-
使用VisualVM 可以确认字符串是最多的。发现一个奇怪的现象:“计算保留大小”之后,这些String的保留大小都是0。使用VisualVM 显示最近的垃圾回收根节点,发现都找不到。
- 使用MAT 寻找内存较大的对象。但意外发现size 只有4xxM。
- MAT的主要目标是排查内存占用量,所以默认大小是不计算不可达对象的。在”Preferences=>Memory Analyzer”中勾选”Keep Unreachable Objects”,关闭mat,删除索引文件Dump同路径下的所有”.index”文件,启动mat,即可看到所有的对象。
- overview 发现size 有1GB
- 保留大小是什么呢?它是分析工具从GC roots开始查找,找到的所有不会回收的对象,然后按照引用关系,计算出这个“对象以及它引用的对象”的内存大小。结合以上现象:这些大String是临时对象,没有什么对象持有它——通过分析这些String的依赖关系也说明了这一点。这些对象是可以被回收的,换句话说,并不是有明显的内存泄露。只是对象大 + 写入太快 导致了频繁的younggc,younggc 忙不过来年轻代满了 新的String 就进入老年代,然后引发fullgc。
- 这些字符串是什么呢?
-
点击Histogram,即可看到类的占用。在类上选择”List Objects”,即可看到所有对象。
retained heap 从高到低排列
- 在对象上选择”Copy=>Value to File”,即可保存到文件。查看文件内容 是一个对象json 后的内容
- 在代码中有大量的
log.debug(JSON.toJSONString(obj));
,obj 在一些场景下会很大。而虽然日志级别是info,debug 日志不会打印。但按照log.debug
的实现,是先执行JSON.toJSONString(obj)
,然后判断debug 日志无需输出,因此还是会频繁的执行JSON.toJSONString(obj)
- 解决这个问题参见 log4j学习
引用
Understanding Java heap memory and Java direct memory
成为JavaGC专家(1)—深入浅出Java垃圾回收机制,这是一个系列的文章,要学习下