JVM虚拟机——java类加载机制

萨瓦迪卡2天前jvm12

那么java类是如何被加载到jvm虚拟机的呢?

一、JAVA8的类加载机制

三句话总结JDK8的类加载机制:

  1. 类缓存:每个类加载器对加载过的类都有一个缓存

  2. 双亲委派:向上委托查找,向下委托加载。

    (向上委托查找是从缓存中查,为了保证系统内置的类不能够被子类覆盖,比如说Java.long.object,这是所有子类分父类。 为了保证父类不被修改)

      3.沙箱保护机制:不允许应用程序加载JDK内部的系统类。(不能写java.开头的类)


总结虽然简单,深入去理解,还是很复杂的。


至于JDK具体如何执行的,不同JDK版本的实现方式是不同的。以下以大家最为熟悉的JDK8进行分析。

2、双亲委派机制


JDK8中的两个类加载体系:

4c0d2399-ab77-4f6f-9707-b23faeb8dbbc.png

左侧是JDK中实现的类加载器,通过parent属性形成父子关系。应用中自定义的类加载器的parent都是AppClassLoader
右侧是JDK中的类加载器实现类。通过类继承的机制形成体系。未来我们就可以通过继承相关的类实现自定义类加载器。
简而言之,左侧是对象,右侧是类。

JDK8中的类加载器都继承于一个统一的抽象类ClassLoader,类加载的核心也在这个父类中。其中,加载类的核心方法如下:

  //类加载器的核心方法 public Class<?> loadClass(String name) throws ClassNotFoundException {        return loadClass(name, false);    }    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) {                long t0 = System.nanoTime();                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.                    long t1 = System.nanoTime();                    c = findClass(name);                    // this is the defining class loader; record the stats                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);                    PerfCounter.getFindClasses().increment();                }            }            if (resolve) {                resolveClass(c);            }            return c;        }    }

这个方法里,就是最为核心的双亲委派机制。虽然从JDK8往后,类加载机制有了很多的调整,但是这段双亲委派的经典代码却没有发生变化。

这里有趣的是,这个loadClass方法是用protected声明的。这意味着,是可以被子类覆盖的。所以,双亲委派机制也是可以被打破的。

当一个类加载器要加载一个类时,整体的过程就是通过双亲委派机制向上委托查找,如果没有查找到,就向下委托加载。整个过程整理如下图:

双亲委派机制过程:将某个class类加载到缓存,这个类加载器会先在自己的缓存中找,看有没有加载过,加载过就返回,如果没有加载就到父加载器的缓存中找,如果有返回,没有就到顶层的加载器BootStapClassLoade中找,去缓存里找如果有返回,如果没有就到对应的目录文件中有没有class文件,有加载,没有到下面的子加载器再找。直到上面都没有,就交由自己加载。

3eba8465-3e8e-48b2-9eb3-81ac3083c2c6.png


另外,还一个有趣的地方,这个loadClass方法中有一个resolve参数,但是,设置这个参数,却只有一个public方法中写死了。

也就是说,在调用类加载器时,程序员是没有办法给这个resolve方法主动传入一个false的。那这个resolve参数设置不是多此一举吗?

其实resolveClass(),是一个native方法。

而其实现的过程称为linking-链接。链接过程的实现功能如下图:

  image.png

其中关于半初始化状态(准备阶段)就是JDK在处理一个类的static静态属性时,会先给这个属性分配一个默认值,作用是占住内存。然后等连接过程完成后,在后面的初始化阶段,再将静态属性从默认值修改为指定的初始值

这里注意,static静态的属性,是属于类的,他是在类初始化过程中维护的。而普通的属性是属于对象的,他是在创建对象的过程中维护的。这两个不要搞混了。
对应到class文件当中,一个是方法,一个是方法。

例如参照一下下面这个案例:

class Apple{
    static Apple apple = new Apple(10);
    static double price = 20.00;
    double totalpay;

    public Apple (double discount) {
        System.out.println("===="+price);
        totalpay = price - discount;
    }
}
public class PriceTest01 {
    public static void main(String[] args) {
        System.out.println(Apple.apple.totalpay);
    }
}
程序打印出的结果是-10 ,而不是10。 这感觉有点反直觉,为什么呢?
因为开始静态apple会默认null,price会默认0,当apple在构造方法的时候,apple还在半初始化的状态,0。

其中Apple.apple访问了类的静态变量,会触发类的初始化,即加载-》链接-》初始化

而当main方法执行构造函数时,price还没有初始化完成,处于链接阶段的准备阶段,其值为默认值0。这时构造函数的price就是0,所以最终打印出来的结果是-10, 而不是 10 。


如何让结果打印出正常的10呢?
1将 代码块static double price = 20.00;  提前到第一行。代码会从上而下执行。

2在代码前加final ,也就是 final static double price = 20.00。表示这个值永远不会变,所以开始直接就是初始值。


后面解析的过程有两个核心的概念:符号引用和直接引用。。

如果A类中有一个静态属性,引用了另一个B类。那么在对类进行初始化的过程中,因为A和B这两个类都没有初始化,JVM并不知道A和B这两个类的具体地址。所以这时,在A类中,只能创建一个不知道具体地址的引用,指向B类。这个引用就称为符号引用。而当A类和B类都完成初始化后,JVM自然就需要将这个符号引用转而指向B类具体的内存地址,这个引用就称为直接引用


思考问题:为什么在ClassLoader的这个loadClass方法中,reslove参数只能传个false,而不让传true?
因为在创建对象的过程中是要申请内存的,所有的java内存就是在进程启动的时候,把所有的类创建,而不能在运行过程中去重新申请内存,这样的话,内存管理就不可控。所以不让我们参与这个连接的过程。不过也有办法去参与这个过程,就是用Class.forName,意味着它会走resolveClass方法 走连接,会临时申请内存。临时申请是申请不下来的。所以还是第一种loadClass方法更安全。

2e480923-12b9-47e0-a5d9-eef69119c566.png


3、沙箱保护机制

双亲委派机制有一个最大的作用就是要保护JDK内部的核心类不会被应用覆盖。而为了保护JDK内部的核心类,JAVA在双亲委派的基础上,还加了一层保险。就是ClassLoader中的下面这个方法。
private ProtectionDomain preDefineClass(String name,  ProtectionDomain pd)
    {
        if (!checkName(name))
            throw new NoClassDefFoundError("IllegalName: " + name);
        // 不允许加载核心类
        if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                ("Prohibited package name: " +
                 name.substring(0, name.lastIndexOf('.')));
        }
        if (pd == null) {
            pd = defaultDomain;
        }
        if (name != null) checkCerts(name, pd.getCodeSource());
        return pd;
    }
这个方法会用在JAVA在内部定义一个类之前。这种简单粗暴的处理方式,当然是有很多时代的因素。也因此在JDK中,你可以看到很多javax开头的包。这个奇怪的包名也是跟这个沙箱保护机制有关系的。
反编译工具:jd-jui

如何对class文件加密,防止反编译?


可以把class文件的二进制改一改,比如在class文件的基础上加了个1。生成一个新的myclass文件


be733f7c-5090-40aa-b10a-90b7a9f937e6.png

新的myclass文件如下:

efa3b93e2136df6dbc16cf49950eaa59.png

要让这个MyClass文件也可以执行使用,那就写一个自己的MyClassLoader类,重写findClass方法。把01忽略。

aaaed4a8-29c0-4bc1-aca1-fee5b44b457d.png


4、类和对象有什么关系

通过类加载模块,我们写的class文件就可以加载到JVM当中。但是类加载模块针对的都是类,而我们写的java程序都是基于对象来执行。类只是创建对象的模板。那么类和对象倒是什么关系呢?
首先:类 Class 在 JVM 中的作用其实就是一个创建对象的模板。也就是说他的作用更多的体现在创建对象的过程当中。而在程序具体执行的过程中,主要是围绕对象在进行,这时候类的作用就不大了。因此,在 JVM 中,并不直接保存在最宝贵最核心的堆内存当中,而是挪到了堆内存以外的一部分内存中(非堆区)。这部分内存,在 JDK8 以前被成为永久带PermSpace,而 JDK8 之后被改为了元空间 MetaSpace
堆空间可以理解为JVM的客厅,所有重要的事情都在客厅处理。元空间可以理解为JVM的库房,东西扔进去基本上就很少管了。
这个元空间逻辑上可以认为是堆空间的一部分,但是他跟堆空间有不同的配置参数,不同的管理方式。因此也可以看成是单独的一块内存。这一块内存就相当于家里的工具间或者地下室,都是放一些用得比较少的东西。最主要就是类的一些相关信息,比如类的元数据、版本信息、注解信息、依赖关系等等。
元空间可以通过-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize参数设置大小。但是大部分情况下,你是不需要管理元空间大小的,JVM 会动态进行分配。
另外,这个元空间也是会进行 GC 垃圾回收的。如果一个类不再使用了,JVM 就会将这个类信息从元空间中删除。但是,显然,对类的回收效率是很低的。大部分情况下,类是不会被回收的。所以堆元空间的垃圾回收基本上是很少有效果的。大部分情况下,我们是不需要管元空间的。除非你的JVM 内存确实非常紧张,这时可以设定 -XX:MaxMetaspaceSize参数,严格控制元空间大小。
然后:在我们创建的每一个对象中,JVM也会保存对应的类信息。
在堆中,每一个对象的头部,还会保存这个对象的类指针(classpoint),指向元空间中的类。这样我们就可以通过一个对象的getClass方法获取到对象所属的类了。这个类指针,我们也是可以通过一个小工具观察到的。
例如,下面这个 Maven依赖就可以帮我们分析一个对象在堆中保存的信息。
<dependency>
   <groupId>org.openjdk.jol</groupId>
   <artifactId>jol-core</artifactId>
   <version>0.17</version>
</dependency>

然后可以用以下方法简单查看一下对象的内存信息。

public class JOLDemo {
    private String id;
    private String name;
    public static void main(String[] args) {
        JOLDemo o = new JOLDemo();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());

        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}
看到的结果大概是这样:

4a653b04-7117-443c-a070-68b8ac36af64.png

这里ClassPoint 实际上就是一个指向元空间对应类的一个指针。当然,具体结果是被压缩过的。
另外Markdown标志位就是对象的一些状态信息。包括对象的 HashCode,锁状态,GC分代年龄等等。


本文原创,转载必追究版权。

分享给朋友:

相关文章

多说评论框怎么用更好

 1.隐藏屏蔽掉多说评论框的版权链接代码?简单css实现:多说隐藏版权链接,在后台自定义css添加:#ds-thread #ds-reset .ds-powered-by { display...

freeMarker Jfinal 获取session里的值

问题:freeMaker session取值的常用格式都试过 session["xxx"],session.xxx 直接xxx 都取不出来?????解决:JFinal与Struts...

开机密码忘记怎么办

1、重新启动计算机,在启动画面出现后马上按下F8键(不同类型型号电脑启动键不一样,参考附加),选择“带命令行的安全模式”。2、运行过程结束时,系统列出了系统超级用户“administrator”和本地...

目标管理法——目标分解法

让自己的人生更幸福更有意义关键是:要将梦想转化为具体的目标,然后合理的分解,达到量化,指标化!现将学习到的两种非常有效的目标分解法分享给所有梦想、有激情的朋友:祝愿大家都能梦想成真! 一、俄...

竟然可以这样打扮!女人呆了!男人痴了!

来个轻松点的哇,惊呆了,肯定贵不了,立刻去瞅瞅...

谈话让别人舒服的程度,决定你成功的高度

职场上,有这样两种截然相反的人:有人生怕别人舒服,尽量让别人不舒服,而只要自己舒服就行;还有一类人生怕别人不舒服,尽量让别人舒服,哪怕委屈自己。猎头公司猎聘的老总有几十万年薪的,也有几百万的,甚至有过...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。