第九周作业
垃圾收集器和 java 的内存分布有着紧密的联系,因此我们要对 jvm 的内存布局回顾一下,jvm 的内存布局大致分为如下:
JVM 运行时数据区域-例子
生成了 2 个部分的内存区域,1)obj 这个引用变量,因为是方法内的变量,放到 JVM Stack 里面,2)真正 Object class 的实例对象,放到 Heap 里面。
上述的 new 语句一共消耗 12 个 bytes,JVM 规定引用占用 4 个 bytes(在 JVM stack),而空对象是 8 个 bytes(在 Heap)
方法结束后,对应 Stack 中的变量马上回收,但是 Heap 中的对象要等到 GC 来回收。
垃圾判断算法
引用计数算法(Reference Counting)
给对象添加一个引用计数器,当有一个地方引用它,计数器加 1,当引用失效,计数器减一,任何时刻计数器为 0 的对象就是不可能再被使用的。
引用计数器算法无法解决对象循环引用的问题。
上图所示,A 引用 B,B 引用 A,但是外界引用都断了,不存在对“环”的引用,但是他们的引用计数器都是 1,引用计数算法就不会对他们进行回收,会一直在内存中。
根搜索算法(Root Tracing)
在实际的生产语言中(java、C#等)都是使用根搜索算法判定对象是否存活
算法基本思路 就是通过一系列的称为“GC Roots”的点作为起始进行向下搜索,当一个对象到 GC Roots 没有任何引用链(
Reference Chain)相连,则证明此对象是不可用的
在 java 语言中,GCRoots 包括
在 JVM 栈(帧中的本地变量)中的引用
方法区中的静态引用
JNI(即一般说的 Native 方法)中的引用
方法区
java 虚拟机规范表示可以不要求虚拟机在这区实现 GC,这区 GC 的“性价比”一般比较低。
在堆中,尤其是在新生代,常规应用进行一次 GC 一般可以回收 70%-95%的空间,而方法区的 FC 效率远小于此。
当前的商业 JVM 都有实现方法区的 GC,主要回收两部分内容:废弃常量与无用类。
类回收需要满足如下三个条件
该类所有的实例都已经被 GC,也就是 JVM 中不存在该 Class 的任何实例
加载该类的 ClassLoader 已经被 GC
该类对应的 java.lang.Class 对象没有在任何地方呗引用,如不能在任何地方通过反射访问该类的方法。
在大量使用反射、冬天代理、CGlib 等字节码框架、动态生成 jsp 以及 OSGI 这类频繁自定义一 ClassLoader 的场景都需要
JVM 具备类卸载的支持以保证方法区不会溢出。
GC 算法
标记 -清楚算法(Mark-Sweep)
标记-整理算法(Mark-Compact)
复制算法(Copying)
分代算法(Generational)
标记 -清楚算法(Mark-Sweep)
算法分为“标记”和”清除” 两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象。
缺点
效率问题,标记和清理两个过程效率都不高
空间问题,标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前
触发另一次的垃圾搜索集动作。
最后阴影部分被回收掉,F、J、M 不能被 root trace 到,因此也会被回收。
效率不高,需要扫描所有对象,堆越大,GC 越大。碎片越严重。
存在内存碎片问题,GC 次数越多,
复制(Coping)搜集算法
将可用内存划分为两块,每次只是用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来的整块内存空间一次性清理掉
这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针、按顺序分配内存
就可以了,实现简单,运行效率高。
只是这种算法的代价是将内存缩小为原来的一半,代价昂贵。
现在的商业虚拟机中都是用了这一种收集算法回收新生代。
将内存分为一块较大的 eden 空间和快较少的 survival 空间,每次使用 eden 和其中一块 survivor,当回收时将 eden 和 survivor 还存活的对象一次性
拷贝到另外一块 survival 空间上,然后清理掉 eden 和用过的 survivor:
Oracle Hotspot 虚拟机默认 eden 和 survivor 的大小比例是 8:1,也就是每次只有 10% 的内存是”浪费”的。
复制收集算法在对象存活率高的时候,效率有所下降。
如果不想浪费 50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都有 100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
只需要扫描存活的对象,效率更高
不会产生碎片
需要浪费额外的内存作为复制区
复制的算法非常适合生命周期比较短的对象,每次 GC 总能回收大部分的对象,复制的开销比较小
根据 IBM 的专门研究,98%的 java 对象只会存活一个 GC 周期,对这些对象很适合用复制算法。而且不用 1:1 的划分工作区和复制区的空间
标记整理算法(Mark-Compact)
标记过程仍然一样,但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存。
没有内存碎片
比 Mark-Sweep 耗费更多的时间进行 compact
分代收集算法(Genaratinal Collecting)
当前商业虚拟机的垃圾收集都是采用“分代收集”算法根据对象不同存活周期将内存划分为几块
一般是把 java 堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,譬如新生代每次 GC 都有大批量对象死去,
只有少量存活,那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集。
综合前面几种 GC 算法的优缺点,针对不同生命周期的对象采用不同的 GC 算法。
Hotspot JVM 6(jdk8 之前)中共划分为三个代:年轻代(Ypung Generation)\老年代(Old Generation)和永久代(Permanent Generation)
年轻代
新生代的对象都放在新生代,年轻代用复制算法进行 GC(理论上年轻代的对象的生命周期非常短,所以适合复制算法)
年轻代分三个区。一个 Eden 区,两个 Survivor 区(可以通过参数设置 Survivor 个数)。对象在 Eden 区中生成,当 Eden 区满时,
此区的存活对象被复制到另外一个 survivor 区,当第二个 Survivor 区也满了的时候,从第一个 Survivor 区复制过来的并且此时还存活的对象,将被复制到老年代,2 个 Survivor
是完全对称,轮流替换。
Eden 和 2 个 Survivor 的缺省比例是 8:1:1,也就是 10%的空间会被浪费,可以根据 GC log 的信息调整大小的比例。
老年代
存放了经过一次或多次 GC 还存活的对象
一般采用 Mark-Sweep 或者 Mark-Compact 算法进行 GC
有多种垃圾收集器可以选择。每种垃圾收集器可以看作一个 GC 算法的具体实现。可以根据具体应用的需求选用合适的垃圾收集器
(追求吞吐量?追求最短的响应时间?)
永久代
比不属于堆(Heap)但是 GC 也会涉及到这个区域
存放了每个 Class 的结构信息,包括常量池、字段描述、方法描述、与垃圾收集要收集的对象关系不大。
内存分配
堆上分配
大多数情况在 eden 上分配,偶尔会直接在 old 上分配
栈上分配
原子类型的局部变量
内存回收
GC 要做的是将那些 dead 的对象所占用的内存回收掉 hotspot 认为没有引用的对象是 dead 的 hotspot 将引用分为四种:Strong、Soft、Week、PhantomStrong 即默认通过 Object o = newObject()这种方式赋值的引用‘Soft、Week、Phantom 这三种则都是继承 Reference
在 Full GC 时会对 Reference 类型的引用进行特殊处理 Soft:内存不够时一定会被 GC、长期不用也会被 GCWeek:一定会被 GC,当被 mark 为 dead,会在 ReferenceQueue 中通知 Phantom:贝莱就没有引用,当 jvm heap 中释放时会通知
###垃圾收集算法
GC 的时机
在分代模型的基础上,GC 从时机上分为两种:scavenge GC 和 Full GC
Scavenge GC(Minor GC)触发时机:新对象生成时,Eden 空间满了理论上 Eden 区大多数对象会在 Scavenge GC 回收,复制算法的执行效率会很高,Scavenge GC 时间比较短。
Full GC 对整个 JVM 进行整理,包括 Young、Old 和 Perm 主要的触发时机:1)、old 满了 2)Perm 满了 3)system.gc()效率很低,尽量减少 Full GC。
评论