------------------------------------------深入理解Java虚拟机------------------------------------------
java内存区域划分
1. 程序计数器
- 当前线程所执行的字节码行号指示器
- 每个线程都有独立的程序计数器
- 如果执行的是java方法,记录字节码指令地址。如果执行Native方法,则为空(Undefined)
- 不会有OutOfMemoryError出现
2. 虚拟机栈
- 线程私有, 生命周期与线程相同
- 描述java方法执行的内存模型:每个方法都会创建一个栈帧用于存储局部变量,方法出口,操作数栈等信息。每个方法调用对应一个栈帧在虚拟机栈中入栈到出栈的过程
- 存放基本数据类型(8种)和对象引用类型(地址的指针或者对象的句柄)
- 请求栈深度大于虚拟机允许深度时,抛出StackOverflowError异常
- 如果动态扩展仍无法申请足够的内存,抛出OutOfMemoryError异常
3. 本地方法栈
- 作用和虚拟机栈一样
- 区别为:本地方法栈服务虚拟机使用到的Native方法
4. 堆
- 虚拟机管理的内存最大的一块
- 被所有线程共享的区域
- 所有对象的实例在此分片内存
- 可细分为多个代
5. 方法区
- 所有线程共享的区域
- 存储类信息,常量,静态变量
- 在HotSpot虚拟机上也称永久代
- 垃圾收集行为很少出现在这个区域,因为可回收的内存很少
6. 运行时常量池
- 是方法区的一部分
- 用于存放编译器生成的各种字面量和符号引用
7. 直接内存
- 不包括在JVM内存区域中,不受JVM参数影响
- JVM使用缓冲区时,会在该区区域分配内存
- 配置时注意给该区域预留空间,而不是把所有内存都分给JVM
- -XX:MaxDirectMemorySize指定,不指定默认与堆最大值一样(-Xmx)
三. HotSpot虚拟机对象
1. 对象的创建
-
收到new指令时,先检查是否能在常量池定位到类的符号引用
-
有则表示类已经被加载,解析和初始化过。否则加载类。
-
根据类大学分配堆内存。分配的方式有
- 指针碰撞:内存规整(无压缩整理功能),仅移动指针。Serial,ParNew虚拟机
- 空闲列表:内存不规整(有压缩整理功能),去空闲列表里找到一块足够的空间。CMS虚拟机
分配过程的并发问题如何解决
- 同步操作:CAS+重试
- 内存按照线程预分配,称为本地线程分配缓冲(TLAB)。-XX:+/-UseTLAB参数决定
-
对象初始化为零
-
设置对象头信息:对象属于哪个类,对象hash码,GC分代年龄,是否启用偏向锁等等
-
执行init方法做程序员需要的初始化
2. 对象的内存布局
对象在内存中的布局分为三个区域:对象头,实例数据,对齐填充
2.1 对象头
- 对象头包括:对象自身的运行时数据,所属类类指针,数组长度(如果是数组对象)
- 运行时数据区官方称为Mark Word
- 运行时数据区是非固定的数据结构,根据标志位不同,存储内容不一样
- 类型指针表明该对象属于哪个类实例
- 如果是数组对象还包括数组的长度
2.2 实例数据
- 对象真正存储的有效信息
- 存储顺序受分片策略参数和源码定义顺序影响
- 分配策略默认将长度长的分配在前面,字段相同的分配到一起
2.3 对齐填充
- 不是必须存在的,仅占位符的作用
- 对象大小必须为8字节整数倍,不足的通过对齐补全
3. 对象的访问定位
-
使用句柄: 堆单独划分一块内存作为句柄池,reference存储句柄地址。对象移动时reference不需要修改。
直接指针:reference直接存储对象地址。速度快。
四. 垃圾收集器与内存分配策略
1. 基本概念
1.1 收集的对象
堆,方法区中的内存区域
1.2 判定对象是否存活的方法
引用计数法
- 给对象添加引用计数器
- 实现简单
- 无法解决对象直接相互循环引用的问题
- 使用的代表:微软COM技术
可达性分析
- 使用的代表:java,c#
- 通过GC roots对象作为起始点,到该对象不可达时,证明对象不可用
- GC roots对象包括以下几种
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法中JNI引用的对象
1.3 引用的分类
- 强引用:普遍存在new之后赋值操作,存在则永远不会被回收
- 软引用:还有用,但并非必须但对象。内存溢出异常之前回收这些对象
- 弱引用:强度比软引用更弱,只能存活到下一次垃圾回收之前
- 虚引用:最弱到引用关系。无法通过虚引用得到对象。存在的目的是当垃圾回收时收到一个系统通知
1.4 方法区(永久代)的回收
- 该区域的垃圾收集效率远远低于新生代(70%-95%)
- 回收两类内容:废弃常量,无用的类
- 判定是否是无用类的条件
- 该类所有实例都被回收
- 加载该类的classload被回收
- 该类的java.lang.Class对象没有在任何地方被引用,无法通过反射访问
- 满足以上条件的无用类可以被回收(不是必须)
2. 垃圾收集算法
2.1 标记-清除算法
- 最基础的收集算法
- 分为标记和清除两个阶段
- 不足之处:
- 效率问题
- 产生大量不连续的内存碎片
2.2 复制算法
- 将内存分为大小相等的两块,每次使用其中的一块
- 一块用完时,将存活的对象复制到另一块
- 现代虚拟机新生代都用该算法
- 不足:
- 内存利用率不高
HotSpot虚拟机将新生代内存分为较大的Eden区和两块较小的survivor空间。大小比例为8:1。
2.3 标记-整理算法
- 对象存活率高时大量的复制会影响效率,老年代使用该算法
- 标记过程与标记-清除算法一样
- 后续步骤并不是清理对象,而是让所有存活的对象都向一段移动,清理边界以外的内存
2.4 分代收集算法
- 根据对象存活周期不同,采用不同的收集算法
- 新生代大量对象死亡,少量存活,采用复制算法
- 老年代对象存活率高,采用标记-清理或者标记-收集算法
3. HotSpot的算法实现
3.1 枚举GC Roots
- 可达性分析枚举GC Roots时 ,必须stop the world
- 目前JVM使用准确式GC,停顿时并不需要一个个检查,而是从预先存放的地方直接取。(HotSpot保存在OopMap数据结构中)
3.2 安全点
- 基于效率考虑,生成OopMap只会才特定的地方,称为安全点
- 安全点的选定方法
- 抢先式中断:现代JVM不采用
- 主动式中断:线程轮询安全点标识,然后挂起
3.3 安全区域
- 对于没有分配cpu的线程(sleep),安全点无法处理,由安全区域解决
- 安全区域指一段代码中引用关系不会发生变化
- 线程进入安全区域时,JVM发起GC就不用管这些线程,离开时需要检查GC是否完成,未完成就需要等待
3.4 垃圾收集器
serial收集器
- 最基本,发展历史最悠久的收集器
- jdk1.3.1之前新生代收集器唯一的选择
- 单一线程收集器
- GC时必须暂停其他所有的工作线程
- 简单高效,对于单CPU的client模式来说是很好的选择
ParNew收集器
- serial收集器的多线程版本
- server模式的JVM首选的新生代收集器
- 单CPU模式下,因线程切换开销,性能绝不比serial好
Parallel Scavenge
- 采用复制算法的新生代收集器,支持多线程
- 可以控制吞吐率
- -XX:MaxGCPauseMillis 最大垃圾收集停顿时间,与吞吐量成反比
- -XX:GCTimeRatio 吞吐量大小
- 提供自适应调节测试
- -XX:UseAdaptiveSizePolicy
- 无法与CMS收集器配合工作
Serial Old
- serial收集器的老年代版本
- 使用标记-整理算法
- 给client模式下虚拟机使用
Parallel Old
- Parallel Scavenge老年代版本
- 多线程,标记-整理算法
- JDK1.6开始提供使用
Concurrent Marked Sweep(CMS)
- 老年代收集器
- 目标是尽可能减少GC停顿时间
- 不会等到老年代空间快满了才回收(和用户线程并发,留内存给用户线程)。配置参数为-XX:CMSInitiazingOccupanyFraction
- 使用标记-清除算法。整个过程分为四步:
- 初始标记:STW,标记GC Roots能关联到的对象,速度很快
- 并发标记:GC Roots Tracing过程。耗时。和用户线程一起执行
- 重新标记:STW,标记并发标记过程中程序运行导致标记变化的对象,时间比初始标记长,远比并发标记短
- 并发清除:耗时。和用户线程一起执行
- 优点:
- 并发收集
- 低停顿
- 缺点:
- 占用正在执行的用户程序的cpu资源
- 无法处理浮动垃圾(并发清理过程中产生的新垃圾无法当次处理掉)
- 内存碎片问题
Garbage First( G1)
- 最前沿的垃圾收集器
- jdk1.7版本发布,替换jdk1.5的CMS
- 堆内存布局与其他收集器不一样,新生代老年代不再是物理隔离的,而是Region集合
- 根据各个region垃圾回收的价值,加入优先级队列。保证每次GC能在有限时间内得到最高的收集率
- 通过Remember set保证跨region的区域不需要全堆扫描
- 运行步骤
- 初始标记:STW,时很短。标记GC Roots关联的对象
- 并发标记:可达性分析,耗时长。可与用户线程并发执行
- 最终标记:STW,修正并发标记阶段用户线程运行导致标记变化的部分
- 筛选回收:排序各个region的回收价值,制定回收计划
- 优点
- 并行与并发
- 分代收集
- 空间整理:不会产生内存碎片
- 可预测的停顿:几乎是实时的垃圾收集
4. 内存分配与回收策略
4.1 对象优先在Eden区分配
-
eden区空间不足时,JVM发起一次Minor GC
Minor GC vs Full GC
- minor gc:新生代gc,频繁发生,速度快
- major gc/full gc:老年代gc,速度慢
4.2 大对象直接进入老年代
- 典型代表是:很长的字符串或数组
- 大对象对JVM不友好,导致频繁发生GC
4.3 长期存活的对象将进入老年代
- 对象经过Eden的第一次minor gc仍然存活,被移动到survivor,年龄+1
- 对象在survivor每经过一次minor gc,年龄+1
- 年龄加到一定程度(默认15),将进入老年代。参数:-XX:MaxTenuringThreshold
4.4 动态年龄判断
- 并不是永远要求年龄达到设定的值才进入老年代
- 当survivor空间中相同年龄所有对象大小大于空间的一半,大于等于该年龄的对象就直接进入老年代
4.5 空间分配担保
- minor gc执行之前会检查老年代最大可用的连续空间是否大于新生代所有对象总空间
- 不成立则判断是否大于历次晋升到老年代对象的平均大小
- 各种条件不满足则进行full gc
5. jvm性能监控
5.1 jdk的命令行工具
- jps:查看运行的虚拟机进程
- jstat:统计信息监控工具。参数有:
- -class:类装载信息
- -gc:监视堆,包括eden、survivor、老年代、持久代空间,gc时间等
- -gccapacity: 同-gc,不过主要关注各区域最大,最小空间
- -gcutil:同-gc,不过主要关注占用百分比
- -gccause:同-gcutil,不过会输出导致上一次GC的原因
- -gcnew:监视新生代
- -gcnewcapacity:同-gcnew,关注最大,最小空间
- -gcold:监视老年代
- -gcoldcapacity:同-gcold,关注最大最小空间
- -gcpermcapacity:永久代最大,最小空间
- -compiler:JIT编译信息
- printcompilation:JIT编译的方法
- jinfo:java配置信息工具。实时查看和调整虚拟机各项参数
- jmap:内存映像工具。参数有:
- -dump:生成java堆存储快照
- -finalizerinfo:等待执行finalize方法的对象
- -heap:显示java堆详细信息:回收期,参数配置,分代状况
- -histo:堆对象统计信息:类,实例数量,总容量
- -permstat:永久代内存状态
- -F:强制生成快照
- jhat:分析dump文件。一般不用。用第三方的VisualVM,eclipse,Memory Analyzer, heap analyzer等
- jstack:java堆栈追踪工具,用于定位长时间停顿的线程当前栈情况
5.2 jdk的可视化工具
- jconsole
- VisualVM