一、JVM整体组成概述
1、整体组成部分
1.1、类加载器(ClassLoader)
1.2、运行时数据区(Runtime Data Area)
1.3、执行引擎(Execution Engine)
1.4、本地库接口(Native Interface)
2、图解
3、整体运行流程
程序在执行之前先要把 java 代码转换成字节码(class 文件),jvm 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中的运行时数据区(Runtime Data Area) ;
而字节码文件是 jvm 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine) 将字节码翻译成底层系统指令再交由CPU 去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这 4 个主要组成部分的职责与功能。
而我们通常所说的 JVM 组成指的是 运行时数据区(Runtime Data Area) ,因为通常需要程序员调试分析的区域就是“运行时数据区”,或者更具体的来说就是“运行时数据区”里面的 Heap(堆)模块。
下面具体看下这几大块
二、类加载器
1、类加载子系统
类加载子系统负责从文件系统或者网络中加载class文件。classLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。 加载的类信息存放于一块称为方法区的内存空间。
class file 存在于硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载 JVM 当中来,根据这个模板实例化出 n 个实例,
class file 加载到 JVM 中,被称为 DNA 元数据模板. 此过程就要有一个运输工具(类加载器 Class Loader),扮演一个快递员的角色。
2、类加载过程
3、加载
(1) 通过类名(地址)获取此类的二进制字节流。
(2)将这个字节流所代表的静态存储结构转换为方法区(元空间)的运行时结构。
(3)在内存中生成一个代表这个类的java.lang.class对象,作为这个类的各种数据的访问入口。
4、 链接
4.1、验证
检验被加载的类是否有正确的内部结构,并和其他类协调一致,如:
(1)验证文件格式是否一致: class 文件在文件开头有特定的文件标识(字节码文件都以 CA FE BA BE 标识开头);主,次版本号是否在当前 java 虚拟机接收范围内;
(2)元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合java 语言规范的要求,例如这个类是否有父类;是否继承浏览不允许被继承的类(final 修饰的类);
(3)....
4.2、准备
准备阶段则负责为类的静态属性分配内存(不包含用 final 修饰的 static 常量),并设置默认初始值,在编译时进行初始化。
如: public static int value = 123;value 在准备阶段后的初始值是 0,而不是 123。
4.3、解析
将类的二进制数据中的符号引用替换成直接引用(符号引用是 Class 文件的逻辑符号,直接引用指向的方法区中某一个地址)
5、初始化
初始化,为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。初始化阶段就是执行底层类构造器方法<clinit>()的过程。此方法不需要定义,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来的。
5.1、初始化时机:
JVM规定:每个类或者接口被首次主动使用时才对其进行初始化,包含以下场景:
(1)通过 new 关键字创建对象
(2)访问类的静态变量,包括读取和更新
(3)访问类的静态方法
(4)对某个类进行反射操作
(5)初始化子类会导致父类的的初始化
(6)执行该类的 main 函数
注意:除了以上几种主动使用,以下情况被动使用,不会加载类:
(1)引用该类的静态常量,注意是常量,不会导致初始化,但是也有意外,这里的常量是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导致类加载,比如:
public final static int NUMBER = 5 ; //不会导致类初始化,被动使用
public final static int RANDOM = new Random().nextInt() ; //会导致类加载
(2)构造某个类的数组时不会导致该类的初始化,比如:
Student[] students = new Student[10] ;
5.2、类的初始化顺序
对static修饰的变量或语句块进行赋值, 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行;如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
顺序是:父类 static –> 子类 static
public class ClassInit{
static{
num = 20;
}
static int num = 10;
public static void main (String[] args) {
//num从准备到初始化值变化过程 num=0 -> num=20 -> num=10
System.out.println(num);//10
}
}
public class ClassInit{
static int num = 10;
static{
num = 20;
}
public static void main (String[] args) {
//num从准备到初始化值变化过程 num=0 -> num=10 -> num=20
System.out.println(num);//20
}
}
6、类加载器分类
站在JVM的角度看,类加载器可以分为两种:
- 引导类加载器(启动类加载器 Bootstrap ClassLoader).
- 其他所有类加载器,这些类加载器由 java 语言实现,独立存在于虚拟机外部,并 且全部继承自抽象类 java.lang.ClassLoader.
站在 java 开发人员的角度来看,类加载器就应当划分得更细致一些.自 JDK1.2 以来 java 一直保持者三层类加载器:
(1)引导类加载器(启动类加载器 BootStrap ClassLoader):
这个类加载器使用 C/C++语言实现,嵌套在 JVM 内部.它用来加载 java 核心类库.并不继承于 java.lang.ClassLoader 没有父加载器.
负责加载扩展类加载器和应用类加载器,并为他们指定父类加载器.
出于安全考虑,引用类加载器只加载存放在<JAVA_HOME>\lib 目录,或者被-Xbootclasspath 参数锁指定的路径中存储放的类。
(2)扩展类加载器(Extension ClassLoader):
Java 语言编写的,由 sun.misc.Launcher$ExtClassLoader 实现.
派生于 ClassLoader 类.
从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 系统安装目录的jre/lib/ext 子目录(扩展目录)下加载类库.如果用户创建的 jar 放在此目录下,也会自动由扩展类加载器加载。
(3)应用程序类加载器(系统类加载器 Application ClassLoader):
Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现.
派生于 ClassLoader 类.
加载我们自己定义的类,用于加载用户类路径(classpath)上所有的类.
该类加载器是程序中默认的类加载器.
ClassLoader 类 , 它 是 一 个 抽 象 类 , 其 后 所 有 的 类 加 载 器 都 继 承 自 ClassLoader(不包括启动类加载器)。
7、双亲委派机制
Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要该类时才会将它的 class 文件加载到内存中生成 class 对象.而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式
7.1、工作原理
(1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请 求委托给父类的加载器去执行;
(2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终 将到达顶层的启动类加载器;
(3)如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完 成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制,如果均加载失败,就会抛出 ClassNotFoundException 异常。
7.2、双亲委派优点:
(1)安全,可避免用户自己编写的类替换 Java 的核心类,如 java.lang.String;
(2)避免类重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次。
7.3、如何打破双亲委派机制
Java 虚拟机的类加载器本身可以满足加载的要求,但是也允许开发者自定义类加载器。
在 ClassLoader 类中涉及类加载的方法有两个,loadClass(String name), findClass(String name),这两个方法并没有被 final 修饰,也就表示其他子类可以重写.
重写 loadClass 方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制, 不推荐)
重写 findClass 方法 (推荐)
我们可以通过自定义类加载重写方法打破双亲委派机制, 再例如 tomcat 等都有自己定义的类加载器。
三、JVM运行时数据区
1、组成概述
JVM 的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从 Java 虚拟机规范,
Java 8 虚拟机规范规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区域:
1.1、程序计数器(Program Counter Register)
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
1.2、Java 虚拟机栈(Java Virtual Machine Stacks)
描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个线帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。
1.3、本地方法栈(Native Method Stack)
与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的。
1.4、Java 堆(Java Heap)
是 Java 虚拟机中内存最大的一块,是被所有线程共享的,在虚拟机启动时候创
建,Java 堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配 内存。
1.5、 方法区(Methed Area)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。方法区是很重要的系统资源,是硬盘和 CPU 的中间桥梁,承载着操作系统和应用程序的实时运行.
JVM 内存布局规定了 Java 在运行过程中内存申请,分配,管理的策略,保证了 JVM的高效稳定运行.不同的 JVM 对于内存的划分方式和管理机制存在着部分差异, 以最为流行的 HotSpot 虚拟机为例:
Java 虚拟机定义了程序运行期间会使用到的运行数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁.另外一些则是与线程一一对应的. 这些与线程对应的区域会随着线程开始和结束而创建销毁,如图: 红色的为多个线程共享,灰色的为单个线程私有的,即 线程间共享:堆,方法区. 线程私有:程序计数器,栈,本地方法栈。