JVM垃圾收集器与内存分配策略
文章目录
1)JVM垃圾收集器
2)HotSpot的算法实现
3)内存分配与回收策略
JVM垃圾收集器
- 对于Java堆和方法区,我们只有在程序运行期间才能知道会创建那些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的正是这部分内存。
- 判定对象是否存活(会回收哪些对象)
- 若一个对象不被任何对象或变量引用,那么他就是无效对象,需要被回收
- 引用计数法
- 为变量设置一个计数器,有一个变量指向它,则计数器加1,不再指向它,则计数器减1
- 实现简单效率高,但无法解决遇到对象互相引用的场景
- 可达性分析法
- 和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。
- 建立一个根节点,并构建一个引用图,当需要判断谁是垃圾时,从根节点进行遍历,没有遍历到的则是垃圾对象,否则就是有用对象
- 引用的种类
- 1.强引用
- 类似"Object obj = new Object()"
- 垃圾收集器永远不会回收被引用的对象,宁愿抛出OutOfMemoryError
- 2.软引用
- 内存不够会被回收
- 有用但非必需
- 3.弱引用
- 无论内存是否足够都会被回收
- 非必需
- 4.虚引用
- 在任何时候都可能被回收
- 1.强引用
- 垃圾收集算法 (如何回收对象)
- 1.标记-清除算法
- 先标记出所有需要回收的对象,然后统一回收所有被标记过的对象
- 容易产生大量不连续内存碎片,如果不连续的碎片过多的话,会导致一些大的对象存不进去
- 2.复制算法
- 将可用内存划分为两块,每次只使用其中的一块,当着一块快用完的时候,就会触发垃圾回收,把存活的对象全部复制到另一块内存中去,然后清理这块内存。
- 优化:HotShot虚拟机是默认按8:1的比例来分配的
- 3.标记-整理算法
- 把垃圾清除之后,会让存活的对象往一个方向靠拢,以此来整理碎片
- 4.分代收集算法
- 根据对象存活周期不同,将Java堆划分成新生代和老年代,并根据各个年代的特点采用最适当的收集算法
- 新生代:大批对象死去,只有少量存活。使用"复制算法",只需复制少量存活对象即可。
- 老年代:对象存活率高。使用"标记—清理算法"或者"标记—整理算法",只需标记较少的回收对象即可。
- 1.标记-清除算法
HotSpot的算法实现
- HotSpot如何发起回收
- 枚举根节点
- 主流Java虚拟机使用的都是准确式GC,在执行系统停顿之后无需检查所有执行上下文和全局的引用位置,而是通过一些办法直接获取到存放对象引用的地方,在HotSpot中是通过一组称为OopMap的数据结构来实现的,完成类加载后会计算出对象某偏移量上某类型数据、JIT编译时会在特定的位置记录栈和寄存器中是引用的位置。这样GC在扫描时就可直接得知这些信息,并快速准确地完成GC Roots的枚举。
- 安全点
- 安全点时可以让程序长时间执行的地方,如方法调用、循环跳转、异常跳转等具有指令序列复用的特征。
- 程序执行时并非在所有地方都停顿执行GC,只在到达安全点时才暂停
- 在安全点上停顿的方案
- 抢占式中断
- 主动式中断
- 安全区域
- 引用关系不会发生变化的一段代码片段,在安全区域中的任意地方开始GC都是安全的,可看做是扩展的安全点。
- 安全点机制只能保证程序执行时,在不太长的时间内遇到可进入GC的安全点,但在程序不执行时(如线程处于Sleep或Blocked状态)线程无法响应JVM的中断请求
- GC收集器 (具体的回收动作)
-
- Serial收集器
- ParNew
- Parallel Scavenge
- 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
- CMS收集器
- 标记-清除算法
- G1收集器
- 复制算法+标记-整合算法
- 枚举根节点
内存分配与回收策略
- 对象的内存分配,往大的方向说,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,则线程优先在TLAB(Thread Local Allocation Buffer,即线程本地分配缓存区)上分配。少数情况下会直接分配在老年代中,分配的规则不是百分百固定的。
- 内存分配规则
- 对象优先在Eden分配
- 大多数情况下对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时虚拟机将发起一次Minor GC
- Minor GC:回收新生代(包括 Eden 和 Survivor 区域),因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
- Major GC / Full GC: 回收老年代,出现了 Major GC,经常会伴随至少一次的 Minor GC,但这并非绝对。Major GC 的速度一般会比 Minor GC 慢 10 倍 以上。
- 大对象直接进入老年代
- 对于需要大量连续内存空间的Java对象(如很长的字符串以及数组),如果大于虚拟机设定的-XX:PretenureSizeThreshold参数值将直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。
- 长期存活的对象将进入老年代
- 虚拟机会给每个对象定义一个年龄计数器,当对象在Eden出生并经过第一次Minor GC后仍存活且能被Survivor容纳的话,将被移动到Survivor空间中并将对象年龄设为1;当对象在Survivor区中每“熬过”一次Minor GC年龄就+1,直至增加到一定程度(默认为15岁,可通过-XX: MaxTenuringThreshold设置)就会被晋升到老年代中。
- 动态对象年龄判定
- 为了能更好地适应不同程序的内存状况,虚拟机并不要求一定要达到-XX: MaxTenuringThreshold设置值才能晋升到老年代,当Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,那么年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保
- 在发生Minor GC之前虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,若是,说明可确保Minor GC是安全的,反之虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败;若允许,会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小;若大于,将尝试进行一次Minor GC,若小于或者不允许担保失败,将改为进行一次Full GC。
- 对象优先在Eden分配
- 内存分配规则
参考
《深入理解Java虚拟机》
内存分配与回收策略
要点提炼| 理解JVM之GC&内存分配