世界上永远存在这样一类人,他能够超越自己的家庭、血缘、环境,他能够挣脱时代对他的束缚,让世界另眼相看。
类加载过程
1 加载
1.1 做了什么
这个阶段,jvm需要做一下三个事
☐ 通过一个类的全限定名来获取其定义的二进制字节流。
☐ 将这个流所代表的静态数据结构转化为方法区中的运行的时数据结构
☐ 在java堆中生成一个java.lang.class对象,作为访问方法区数据结构的入口
1.2 注意
☐ 二进制流不一定类文件中获取,也可以是从网络中,反正是合法的class文件流就 行,因此jar,war,jsp,Applet诞生了
☐ 这个阶段是我们能控制的最强的阶段,因为我们可以自定义类加载器来加载类
1.3 类加载器
类加载器的诞生是为了实现类的加载,但是现在他的功能远不止加载类,他可以用来确认一个类的唯一性,只有同一个类加载器实例加载出来的类,才能被视为相同的类,如equals,InstanceOf方法。
1.3.1 分类
站在Java虚拟机的角度来讲,只存在两种不同的类加载器:
- C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分。
- Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:
☐ 启动类加载器:Bootstrap ClassLoader,跟上面相同。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
☐ 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
☐ 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
☐ 我们还可以自定义classloader类加载器,只需继承classloader抽象类即可
实现类加载器并利用不同加载器加载类不同的特性实现热加载
public interface Message { void send(); }
public class MessageImpl implements Message{ @Override public void send() { System.out.println("发生消息bbb"); } }
public class MyClassLoader extends ClassLoader { //类加载器名称 private String name; //类加载路径 private String path; public MyClassLoader(ClassLoader parent, String name, String path) { super(parent); //父类加载器 this.name = name; this.path = path; } /** * 重写父类的loadClass方法 */ @Override public Class<?> loadClass(String name) throws ClassNotFoundException { Class<?> clazz = null; //判断是不是本类 if(this.name.equals(name)) { //查找是否已经被加载过了 clazz = this.findLoadedClass(name); if(clazz == null) { //如果没找到则继续去查找 clazz = this.findClass(name); } } return super.loadClass(name); } /** * 重写findClass方法,自定义规则 */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //转成二进制字节流,因为JVM只认识二进制不认识字符串 byte[] b = readFileToByteArray(name); return this.defineClass(this.name, b, 0, b.length); } /** * 将包名转换成全路径名,比如 * * temp.a.com.dn.Demo -> D:/temp/a/com/dn/Demo.class * * @param name * @return */ private byte[] readFileToByteArray(String name) { InputStream is = null; byte[] rtnData = null; //转换 name = name.replaceAll("\\.", "/"); //拼接 String filePath = this.path + name + ".class"; File file = new File(filePath); ByteArrayOutputStream os = new ByteArrayOutputStream(); try { is = new FileInputStream(file); int tmp = 0; while((tmp = is.read()) != -1) { os.write(tmp); } rtnData = os.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { if(null != is) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } if(null != os) { try { os.close(); } catch (IOException e) { e.printStackTrace(); } } } return rtnData; } public Class<?> loadClass() throws ClassNotFoundException { return loadClass(this.name); } }
主测试类:public class Main { private static final String name = "clasaLoader.MessageImpl"; private static final String path = "C:\\Users\\Administrator\\IdeaProjects\\ThinkInJava\\out\\production\\ThinkInJava\\"; public static void main(String[] args)throws Exception{ Message message = null; while (true){ MyClassLoader classLoader = new MyClassLoader(Thread.currentThread().getContextClassLoader(), name, path); Class clazz = classLoader.loadClass(); message = (Message)clazz.newInstance(); message.send(); Thread.sleep(3000); } } }
如果我们把MessageImpl类中send的内容改为aaa,则会输出aaa而不需重启程序
1.3.2 双亲委派机制
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
JVM通过调用classloader中的loadclass来实现类加载,而我们一般不需要重写这个方法,而只需重写findClass 因为这个方法保证了双亲委派机制,但是也反映出双亲委派机制是可以被打破的
以下是loadclass的源码:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { //如果有父类加载器就尝试父记载器加载, //因为拓展类加载器将其父级启动类加载器视为了null, //而我们自己写的程序是无法调用启动类加载器的 if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } //如果父类加载器加载失败,则我们自己尝试加载 if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
通过以上的方法我们就实现了双亲委派机制。
那么为什么要使用这个机制呢?
这是为了java类有一种优先级的关系,这可以保证虚拟机中只存在一个类的实例从而不出错。
注意就算我们自定义了java.lang.string类的类加载器,那么也不会加载成功,因为权限问题,java.lang下的类只能有启动类加载,所以我们永远也不能写自己的String方法,当然也不能继承String(因为这个类是final的)
2 验证
确保文件流是安全的,可以被加载,不会威胁到虚拟机的安全
3 准备
这个阶段用于对类变量(static修饰的)进行赋默认值,如
static int a = 1;
初始阶段会对a赋值0,因为int型默认值就是0,这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,放于类构造器<clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。
但是如果是
static final int a = 1;
那么初始阶段 a= 1;因为在class文件编译的时候,就已经在常量池中加了这个属性的
ConstantValue(即同时被final与static修饰)
4 解析
解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程
个人理解是像是将程序中约定的类的逻辑地址转化为实际在内存中类的物理地址
5 初始化
作用 执行类构造器<clinit>()方法
关于<clinit>方法:
☐ <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。
☐ 2、<clinit>()方法与实例构造器<init>()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此,在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
☐ 3、<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
☐ 4、接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成<clinit>()方法。但是接口与类不同的是:执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
☐ 5、虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。