读深入理解JAVA虚拟机 第二三章,记一下内容
垃圾收集器与内存分配策略
判断对象是否没有被引用的方法:
1,引用计数算法,每当有地方引用,就+1,引用失效,就-1;,计数器为0就表示不能再被使用; 问题:不能解决对象之间的循环引用
2,可达性分析算法 Reachability Analysis,主流语言都使用它,做法:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链的相连时,就证明此对象不可用。
比如说abc三个互相连着,但是和作为GC Root的D,E,F都没有相连,那abc就是没用的了。
可以作为GC Root的对象包括:
1,虚拟机栈中引用的对象(栈帧中的本地变量表)其实就是正在使用的对象
2,方法区中类静态属性引用的对象 static xxx = xxx;
3,方法区中常量引用的对象 final static xxx;
4,本地方法栈中JNI(Native方法)引用的对象
引用
JDK1.2以后,在除了有被引用和没有被引用之外,又增加了几种引用情况
用来适用这种场景:当内存空间足够的时候,就保留在内存中,当内存空间紧张的时候,就抛弃,类似于缓存的功能。
JDK1.2扩充后的引用的概念:
强引用(Strong Reference):普通的引用,比如 Object obj = new Object(); 只要引用还在,就不会被回收
软引用(Soft Reference) :描述一下还有用但并非必须的对象,在系统将要发生内存溢出异常之前,会将这些对象列进回收范围之中,进行第二次回收(回收,发现要溢出,进行第二次回收的时候才回收软引用),如果这次回收还是没有足够内存,才抛出内存溢出异常。 jdk1.2后,通过SoftReference类来实现软引用
弱引用(Weak Reference):描述非必须的对象,比软引用弱,在下一次垃圾回收发生的时候,无论内存是否够用,都会被回收掉,jdk1.2后,通过WeakReference类来实现弱引用。
虚引用(Phantom Reference):又叫幽灵引用或者幻影引用,是最弱的一种引用关系,无法通过虚引用来取得一个对象实例,唯一作用是在对象被回收的时候能收到一个系统通知。PhantomReference类来实现
回收方法区
永久代,包括方法区,的垃圾回收,主要是两部分:废弃常量和无用的类
比如一个字符abc已经在常量池了,但是现在没有任何String引用了,就会被回收,其他类似的类(接口),方法,字段的引用也类似。
满足以下三个条件就是无用的类:
1,该类所有的实例都已经被回收,java堆中不存在任何该类的实例
2,加载该类的classLoader已经被回收
3,该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
在大量使用反射,动态代理,GCLib等byteCode框架,动态生成JSP以及OSGI这类频繁自定义classloader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
垃圾收集算法
标记-清除算法
最基础的收集算法(Mark-Sweep)。
算法分为标记和清除两个阶段;
1,标记出所有需要回收的对象,
2,统一回收所有被标记的对象
问题在于效率不高,以及有大量内存碎片。
但是很多算法都是以这个作为基础来优化的
复制算法
coping算法,解决效率问题
把内存化为相等的两块,每次只用其中一块,当一块满了,就把还存活的复制到空的一块去,再把已使用的内存一次性清理掉,这样每次都能直接回收一半区域,也不用去考虑碎片情况,代价是内存空间只剩下一半
商业虚拟机都是采用这种算法来回收新生代的,
新生代中98%的对象都是“朝生夕死”的,商业不需要按照1:1来划分内存空间,而是将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
当内存回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,然后清理掉Eden和刚才用过的Survivor空间。
HotSpot虚拟机默认的Eden和Survivor大小比是9:1。
也就是每次新生代中,只有10%的内存会被浪费。
如果Survivor没有足够的内存装下这一次新生代收集下来的存活对象,那么这些对象将直接跳过分配担保机制进入老年代。
分配担保:
标记-整理算法
老年代中,可能有大量的数据都是存活着的,所以,为了应对这个特点,就有了标记-整理算法
先标记死去的数据,然后让所有存活的对象都向一端移动,然后直接清理调端边界以外的内存。
(标记不要的丢掉,要的整上去)
分代收集算法
Generational Collection,
根据对象存活周期的不同,将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个代的特点采用最适当的收集算法。
比如新生代对象死的快,一般用复制算法,老年代对象存活率高,一般用标记-清除或者标记-整理算法。
垃圾收集器算法实现中的点
1,GCRoot**枚举根节点**,通过扫描,找到所有的根节点。但是这个动作很耗性能,所以就需要一个专门的地方去记录这些信息,在HotSpot中是一个OopMap来记录的,但是也没有办法记录全部的信息,因为太占空间了。
所以就需要每个线程在特定的位置,去记录自己的情况,这个位置就是安全点
2,安全点:safepoint, 程序并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。因为在这些安全点,记录了对象的内存信息,然后GC才能扫描到正确信息去做GC工作。
而安全点的产生,一般是在”长时间执行”的情况下,最明显的特征就是指令序列复用,列如方法调用,循环跳转,异常跳转等,具有这些功能的指令才会产生安全点。
感觉说白了就是,我被通知要GC了,要找安全点了,这时候我在方法调用的途中,那就暂停,赶紧去安全点存档,然后等GC结束再运行。
方式:GC的时候,设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志为true的时候,就把自己中断挂起,轮询标志的地点和安全点是重合的。
这里理解不是很明白
2,安全区,Safe Region类似安全点,比如一些已经挂起的程序,不会去轮询,所以就需要提前把自己放到安全区,
安全区是指,在一段代码片段之中,引用关系不会发生变化,在这个区域的任意地方开始GC都是安全的。
在线程执行到safe region中的代码时,首先标识自己已经进入了safe region,然后在这个期间,如果发生GC,就可以不用管安全点的事情了,等要离开安全区的时候,要先检查是不是系统正在发生GC,如果正在发生,那就要等待可以离开安全区的信号。
内存分配与回收策略
1,大多数情况下,对象在新生代Eden区中分配,当Eden没有足够空间进行分配时,虚拟机将发起一次Minor GC。
2,大对象直接进入老年代,比如很长的字符串或者数组。需要大量连续的内存空间的java对象,写程序的时候应该尽量避免大对象,以及朝生夕灭的短命大对象(因为这样会频繁在老年代GC啊,慢的嘞)
3,长期存活的对象将进入老年代,虚拟机给对象定义了age,一个对象在Eden出生并且经过一次Minor GC后存活,并且被移入Survivor时,年龄为1岁;对象在Survivor区中,每“熬过”一次Minor GC,年龄就增加1岁,到达一定程度时(默认15岁),就会被晋升到老年代。
4,动态对象年龄判断:如果在Survivor空间中,同年龄对象大小占据了过半的Survivor空间的时候,年龄大于等于这批对象的对象,就可以进入老年代,而不需要等待默认设置的年龄时间
打个比方,Survivor大小为2M,现在有2个同年龄的数据,都是1岁,分别大小是600K,和700K,加起来超过了1M,符合了过半原则,虽然年龄很小,但是占据空间太多了,所以直接在下次丢进老年代。
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现Major GC,经常会伴随着至少一次Minor GC(不绝对,取决于收集器类型)
Major GC的速度一般比Minor GC慢十倍以上。