《深入理解java虚拟机》第三章 垃圾收集器与内存分配策略

时间:2022-12-27 15:28:06
第三章 垃圾收集器与内存分配策略
3.1 概述
  1. 哪些内存需要回收
  2. 何时回收
  3. 如何回收
程序计数器、虚拟机栈、本地方法栈3个区域随线程而生灭。 java堆和方法区的内存需要回收。
 
3.2 对象已死吗
  什么时候回收内存?
 
3.2.1 引用计数法
给对象中添加一个引用计数器,有地方引用时,计数器加1;当引用失效时,计数器减1。任何时刻计数器为0时的对象就是不可能再被使用的了。
存在问题:对象间的循环引用。  虚拟机不是通过这种方法判断对象是否存活。
 
3.2.2 可达性分析算法
通过一系列"GC Roots"对象作为起始点,从这些节点向下搜索,走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,从GC Roots到这个对象不可达),证明此对象不可用。
java中可以作为GC Roots的对象包含:
  1. 虚拟机栈(栈帧中的本地变量表)引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即一般说的native方法)引用的对象
3.2.3 再谈引用
JDK 1.2之后,引用扩充为
强引用:程序代码中普遍存在的,类似“Object obj = new Object()”这类的引用。只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。
软引用:还有用但非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中进行第二次回收。
弱引用: 只能存活到下一次垃圾收集发生之前。
虚引用:唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
 
3.2.4 生存还是死亡
宣告一个对象的死亡,至少要经历两次标记过程:如果对象与GC Roots没有引用链,它会被第一次标记并进行筛选,筛选的条件是此对象是否有必要执行finalize()方法。如果对象没有覆盖finalize()方法或者虚拟机已经执行过finalize()方法,虚拟机将都视为“没有必要执行”。如果对象在被判断有必要执行finalize()方法,则会被加入一个F-Queue的对队列中,稍后由一个由虚拟机创建的、低优先级的Finalizer线程去执行队列中的对象的finalize()方法。 对象可以在finalize()中拯救自己,关联一个对象。否则就真被清除了。注意:一个对象的finalize()方法只能被系统自动调用一次。尽量避免使用finalize()方法。
 
3.2.5 回收方法区
永久代的垃圾回收主要有两部分:废弃常量和无用的类。
无用的类要满足以下条件,就“可以“回收
  1. 该类所有的实例都已经回收,也就是java堆中不存在该类的实例。
  2. 加载该类的ClassLoader已经被回收
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。
 
 
3.3 垃圾收集算法 
《深入理解java虚拟机》第三章 垃圾收集器与内存分配策略《深入理解java虚拟机》第三章 垃圾收集器与内存分配策略《深入理解java虚拟机》第三章 垃圾收集器与内存分配策略《深入理解java虚拟机》第三章 垃圾收集器与内存分配策略
1. 新生代(Young Generation):也有叫做年轻代的,这里使用《深入理解JAVA虚拟机》中的叫法,下同。
其实看名称就能看出一些,一般情况下,新创建的对象都会存放到新生代中(大对象除外)。
新生代中对象的特点是:很快就会被GC回收掉的或者不是特别大的对象。
为了方便垃圾收集,新生代又分出了一个Eden区,两个 Survivor区。
JVM 每次只会使用 Eden区 和其中的一块 Survivor 区域来为对象服务,另一块Survivor区域是空的,用于垃圾回收。
举个例子,第一次回收的时候,虚拟机会将 Eden区+Survivor(from)区域的存活对象复制到Survivor(to)上(存活对象小于Survivor(to)的空间),清空Survivor(from),虚拟机使用Eden区+Survivor(to);
第二次回收的时候,虚拟机再将Eden区+Survivor(to)存活的对象复制到Survivor(from)。
这三个区域默认情况下是按照8:1:1分配,也可以手动配置。
2. 老年代(Old Generation):在新生代每进行一次垃圾收集后,就会给存活的对象“加1岁”,当年龄达到一定数量的时候就会进入老年代(默认是15,可以通过-XX:MaxTenuringThreshold来设置)。
另外,比较大的对象也会进入老年代,可以-XX:PretenureSizeThreshold进行设置。
如-XX:PretenureSizeThreshold3M,那么大于3M的对象就会直接就进入老年代。
因此,老年代中存放的都是一些生命周期较长的对象或者特别大的对象。
3. 永久代(Permanent Generation ):即JVM的方法区。在这里存放着一些被虚拟机加载的类信息(别忘了还有动态生成的类)的静态文件,这就导致了这个区中的东西比老年代和新生代更不容易回收。
永久代大小通过-XX:MaxPermSize=<N>进行设置。
4. 元空间(Metaspace):从JDK 8开始,Java开始使用元空间取代永久代,元空间并不在虚拟机中,而是直接使用本地内存。
那么,默认情况下,元空间的大小仅受本地内存限制。当然,也可以对元空间的大小手动的配置。

 

 
3.3.1 标记-清除算法
首先标记所有需要回收的对象,在标记统一完成后回收所有标记的对象。 不足:1 标记和回收的效率都不高 2标记清除后会产生大量不连续的内存碎片,碎片太多可能会导致以后再重新运行时需要分配较大对象后,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
 
3.3.2 复制算法
它将可用的内存按容量分为大小相等的两块,每次只使用其中的一块。当这块内存用完时,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
一块较大的Eden 和两块较小的Survivir。分配担保。
 
3.3.3标记-整理算法
同标记清除算法,但是在清除时让所有存活的对象都向一端移动,然后清理掉边界以外的内存。
 
3.3.4分代收集算法
根据对象存活周期不同将内存划分为几块,一般是把java堆分为新生代和老年代。在新生代中,每次垃圾收集时都发生大批对象死去,就选用复制算法。在老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理 或标记-整理算法。
 
3.4 HotSpot的算法实现
3.4.1 枚举根节点
GC进行时必须停顿所有java执行线程,这件事被称为“Stop the World”。
3.4.2安全点
程序执行时,只有在安全点才能暂停开始GC。以及如何让线程都跑到安全点再停顿。分为抢占式中断和主动式中断。
1.抢占式。现在基本没有使用抢占式。 所有线程停止,如果没到安全点则继续执行到安全点。
2.主动式。设置一个标记,各个线程执行时主动轮询这个标记,发现中断标记为真时就自己中断挂起,轮询标记的地方和安全点是重合的。
3.4.3 安全区域
安全点的扩充。安全区域是指一段代码中,引用关系不会发生改变,区域中任何一点开始GC都是安全的。
 
3.5 垃圾收集器 举例+ CG日志分析
 
3.6 内存分配与回收机制
对象的内存分配,往大了说就是在堆上分配,对象主要分配在新生区Eden区上,如果启动了本地线程分配缓存,将按线程优先在TLAB上分配。少数情况下可能会直接分配到老年代中。
 
3.6.1对象优先在Eden分配
大多数情况下,对象在Eden区中分配,当Eden区中没有足够空间进行分配时,虚拟机会发生一次Minor GC.
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现Major GC,经常会伴随最少一次的minor gc。一般比前者速度慢10倍以上
 
3.6.2 大对象直接进入老年代
所谓大对象是指,需要大量连续内存空间的java对象,最典型大对象就是很长的字符串以及数组。目的:避免大量复制回收内存。有些回收器可以设置大对象最大内存上限标准。
 
3.6.3长期存活的对象将进入老年代
每一个对象定义了一个对象年龄计数器,对象在Eden区域出生并且经历一次minor GC存活,且能被survivor 容纳,则年龄为1,以后每熬过一次minor GC,年龄加一,超过一定值(默认15)会被移至老年代。
 
3.6.4 动态对象年龄判断
如果在survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代,无需等待MaxTenuringThreshold中的要求。
 
3.6.5分配担保
如果分配担保失败,进行一次Full GC。