本文为阅读《深入理解Java虚拟机》一书的阅读总结,仅围绕JVM如何运行字节码文件(*.class)探究Java虚拟机的运行机制,在本书的基础上对JVM做了更易理解也更为基础的介绍。
我们编写的Java代码文件,如:
public class HelloWorld {
public static void main(String[] args){
System.out.pringln("Hello Java!");
}
}
要以计算机能够理解的方式运行,必须以计算机能够理解的方式沟通,JVM正是这个沟通的媒介,正式因为JVM的存在,Java技术体系才具备了跨平台的优势。
对于JVM来说,它能识别的只是字节码文件,字节码文件具有严格的格式规范,本书的第六章也对此予以了分析,受制于篇幅,本文不予以介绍,详细规范可以参照JVM规范。这也是与平台无关的奥秘所在,本书的JVM并不关心字节码文件编译自何种编程语言,只要编程语言生成的字节码文件满足字节码文件的格式要求,因此Java编程语言只是生成字节码文件的一种途径。
javac命令可以将.java文件编译成.class文件,一个java代码文件或者一个字节码文件相当于一个类或接口(以下统一以类来叙述)的身份描述说明,二者的唯一不同点在于,java 文件是一种程序员可以方便阅读的格式,字节码文件则是JVM方便“阅读”的格式。
类的加载
JVM如何去拿到字节码文件呢?这正是JVM虚拟机的类加载机制,JVM并不关心字节码来源于网络还是本机的磁盘,它认识的只是一串二进制流。
虚拟机把描述类的数据从二进制流加载到内存,在最终形成能被JVM直接使用的类之前,它需要完成以下几件事:
加载
加载过程就是通过类的全限定名获取定义此类的二进制字节流,将字节流所代表的静态存储结构转化为运行时的数据结构,再在内存生成一个代表这个类的的java.lang.Class对象,作为这个类各种数据的访问入口。
实现类加载的代码模块叫做类加载器,每一个类加载器都拥有一个独立的类名称空间,因此,比较两个类是否相等,只有这两个类由同一个类加载器加载才有意义。
校验
之前说过,字节码文有严格的格式要求,有了字节码文件,检查该文件包含的的字节流是否符合当前虚拟机格式的要求,是否存在威胁虚拟机安全的恶意代码,这就是这个阶段要完成的事情。又分为文件格式验证、元数据验证、字节码验证、符号引用验证等多个阶段,整体是一个从形式到内容,从整体到部分的验证过程。
准备
为类变量,即static变量分配存储空间并初始化。
解析
将字节码文件中的符号引用替换为直接引用,符号引用即描述引用目标位置的字面量;直接引用即引用目标实际的内存位置,可以是一个指针,也可以是一个偏移量。
初始化
与前四个步骤不同,类是否初始化有几个触发的必备条件:
- new 一个类的实例、读取或设置一个类的静态字段
- 反射调用,如果没初始化,则要先初始化
- 子类被初始化
- 执行主类(包含main方法的类)在虚拟机启动时初始化
在满足这几个条件中的任意一个后,类加载会进入初始化阶段,执行类的<clinit>()方法。这个方法由编译器自动收集类的所有类变量的赋值动作合并产生。
类信息存放
在类加载的过程中,各种数据如何存放,涉及到JVM的内存模型。
在C、C++中,内存可以看作一个一个的小格子,需要多少的小格子,格子不需要了还都要程序员手动去管理。在Java中,内存的分配由虚拟机的内存管理模块管理,不需要手动去分配和回收。
虚拟机对内存分区是为了简化内存分配和回收这两个过程。
运行时数据区域由以下几个部分构成:
线程共享的区域:
方法区
类被加载后,类信息、常量、静态变量等数据存放到这个区域,反射机制所获取的类的信息就存放在这个区域。对此区域的内存回收比较少见,主要设计到常量池的回收和类型的卸载,回收的效率通常比较低。
运行时常量池是方法区的一部分,主要用来存放编译期生成的各种字面量和符号引用。
堆
堆是虚拟机所管理内存中最大的一块,也是垃圾收集的主要场所,几乎所有的对象实例都在堆上分配内存。
线程私有的内存区域:
程序计数器
虚拟机通过线程轮流切换并分配处理器执行时间来实现多线程操作,在线程不断切换的过程中,每个线程要记住线程恢复后的执行位置即下一条需要执行的指令,因此这个内存区域是线程私有的。
虚拟机栈
虚拟机栈保存的是Java方法的内存模型,和Java方法相关的信息以一个栈帧的形式保存,每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。为线程私有,与线程的生命周期相同。
本地方法栈
本地方法栈与虚拟机栈作用相似,区别在于虚拟机栈为字节码服务,而本地方法栈为虚拟机使用到的Navtive方法服务。
内存回收
在类的加载过程中,虚拟机给它们分配了大量的内存,如果不进行内存回收,注定导致内存溢出。
由于线程私有的内存区域(程序计数器、虚拟机栈、本地方法栈)生命周期和线程相同,它们的内存分配和回收都有确定性。因此垃圾回收主要考虑的是对堆和方法区的回收,这两个区域内存的分配和回收都是动态的。
如何去判断堆中的对象是否该被回收是垃圾回收的核心。
- 引用计数算法
有引用,计数器值加1,引用失效,计数器值减1
存在对象之间循环引用的问题,导致内存无法被回收。 - 可达性分析算法
垃圾回收算法
- 标记-清除算法
标记所有需要回收的对象,然后统一回收。
缺点: 标记和清除两个过程效率都不高,产生大量不连续的内存碎片 - 复制算法(新生代)
将内存分为两块,每次只使用其中的一块,当一块的内存使用完了,将还存活的对象复制到另一块上去,为主流商业虚拟机采用回收新生代。
缺点:可利用空间减小 - 标记-整理算法(老年代)
老年代因为对象存活率较高,使用复制算法效率较低,据此,有人提标记-整理算法,标记过程与标记-清除算法相似,只是后续将存活的对象向一端移动,然后直接清理掉边界以外的内存。
垃圾回收器
目前商用的虚拟机将内存分为新生代和老年代,以HotSpot虚拟机(JDK1.7)为例,根据各个年代的特点和是否多线程的不同,借助七个不同的垃圾收集器来合理的回收内存。
- Serial收集器
单线程收集器,在垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束,Client模式下(C1编译,即将字节码简易编译为机器码)默认新生代收集器 - ParNew收集器
Serial收集器的多线程版本,虚拟机Server模式(C2编译,在字节码向机器码编译的过程中启用较多的优化)新生代默认收集器 - Parallel Scavenge 收集器
与CMS等收集器追求缩短垃圾收集时用户线程的停顿时间不同,Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,即减少垃圾收集的总时间。因此该收集器也被称为“吞吐量优先”收集器。 - Serial Old收集器
Serial的老年代版本,单线程收集器,使用标记-整理算法 - Parallel Old收集器
配合Parall Scavenge使用,在此收集器之前,Parallel Scavenge 收集器只能搭配Serial Old使用,无法与CMS收集器配合,此收集齐出现后,“吞吐量优先”有了名符其实的组合。 - CMS 收集器
以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法实现,有并发收集、低停顿的优势。
缺点:
- 对CPU资源非常敏感
- 无法处理浮动垃圾
- 产生大量空间碎片
- G1收集器
面向服务端应用,在未来作为CMS收集器的替代者
特点:
- 能够独立管理整个堆
- 从整体上来看基于标记-清理算法,局部上来看基于复制算法,不会产生内存空间碎片
分配策略
- 对象优先在新生代Eden分配,在Eden没有足够的空间进行分配时,虚拟机将发起一次Minor GC
- 大对象直接进入老年代,
- 长期存活的对象将进入老年代,在Eden中出生并经过第一次Minor GC 后仍然存活,并且能被Survivor容纳,将被移动到Survivor空间中,每熬过一次Minor GC ,对象的年龄增加1,HotSpot虚拟机默认达到15岁,将会晋升到老年代中。