JVM初探——JAVA类加载器总结

1.java类加载过程是什么?
JVM将类加载过程分为三个步骤:加载(loader),链接(Link),和初始化(Initialize),而其中的链接又分为三个步骤。
在这里插入图片描述
装载(加载):查找并加载类的二进制数据。
链接:
验证:确保并加载类的正确性。
准备:为类的静态变量分配内存,并且使其初始化为默认值
解析:把类的符号引用转成直接引用
初始化:为类的静态变量赋予正确的初始值。

2.接下来,我们具体分析下每一个过程中到底发生了什么
(1)类的加载
类的装载实际上是将类的.class文件中的二进制数据读到方法区中,然后在JAVA堆中创建java.lang.class对象,用来封装类在方法区中的数据结构,类的装载的最终产物是JAVA堆中的class对象,class对象封装了类在方法区中的数据结构,并向JAVA程序员提供了访问该方法区的数据结构的入口。

通过不同的类加载器,可以从来自不同来源加载类的二进制数据。通常有以下几个来源:
(1):通过本地系统加载class文件,这是绝大多数类的加载方式。
(2):从JAR包加载class文件,这种方式也是很常见的,JDBC编程时用到的数据库驱动类就放在jar包下,JVM可以直接从JAR包下加载该class文件。
(3):通过网络加载class文件
(4):把一个java源文件动态编译,并执行加载。
类加载器无需等到“首次使用”时才加载该类,通常虚拟机规范允许系统预先加载某个类。

注意1:加载阶段可以使用系统提供的类加载器来完成,也可以通过用户的自定义类加载器来完成。加载阶段和链接阶段的部分内容(例如一部分字节码格式的验证)是交叉进行的,加载阶段尚未完成,连接阶段就已经开始。
注意2:加载阶段完成以后,虚拟机外部的二进制字节流就按照虚拟机所需要的格式保存在方法区中,而且在JAVA堆中创建Java.lang.Class对象。

(2)类的链接
类的链接又分为三大步骤:分别是验证阶段,准备阶段,和解析阶段
1.验证阶段
验证阶段主要是用于检查被加载的类是否有正确内部结构,并和其他类协调一致,java是相对C++语言更加安全的语言,例如,它会检查数组是否有越界异常,验证阶段是非常重要的一个阶段,它会直接保障应用是否会恶意入侵的一道非常重要的保障,验证的目的在于确保class文件字节流中包含信息是否会危害到当前的java虚拟机,其中包含四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。

   (1)文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。

   (2)元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。

   (3)字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。

   (4)符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。

2.准备阶段
类准备阶段为静态变量开辟内存,并为他设置默认值。
这些过程都将会在方法区中进行分配,准备阶段不分配类的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配到JAVA堆中。
比如:
Public static int value =100;
在准备阶段的时候赋予value值默认值为0,在初始化阶段的时候赋予实际值100。
Public static final int value =100;
对于常量的赋值在准备阶段就赋予100;

3.解析阶段
解析阶段将类的二进制数据的符号引用转变为直接引用
符号引用是用一组符号来描述所定义的目标,布局和内存无关。
直接引用是指向目标的指针,能够直接定位语句,该引用是一定和布局有关的,并且一定是被加载进来的。

(3)初始化
初始化阶段貌似看起来和准备阶段有所矛盾,但其实没有矛盾。如果类中有语句是private static int a = 10;它在类的加载中的阶段是这样的,首先字节码文件被加载到内存中,先进行连接的验证这个步骤,经过验证的步骤后,再进入连接的准备阶段,给a分配内存,因为a是静态变量,所有在准备阶段给它赋值为0,然后到解析,最后在初始化阶段,将它真正的值10赋给a,所以就有了a=10;

3.在面试过程中,面试官通常会问我们类的加载的过程是怎么样的,你应该如何回答?(面试题)
类的加载和执行分为五大阶段,分别是加载,链接(验证,准备,解析),初始化,使用和卸载
类的加载阶段就是讲经过编译阶段生成的class对象加载到内存中,并且在方法区中生成该类的信息,在堆中生成java.lang.class对象,用来封装类在方法区中的数据结构,实际上类的加载的最终产物是java.lang.class对象。类的链接又分为三大步骤,在验证阶段,我们要验证class文件的文件格式,元数据,字节码以及符号引用。在准备阶段,为类的静态变量分配内存并赋予默认值在解析阶段将类的符号引用变为直接引用。最后一个阶段是初始化阶段,这个阶段为类的静态变量,类的静态的代码块赋予真正的值,在这里,我们的顺序是先父类的静态变量和静态代码块,再是子类的静态变量和静态代码块。

4.类什么时候才被初始化(类的加载时机)?
(1):当实例化一个对象时,即new一个对象的时候。
(2):访问某个类或者接口的静态变量的时候,或者对静态变量进行赋值的时候。
(3):调用类的静态方法时
(4):反射(class.forname(“com.lpd.load”))
(5):初始化一个类的子类的时候(当初始化子类的时候先初始化父类)接口除外
(6):JVM启动时标明的启动类,即文件名和类名相同的类。
在这里插入图片描述

5.类加载的时候肯定需要类加载器,那么有哪几种类加载器呢?
类加载器负责加载所有的类,其为所有被载入内存(虚拟机)的类生成一个java.lang.class实例对象。一旦一个类被加载进JVM中,同一个类就不会被再次载入。正如一个对象只有唯一的标识一样,一个载入JVM的类也有且仅有唯一的标识。在JAVA中,一个类用全限定类名(包名和类名)作为标识。而在JVM中,一个类用全限定类名和类加载器作为唯一标识。
例如:如果在pg包下有一个名为person的类,被加载器ClassLoader的实例kl负责加载,则该person对应的class文对象在JVM中被表示为person.pg.kl。这一久意味着不同类加载器加载一个相同的class文件是不同的,他们所加载所得到的是完全不同的,互相不兼容。

从JVM角度来看,只存在两种类加载器,一种为启动类加载器(bootstap classaloader),这个加载器是由C++实现,是虚拟机自身的一部分,另外的加载器都是由JAVA语言实现的,独立于虚拟机外部,并且都继承自抽象类java.lang.classloader。
启动类加载器(BootStrap ClassLoader):也可以称之为根类加载器,它负责加载虚拟机启动时需要的类库,它只能加载自己能够识别的类。

扩展类加载器(Extendition Classloader):它负责加载<JAVA_HOME>/LIB/EXT目录下的或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以使用扩展类加载器。

应用程序类加载器(Application ClassLoader):也可以称之为用户类加载器。负责加载用户路径Classapth上所指定的类库,如果应用程序没有自定义过自己的类加载器,一般情况下这就是默认的类加载器。

自定义类加载器:自己定义的类加载器。

类加载器加载class文件大概分为以下几个步骤:
(1)检测此class是否加载过,即在缓冲区是否有该类对应的class文件对象,如果有则直接进入第八步,如果没有则进行第二步。
(2)如果没有父类加载器,则要么是parent根类加载器,要么本身就是根类加载器。则跳到第4步,否则进行第三步。
(3)请求使用父类加载器去载入目标类,如果载入成功就跳到第八步,否则接着执行第五步。
(4)请求使用根类加载器去载入目标类,如果载入成功就跳到第八步,否则跳至第七步。
(5)当前类加载器尝试寻找class文件,如果找到就执行第六步,如果找不到第七步。
(6)从文件中载入class,成功后跳转至第八步。
(7)跑出ClassNotfoundException异常。
(8)返回对应的java.lang.Class对象。
注意:此执行过程实则为java类加载机制中的双亲委派机制和缓存机制。

6.上面我们提到了类加载机制,那么类加载机制分为哪几种?
Java的类加载机制主要分为以下三个:
(1)全盘负责:就是当一个类加载器加载一个class文件时,该class文件所依赖和引用的class文件都是由这个类加载器负责,除非显示使用另一个类加载器来载入。
(2)双亲委派:双亲委派机制就是先让此类的父类加载器来试图加载,只有在父类加载器无法完成这个任务的时候,才尝试从自己的类路径中加载该class。通俗点讲,就是当一个类加载器接到加载的任务的时候,先交给他的父类加载器尝试加载,依次往上找他的父类,如果父类加载器能够完成这个任务,则成功加载。只有父类加载器无法完成这个任务的时候,才自己去加载。
(3)缓存机制:缓存机制保证所有加载过得class都会被缓存,当程序中使用某一个class时,类加载器先会去缓存区中寻找该class,只有当缓存区中不存在这个class对象时,JVM才会读取该类对应的二进制码数据,将其转换成class对象,放入缓存区中,这就是为什么修改了class文件后,必须要重新启动JVM,程序才能被载入。

7.面试中经常会问到双亲加载机制,那么双亲加载机制的具体流程是什么呢?(面试题)
在这里插入图片描述
双亲委派机制的工作原理是如果一个类收到了加载的任务的时候,它不会自己先去加载,它先交给它的父类加载器进行执行,如果父类加载器还有父类加载器,他就会继续往上委托,最终到达最顶层的启动类加载器,如果父类加载器可以完成任务,那就成功返回,如果完不成任务,子类加载器才会尝试进行加载。这就是双亲委派模式。就是儿子都很懒,什么任务都要交给父亲来完成,如果父亲完成不了,才会交给儿子再想办法来完成。
优势:
双亲委派机制的
(1)第一大优势是java类随着类加载器一起具备了一种带有优先级的层次关系,通过这种关系,可以避免类的重复加载,当父类加载器已经加载了这个类以后,那么子类加载器就不会再次重复加载。
(2)另一大优势是安全因素,java核心api定义的类型不会被替换,当从网络上传来一个java.lang.Interger类时,通过双亲委派机制传递到启动类加载器,而启动类加载器在核心JAVA api发现这个名字的类,发现该类已经被加载,并不会重新加载网络传输过来的这个类,而是直接从缓存区中传过来已经加载过的Integer.class。这样可以防止核心API被篡改。

8.简述一个类的生命周期。(面试题)
一个类从加载到虚拟机内存开始,直到卸载出内存为止,它的生命周期包括:加载,验证,准备,解析,初始化,使用以及卸载。一共七个阶段。

9.有几种情况类结束它的生命周期?
(1)执行了system.exit()方法
(2)程序正常运行完
(3)程序在执行的过程中遇到了异常或者错误而异常终止。
(4)由于操作系统出现错误而导致JAVA虚拟机停止运行

10.请简述JAVA中new一个对象的过程?(面试题)
简洁回答:
第一步为类的加载和初始化(初次使用的时候),具体可分为加载,验证,准备,解析以及初始化这几大步骤。第二步为创建对象,具体可分为:
1.在堆区为实例对象分配内存
2.为实例变量赋予默认值
3.执行实例初始化代码(先父类后子类,先非静态代码块再构造方法),
4.将堆区对象的地址赋给栈内的引用变量。

深层次回答:
当JAVA虚拟机遇到new指令的时候,先会去检查这个new指令的参数在常量池中能否定位到一个类的符号引用,并且检查这个符号引用代表的类是够已经被加载、解析和初始化过,如果没有,则必须先进行相应的类加载过程。类加载过程具体分为加载,验证,准备,解析以及初始化这几大步骤。
当类加载完成后,接下来虚拟机为实例对象分配内存,对象所需的内存大小在类加载完成后便可以完全确定,为对象分配内存空间的任务等同于把一块确定大小的内存从JAVA堆中划分出来,目前常用的两种方式为:指针碰撞和空闲列表。
内存分配完成之后,虚拟机将分配到内存空间的数据都初始化为默认值(不包括对象头),接下来虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息,对象的哈希码以及对象的GC分代年龄,这些信息都存储在对象头中,从虚拟机的角度来看,一个新的对象已经产生,但是从程序的角度来看,执行new操作后接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正的对象就产生了。

11.为对象分配空间的任务等同于把一块确定大小的内存从JAVA堆中划分出来,有哪些方式?(面试题)
有指针碰撞方式和空闲列表方式。
**指针碰撞方式:**假设JAVA堆中的内存是绝对规范的,一部分用来存储用过的内存,一部分用来存储空闲的内存,中间放着一个指针作为分界点的指示器。那所分配内存就相当于向指针向空闲区域移动对象大小的距离。
**空闲列表方式:**假设JAVA堆中的内存并不是规整的,已使用的内存和空闲的内存是相互交错的,虚拟机必须维护一个空闲列表,记录上内存中有哪些空间是可以用的,在分配的时候在内存空间中找到够对象大小的内存提供给它。

12.如何解决对象空间分配的时候的并发问题?(面试题)
在并发情情况时,不一定是线程安全的,因为当你给A对象分配好内存,指针还没有修改,对象B又用了以前指针分配内存的情况,解决这个问题有两种方案。
第一种为分配内存空间的动作同时进行,实际上虚拟机采用CAS加上失败重试的方式保证了更新操作的原子性。
第二种方式为内存分配的动作按照线程分配在不同的内存进空间进行。在每个线程的JAVA堆中预先分配好一小块内存,称为本地线程分配缓存。

13.请简述对象的内存布局(面试题)
分别为对象头,实例数据和对象填充。
其中对象头中第一部分存储对象自身运行时的数据,例如GC分代年龄,哈希吗,锁状态标识,线程持有的锁,偏向时间戳,偏向线程ID,官方称为“MARK WORD”。第二部分存储的是类型指针,即对象指向它的类元数据的指针。虚拟机通过这个指针来确定这个对象属于哪个类。
实例数据中存储的是对象真正存储的有效数据。
对象填充不是必然存在的,也没有特别的意义,仅仅是占位符。

谢谢观看,欢迎指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值