java class loading


Java 的加载机制

这里我先要说下类和对象。

java 中,可以将一组特征放到一个 class 里面,我们称之为类,是一个抽象概念。

而对象则是这个类的具象。

比如清华大学之于大学。

那我们说 java 的加载机制,事实上说的是类的加载机制。这个加载,只会加载一次

如何加载,什么时候加载,请看后续。


通常而言, java 的加载机制分为定义上的三步:

  • 加载
  • 连接
  • 初始化

事实上的五步:

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

再加上 使用卸载, 就是完整的类的生命周期了。

加载

通过 ClassLoader 将 .class 字节码文件,加载到内存中。

这里加载的类,不仅可以是本地的 class 文件,还可以是 zip 包, jar 包, 甚至网络地址上的文件。所以我们可以实现自己的 ClassLoader, 来处理不同 class 的加载。

比如,有时因为安全问题, 我们对一些 class 文件加密(毕竟反编译太简单) ,然后通过自定义的 ClassLoader 先解密,然后再加载到内存中。

在 java8 及之前, java 的加载类有

  • BootstrapClassLoader : 最顶层的加载器,负责加载 lib jar
  • ExtensionClassLoader : 负责加载 ext jar
  • AppClassLoader : 加载当前 classpath 下的 jar

从 java9 开始,java 开始支持 modlue 的概念, 它的类加载器,也变成了:

  • BootstrapClassLoader
  • PlatformClassLoader
  • AppClassLoader

双亲委派模型

java 加载一个类时, 会首先把该请求, 委派给该类的加载器的父加载器,直到顶层加载器。然后自顶而下,直到有加载器能处理这个请求为止。如果该类已经加载过,则直接返回。

所以这里的双亲是个什么鬼? 我也不知道了…


校验

  • 文件格式校验: 字节码格式校验,是否符合 class 文件规范
  • 元数据校验 : 我理解为是对 java 语义的校验
  • 字节码校验: 我理解为安全校验,即判断这个 class 文件是否会对 jvm 做出伤害行为

准备

对类变量(static) 分配内存并作零值处理。

这里的零值,指类变量的类型的默认值, 如 int 默认值为 0 , 对象默认值为 null 等

这里并不会去执行代码!

比如 private static int a = 2 , 此时并不会将 2 赋值给 a 。

但如果该类变量有 final 修饰,则会立即赋值。

TODO: 这里的内存分配,是分配到 java 的方法区,那么 jvm 的内存结构是怎样的?


解析

解析是将符号引用替换为直接引用。

  • 符号引用: 通过一组符号来描述目标
  • 直接引用: 可以直接执行目标的指针、相对偏移量、或者是一个能直接定位到目标的句柄

TODO


初始化

初始化阶段, 我们的 java 代码,才会被真正的执行。我们假设该类为 A,则:

  1. 如果 A 的父类未初始化,则初始化父类
  2. 如果类中包含类变量赋值操作或者静态代码块,jvm 会首先生成一个叫做 类构造器方法 的东西。它其实就是把我们类里面所有的类变量和静态代码块放到了一起, 然后先跑一波。这意味着, 会先执行父类和子类的静态代码块。

什么时候会进行初始化?

  • new 关键字
  • 反射
  • 遇到 getstatic 、 putstatic 、 invokestatic 字节码。 这里的静态变量,需要是直接定义该变量的类。如果通过子类调用父类的静态变量,则只有父类会初始化,子类不会。
    • 读取类变量(未被 final 修饰,下同)
    • 设置类变量
    • 执行静态方法
  • 启动时,包含 main 方法的那个类会先初始化

注意: A[] a = new A[1]; 这种数组形式,并不会触发 A 的初始化

JVM 保证: java 在进行类初始化的时候,会上锁,从而保证线程安全。


Singleton

java 中的 singleton ,有多种实现方式。我们现在介绍几种:

class Hungry {
    private static Hungry instance = new Hungry();
    private Hungry() {}
    public static Hungry getInstance() {
        return instance;
    }
}

按照上面所说的加载机制, 当我们调用 getInstance 静态方法时, 如果该类还没有初始化,则会首先初始化。

类中有静态变量,所以会首先执行类构造器方法, 进而执行 new Hungry() ,于是 instance 被实例化。

整个过程, 由于 java 类加载机制(加载一次 + 线程锁),从而保证了单例的实现。


class DCLFull {
    private volatile static DCLFull instance;
    private DCLFull() {}
    public static DCLFull getInstance() {
        if (instance == null) {
            synchronized (DCLFull.class) {
                if (instance == null) {
                    instance = new DCLFull();
                }
            }
        }
        return instance;
    }
}

double-check 实现的饱汉模式(即传说中的 DCK , Double-Check-Lock)。

第一个 if 用来保证无竞争时,不用锁。

第二个 if 用来保证,当 A 、 B 同时进入到第一个 if 后, A 等待, B 获取锁且完成实例化释放锁后, A 再次进入, 不会再次创建一个新对象。

instance 定义中的 volatile 修饰符: 该修饰符的作用有多线程间数据的可见性, 和禁止指令重排。volatile 在这里用来防止指令重排。
java 中, new 操作,可以分为三步:

  • memory=allocate(); //1:分配内存空间
  • ctorInstance(); //2:初始化对象
  • singleton=memory; //3:设置singleton指向刚排序的内存空间

jvm 对于不会变更结果的指令,会重排优化, 所以这里的顺序,可能是 1 -> 2 -> 3 , 也可能是 1 -> 3 -> 2 。

对于后者, 假设线程 A 运行到最后一步之前, cpu 被线程 B 抢占,则 B 会获取到一个非 null 的但未完成初始化的对象,这就可能会发生异常。

这里通过 volatile 来避免这种情况 。


class StaticFull {
    private StaticFull() {}
    private static class InnerStaticFull {
        private static StaticFull instance = new StaticFull();
    }
    public static StaticFull getInstance() {
        return InnerStaticFull.instance;
    }
}

通过 java 类加载机制保证的饱汉模式的单例实现。

加载 StaticFull 类时, 静态内部类并不会被加载,只有在调用 getInstance 时, 静态内部类才会被加载,instance 才会被实例化。


enum EnumSingleton {
    INSTANCE,
    ;

    public void doSomething() {
        System.out.println("enum");
    }
}

通过 enum 来实现。可以保证单例,同时能保证反射、序列化/反序列化安全


Java 内存模型

模型

Java 内存模型

Heap

Java 中几乎所有对象都在堆中分配。该内存空间会被所有线程共享

Java世界中“几乎”所有的对象都在堆中分配,但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从jdk 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

– 摘自 java guide [1]

其中 java heap 被分为 Eden , Servivor0 (S0), Servivor (S1) 以及 Old Gen 。其中 Eden + S0 + S1 又被称为 新生代, Old Gen 被称为 老年代

当我们创建对象时, 正常情况下会首先进入 Eden 区,但是大对象会直接进入 Old Gen 。

对象在 Eden 出生后, 经历一次 Minor GC 仍然存活的话,就会被移到 S0 或者 S1, 同时年龄设为 1。当年龄达到一定值,就会被移动到 Old Gen 。

“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。

– 摘自 java guide [1]


VM 栈

线程私有, 由一个个栈帧组成, 每个栈帧都包含:

  • 局部变量表:八大原始类型, 对象引用, returnAddress
  • 操作数栈
  • 动态链接
  • 方法出口信息

本地方法栈

与 VM 栈类似, 用来服务于 native 方法。而 java 中的 native 方法多为 c 语言编写,因此这个栈用被成为 C 栈


程序计数器

用来记录当前线程所执行的字节码的位置。

  • 通过更改程序计数器, 读取下一条指令
  • 当线程被抢占 CPU ,恢复后, 通过程序计数器,获取上次执行的位置,并继续执行下一条指令

元空间

图中“元数据”为笔误,应该是元空间。

元空间使用的内存,与 Heap 内存不再连续,直接使用本地内存。方法区被放入到元空间。

可通过 MaxMetaspaceSize 设置大小


方法区

用来存储 java 类信息, 运行时常量池等

GC

img

Minor GC / Major GC / Full GC 参见 这里

暂时搞不懂这些了。。。

下面的内容来自这里

当 Eden 区的空间耗尽了怎么办?这个时候 Java虚拟机便会触发一次 Minor GC来收集新生代的垃圾,存活下来的对象,则会被送到 Survivor区。

当发生 Minor GC时,Eden 区和 from 指向的 Survivor 区中的存活对象会被复制(此处采用标记 - 复制算法)到 to 指向的 Survivor区中,然后交换 from 和 to指针,以保证下一次 Minor GC时,to 指向的 Survivor区还是空的

Reference


文章作者: peifeng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 peifeng !
 上一篇
用微信公众号给你的 Blog 鉴权 用微信公众号给你的 Blog 鉴权
通过 Nginx 代理的静态 blog , 虽然只是静态 blog , 可也希望有些页面能做权限管理。 Nginx 提供了 Basic Auth 功能。不过我更喜欢通过微信来做简单的验证功能。下面就是我个人的实现过程。
2020-05-02
下一篇 
Sorts Sorts
Sorts本文主要介绍几种常见的排序算法,以及 Bloom 过滤器。 排序在正式介绍排序之前,先说下排序中的几个名词: 稳定 : 我们常说,这个排序算法是稳定的,那个是不稳定的,这里的稳定,是指,两个相同大小的元素 a,b ,在排序前后,
2020-04-26
  目录