1. 类的加载和初始化
在程序用到一个类的时候,如果类还没有加载到内存,JVM会对类进行加载、连接和初始化。
类加载由类加载器完成,类加载器通常由JVM提供(系统类加载器)。开发者也可以通过继承ClassLoader创建自己的类加载器。
类加载器加载类的二进制数据,可以从以下几个地方获得:
- 从本地文件系统加载class文件
- 从JAR包加载class文件
- 网络加载class文件
- 动态编译Java源文件实现加载。
类被加载后,系统会为类生成一个对应的Class对象,通过类连接把类的二进制数据合并到JRE中,称为类连接。类连接有三个阶段: - 验证:检验被加载的类是否有正确的内部结构
- 准备:为类的静态属性分配内存,设置默认初始值
- 解析:将二进制数据中的符号引用替换成直接引用
虚拟机对类进行初始化,主要是对静态属性进行初始化。声明静态属性的初始值,可在声明静态属性时指定初始值,也可以使用静态初始块为静态属性指定初始值。
public class ClassTest {
static int a = 1;// 声明的时候执行初始值
static int b;
static {// 静态代码块,第一次加载类的时候执行
System.out.println("静态代码块");
System.out.println(b);// 输出默认值0
b = 2;
System.out.println(b);// 输出2
}
{//每次new对象都会执行
System.out.println("实例代码块");
}
}
测试代码:
public static void main(String[] args) {
ClassTest test1 = new ClassTest();
ClassTest test2 = new ClassTest();
}
输出结果:
JVM初始化一个类的时候,步骤如下:
- 如果类没被加载和连接,程序先加载并连接该类。
- 如果该类的直接父类没有被初始化,则先初始化直接父类
- 如果类中有初始化语句,则系统依次执行这些初始化语句
JAVA中通过以下6中方法,可以触发一个类的初始化:
a) 创建类的实例。new一个对象、通过反序列化或反射来创建类的实例
b) 调用某个类的静态方法
c) 访问某个类或接口的静态属性,或为静态属性赋值
d) 使用反射方式强制创建某个类的java.lang.Class对象
e) 初始化某个类的子类,该子类的所有父类都会被初始化
f) 用java.exe运行某个主类
另外,对于一个final修饰的静态属性,如果该属性在编译时就能得到属性值,访问该属性时不会触发类的初始化。如:
public class ClassTest {
final static int a = 1;// 声明的时候执行初始值
}
public static void main(String[] args) {
System.out.println(ClassTest.a);//不会触发ClassTest初始化
}
2. 类加载器
类加载器负责将.class文件加载到内存中,并生成对应的java.lang.Class对象。同一个类只能被加载到JVM中一次。在Java中,用类的全限定名(包名+类名)作为标识,区分是不是同一个类,即同一个包下不能有重名的类。JVM在加载类的时候,用类的全限定名+加载器的名字作为唯一标识。即同一个类可以被不同的类加载器加载,每个类加载器只能加载一次。
JVM启动时,有三个类加载器:
- Bootstrap ClassLoader:根类加载器,负责加载Java的核心类,它不是ClassLoader的子类,而是由JVM自身实现的。核心类在jre/lib下
- Extension ClassLoader:扩展类加载器,负责加载JRE扩展目录(jre/lib/ext)中的JAR包
- System ClassLoader:系统类加载器,负责在JVM启动时,加载来自java中的-classpath选项或java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。
JVM加载类主要有如下三种机制: - 全盘负责:当一个类加载器负责加载某个Class的时候,该Class所依赖的和引用的其它Class将由该类加载器负责载入。除非显示使用另外一个类加载器来载入。
- 父类委托:先让parent类加载器加载该Class,只有当parent加载器无法加载该类时才尝试从自己的类路径中加载该类。
- 缓存机制:缓存机制将会保证所有被加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存中搜寻该Class,只有当缓存中不存在时,系统才会重新读取该类的二进制数据,并将其转换为Class对象。因此修改了Class后必须重新启动JVM。
public static void main(String[] args) throws IOException {
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemLoader);
// 获取系统类加载器的加载路径,通常是CLASSPATH环境变量指定。
// 如果没有指定CLASSPATH,默认是当前路径为系统类加载器的加载路径
Enumeration<URL> eml = systemLoader.getResources("");
while (eml.hasMoreElements()) {
System.out.println(eml.nextElement());
}
ClassLoader parentLoader = systemLoader.getParent();
System.out.println("扩展类加载器:"+parentLoader);
System.out.println("扩展类加载器的加载路径:"+System.getProperty("java.ext.dirs"));
System.out.println("扩展类加载器的parent:"+parentLoader.getParent());
}
3. 反射
3.1 获得Class对象
每个类被加载后,都会为该类生成一个对应的Class对象,通过该Class对象可以访问JVM中的这个类。Java中获得Class对象有三种方式:
- 使用Class.forName(“类全限定名”)
- 调用某个类的class属性。如Person.class
- 调用某个对象的getClass()方法。
通过Class对象可以访问对象的构造器、方法、属性。
3.2 使用反射操作对象
Class类中有可以访问类的Constructor(构造方法)、Methods(方法)、Field(属性)
方法名中带Declared的,表示类中声明的成员,无关访问修饰符。
package classtest;
public class ClassTest {
private String name;
public ClassTest() {
System.out.println("ClassTest无参构造");
}
public ClassTest(String name) {
this.name = name;
System.out.println("ClassTest有参构造");
}
public void info() {
System.out.println("info()");
}
public void info(String in) {
System.out.println("info(String in)" + in);
}
private void pri() {
}
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException {
Class clazz =Class.forName("classtest.ClassTest");
Constructor[] constructors = clazz.getDeclaredConstructors();// 获取所有构造器
for (Constructor cons : constructors) {
System.out.println(cons);
}
Method[] methods = clazz.getMethods();// 获取类中所有public方法
for (Method m : methods) {
System.out.println(m);
}
Method info = clazz.getMethod("info", String.class);// 获取info(String)这个方法
Field[] fields = clazz.getDeclaredFields();//获取所有声明的属性,与访问权限无关
for(Field f:fields){
System.out.println(f);
}
}