1、 什么是类加载
类加载
是指类加载器
将Class字节码文件
加载进JVM方法区
,生成Class对象
的过程。
一般我们用new
关键字创建对象实例时,JVM
会先将该类的Class字节码文件
从磁盘加载进内存(JVM方法区
),然后根据生成的Class对象
在堆
中创建实例。
触发类加载的几种情况:
- 遇到new,getstatic,putstatic,invokestatic这4条指令;
- 使用java.lang.reflect包的方法对类进行反射调用;(后面有说明)
- 初始化一个类的时候,如果发现其父类没有进行过初始化,则先初始化其父类;
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类;
tips:JVM
并不是一开始就将所有可能要用的class加载进内存,而是上面这几种情况才会触发类加载
代码执行顺序:父类的静态代码块>子类的静态代码块>父类代码块>父类构造方法>子类代码块>子类构造方法;
2、自定义类加载器(继承ClassLoader)
2.1、只重写findClass()方法(推荐)
- 当我们调用自定义加载器的
loadClass
方法会调用其父类的loadClass
方法(因为我们没有重写loadClass
方法),而父类的loadClass
方法会调用我们自己写的findClass()方法
- 这样做的好处是比较简单,而且不会破坏
双亲委派
机制。我们只需要将我们从任意渠道获取的class字节码文件交给defineClass
函数即可,这样就实现了加载其他地方的class文件了。比如我们常用的LeetCode
,它可能就是通过这种方式来运行我们写的代码。
代码大概如下:
byte[] raw = getBytes(className);//获取class文件的字节数组
class1 = defineClass(arg0, raw, 0, raw.length);//交给defineClass返回Class对象
2.2、重写loadClass方法
如果你一定要打破双亲委派
机制,也是可以的。此时就需要重写loadClass方法
。因为双亲委派
的逻辑就在这里,我们重写后不走这个逻辑就好啦。(我写的代码我说了算!哈哈。但是真的不推荐,除非你业务有特殊需求,比如tomcat那样的等等)
`loadClass方法`代码大概如下:
ClassLoader classLoader=getSystemClassLoader().getParent();//先获取ext类加载器
try {
c=classLoader.loadClass(name);
System.out.println(classLoader+"加载了"+name);
return c;
} catch (Exception e) {
System.out.println(classLoader+"没加载到"+name);//(1)
}
try {
c = findClass(name);//(2)
System.out.println("自定义的加载了"+name);
} catch (Exception e) {
System.out.println("自定义的没有加载到"+name);
}
- 注意: 为什么要先用ext类加载器加载?因为每一个类都继承了object类,加载一个类时会先加载父类,而在
ClassLoder类
的preDefineClass()方法
里写死了如果你加载java.*
的类就不得行,所以你得先用ext类加载器加载java.lang.Object类。 - 流程大致如下:
1、首先调用loadClass方法
,ext类加载器加载类,肯定加载不到(因为这个类是其他地方的,比如另一个电脑里),捕获异常执行(1)后继续
2、;来到(2),进入我们自定义的findClass()
,获取class文件的二进制流,交给defineClass()
方法,defineClass()
方法会先加载父类java.lang.object,因此他会再次调用loadClass方法
,此时ext类加载器加载java.lang.object后返回。再回到上一步的方法(递归)完成加载。
tips:Tomcat为什么要自定义类加载器(WebAppClassLoder
)打破双亲委派
机制?
因为一个Tomcat启动后就是一个Java进程,只有一个JVM,而一个Tomcat容器内可能有多个应用。并且完成有可能存在两个名字相同的类,而区分方法区里Class对象
的标识为名称+类加载器,所以如果此时还采用双亲委派
机制就无法实现同名类共存,所以Tomcat为容器内每一个应用都创建了一个类加载器(WebAppClassLoder
),并且这个类加载器(WebAppClassLoder
)打破了双亲委派
机制,也就是不会交给父亲加载器去加载,而是自己加载。
其实这也不绝对,因为Tomca的t类加载器(WebAppClassLoder
)是可选的,有一个参数delegateLoad
默认为false,表示打破双亲委派
机制,当然也可以通过配置文件修改为true,就不打破咯。
3、反射与类加载器???
反射一般用Class
类的forName()
方法,此方法会根据类的全限定名将类加载进方法区(用的是AppClassLoader类加载器),返回一个Class对象(这个获取Class对象的过程不是反射,而通过这个Class对象在运行时动态获取类的成员和方法的这种特性才叫反射),我们就可以“解刨”这个类了(但只能是classpath下的才行哈,不能向前面那样获取别处的,不然会ClassNotFound)。
那反射有啥用,为什么会有这么个玩意儿?都在我类路径下了,我直接new不行吗?
一般来说,反射对我们确实没啥用,但是它是框架的核心!诚然,都在类路径下了,直接new当然可以,而且更快(反射很耗性能)。故反射一般用于不能直接new的场景(见3.2)。而且你直接new相当于写死了,在编译时就确定了类的类型。当然你直接yongClass.forName(com.xxx)
,也是写死了(运行到这里才确定类的类型),故而一般我们是从配置文件里读取类的全限定名,如果要替换类将新类的class文件放到classpath下,修改配置文件即可,不用改代码,这就很nice了啊。
3.1、反射的简单使用
我们都知道String是不可变的,原因是private final char value[];
value属性是私有的,外部无法修改(final修饰只是指向value数组的指针不可变,但是value数组的内容是可变的),因此我们可以通过反射获取String的私有成员进行修改,从而达到“保证s引用的指向不变,最终将输出变成abcd”。
public static void main(String[] args) {
String s = new String("abc");
// 在这中间可以添加N行代码,但必须保证s引用的指向不变,最终将输出变成abcd
Field value = s.getClass().getDeclaredField("value");
value.setAccessible(true);
value.set(s, "abcd".toCharArray());
System.out.println(s);
}
是不是挺无聊的?实际上反射一般要结合类加载器一起使用。
3.2、反射结合类加载器一起使用
假如,你的类就在classpath下,反射除了可以获取、修改私有成员、方法外,确实没啥其他用,所以我们一般用不到,但前面也说了,反射是框架的核心,那框架中反射是如何使用的呢?
首先要明白框架和工具类的区别:框架是主动去调用我们提供的类,而工具类是给我们调用的类。
所以,框架的作者在创建框架时,并没有我们写的类,他就是通过反射结合类加载器来获取我们的类,我猜他(spring)应该是这样写的:
public class BeanFactory {
private Map<String, Object> beanMap = new HashMap<String, Object>();
/**
* bean工厂的初始化.
* @param xml xml配置文件
*/
public void init(String xml) {
try {
//读取指定的配置文件
SAXReader reader = new SAXReader();
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//从class目录下获取指定的xml文件,这里是用类加载器来加载xml配置文件
InputStream ins = classLoader.getResourceAsStream(xml);
Document doc = reader.read(ins);
Element root = doc.getRootElement();
Element foo;
//遍历bean
for (Iterator i = root.elementIterator("bean"); i.hasNext();) {
foo = (Element) i.next();
//获取bean的属性id和class
Attribute id = foo.attribute("id");
Attribute cls = foo.attribute("class");
//利用Java反射机制,通过class的名称获取Class对象
Class bean = Class.forName(cls.getText());
//获取对应class的信息
java.beans.BeanInfo info = java.beans.Introspector.getBeanInfo(bean);
//获取其属性描述
java.beans.PropertyDescriptor pd[] = info.getPropertyDescriptors();
//设置值的方法
Method mSet = null;
//创建一个对象
Object obj = bean.newInstance();
//遍历该bean的property属性
for (Iterator ite = foo.elementIterator("property"); ite.hasNext();) {
Element foo2 = (Element) ite.next();
//获取该property的name属性
Attribute name = foo2.attribute("name");
String value = null;
//获取该property的子元素value的值
for(Iterator ite1 = foo2.elementIterator("value"); ite1.hasNext();) {
Element node = (Element) ite1.next();
value = node.getText();
break;
}
for (int k = 0; k < pd.length; k++) {
if (pd[k].getName().equalsIgnoreCase(name.getText())) {
mSet = pd[k].getWriteMethod();
//利用Java的反射极致调用对象的某个set方法,并将值设置进去
mSet.invoke(obj, value);
}
}
}
//将对象放入beanMap中,其中key为id值,value为对象
beanMap.put(id.getText(), obj);
}
} catch (Exception e) {
System.out.println(e.toString());
}
}
//other codes
}
spring的xml文件配置bean大致如下:
<bean id="user2" class="com.baidu.User" >
<property name="age" value="28"></property>
<property name="name" value="王五"></property>
</bean>
回到反射有啥用,为什么会有这么个玩意儿?都在我类路径下了,我直接new不行吗?
虽然这些类都在你的classpath下,但是是框架要去调用你的类(不是你去调),框架不可能提前知道你的这些类,所以框架只能向上面这样写,你要做的就是把你写的类在xml文件里写好,告诉框架你类的信息(全限定名等等),然后框架会先用类加载器将xml文件读入,循环遍历你所有的bean标签,拿到bean标签里的id
、 class
里的值,然后根据 class
里的值(全限定名)通过下面这样将类加载进方法区,得到Class对象。然后通过反射创建实例,再赋值啊什么的等等(详见上面的代码)。
//利用Java反射机制,通过class的名称获取Class对象
Class bean = Class.forName(cls.getText());
//创建一个对象
Object obj = bean.newInstance();
所以现在就不发出反射有啥用,为什么会有这么个玩意儿?都在我类路径下了,我直接new不行吗?
这种疑问了吧?你当然可以直接new,但是框架不能啊,它要调你的类,只能通过上面这种方式。
3.3、思考LeetCode是如何加载我们写的类的呢?
我们刷题时常用的LeetCode是如何加载我们写的类的呢?我们写的这个类肯定不在LeetCode的classpath下啊?难道是通过网络传输到LeetCode服务器下,再将编译好的字节码放到classpath下?
显然不是。我猜哈,它肯定是自己写了一个类加载器,从写findClass()
方法,将我们类的字节码信息传入,就可以获取我们类的Class对象了,再通过反射调用指定的方法(LeetCode的方法名是不能改的)。