title: 深入理解Java虚拟机
1.前言
如果开发人员不了解虚拟机的一些技术特性的运行原理,就无法写出最适合虚拟机运行和自优化的代码。
2.走进Java
- java的优点:
- 结构严谨,面向对象的编程语言
- 摆脱平台束缚,“一次编码,到处运行”
- 相对安全的内存管理和访问机制,避免绝大部分的内存泄漏和指针越界问题
- 热点代码检测和运行是编译及优化
- java程序设计语言、java虚拟机、java api类库统称为JDK
- java api 类库中的javase api子集和java虚拟机统称为JRE
- JDK是程序开发的最小环境; JRE是支持Java程序运行的标准环境
3.自动内存管理机制
3.1Java内存区域和内存溢出异常
- 线程私有
- 程序计数器(1.通过改变计数器的值来选取下一条需要执行的字节码指令,2.每条线程都有一个独立的程序计数器,3.唯一一个没有任何OutOfMemoryError的区域)
- Java虚拟机栈。(1.java方法在执行时创建用于存储局部变量表等 2.局部变量表存放编译期可知的基本数据类型boolean,byte,char…,对象引用等 3.局部变量表所需内存在编译期间完成分配 4.两种异常:StackOverflowError, OutOfMemoryError)
- 本地方法栈 (1.为虚拟机使用的Native方法服务 2.两种异常:StackOverflowError, OutOfMemoryError)
- 线程共享
- Java 堆(1.存放对象实例 2.垃圾收集器管理的主要区域,‘GC堆’ 3. -Xms和-Xmx控制 4.OutOfMemoryError)
- 方法区(1.已被虚拟机加载的类信息,常量,静态变量等 2.OutOfMemoryError)
- 运行时常量池(方法区的一部分)(1.OutOfMemoryError)
- 直接内存(1.不是虚拟机运行时的数据区的一部分 2.OutOfMemoryError)
3.2 对象的创建
虚拟机遇到一条new指令 -》 检查这个指令参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否正被加载,解析和初始化过。如果没有,则进行相应类的加载过程-》 在类加载检查通过后,为新生对象分配内存 -》 内存分配完成后,将分配到的内存空间都初始化为零值 -》 对对象头进行必要设置 -》 执行init方法,按程序员意愿初始化。
3.3 对象的内存布局
- 对象头
- 存储对象自身运行时的数据
- 类型指针,通过这个来确定这个对象属于哪个类的哪个实例
- 实例数据
- 对象的有效信息
- 对齐填充
- 不是必然存在,没有特别含义,仅仅占位符的作用
3.4 对象的方位定位
- 句柄
- java堆划分内存作为句柄池
- 句柄包含对象的实例数据和类型数据
- 直接指针
- 需要考虑如何访问方法区的类型数据
4. 垃圾收集器与内存分配策略
4.1 概述
-
GC需要完成的事情
- 哪些内存需要回收
- 什么时候回收
- 如何回收
-
为什么要了解GC和内存分配
当需要排查内存溢出,内存泄漏问题时; 当垃圾收集器成为系统达到更高并发量的瓶颈时,我们就需要对这种“自动化”技术实施必要的监控和调节。
程序计数器、虚拟机栈、本地方法栈随线程而生,随线程而灭。所以垃圾收集器关注的就是Java堆和方法区。
4.2 对象已死?
4.2.1判断算法
- 引用计数算法
主流的java虚拟机没有选用计数算法,很难解决对象之间相互循环引用。 - 可达性分析算法
GC ROOTS 的对象作为起始点,从这个节点开始向下搜索,当一个对象到GC ROOTS没有任何引用链相连时,则证明对象不可用。
4.2.2 引用
- 强引用
Object object = new Object(); 。 只要强引用存在,垃圾收集器永远不会回收被引用的对象。 - 软引用
有用但并非必须的对象。在系统将要发生内存溢出之前,会将这些对象列为回收范围进行第二次回收。如果这次回收还没有足够内存,则抛异常。 - 弱引用
比软引用更弱一些。对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论内存是否足够,都会回收。 - 虚引用
最弱的引用关系。一个对象是否有虚引用,完全不会对其生存时间造成影响。同样,也无法通过该引用获取对象实例。 唯一目的就是被回收时收到一个系统通知。
4.2.3 生存还是死亡
即使在可达性分析算法中不可达的对象,已并非非死不可。 一个对象死亡至少经历两次标记过程。如果对象在进行可达算法发现没有与GC ROOTS相连接的引用链,进行第一次标记并筛选,筛选条件是此对象是否有必要执行finalize()方法。 当对象没有覆盖此方法,或者虚拟机已经调用过,则没有必要执行。 如果判定为有必要执行finalize方法,则会被放入一个队列,稍后GC将对这个队列的对象进行第二次小规模标记,如果对象在finalize方法中重新与引用链上的任何一个对象建立关联就能拯救自己。
任何一个对象的finalize方法都只会被调用一次。 注: 该方法能做的工作,使用try-finally或其他方法会更好,更及时,所以此方法不常用。
4.2.4 回收方法区
永久代(方法区)的垃圾收集效率较低。回收内容: 废弃常量和无用类。
- 废弃常量判断
跟java堆中的对象非常相似。 - 废弃类判断
- Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类方法
在大量使用反射、动态代理、CGLIB等ByteCode框架、动态生成jsp以及OSGI这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载功能,以保证永久代不会溢出
4.3 垃圾收集算法
- 标记-清除算法
标记和清除两个阶段。两个缺点:效率问题,空间问题。 - 复制算法
内存分为两半。每次用一半,当一块用完,就将还存活的对象复制到另外一块上。
新生代采用这种算法。内存分为一块较大的Eden区和两块Survivor区,每次使用Eden和其中的一块Survivor。当另外一块Survivor无法存放上一次新生代收集的存活对象时,这些对象将直接进入老年代。
- 标记-整理算法
老年代使用该算法。过程同标记-清除一样,但后续是让所有存活的对象都往一端移动,然后直接清除边界外的内存。 - 分代收集算法
将java堆分为新生代和老年代。根据各个年代的特点选择合适的算法。新生代中每次都有大批对象死亡,少量存活,选用复制算法就可以复制少量存活对象完成收集; 而老年代中,对象存活率高,没有额外的空间进行分配担保,就必须使用“标记-清理”或者“标记-整理”来进行回收。
4.4 内存分配与回收策略
4.4.1 对象优先在Eden区
对象优先在Eden区分配,当Eden区没有足够内存,虚拟机将发生一次Minor GC。
1.新生代GC(Minor GC) : 指发生在新生代的垃圾收集,该GC发生频繁,回收速度快
2.老年代GC(Major GC/ Full GC) : 指发生在老年代的垃圾收集。出现该收集一般会伴随至少一次的Minor GC。 Major GC 很慢,慢10倍以上。
4.4.2 大对象直接进入老年代
需要大量连续内存空间的java 对象,最典型的就是很长的字符串和数组。
4.4.3 长期存活的对象进入老年代
虚拟机对每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经历一次Minor GC后仍然存活,并且被Survivor容纳,将被移动到Survivor中,年龄设为1。 对象在Survivor区每熬过一次Minor GC,年龄就会+1,当增加到一定程度,就会被晋升到老年代。
4.4.4 动态对象年龄判定
为了更好适应不同程序的内存情况,虚拟机并不是永远要求对象达到指定的年龄才晋升到老年代。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代。
4.4.5 空间分配担保
在发生Minor GC前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果大于,Minor GC确保安全。
如果不大于,虚拟机会查看HandlePromotionFilure设置是否允许担保失败。
如果允许担保失败,会继续检查老年代最大连续可用空间是否大于历次晋升到老年代对象的平均大小。
如果大于,将尝试进行一次Minor GC,尽管这次是有风险的。
如果平均值小于,或者参数设置为不允许冒险,这时候要改为进行一次Full GC
5. 虚拟机性能监控和故障处理工具
5.1 概述
给一个系统定位问题时,知识、经验是关键基础;数据是依据;工具是运用知识处理数据的手段。
5.2 JDK的命令行工具
5.2.1 jps: 虚拟机进程状况工具
可以列出正在运行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一ID。
jps -l
5.2.2 jstat: 虚拟机统计信息监视工具
jstat 是用于监视虚拟机各种运行状态信息的命令行工具。
jstat -gcutil VMID
不如VisualVM直观
5.2.3 jinfo: java配置信息工具
jinfo 是实时查看和调整虚拟机各项参数。
jinfo -flag CMSInitiatingOccupancyFraction 7156
5.2.4 jmap: java内存映像工具
用于生成堆转储快照。
- 生成堆转储快照的几种方式
- -XX:+HeapDumpOnOutOfMemoryError。 当出现OOM异常后生成dump文件
- -XX: +HeapDumpOnCtrlBreak。使用ctrl+break让虚拟机生成dump。
- 在Linux系统下,通过kill -3命令发送进程退出信号来吓一下虚拟机,生成dump
5.2.4 jhat: 虚拟机堆转储快照分析工具
与jmap搭配使用,分析jmap生成的dump。
5.2.4 jstack: java堆栈跟踪工具
生成虚拟机当前时刻的线程快照。生成线程的快照目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致长时间等待等。
5.3 jdk的可视化工具
- Jconsole: 虚拟机监控工具
- Visual VM : 多合一故障处理工具
jdk出了提供大量的命令行工具外,还有两个可视化工具: Jconsole 、 Visual VM
5.3.1 JConsole: Java监视与管理控制台
- 启动JConsole。 JDK/bin目录下的jconsole.exe
- 内存监控: 相当于jstat命令
- 线程监控: 相当于jstack命令
Integer.valueOf() 方法基于减少对象创建次数和内存节省考虑, -128 ~ 127之间的数字会被缓存,当valueOf()方法传入参数在这个范围之内,将直接返回缓存中的对象。
5.3.1 VisualVM : 多合一故障处理工具
运行监视和故障处理程序。
- 首次安装后,需要手工安装插件。其中 概述、监视、线程、MBeans功能和JConsole差别不大。
- 生成、浏览堆转储快照
- 分析程序性能 (尽量不要在生产环境中使用该功能)
- BTrace动态日志追踪 (在不停止目标程序运行的前提下,加入原本并不存在的代码)