JVM笔记01-类加载

0. 前言

JVM笔记系列,以JDK1.7为基准,主要以《深入理解Java虚拟机》(第二版)和《Java虚拟机规范(Java SE 7版)》 为参考,主要包括下图所示的五部分内容:1.类加载,2.内存区域,3.垃圾回收,4.JVM参数,5.JVM监控工具。

本人是Java程序员,重点关注这些有助于优化开发、性能调优、问题解决等这些和具体生产密切相关的部分;关于Class的文件结构、编译、指令等部分,可以阅读上述书籍或其它材料。

jvm.png

本文主要记录类加载相关知识,文章结构和主要知识点如下:

类加载.png

1. 概念

类的加载,是指类加载器把class文件(字节码)加载到内存,并对数据进行验证、转化解析和初始化,最终形成可以被JVM使用的Java类型。

class文件可以从本地系统、网络、压缩文件、数据库等任意位置加载,也可以是动态编译的class文件(例如JSP)。

类加载器:通过一个类的全限定名来获取描述此类的二进制字节流,实现这个动作的代码被成为“类加载器”。对于任何一个类,它在JVM中的唯一性,由加载它的类加载器和这个类本身一起确立,即使是同一个Class文件,在同一个JVM中,如果被两个不同的类加载器加载,他们必不相等。

2. 类的生命周期

如下图所示,类的生命周期包加载、验证、准备、接卸、初始化、使用和卸载7个阶段。其中加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,出于支持Java语言动态绑定的目的,解析阶段也可以在初始化之后再执行。

class-life-circle.png

2.1 加载

加载是类加载过程的第一个阶段,JVM需要完成以下三个事情。

  1. 通过一个类的全限定名(包名+类名)来获取定义此类的二进制字节流。
  2. 将该字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成代表这个类的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个动作。

  1. 文件格式验证,验证字节流是否符合Class文件格式规范,例如是否属于本版本的JVM处理的范围内。
  2. 元数据验证,对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范要求。
  3. 字节码验证,通过对数据流和控制流的分析,确认程序语义是合法和符合逻辑的
  4. 符号引用验证,确保解析动作能够正常执行。

验证阶段很重要,但不是必须的,如果所引用的类已经反复验证,可以采用-Xverifynone参数关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

2.3 准备

准备阶段是为类的静态变量分配内存,并将其初始化为默认值。

  1. 这里说的类的变量,是static修饰的静态变量,而不是类的实例常量。
  2. 这里说的初始化默认值,是其零值,而不是在java代码中显式为其赋的值。

假设一个类的变量定义为:public static int value = 123;那么,在准备阶段,value的值为零值,即0。

2.4 解析

解析阶段是把常量池中的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用限定符等7类符号引用进行。

符号引用就是一组符号来描述目标,可以是任何字面量;直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

2.5 初始化

一个类,只有经过加载和连接阶段,才会进入初始化阶段。初始化为类的变量赋予正确的初始值,无论是在声明类变量时为其指定初始值,还是通过静态代码为其指定初始值。

假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类。假如类中存在初始化语句,那就依次执行这些初始化语句。

初始化的时机发生在首次主动使用的时候,包括创建类的实例、访问类(接口)的静态变量或对静态变量赋值、调用类的静态方法、反射、初始化该类的子类、JVM启动时被标名为启动类的类。

2.6 卸载

当一个类的Class对象不再被引用,Class对象就会结束生命周期,其在方法区中的数据也会被卸载。

3. 类加载器

站在开发人员的角度,类加载器分为以下三分:启动类加载器、扩展类加载器、应用类加载器。

class-loader.png
  1. 启动类加载器(Bootstrap ClassLoader): C++实现的(Hotspot),负责加载jre/lib目录下的类库(例如rt.jar),或者使用-Xbootclasspath参数指定的路径中类库。我们无法在java代码中直接引用启动类加载器。

  2. 扩展类加载器(Extension ClassLoader):在sun.misc.Launcher$ExtClassLoader中实现,负责加载jre/lib/ext目录中的类库,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),我们可以在代码中直接使用扩展类加载器。

  3. 应用类加载器(Application ClassLoader):在sun.misc.Launcher$AppClassLoader中实现,负责加载ClassPath中所指定的类,如果程序中没有定义自己的类加载器,那么默认使用应用类加载器。

  4. 自定义类加载器
    通常情况下,我们都是直接使用系统类加载器,但有时候我们也需要自定义类加载器,比如加载从网络传来的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。异常是可以捕获和采取补救措施的。

(完)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容