判断对象是否可回收
判断一个对象是否可以被回收,即还有没有其它指针指向它,如果没有了,说明这个对象已经不可以再被使用到了,就可以回收掉它的内存空间。主要有两种方式可以判断:
- 引用计数法:每当有一个引用引用到对象,就给对象的引用计数加一,这样当某个对象的引用计数为0时,就说明该对象已经可以被回收了,但是该方法有循环引用的问题。
- 可达性分析算法:以GC Roots对象为起点开始搜索引用链,凡是能搜索到的对象都是存活的对象,未能搜索到的对象都是可回收的对象。
Java虚拟机中多使用可达性分析算法,其中GC Roots对象有4类:
- 方法区中的常量引用的对象。
- 方法区中静态变量引用的对象。
- 栈中局部变量引用的对象。
- 本地方法栈中引用的对象。
Java中对象有四种引用:强引用,一定不会被垃圾收集;软引用,空间不足时会被收集;弱引用,下一次垃圾收集就被收集;虚引用,不会对对象的生命周期产生任何影响。
对类的回收
虚拟机对类的回收十分严苛,需要满足三个条件,并且满足了也不一定会回收:
- 类的所有实例已经被回收。
- 加载该类的ClassLoader已经被回收。
- 该类对应的Class对象没有被引用,不能通过反射获得该类。
垃圾收集算法
- 标记-清除算法:虚拟机标记可达对象后,将不可达的对象统一清除。这种算法效率比较低,而且容易造成空间碎片。
- 标记-整理算法:虚拟机将可达的对象全部移动到内存的一侧,将边界以外的空间一起释放。好处是无内存碎片,缺点是效率低。
- 复制算法:将内存分两块,只使用一块,在垃圾收集时将存活对象移动到另一块即可。好处是效率高,但是内存只使用到一半。虚拟机内实现一般分为一块Eden与两块Survivor,比例8:1:1,每次使用Eden与一块Survivor,留一块Survivor做垃圾收集时的复制空间,这样可以提高空间利用率。
Stop the world
由于不能出现可达性分析过程中对象引用关系还在不断变化的情况,因此GC时必须停顿所有的Java执行线程,会影响系统响应以及用户体验,在HotSpot实现中,使用一组称为OopMap的数据结构来存储哪些地方存放着对象引用,这样可以减少GC时扫描全局引用的时间。
HotSpot并没有为每条指令都生成OopMap,只在“特定的位置”记录,这些位置称为安全点,因此程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能。但还有一个问题需要考虑,如何在GC时让所有线程都跑到最近的安全点上停顿,有两种方式:
- 抢先示中中断,不需要线程的执行代码配合,在GC时,中断所有线程,如果发现有线程中断的地方不在安全点上,就让它跑到安全点上,现在几乎没有虚拟机采用这种方法。
- 主动式中断,当GC需要中断线程时,设置一个标志,各个线程主动轮询这个标志,发现为真时自己中断挂起。
当线程处于挂起状态时,无法响应中断请求,这种时候需要安全区域来解决。安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任何地方开始GC都是安全的,安全区域可以看作是扩展的安全区域。
垃圾收集器
很多垃圾收集器采用分代收集的方式,将堆中的内存分为新生代和老年代,新生代的对象通常存活时间较短,回收比较频繁,采用效率高的复制算法,老年代的对象存活时间很长,回收不频繁,采用标记-清除或标记-整理算法。
- Serial 与 Serial Old 收集器:单线程收集器,垃圾收集时需要暂停其它工作线程,直到收集结束。主要用在Client模式下的虚拟机。
- ParNew 收集器: Serial收集器的多线程版本,主要用在Server模式的虚拟机上,能与CMS收集器配合工作。
- Parallel Scavenge 与 Parallel Old 收集器,多线程吞吐量优先收集器。
CMS收集器
目标是获得最短的回收停顿时间,基于标记清除算法实现,在Server端使用比较多。运作过程包括初始标记,并发标记,重新标记与并发清除。初始标记只标记GC Roots能直接关联到的对象,速度很快;并发标记就是进行可达性分析的过程;重新标记修正并发标记期间引用关系发生变化的部分;并发清除则是清除不可达对象,回收空间。
整个过程中,并发标记与并发清楚可以和用户线程一起工作,因此停顿时间只有初始标记和重新标记的过程,但时间并不长。
CMS收集器的缺点是有空间碎片产生,无法处理浮动垃圾以及对CPU资源十分敏感。
G1收集器
G1收集器主要应用在Server端。它将对划分为多个大小相等的独立区域,新生代和老年代不再物理隔离。它会跟踪每个区域垃圾堆积的大小,维护一个优先列表,回收时回收价值最大的区域。运作工程包括初始标记,并发标记,最终标记,筛选回收。由于可以控制回收多少块区域,因此可以得到一个可预测的停顿时间。
对象内存分配
内存分配策略
- 对象优先在新生代的Eden分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代,可以设定年龄阈值或者动态年龄判断
分配内存
在堆上为对象分配内存是,如果内存是绝对规整的,可以使用指针碰撞,直接让指针偏移出对象内存大小即可。如果内存不规则,需要维护一个空闲列表,从列表中选择能满足对象内存需求的内存块分配给对象。
对象在分配时,也需要保证线程的安全,通常本地线程会预先分到一块内存空间作为本地线程缓冲,该线程创建的对象都在这块空间内分配,当缓冲用完后,需要申请新的空间时才需要同步锁定。
Minor GC 与 Full GC
Minor GC 主要回收新生代的空间,Major GC 主要回收老年代空间,Full GC 回收老年代与新生代与元空间。
触发full gc 的条件:
- 调用System.gc() 方法,但是不一定会执行
- 老年代空间不足
- 空间分配担保失败
- 永久代( jdk 1.7 之前) 或 元空间不足
- CMS GC 发生错误
finalize()方法
对象被回收时,如果对象覆盖了Object类的finalize()方法,虚拟机会在回收前执行对象的finalize()方法,但是不保证什么时候开始执行,也并不保证一定会执行完这个方法。
元空间
在 Java8 中, 永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。 类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中, 这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制。