为了账号安全,请及时绑定邮箱和手机立即绑定

类加载原理分析&动态加载Jar/Dex(一)

标签:
Java Android
JVM类加载机制

JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,如下图:
Paste_Image.png

由于本文主要讲解的是类的 加载 部分,所以加载,验证,准备,解析,初始化仅仅作下简单的回顾,详细内容参阅《深入理解Java虚拟机》

加载

类的加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说,当程序使用任何类时,系统都会为之创建一个java.lang.Class对象。

类也是一种对象,就像概念主要用于定义和描述其他的事物(反射中class含有各种信息),但概念本身也是一种事物。

通常有下面几种来源 加载 类的二进制数据

  1. 从本地文件系统加载class文件
  2. 从jar包中加载class文件,如从F盘动态加载jdbc的mysql驱动。
  3. 通过网络加载(典型应用Applet)
  4. 把一个java源文件动态编译并加载
  5. 从zip包读取,如jar,war,ear。
  6. 运算时计算生成(动态代理技术)
  7. 数据库中读取(可以加密处理)
  8. 其他文件生成(jsp文件生成对应的class文件)
验证

这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备

准备阶段开始为 类变量(即static变量) 分配内存并设置 类变量的初始值(注意初始值一般为0,null也是零值)的 阶段,这些变量所使用的内存都将在方法区进行分配。

注意
这里进行内存分配的仅包含 《类变量》即static变量,而不包括实例变量不包括实例变量不包括实例变量,重要的事说3遍,实例变量将会在 对象实例化时随着对象一起分配在Java堆中。且初始值 通常情况 下 是数据类型 的零值

Paste_Image.png

public class Animal {
    private static int age = 20;
}

那变量age在 准备阶段 过后的初始值是0而不是20,因为这时候还没有执行任何Java方法。所以把age赋值为20的动作将在 初始化 阶段执行。

再次注意
存在一种特殊情况,如果上面的类变量声明为final的,则此时(准备阶段)就会被初始化为20。
public static final int age = 20;
编译时候,JavaC将会为age生成ConstantValue属性,在准备阶段 虚拟机就会根据ConstantValue的设置将age赋值为20。

解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用(内存地址)的过程。

这里简单说下常量池

常量池
1. 字面量 比较接近Java语言层面,如String字符串,声明final的常量等
2. 符号引用 属于编译原理方面的概念:1.包括类和接口的全限定名 2.字段的名称和描述符3.方法的名称和描述符

符号引用大概是下面几种 类型

1. CONSTANT_Class_info
2. CONSTANT_Field_info
3. CONSTANT_Method_info

的常量。

初始化

类加载的最后一个阶段,除了加载阶段我们可以通过自定义类加载器参与之外,其余完全又JVM主导。到了初始化阶段,才真正开始执行类中定义的Java程序代码(字节码)

这里需要区分下<init> 和<client>

  1. <init>指的是实例构造器,也就是构造函数
  2. <client>指的是类构造器,这个构造器是jvm自动合并生成的。
    它合并static变量的赋值操作(1. 注意是赋值操作,仅声明的不会触发<client>,毕竟前面准备阶段已经默认赋过值为0了,2. final static的也是这样哦)和static{ }语句块生成,且虚拟机保证<client>执行前,父类的<client>已经执行完毕,所以说父类如果定义static块的话,一定比子类先执行,当然了,如果一个类或接口中没有static变量的赋值操作和static{ }语句块,那么<client>也不会被JVM生成。最后还要注意一点,static变量的赋值操作和static{}语句块合并的顺序是由语句在源文件中出现的顺序所决定的。

静态语句块只能访问定义在静态语句块之前的变量,定义在它之后的变量,前面的静态语句块只能赋值,不能访问。

Paste_Image.png

类初始化的时机

当Java程序 首次主动通过下面6种方式使用某个类或接口时候,系统就会初始化该类或接口,假如这个类还没有被 加载和连接,则程序先加载并连接该类。类的初始化只会发生一次,再次使用new,callMethod等等都不会重复初始化。

  1. 生成类的实例,如(1)new (2)反射newInstance (3)序列化生成obj
  2. 调用static的方法,如LogUtil.i(TAG,"fucking");
  3. 访问类或接口的 static变量,或者为static变量赋值。注意有特例(一会说明)。
  4. Class.forName(name);
  5. 初始化某个类的子类,子类的所有父类都被初始化
  6. java.exe 运行Main类(public static void main),jvm会先初始化该主类。

刚才说 3 有一个特例,需要特别指出,仍然是static的变量,前面说过,如果是 static final类型的则会在准备阶段 就给赋值并加入常量池。所以仅仅访问某个类的常量并不会导致该类初始化。

class Person{
    public static  int age = 20;

    static {
        System.out.println("静态初始化!");
    }
}

public class Test {

    public static void main(String args[]){
      System.out.println(Person.age);
  }

}

没有final修饰的情况打印

静态初始化!
20

加上final修饰后打印

20

以下是不会执行类初始化的几种情况

  1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  2. 定义对象数组,不会触发该类的初始化。
  3. 就是上面说的那种情况,类A引用类B的static final常量不会导致类B初始化。
  4. 通过类名获取Class对象,不会触发类的初始化。如
    System.out.println(Person.class);
  5. 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  6. 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

JVM类加载机制算是结尾了,不过在参考其他文章时候发现一个非常棒的例子,可以很好的验证上面的结论。

出处是 简书:小腊月

public class Singleton {
  private static Singleton singleton = new Singleton();
  public static int counter1;
  public static int counter2 = 0;
  private Singleton() {
      counter1++;
      counter2++;
  }
  public static Singleton getSingleton() {
      return singleton;
  }
 }
public class Main{
  public static void main(String args[]){
      Singleton singleton = Singleton.getSingleton();
      System.out.println("counter1="+singleton.counter1);
      System.out.println("counter2="+singleton.counter2);
  }
}

根据 类初始化的时机 所作的结论

  1. 执行Main方法,根据结论6,会首先初始化Main类,Main类从(加载开始 ----> 初始化结束)
  2. 执行到Singleton.getSingleton();时候,根据结论2,直接 【先】 触发【类的初始化】初始化Singleton类,Singleton类首次初始化,所以从 加载部分开始执行,执行到 准备阶段 所有static变量都被设置为初始值。此时
    public static int counter1 = 0;
    public static int counter2 = 0;
    private static Singleton singleton = null;
  3. Singleton执行到初始化阶段,生成类构造器<client>,类构造器会合并 static变量的赋值操作和 static语句块。合并后执行
    
    public static int counter1 ; // 由于 counter1没被赋值,所以不会被合并进去

public void client() {// 伪代码:<client>方法体内容
Test singleton = new Test();//(1)
int counter2 = 0;// (2)
}

4. **初始化阶段** 执行client内代码,执行到(1)处,此时counter1和counter2都变为1。
5. **初始化阶段** 执行client代码,执行到(2)处,counter2又被设置为0。
6. **初始化结束** ,回到Main方法的Singleton.getSingleton();继续执行main方法,最后输出结束。

最后打印结果为: 

counter1= 1
counter2= 0


## 详解类加载(第一阶段)
类加载部分是我们能够操作的部分,其他部分不需要我们管理。

Jvm启动时候默认至少开启了3个类加载器,分别是Bootstrap ClassLoader,Extension ClassLoader,Application ClassLoader各自加载各自管辖的区域。

![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1281543-3f7c2473524f8307.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

1. 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
2. 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
3. 应用程序类加载器(Application ClassLoader)或者叫**System ClassLoader**:负责加载用户路径(classpath)上的类库。

![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1281543-c86d07145d0bfadf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

>此程序说明:Java中getClassLoader用的一般和getSystemClassLoader是一个实例。源码中ClassLoader默认的构造器也说明这点,initSystemClassLoader里面会获取sun.misc.Launcher.getLauncher().getClassLoader()作为默认的parent。
**这里重点强调**:Android的ClassLoader类也有一个getSystemClassLoader()方法,但是又被改写了,后面再说明这个问题。

**再看下Android的类加载器的继承关系图**

![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1281543-0c081a7a812f8119.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

**注意**
1. 双亲委派是组合关系,就像上面图中JVM提供的3种类加载器一样。而这里是继承关系图。

2. 上面这个继承图画的不是很正确,网上找来另一张图,不过凑合着用吧,描述写的不错,稍微说明一下: BootClassLoader是ClassLoader的内部类,而URLClassLoader继承自SecureClassLoader,这两个类都是和Java里面的,一模一样,算是Java提供给我们的一个自定义ClassLoader工具类,专门用于加载本地或网络的jar文件(只能加载jar),但是Android不支持直接加载没有处理过的jar,一般都是dex过的,所以这两个类算是 **废物类**。去除这两个,真正需要研究的就只有BootClassLoader,PathClassLoader,DexClassLoader了。另外注意BootClassLoader和Java中那个Bootstrap ClassLoader没有半毛钱关系。

![Paste_Image.png](http://upload-images.jianshu.io/upload_images/1281543-458da3196f4df7e0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

下面是BootClassLoader 的局部代码,只是证明它是Java写的,不是C++,且它是ClassLoader 的内部类。Java的Bootstrap ClassLoader(C++实现)

class BootClassLoader extends ClassLoader {

private static BootClassLoader instance;

@FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
public static synchronized BootClassLoader getInstance() {
    if (instance == null) {
        instance = new BootClassLoader();
    }

    return instance;
}

public BootClassLoader() {
    super(null);
}

// ......................................省略
}

>总结:Java和Android基本所有ClassLoader都是间接或直接继承自ClassLoader类,除了Java的Bootstrap ClassLoader。

下面用代码测试下App启动默认到底有几个类加载器,和Java区别是什么?

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);

    ClassLoader classLoader1 = getClassLoader();// classLoader1 和 classLoader2 是同一个实例
    ClassLoader classLoader2 = TestActivity.class.getClassLoader();

    ClassLoader classLoader3 = ClassLoader.getSystemClassLoader();

    if (classLoader2 == classLoader3) {
        Log.i(TAG, "classLoader(1,2) == classLoader3 : true");
    } else {
        Log.i(TAG, "classLoader(1,2) == classLoader3 : false");
    }

    ClassLoader classLoader4 = Context.class.getClassLoader();
    ClassLoader classLoader5 = ListView.class.getClassLoader();
    ClassLoader classLoader6 = ClassLoader.class.getClassLoader();

    Log.i(TAG, classLoader1.toString());
    Log.i(TAG, classLoader2.toString());
    Log.i(TAG, classLoader3.toString());
    Log.i(TAG, classLoader4.toString());
    Log.i(TAG, classLoader5.toString());
    Log.i(TAG, classLoader6.toString());

    ClassLoader _classLoader = ClassLoader.getSystemClassLoader();
    while(_classLoader != null){
        Log.i(TAG, "current classLoader : " + _classLoader);
        _classLoader = _classLoader.getParent();
    }
}
打印结果

classLoader(1,2) == classLoader3 : false

dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.less.tplayer.baidu-1.apk"],nativeLibraryDirectories=[/data/app-lib/com.less.tplayer.baidu-1, /system/lib]]]
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.less.tplayer.baidu-1.apk"],nativeLibraryDirectories=[/data/app-lib/com.less.tplayer.baidu-1, /system/lib]]]

dalvik.system.PathClassLoader[DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib]]]

java.lang.BootClassLoader@4a67f6bc
java.lang.BootClassLoader@4a67f6bc
java.lang.BootClassLoader@4a67f6bc

current classLoader : dalvik.system.PathClassLoader[DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib]]]
current classLoader : java.lang.BootClassLoader@4a67f6bc


(1) Android启动时候则至少启动了3个类加载器,和Java有区别的是,Android的类加载器均是继承自ClassLoader,没有一个用C++实现。
(2)分别测试2个App,getClassLoader(PathClassLoader)的hashCode相等,获取的是同一个实例。
(2)其中BootClassLoader用于加载Android SDK级别的类,如Context,ListView等等,PathClassLoader用于加载我们自己APP里面的类,可以在应用中通过getClassLoader()获取到,看起来就像Java中

BootClassLoader -> BootStrapClassLoader,ExtClassLoader
PathClassLoader -> AppClassLoader

(3)通过打印日志推断Java中getSystemClassLoader和当前classpath下getClassLoader是一个实例,Android中getSystemClassLoader和TestActivity.class.getClassLoader获取到的是两个不同的PathClassLoader 实例。

**对比Java和Android的getSystemClassLoader获取方式**

Java获取getSystemClassLoader

private static ClassLoader sClassLoader;
private static boolean sclSet;

public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
// ...
return sClassLoader;
}

private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (sClassLoader != null){
throw new IllegalStateException("recursive invocation");
}
// ...
sun.misc.Launcher launcher = sun.misc.Launcher.getLauncher();
if (launcher != null){
sClassLoader = launcher.getClassLoader();
}
sclSet = true;
}
}


Android获取getSystemClassLoader

public abstract class ClassLoader {

static private class SystemClassLoader {
    public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}

public static ClassLoader getSystemClassLoader() {
    return SystemClassLoader.loader;
}

private static ClassLoader createSystemClassLoader() {
    String classPath = System.getProperty("java.class.path", ".");
    String librarySearchPath = System.getProperty("java.library.path", "");
    return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}   

}


(4)大家需要重点研究的类就是PathClassLoader和DexClassLoader即可,直接查阅源码来区分和Jvm实现的不同点。

**双亲委派模型**
下面这段代码几乎说明了一切

public abstract class ClassLoader {
// 组合方式的双亲委派模型
private final ClassLoader parent;

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    // 先让parent找,递归调用,有点像Android事件分发机制,但那个是使用继承方式,这个是使用组合方式 递归。
                    c = parent.loadClass(name, false);
                } else {
                    // 使用BootstrapClassLoader加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // nothing to do
            }

            if (c == null) {
                // 最后交给自己的时候,自己却无情抛出一个异常,告诉别人 => 太费体力,不想找了,除非你给我造娃!
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

}


>总结: 
>1. DexClassLoader可以加载apk,jar,及dex文件,但PathClassLoader只能加载已安装到系统中(即/data/app目录下)的apk文件。至于为什么,看下构造函数就知道了,DexClassLoader和PathClassLoader都是继承BaseDexClassLoader,两个类的区别仅仅是构造函数 DexClassLoader比PathClassLoader多了一个optimizedDirectory参数,其他完全一样,optimizedDirectory就是我们下面将要讲到动态加载dex时候File dexOutputDir = getDir("dex", 0),这个参数,懂了吧!
>2. Java和Android的加载器的基类 都是ClassLoader,除了Java中的Bootstrap ClassLoader是C++实现的。
>3. 远端的class只能由自定义ClassLoader加载,JVM三大加载器管辖不了,除非反射修改管辖区域(没这么做过,只是猜想)。
>4. 双亲委派模型是Java设计者推荐给我们的类加载器实现模式,但是不一定非要遵循,我们可以在入口处loadClass()做点手脚,吐槽下这里的命名,个人感觉findClass和loadClass的 **名字和逻辑** 有点起反了的味道,注意下就行了。

**Java和Android中ClassLoader的异同**
Java和Android虽然很多类都是同一个包名+类名,但是很多类都被修改过了,如ClassLoader,在Java中defineClass是一个非常重要的一个方法,用于负责将字节码文件(即class文件来源于文件或网络等) 读入字节数组byte[]b内,并把它转换为Class对象。 但是 Android中 此方法却被舍弃,仅仅抛出一个异常(因为Android不能直接加载class文件,而是加载dex文件)。

Java中的ClassLoader.defineClass

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}


Android中的ClassLoader.defineClass

@Deprecated
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
throw new UnsupportedOperationException("can't load this type of class file");
}


**Java的三种类加载器**

public class Main {

public static void main(String[] args) {
    Main main = new Main();
    System.out.println(main.getClass().getClassLoader());
    System.out.println(ClassLoader.getSystemClassLoader());

    System.out.println(main.getClass().getClassLoader().getParent());

    // bootstrap class loader 它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
    System.out.println(main.getClass().getClassLoader().getParent().getParent());

}

}

输出结果是:

sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$AppClassLoader@73d16e93

sun.misc.Launcher$ExtClassLoader@15db9742
null


查看ClassLoader.getParent方法的注释,可以看出,如果classLoader的parent是bootstrap class loader则返回null。

/**

  • Returns the parent class loader for delegation. Some implementations may
  • use <tt>null</tt> to represent the bootstrap class loader. This method
  • will return <tt>null</tt> in such implementations if this class loader's
  • parent is the bootstrap class loader.
    */

    @CallerSensitive
    public final ClassLoader getParent() {
    if (parent == null)
    return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
    checkClassLoaderPermission(this, Reflection.getCallerClass());
    }
    return parent;
    }

点击查看更多内容
5人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
30
获赞与收藏
274

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消