Java是使用 双亲委派模型 来进行类的加载的,所以在描述类加载过程前,我们先看一下它的工作过程
双亲委托模型的工作过程是:
如果一个类加载器(ClassLoader)收到了类加载的请求,它首先不会自己去尝试加载这个类,
而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的
启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,
子加载器才会尝试自己去加载。使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。
java文件通过编译器变成.class文件,接下来类加载器将这些.class文件加载到JVM内存中,即在运行时数据区的方法区内。
类加载的具体过程
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
加载
加载时类加载机制的第一个过程,主要完成以下三件事
1、通过一个类的全限定名来获取其定义的二进制字节流 类加载器 find()找到类全路径,然后基于类加载双亲委派机制进行类装载
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口
我们可以用自带的类加载器加载,也可以使用自己自定义的类加载器加载
验证
确保被加载的类正确性,也是连接阶段的第一步,我们加载好的.class文件不能对我们的虚拟机有害,所以需要做一些基本的验证工作
1、文件格式的验证:验证.class文件字节流是否符合class文件的规范,并且是否能够被当前版本的虚拟机处理
2、元数据验证:主要时对字节码描述的信息进行语义分析,保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类
3、字节码验证:通过数据流和控制流分析,确定程序语义时合法的符合逻辑的
4、符号引用验证:将符号引用转化为直接引用的时候,主要对类自身以外的信息进行校验,确保解析动作能够完成
准备
准备阶段主要为类变量分配内存并设置初始值。这些内存都在方法区分配,这个阶段需要主要类变量和初始值
类变量:会分配内存,但是实例变量不会,实例变量主要随着对象的实例一块分配到堆内存中
初始值:这里的初始值是指数据类型的默认值
解析
虚拟机将常量池中的符号引用转化为直接引用的过程
符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量
直接引用:是可以指向目标的指针,相对偏移量或者是一个能直接或简介定位到目标
初始化
准备阶段已经为类变量赋过一次值,在初始化阶段,程序员可以根据自己的需求来赋值
在初始化阶段,主要为类的静态变量赋予正确的初始值,jvm负责对类进行初始化,主要对类变量进行初始化
声明类变量时指定初始值
使用静态代码块为类变量指定初始值
何时会触发类的初始化:
1、创建类的实例,也就是new的方式
2、访问某个类或接口的静态变量,或者对静态变量赋值
3、调用类的静态方法
4、反射
5、初始化某个类的子类,则其父类也会被初始化
【二】类加载器
双亲委派机制:jvm类加载默认采用的是双亲委派机制,通俗的讲,就是某个特定的类加载器在接到加载类的请求时,会优先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回,无法加载才自己加载。
作用:避免重复加载,更安全,如果不是双亲委派,那么用户在自己的classpath编写一个类,就无法宝成类的唯一性
2、运行时数据区
方法区(Method Area)
方法区只有一个,线程共享的内存区域(线程非安全),生命周期是跟虚拟机是一样的;方法区存储类信息,常量,静态变量,及时编译器编译之后的代码;逻辑上属于堆的部分,垃圾回收一般不会讨论该区域;可能会发生内存溢出(OOM)或栈溢出;
堆 (Heap)
堆只有一个,线程共享的内存区域(线程非安全),生命周期和虚拟机一样, 存储对象或数组,垃圾回收的主要区域,可能会发生OOM异常;对于大多数应用来说,堆空间是jvm内存中最大的一块。Java堆是被所有线程共享,虚拟机启动时创建,此内存区域唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也就变得不那么绝对了。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。从内存回收角度看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;再细致一点的有Eden空间,From Survivor空间,To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好的回收内存,或者更快的分配内存。(如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。)
java虚拟机栈(Stack)
线程私有,生命周期和线程相同,虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧 用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。局部变量表存放了编译期可知的各种基本类型数据(boolean、byte、char、short、int、float、long、double)、对象引用、returnAddress类型(指向了一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量表空间(slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法所需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。在Java虚拟机规范中,对此区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出Stack OverflowError异常;如果虚拟机栈可以动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常
栈帧的组成部分:
1、局部变量表
2、操作数栈
3、动态链接
4、方法返回地址
本地方法栈
本地方法栈与虚拟机栈发挥的作用非常相似,区别就是虚拟机栈为虚拟机执行Java方法,本地方法栈则是为虚拟机使用到的Native方法服务。
程序计数器
程序计数器是一块较小的内存空间,它是当前线程执行字节码的行号指示器,字节码解释工作器就是通过改变这个计数器的值来选取下一条需要执行的指令。它是线程私有的内存,也是唯一一个没有OOM异常的区域。