Java类加载(简单清晰)

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标签里的idclass里的值,然后根据 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的方法名是不能改的)。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值