类加载流程图
类加载过程.jpg
执行的每一个class文件都需要加载,加载完成在方法区创建一个class文件的对象,记录class文件信息。
类加载的时机
其中加载阶段和连接阶段是交叉进行的,并不是加载完才开始连接 ,其中连接的解析过程和初始化过程也是交叉进行的。这个过
程不一定是完全执行完的,存在加载进来,但是没有初始化。
加载
1.通过一个类的全类名,获取类的的class文件二进制子节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据结构的访问入口
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危
害虚拟机自身的安全。
准备
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。
准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
1. public static int value=1;
在准备阶段value初始值为0 。在初始化阶段才会变为1。
2.public static final int value =1;
被final和static 修饰的基本类型和String不会再这个阶段赋值初始值
编译时Javac将会为该常量生成ConstantValue属性,在类加载的准备阶段虚拟机便会根据ConstantValue为常量设置相应的
值,如果该变量没有被final修饰,或者并非基本类型及字符串,则选择在类构造器中进行初始化。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化
类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余
动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的。
类初始化时机:
1、创建类的实例
2、访问类的静态变量(除常量【被final修辞的静态变量】原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不
是域(field)来对待。
3、访问类的静态方法
4、反射如(Class.forName("my.xyz.Test"))
5、当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化
6、虚拟机启动时,定义了main()方法的那个类先初始化
举例
<1>static 变量准备阶段初始值,初始化阶段赋值
public static void main(String[] args) {
CheckPrepareAssignmentTest checkPrepareAssignmentTest =new CheckPrepareAssignmentTest();
}
}
class CheckPrepareAssignmentTest{
/*
* 准备阶段会根据先后顺序给每个静态变量赋值,所有这个实例初始化为优先执行,再此之前后面的静态变量还没有没有赋值
* 所以此时打印的字段应该是准备阶段赋予的初始值
*/
private static CheckPrepareAssignmentTest checkPrepareAssignmentTest = new CheckPrepareAssignmentTest();
private final static Integer a = 1;
private final static int b = 1;
private static int c = 1;
public CheckPrepareAssignmentTest(){
System.out.println(String.format("初始化方法 此时的 a = %s b=%s c=%s",a,b,c));
}
}
CheckPrepareAssignmentTest 类中创建一个自身的静态变量,准备阶段会根据先后顺序给每个静态变量赋值,所有这个实例初始化为优先执行,再此之前后面的静态变量还没有没有赋值所以此时打印的字段应该是准备阶段赋予的初始值。
结果
初始化方法 此时的 a = null b=1 c=0
初始化方法 此时的 a = 1 b=1 c=1
验证结论:
1.准备阶段会给静态变量赋值,包括static final 修饰的非基本类型和String
2.static final 修饰的基本类型或者String 会直接从ContantsValue中赋值,不会赋初始值(b)
<2>加载但是不一定初始化
public class StudyClassLoader
{
public static void main(String[] args)
{
System.out.println(Children.a);
}
}
class Father{
public static int a = 1;
static {
System.out.println("Father static{} 执行");
}
}
class Children extends Father{
static {
System.out.println("Children static{} 执行");
}
}
结果
Father static{} 执行
1
结论:
1.调用父类的静态变量,本身不需要初始化
2.Children类对象信息加载了,但是本身并没有初始化(初始化会打印Static{}里面的内容)
类加载器
BootStarp类加载器
由c/c++语言编写,启动类加载器,并非继承ClassLoader(疑问:是否是因为如果是java编写
那么,谁又来加载它呢),基本上Jdk里面的核心类都是BootStarp类加载器加载的
public class ClassLoaderTest
{
public static void main(String[] args)
{
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL e:urls){
System.out.println(e.toExternalForm());
}
}
}
结果
file:/C:/Program%20Files/Java/jdk1.8.0_161/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_161/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_161/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_161/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_161/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_161/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_161/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_161/jre/classes
这些都是BootStarp类加载器加载的包路径
拓展类加载器(Extension ClassLoader)
1.java语言编写 ,由sun.misc.Launcher$ExtClassLoader实现。
2.派生于ClassLoader类
3.从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载
类库。如果用户创建的JAR放在此目录下,也会由拓展类加载器自动加载
在源码中可知ExtClassLoader加载类的目录
方法:sun.misc.Launcher.ExtClassLoader#getExtDirs
核心代码:String var0 = System.getProperty("java.ext.dirs");
应用程序类加载器(系统类加载器,AppClassLoader)
1.java语言编写, 由sun.misc.Launcher$AppClassLoader实现。
2.派生于ClassLoader类
3.它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库
4.该类加载器是程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载
5.通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器
自定义类加载器
1.隔离加载类
2.修改类加载的方式
3.拓展加载源
4.防止源码泄漏
这里没有研究
双亲委派机制
类加载器图.png
作用
1.避免类的重复加载,如上
2.保护程序安全,防止核心API被随意修改
启动类加载器可以抢在标准扩展类装载器之前去装载类,而标准扩展类装载器可以抢在类路径加载器之前去装载那个类,类路径
装载 器又可以抢在自定义类加载器之前去加载它。所以Java虚拟机先从最可信的Java核心API查找类型,这是为了防止不可靠
的类扮演被信任的类,试想一 下,网络上有个名叫java.lang.Integer的类,它是某个黑客为了想混进java.lang包所起的名字,
实际上里面含有恶意代码,但是这种 伎俩在双亲模式加载体系结构下是行不通的,因为网络类加载器在加载它的时候,它首先
调用双亲类加载器,这样一直向上委托,直到启动类加载器,而启动类加载 器在核心Java API里发现了这个名字的类,所以它
就直接加载Java核心API的java.lang.Integer类,然后将这个类返回,所以自始自终网络上的 java.lang.Integer的类是不
会被加载的。
3.保证核心API包的访问权限
但是如果这个移动代码不是去试图替换一个被信任的类(就是前面说的那种情况),而是想在一个被信任的包中插入一个全新
的类型,情况会怎样呢?比如一个名为 java.lang.Virus的类,经过双亲委托模式,最终类装载器试图从网络上下载这个类
,因为网络类装载器的双亲们都没有这个类(当然没有了,
因为 是病毒嘛)。假设成功下载了这个类,那你肯定会想,Virus和lang下的其他类痛在java.lang包下,暗示这个类是Java
API的一部分,那么是不是也拥有修改Java.lang包中数据的权
限呢?答案当然不是,因为要取得访问和修改java.lang包中的权 限,java.lang.Virus和java.lang下其他类必须是属于同一
个运行时包的,什么是运行时包?运行时包是指由同一个类装载器装载的、属 于同一个包的、多个类型的集合。考虑一下,
java.lang.Virus和java.lang其他类是同一个类装载器装载的吗?不是 的!java.lang.Virus是由网络类装载器装载的!
自定义类:java.lang.Test(java.lang包需要访问权限,阻止我们用包名自定义类)
总结一句:就是会从最上面的类加载器开始加载,知道找到能加载该类的类加载器加载为止,有点像责任链模式