您的位置:首页 > 编程语言 > Java开发

Java 类加载机制 ClassLoader Class.forName 内存管理 垃圾回收GC

2015-08-27 10:26 951 查看
【转载】 :/article/3520430.html

Java之类加载机制

类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指导开发者采取更有效的措施配合程序执行。

研究类加载机制的第二个目的是让程序能动态的控制类加载,比如热部署等,提高程序的灵活性和适应性。

一、简单过程

Java程序运行的场所是内存,当在命令行下执行:

java HelloWorld

命令的时候,JVM会将HelloWorld.class加载到内存中,并形成一个Class的对象HelloWorld.class。

其中的过程就是类加载过程:

1、寻找jre目录,寻找jvm.dll,并初始化JVM;

2、产生一个Bootstrap Loader(启动类加载器);

3、Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为Bootstrap Loader。

4、Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extended Loader。

5、最后由AppClass Loader加载HelloWorld类。

以上就是类加载的最一般的过程。

二、类加载器各自搜索的目录

为了弄清楚这个问题,首先还要看看System类的API doc文档。
1、Bootstrap Loader(启动类加载器):加载System.getProperty("sun.boot.class.path")所指定的路径或jar。
2、Extended Loader(标准扩展类加载器ExtClassLoader):加载System.getProperty("java.ext.dirs")所指定的路径或jar。在使用Java运行程序时,也可以指定其搜索路径,例如:java -Djava.ext.dirs=d:/projects/testproj/classes HelloWorld
3、AppClass Loader(系统类加载器AppClassLoader):加载System.getProperty("java.class.path")所指定的路径或jar。在使用Java运行程序时,也可以加上-cp来覆盖原有的Classpath设置,例如: java -cp ./lavasoft/classes HelloWorld
ExtClassLoader和AppClassLoader在JVM启动后,会在JVM中保存一份,并且在程序运行中无法改变其搜索路径。如果想在运行时从其他搜索路径加载类,就要产生新的类加载器。
三、类加载器的特点

1、运行一个程序时,总是由AppClass Loader(系统类加载器)开始加载指定的类。
2、在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。

3、Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null.

四、类加载器的获取
很容易,看下面例子

public class HelloWorld {

public static void main(String[] args) {

HelloWorld hello = new HelloWorld();

Class c = hello.getClass();

ClassLoader loader = c.getClassLoader();

System.out.println(loader);

System.out.println(loader.getParent());

System.out.println(loader.getParent().getParent());

}

}

打印结果:

sun.misc.Launcher$AppClassLoader@19821f

sun.misc.Launcher$ExtClassLoader@addbf1

null

Process finished with exit code 0

从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是Bootstrap Loader(启动类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。
五、类的加载
类加载有三种方式:
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载
三种方式区别比较大,看个例子就明白了:

public class HelloWorld {

public static void main(String[] args) throws ClassNotFoundException
{

ClassLoader loader = HelloWorld. class.getClassLoader();

System.out.println(loader);

//使用ClassLoader.loadClass()来加载类,不会执行初始化块

loader.loadClass( "Test2");

//使用Class.forName()来加载类,默认会执行初始化块

// Class.forName("Test2");

//使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块

// Class.forName("Test2", false, loader);

}

}

public class Test2 {

static {

System.out.println( "静态初始化块执行了!");

}

}

分别切换加载方式,会有不同的输出结果。
六、自定义ClassLoader
为了说明问题,先看例子:

package test;

import java.net.MalformedURLException;

import java.net.URL;

import java.net.URLClassLoader;

/**

* 自定义ClassLoader

*

* @author leizhimin 2009-7-29 22:05:48

*/

public class MyClassLoader {

public static void main(String[] args) throws MalformedURLException,
ClassNotFoundException, IllegalAccessException, InstantiationException {

URL url = new URL( "file:/E://projects//testScanner//out//production//testScanner");

ClassLoader myloader = new URLClassLoader( new URL[]{url});

Class c = myloader.loadClass( "test.Test3");

System.out.println( "----------");

Test3 t3 = (Test3) c.newInstance();

}

}

public class Test3 {

static {

System.out.println( "Test3的静态初始化块执行了!");

}

}

运行后:

----------

Test3的静态初始化块执行了!

Process finished with exit code 0

可以看出自定义了ClassLoader myloader = new URLClassLoader(new URL[]{url});已经成功将类Test3加载到内存了,并通过默认构造方法构造了对象Test3 t3 = (Test3) c.newInstance();
有关ClassLoader还有很重要一点:
同一个ClassLoader加载的类文件,只有一个Class实例。但是,如果同一个类文件被不同的ClassLoader载入,则会有两份不同的ClassLoader实例(前提是着两个类加载器不能用相同的父类加载器)。

在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

ClassLoader是用来处理类加载的类,它管理着具体类的运行时上下文。

1.ClassLoader存在的模块意义:

1)从java的package定义出发:

classloader是通过分层的关联方式来管理运行中使用的类,不同的classloader中管理的类是不相同的,或者即便两个类毫无二致(除了路径)也是不同的两个类,在进行强制转换时也会抛出ClassCastException。所以,通过classloader的限制,我们可以建立不同的package路径以区别不同的类(注意这里的“不同”是指,命名和实现完全一致,但是有不同的包路径。)。那么也是因为有特定的classloader,我们可以实现具体模块的加载,而不影响jvm中其他类,即发生类加载的冲突。

2)但是,如果两个在不同路径下的类(我们假定,这两个类定义中,不存在package声明,完全一样的两个类),经过不同的classloader加载,这两个类在jvm中产生的实例可以相互转换吗?

答案是否定的。即便这两个类除了存在位置不同之外,都完全一样。经由不同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默认实现。我们结合源代码说明一下:

?
在这段代码中,应该已经说明了很多问题,那就是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中加载完成。但是,再强调一下,那就是如果类有不同路径或者不同包名,那就是不同类定义。


java classLoader 体系结构

Bootstrap ClassLoader/启动类加载器

主要负责jdk_home/lib目录下的核心 api 或 -Xbootclasspath 选项指定的jar包装入工作。
Extension ClassLoader/扩展类加载器

主要负责jdk_home/lib/ext目录下的jar包或 -Djava.ext.dirs 指定目录下的jar包装入工作。
System ClassLoader/系统类加载器

主要负责java -classpath/-Djava.class.path所指的目录下的类与jar包装入工作。
User Custom ClassLoader/用户自定义类加载器(java.lang.ClassLoader的子类)

在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性。

类加载器的特性:

每个ClassLoader都维护了一份自己的名称空间, 同一个名称空间里不能出现两个同名的类。
为了实现java安全沙箱模型顶层的类加载器安全机制, java默认采用了 " 双亲委派的加载链 " 结构。





classloader-architecture





classloader-class-diagram

类图中, BootstrapClassLoader是一个单独的java类, 其实在这里, 不应该叫他是一个java类。因为,它已经完全不用java实现了。它是在jvm启动时, 就被构造起来的, 负责java平台核心库。

自定义类加载器加载一个类的步骤





classloader-load-class

ClassLoader 类加载逻辑分析, 以下逻辑是除 BootstrapClassLoader 外的类加载器加载流程:

// 检查类是否已被装载过
Class c = findLoadedClass(name);
if (c == null ) {
// 指定类未被装载过
try {
if (parent != null ) {
// 如果父类加载器不为空, 则委派给父类加载
c = parent.loadClass(name, false );
} else {
// 如果父类加载器为空, 则委派给启动类加载加载
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 启动类加载器或父类加载器抛出异常后, 当前类加载器将其
// 捕获, 并通过findClass方法, 由自身加载
c = findClass(name);
}
}

线程上下文类加载器

java默认的线程上下文类加载器是 系统类加载器(AppClassLoader)。

// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader" );
}

// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);

以上代码摘自sun.misc.Launch的无参构造函数Launch()。

使用线程上下文类加载器, 可以在执行线程中, 抛弃双亲委派加载链模式, 使用线程上下文里的类加载器加载类.

典型的例子有, 通过线程上下文来加载第三方库jndi实现, 而不依赖于双亲委派.

大部分java app服务器(jboss, tomcat..)也是采用contextClassLoader来处理web服务。

还有一些采用 hotswap 特性的框架, 也使用了线程上下文类加载器, 比如 seasar (full stack framework in japenese).

线程上下文从根本解决了一般应用不能违背双亲委派模式的问题.

使java类加载体系显得更灵活.

随着多核时代的来临, 相信多线程开发将会越来越多地进入程序员的实际编码过程中. 因此,

在编写基础设施时, 通过使用线程上下文来加载类, 应该是一个很好的选择。

当然, 好东西都有利弊. 使用线程上下文加载类, 也要注意, 保证多根需要通信的线程间的类加载器应该是同一个,

防止因为不同的类加载器, 导致类型转换异常(ClassCastException)。

为什么要使用这种双亲委托模式呢?

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时被加载,所以用户自定义类是无法加载一个自定义的ClassLoader。

java动态载入class的两种方式:

implicit隐式,即利用实例化才载入的特性来动态载入class
explicit显式方式,又分两种方式:

java.lang.Class的forName()方法
java.lang.ClassLoader的loadClass()方法

用Class.forName加载类

Class.forName使用的是被调用者的类加载器来加载类的。

这种特性, 证明了java类加载器中的名称空间是唯一的, 不会相互干扰。

即在一般情况下, 保证同一个类中所关联的其他类都是由当前类的类加载器所加载的。

public static Class forName(String className)
throws ClassNotFoundException {
return forName0(className, true , ClassLoader.getCallerClassLoader());
}

/** Called after security checks have been made. */
private static native Class forName0(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException;

上面中 ClassLoader.getCallerClassLoader 就是得到调用当前forName方法的类的类加载器

static块在什么时候执行?

当调用forName(String)载入class时执行,如果调用ClassLoader.loadClass并不会执行.forName(String,false,ClassLoader)时也不会执行.
如果载入Class时没有执行static块则在第一次实例化时执行.比如new ,Class.newInstance()操作
static块仅执行一次

各个java类由哪些classLoader加载?

java类可以通过实例.getClass.getClassLoader()得知
接口由AppClassLoader(System ClassLoader,可以由ClassLoader.getSystemClassLoader()获得实例)载入
ClassLoader类由bootstrap loader载入

NoClassDefFoundError和ClassNotFoundException

NoClassDefFoundError:当java源文件已编译成.class文件,但是ClassLoader在运行期间在其搜寻路径load某个类时,没有找到.class文件则报这个错
ClassNotFoundException:试图通过一个String变量来创建一个Class类时不成功则抛出这个异常

垃圾回收分为两大步骤:识别垃圾 和回收垃圾

识别垃圾有两大基本方法

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):是J***A对象生死存亡的地区,J***A对象的出生,成长,死亡都在这个区域完成。堆空间又分别按J***A对象的创建和年龄特征分为养老区和新生区。

新生区(Young (New) generation space ):新生区的作用包括J***A对象的创建和从J***A对象中筛选出能进入养老区的J***A对象。

伊甸园(Eden space):J***A对空间中的所有对象在此出生,该区的名字因此而得名。也即是说当你的J***A程序运行时,需要创建新的对象,JVM将在该区为你创建一个指定的对象供程序使用。创建对象的依据即是永久存储区中的元数据。

幸存者0区(Survivor 0 space)和幸存者1区(Survivor1
space):当伊甸园的控件用完时,程序又需要创建对象;此时JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁工作。同时将伊甸园中的还有其他对象引用的对象移动到幸存者0区。幸存者0区就是用于存放伊甸园垃圾回收时所幸存下来的J***A对象。当将伊甸园中的还有其他对象引用的对象移动到幸存者0区时,如果幸存者0区也没有空间来存放这些对象时,JVM的垃圾回收器将对幸存者0区进行垃圾回收处理,将幸存者0区中不在有其他对象引用的J***A对象进行销毁,将幸存者0区中还有其他对象引用的对象移动到幸存者1区。幸存者1区的作用就是用于存放幸存者0区垃圾回收处理所幸存下来的J***A对象。

养老区(Tenure (Old) generation space):用于保存从新生区筛选出来的J***A对象。

上面我们看了JVM的内存分区管理,现在我们来看JVM的垃圾回收工作是怎样运作的。首先当启动J2EE应用服务器时,JVM随之启动,并将JDK的类和接口,应用服务器运行时需要的类和接口以及J2EE应用的类和接口定义文件也及编译后的Class文件或JAR包中的Class文件装载到JVM的永久存储区。在伊甸园中创建JVM,应用服务器运行时必须的J***A对象,创建J2EE应用启动时必须创建的J***A对象;J2EE应用启动完毕,可对外提供服务。

JVM在伊甸园区根据用户的每次请求创建相应的J***A对象,当伊甸园的空间不足以用来创建新J***A对象的时候,JVM的垃圾回收器执行对伊甸园区的垃圾回收工作,销毁那些不再被其他对象引用的J***A对象(如果该对象仅仅被一个没有其他对象引用的对象引用的话,此对象也被归为没有存在的必要,依此类推),并将那些被其他对象所引用的J***A对象移动到幸存者0区。

如果幸存者0区有足够控件存放则直接放到幸存者0区;如果幸存者0区没有足够空间存放,则JVM的垃圾回收器执行对幸存者0区的垃圾回收工作,销毁那些不再被其他对象引用的J***A对象(如果该对象仅仅被一个没有其他对象引用的对象引用的话,此对象也被归为没有存在的必要,依此类推),并将那些被其他对象所引用的J***A对象移动到幸存者1区。

如果幸存者1区有足够控件存放则直接放到幸存者1区;如果幸存者0区没有足够空间存放,则JVM的垃圾回收器执行对幸存者0区的垃圾回收工作,销毁那些不再被其他对象引用的J***A对象(如果该对象仅仅被一个没有其他对象引用的对象引用的话,此对象也被归为没有存在的必要,依此类推),并将那些被其他对象所引用的J***A对象移动到养老区。

如果养老区有足够控件存放则直接放到养老区;如果养老区没有足够空间存放,则JVM的垃圾回收器执行对养老区区的垃圾回收工作,销毁那些不再被其他对象引用的J***A对象(如果该对象仅仅被一个没有其他对象引用的对象引用的话,此对象也被归为没有存在的必要,依此类推),并保留那些被其他对象所引用的J***A对象。如果到最后养老区,幸存者1区,幸存者0区和伊甸园区都没有空间的话,则JVM会报告“JVM堆空间溢出(java.lang.OutOfMemoryError:
Java heap space)”,也即是在堆空间没有空间来创建对象。

这就是JVM的内存分区管理,相比不分区来说;一般情况下,垃圾回收的速度要快很多;因为在没有必要的时候不用扫描整片内存而节省了大量时间。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: