心得:Java中垃圾回收和内存可以实现高度的自动化,栈帧可以由JVM自动分配和回收,局部变量表和操作数栈也可以在编译时就确定好,堆中的内存分配和回收才是JVM关注的重点,JVM实现大多采用可达性分析来标记存活对象,什么时候标记?让用户线程主动跑到那些安全的地方(引用关系不变的时候,SafePoint和Safe Region),再由GC收集器来标记进行处理。
不同的垃圾收集器甚至可以决定堆的内存布局,比如G1的“化增为零”一方面借助Remember Set可以更细粒度的进行并发标记和回收。
分代是GC种重要的思想,对不同特点对象进行各自适合的回收策略,Minor GC一般是新生代采用Copying算法,“空间换时间”,也是因为新生代大部分对象“朝生夕死”;Full GC在老年代一般是Mark and Compact两个步骤,不用额外空间,但停顿长,可谓“用时间省空间”;
CMS和G1更是采用并行+并发的手段,但一个新的问题,就是并发期间的用户线程的内存开销(CMS和G1各有对应策略)和对象引用关系的变化,因此它们都有remark的过程。
总之,不同的场景用不同的技术,“知其然”的同时能够“知其所以然”才能在实际的场景下选择“对的”技术。
垃圾回收是一个复杂的系统问题,本人认识还是十分有限。。。
学习参考资料:
(1)《深入Java虚拟机》(第二版);
(2)RednaxelaFX的回答—现代JVM中的Safe Region和Safe Point到底是如何定义和划分的?
(3)G1垃圾收集器入门
(4)我在知乎的提问(多谢RednaxelaFX大神的回答)
(5)CMS的原始论文:A Generational Mostly-concurrent Garbage Collector
一个小栗子(我在知乎的提问)
一个问题Java中的对象到底占多少内存?
JVM规范也不能回答这个问题,因为它是一个公有设计;
The Java Virtual Machine does not mandate any particular internal structure for objects.
看过书的同学应该都知道,对象由对象头+实例数据+padding组成;我利用Instrumentation做了一个小小的实验,基于64位JDK 8的Hotspot:
/*
基本信息:对象内存布局,对象的大小
注意:以HotSpot为例,Java中的对象内存布局包括:MarkWord,ClassPointer,实例数据,padding
如果是数组还有数组长度;如果开启了字段压缩,会进行指针压缩,子类的窄变量会插入父类的宽变量之中
*/
public static void main(String[] args) {
//Object
System.out.println(ClassUtils.sizeOf(new Object())); //16 8字节MarkWord,4字节klass指针,4字节padding
//数组
System.out.println(ClassUtils.sizeOf(new byte[0])); //16 8字节MarkWord,4字节klass指针,4字节数组长度
System.out.println(ClassUtils.sizeOf(new byte[7])); //24 padding补齐
System.out.println(ClassUtils.sizeOf(new byte[_1MB])); //1024 * 1024 + 16 = 1048592
//窄对象
System.out.println(ClassUtils.sizeOf(new Integer(1))); //16 int和klass补齐
System.out.println(ClassUtils.sizeOf(new Byte("1"))); //16 byte和klass补齐
System.out.println(ClassUtils.sizeOf(new Character('a'))); //16 char和klass补齐
}
因为对象的大小一定是8的倍数,可以看到Hotspot很节省的将类型指针和数组长度或者int,byte,char合并保存了,而64位的Hotspot中reference的长度仍然是4个字节,一些博客上有说成8个字节的。
1. 确定回收对象
引用计数和可达性分析(counting和tracing)
前者通过对象的引用计数器来记录被引次数,显然的一个问题是循环引用;Java采用的是可达性分析;
从GC Roots出发,延引用链对对象进行搜索,没有任何引用链和GC Roots相连的对象就被成为不可达的,被判定为可回收的对象;
GC Roots(方法区和栈中):
(1)虚拟机栈(栈帧中的局部变量表);
(2)本地方法栈JNI引用的对象;
(3)方法区中类静态属性;
(4)方法区中常量引用;
引用类型
就像进程的状态不能由简单的运行和终止描述一样;引用也需要进行一步细分:
强引用:永远不会被回收掉的对象;
软引用:如果一次回收后,内存还是不足,才进行回收,如果再不够,OOM;
弱引用:发生GC时,无论内存是否足够都会被回收;
虚引用:程序不能引用到,但是被回收时可以收到一个通知;
一个很重要的应用就是缓存,在内存中缓存一定要注意防止内存泄漏,在Java Collection Framework中,容器在删除是都执行置空的操作;
另一个注意的是可以使用WeakHashMap作为缓存容器;如果不是WeakHashMap一定要控制数量和及时清除(Integer.valueOf等就控制了数量);
终结(finalize)
从对象的可触及性来说还有2个状态:
可复活和不可触及(两次标记):
在判定为不可达后:
(1)可复活:一次标记,对象分为没有必要执行finalize方法(包括没有覆盖和已经执行两种)和需要执行finalize方法,前者直接就可以被回收;
(2)有必要执行finalize的方法被放在F-Queue队列中,有JVM中一个低优先级的Finalizer线程去触发它们的finalize方法(不会等待方法结束),如果在finalize方法中对象有引用链建立了连接就会被“复活”,否则就Over;
一个对象的finalize方法只能被执行一次,也就是说一个对象甭想自救两次!
方法区中的回收
对于常量来说,没有任何东西引用,那么也是可以被回收的;
类的卸载:条件非常苛刻(JVM规范没有要求在方法区中实现垃圾回收,Hotspot中有但是类还是很难被卸载)
(1)该类所有的实例被回收;
(2)对应的ClassLoader被回收;
(3)对应的Class对象没有引用;
栈的回收是虚拟机静态分配和回收,栈帧的大小可以在编译时确定,JVM通过栈帧的分配和回收很容易(开销很低)就完成;
方法区的回收中类的卸载很棘手,对于大量用反射,动态代理,CGLIB等技术的程序,JVM要能够卸载类;
主要的一个问题就是堆中内存的分配和卸载;
2. 垃圾回收算法
标记-清除算法(Mark-Sweep)
问题:
(1)空间碎片;
(2)效率:标记和清除两个过程相对来说不高;
复制算法(Copying)
原理:两块内存来回复制,因为有一块空白的内存可以直接复制,因此不用再分Mark,Sweep或者Compact多个阶段了,空间换时间;
当然我们知道最后的设计是:一个Eden+两个较小的Survivor,这是由于Java中对象98%的新对象都可以被回收的统计数据得来的经验;
标记-整理算法
区别与“标记-清除”,整理指的是不再原来位置直接进行回收,而是存活的对象向一端移动,最后界限之外的部分直接清理掉;
根据不同对象的特点,采用分代的方式垃圾回收;
3. HotSpot的垃圾回收算法实现
什么时候回收垃圾,怎样尽量降低对用户线程的影响不同的业务需求对垃圾回收有什么不同的要求?
枚举根节点
一致性:进行引用链分析显然要基于一个一致性的快照,不能因为分析过程中引用关系变化而导致错误;
Stop the world:一个简单直接的办法,但是显然会产生停顿;
OopMap:为了避免进行全盘扫描,借助与OopMap这样的数据结构保存对象的被引用范围,告诉JVM哪些地方存折对象的引用;
安全点(节省开销,安全性)
定义:the thread’s representation of it’s Java mac