JVM内存举例说明
public void method() {
Object object = new Object();
/*生成了2部分的内存区域,1)object这个引用变量,因为
是方法内的变量,放到JVM Stack里面,2)真正Object
class的实例对象,放到Heap里面
上述 的new语句一共消耗12个bytes, JVM规定引用占4
个bytes (在JVM Stack), 而空对象是8个bytes(在Heap)
方法结束后,对应Stack中的变量马上回收,但是Heap
中的对象要等到GC来回收、*/
}
jvm 垃圾回收(GC)模型
垃圾判断算法
引用计数算法(Reference Counting)
给对象添加一个引用计数器,当有一个地方引用它,计数器加 1,当引用失效,计数器减 1,任何时刻计数器为 0的对象就是不可能再被使用的。
引用计数算法无法解决对象循环引用问题(A、B 相互引用)
根搜索算法(Root Tracing)
在实际的生产语言中(Java、 C# 等),都是使用根搜索算法判定对象是否存活。
算法基本思路就是通过一系列的称为 "GCRoots" 的点作为起始进行向下搜索,当一个对象到 GCRoots 没有任何引用链 (Reference Chain) 相连,则证明此对是不可用的
在Java语言中,GC Roots 包括
- 在VM栈(帧中的本地变量)中的引用
- 方法区中的静态引用
- JNI (即一般说的Native方法) 中的引用
方法区
- Java虛拟机规范表示可以不要求虚拟机在这区实现 GC,这区 GC 的“性价比”一般比较低
- 在堆中,尤其是在新生代,常规应用进行 I 次 GC 一般可以回收 70%~95% 的空间,而方法区的 GC 效率远小于此
- 当前的商业 JVM 都有实现方法区的 GC,主要回收两部分内容:废弃常量与无用类
主要回收两部分内容:废弃常量与无用类
类回收需要满足如下3个条件:
- 该类所有的实例都已经被 GC, 也就是 JVM 中不存在该 Class 的任何实例
- 加载该类的 ClassLoader 已经被 GC (类加载器与类加载器加载的对象之间是双向引用的,要想 Class 被回收, ClassLoader 也要被回收)
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法
在大量使用反射、动态代理、CGLib 等字节码框架、动态生成 JSP 以及 OSGi 这类频繁自定义 Classloader 的场景都需要 JVM 具备类卸载的支持以保证方法区不会溢出
GC 算法
- 标记-清除算法(Mark-Sweep)
- 标记-整理(压缩)算法(Mark-COmpact)
- 复制算法(Copying)
- 分代算法(Generational)
标记一清除算法(Mark-Sweep)
算法分为“标记”和“清除”两个阶段, 首先标记出所有需要回收的对象,然后回 收所有需要回收的对象
缺点
- 效率问题,标记和清理两个过程效率都不高
- 空间问题, 标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次的垃圾搜集动作
效率不高,需要扫描所有对象。堆越大,GC 越慢 存在内存碎片问题。GC 次数越多,碎片越为严重
复制(Copying) 搜集算法
将可用内存划分为两块,每次只使用其中的一块,当一半区内存用完了,仅将还存活 的对象复制到另外一块上面,然后就把原来整块内存空间一次性清理掉,
这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效。
只是这种算法的代价是将内存缩小为原来的一半,代价高昂
现在的商业虚拟机中都是用了这一种收集算法来回收新生代(刚 new 出来的对象实例,位于新生代,当在新生代经过几轮垃圾回收,还未被回收的进入老年代)
将内存分为一块较大的 eden 空间和 2 块较少的 survivor 空间,每次使用 eden 和其中一块 survivor, 当回收时将 eden 和 survivor 还存活的对象一次性拷贝到另外一块 survivor 空间上,然后清理掉 eden 和用过的 survivor
Oracle Hotspot 虚拟机默认 eden 和 survivor 的大小比例是 8:1,也就是每次只有 10% 的内存是“浪费”的
复制收集算法在对象存活率高的时候,效率有所下降
如果不想浪费50%的空间,就需要有额外的空间进行分配担保用于应付半区内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法
- 只需要扫描存活的对象,效率更高
- 不会产生碎片
- 需要浪费额外的内存作为复制区
- 复制算法非常适合生命周期比较短的对象,因为每次GC总能回收大部分的对象,复制的开销比较小
- 根据 IBM 的专门研究,98% 的 Java 对象只会存活 1 个 GC 周期,对这些对象很适合用复制算法。而且 不用 1 : 1 的划分工作区和复制区的空间
标记一整理(压缩)(Mark-Compact)算法
标记过程仍然一样,但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存。
没有内存碎片
比Mark-Sweep耗费更多的时间进行compact
分代收集。( GenerationalCollecting)算法
当前商业虚拟机的垃圾收集都是采用“分代收集”( Generational Collecting)算法,根据对象不同的存活周期将内存划分为几块。
一般是把 Java 堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,譬如新生代每次GC都有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本,就可以完成收集。
综合前面几种 GC 算法的优缺点,针对不同的生命周期的对象采取不同的 GC 算法
Hotspot JVM 6中共划分为三个代: (举个例子而已,已经过时了)
- 年轻代(Young Generation)
- 老年代(Old Generation)和
- 永久代( Permanent Generation
年轻代
年轻代(Young Generation) 新生成的对象都放在新生代。年轻代用复制算法进行 GC (理论上年轻代对象的生命周期非常短,所以适合复制算法)
年轻代分三个区。一个Eden区,两个Survivor区(可以通过参数设置Survivor个数)。对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到一个Survivor区,当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当第二个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制到老年代。2个Survivor是完全对称,轮流替换。
Eden和 2 个 Survivor 的缺省比例是 8:1:1,也就是 10% 的空间会被 浪费。可以根据 GC log 的信息调整大小的比例
老年代
- 老年代(Old Generation)存放了经过一次或多次GC还存活的对象
- 一般采用 Mark-Sweep 或者 Mark-Compact 算法进行 GC
- 有多种垃圾收集器可以选择。每种垃圾收集器可以看作一个 GC 算法的具体实现。可以根据具体应用的需求选用合适的垃圾收集器(追求吞吐量 ? 追求最短的响应时间? )
永久代
- 并不属于堆(Heap).但是GC也会涉及到这个区域
- 存放了每个Class的结构信息,包括常量池、字段描述、方法描述。与垃圾收集要收集的Java对象关系不大
内存分配
- 堆上分配
大多数情况在 eden 上分配,偶尔会直接在 old 上分配
细节取决于GC的实现
- 栈上分配
原子类型的局部变量
内存回收
GC要做的是将那些dead的对象所占用的内存回收掉
- Hotspot认为没有引用的对象是dead的
- Hotspot将引用分为四种: Strong、 Soft、Weak(弱引用)、Phantom(虚引用)
- Strong 即默认通过Object o= new Object()这种方式赋值的引用
- Soft、Weak、 Phantom这 三种则都是继承 Reference
在Full GC时会对Reference类型的引用进行特殊处理
- Soft:内存不够时一定会被GC、长期不用也会被GC
- Weak: 一定会被GC, 当被 mark 为 dead, 会在ReferenceQueue中通知
- Phantom: 本来就没引用,当从jvm heap中释放时会通知
垃圾收集算法
GC 的时机
- 在分代模型的基础上,GC从时机上分为两种: Scavenge GC和Full GC
- Scavenge GC (Minor GC)
- 触发时机:新对象生成时,Eden空间满了
- 理论上 Eden 区大多数对象会在 ScavengeGC 回收,复制算法的执 行效率会很高,ScavengeGC 时间比较短。
- Full GC
- 对整个 JVM 进行整理,包括 Young、Old 和 Perm
- 主要的触发时机: 1) Old满了2) Perm满了3) system.gc()
- 效率很低,尽量减少Full GC。
垃圾回收器(Garbage Collector)
- 分代模型: GC的宏观愿景;
- 垃圾回收器: GC的具体实现
- Hotspot JVM提供多种垃圾回收器,我们需要根据具体应用的需要采用不同的回收器
- 没有万能的垃圾回收器,每种垃圾回收器都有自己的适用场景
垃圾收集器的‘并行”和并发
- 并行(Parallel):指多个收集器的线程同时工作,但是用户线程处于等待状态 8 并发(Concurrent):指收集器在工作的同时,可以允许用户线程工作。
- 并发不代表解决了GC停顿的问题,在关键的步骤还是要停顿。比如在收集器标记垃圾的时候。但在清除垃圾的时候,用户线程可以和GC线程并发执行。
Serial收集器
- 最早的收集器,单线程进行GC,(收集时会暂停所有工作线程,Stop The World,STW)
- New和Old Generation都可以使用,在新生代,采用复制算法, 在老年代,采用Mark-Compact算法
- 因为是单线程GC,没有多线程切换的额外开销,简单实用
- Hotspot Client模式默认的新生代收集器
ParNew收集器
- ParNew收集器就是Serial的多线程版本,除了使用多个收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Seria收集器一模一样。
- 对应的这种收集器是虚拟机运行在Server模式的默认新生代收集器,在单CPU的环境中,ParNew收集器并不会比Serial收集器有更好的效果
- Serial收集器在新生代的多线程版本
- 使用复制算法(因为针对新生代)只有在多CPU的环境下,效率才会比Serial收集器高
- 可以通过-XX:ParallelGC Threads来控制GC线程数的多少。需要结合具体CPU的个数
- Server模式下新生代的缺省收集器
Parallel Scavenge收集器
Parallel Scavenge 收集器也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与 ParNew 收集器有所不同,它是以吞吐量最大化(即 GC 时间占总运行时间最小)为目标的收集器实现,它允许较长时间的 STW 换取总吞吐量最大化