0. 前言
JVM笔记系列,以JDK1.7为基准,主要以《深入理解Java虚拟机》(第二版)和《Java虚拟机规范(Java SE 7版)》 为参考,主要包括下图所示的五部分内容:1.类加载,2.内存区域,3.垃圾回收,4.JVM参数,5.JVM监控工具。
本人是Java程序员,重点关注这些有助于优化开发、性能调优、问题解决等这些和具体生产密切相关的部分;关于Class的文件结构、编译、指令等部分,可以阅读上述书籍或其它材料。
本文主要记录类加载相关知识,文章结构和主要知识点如下:
1. 概念
类的加载,是指类加载器把class文件(字节码)加载到内存,并对数据进行验证、转化解析和初始化,最终形成可以被JVM使用的Java类型。
class文件可以从本地系统、网络、压缩文件、数据库等任意位置加载,也可以是动态编译的class文件(例如JSP)。
类加载器:通过一个类的全限定名来获取描述此类的二进制字节流,实现这个动作的代码被成为“类加载器”。对于任何一个类,它在JVM中的唯一性,由加载它的类加载器和这个类本身一起确立,即使是同一个Class文件,在同一个JVM中,如果被两个不同的类加载器加载,他们必不相等。
2. 类的生命周期
如下图所示,类的生命周期包加载、验证、准备、接卸、初始化、使用和卸载7个阶段。其中加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,出于支持Java语言动态绑定的目的,解析阶段也可以在初始化之后再执行。
2.1 加载
加载是类加载过程的第一个阶段,JVM需要完成以下三个事情。
- 通过一个类的全限定名(包名+类名)来获取定义此类的二进制字节流。
- 将该字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成代表这个类的java.lang.Class对象,作为方法区该类的各种数据的访问入口。
加载阶段是开发人员可以介入的阶段,可以决定使用何种类加载器。
关于java.lang.Class对象在内存中的具体位置,有同学直接说是Heap,这里补充一点,在JDK1.7中,Class对象位于PermGen(永久代)中,PermGen是Heap的逻辑部分。JDK1.8中移除了PermGen,Class对象位于Heap。
关于JDK1.7中PermGen中的存放的数据,可以参考下面的链接:http://rednaxelafx.iteye.com/blog/730461。
2.2 验证
验证是为了确保被加载的类的正确性,确保其符合JVM要求,并不会危害JVM自身安全。验证阶段包括以下4个动作。
- 文件格式验证,验证字节流是否符合Class文件格式规范,例如是否属于本版本的JVM处理的范围内。
- 元数据验证,对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范要求。
- 字节码验证,通过对数据流和控制流的分析,确认程序语义是合法和符合逻辑的
- 符号引用验证,确保解析动作能够正常执行。
验证阶段很重要,但不是必须的,如果所引用的类已经反复验证,可以采用-Xverifynone参数关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
2.3 准备
准备阶段是为类的静态变量分配内存,并将其初始化为默认值。
- 这里说的类的变量,是static修饰的静态变量,而不是类的实例常量。
- 这里说的初始化默认值,是其零值,而不是在java代码中显式为其赋的值。
假设一个类的变量定义为:public static int value = 123;那么,在准备阶段,value的值为零值,即0。
2.4 解析
解析阶段是把常量池中的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用限定符等7类符号引用进行。
符号引用就是一组符号来描述目标,可以是任何字面量;直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
2.5 初始化
一个类,只有经过加载和连接阶段,才会进入初始化阶段。初始化为类的变量赋予正确的初始值,无论是在声明类变量时为其指定初始值,还是通过静态代码为其指定初始值。
假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类。假如类中存在初始化语句,那就依次执行这些初始化语句。
初始化的时机发生在首次主动使用的时候,包括创建类的实例、访问类(接口)的静态变量或对静态变量赋值、调用类的静态方法、反射、初始化该类的子类、JVM启动时被标名为启动类的类。
2.6 卸载
当一个类的Class对象不再被引用,Class对象就会结束生命周期,其在方法区中的数据也会被卸载。
3. 类加载器
站在开发人员的角度,类加载器分为以下三分:启动类加载器、扩展类加载器、应用类加载器。
启动类加载器(Bootstrap ClassLoader): C++实现的(Hotspot),负责加载jre/lib目录下的类库(例如rt.jar),或者使用-Xbootclasspath参数指定的路径中类库。我们无法在java代码中直接引用启动类加载器。
扩展类加载器(Extension ClassLoader):在sun.misc.Launcher$ExtClassLoader中实现,负责加载jre/lib/ext目录中的类库,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),我们可以在代码中直接使用扩展类加载器。
应用类加载器(Application ClassLoader):在sun.misc.Launcher$AppClassLoader中实现,负责加载ClassPath中所指定的类,如果程序中没有定义自己的类加载器,那么默认使用应用类加载器。
-
自定义类加载器
通常情况下,我们都是直接使用系统类加载器,但有时候我们也需要自定义类加载器,比如加载从网络传来的Class,或者解密被加密的Class。自定义类加载器,一般来说继承ClassLoader,并重新findClass方法即可。public class MyClassLoader extends ClassLoader { // 类的根路径,要和ClassPath不同,否则就被AppClassLoader加载了。 private String root; public MyClassLoader(String root) { this.root = root; } /** * 重写父类的findClass方法,这样符合双亲委派模型 * * @param name 全限定符的类名 * @return 该类的Class对象 * @throws ClassNotFoundException */ @Override protected Class> findClass(String name) throws ClassNotFoundException { byte[] classData = loadClassData(name); if (classData == null) { throw new ClassNotFoundException(name); } else { return defineClass(name, classData, 0, classData.length); } } /** * 获取字节码,是自定义ClassLoader的核心部分 * * @param className 全限定符的类名 * @return 类的字节数组 */ private byte[] loadClassData(String className) { String fileName = root + File.separator + className.replace(".", File.separator) + ".class"; InputStream in = null; ByteArrayOutputStream out = null; try { in = new FileInputStream(fileName); out = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int length = 0; while ((length = in.read(buffer)) != -1) { out.write(buffer, 0, length); } return out.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { if (out != null) { out.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (in != null) { in.close(); } } catch (IOException e) { e.printStackTrace(); } } return null; } }
如果重写loadClass方法,容易破坏双亲模型;另外还需要注意,通过自定义类加载器加载的类,不能放在ClassPath下面,否则,根据双亲委派模型,会导致该类被AppClassLoader加载。
4. 类加载的几个机制
全盘负责:当一个ClassLoader负责加载某个class时,该class所依赖和引用的其它class,均由该ClassLoader负责;除非显示地使用另外一个ClassLoader来载入。
缓存机制:缓存机制使得所有被加载过得class都会被缓存起来,当程序需要使用某个class的时候,类加载器首先在cache中寻找该class。只有在cache中找不到,才会读取该class的字节码,并将其转换成Class对象,存入缓存区。
双亲委托模型:当一个类加载器收到加载Class的请求时,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。双亲委派模型可以防止内存中出现多份同样的字节码。
我们通过重写ClassLoader的loadClass()方法可以破坏双亲委托模型,还可以通过线程上下文类加载器(Thread Context ClassLoader)实现让启动类加载器“认识”子类加载器加载的Class(例如JNDI、JDBC等)。此外,代码热替换、模块热部署,也需要我们通过违反双亲委托模型来实现。
5. 类的加载方式
类的加载方式有三种。
1.使用new关键字 属于静态加载,使用当前的类加载器。
Cat cat = new Cat();
2.使用反射机制Class.forName() 属于动态加载,使用当前的类加载器。
Class clazz = Class.forName(“org.animal.Dog”);
3.使用ClassLoader.loadClass() 属于动态加载,使用指定的类加载器,例如用来加载不在classpath下的类。
Class clazz = classLoader.loadClass(“org.animal.Dog”);
关于Class.forName(className)和ClassLoader.loadClass(className)的区别,主要有以下两点。
- Class.forName使用的是当前类加载器,而ClassLoader.loadClass可以使用其它的类加载器。
- Class.forName装载的Class已经被初始化,而ClassLoader.loadClass装载的class还没有被连接(验证、准备、解析)。即Class.forName会执行类中的static代码快,ClassLoader.loadClass只是把class文件加载到jvm中,只有newInstance的时候才会执行static代码块。
在JDBC编程中,第一步就要加载驱动,即使用Class.forName("com.mysql.jdbc.Driver");
;换成ClassLoader.loadClass方式就不行了。因为驱动类中包含静态块,静态块中的代码把Driver注册到了DriverManager,Class.forName的方式才会执行static代码块。
com.mysql.jdbc.Driver源码如下:
// Register ourselves with the DriverManager
static {
try {
Java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
6. 类加载相关的两个异常
类加载相关的两个异常分别是NoClassDefFoundError和ClassNotFoundException,前者是错误(Error),后者是异常。
NoClassDefFoundError产生的原因是,要查找的类在编译时是存在的,运行时候却找不到了;即连接时从内存里找不到需要的class。编译代码工程,然后删除某个class文件,再执行程序,就能出现这个错误。解决这个问题的办法就是,查找那些开发期间位于classpath下但运行期却不在classpath下的类。
ClassNotFoundException产生的原因是加载时从外部存储(文件、网络等)找不到需要加载的class。此外,如果一个类已经被某个类加载器加载到内存里了,此时另一个加载器又尝试动态地从同一个包中加载这个类,也可能导致ClassNotFoundException。异常是可以捕获和采取补救措施的。
(完)