java 类型的加载,连接,初始化都是在程序运行期间完成的。
运行期动态加载和动态连接(动态扩展的语言, 可以等到运行时再指定接口的实际实现类)。
可以使用java预定义和自定义类加载器,让一个本地的应用程序从网络或其它地方加载一个二进制流作为程序代码的一部分。
如果初始化在解析之前,则称为动态绑定或晚期绑定。
问题一,下面的输出结果是啥?
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
SuperClass[] sca = new SuperClass[10];
System.out.println(ConstClass.HELLOWORLD);
}
}
控制台输出:
SuperClass init!123
hello world
困惑:单独执行 SuperClass[] sca = new SuperClass[10]; SuperClass和SubClass都不会初始化
SubClass.value SubClass类没有初始化, SuperClass被初始化。
ConstClass.HELLOWORLD ConstClass没有初始化。
原理
- 对于静态字段或者静态方法,只有直接定义这个字段或方法的类才会被初始化。
- 编译阶段通过常量传播优化,已经将'hello world'值存储到了NotInitialization类的常量池中了。
初始化(有且只有下面7种初始化触发的场景)
1. new User(); new 实例化
2. User.CONST_SEX 访问静态字段 (final修饰,编译器把结果放入常量池的静态字段除外)
3. UserFactory.getInstance() 访问静态方法
4. 使用java.lang.reflect包的方法对类进行反射调用的时候
5. 初始化一个类的时候如果发现它的父类还没有进行初始化,则先初始化父类
6. 虚拟机启动时的主类(main方法的那个类)
7.不懂就不写了todo(jdk1.7动态语言支持如果java.lang.invoke.MethodHandle实例最后的解析结果xxx)
反射调用的例子
package classloader;
import java.lang.reflect.Field;
public class Foo {
public static void main(String[] args) {
Student stu=new Student();
try {
Field field=stu.getClass().getDeclaredField("name");
field.setAccessible(true);
//获取用get类方法。
System.out.println(field.get(stu));
//设置用set类方法
field.set(stu, "名字被我改了,哈哈");
System.out.println(stu);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Student{
private String name;
static {
System.out.println("Student init!");
}
public String toString() {
System.out.println(this.name);
return this.name;
}
}
控制台输出:
Student init!
null
名字被我改了,哈哈
名字被我改了,哈哈
加载
JVM规范要求虚拟机完成三件事情:
1. 通过类的全限定名获取定义此类的二进制字节流。
2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3. 在内存中生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据的访问入口。
知识点落地: 二进制流可以通过重写一个类加载器的loadClass()方法,来控制字节流的获取方式。(2,3todo)
验证(无可落地知识点)
准备
为类的变量(被static修饰的变量)分配内存并设置初始值,这些变量所使用的内存在方法区中进行分配。
上面说的初始值‘通常情况下’是数据类型的零值。
public static int value = 123;//初始化时value = 0; 真正的赋值是在初始化阶段才会执行。
但如果是public static final int value = 123;//因为字段属性表中存在ConstValue属性
解析
初始化
执行类构造器<clinit>()方法,它是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
clinit() 与 init() 比较
1. init()需要显示调用。子类clinit()执行前,默认父类clinit()方法先执行,所以第一个执行clinit()方法的类肯定是java.lang.Object
2. clinit在没有静态语句块和变量赋值操作的时候编译器不会生成clinit()方法。
3. 接口中定义的变量只有在使用时候才会初始化。也会生产clinit(),但只在定义的接口中执行这个clinit()方法。
4. clinit()方法是线程安全的,而且只能被初始化一次。
类加载器
类加载阶段中的 "通过一个类的全限定名来获取描述此类的二进制字节流" 是java虚拟机外部实现的,实现这个动作的代码模块成为“类加载器”。有了这个定义,才有了 类层次划分,OSGI,热部署,代码加密等领域。
类的身份证(在java虚拟机中的唯一性):加载它的类加载器 + 类本身。
每个类加载器,都有一个独立的类名称空间。
唯一性: A.class.equals(B.class)/ isInstance / isAssignableFrom 或者 实例 instanceof B.class ,见如下例子:
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myClassLoader = 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 (Exception e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myClassLoader.loadClass("classloader.ClassLoaderTest").newInstance();
System.out.println(obj.getClass().getName());
System.out.println(obj instanceof ClassLoaderTest);
System.out.println(obj.getClass().equals(ClassLoaderTest.class));
System.out.println(obj.getClass().isInstance(ClassLoaderTest.class));
System.out.println(obj.getClass().isAssignableFrom(ClassLoaderTest.class));
Object obj2 = ClassLoaderTest.class.newInstance();
System.out.println(obj2 instanceof ClassLoaderTest);
System.out.println(obj2.getClass().equals(ClassLoaderTest.class));
System.out.println(obj2.getClass().isInstance(ClassLoaderTest.class));
System.out.println(obj2.getClass().isAssignableFrom(ClassLoaderTest.class));
}
}
双亲委派模型
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器完成。只有父加载器无法加载,子加载器才会尝试自己加载。