Java的类初始化的详解

来源:互联网 发布:算法设计与分析王红梅 编辑:程序博客网 时间:2024/06/10 22:48

           前两周看到了Java编程思想的初始化及类的加载那里,一直没找时间把它总结出来,对于初始化和类的加载过程,感觉Java编程思想讲的较浅还不够深入,于是我结合Java疯狂讲义2和人家博客后,就打算按照自己的理解来把它梳理一下。

         之前我一直对Java有个疑问,那就是一个Java类文件在首次被使用时,需要经过什么步骤。这里我参考了书籍我画个JVM对首次使用类的处理步骤流程图:


         首先我先从类的加载入手吧,Java语言其他的语言,采用了一种不同的加载方式。每个类的编译代码都存在于它自己的独立文件中,该文件只在需要使用程序代码时才会被加载。那么什么情况下类的字节码会被加载呢?这里我画了个思维导图:

    类的加载是将类的class文件读入内存中并为之创建一个java.lang.Class对象。这过程需要借助类的加载器来完成。天啊,对于Java接触不长久来说的人,什么叫类的加载器啊,幸好谷歌和百度老人懂,于是我上网查了一下看三个多小时,终于弄明白了是怎么一回事啦。

    Java的类加载器是负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会再次载入(通常是由一个类其全限定类名和其类加载器作为其唯一标识),在Java中类加载器有三类,这里我来画个表图来描述这三类。


      基于这方面我做了一些验证先从根类加载器开始,以下是一些代码验证:

 代码:

输出结果:

file:/D:/java/lib/resources.jar
file:/D:/java/lib/rt.jar
file:/D:/java/lib/sunrsasign.jar
file:/D:/java/lib/jsse.jar
file:/D:/java/lib/jce.jar
file:/D:/java/lib/charsets.jar
file:/D:/java/lib/jfr.jar
file:/D:/java/classes

这里为什么会出现编译错误呢?这类加载器是由JVM本身去实现,开发者是无法获得该引用的,所以会出现编译错误。从输出结果来看,它确实加载lib的几个库类,这里我不太明白,就是我去安装包查看文件时,发现文件没有classes,和sunsasign.jar这两个文件,这里我目前还不知道它们两个是怎么来的,希望阅览者知情的话,麻烦留言告知一下,本人将会非常感谢。

现在我来看看扩展类的情况,以下是代码验证:

public class ClassLoaderTest{public static void main(String[] args) {//获得系统类加载器ClassLoader systemLoader=ClassLoader.getSystemClassLoader();//获得扩展类加载器ClassLoader extensionLoader=systemLoader.getParent();//输出扩展加载器指定的加载路径System.out.println("扩展类加载器路径:"+"\n"+System.getProperty("java.ext.dirs"));//获得扩展类加载器的加载的库类URL[] urls=((URLClassLoader)extensionLoader).getURLs();System.out.println("扩展类加载器加载的库类");for(int i=0;i<urls.length;i++){System.out.println(urls[i]);}}}
输出结果:

扩展类加载器路径:
D:\java\lib\ext;C:\windows\Sun\Java\lib\ext
扩展类加载器加载的库类
file:/D:/java/lib/ext/access-bridge-64.jar
file:/D:/java/lib/ext/dnsns.jar
file:/D:/java/lib/ext/jaccess.jar
file:/D:/java/lib/ext/localedata.jar
file:/D:/java/lib/ext/sunec.jar
file:/D:/java/lib/ext/sunjce_provider.jar
file:/D:/java/lib/ext/sunmscapi.jar
file:/D:/java/lib/ext/zipfs.jar
从这输出结果也看出扩展类确实加载java.ext.dirs下的库类,这里就跟我查看该目录的文件jar包就一一对应了。

现在我验证一下最后一种系统类的加载器,目录工程路径为D:/AndroidProject/test

代码:

public class ClassLoaderTest{public static void main(String[] args) {//获得系统类加载器ClassLoader systemLoader=ClassLoader.getSystemClassLoader();//输出系统类加载器classpath环境变量指定的加载路径System.out.println("系统类加载器路径:"+"\n"+System.getProperty("java.class.path"));//获得系统类加载器的加载的库类URL[] urls=((URLClassLoader)systemLoader).getURLs();System.out.println("系统类加载器加载的库类");for(int i=0;i<urls.length;i++){System.out.println(urls[i]);}}}

输出结果:

系统类加载器路径:
D:\AndroidProject\test\bin
系统类加载器加载的库类
file:/D:/AndroidProject/test/bin/

这些就是这三个类的加载器了所负责加载的路径测试了,现在我想串联起自己刚开始初学Java时,感到的奇怪的地方。

疑问一:我们为什么可以用一些String,System,Object类呢?是谁帮我们把这些类载入内存的。

疑问二:我们会为什么可以不用设置classpath环境变量来指定系统类加载器所加载的类呢?

解答一:由上面知道,根类加载器是由JVM本身实现,是主要由C++写的一个加载,从它负责的加载路径可以看出,它是负责加载核心库的一个伴随JVM启动的加载器,而那些String,System,Object等类刚好位于这些核心库类中,再结合JAVA语言是采用需要使用该类时才会加载该类,所以当我们在程序使用那些Java库类时,是根类加载器帮我们加载到内存,而不是其他的加载器。

解答二:刚刚学Java时,通常会按照网上设置path和classpath环境变量的,但是那是我发现不设置classpath也没什么影响啊,那谁帮我们找到了指定要加载类的路径呢?从上面可以看出是系统类加载器负责加载我们的写的那些类,对已classpath的描述这里甲骨文公司有两段描述:The class path is the path that the Java runtime environment searches for classes and other resource files. The class search path (more commonly known by the shorter name, "class path") can be set using either the -classpathoption when calling a JDK tool (the preferred method) or by setting the CLASSPATH environment variable. The -classpath option is preferred because you can set it individually for each application without affecting other applications and without other applications modifying its value.The default class path is the current directory. Setting the CLASSPATH variable or using the -classpath command-line option overrides that default, so if you want to include the current directory in the search path, you must include "." in the new settings.这里我借助了谷歌翻译,自己就再把翻译改正一下,大概意思:类路径是让Java运行环境来寻找类和其它资源文件的路径。类搜索路径(普遍称为"class path"的短名)可以用JDK tool(首选方法) -classpath操作或者classpath环境变量来设置其值,这个-classpath操作是较好的方式,因为你可以单独为每个应用程序设置而不会影响其他的应用且不会让其他的应用可以修改它,默认类搜索的路径是当前路径,可以设置classpath环境变量或使用-classpath命令符可以类搜索路径,如果你要在当前路径搜索加载类的话,你就需要把当前路径加到classpath环境变量或-classpath命令中("."如果classpath没设置,当前路径会作为类搜索路径(即系统类加载器加载当前的路径的类),如果classpath设置的话,类搜素路径即为classpath设置的路径,(即即系统类加载器加载classpath设置路径下的类)                                

   这三个加载器之间存在继承关系,如果我们自己需要开发自定义的加载器,通常需要继承ClassLoader子类。这里我画个图来描述它们之间的关系:

     


刚开始看到三个图,我感到很奇怪,扩展类和系统类不是说继承根类加载器的怎么,怎么看到上面两个UML继承图没有出现根类加载器呢?这里我上网查后才得之其中的原因,这里我就不描述什么,阅览者可以点击下面链接来了解一下:

加载器之间的关系的详解

      大多人知道类加载机制有三种:

1全盘负责。就是由一个类加载器负责加载某个class时,该class所依赖的和引用的其他class也将由该类加载器负责载入

2父类委托所谓父类委托,则是让父类加载器试图加载该class,只有在父加载器无法加载该类时才尝试从自己的类路径加载该类。

3缓存机制。缓存机制将会保证所有加载过的class都会被缓存,当程序中需要使用某个class时,类加载器先从缓存区中搜寻该class,只有当缓存区中不存在该class对象时,系统才会读取该类对应的二进制数据。

JVM默认采取的加载机制是父类委托机制,我参考书籍写的一个通过父类委托机制实现类加载器加载class步骤来画个流程图:

现在我写一些代码验证一下:


现在我在D:\AndroidProject\test\src\test目录下建立一个标准JavaBean类,其代码:

package test;public class JavaBean{private  String name;private  int id;public JavaBean(){super();}public JavaBean(String name, int id){super();this.name = name;this.id = id;}public String getName(){return name;}public void setName(String name){this.name = name;}public int getId(){return id;}public void setId(int id){this.id = id;}}
现在我在相同路径下再写一个ClassLoaderTest类来加载这个类,代码:

public class ClassLoaderTest{    public static void main(String[] args)     {        try        {            ////调用加载当前类的类加载器(这里即为系统类加载器)加载JavaBean            Class test=Class.forName("test.JavaBean");            //查看被加载的JavaBean类型是被那个类加载器加载的            System.out.println(test.getClassLoader());        } catch (ClassNotFoundException e)        {            System.out.println("异常");            e.printStackTrace();        }            }}

输出结果:

sun.misc.Launcher$AppClassLoader@605df3c5

很明显JavaBean类是由系统类加载器加载,在当前目录下还存在JavaBean class文件前提下,我再把JavaBean class打成jar包放到扩展类加载器所加载的路径,会发生什么情况呢?

再次运行ClassLoaderTest类,其输出结果:

sun.misc.Launcher$ExtClassLoader@25082661

如果我把当前路径中 JavaBean class和扩张类加载器类加载路径下的JavaBean jar包删掉同时,再根类加载器加载的路径下放一个JavaBean jar包会发生什么情况:

  

再次运行ClassLoaderTest类,输出结果:

异常
java.lang.ClassNotFoundException: test.JavaBean
    at java.net.URLClassLoader$1.run(Unknown Source)
    at java.net.URLClassLoader$1.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Unknown Source)
    at test.ClassLoaderTest.main(ClassLoaderTest.java:16)
抛出了异常,有人可能就疑问,根据上面类加载器加载class的流程图,不是可以由根类加载器加载的吗?为什么抛出了异常呢?我用了谷歌查了一大推外国资料,都没找原因,只看从上面那个链接是这样的描述:

虚拟机出于安全等因素考虑,不会加载< Java_Runtime_Home >/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。

由于本人接触Java时间不长,就不知道该原因是不是这样了。但是总这些测试来看,当一个类调用的加载器默认是系统类加载器,如果父加载器负责加载的路径下有该类的class 文件,就有父加载器去加载,否则有当前加载器去当前路径或classpath指定路径加载该类,可以看出Java默认的加载机制确实通过委托机制去加载一个类的。

       那么JVM加载了一个类后,是不是马上初始化呢?不是,还有经过类的连接这一步,连接总共有三个步骤,我画思维导图来描述:

从图的可以看出,一个类的运行时的异常通常是在类的连接的验证阶段抛出的。

现在我来总结最后一个类的初始化过程了,类初始化过程会涉及到静态字段,静态代码快,普通代码块,普通变量,常量,构造器这一系列,动作,我记得我刚开始学Java时,常常被它们初始顺序给弄混淆了,在这里我结合书籍,总结出这些初始化的顺序,这里我再画个顺序流程图来描述:


现在我根据这个我写一些代码验证:

代码:

public class ExampleParent{//静态代码块static{System.out.println("ExampleParent的静态代码块");}//普通代码块{System.out.println("ExampleParent的普通代码块");}//构造器public ExampleParent(){System.out.println("ExampleParent的构造器");}}
public class Example  extends ExampleParent{//成员变量ExampleParent exampleParent3=new ExampleParent();//静态的字段static  ExampleParent exampleParent1=new ExampleParent();//静态的代码块static{System.out.println("Example的静态代码块");}//普通代码块{System.out.println("Example的普通代码块");}private String name;private int id;//构造器public Example(String name, int id){super();this.name = name;this.id = id;System.out.println("name是"+name+" id是"+id);}}
public class ExampleTest  extends Example{public ExampleTest(String name, int id){super(name, id);}public ExampleTest(){this("beyondboy",88);}//普通代码块{System.out.println("ExampleTest的普通代码块");}//静态代码块static{System.out.println("ExampleTest的静态代码块");}//静态字段static Example example=new Example("beyondboy", 10);public static void main(String[] args){System.out.println("ExampleTest的静态main方法");new ExampleTest();new ExampleTest();}}
输出结果:

ExampleParent的静态代码块
ExampleParent的普通代码块
ExampleParent的构造器
Example的静态代码块
ExampleTest的静态代码块
ExampleParent的普通代码块
ExampleParent的构造器
ExampleParent的普通代码块
ExampleParent的构造器
Example的普通代码块
name是beyondboy id是10
ExampleTest的静态main方法
ExampleParent的普通代码块
ExampleParent的构造器
ExampleParent的普通代码块
ExampleParent的构造器
Example的普通代码块
name是beyondboy id是88
ExampleTest的普通代码块
ExampleParent的普通代码块
ExampleParent的构造器
ExampleParent的普通代码块
ExampleParent的构造器
Example的普通代码块
name是beyondboy id是88
ExampleTest的普通代码块
 因为要期末考试了,所以我的时间较紧,就没有详细分步去写各种情况,我就一次性综合了所有出现的情况,这里输出结果比较长,可能对Java初学者来说看不太懂,如果看不懂的话,就自己百度一下,按照其上面流程图去观察,去猜别人写的例子。对于Java的初始化整个过程我就暂时总结到这,以后等我下学期学了计算机算法分析与设计,我会尝试再把它串联到JVM GC垃圾回收过程,整篇文章我使用了整整一天的时间去写,由于时间较紧,可能会有不少的错误,希望阅览者能给予指正!

这篇博客我参考资料:

Java编程思想4

Java疯狂讲义2

博客






    



0 0