共计 2800 个字符,预计需要花费 7 分钟才能阅读完成。
带着三个问题看垃圾回收
1. 回收谁
2. 什么时候回收
3. 怎么回收
1. 回收谁
引用计数法:给对象中添加一个引用计数器,每当有对象引用它时,计数器就加 1,引用失效就减 1,计数器到 0 的时候代表不能使用该对象,不能解决循环引用的问题
可达性分析:通过 GCRoots 做为起点,从这个起点向下搜索,当一个对象到 GCRoots 没有任何引用链相连的时候,这个对象是不可用的,可以回收。虚拟机栈(栈帧变量表中引用的对象),方法区(类的静态属性引用的对象,常量引用的对象),本地方法中 JNI(Native 引用的对象)
“ 食之无味,弃之可惜 ”
强引用(认可 OOM,也不会回收)
软引用(系统 OOM 之前,这些对象被回收)
弱引用(无论内存够不够,都会回收)
虚引用(只会收到回收通知)
“ 最后一刻挣扎 ”
一个对象的死亡,至少要被标记两次,第一次看有没有必要执行该对象中的 finalize 方法,如果该方法被调用过或者对象没有覆盖整个方法,就没有必要执行 finalize。如果执行了 finalize,可以在方法里面自救,自救方案是与引用链上任何一个对象关联即可。不建议使用
方法区的回收
回收效率低,回收严谨。只有满足以下三点才会回收
1. 该类的所有实例都被回收,堆中不存在任何该类的实例
2. 加载该类的 ClassLoader 已被回收
3. 该类的 java.lang.Class 对象没有任何地方被应用,无法通过反射来访问该类
2. 什么时候回收
应用线程空闲时
内存满的时候
3. 怎么回收
标记清除算法
先标记要回收的对象,在统一清除。缺陷是会产生大量不连续的内存碎片,在分配大对象时,不得不提前触发另一次垃圾收集动作
复制算法
将内存分 AB 两块,每次只用一块,A 的内存用完了,回收的时候就将 A 还存活的对象放在 B 上,然后统一清理 A。缺陷是对象多的时候浪费复制时间,对内存的开销也比较大。
标记整理算法
标记出所有要回收的对象,标记完成后,将存活的对象移动一端,然后清理掉端边界以外的内存。
分代收集算法
根据新生代和老年代的特点,使用分代收集算法
因为新生代朝生夕死,所以用复制算法,仅需要复制少量对象。
老年代存活率高,对象多,没有额外空间进行分配,就使用标记 - 整理算法。可以自由搭配很多种,不过大致的类型就是以下几种。
串行收集器(Serial)
-XX:+SerialGC
单线程收集器,这里的单线程不是指垃圾回收的线程只有一个,而是相对于应用程序来讲,在回收垃圾的时候要暂停应用程序(STW)
在内存不足时,串行 GC 设置停顿标识,当所有线程到达安全点后,应用程序暂停,开始垃圾收集。适合堆内存不高且单核的 cpu 使用。
并行收集器(ParNew,Parallel Scavenge)
-XX:+UseParNewGC
ParNew 是 Serial 收集器的多线程版本,搭配老年代 CMS 首选。适合多核 cpu。ParallelScavenge 更关注吞吐量,同样需要 STW,适合和前台交互少的系统,后台处理任务量大的
并发收集器(CMS)
更关注低延迟的收集器,分为以下四个阶段,适合内存大,多核 cpu。缺陷:消耗内存过的大,容易引起 fullGC。有碎片,为防止 fullGC,默认开启碎片整理参数
初始标记:以 STW 的方式标记所有根对象,很快
并发标记,与程序并发执行,标记出所有根路径的可达路径
重新标记,以 STW 标记有可能在这期间错过的,同样很快
并发清除,将不可达对象并发回收
G1 收集器
引入了 Region 概念,和 CMS 比较像,只不过有 Region 的优势
观看 GC 日志
33.125 代表虚拟机启动到现在,经过了多少秒
Full GC 和 GC 代表停顿类型,不是为了区分新生代和老年代的,如果有 Full,代表是以 SWT 触发的垃圾收集
DefNew,Tenured,Perm 才是区域,发生在什么区域上的
3324K – > 152K(3712K):GC 前该内存区域已使用的容量 -> GC 后该内存已使用的容量(总容量)
内存分配与回收策略
- 对象优先在 Eden 分配,如果说 Eden 内存空间不足,就会发生 Minor GC
- 大对象直接进入老年代,大对象需要大量连续内存空间的 Java 对象,比如很长的字符串和大型数组,1、导致内存有空间,还是需要提前进行垃圾回收获取连续空间来放他们,2、会进行大量的内存复制。-XX:PretenureSizeThreshold 参数,大于这个数量直接在老年代分配,缺省为 0,表示绝不会直接分配在老年代。
- 长期存活的对象将进入老年代,默认 15 岁,-XX:MaxTenuringThreshold 调整
- 动态对象年龄判定,为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄
- 空间分配担保:新生代中有大量的对象存活,survivor 空间不够,当出现大量对象在 MinorGC 后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把 Survivor 无法容纳的对象直接进入老年代. 只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小,就进行 Minor GC,否则 FullGC。
内存泄漏和内存溢出
内存泄漏是该释放的对象没有得到释放
内存溢出是撑爆了内存,对象太多了
JDK 为我们提供的工具
- jps
列出当前机器上正在运行的虚拟机进程
-p : 仅仅显示 VM 标示,不显示 jar,class, main 参数等信息.
-m: 输出主函数传入的参数. 下的 hello 就是在执行程序时从命令行输入的参数
-l: 输出应用程序主类完整 package 名称或 jar 完整名称.
-v: 列出 jvm 参数, -Xms20m -Xmx50m 是启动程序指定的 jvm 参数
- jstat
是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据,在没有 GUI 图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。
假设需要每 250 毫秒查询一次进程 2764 垃圾收集状况,一共查询 20 次,那命令应当是:jstat-gc 2764 250 20
常用参数:
-class (类加载器)
-compiler (JIT)
-gc (GC 堆状态)
-gccapacity (各区大小)
-gccause (最近一次 GC 统计和原因)
-gcnew (新区统计)
-gcnewcapacity (新区大小)
-gcold (老区统计)
-gcoldcapacity (老区大小)
-gcpermcapacity (永久区大小)
-gcutil (GC 统计汇总)
-printcompilation (HotSpot 编译统计)
- jmap
- jstack
- ………