java垃圾收集算法

32次阅读

共计 2533 个字符,预计需要花费 7 分钟才能阅读完成。

基础背景
运行时数据区域
虚拟机结构图

程序计数器:
每个线程独有一份, 用作记录编译后的 class 文件行号

虚拟机栈: 以栈帧为单位存放局部变量.
Native 方法栈: 和虚拟机栈类似, 不过,一个本地方法是这样一个方法:该方法的实现由非 java 语言实现,比如 C 语言实现。很多其它的编程语言都有这一机制,比如在 C ++ 中,你可以告知 C ++ 编译器去调用一个 C 语言编写的方法
方法区: 运行时常量池, 存放编译器的字面量和符号引用, 也可以在运行时动态加入.
java 堆: 存放对象的实例, 是垃圾回收的主战场,

创建一个对象

比如执行 new MyClass();
去常量池中寻找, 查看类是否被加载. 如果没加载, 则加载 class.
在 java 堆中分配内存空间, 方式有以下两种:

指针碰撞: 把指针向空闲对象移动与对象占用内存大小相等的距离, 使用的收集器有 Serial、ParNes 等
空闲列表: 虚拟机维护一个列表, 记录可用的内存块, 分配给对象列表中一块足够大的内存空间, 使用的收集器有 CMS 等.
如何分配内存, 由垃圾回收器决定.

内存的具体分配过程中有同步和预留空白区的方式
内存分配好后, 再执行 init()方法, 初始化实例.

对象头
对象头主要记录对象的 hashcode,GC 标记,元数据地址,以及关于对象锁的使用,年龄代,偏向线程等。

hash 用于快速寻找对象
对象头大小 32bit/64bit, 由虚拟机决定
实例数据区的数据类型, 按照相似放在一起.

对象中的访问定位
方式
句柄池

句柄池从堆中划分
由实例地址和类型数据地址构成

指针
可直接通过指针访问到实例对象
优劣
句柄的使用, 方便了实例位置的改变, 可以不改变引用, 但是访问速度相对于指针低一些.
JVM 垃圾回收
判断可否回收的算法
1. 引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器都为 0 的对象就是不再被使用的,垃圾收集器将回收该对象使用的内存。
可达性分析算法:
通过一系列的名为 GC Root 的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链, 当一个对象到 GC Root 没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。实例的位置:

java 虚拟机栈 (栈帧中的本地变量表) 中的引用的对象。
方法区中的类静态属性引用的对象。
方法区中的常量引用的对象。
本地方法栈中 JNI 本地方法的引用对象。

不可达对象到死亡还需要两次标记, 第一次, 标记后进入 F -Queue 队列, 第二次标记时只有 finalize()中有拯救自己的方法的实例才能自救成功, 比如将自己应用给其它变量.
垃圾回收算法
方法区的回收
常量池的回收
没有被引用, 即可被回收
class 对象回收

所有实例都被回收
所有 classLoader 都被回收
java.lang.class 对象没有被任何地方引用, 即无法在任何地方使用反射访问类.
最终是否被回收, 还得看 JVM 参数配置

java 堆回收算法

标记清除算法: 先标记判定, 再一次性清除.
产生了大量碎片, 且效率低下

复制算法: 把可用内存划分为两块, 一块用完后, 就将活下来的实例放到另一块内存区.
优缺点: 没有了碎片化问题, 但内存大小减少了一半

标记整理算法: 在标记 - 清除算法基础上做了改进,标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。
标记 - 整理算法相比标记 - 清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记整理算法效率会大大提高。

分代收集算法: 根据内存中对象的存活周期不同,将内存划分为几块,java 的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。现在的 Java 虚拟机就联合使用了分代复制、标记 - 清除和标记 - 整理算法.java 虚拟机垃圾收集器关注的内存结构如下:

新生代
研究表明,新生代中 98% 的对象是朝生夕死的短生命周期对象,所以不需要将新生代划分为容量大小相等的两部分内存,而是将新生代分为 Eden 区,Survivor from 和 Survivor to 三部分,其占新生代内存容量默认比例分别为 8:1:1,其中 Survivor from 和 Survivor to 总有一个区域是空白,只有 Eden 和其中一个 Survivor 总共 90% 的新生代容量用于为新创建的对象分配内存,只有 10% 的 Survivor 内存浪费,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的 Survivor 内存区域中,Eden 和非空白的 Survivor 进行标记 - 清理回收,两个 Survivor 区域是轮换的。
年老代

年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记 - 整理垃圾回收算法。
Java 虚拟机对年老代的垃圾回收称为 MajorGC/Full GC,次数相对比较少,每次回收的时间也比较长。

堆分配和回收策略
分配

优先在 Eden 上分配, 空间不足, 虚拟机发起 minor GC.
大对象直接进入老年代, 防止折磨新生代空间.[参数设置 -XX:PretrnureSizeThreshold=[字节数]]
长大后的对象进入老年代, 在 survivor 中熬过一次, 就长一岁,15 岁时就进入老年代[阈值设置 -XX:MaxTenuringThreshold=[岁数]]
相同年龄的对象, 若大于或等于空间的一半, 也直接进入老年代.

附:JVM 参数整理
参数调优建议

永久代:-XX:PermSize20M -XX:MaxPermSize20M

堆大小: -Xms20M -Xmx20M

新生代: -Xmn10M

Eden 与 survior 比率: -XX:SurvivorRation=8

让大对象直接进入老年代: -XX:PretrnureSizeThreshold=1B

年龄阈值:-XX:MaxTenuringThreshold=15

日志命令: 参考连接

正文完
 0