Java之类加载机制 html
为了弄清楚这个问题,首先还要看看System类的API doc文档。 java
在java.lang包里有个ClassLoader类,ClassLoader 的基本目标是对类的请求提供服务,按需动态装载类和资
源,只有当一个类要使用(使用new 关键字来实例化一个类)的时候,类加载器才会加载这个类并初始化。
一个Java应用程序可使用不一样类型的类加载器。例如Web Application Server中,Servlet的加载使用开发
商自定义的类加载器, java.lang.String在使用JVM系统加载器,Bootstrap Class Loader,开发商定义的其余类
则由AppClassLoader加载。在JVM里由类名和类加载器区别不一样的Java类型。所以,JVM容许咱们使用不一样
的加载器加载相同namespace的java类,而实际上这些相同namespace的java类能够是彻底不一样的类。这种
机制能够保证JDK自带的java.lang.String是惟一的。
2. 加载类的两种方式:
(1) 隐式方式
使用new关键字让类加载器按需求载入所需的类
(2) 显式方式
由 java.lang.Class的forName()方法加载
public static Class forName(String className)
public static Class forName(String className, boolean initialize,ClassLoader loader)
参数说明:
className - 所需类的彻底限定名
initialize - 是否必须初始化类(静态代码块的初始化)
loader - 用于加载类的类加载器 程序员
调用只有一个参数的forName()方法等效于 Class.forName(className, true, loader)。
这两个方法,最后都要链接到原生方法forName0(),其定义以下:
private static native Class forName0(String name, boolean initialize,ClassLoader loader)
throws ClassNotFoundException;
只有一个参数的forName()方法,最后调用的是:
forName0(className, true, ClassLoader.getCallerClassLoader());
而三个参数的forName(),最后调用的是:
forName0(name, initialize, loader);
因此,无论使用的是new 來实例化某个类、或是使用只有一个参数的Class.forName()方法,内部都隐含
了“载入类 + 运行静态代码块”的步骤。而使用具备三个参数的Class.forName()方法时,若是第二个参数
为false,那么类加载器只会加载类,而不会初始化静态代码块,只有当实例化这个类的时候,静态代码块
才会被初始化,静态代码块是在类第一次实例化的时候才初始化的。
直接使用类加载器
得到对象所属的类 : getClass()方法
得到该类的类加载器 : getClassLoader()方法
3.执行java XXX.class的过程
找到JRE——》找到jvm.dll——》启动JVM并进行初始化——》产生Bootstrap Loader——》
载入ExtClassLoader——》载入AppClassLoader——》执行java XXX.class web
ClassLoader是用来处理类加载的类,它管理着具体类的运行时上下文。 算法
1.ClassLoader存在的模块意义: bootstrap
1)从java的package定义出发: api
classloader是经过分层的关联方式来管理运行中使用的类,不一样的classloader中管理的类是不相同的,或者即使两个类毫无二致(除了路径)也是不一样的两个类,在进行强制转换时也会抛出ClassCastException。因此,经过classloader的限制,咱们能够创建不一样的package路径以区别不一样的类(注意这里的“不一样”是指,命名和实现彻底一致,可是有不一样的包路径。)。那么也是由于有特定的classloader,咱们能够实现具体模块的加载,而不影响jvm中其余类,即发生类加载的冲突。 缓存
2)可是,若是两个在不一样路径下的类(咱们假定,这两个类定义中,不存在package声明,彻底同样的两个类),通过不一样的classloader加载,这两个类在jvm中产生的实例能够相互转换吗? tomcat
答案是否认的。即使这两个类除了存在位置不一样以外,都彻底同样。经由不一样classloader加载的两个类依然是不一样的两个对象。经过Class.newInstance()或者Class.getConstructor().newInstance()产生的对象是彻底不一样的实例。 安全
以上两种状况,package可使得咱们的软件架构清晰,但那不是最终做用,若是跟classloader结合起来理解,效果更好。
2.ClassLoader的类加载机制:
ClassLoader做为java的一个默认抽象类,给咱们带来了极大的方便,若是咱们要本身实现相应的类加载算法的话。
每一个类都有一个对应的class与之绑定,而且能够经过MyClass.class方式来获取这个Class对象。经过Class对象,咱们就能获取加载这个类的classloader。可是,咱们如今要研究的是,一个类,是如何经过classloader加载到jvm中的。
其中有几个关键方法,值得咱们了解一番:
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException;
咱们能够假设一个实例在创建时,例如经过new方式,是经由如此步骤实现:ClassLoader.loadClass("classname",false).newInstance()。
接下来须要考虑的是loadClass方法为咱们作了哪些工做?如何跟对应的.class文件结合,如何将对应的文件变成咱们的Class对象,如何得到咱们须要的类?
在ClassLoader类中,已经有了loadClass默认实现。咱们结合源代码说明一下:
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先检查,jvm中是否已经加载了对应名称的类,findLoadedClass(String )方法其实是findLoadedClass0方法的wrapped方法,作了检查类名的工 //做,而findLoadedClass0则是一个native方法,经过底层来查看jvm中的对象。 Class c = findLoadedClass(name); if (c == null) {//类还未加载 try { if (parent != null) { //在类还未加载的状况下,咱们首先应该将加载工做交由父classloader来处理。 c = parent.loadClass(name, false); } else { //返回一个由bootstrap class loader加载的类,若是不存在就返回null 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. c = findClass(name);//这里是咱们的入手点,也就是指定咱们本身的类加载实现 } } if (resolve) { resolveClass(c);//用来作类连接操做 } return c; }
在这段代码中,应该已经说明了不少问题,那就是jvm会缓存加载的类,因此,在咱们要求classloader为咱们加载类时,要先经过findLoadedClass方法来查看是否已经存在了这个类。不存在时,就要先由其parent class loader 来loadClass,固然能够迭代这种操做一直到找到这个类的加载定义。若是这样仍是不能解决问题,对于咱们本身实现的class loader而言,能够再交由system class loader来loadClass,若是再不行,那就让findBootstrapClassOrNull。经历了如此路程,依然不能解决问题时,那就要咱们出马来摆平,经过本身实现的findClass(String)方法来实现具体的类加载。
这段实现代码摘自Andreas Schaefer写的文章中的代码(这篇文章至关精彩)
protected Class findClass( String pClassName )
throws ClassNotFoundException {
try {
System.out.println( "Current dir: " + new File( mDirectory ).getAbsolutePath() );
File lClassFile = new File( mDirectory, pClassName + ".class" );
InputStream lInput = new BufferedInputStream( new FileInputStream( lClassFile ) );
ByteArrayOutputStream lOutput = new ByteArrayOutputStream();
int i = 0;
while( ( i = lInput.read() ) >= 0 ) {
lOutput.write( i );
}
byte[] lBytes = lOutput.toByteArray();
return defineClass( pClassName, lBytes, 0, lBytes.length );
} catch( Exception e ) {
throw new ClassNotFoundException( "Class: " + pClassName + " could not be found" );
}
}
findClass方法主要的工做是在指定路径中查找咱们须要的类。若是存在此命名的类,那么就将class文件加载到jvm中,再由defineClass方法(一个native方法)来生成具体的Class对象。
通常来讲,通过上述方式来加载类的话,咱们的类可能都在一个classloader中加载完成。可是,再强调一下,那就是若是类有不一样路径或者不一样包名,那就是不一样类定义。
类加载器的特性:
classloader-architecture
classloader-class-diagram
类图中, BootstrapClassLoader是一个单独的java类, 其实在这里, 不该该叫他是一个java类。由于,它已经彻底不用java实现了。它是在jvm启动时, 就被构造起来的, 负责java平台核心库。
自定义类加载器加载一个类的步骤
classloader-load-class
ClassLoader 类加载逻辑分析, 如下逻辑是除 BootstrapClassLoader 外的类加载器加载流程:
线程上下文类加载器
java默认的线程上下文类加载器是 系统类加载器(AppClassLoader)。
以上代码摘自sun.misc.Launch的无参构造函数Launch()。
使用线程上下文类加载器, 能够在执行线程中, 抛弃双亲委派加载链模式, 使用线程上下文里的类加载器加载类.
典型的例子有, 经过线程上下文来加载第三方库jndi实现, 而不依赖于双亲委派.
大部分java app服务器(jboss, tomcat..)也是采用contextClassLoader来处理web服务。
还有一些采用 hotswap 特性的框架, 也使用了线程上下文类加载器, 好比 seasar (full stack framework in japenese).
线程上下文从根本解决了通常应用不能违背双亲委派模式的问题.
使java类加载体系显得更灵活.
随着多核时代的来临, 相信多线程开发将会愈来愈多地进入程序员的实际编码过程当中. 所以,
在编写基础设施时, 经过使用线程上下文来加载类, 应该是一个很好的选择。
固然, 好东西都有利弊. 使用线程上下文加载类, 也要注意, 保证多根须要通讯的线程间的类加载器应该是同一个,
防止由于不一样的类加载器, 致使类型转换异常(ClassCastException)。
为何要使用这种双亲委托模式呢?
java动态载入class的两种方式:
用Class.forName加载类
Class.forName使用的是被调用者的类加载器来加载类的。
这种特性, 证实了java类加载器中的名称空间是惟一的, 不会相互干扰。
即在通常状况下, 保证同一个类中所关联的其余类都是由当前类的类加载器所加载的。
上面中 ClassLoader.getCallerClassLoader 就是获得调用当前forName方法的类的类加载器
static块在何时执行?
各个java类由哪些classLoader加载?
NoClassDefFoundError和ClassNotFoundException
垃圾回收分为两大步骤:识别垃圾 和回收垃圾
识别垃圾有两大基本方法
1.计数器法
每一个对象有一个相应的计数器,统计当前被引用的个数,每次被引用或者失去引用都会更新该计数器。
优势:识别垃圾快,只需判断计数器是否为零。
缺点:增长了维护计数器的成本,没法在对象互相引用的状况下识别垃圾,所以,适用于对实时性要求很是高的系统。
2.追踪法
从根对象(例如局部变量)出发,逐一遍历它的引用。若没法被扫描到,即认定为垃圾,实际状况中通常采用该方法。
回收垃圾最重要的是要最大限度地减小内存碎片。
两种两大基本方法:
1.移动活对象覆盖内存碎片,使对象间的内存空白增大。
2.拷贝全部的活对象到另一块完整的空白内存,而后一次释放原来的内存。
一般第二种方法可以最大的减小内存碎片,可是缺点是在拷贝过程当中会终止程序的运行。
引入分级的概念,一般一个程序中大部分对象的生命周期很短,只有小部分的对象有比较长的生命。而偏偏使得拷贝方法性能打折扣的是重复拷贝那些长命的对象。所以,把对象分红几个级别,在低级别呆到必定时间就将其升级。相应地越高级别,回收的次数越少。最理想的状况是,每次回收最低级别的对象所有失效,一次性就能够回收该级别全部内存,提升效率。同时,因为每次只回收一个级别,不需遍历全部对象,控制了整个回收的时间。
因为垃圾识别是经过识别引用来达到,为了增长程序对垃圾回收的控制。提供了引用对象的概念,细化了引用的类型,分别是StrongReference,SoftReference, WeakReference, PhantomReference。其中强引用就是普通的java引用,其余三种类型至关于一个包装器,一方面使得垃圾回收器区分引用类型作不一样的处理,另外一方面程序经过他们仍然能够获得强引用。
分代垃圾回收机制:
如上图所示,现代GC采用分区管理机制的JVM将JVM所管理的全部内存资源分为2个大的部分。永久存储区(Permanent Space)和堆空间(The Heap Space)。其中堆空间又分为新生区(Young (New) generation space)和养老区(Tenure (Old) generation space),新生区又分为伊甸园(Eden space),幸存者0区(Survivor 0 space)和幸存者1区(Survivor 1 space)。具体分区以下图:
那JVM他的这些分区各有什么用途,请看下面的解说。
永久存储区(Permanent Space):永久存储区是JVM的驻留内存,用于存放JDK自身所携带的Class,Interface的元数据,应用服务器容许必须的Class,Interface的元数据和Java程序运行时须要的Class和Interface的元数据。被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM时,释放此区域所控制的内存。
堆空间(The Heap Space):是JAVA对象生死存亡的地区,JAVA对象的出生,成长,死亡都在这个区域完成。堆空间又分别按JAVA对象的建立和年龄特征分为养老区和新生区。
新生区(Young (New) generation space ):新生区的做用包括JAVA对象的建立和从JAVA对象中筛选出能进入养老区的JAVA对象。
伊甸园(Eden space):JAVA对空间中的全部对象在此出生,该区的名字所以而得名。也便是说当你的JAVA程序运行时,须要建立新的对象,JVM将在该区为你建立一个指定的对象供程序使用。建立对象的依据便是永久存储区中的元数据。
幸存者0区(Survivor 0 space)和幸存者1区(Survivor1 space):当伊甸园的控件用完时,程序又须要建立对象;此时JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的再也不被其余对象所引用的对象进行销毁工做。同时将伊甸园中的还有其余对象引用的对象移动到幸存者0区。幸存者0区就是用于存放伊甸园垃圾回收时所幸存下来的JAVA对象。当将伊甸园中的还有其余对象引用的对象移动到幸存者0区时,若是幸存者0区也没有空间来存放这些对象时,JVM的垃圾回收器将对幸存者0区进行垃圾回收处理,将幸存者0区中不在有其余对象引用的JAVA对象进行销毁,将幸存者0区中还有其余对象引用的对象移动到幸存者1区。幸存者1区的做用就是用于存放幸存者0区垃圾回收处理所幸存下来的JAVA对象。
养老区(Tenure (Old) generation space):用于保存重新生区筛选出来的JAVA对象。
上面咱们看了JVM的内存分区管理,如今咱们来看JVM的垃圾回收工做是怎样运做的。首先当启动J2EE应用服务器时,JVM随之启动,并将JDK的类和接口,应用服务器运行时须要的类和接口以及J2EE应用的类和接口定义文件也及编译后的Class文件或JAR包中的Class文件装载到JVM的永久存储区。在伊甸园中建立JVM,应用服务器运行时必须的JAVA对象,建立J2EE应用启动时必须建立的JAVA对象;J2EE应用启动完毕,可对外提供服务。
JVM在伊甸园区根据用户的每次请求建立相应的JAVA对象,当伊甸园的空间不足以用来建立新JAVA对象的时候,JVM的垃圾回收器执行对伊甸园区的垃圾回收工做,销毁那些再也不被其余对象引用的JAVA对象(若是该对象仅仅被一个没有其余对象引用的对象引用的话,此对象也被归为没有存在的必要,依此类推),并将那些被其余对象所引用的JAVA对象移动到幸存者0区。
若是幸存者0区有足够控件存放则直接放到幸存者0区;若是幸存者0区没有足够空间存放,则JVM的垃圾回收器执行对幸存者0区的垃圾回收工做,销毁那些再也不被其余对象引用的JAVA对象(若是该对象仅仅被一个没有其余对象引用的对象引用的话,此对象也被归为没有存在的必要,依此类推),并将那些被其余对象所引用的JAVA对象移动到幸存者1区。
若是幸存者1区有足够控件存放则直接放到幸存者1区;若是幸存者0区没有足够空间存放,则JVM的垃圾回收器执行对幸存者0区的垃圾回收工做,销毁那些再也不被其余对象引用的JAVA对象(若是该对象仅仅被一个没有其余对象引用的对象引用的话,此对象也被归为没有存在的必要,依此类推),并将那些被其余对象所引用的JAVA对象移动到养老区。
若是养老区有足够控件存放则直接放到养老区;若是养老区没有足够空间存放,则JVM的垃圾回收器执行对养老区区的垃圾回收工做,销毁那些再也不被其余对象引用的JAVA对象(若是该对象仅仅被一个没有其余对象引用的对象引用的话,此对象也被归为没有存在的必要,依此类推),并保留那些被其余对象所引用的JAVA对象。若是到最后养老区,幸存者1区,幸存者0区和伊甸园区都没有空间的话,则JVM会报告“JVM堆空间溢出(java.lang.OutOfMemoryError: Java heap space)”,也便是在堆空间没有空间来建立对象。
这就是JVM的内存分区管理,相比不分区来讲;通常状况下,垃圾回收的速度要快不少;由于在没有必要的时候不用扫描整片内存而节省了大量时间。