带你走进JVM之类加载机制

2 篇文章 0 订阅

什么是JVM?

1.JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
2.Java虚拟机包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。 JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行 ,是Java跨平台的原因。

下面这个关系图,说明了JDK JRE JVM之间的关系:
在这里插入图片描述

关于JVM的组成?

类装载子系统(ClassLoader),运行时数据区,执行引擎,内存回收,类文件结构。

那么什么是类加载机制呢?

虚拟机把描述类的数据从Class文件加载到内存,并对这些数据进行校验,转换 解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

类加载的七个阶段?

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括:
在这里插入图片描述
一、加载
在加载阶段,就是类class文件读入内存,并为之创建一个java.lang.class对象。这个过程虚拟机主要完成三件事:
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。
3.在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区域数据的访问入。

二、验证
验证阶段是连接的第一步,作用是保证Class文件的字节流包含的信息符合JVM规范。如果验证失败,就会抛出一个java.lang.VerifyError异常或其子类异常。验证过程分为四个阶段:
1.文件格式的验证,验证当前字节流是否能被JVM识别。
2.元数据的验证,验证它的父类,它的继承,是否是抽象类等。
3.字节码验证,验证逻辑是否合理。
4.符号引用的验证 验证是否能通过生成的Class对象找到对应的数据。

三、准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
这个阶段需要特别注意以下几点:
1.此阶段进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2.这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

eg:假设一个类变量的定义为:pirvate static int size = 6,
那么在这个阶段,size的值为0,而不是12。因为这时候尚未开始执行任何Java方法,而把size赋值为6的putstatic指令是在程序编译后,存放于类构造器()方法之中的,所以把size赋值为6的动作将在初始化阶段才会执行。( final修饰的类变量将会赋值成真实的值)

下图为Java中所有基本数据类型以及reference类型的默认零值:
在这里插入图片描述
另外还需要注意的是:
1.对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而- 对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
2.对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过,
而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值。
总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
3.对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
4.如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

四、解析
解析过程是将常量池内的符号引用替换成直接引用。主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。

五、初始化
在准备阶段,类变量已经经过一次初始化了。在初始化阶段,则是根据程序员通过程序制定的计划去初始化类的变量和其他资源。这些资源有static{}块,构造函数,父类的初始化等。
(到了初始化阶段,才真正开始执行类中定义的Java代码)。

六、使用
使用阶段就是根据程序定义的行为执行。
新线程—程序计数器----jvm栈执行(对象引用)-----堆内存(直接引用)----方法区。

七、卸载
GC垃圾回收。
执行了System.exit()方法。
程序正常执行结束。
程序在执行过程中遇到了异常或错误而异常终止。
由于操作系统出现错误而导致Java虚拟机进程终止。

关于类加载器?

一、站在Java虚拟机的角度来讲,只存在两种不同的类加载器:
1.启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分。
2.所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

二、站在Java开发人员的角度来看,类加载器可以大致划分为以下四类:
1.启动类加载器(Bootstrap ClassLoader)
负责加载JAVA_HOME\lib目录中的或通过-Xbootclasspath参数指定路径中的且被虚拟机认可(按文件名识别,如tr.jar)的类。
2.扩展类加载器(Extension ClassLoader)
负责加载JAVA_HOME\ext目录中的或通过java.ext.dirs系统变量指定路径中的类库。
3.应用程序类加载器(Application ClassLoader)
负责加载用户路径(classpath)上的类库。JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
4.自定义加载器 (User ClassLoader)

这几种类加载器的层次关系:
在这里插入图片描述

双亲委派模型?

类加载器之间的这种层次关系叫做双亲委派模型。 双亲委派模型要求除了顶层的启动类加载器(Bootstrap ClassLoader)外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不是以继承关系实现的,而是用组合实现的。

双亲委派模型的工作过程?

概括:向上检查,从下加载。
如果一个类接受到类加载请求,他自己不会去加载这个请求,而是将这个类加载请求委派给父类加载器,这样一层一层传送,直到到达启动类加载器(Bootstrap ClassLoader)。
只有当父类加载器无法加载这个请求时,子加载器才会尝试自己去加载。
在这里插入图片描述

双亲委派模型的代码实现?

双亲委派模型的代码实现集中在java.lang.ClassLoader的loadClass()方法当中。

首先检查类是否被加载,没有则调用父类加载器的loadClass()方法;
若父类加载器为空,则默认使用启动类加载器作为父加载器;
若父类加载失败,抛出ClassNotFoundException 异常后,再调用自己的findClass() 方法。

loadClass源代码如下:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    //1 首先检查类是否被加载
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
             //2 没有则调用父类加载器的loadClass()方法;
                c = parent.loadClass(name, false);
            } else {
            //3 若父类加载器为空,则默认使用启动类加载器作为父加载器;
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
           //4 若父类加载失败,抛出ClassNotFoundException 异常后,这个方法就是加载的核心代码
            c = findClass(name);
        }
    }
    if (resolve) {
        //5 再调用自己的findClass() 方法。
        resolveClass(c);
    }
    return c;
}

自定义类加载器?

    class NetworkClassLoader extends ClassLoader {
           String host;
           int port;
  
           public Class findClass(String name) {
               byte[] b = loadClassData(name);
               return defineClass(name, b, 0, b.length);
           }
  
           private byte[] loadClassData(String name) {
               // load the class data from the connection
               &nbsp;.&nbsp;.&nbsp;.
           }
       }

如何破环双亲委派?

双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题。即越基础的类由越上层的加载器进行加载。
若加载的基础类中需要回调用户代码,而这时顶层的类加载器无法识别这些用户代码,怎么办呢?这时就需要破坏双亲委派模型了。
java默认的线程上下文类加载器是系统类加载器(AppClassLoader)。

// Now create the class loader to use to launch the application    
   try {    
       loader = AppClassLoader.getAppClassLoader(extcl);    
   } catch (IOException e) {    
       throw new InternalError(    
   "Could not create application class loader" );    
   }    
        
   // Also set the context class loader for the primordial thread.    
  Thread.currentThread().setContextClassLoader(loader);    

使用线程上下文类加载器,可以在执行线程中,抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。
典型的例子有,通过线程上下文来加载第三方库jndi实现,而不依赖于双亲委派。
大部分java app服务器(jboss, tomcat…)也是采用contextClassLoader来处理web服务。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菜鸟驿站ㅤ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值