JVM 类加载机制原理
在类生命周期里面分析过,类首先要被加载,形成一个Class实例,然后进行动态连接,然后进行初始化、实例化等,最后在类型不可触及时,被卸载收集。然而类是被什么加载到JVM内存里的呢?当然是类加载器。下面,详细分析下类加载器。java虚拟机中可以安装多个类加载器,系统默认主要有三个类加载器,每个类负责加载特定位置的类:BootStrap,ExtClassLoader,AppClassLoade
在类生命周期里面分析过,类首先要被加载,形成一个Class实例,然后进行动态连接,然后进行初始化、实例化等,最后在类型不可触及时,被卸载收集。然而类是被什么加载到JVM内存里的呢?当然是类加载器。下面,详细分析下类加载器。
java虚拟机中可以安装多个类加载器,系统默认主要有三个类加载器,每个类负责加载特定位置的类:BootStrap,ExtClassLoader,AppClassLoader。当然也可以自定义类加载器,自定义的加载器必须继承ClassLoader。
1.启动类加载器(Bootstrap):加载JVM的类加载器,它是采用本地代码实现的,如C/C++等。它涉及到了具体JVM的本地实现细节,所以,开发者无法获得该加载器的引用。它负责将%JAVA_HOME%/lib目录下的类库加载到内存中;
2.标准扩展类加载器(Extension):开发者可以使用这种类型的加载器来加载类。它负责将%JAVA_HOME%/lib/ext或由系统变量 java.ext.dir 指定位置中的类库加载到内存中
3.系统类加载器(System):开发者可以使用这种类型的加载器来加载类。它负责将CLASSPATH指定的路径中的类库加载到内存中
4.用户自定义的类加载器:
JVM在加载类 时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
为了更好的理解类的加载机制,我们先来看看类加载器中几个重要的方法:
loadClass()方法:加载器的总调度方法。它会根据加载器双亲委派机制来选择适当的加载器加载出指定类名称的Class实例
findClass()方法:读取class文件形成一个字节数组,然后将该字节数组传递给 defineClass方法,试着导入这个类型,返回一个Class实例
defineClass方法:根据传入的资源,调用本地方法进行具体的类的加载过程。加载成功会返回一个Class实例
===http://developer.51cto.com/art/201103/249613.htm
1. Classloader的作用,概括来说就是将编译后的class装载、加载到机器内存中,为了以后的程序的执行提供前提条件。
2. 一段程序引发的思考:
风中叶老师在他的视频中给了我们一段程序,号称是世界上所有的Java程序员都会犯的错误。
诡异代码如下:
Java代码
- package test01;
- class Singleton {
- public static Singleton singleton = new Singleton();
- public static int a;
- public static int b = 0;
- private Singleton() {
- super();
- a++;
- b++;
- }
- public static Singleton GetInstence() {
- return singleton;
- }
- }
- public class MyTest {
- /**
- * @param args
- */
- public static void main(String[] args) {
- Singleton mysingleton = Singleton.GetInstence();
- System.out.println(mysingleton.a);
- System.out.println(mysingleton.b);
- }
- }
一般不假思索的结论就是,a=1,b=1。给出的原因是:a、b都是静态变量,在构造函数调用的时候已经对a和b都加1了。答案就都是1。但是运行完后答案却是a=1,b=0。
下面我们将代码稍微变一下
Java代码
- public static Singleton singleton = new Singleton();
- public static int a;
- public static int b = 0;
的代码部分替换成
Java代码
- public static int a;
- public static int b = 0;
- public static Singleton singleton = new Singleton();
效果就是刚才预期的a=1,b=1。
为什么呢,这3句无非就是静态变量的声明、初始化,值的变化和声明的顺序还有关系吗?Java不是面向对象的吗?怎么和结构化的语言似地,顺序还有关系。这个就是和Java虚拟机JVM加载类的原理有着直接的关系。
1. 类在JVM中的工作原理
要想使用一个Java类为自己工作,必须经过以下几个过程
1):类加载load:从字节码二进制文件——.class文件将类加载到内存,从而达到类的从硬盘上到内存上的一个迁移,所有的程序必须加载到内存才能工作。将内存中的class放到运行时数据区的方法区内,之后在堆区建立一个java.lang.Class对象,用来封装方法区的数据结构。这个时候就体现出了万事万物皆对象了,干什么事情都得有个对象。就是到了最底层究竟是鸡生蛋,还是蛋生鸡呢?类加载的最终产物就是堆中的一个java.lang.Class对象。
2):连接:连接又分为以下小步骤
验证:出于安全性的考虑,验证内存中的字节码是否符合JVM的规范,类的结构规范、语义检查、字节码操作是否合法、这个是为了防止用户自己建立一个非法的XX.class文件就进行工作了,或者是JVM版本冲突的问题,比如在JDK6下面编译通过的class(其中包含注解特性的类),是不能在JDK1.4的JVM下运行的。
准备:将类的静态变量进行分配内存空间、初始化默认值。(对象还没生成呢,所以这个时候没有实例变量什么事情)
解析:把类的符号引用转为直接引用(保留)
3):类的初始化: 将类的静态变量赋予正确的初始值,这个初始值是开发者自己定义时赋予的初始值,而不是默认值。
2. 类的主动使用与被动使用
以下是视为主动使用一个类,其他情况均视为被动使用!
1):初学者最为常用的new一个类的实例对象(声明不叫主动使用)
2):对类的静态变量进行读取、赋值操作的。
3):直接调用类的静态方法。
4):反射调用一个类的方法。
5):初始化一个类的子类的时候,父类也相当于被程序主动调用了(如果调用子类的静态变量是从父类继承过来并没有复写的,那么也就相当于只用到了父类的东东,和子类无关,所以这个时候子类不需要进行类初始化)。
6):直接运行一个main函数入口的类。
所有的JVM实现(不同的厂商有不同的实现,有人就说IBM的实现比Sun的要好……)在首次主动调用类和接口的时候才会初始化他们。
1. 类的加载方式
1):本地编译好的class中直接加载
2):网络加载:java.net.URLClassLoader可以加载url指定的类
3):从jar、zip等等压缩文件加载类,自动解析jar文件找到class文件去加载util类
4):从java源代码文件动态编译成为class文件
2. 类加载器
JVM自带的默认加载器
1):根类加载器:bootstrap,由C++编写,所有Java程序无法获得。
2):扩展类加载器:由Java编写。
3):系统类、应用类加载器:由Java编写。
用户自定义的类加载器:java.lang.ClassLoader的子类,用户可以定制类的加载方式。每一个类都包含了加载他的ClassLoader的一个引用——getClass().getClassLoader()。如果返回的是null,证明加载他的ClassLoader是根加载器bootstrap。
如下代码
这里面的指针就是C++的指针
1. 回顾那个诡异的代码
从入口开始看
Singleton mysingleton = Singleton.GetInstence();
是根据内部类的静态方法要一个Singleton实例。
这个时候就属于主动调用Singleton类了。
之后内存开始加载Singleton类
1):对Singleton的所有的静态变量分配空间,赋默认的值,所以在这个时候,singleton=null、a=0、b=0。注意b的0是默认值,并不是咱们手工为其赋予的的那个0值。
2):之后对静态变量赋值,这个时候的赋值就是我们在程序里手工初始化的那个值了。此时singleton = new Singleton();调用了构造方法。构造方法里面a=1、b=1。之后接着顺序往下执行。
3):
- public static int a;
- public static int b = 0;
a没有赋值,保持原状a=1。b被赋值了,b原先的1值被覆盖了,b=0。所以结果就是这么来的。类中的静态块static块也是顺序地从上到下执行的。
2. 编译时常量、非编译时常量的静态变量
如下代码
Java代码
- package test01;
- class FinalStatic {
- public static final int A = 4 + 4;
- static {
- System.out.println("如果执行了,证明类初始化了……");
- }
- }
- public class MyTest03 {
- /**
- * @param args
- */
- public static void main(String[] args) {
- System.out.println(FinalStatic.A);
- }
- }
结果是只打印出了8,证明类并没有初始化。反编译源码发现class里面的内容是
public static final int A = 8;
也就是说编译器很智能的、在编译的时候自己就能算出4+4是8,是一个固定的数字。没有什么未知的因素在里面。
将代码稍微改一下
public static final int A = 4 + new Random().nextInt(10);
这个时候静态块就执行了,证明类初始化了。在静态final变量在编译时不定的情况下。如果客户程序这个时候访问了该类的静态变量,那就会对类进行初始化,所以尽量静态final变量尽量没什么可变因素在里面1,否则性能会有所下降。
1. ClassLoader的剖析
ClassLoader的loadClass方法加载一个类不属于主动调用,不会导致类的初始化。如下代码块
Java代码
- ClassLoader classLoader = ClassLoader.getSystemClassLoader();
- Class clazz = classLoader.loadClass("test01.ClassDemo");
并不会让类加载器初始化test01.ClassDemo,因为这不属于主动调用此类。
ClassLoader的关系:
根加载器——》扩展类加载器——》应用类加载器——》用户自定义类加载器
加载类的过程是首先从根加载器开始加载、根加载器加载不了的,由扩展类加载器加载,再加载不了的有应用加载器加载,应用加载器如果还加载不了就由自定义的加载器(一定继承自java.lang. ClassLoader)加载、如果自定义的加载器还加载不了。而且下面已经没有再特殊的类加载器了,就会抛出ClassNotFoundException,表面上异常是类找不到,实际上是class加载失败,更不能创建该类的Class对象。
若一个类能在某一层类加载器成功加载,那么这一层的加载器称为定义类加载器。那么在这层类生成的Class引用返回下一层加载器叫做初始类加载器。因为加载成功后返回一个Class引用给它的服务对象——也就是调用它的类加载器。考虑到安全,父委托加载机制。
ClassLoader加载类的原代码如下
Java代码
- protected synchronized Class loadClass(String name, boolean resolve)
- throws ClassNotFoundException
- {
- // First, check if the class has already been loaded
- Class c = findLoadedClass(name);
- if (c == null) {
- try {
- 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.
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
初始化系统ClassLoader代码如下
Java代码
- private static synchronized void initSystemClassLoader() {
- if (!sclSet) {
- if (scl != null)
- throw new IllegalStateException("recursive invocation");
- sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
- if (l != null) {
- Throwable oops = null;
- scl = l.getClassLoader();
- try {
- PrivilegedExceptionAction a;
- a = new SystemClassLoaderAction(scl);
- scl = (ClassLoader) AccessController.doPrivileged(a);
- } catch (PrivilegedActionException pae) {
- oops = pae.getCause();
- if (oops instanceof InvocationTargetException) {
- oops = oops.getCause();
- }
- }
- if (oops != null) {
- if (oops instanceof Error) {
- throw (Error) oops;
- } else {
- // wrap the exception
- throw new Error(oops);
- }
- }
- }
- sclSet = true;
- }
- }
它里面调用了很多native的方法,也就是通过JNI调用底层C++的代码。
当一个类被加载、连接、初始化后,它的生命周期就开始了,当代表该类的Class对象不再被引用、即已经不可触及的时候,Class对象的生命周期结束。那么该类的方法区内的数据也会被卸载,从而结束该类的生命周期。一个类的生命周期取决于它Class对象的生命周期。由Java虚拟机自带的默认加载器(根加载器、扩展加载器、系统加载器)所加载的类在JVM生命周期中始终不被卸载。所以这些类的Class对象(我称其为实例的模板对象)始终能被触及!而由用户自定义的类加载器所加载的类会被卸载掉!
===================
开发自己的类加载器
虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输 Java 类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在 Java 虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。
文件系统类加载器
第一个类加载器用来加载存储在文件系统上的 Java 字节代码。完整的实现如 代码清单 6所示。
清单 6. 文件系统类加载器
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } }
如 代码清单 6所示,类 FileSystemClassLoader
继承自类 java.lang.ClassLoader
。在 表 1中列出的 java.lang.ClassLoader
类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)
方法即可。java.lang.ClassLoader
类的方法 loadClass()
封装了前面提到的代理模式的实现。该方法会首先调用 findLoadedClass()
方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()
方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用 findClass()
方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()
方法,而是覆写 findClass()
方法。
类 FileSystemClassLoader
的 findClass()
方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass()
方法来把这些字节代码转换成 java.lang.Class
类的实例。
网络类加载器
下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。
类 NetworkClassLoader
负责通过网络下载 Java 类字节代码并定义出 Java 类。它的实现与 FileSystemClassLoader
类似。在通过NetworkClassLoader
加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用 Java 反射 API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用 Java 反射 API 可以直接调用 Java 类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。网络类加载器的具体代码见 下载。
在介绍完如何开发自己的类加载器之后,下面说明类加载器和 Web 容器的关系。
类加载器与 Web 容器
对于运行在 Java EE™容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:
- 每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在
WEB-INF/classes
和WEB-INF/lib
目录下面。 -
- 多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
-
- 当 出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。
往往在项目中会包含很多相同的jar包,这样,tomcat加载多个项目的时候会经常出现java.lang.OutOfMemoryError: PermGen space内存溢出的异常。
PermGen space到底是什么异常呢?
permGen space的全称是Permanent Generation space (内存永久保存区域) ,从表面看这个问题就是内存溢出了,解决方案就是加大服务器的内存。但是为什么会出现内存溢出呢?class在被类加载器loader的时候会放入到permGen space这个区域,他和存放Instance的Heap区域不同,Garbage Collection 不会在主程序运行期间对这个区域进行清理,所以如果我们的应用中有很多class被加载到这个区域时,就可能会出现java.lang.OutOfMemoryError: PermGen space内存溢出的异常了。
如何解决?
以tomcat容器为例。
1、手动设置MaxPermSize的大小,vim ${ TOMCAT_HOME }/bin/catalina.sh,找到echo "Using CATALINA_BASE: $CATALINA_BASE",在此之上加入:JAVA_OPTS="-server -XX:PermSize=64M -XX:MaxPermSize=128m。
2、将多应用之间的相同的jar分离出来,放置${ TOMCAT_HOME }/shared/lib目录下,修改${ TOMCAT_HOME }/conf/catalina.properties文件中shared.loader=${catalina.base}/shared/lib,${catalina.base}/shared/lib/*.jar
也可以将公用的jar全部放置${ TOMCAT_HOME }/lib包下。
这样子就可以减少jar重复加载占用空间内存了。
更多推荐
所有评论(0)