jvm类加载以及初始化 | 青训营笔记

101 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第2篇笔记。

类加载时机

加载,验证,准备,初始化,卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这样的加载过程进行,但是解析阶段不一定,它在某种情况之下可以在初始化之后开始,关于对什么时候开始加载,java虚拟机规范没有明确的强制约束,这一点可以交给java虚拟机的具体实现来自由把握,但是对于初始化阶段,java虚拟机进行了严格规定,只有在这六种情况下才能进行

初始化阶段六种情况

  • 1.遇到new、getstatic、putstatic或invokestatic这四条字节码指令的时候,如果类型没有进行初始化,则需要触发其初始化阶段,能够生成这四条指令的java代码场景有:

    • 使用new关键字实例化对象的时候
    • 读取或设置一个类型的静态字段的时候(被final修饰、已在编译期间把结果放在常量池的静态字段除外)
    • 使用一个类型的静态方法的时候
  • 2.使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行初始化,则需要先触发初始化

  • 3.当初始化类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化 (接口与这个有区别,一个接口在初始化时候,并不要求其父接口全部都完成初始化,只有在真正使用到父接口的时候才会初始化)

  • 4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类

  • 5.当使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getstatic、REF_putstatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类还没有进行过初始化,则需要先触发其初始化

  • 6.当一个类中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,name这个接口要在其之前被初始化

2对象创建探秘

2.1对象的创建

  • 1.类加载的检查:

    • 当java虚拟机遇到了一条字节码new指令的时候首先会去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且去检查这个符号引用代表的类是否被加载解析和初始化过,如果没有则进行类加载过程
  • 2.分配内存

    • 类加载之后需要为新生对象分配内存,对象所需要的内存大小在类加载完成之后便可以完全的确定,为对象分配空间的任务实际上等同于把一块确定大小的内存块从java堆中划分出来 对象分配内存的两种方式如下:

    • 指针碰撞:

      • 原理: 假设java堆中的内存是绝对规整的所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是把那个指针向空闲空间方向挪动与对象大小相等的距离
      • 适用场景: 堆内存规整(即没有内存碎片)的情况下
      • 使用该分配方式的GC收集器: Serial,ParNew
    • 空闲列表:

      • 原理: java堆中的内存并不是规整的,已经被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容
      • 使用场景:堆内存不规整情况下
      • 使用该分配的GC收集器:CMS
  • 内存分配并发问题

    对象创建在虚拟机中是非常频繁的行为,计时仅仅修改一个指针所指向的位置,在并发的情况下也不是线程安全的,可能出现在正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况

    • 解决以上问题有两个可选方案

    • CAS+失败重试:

      • 对分配内存空间的动作进行同步处理--实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性
    • TLAB(本地线程分配缓冲 Thread Local Allocation Buffer):

      • 为每一个线程预先在Eden区分配一块内存,JVM在给线程中的对象分配内存时候,首先在TLAB中分配,当对象大于TLAB中剩余的内存或者TLAB中的内存使用结束的时候再采用上述CAS+失败重试的方式进行内存分配,虚拟机是否使用TLAB可以通过-XX:+/-UseTLAB参数来设定
  • 3.初始化零值

    上述的内存分配结束之后,虚拟机必须将分配到的内存空间都初始化为零值(但不包括对象头),如果使用了TLAB的话,这一项工作可以提前至TLAB分配的时候就顺便进行,这个操作保证了对象的实例字段在java代码中可以不赋初始值就可以直接使用,是程序能访问到这些字段的数据类型的零值---这里对象的实例字段赋值,类赋值是在编译中实现的(准备阶段)

  • 4.设置对象头

    • 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
  • 5.执行init方法

    • 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来