<a href="http://www.itheima.com" target="blank">android培训</a>、<a href="http://www.itheima.com" target="blank">java培训</a>、期待与您交流!
类加载器
Java虚拟机与程序的生命周期
在如下几种情况下,java虚拟机将结束生命周期
1. 执行了System.exit()方法\
2. 程序正常执行结束
3. 程序在执行过程中遇到了异常或错误而异常终止
4. 由于操作系统出现错误而导致java虚拟机进程终止
类的加载,连接与初始化
加载:查找并加载类的二进制数据
连接:
验证:确保被加载的类的正确性
准备:为类的静态变量分配内存,并将其初始化为默认值
解析:把类中的符号引用转换为直接引用
初始化:为类的静态变量赋于正确的初始值
Java程序对类的使用方式可分为两种
主动使用(六种)
Ø 创建类的实例
Ø 访问某个类和接口的静态变量,或者对该静态变量赋值
Ø 调用类的静态方法
Ø 反射(如:Class.forName(“java.lang.ArrayList”))
Ø 初始化一个类的子类
Ø Java虚拟机启动时被标明为启动类的类(如用cmd运行某个java类)
被动使用(除了主动使用外)
所有的Java虚拟机实现必须在每个类或接口被Java程序”首次主动使用”时才被始化他们
类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构.
加载.class文件的几种方式
. 从本地系统中直接加载
. 通过网络下载.class文件(URLClassLoadder)
. 从zip,jar等归档文件中加载.class文件
. 从专有数据库中提取.class文件
. 将Java源文件动态编译为.class文件
类的加载的最终产品是位于堆区中的Class对象.
Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口.
类的加载有两种类型:
Ø Java虚拟机自带的类加载器
1. 根类加载器(Bootstrap).//C++编写.程序员无法获得该类
2. 扩展类加载器(Extension).//java编写
3. 系统类加载器(System). //java编写
Ø 用户自定义的类加载器
1. jav.lang.ClassLoader的子类
2. 用户可以定制类的加载方式
类加载器并不需要等到某个类被”首次主动使用”时再加载它.
JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过和中遇到了.class文件
存在或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkeageError),如果这个类一直没有被程序使用,那么类加载器就不会报告错误.
类的验证
类被加载后,就进入连接阶段,连接就是将已经读入到内存中的类的二进制数据合并到虚拟机的运行环境中去.
类的验证内容:
1. 类文件的结构检查
确保类文件遵从Java类文件的固定格式
2. 语义检查
确保类本身符合Java语言的语法规定,比如验证final类型的类没有子类,以及final类型的方法没有被覆盖
3. 字节码验证
确保字节码流能够被Java虚拟机安全的执行.字节码流代表Java方法(包括静态方法和实例方法),它是由被称作操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数.字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数.
4. 二进制兼容性验证
确保相互引用的类之间协调一致.例如:在Worker类的gotoWork()方法中会调用Car类的run()方法.Java虚拟机在验证Worker类时会检查在方法区内是否存在Car类的run()方法,假如不存在(当Worker类和Car类的版本不兼容,就会出现这种问题),就会抛出NoSuchMethodError错误.
类的准备:
在准备阶段,Java虚拟机为类的静态变量分配内存.并设置默认初始值.例如:对以下Sample类,在准备阶段,将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量分配8个字节的内存空间,并且赋予默认值0.
public void Sample
{
private static int a = 1;
public static long b ;
static
{
b = 2;
}
…
}
类的解析:
在解析阶段,Java虚拟机会把类的二进制数据中的符号引用替换为直接引用,例如:在Worker类中的gotoWork()方法中会引用Car类的run()方法.
public void gotoWork()
{
Car.run();//这段代码在Worker类的二进制数据中表示为符号引用
}
在Worker类的二进制数据中,包含了一个对Car类run()方法的符号引用,它由run()方法的命名和相关描述符组成.在解析阶段,Java虚拟机会把这个符号引用替换为一个指针,该指针指向Car类的run()方法区内的内存位置,这个指针就是直接引用.
类的初始化:
在初始化阶段,Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值.在程序中,静态变量的初始化有两种途径:
1. 在静态变量的声明处进行初始化;
2. 在静态代码块中进行初始化.
例如在以下代码中,静态变量a和b都被显示初始化,而静态变量c没有被显示初始化,它将保持默认值0.
public class Saple
{
private static int a = 1;
public static long b;
public static long c;
static
{
b = 2;
}
}
静态变量的声明语句,以及静态代码块都被看用类的初始化语句,Java虚拟机会按照初始化语句在类文件中的先后顺序
来依次执行它们.例如以下类被初始化后,它的静态变量a的取值为4.
public class Sample
{
static int a = 1;
static
{
a = 2;
}
static
{
a = 4;
}
public static void main(String[] args)
{
System.out.println(“a=”+a);//打印a=4
}
}
类的初始化步骤:
Ø 假如这个类还没有被加载和连接,那就先加载和连接
Ø 假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类
Ø 假如类中存在初始化语句,那就依次执行这些初始化语句.
类的初始化时机
1.除了上述六种情形,其它使用Java类的方式都被看作是被动使用,不会导致类的初始化.
2.只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用
public class Test3
{
public static void main(String[] args)
{
System.out.println(Child.a);//不会初始化Child因为a不是当前类Child中的静态变量
Child.show();//该方法也不Child类中的静态方法,所以也不会初始化Child类
}
}
class Parent
{
static int a = 3;
static
{
System.out.println("hello world!");
}
public static void show()
{
System.out.println("parent");
}
}
class Child extends Parent
{
static
{
System.out.println("child");
}
}
3.调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化.
类加载器
Java虚拟机自带了以下几种加载器
l 根(Bootstrap)类加载器:该加载器没有父加载器.它负责加载虚拟机的核心类库,如java.lang.*等.根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库.根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承java.lang.ClassLoader类.
l 扩展(Extension)类加载器:它的父加载器为根类加载器.它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK安装目录的jie\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载.扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类.
l 系统(System)类加载器:也称为应用加载器,它的父加载器为扩展类加载器.它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器,系统类加载器是纯Java类,是java.lang.ClassLoader类的子类.
Java编译器会为虚拟机转换源指令.虚拟机代码储存在以.class为扩展名的类文件中.每个类文件都包含某个类或接口的定义和代码实现.这些类文件必须由一个解释器进行解释,该解释器能够将虚拟机的指令集翻译成目标机器的机器语言.
Java虚拟机中的所有类加载器采用具有父子关系的树型结构进行组织,在实例化每个类加载对象时,需要为其指定一个父级类加载器对象或者默认采用系统壮装载器为其父级类加载器.
类加载器的层次结构:
类加载器的委托机制
当java虚拟机要加载一个类时,用哪个类加载器呢?
Ø 首先当前线程的类加载器去加载线程中的第一个类
Ø 如果类A中引用了类B,java虚拟机将使用加载类A的类加载器加载类B.
Ø 还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类.
每个类加载器加载类时,又先委托给其上级类加载器
Ø 当所有加载器没有加载类,回到发起者类加载器,还加载不了,则抛出ClassNotFoundException,不是再去找发起者类加载器下面的加载器.
Ø 类加载器有一种父子关系.除了Bootstrap类加载器外,每个类加载器都有且只有一个父类加载器.根据规定,类加载器会为它的父类加载器提供一个机会,以便加载任何给定的类,并且只有在其父类加载失败时,它才会加载该给定类.例如:当要求系统(System)加载器加载一个系统类(比如:java.util.ArrayList)时,它的首先要求扩展类(Extension)加载器进行加载,该扩展类加载器则首行要求引导类(Bootstrap)加载器进行加载.引导类加载器查找并加载rt.jar中的这个类,而无须其它的类加载器做更多的搜索.
定义类加载器:如果某类加载器能够加载一个类,那么该类加载器就称作定义类加载器,定义类加载器及其所有子加载器称作初始类加载器.
需要指出的是,加载器之间的父子关系实际上指的是加载器对象之间的包装关系,而不是类之间的继承关系.一对父子加载器可以是同一个加载器的两个实例,也可能不是.在子加载器对象中包装了一个父加载器对象.
父亲委托机制的优点是能够提高软件系统的安全性.因为在此机制下,用户自定我的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器加载的可靠代码.例如:java.lang.Object类总是由根类加载器加载,其它任何用户自定义的类加载器都不能加载含有恶意代码的java.lang.Object类.
命名空间:每个类加载器都有自已的命名空间,命名空间由该类加载器及所有父加载器所加载的类组成.在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,有可能出现类的完整名字(包括类的包名)相同的两个类.
运行时包:由同一类加载器加载的属于相同包的类组成了运行时包.决定两个类是不是属于同一个运行时包,不仅要看他们的运行时包是否相同,还要看定义类加载器是否相同,只有属于同一运行时包的类才能互相访问包可见(即默认访问级别)的类和类成员.这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员.假设用户自定义了一个类java.lang.Spy,并由用户自定义的类加载器加载,由于java.lang.Spy和核心类库java.lang.*由不同的类加载器加载,它们属于不同的运行时包,所以java.lang.Spy不能访问核心类库java.lang包中的可见成员.
编写自己的类加载器:
我们可以编写自己的用于特殊目的的类加载器,这使得我们可以在向虚拟机传递字节码之前执行定制的检查.
如果要编写自己的类加载器,只需要继承ClassLoader类,然后覆盖findClass(String className)方法.