第三章 垃圾收集器与内存分配策略
第三章 垃圾收集器与内存分配策略
3.1 概述
垃圾回收器需要完成的三件事:
- 哪些内存需要回收
- 什么时候回收
- 怎么回收
虚拟机栈、程序计数器、本地方法栈不需要垃圾回收;Java堆和方法区需要。
3.2 判断对象是否存活
3.2.1 引用计数算法
概述:每有一个引用,引用数就加1;每消失一个引用,引用数就减1;引用数到0了,就是要回收的对象。
缺点:无法解决循环引用问题 。如obja = objb和objb = obja。
3.2.2 可达性分析算法
概述:从“GC Roots”开始向下搜索,留下可达的,干掉不可达的。
可作为GC Roots的:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,如各个线程被调用的方法栈中使用到的参数、局部变量、临时变量等
- 方法区中类静态属性引用的对象。
- 在方法区中常量引用的对象。
- 在本地方法栈中JNI(Native方法)引用的对象
- Java虚拟机内部的引用
- 所有被同步锁(synchronized关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
- 根据选用的垃圾收集器和当前回收的区域的不同,可有其他对象临时加入,如分代收集和局部回收
3.2.3 再谈引用
JDK1.2后,几个引用概念:
- 强引用:程序中的引用赋值,类似Object obj = new Object();只要强引用存在,垃圾收集器就不会回收被引用的对象
- 软引用:描述一些还有用,但不是必须的对象。被软引用的对象,在系统要发生内存溢出时,会将其进行二次回收,如果内存还不够,再抛出内存溢出异常。
- 弱引用:类似软引用,但强度要低一些。只能生存到下一次垃圾收集器发生为止,无论内存够不够,被弱引用的对象都会被回收。
- 虚引用:不会对对象的生存事件产生影响,也无法通过其获得对象的实例。只用作一个对象在被回收时,得到一个通知。
Java弱引用(WeakReference)的理解与使用_Java_零度的博客专栏-****博客
3.2.4 生存还是死亡?
宣告对象死亡,要经过两次标记:
- 没有与GC Roots相连接的引用链。
- 没有在finalize()方法中逃脱。
判断有必要执行finalize()方法:
- 该方法已被覆盖
- 该方法从未被虚拟机调用过
如果判断对象有必要执行finalize()方法,就被放入一个低优先级的F-Queue中,等待执行该方法。“执行”,并不意味会等它运行结束。finalize()方法是对象逃脱回收的最后一次机会。
任何一个对象的finalize()方法只会执行一次,如果在下一次回收,该对象的finalize()方法不会执行。
现在不推荐使用finalize()方法。即使在关闭资源这类工作,因为有try-finally语句。
3.2.5 回收方法区
回收方法区性价比很低:
- 回收的空间少
- 回收的条件苛刻
Spring????
在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP已经OSGi这类频繁自定义类加载器的场景中,通常需要Java虚拟机有类型卸载的能力,以保证不会对方法区造成过大的压力。
3.3 垃圾收集算法
推荐阅读:Richard Jones编写的《垃圾回收算法手册》第 2~4章。
从如何判断对象死亡出发,垃圾收集算法分为:
- 引用计数式
- 追踪式
3.3.1 分代收集理论
分代收集理论建立在两个分代假说之上:
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次GC的对象越难以消亡
这两个假说奠定了垃圾收集器的一致设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象根据其年龄(对象熬过垃圾收集的次数)分配到不同的区域中储存。
由于跨代调用的情况存在,出现第三条假说:
- 跨代引用假说:跨代引用相对同代引用来说仅占极少数
依据这条假说,我们将新生代上建立一个全局的数据结构,它将老年代分为若干小块,并标识出哪一块会发生跨代调用,当发生Minor GC时,只有这部分内存里的对象会被加入到GC Roots进行扫描
GC种类:
- 部分收集(Partial GC):不是完整收集整个Java堆。
- 新生代收集(Minor GC / Young GC):只收集新生代
- 老年代收集(Major GC / Old GC):只收集老年代。只有CMS收集器会这样
- 混合收集(Mixed GC):收集全部新生代和部分老年代。只有GI收集器有这种行为
- 整堆收集(Full GC):收集整个Java堆和方法区
3.3.2 标记-清除 算法
算法分标记和清除两个阶段:
- 首先标记需要回收的对象
- 对标记的对象进行回收
也可以反过来:
- 标记存活的对象
- 回收未标记的对象
缺点:
- 效率不稳定
- 内存空间碎片化
3.3.3 标记-复制 算法
为解决上一种算法面对大量需要回收对象效率低下的问题。
概述:将内存划为大小相等的两块,每次只用一块,当这一块用完了,就将存活的对象复制到另一块,再将这块空间一次全部清理。
缺点:可用内存缩小了一半;而对于复制开销,由于存活下的对象很少,所以还是值得的。
Appel式回收:
- 把新生代分为一块较大的Eden空间和两块较小的Survivor空间。
- 每次内存只使用Eden空间和一块Survivor空间。
- 发生垃圾收集时,将Eden和Survivor中存活的对象,复制到另一块Survivor空间,并清理那两块空间。
- 当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(多为老年代)进行分配担保。
3.3.4 标记-整理 算法
算法过程:
- 将存活的对象进行标记
- 将标记的对象往内存的一端移动
- 清理掉边界外的空间
3.7 选择合适的垃圾收集器
Epsilon收集器
- 不能进行垃圾收集的“垃圾收集器”
- 用于存活时间较短的应用(这时候只需要JVM可以合理分配内存,程序在堆耗尽之前就会退出)
垃圾收集器的职责:
- 进行垃圾收集
- 堆的管理与布局、对象的分配、与解释器、编译器、监控子系统的协作
选择垃圾收集器需要考虑的因素:
- 应用程序的关注点是什么?吞吐量?延迟?内存占用?
- 运行应用的基础设施如何?系统架构?处理器数量?分配的内存?操作系统?
- 使用的JDK的发行商?版本号?
GC日志详解:
深入理解JAVA虚拟机—GC日志详解_Java_brushli的专栏-****博客
3.8 实战
针对大对象的优化:
什么是大对象:需要大量连续内存的Java对象。
HotSpot虚拟机提供了“-XX:PretenureSizeThreshold"参数指定大于设定值的对象直接在老年代分配。
注意:该参数只对Serial和ParNew两款新生代收集器有效,HotSpot的其他新生代收集器,比如Parallel Scavenge并不支持此参数。可以使用ParNew加CMS收集器组合。
空间分配担保过程