文章目录
JVM(Java Virtual Machine)
Java程序的一大特性就是跨平台特性。Java虚拟机(JVM)是实现这一特性的关键。因为字节码文件(.class)可以在任何具有Java虚拟机的计算机或者电子设备上运行,Java虚拟机中的Java解释器负责将.class文件解释成为特定的机器码进行运行。从而可以达到一次编译到处运行的效果。这一特性就好比各个厂商的浏览器各不相同,但是html都能在这些浏览器上正常显示。
JVM内存区域划分
JVM 大致可以分为三部分,分别是:类加载器,运行时数据区和执行引擎。
类加载器
类加载器负责加载编译好的.class字节码文件,装入内存,使JVM可以通过实例化或其它方式使用加载后的类。
类加载器分类
- 启动类加载器(BootStrap Class Loader):负责加载rt.jar文件中所有的Java类。这个类加载器是由C++实现的,并且无法被程序引用。
- 扩展类加载器(Extension Class Loader):负责加载<JAVA_HOME>/lib/ext下的类库。
- 应用程序类加载器(Application Class Loader):负责加载启动参数中指定的Classpath中的所有类库。
- 用户自定义类加载器(User Defined Class Loader):由用户自定义类的加载规则,可以手动控制加载过程中的步骤。
双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
为什么需要双亲委派模型呢?
众所周知 java.lang.Object 是 java 中所有类的父类,它存放在 rt.jar 之中,按照双亲委派模型,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都应该是同一个类。但是如果没有使用双亲委派模型,由各个类加载器自行去加载,显然,这就存在很大风险,用户完全可以恶意编写一个java.lang.Object类,然后放到ClassPath下,那系统就会出现多个Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
类加载器工作原理
类的生命周期分为:
加载→连接(验证→准备→解析)→初始化→使用→卸载,五个阶段
加载阶段
- 通过一个类的全限定名来获取此类的二进制字节流;
- 将字节流所代表的的静态存储结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表这个类的 java.lang.class 对象, 作为方法区这个类的各种数据的访问入口。
一个类的二进制字节流可以从zip、网络和数据库中获取,也可以在运行时动态生成,也可以通过.java文件以外的文件生成(如jsp文件)。
通常一个类的加载阶段中的获取二进制字节流的操作是可控的(通过实现自定义类加载器)。
验证阶段
验证阶段是为了确保.class文件字节流中包含的信息符合当前虚拟机的要求。
- 文件格式验证:保证输入的字节流能正确的解析并存储于方法区内,格式上符合描述一个java类型信息的要求。
- 是否以魔数0xCAFEBABE开头
- 主次版本号是否在当前虚拟机的处理范围内
- 常量池的常量是否有不被支持的常量类型
- …
- 元数据验证:对字节码描述的信息进行语义分析,确保符合java语言规范。
- 是否有父类
- 是否继承了不允许被继承的类
- …
- 字节码验证:通过数据流和控制流分析确定程序语义是合法的符合逻辑的。对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机的行为
- 符号引用验证:对类自身以外的信息(常量池中的各种符号引用)进行匹配性校验
- 引用中通过全限定名描述的类能否找到
- …
通常如果我们运行的程序已经经过了严格的验证阶段,可以通过-Xverify:none 关闭验证措施,缩短类加载的时间。
准备阶段
正式为类变量分配内存并设置变量初始值。(初始值,不是程序在开发时设置的默认值,而是变量所属类型的初始值如int初始值是0)
解析阶段
将常量池内的符号引用替换为直接引用的过程
- 类或接口的解析:加载目标类,确认访问权限
- 字段解析:解析字段所属类或接口的符号引用
- 类方法解析:解析出类方法所属类或接口的符号引用
- 接口方法解析:解析出接口方法所属类或接口的符号引用。
初始化阶段
初始化类变量和其他资源,为类变量赋值(使用开发时设置的默认值)
运行时数据区
运行时数据区由方法区、堆、Java虚拟机栈、程序计数器、本地方法栈组成。
程序计数器
当前线程所执行字节码的行号指示器,由于Java是支持多线程执行的,所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
java 虚拟机栈
java虚拟机栈也是线程私有的,生命周期与线程相同。java虚拟机栈描述的是java方法执行的内存模型,每个方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈
与虚拟机栈相似,不同的是虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。
java 堆
这是线程共享的内存区域,用于存放对象实例,几乎所有的对象实例都在这里分配内存。这里也是java垃圾收集器管理的主要区域。从内存回收角度java堆还可分为新生代和老年代等等。
方法区
与java堆一样,这里也是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量等数据。
执行引擎
JVM执行java代码的方式主要有解释执行(通过解释器执行)、编译执行(通过即时编译器产生本地代码执行)两种方式