先抛出几个问题:
1.执行main(),类加载过程:
启动Java进程初始化JVM时,会调用内核里面的main程序入口函数,这个函数链接着Java的启动方法,接着会先加载JVM、JDK自身的类,最后会根据主方法中的代码,按需加载自定义的类.......
2..类加载机制和对象的创建有什么关系吗?
类加载和创建对象的关系,其实很简单呀,一个类被定义好之后,在本地编译后是class字节码,这是不能直接用于创建对象的,所以想要创建对象,就必须先把类的元信息载入内存,所以创建对象前必须要装载类。
3.双亲委派的作用?
避免重复加载,以及避免核心类篡改,好比现在我某个jar包中,也使用了String类,但如果没有双亲委派,就只能自己再加载一个String类,有了双亲委派之后,直接从上层类加载器里面拿就可以了。还有,如果没有双亲委派,这意味着所有类加载器,都可以加载任意的类,那用户可以直接自己写一个全限定名相同的Object类,将Java原生的Object替换掉。
4.双亲委派机制?
父类是不可以把自己的任务,下抛给子类加载器去完成的。
而双亲委派机制里面,子类的加载任务先交给父类去完成,如果父类加载器无法加载,在反抛给子类加载器自己去加载。
这里要记住的是,双亲委派机制里面的父类加载器,抛给子类加载器的任务,本身就属于子类加载器,那个任务并不是父类加载器的。
比如现在有一个类a,它属于顶层父类加载器的加载范围,此时这个类,就必须由顶层父类加载器自己来加载,不可以下抛给子类加载器。
5.静态常量不会导致类初始化,子类调用父类的静态属性不会导致类初始化
(1)静态常量不会导致类初始化 因为静态常量在类初始化前就已经赋值(编译时,已经初始化。这个编译不是指.java文件编译成.class文件的过程,而是指类加载时,字节码被编译成机器码的时候。)
(2)子类继承的父类静态属性/方法实际上还是父类的 调用时只会初始化父类
1.类的加载过程
类加载过程被分为三个步骤,五个阶段,分别为加载、验证、准备、解析以及初始化。加载、验证、准备、初始化这四个阶段的顺序是确定的。但解析阶段不一定,为了支持Java语言的运行时绑定特性,在某些情况下可以在初始化阶段之后再开始(也称为动态绑定或晚期绑定)。
1。加载过程:加载阶段是指通过完全限定名查找Class文件二进制数据并将其加载进内存的过程。
在堆中间中为其创建一个Class
对象,作为程序访问这些数据的入口
2.验证阶段
验证阶段主要用于确保被加载的Class
正确性,检测Class
字节流中的数据是否符合虚拟机的要求,确保不会危害虚拟机自身安全。
3.准备阶段
准备阶段主要是为类中声明的静态变量分配内存空间,并将其初始化成默认值(零值)。不过值得注意的是:这个默认值并非指在Java代码中显式赋予的值,而是指数据类型的默认值。如static int i = 5;
这里只会将i
初始化为0。
在这里进行的内存分配仅包括类成员(static
成员),而实例成员则会在创建具体的Java对象时被一起分配在堆空间中。同时也不包含使用final
修饰的static
成员,因为final
在编译的时候就会分配了,准备阶段会显示初始化。
4.解析阶段
解析阶段主要是把类中对常量池内的符号引用转换为直接引用的过程。值得一提的是,解析操作往往会伴随着JVM在执行完初始化之后再执行
符号引用:用一组符号来描述引用的目标,符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中
直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
5.初始化步骤
类的静态变量赋予正确的初始值,也就是在声明静态变量时指定的初始化值以及静态代码块中的赋值。
初始化情况分为六种:
1.设置静态变量,调用静态变量,调用静态方法,new实例
2.通过反射
3.初始化一个类发现父类没有初始化
3.虚拟机启动时,需指定一个要执行的主类(包含main方法的那个类),虚拟机会初始化这个主类(如SpringBoot的启动类)
同时,一个类被触发初始化时,在它进行初始化的时候,大体步骤如下:
- 如果类还未被加载、连接则先进行加载、连接步骤
- 如果当前类存在直接父类未被初始化,则先初始化直接父类
- 构造器方法中指令按照语句在源文中出现的顺序执行
除了以上几种情况外,其他使用类的方式被看做是对类的被动引用,不会导致类的初始化。比如在子类中调用父类的静态字段、定义该类的数组方式引用、调用该类的常量等情况都不会触发类进行初始化。
2.类的加载器
1.Bootstrap 引导类加载器
引导类加载器只为JVM提供加载服务,开发者不能直接使用它来加载自己的类,加载核心类。
2.Extension 拓展类加载器
3.Application 系统类加载器
应用程序类加载器,也是由sun公司实现的,位于HotSpot
源码目录中的sun.misc.Launcher$AppClassLoader
位置。它负责加载系统类路径java -classpath
或-D java.class.path
指定路径下的类库,也就是经常用到的classpath
路径。应用程序类加载器也可以直接被开发者使用。
。
JVM的类加载机制是按需加载的模式运行的,也就是代表着:所有类并不会在程序启动时全部加载,而是当需要用到某个类发现它未加载时,才会去触发加载的过程。
Java中的类加载器会被组织成存在父子级关系的层级结构。同时,类加载器之间也存在代理模式,当一个类需要被加载时,首先会依次根据层级结构检查自己父加载器是否对这个类进行了加载,如果父层已经装载了则可以直接使用,反之,如果未被装载则依次从上至下询问,是否在可加载范围,是否允许被当前层级的加载器加载,如果可以则加载。
每个类加载器都拥有一个自己的命名空间,命名空间的作用是用于存储被自身加载过的所有类的全限定名(Fully Qualified Class Name
) ,子类加载器查找父类加载器是否加载过一个类时,就是通过类的权限定名在父类的命名空间中进行匹配。而Java虚拟机判断两个类是否相同的基准就是通过ClassLoaderId + PackageName + ClassName
进行判断,也就代表着,Java程序运行过程中,是允许存在两个包名和类名完全一致的class
的,只需要使用不同的类加载器加载即可,这也就是Java类加载器存在的隔离性问题,而Java为了解决这个问题,JVM引入了双亲委派机制(稍后分析)。
3.双亲委派机制
优势:
java类随着它的类加载器存在了一种优先级的层次关系,这样做的优势在于,可以避免一个类在不同层级的类加载器中重复加载,如果父类加载器已经加载过该类了,那么就不需要子类加载器再加载一次。其次,也可以保障Java核心类的安全性问题,比如通过网络传输过来一个java.lang.String
类,需要被加载时,通过这种双亲委派的方式,最终找到Bootstrap
加载器后,发现该类已经被加载,从而就不会再加载传输过来的java.lang.String
类,而是直接返回Bootstrap
加载的String.class
。这样可以有效防止Java的核心API类在运行时被篡改,从而保证所有子类共享同一基础类,减少性能开销和安全隐患问题。
4.自定义类加载器(也符合双亲委派机制)
使用自定义加载器场合:
1.class文件不在classpath路径下
2.class文件在网络传输中进行了加密
继承CLASSLOAD类 ,重写了父类的ClassLoader.findClass()
方法,利用defineClass()
方法在JVM内存中生成了最终的Class
对象。
5.双亲委派机制的破坏
Java程序启动 → JVM初始化C++编写的Bootstrap
启动类加载器 → Bootstrap
加载Java核心类(核心类中包含Launcher
类) → Bootstrap
加载Launcher
类,其中触发Launcher
构造函数 → Bootstrap
执行Launcher
构造函数的逻辑 → Bootstrap
初始化并创建Ext、App
类加载器 → Launcher
类的构造函数中将Ext
设置为App
的父类加载器 → 同时再将App
设置为默认的线程上下文类加载器 → Bootstrap
继续加载其他Java核心类(如:SPI接口) → SPI接口中调用了第三方实现类的方法 → Bootstrap
尝试去加载第三方实现类,发现不在自己的加载范围内,无法加载 → 依赖于SPI的动态服务发现机制,这些实现类会被交由线程上下文类加载器进行加载(在前面讲过,线程上下文加载器在Launcher
构造函数被设置为了App
类加载器) → 通过App
系统类加载器加载第三方实现类,发现这些实现类在App
的加载范围内,可以被加载,SPI接口的实现类加载完成.....
sun.misc.Launcher类是java的入口,在启动java应用的时候会首先创建Launcher类,创建Launcher类的时候回准备应用程序运行中需要的类加载器。
我的理解:Bootstrap把Java核心类(SPI接口)委托给子类加载器(线程上下文类加载器)打破了双亲委派机制