Java JVM 基础
Java JVM 基础
参考《深入理解Java虚拟机》
目录
Java 1.8
- 运行时数据区
- 垃圾收集算法
- 垃圾收集器
一、运行时数据区
1.1 程序计数器
- (Program Counter Register)【线程隔离】
- 可简单理解为当前线程所执行的字节码行号指示器;
- 如果是java方法,计数器记录指向虚拟机字节码指令地址;如果是native方法,计数器值为undefined;
- 【异常相关】唯一一个没规定OOM的区域。
1.2 虚拟机栈
- (VM Stack)【线程隔离】
- 其描述的是Java方法执行的内存模型:【重点】每个方法执行的过程都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法调用到执行结束,对应着一个栈帧在虚拟机栈中入栈到出栈的过程;
- 【异常相关】如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。方法每次调用都会创建一个栈帧,然后栈帧压栈,总不能无限压栈吧(初看没看懂,搜了下,发现也有人没看懂,心里平衡点);
- 【异常相关】大部分虚拟机栈都可以动态扩展,如果扩展时无法申请到足够的内存,就会OOM。
- 代码示例,见文末 code01.StackOverflowError
1.3 本地方法栈
- (Native Method Stack)【线程隔离】
- 与虚拟机栈的区别是,虚拟机栈执行java方法(字节码)服务;本非方法栈则为native方法服务;
- 【异常相关】StackOverflowError、OOM。
1.4 堆
- (Heap)【线程共享】
- GC堆(垃圾堆),是垃圾收集器管理的主要区域;
- java1.8之后【年轻代、老年代】。
- 代码示例,见文末 code02.OOM-heap
1.5 元数据区
- (Metaspace)【线程共享】
- jvm config example:
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=50m
- 元数据区取代了永久代,本质上都是方法区的实现,用来存放虚拟机加载的类信息、常量、静态变量、JIT编译后的代码。
- 代码示例,见文末 code03.OOM-metaspace
二、垃圾收集算法
对象在否?引用计数算法、可达性分析算法
可达性分析
通过一系列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/Reference Chain,
当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 因此也会被判定为可回收的对象。在java中可做GC Roots的对象包括:
- 方法区: 类静态属性引用的对象;
- 方法区: 常量引用的对象;
- 虚拟机栈(本地变量表)中引用的对象.
- 本地方法栈JNI(Native方法)中引用的对象。
即使在可达性分析算法中不可达的对象,VM也并不是马上对其回收,至少还要经历两次标记过程:
- 第一次是在可达性分析后发现没有与GC Roots相连接的引用链
- 第二次是GC对在F-Queue执行队列中的对象进行的小规模标记(对象需要覆盖finalize()方法且没被调用过)
标记-清除算法(先标记,再清除,清除后空间不连续,产生大量内存碎片)
复制算法
- 年轻代:Eden : Survivor = 1:8,会有10%的内存“闲置”;
- 每次GC后,存活的对象都会放在剩余的10%内存中,也就是To Survivor;
- 当然,如果剩余的10%内存不够用呢,就需要依赖老年代进行分配担保。
标记-整理算法
- 如果对象存活率较高,那么复制算法就不好用了;
- 标记-清除算法之后,将所有存活的对象都向一端移动,然后清理掉边界以外的内存。
分代收集算法
- 典型就是分为新生代和老年代
- 新生代,存活率低,就使用复制算法
- 老年代,存活率高,并且没有额外的空间做担保,所以使用“标记-清除”或者“标记-整理”算法
三、垃圾收集器
3.1 新生代收集器 Serial [jdk 1.3]
- 【单线程】历史悠久,新生代收集器,复制算法;
- GC时要STW,直到GC完成(你妈妈在打扫卫生,你一边乱扔纸屑,所以必须STW,你得老老实实坐着);
- Client 模式下默认的新生代收集器(与其他单线程收集器相比,简单高效)。
3.2 新生代收集器 ParNew
- 【并行多线程】新生代收集器,复制算法,Serial收集器的多线程版本;
- 单CPU下,不会比Serial好;甚至双CPU都不能100%超越Serial;
- Server模式下首选新生代收集器,重要原因是,他能和CMS(真正意义上的并发收集器)配合工作。
3.3 新生代收集器 Parallel Scavenge [jdk 1.4]
- 【Throughput】吞吐量优先
- 【并行多线程】新生代收集器,复制算法;
- 关注点不一样,目标为可控的吞吐量(Throughput),其他的关注点是尽可能缩短GC STW时间;
- 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间)
3.4 老年代收集器 Serial Old [jdk 1.5]
- 【单线程】老年代收集器,标记 - 整理算法;
- 主要用户Client 模式下虚拟机;Server模式下,1. JDK1.6以前与PS搭配使用;2. CMC收集器后备方案。
3.5 老年代收集器 Parallel Old [jdk 1.6]
- 【Throughput】吞吐量优先
- 【并行多线程】老年代收集器,标记 - 整理算法;
- jdk1.6以前,如果选了PS,就不能选CMS了,只能选Serial Old;
- 吞吐量优先第一组合。
3.6 老年代收集器 CMS [jdk 1.5]
- 【并发多线程】老年代收集器,基于标记 - 清除(初始 & 并发 & 重新 标记,并发清除);
- 关注点不一样,目标为可控的吞吐量(Throughput),其他的关注点是尽可能缩短GC STW时间;
- 并发低停顿;缺点:CPU资源非常敏感、无法处理浮动垃圾、基于标记清除多碎片。
3.7 G1收集器 [jdk 1.6预览版, 1.7第一版, 1.8全功能完成, 1.9作为默认GC ]
- 【并行与并发】新生代+老年代收集器,整体基于标记 - 整理,局部(两个Region之间)基于复制算法;
- 消除新生代和老年代的物理隔离,将堆区划分为许多相同大小的Region单元;
- 使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
垃圾收集器总结
新生代收集器
- Serial 单线程 [复制算法]
- ParNew 并行多线程 [复制算法]
- Parallel Scavenge 并行多线程 吞吐量优先 [复制算法]
老年代收集器
- Serial Old 单线程 [标记-整理算法]
- Parallel Old 并行多线程 吞吐量优先 [标记-整理算法]
- CMS 并发多线程 [基于标记-清除](内存碎片多)真正意义上的并发收集器
全区域收集器 —— G1收集器 并发多线程 [复制算法 / 标记-整理算法]
- 全区域回收,替代CMS。同样用卡表处理跨代指针,但设计比CMS更加复杂,牺牲部分内存;
- 当然,小内存服务器上现在的CMS还是比较适合的,当然随着HotSpot的开发者对G1的不断优化,这个内存的要求可能会更小点;
- 在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间。