JVM
1. 什么是 JVM
1.1 JVM(Java Virtual Machine,Java 虚拟机)
2. JVM 的组成
2.1 JVM 的组成分为两种:整体组成部分和运行时数据区组成部分
2.2 JVM 整体组成:
JVM 的整体组成可分为以下四个部分:
1、类加载器(ClassLodaer)
2、运行时数据区(Runtime Data Area)
3、执行引擎(Execution Engine)
4、本地库接口(Native interface)
各个组成部分的:
-程序在执行之前先要把 Java 代码转换成字节码文件(class 文件),JVM 首先需要把字节码通过一定的方式 类加载器(ClassLoader)把文件加载到内存中 运行时数据区(Runtime Data Area),而字节码文件是 JVM 的一套指定集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由 CPU 去执行,而这个过程需要调用其他语言的接口 本地库接口(Native interface)来实现整个程序的功能
-这就是这4个主要组成部分的职责与功能复制代码
2.3 运行时数据区组成:
JVM 的运行时数据区,不同虚拟机实现可能略微有所不同,但都会遵从 Java 虚拟机规范,JDK 1.8 虚拟机规范规定,Java 虚拟机所管理的内存将包括以下几个运行时数据区域:
1、程序计数器(Program Counter Register)
2、Java 虚拟机栈(Java Virtual Machine Stacks)
3、本地方法栈(Native Method Stack)
4、Java 堆(Java Heap)
5、方法区(Methed Area)
接下来我们分别介绍每个区域的用途:
1、Java 程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的信号指示器。在虚拟机的概念模型里,字节码解析器的工作是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
特性:内存私有。
由于 JVM 的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,也就是任何时刻,一个处理器(或者说一个内核)都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每个线程都有独立的程序计数器。
异常规定:无
如果线程正在执行 Java 中的方法,程序计数器记录的就是正在执行虚拟机字节码指令的地址,如果是 Native 方法,这个计数器就位空(undefined),因此该内存区域是唯一一个在 Java 虚拟机规范中没有规定 OutOfMemoryError 的区域。
2、Java 虚拟机栈
Java 虚拟机栈(Java Virtual Machine Stacks)描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个线帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。
特性:内存私有,他的生命周期和线程相同。
异常规定:StackOverflowError、OutOfMemoryError
-如果线程请求的栈深度大于虚拟机所允许的栈深度就会抛出 StackOverFlow 异常
-如果虚拟机是可以动态扩展的,如果扩展时无法申请到足够的内存就会抛出 OutOfMemoryError 异常
3、本地方法栈
-本地方法栈(Native Method Stack)与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用的 Native 方法服务的
-在 Java 虚拟机规范中对于本地方法栈没有特殊的要求,虚拟机可以自由的实现它,因此在 SunHotSpot 虚拟机直接把本地方法栈和虚拟机栈合二为一了
特性和异常:同虚拟机栈,请参考上面知识点。
4、Java 堆
Java 堆(Java Heap)是 Java 虚拟机中内存最大的一块,是由年轻代和老年代组成,而年轻代又被分为三部分,Eden 空间、From Survivor 空间、To Survivor 空间,是被所有线程共享的,在虚拟机启动的时候创建,Java 堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,随着 JIT(Just-In-Time Compiler,即时编译器) 编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化的技术会导致一些微妙的变化,所有的对象分都分配在堆上渐渐变得不那么"绝对"了。
特性:内存共享
异常规定:OutOfMemoryError
-如果在堆中没有内存完成实例分配,并且堆不可以扩展时,将会抛出 OutOfMemoryError
-Java 虚拟机规范规定,Java 堆可以处理在物理上不连续的内存空间中,只要逻辑上连续即可,就像我们的磁盘空间一样。在实现上也可以是固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是克扩展的,通过 -Xmx 和 -Xms 控制。
5、方法区
方法区(Method Area)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据
误区:方法区不等于永生代。
很多人把方法区称作"永生代"(Permanent Generation),本质上两者并不等价,只是 HotSpot 虚拟机垃圾回收器团队把 GC 分代收集扩展到了方法区,或者说是用来永久代来实现方法区而已,这样能省去专门为方法区编写内存管理的代码,但是在 JDK1.8 也移除了"永久代",使用 Native Memory 来实现方区。
特性:内存共享
异常规定:OutOfMemoryError
当方法无法满足内存分配时会抛出 OutOfMemoryError 异常。
复制代码
3. JVM 内存结构示意图
4. 类加载原理机制
4.1 类加载过程图解(类的生命周期):
4.2 类加载详解
4.2.1 类的加载过程:
类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class 对象,Class 对象封装了类在方法区的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构的接口。
JVM 将类的加载过程分为三个步骤:装载(Load)、链接(Link)和初始化(Initialize)而连接又分为三个步骤如上图所示:
1、装载(Load):
-查找并加载类的二进制数据
2、链接(Link):
-验证:确保被加载类的正确性
-准备:为类中的符号引用转换为直接引用
-解析:把类中的符号引用转换为直接引用
3、初始化(Initialize):
-为类的静态变量赋予正确的初始值
-为什么我们要有验证这一步呢?首先如果由编译器生成的 class 文件,它肯定符合 JVM 字节码格式的,但是万一有高手自己写了一个 class 文件,让 JVM 加载运行,用于恶意用途,就不妙了,因此这个 class 文件要先过验证这一关,不符合的话不会让它继续执行,为了安全考虑
-准备阶段和初始化阶段看似有些矛盾,其实是不矛盾的,如果类中有语句:private static int a= 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给 a 分配内存,因为变量 a 是 static 的,所以此时 a 等于 int 类型的默认初始值 0,即 a = 0,然后到解析,到初始化这一步骤时,才把 a 真正的值 10 赋给 a,此时 a = 10
复制代码
4.2.2 类的初始化:
类什么时候才被初始化:
1、创建类的实例,也就是 new 一个对象
2、访问某个类或接口的静态变量,或者对该静态变量赋值
3、调用类的静态方法
4、反射(Class.forName("com.gkedu.load"))
5、初始化一个类的子类(会首先初始化子类的父类)
6、JVM 启动时标明的启动类,即文件名和类名相同的那个类
只有这 6 种情况才会导致类的初始化。
类的初始化步骤:
1、如果这个类还没有被加载和链接,那么先进行加载和链接
2、假如这个类存在直接父类,并且这个类还没被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)
3、加入类中存在初始化语句(如 static 变量和 static 块),那就依次执行这些初始化语句
复制代码
4.2.3 类的加载:
类的加载指的是将类的 .class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后将在堆内创建一个这个类的 Java.lang.Class 对象,用来封装类在方法区的类的对象。看下图:
复制代码
类的加载的最终的产品是位于堆区中的 Class 对象。
Class 对象封装了类在方法区的内存 数据结构,并且向 Java 程序员提供了访问方法区的数据结构接口。
加载 .class 文件的几种方式:
1、从本地系统直接加载。
2、通过网络下载 .class 文件。
3、从 zip jar 等归档案文件中加载 .class 文件。
4、从专有 数据库 中提取 .class 文件。
5、将 Java 源文件动态编译为 .class 文件(服务器)。
加载类的方式:
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载
Class.forName()和ClassLoader.loadClass()的区别:
1、Class.forName():将类的 .class 文件加载到 JVM 中之外,还会对类进行解释,执行类中的 static 块;
2、ClassLoader.loadClass():只干一件事情,就是将 .class 文件加载到 JVM 中,不会执行 static 中的内容,只有在 newInstance 才会执行 static 块;
3、Class.forName(name, initialize, loader):带参函数也可以控制是否加载 static 块。并且只有调用了 newInstance()方法采用调用构造函数,创建类的对象。
复制代码
4.2.4 加载器:
JVM 的类加载是通过 ClassLoader 及子类来完成的,类的层次关系和加载顺序可以由下图来描述:
复制代码
1、Bootstrap ClassLoader(启动类加载器):
负责加载 $JAVA_HOME 中 jre/lib/rt.jar 里所有的 class,由 C++ 实现,不是 ClassLoader 子类。
2、Extension ClassLoader(扩展类加载器):
负责加载 Java 平台中扩展功能的一些 jar 包,包括 $JAVA_HOME 中jre/lib/*.jar 或 -Djava.ext.dirs 指定目录下的 jar 包。
3、App ClassLodaer(应用类加载器):
负责记载 classpath 中指定的 jar 包及目录中 class。
4、Custom ClassLoader(自定义类加载器):
属于应用程序根据自身需要定义的 ClassLoader,如 tomcat、jboss 都会根据 j2ee 规范自行实现 ClassLoader。
加载过程中会先检查类是否被已加载,检查顺序是自底向上,从 Custom ClassLoader 到 BootStrap ClassLoader 逐层检查,只要某个 ClassLoader 已加载就视为已加载此类,保证此类只被所有 ClassLoader 加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。复制代码
4.2.5 委派机制:
JVM 在加载类时默认采用的是 双亲委派 机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器完全可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
双亲委派机制:
1、当 AppClassLoader 加载一个 class 时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器 ExtClassLoader 去完成。
2、当 ExtClassLoader 加载一个 class 时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给 BootStrapClassLoader 去完成。
3、如果 BootStrapClassLoader 加载失败(例如在 $JAVA_HOME/jre/lib 里未查找到该 class),会使用 ExtClassLoader 来尝试加载。
4、若 ExtClassLoader 也加载失败,则会使用 AppClassLoader 来加载,如果 AppClassLoader 也加载失败,则会报出异常 ClassNotFoundException。复制代码