Android-Daily-Interview icon indicating copy to clipboard operation
Android-Daily-Interview copied to clipboard

2019-05-05:简述JVM中类的加载机制与加载过程?

Open Shanlovana opened this issue 5 years ago • 9 comments

Shanlovana avatar Apr 30 '19 23:04 Shanlovana

因为没说是什么JVM,这里假设hotspot 因为我也是小白,不保证正确 1.当要使用类时,JVM会判断此类是否已被加载到当前环境里,没有则会调用当前环境的ClassLoader.loadClass获得类 2.一般情况下,ClassLoader先尝试查找当前ClassLoader已加载的类,找不到则委托给父ClassLoader,通过双亲委派机制一层一层的往上传递,直到BootClassLoader(当然不一定要遵守此规则) 3.如果父ClassLoader无法加载此类,调用findClass方法加载,此时进入正题 4.findClass去找对应的.class文件(android里是去dex文件里找对应的类),找不到就抛ClassNotFoundException,然后加载到内存中 5.加载到内存中以后,首先校验格式(比如.class文件默认有8个字节的魔术字等),然后判断是否需要链接 6.如果需要链接,对类进行解析及初始化,比如初始化静态变量,初始化静态方法,执行静态代码块等,此阶段发生异常会引发LinkageError/ExceptionInInitializerError 7.如果走到这里一切都好,类加载的很成功,就返回Class,否则加载失败 参考资料:JVM规范-类的加载,链接和初始化

最后,五一快乐

canyie avatar May 01 '19 02:05 canyie

因为没说是什么JVM,这里假设hotspot 因为我也是小白,不保证正确 1.当要使用类时,JVM会判断此类是否已被加载到当前环境里,没有则会调用当前环境的ClassLoader.loadCLass获得类 2.一般情况下,ClassLoader先尝试查找当前ClassLoader已加载的类,找不到则委托给父ClassLoader,通过双亲委派机制一层一层的往上传递,直到BootClassLoader(当然不一定要遵守此规则) 3.如果父ClassLoader无法加载此类,调用findClass方法加载,此时进入正题 4.findClass去找对应的.class文件(android里是去dex文件里找对应的类),找不到就抛ClassNotFoundException,然后加载到内存中 5.加载到内存中以后,首先校验格式(比如.class文件默认有8个字节的魔术字等),然后判断是否需要链接 6.如果需要链接,对类进行解析及初始化,比如初始化静态变量,初始化静态方法,执行静态代码块等,此阶段发生异常会引发LinkageError/ExceptionInInitializerError 7.如果走到这里一切都好,类加载的很成功,就返回Class,否则抛异常(ClassNotFoundException/NoClassDefFoundError/ClassFormatError/LinkageError/ExceptionInInitializerError等等)

参考资料:JVM规范-类的加载,链接和初始化

最后,五一快乐

jvm跟DVM有所区别吗

ADrunkenLiBai avatar May 05 '19 01:05 ADrunkenLiBai

下面是我之前看深入理解JVM记得笔记。直接复制了。

1.1 概述

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

1.2 类加载的过程

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化
  • 使用
  • 卸载

其中验证,准备,解析三个部分称为连接。其中解析和初始化的顺序可能会不同(可能先解析后初始化,也可能先初始化后解析)。

1.2.1 关于初始化

5种情况会触发类的初始化

  • 遇到new,getstatic,putstatic,invokesstatic这四个字节码指令时,如果类没有被初始化
  • 使用java.lang.reflect包的方法对类进行反射时,如果类没有被初始化,则先触发其初始化
  • 当初始化一个类时,其父类没有被初始化,则需要父类先初始化
  • 虚拟机启动时,用户需要制定一个执行的主类,虚拟机会先初始化这个类
  • JDK 1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有被初始化

1.3 类加载的过程

  • 通过一个类的全限名来获取定义此类的二进制字节流
  • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

这里顺带再说下对象的加载过程。

1. 对象的创建

  • java虚拟机遇到一个new指令
  • 检查new引用代表的类是否被加载,解析和初始化
    • 加载过
    • 没有加载过,先执行相应类的加载过程
  • 虚拟机为对象分配内存
    • 对象所需要的内存大小在类加载过后便可以确定
    • 为对象分配空间的过程等同于把一块确定大小的内存从java堆中划分出来。
      • java堆绝对规整
        • 绝对规整解释:所有用过的内存放在一边,空闲的内存放在另一边中间放着一个指针作为分界点的指示器。
        • 分配过程:指针向空闲空间那边挪动一段与对象大小相等的距离。这种分配方式称为“指针碰撞”。
      • java堆不规整
        • 不规整解释:已使用的内存和未使用的内存相互交错,虚拟机维护一个列表,记录那些内存是可用的。
        • 分配过程:分配时从列表中找一块足够大的空间划分给对象实例。并更新列表上的记录。这种分配方式称为“空闲列表”。
      • java堆是否规整是由所采用的垃圾收集器是否带有压缩整理功能决定的。
  • 对象创建时并发问题
    • 描述:对象创建在虚拟机中是非常频繁的,因此在并发情况下是线程不安全的。可能指针正在为A对象分配内存,对象B又同时使用了原来的指针来分配内存。
    • 解决方案
      • 第一种方式:
        • 分配内存的动作进行同步处理
      • 第二种方式:
        • 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲。哪个线程要分配内存就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时才需要同步锁定。
        • TLAB解释:TLAB全称ThreadLocalAllocBuffer,是线程的一块私有内存,如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用,这个申请动作还是需要原子操作的。
  • 虚拟机将分配到的内存空间都初始化为零值(不包括对象头),这一操作保证了对象的实例字段在java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
  • 虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息。这些信息存在对象头(Object Header)之中。
  • 执行对象初始化,即把对象按照程序员的意愿进行初始化。

2.对象的访问定位

  • 前面的学习我们知道,java虚拟机栈保存的是对象的句柄或者对象地址。所以访问对象需要通过栈上的局部变量表(reference)来操作堆上的具体对象。因为reference只是一个只想对象的引用,并没有定义这个引用应该通过什么方式定位,访问堆中的对象的具体位置,所以对象访问的方法取决于虚拟机的实现方式。目前主流的访问方式有以下两种
    • 句柄式:
      • java堆中有一块内存作为句柄池,reference存储的就是对象的句柄地址。句柄包含了对象实例数据与类型数据各自具体的地址信息。
      • 优点:对象被移动时(垃圾回收时,整理内存时,很有可能被移动)只会改变句柄中的实例数据指针,不用改变reference
    • 直接指针访问:
      • reference存储的直接就是对象地址
      • 优点:快,省略了一次句柄到具体位置的访问时间。因为对象比较多,所以这个时间很可观

MoJieBlog avatar May 05 '19 01:05 MoJieBlog

嗯,上面大佬们都说的差不多了。 先带上一张图 image Java中的类加载过程为,加载-连接-初始化 那类加载是什么意思呢? 类加载指的是class文件读入内存,并为其创建一个Class对象,也就是程序使用任何一个类的时候,都会为其创建一个java.lang.Class对象。

连接是什么意思呢?

连接阶段主要负责将类的二进制数据合并到jre中。 --但是为什么要将数据合并到jre呢? jre指的是java的运行时环境,也就是java虚拟机,因为各大平台不一样,也就是运行环境,所以java虚拟机相当于屏蔽了这些条件,这样java程序就可以实现编译一次,处处运行。也就是我们刚开始学java时的第一句话。 初始化呢? 简单来说,虚拟机负责对类进行初始化,其实也就是对变量进行初始化,初始化的时候,遵循以下3个条件:

  1. 假如这个类没有被加载和连接,则程序先加载并连接该类。
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
  3. 假如该类中有初始化语句,则系统一次执行这些初始化语句。 不过需要注意一点,当调用被final修饰的类变量时(基本类型),并不会引起类的初始化。

类加载机制的话,主要有以下3种: 全盘负责: 双亲委托: 缓存机制:

双亲委托的话,用一个简单栗子来解释。 比如说你自己定义了一个java.lang.String类,但是你永远调用不了它,为什么呢? 因为String属于核心类,在类加载的过程中,由引导类加载器去加载,所以永远用不了自定义的这个类。 双亲委托机制也就是,当一个类加载器收到类加载请求后,他不会先去加载,而是让它的父加载器先去尝试,层层向上,只有父类无法加载,这时才会尝试让子类去加载,这也就是自己写的为啥无法加载。 那缓存机制是什么呢? 缓存机制保证我们加载过的类都会被缓存,这样当程序使用某个类时,首先从缓存区中去找,当找到了直接返回Class对象,避免了重复加载。这也就是为什么修改了代码,需要重新编译运行才会生效的原因了。 如果有什么问题,也欢迎各位大佬们补充:

Petterpx avatar May 05 '19 02:05 Petterpx

java中的类加载机制

Java语言系统自带有三个类加载器: 

  • Bootstrap ClassLoader 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、 resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。
  • Extention ClassLoader 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还 可以加载-D java.ext.dirs选项指定的目录。 
  • Appclass Loader也称为SystemAppClass 加载当前应用的classpath的所有类。

我们需要知道这三个加载器的加载顺序

  1. Bootstrap CLassloder

  2. Extention ClassLoader

  3. AppClassLoader

然后需要知道这个加载顺序具体执行策略 双亲委托机制

一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。

具体参见 我的博客

yangfanggang avatar May 05 '19 09:05 yangfanggang

@ADrunkenLiBai 回答你那个问题: 在JVM规范里,任何能够加载与执行Java 字节码(即class文件)的虚拟机都能叫JVM 但是Dalvik VM略有不同,其并没有完全遵守JVM规范,比如它不能执行class,只能加载dex文件格式(个人推测是害怕侵权?)

canyie avatar May 06 '19 03:05 canyie

类加载的过程分为三个部分: 1,加载 类加载是将.class 文件中的二进制数据读入内存中,将其放在运行时数据区的方法区内,然后在堆内存中创建一个对象,又来封装类在方法区内的数据接口。类加载的最终产品是为与堆中的Class 对象。 2,类的连接 类加载完成后,就会进入连接阶段,连接阶段负责把类的二进制数据合并到jre中,可分为三个阶段 1,验证。 2,准备。 3,解析 3,类的初始化 在类的初始化阶段,虚拟机负责对类进行初始化,主要是对类变量进行初始化。JVM初始化一个类包含如下几个步骤 1. 假如这个类没有被加载和连接,则程序加载并连接该类。 2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类。 3. 假如类中有初始化语句,则系统一次执行这些初始化语句。

   当执行第二步时,系统对直接的父类的初始化步骤也遵循此步骤。如果该类又有直接父类,则系统会再次重复这三个步骤来初始化该类。所以最先初始化的总是java.long.Object类。

LvKang-insist avatar May 09 '19 00:05 LvKang-insist

    加载机制:
        1,缓存机制,先从缓存区搜索
        2,全盘负责,当一个类加载器负责加载某个class,则Class所依赖和引用的其他的Class也由它负责,除非显示使用另一个。
        3,双亲委派,先让父加载器试图加载,只有当父加载器无法加载时,才尝试从自己的路径加载

    加载过程:[2,3,4统一称为连接]
        1,加载:获取到Class文件、转化为方法区的数据结构,并创建一个引用对象
        2,验证:验证Class文件的合法性
        3,准备:分配类变量内存,并设置"初始化值";(static 为0,static final 为ContentValue值)
        4,解析:将符号引用转化为直接引用
        5,初始化:将直接引用的类变量执行clinit方法,进行赋值

yline avatar Jan 06 '20 08:01 yline

通俗易懂的文章 图解JVM类加载机制与类加载过程

senlinxuefeng avatar Jan 11 '22 07:01 senlinxuefeng