本文是笔者看周志明的《深入理解Java虚拟机》做的一些个人的总结,希望各位读者能够指出菜鸡笔者的错误并且斧正。
Java垃圾回收的主要作用区域:堆内存
对于线程私有的虚拟机栈,本地方法栈,PC来说。它们的生命周期自然而然随着线程的结束而结束,因此对于这些区域来说,不需要专门的设置GC。
对于方法区而言,虽然有的Java虚拟机对方法区实施了垃圾回收,但是对方法区进行GC实际上主要是对常量池进行GC(类卸载的条件太苛刻了)。
因此,进行垃圾回收的最主要的内存区域就是堆内存。
如何判断对象已死?
- 引用计数法 2. 可达性分析算法
引用计数法:
在对象中添加一个引用计数器,每当有一个地方引用它,计数器的值就加一,当引用失效,计数器值就减一。任何时候计数器的值为0的时候,这个对象是不可能再被使用的。
然而在Java领域里面,主流的Java虚拟机都没有采用这个方法进行内存管理。
可达性分析算法:
基本思路:根据一系列称为“GC Roots” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,如果某个对象到GC Roots间没有任何引用链相连,证明此对象是不可能再被使用的。
GC Roots集合并不是一成不变的。堆内存中一个区域的对象完全可能被其他区域的对象引用,这就需要把关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。
引用的不同类型:
- 强引用,只要引用关系还存在,就不会被GC(最传统的引用关系)
- 软引用,食之无味,弃之可惜的引用。在发生OOM的时候,会先把这些对象进行一个回收,如果内存还是不够,才会报OOM。
- 弱引用,比软引用更低一个级别。关联的对象只能活到下一次GC,无论内存够不够都会被回收。
- 虚引用,为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被回收时收到一个系统通知。
对于方法区的回收:
对方法区的回收主要分为两个部分:废弃的常量和不再使用的类型。
对于常量的回收与Java堆中对象的回收非常相似。
如果有一个字符串 “java” 曾经进入过字符串常量池,但是没有一个字符串对象的值是 “java”, 如果此时发生GC,“java” 常量就会被清出常量池。
不再使用的类型(类卸载):
类卸载需要满足几个条件:
- 该类的所有实例都已经被回收
- 加载该类的类加载器已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过发射访问该类的方法。
垃圾收集算法:
分代收集理论:
分代收集理论包括三个主要的假说:
- 弱分代假说:大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次GC的对象就越难以消亡
- 跨代引用相对于同代引用来说仅占极少数,存在相互引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。
三种主流的垃圾收集算法:
- 标记-清除算法:
主体思路:标记处需要回收的对象,标记完成后,统一回收掉所有被标记的对象。
缺点:1. 执行效率不稳定 2. 容易产生内存空间碎片化的问题。
- 标记-复制算法:
标记复制算法会涉及到堆分区的问题:
- 半区复制:把堆按照内存大小分为相等的两份,每次只使用其中的一份。这一块的内存用完了,就把还存活的对象复制到另一块上,然后彻底清除这一块内存。
缺点:内存使用率只有原来的一半,空间浪费太多了。
- Appel式回收:8 :1 :1布局
Appel式回收的具体做法是把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用一块Eden空间和一块Survivor空间。发生GC的时候,把存活的对象复制到剩下的一个Survivor空间。当Survivor空间不能容纳下存活的对象,那么就放入老年代。
标记-整理算法:
标记-复制算法在存活对象比较多的时候,需要进行比较多的复制操作,对于老年代这种对象存活率比较高的情况,标记-复制算法就不够合适。
标记-整理算法是让所有存活的对象都向内存空间的一端移动,然后直接清除边界外的内存。
移动和不移动对象的trade off:
移动对象,对象的地址产生了变化,因此需要停顿用户线程以保证用户线程使用的对象地址正确,因此增加了用户线程的响应时间。
不移动对象:响应时间快,但是程序的吞吐量低,因为不移动对象会使得需要维护空闲链表来管理内存,相比于指针碰撞法,分配内存时就显得低效了。
对于具体的垃圾收集器而言,如果关注吞吐量,那么一般选用对象移动的算法。如果关注延迟和时效性,那么选用不移动的算法。