JVM, 中文名是Java虚拟机, 正如它的名字, 是一个虚拟机器,来模拟通用的物理机。 JVM是一个标准,一套规范, 规定了.class文件在其内部运行的相关标准和规范。 及其相关的内部构成。 比如:所有的JVM都是基于栈结构的运行方式。那么不符合这种要求的,不算是JVM, 如Android中所使用的Dalvik 虚拟机就不能称作是JAVA 虚拟机, 因为它是基于寄存器(最新的Android系统据说已经放弃了Dalvik VM, 而是使用ART)。
JVM相关的产品有很多, 通常最有名的莫过于现在Oracle公司所有的HotSpot 虚拟机。因此, 这里讨论的都是HotSpot虚拟机, 如果没有特别说明。
2、类加载?
类加载, 是通过JVM的类加载器从JVM外部以二进制字节流的方式加载到JVM中。但JVM本身有至少三种类加载器:BootStrap(根类加载器,C++实现, 加载位于jre/lib/rt.jar)、Extension(扩展类加载器, 主要用于加载jre/lib/ext/下的jar)、System(加载classpath环境变量所指定的class);当然还有,自定义的类加载器(用于实现自己的类加载器, 如Tomcat中就实现多个类加载器,用来管理不同的jar)。
如果, 我有一个HelloWorld的类需要加载, 首先类加载器会去从最底层的类加载器去验证这个类是否被加载, 如果没有, 则委托给上一次的类加载器验证是否被加载, 如果到BootStrap类加载器都没有发现HelloWorld类被加载, 那么类加载器将执行加载任务, 如果根类加载器没有加载, 则委托给下一级的Extension类加载器去尝试加载,直到这个类被加载成功。 参考下图:
需要注意的是:如果一个类被不同的类加载器加载, 那么就是两个不同的类。
3、类加载的具体过程?
被java编译器(不仅限于, 还有其他任何的可以编辑成为.class的编译器)编译过的.class文件(可能是以jar、war、jsp等形式), 经过类加载器加载 、 验证、准备、解析、初始化之后, 才可以被使用。基本的过程如下:
-
加载: 首先,通过一个类的全类名来获取此类的二进制字节流。其次,将类中所代表的静态存储结构转换为运行时数据结构, 最后,生成一个代表加载的类的java.lang.Class对象, 作为方法区这个类的所有数据的访问入口。加载完成之后, 虚拟机外部的二进制静态数据结构就转换成了虚拟机所需要的结构存储在方法区中(至于如何转换, 则由具体虚拟机自己定义实现), 而所生成的Class对象, 则存放在方法区中, 用来作为程序访问方法区中数据的外部接口。
-
验证:其目的就是保证加载进来的.class文件不会危害到虚拟机本身, 且内容符合当前虚拟机规范要求。主要验证的内容大致有:文件格式、元数据验证、字节码验证、符号引用验证。其中文件格式验证, 主要确保符合class文件格式规范(如文本后缀为.class的文件将验证不通过), 以及主次版本号, 验证是否当前JVM可以处理等。元数据验证,主要验证编译后的字节码描述信息是否符合java语法规范。字节码验证, 其最为复杂, 主要通过控制流和数据流确定语义是否合法、符合逻辑。符号引用验证,可以看做是除自身以外(常量池中各种引用符号)的信息匹配校验,如通过持有的引用能否找到对应的实例。
-
准备:正式为类变量分配内存,并设置类变量的初始值。这些变量都会在方法区中进行分配。
-
解析:将常量池内的符号引用替换为直接引用的过程。主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄等。
-
初始化:加载的最后阶段, 程序真正运行的开始。
既然类以及加载到JVM中, 那么数据如何真正的运行?如下图:
类加载进来, JVM是通过上图所示的区域来运行和管理这些加载进来的CLASS。即程序运行的是时候, 由上面逻辑单元来运行程序, 包括:方法区、堆、本地方法栈、栈、程序计数器(PC)五大部分组成(有些VM说常量池也是其中的一个单元, 但是HotSpot VM中的常量池是方法区中的一部分)。(注意线程共享)
-
程序计数器 (PC):可以看做是当前线程执行字节码的行号指示器。字节码解释器工作的时候就是通过这个计数器的值来选取下一条需要执行的字节码指令, 分支, 循环、跳转、异常处理、线程恢复等基础功能依赖计数器完成。
-
虚拟机栈:和计数器一样, 也是线程私有的,生命周期同线程一致。每个方法在执行时,都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法调入则入栈, 方法执行完则出站。局部变量表存储各种基本类型数据(java的8种,其中long,double占用2个局部变量控件,其余数据占用1个)、对象引用(reference类型)。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时, 这个方法需要在帧中分配多大的局部变量空间是完全确定的。在方法运行期间是不会改变局部变量表的大小的。
-
本地方法栈:此栈和JVM栈作用非常类似, 不同在于本地方法栈为虚拟机使用到的Native方法服务, 而JVM栈则是为Java执行的方法服务。Sun HotSpot虚拟机, 直接把本地方法栈和虚拟机栈合二为一。本地方法栈也会抛出StackOverFlowError和OutOfMemoryError异常。
-
Java堆:是JVM管理内存中最大的一块。被所有线程共享一块区域。堆是GC垃圾收集器管理的主要区域。从内存回收角度看, java堆被分为新生代、老年代, 再细致一点有其他的划分。这些目的主要就是更快的分配和回收内存。
-
方法区:和java堆相同, 线程共享区域, 用来存储已被虚拟机加载的类信息, 常量、静态变量、即时编译器编译后的代码等数据。有人称作此方法区为“永久带”, 本质上不等价,只是HotSpot VM将GC分代收集扩展到了方法区,这样HotSpot的垃圾收集器管理方法区和管理java堆一样(优点:不用专门为方法区写一套垃圾收集器, 缺点:容易导致内存溢出)。官方现在拥也有放弃永久带并改为采用Native Memory来实现方法区的计划,目前已经发布的JDK7中的HotSpot中, 已经将原本放在方法区中的字符串常量池移出了。
-
运行时常量池:是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述外,还有一项就是常量池, 用于存放编译期间生成的各种字面量和符号引用 ,这部分内容在类加载后进入方法区的运行时常量池中存放。
在java运行时区域中, 程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随程而灭。因为这几个区域
的内存分配和回收都是具有确定性,这几个区域不需要过多考虑回收的问题。因为方法结束之后或线程结束之后,
内存自然就跟着回收了(这不是绝对的, 因为如果当栈内存中的引用很消耗内存的时候, 需要手动将引用置为null,
以便垃圾收集器回收大对象)。而java堆和方法区不一样,一个接口中的多个实现类需要的内存可能不一样, 一个
方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时,才知道会创建哪些对象, 垃圾收集
器关注的就是这部分内存。其也是动态的。
垃圾收集器的区域如下图:
垃圾收集本是有一套非常复杂的算法, 如果在方法区中(HotSpot VM中的永久带)进行垃圾收集, 那么其性价比极
底的,因为垃圾回收主要收集永久带中的两部分内容:废弃的常量和无用的类。回收永久带中的常量和方法区非常相
似。但是在堆中, 尤其是在新生代中,常规应用进行一次垃圾收集, 一般可以回收70%——95%的空间。而永久带
的垃圾收集要远地与此。
如上图所示, 每一个黑框中都是一个垃圾收集器, 对应特定的垃圾收集算法, 来挺高整体的垃圾收集效率。