在java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的
加载:
- 通过类的全限定名来获取定义此类的二进制流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类各种数据的访问入口
上面第一点,有很大的灵活性,可以从ZIP包中读取,从网络中获取,运行时计算生成(动态代理proxy),由其他文件生成(jsp),从数据库读取....
加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需要的格式存储在方法区之中。然后在内存中实例化一个java.lang.Class对象(并没有明确规定是在java堆中,对与HotsPot虚拟机而言,Class对象比较特殊,它虽然是对象,但是放在方法区里面),这个对象作为程序访问方法区中的这些类型数据的外部接口。
验证:
各种验证....
符号引用验证,对类自身以外(常量池中各种符号引用)的信息进行匹配性校验
符号引用中通过字符串描述的权限定名是否能找到对应的类....
准备:
正式为类变量分配内存并设置类变量初始值阶段,这些变量使用方法区内存。
解析:
将常量池中的符号引用替换为直接引用
初始化:
<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的。虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步。
类初始化条件:
- 使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用这个类的静态方法。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化则需要先触发其初始化。
- 当初始化一个类的时候,发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法那个类),虚拟机会先初始化这个类
- 当使用1.7的动态语言支持,如果一个,,,,
以上5中情景称为对一个类的主动引用,除此之外,所有引用类的方式都不会触发初始化,成为被动引用。
例:
- 通过子类引用父类的静态字段,只会触发父类的初始化而不会触发子类的初始化。
- 通过数组定义来引用类,不会触发此类的初始化
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类初始化。
类与加载器
对与任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在jvm中的唯一性。
比较两个类是否“相等”,只有在这个两个类是同一个类加载器加载的前提下才有意义。否则即使这两个类来源同一个Class文件,被同一个虚拟机加载,但是加载他们的加载器不同,那么这两个类必定不相等。
这里所指的相等,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。
public class RunMain {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream in = getClass().getResourceAsStream(fileName);
if (in == null)
return super.loadClass(name);
byte[] b = new byte[in.available()];
in.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
};
Object obj = myLoader.loadClass("i.main.RunMain").newInstance();
System.out.println(obj.getClass());//class i.main.RunMain
System.out.println(obj instanceof i.main.RunMain);//false
}
}
双亲委派模型
一种启动类加载器(Bootstrap ClassLoader)虚拟机实现的加载器
另一种java语言实现,独立于虚拟机外部,都继承自抽象类java.lang.ClassLoader
- 启动类加载器(Bootstrap ClassLoader):这个类加载器负责加载存放在<JAVA_HOME>\lib目录中的,启动类加载器无法被java程序直接引用,在自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用null代替即可。
- 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,可以直接使用扩展类加载器。
- 启用程序类加载器(Application ClassLoader):这个类由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般称为系统加载器。它负责加载用户类路径(ClassPath)上所制定的类库,可以直接使用这个类加载器,如果应用程序中没有自定义过的类加载器,一般情况下这个就是程序中默认的类加载器。
- 如果有必须要还可以加入自己定义的类加载器
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。否则的话,如果不使用该模型的话,如果用户自定义一个java.lang.Object类且存放在classpath中,那么系统中将会出现多个Object类,应用程序也会变得很混乱。如果我们自定义一个rt.jar中已有类的同名Java类,会发现JVM可以正常编译,但该类永远无法被加载运行。
String str = a+b+c 编译成.class文件后,会通过StringBuilder优化