前言

JAVA代码经过编译从源码变为字节码,字节码可以被JVM解读,使得JVM屏蔽了语言级别的限制。才有了现在的kotlin、Scala、Clojure、Groovy等语言。

字节码文件中描述了类的各种信息,都需要加载到虚拟机之后才能运行和使用。

简单学习了类加载进制后,写一篇文章记录一下以便加深记忆与理解。

类加载概述

  • 什么是类加载机制?

    Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换、解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
  • 类加载机制是在编译期还是运行期?

    类型的加载、连接和初始化过程都是在程序运行期间完成的。

    缺点:类加载有一些性能消耗,IO

    优点:为Java应用提供了极高的扩展性和灵活性

Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。例如,编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类,用户可以通过Java预置的或自定义类加载器,让某个本地的应用程序在运行时从网络或其他地方上加载一个二进制流作为其程序代码的一部分。

  • 类加载的7个阶段

    一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。这七个阶段的发生顺序下图所示。

图中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。强调这点是因为这几个阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。

加载

“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,不要混淆这两个看起来很相似的名词。在加载阶段,Java虚拟机需要完成以下三件事情:

1)通过一个类的全限定名来获取定义此类的二进制字节流。

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

  • 相对于类加载过程的其他阶段,非数组类型的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的阶段。这就是下一个章节【Java类加载器】的前提。
  • 加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

    我是这样理解的:class文件一般很多,当加载了一部分文件,已经可以开始验证了,比如验证字节码是否符合虚拟机规范的规定。
  • 查看加载与卸载的过程,可以分别使用虚拟机参数-XX:+TraceClassLoading-XX:+TraceClassUnloading

虚拟机参数简介:

-XX:+<option> 表示开启option选项
-XX:-<option> 表示关闭option选项
-XX:<option>=<value> 表示把option选项的值设置为value

忘记从哪里看到的了,找了很久也没找到,说的是如果类被加载,但加载失败,但是程序并未主动使用该类,不会报NoClassDefFoundError。这一点待验证(不要参考这一点)。

后来想起是从张龙老师视频中找到的,不知道正确与否啊?

但想想也说得过去,如果加载失败,但是都不参与连接阶段,那么就不需要从二进制流读入内存,不报错也无所谓。但是具体的虚拟机如Hotspot是怎么实现的尚不知。

验证

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

Java语言本身是相对安全的编程语言(起码对于C/C++来说是相对安全的),但Class文件并不一定只能由Java源码编译而来,它可以使用包括靠键盘0和1直接在二进制编辑器中敲出Class文件在内的任何途径产生。上述Java代码无法做到的事情在字节码层面上都是可以实现的,至少语义上是可以表达出来的。Java虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃,所以验证字节码是Java虚拟机保护自身的一项必要措施。

验证阶段将做一下几个工作:

  • 文件格式验证:验证字节码格式是否符合规范

这个地方要说一点和开发者相关的。.class文件的第5~第8个字节表示的是该.class文件的主次版本号,验证的时候会对这4个字节做一个验证,高版本的JDK能向下兼容以前版本的.class文件,但不能运行以后的class文件,即使文件格式未发生任何变化,虚拟机也必须拒绝执行超过其版本号的.class文件。举个具体的例子,如果一段.java代码是在JDK1.6下编译的,那么JDK1.6、JDK1.7的环境能运行这个.java代码生成的.class文件,但是JDK1.5、JDK1.4乃更低的JDK版本是无法运行这个.java代码生成的.class文件的。如果运行,会抛出java.lang.UnsupportedClassVersionError,这个小细节,务必注意。

  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求
  • 字节码验证:是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。

准备

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。

这些变量所使用的内存在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了。

  • 这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
  • 这个阶段赋初始值的变量指的是那些不被final修饰的static变量,【赋零值】

    比如"public static int value = 123;",value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;

    比如"public static final int value = 123;"就不一样了,编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据Con-stantValue的设置将value赋值为123。

解析

解析阶段因为自己对字节码文件格式还不是非常明白,暂时不太懂,先留着,后面加强了字节码文件格式后,再来补充。

初始化

类加载一直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。

进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。

我们也可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器()方法的过程。

()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物,但我们非常有必要了解这个方法具体是如何产生的,以及()方法执行过程中各种可能会影响程序运行行为的细节,这部分比起其他类加载过程更贴近于普通的程序开发人员的实际工作。

但是这个<clinit>()还不是很懂。自己需要学习class文件结构。

关于这个<clinit>()可以研究一下这篇文章。从一道题看类的加载与实例化过程、NoClassDefFoundError异常

后面学习了回来补充这一段。

对于初始化阶段,《Java虚拟机规范8》(下方引用文字是原文)则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

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

    ·使用new关键字实例化对象的时候。

    ·读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。

    ·调用一个类型的静态方法的时候。
  2. 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  3. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
  4. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  5. 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
  6. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

这六种场景中的行为称为对一个类型进行主动引用。除此之外,所有引用类型的方式都不会触发初始化,称为被动引用。

引用官网虚拟机jvms8规范5.5章节Initialization

A class or interface C may be initialized only as a result of:
1 The execution of any one of the Java Virtual Machine instructions new,
getstatic, putstatic, or invokestatic that references C (§new, §getstatic, §putstatic, §invokestatic). These instructions reference a class or interface directly or
indirectly through either a field reference or a method reference. Upon execution of a new instruction, the referenced class is initialized if it has not been initialized already.
Upon execution of a getstatic, putstatic, or invokestatic instruction, the class or interface that declared the resolved field or method is initialized if it has not been
initialized already. 2 The first invocation of a java.lang.invoke.MethodHandle instance which was the result of method handle resolution (§5.4.3.5) for a method handle of kind 2 (REF_getStatic), 4 (REF_putStatic), 6 (REF_invokeStatic), or 8 (REF_newInvokeSpecial). This implies that the class of a bootstrap method is initialized when the bootstrap method is invoked for an invokedynamic instruction (§invokedynamic), as part of the continuing resolution of the call site specifier. 3 Invocation of certain reflective methods in the class library (§2.12), for example, in class Class or in package java.lang.reflect. 4 If C is a class, the initialization of one of its subclasses. 5 If C is an interface that declares a non-abstract, non-static method, the initialization of a class that implements C directly or indirectly. 6 If C is a class, its designation as the initial class at Java Virtual Machine startup (§5.2).

下面介绍几个被动引用的例子:

例1
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
} class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
} public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}

run with +XX:+TraceClassLoading输出如下,子类会被加载,但是不会初始化。

...
[Loaded com.jamie.basicstudy.jvm01.NotInitialization from file:/D:/CodeFolder/jamie/basic_study/target/classes/]
...
[Loaded com.jamie.basicstudy.jvm01.SuperClass from file:/D:/CodeFolder/jamie/basic_study/target/classes/]
[Loaded com.jamie.basicstudy.jvm01.SubClass from file:/D:/CodeFolder/jamie/basic_study/target/classes/]
SuperClass init!
123
例2
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
} class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
} public class NotInitialization {
public static void main(String[] args) {
SuperClass[] a = new SuperClass[10];
}
}

run with +XX:+TraceClassLoading输出如下,子类不会被加载,父类被会加载,但是父类不会初始化。

...
[Loaded com.jamie.basicstudy.jvm01.NotInitialization from file:/D:/CodeFolder/jamie/basic_study/target/classes/]
...
[Loaded com.jamie.basicstudy.jvm01.SuperClass from file:/D:/CodeFolder/jamie/basic_study/target/classes/]
例3
class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final int value = 123;
} public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.value);
}
}

run with +XX:+TraceClassLoading输出如下,ConstClass类不会被加载,只会加载主类。

...
[Loaded com.jamie.basicstudy.jvm01.NotInitialization from file:/D:/CodeFolder/jamie/basic_study/target/classes/]
...
123
  • 在编译阶段通过常量传播优化,已经将此常量的值“hello world”直接存储在NotInitialization类的常量池中,以后NotInitialization对常量ConstClass.HELLOWORLD的引用,实际都被转化为NotInitialization类对自身常量池的引用了。也就是说,实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编译成Class文件后就已不存在任何联系了。
  • 接口与类真正有所区别的是前面讲述的六种“有且仅有”需要触发初始化场景中的第三种:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

追加例子

静态成员初始化例1

package com.jamie.jvmstidy;

public class TestPreparationInitialization {
public static void main(String[] args) {
Singleton singleton = Singleton.get();
System.out.println("main方法counter1=" + Singleton.counter1);
System.out.println("main方法counter2=" + Singleton.counter2);
}
} class Singleton {
public static int counter1 = 0;
public static int counter2 = 0;
private static Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
System.out.println("构造器counter1=" + counter1);
System.out.println("构造器counter2=" + counter2);
} public static Singleton get() {
return singleton;
}
}

run with +XX:+TraceClassLoading输出

[Loaded com.jamie.jvmstidy.TestPreparationInitialization from file:/F:/CodeFolder/SELF_STUDY/jvmstudy/target/classes/]
...
[Loaded com.jamie.jvmstidy.Singleton from file:/F:/CodeFolder/SELF_STUDY/jvmstudy/target/classes/]
构造器counter1=1
构造器counter2=1
main方法counter1=1
main方法counter2=1

静态成员初始化例2

package com.jamie.jvmstidy;

public class TestPreparationInitialization {
public static void main(String[] args) {
Singleton singleton = Singleton.get();
System.out.println("main方法counter1=" + Singleton.counter1);
System.out.println("main方法counter2=" + Singleton.counter2);
}
} class Singleton {
public static int counter1 = 0;
private static Singleton singleton = new Singleton();
public static int counter2 = 0;
private Singleton() {
counter1++;
counter2++;
System.out.println("构造器counter1=" + counter1);
System.out.println("构造器counter2=" + counter2);
} public static Singleton get() {
return singleton;
}
}

run with +XX:+TraceClassLoading输出

[Loaded com.jamie.jvmstidy.TestPreparationInitialization from file:/F:/CodeFolder/SELF_STUDY/jvmstudy/target/classes/]
...
[Loaded com.jamie.jvmstidy.Singleton from file:/F:/CodeFolder/SELF_STUDY/jvmstudy/target/classes/]
构造器counter1=1
构造器counter2=1
main方法counter1=1
main方法counter2=0

为什么结果不同?

static成员的顺序在初始化过程中是很重要的,当类加载进行到准备阶段时,先执行赋零值操作;然后到了初始化阶段,接受外部赋值行为。

例1中,先为counter2 赋0(外部值),然后调用new Singleton()构造方法,为两个变量加1。最终得到counter1=1,counter2=1

例2中,先为调用new Singleton()构造方法,此时值都自增1,得到2个1。然后为counter2 赋0(外部值),最终得到counter1=1,counter2=0

说明

初始化阶段,每个类中的静态成员的初始化赋值过程是从上往下的。所以静态成员的顺序是很重要的。开发中需注意。

(画外音:这个例子除了加深对初始化阶段的认识,没有其他意义,大家看看就好)

静态成员初始化例3

package com.jamie.jvmstidy;

class Test {
static {
System.out.println("Test initialized");
}
} public class TestClassLoader {
public static void main(String[] args) throws Exception {
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class<?> aClass = loader.loadClass("com.jamie.jvmstidy.Test");
System.out.println(aClass);
System.out.println("=====================");
Thread.sleep(1000);
aClass = Class.forName("com.jamie.jvmstidy.Test");
System.out.println(aClass);
}
}

输出结果:

class com.jamie.jvmstidy.Test
=====================
(这里睡了1秒)
Test initialized
class com.jamie.jvmstidy.Test

查看Class.forName()源码可以看到内部用了反射包Reflection.getCallerClass();,JVM规范的使用了反射包,就需要立即初始化。

而使用ClassLoader直接loadClass()是不会触发初始化的。

【Java虚拟机2】Java类加载机制的更多相关文章

  1. 《深入理解 Java 虚拟机》学习 -- 类加载机制

    <深入理解 Java 虚拟机>学习 -- 类加载机制 1. 概述 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的 J ...

  2. java虚拟机(一)——内存管理机制与OOM异常

    一  java内存区域与内存溢出异常(OOM) 1)运行时数据区域划分        1.程序计数器(Program Conuter Register) 程序计数器是一块较小的内存空间,它是当前线程执 ...

  3. 深入java虚拟机学习 -- 内存管理机制

    前面说过了类的加载机制,里面讲到了类的初始化中时用到了一部分内存管理的知识,这里让我们来看下Java虚拟机是如何管理内存的. 先让我们来看张图 有些文章中对线程隔离区还称之为线程独占区,其实是一个意思 ...

  4. 深入理解 Java 虚拟机——走近 Java

    1.1 - 概述 Java 总述:Java 不仅是一门编程语言,还是一个由一系列 计算机软件 和 规范 形成的技术体系,这个技术体系提供了完整的用于软件开发和跨平台部署的支持环境,并广泛应用于 嵌入式 ...

  5. Java虚拟机之Java内存区域

    Java虚拟机运行时数据区域 ⑴背景:对于c/c++来说程序员来说,需要经常去关心内存运行情况,但对于Java程序员,只需要在必要时关心内存运行情况,这是因为在Java虚拟机自动内存管理机制的帮助下, ...

  6. 深入理解Java虚拟机之Java内存区域与内存溢出异常

    Java内存区域与内存溢出异常 运行时数据区域 程序计数器 用于记录从内存执行的下一条指令的地址,线程私有的一小块内存,也是唯一不会报出OOM异常的区域 Java虚拟机栈 Java虚拟机栈(Java ...

  7. 《深入理解Java虚拟机》-Java代码是如何运行的

    问题一:Java与C++区别 1.Java需要运行时环境,包括Java虚拟机以及Java核心类库等. 2.C++无需额外的运行时,通常编译后的代码可以让机器直接读取,即机器码 问题一:Java为什么要 ...

  8. 深入理解Java虚拟机-走进Java

    一.Java技术体系 从广义上讲, Clojure. JRuby. Groovy等运行于Java虚拟机上的语言及其相关的程序都属于Java技术体系中的一员. 如果仅从传统意义上来看, Sun官方所定义 ...

  9. Java魔法堂:类加载机制入了个门

    一.前言 当在CMD/SHELL中输入 $ java Main<CR><LF> 后,Main程序就开始运行了,但在运行之前总得先把Main.class及其所依赖的类加载到JVM ...

  10. 深入Java虚拟机:多态性实现机制——静态分派与动态分派

    方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址.这个特性给Java带来了更强大的动态扩 ...

随机推荐

  1. 如何实现 Android 短视频跨页面的流畅续播?

    在一切皆可视频化的今天,短视频内容作为移动端产品新的促活点,受到了越来越多的重视与投入,同时短视频也是增加用户粘性.增加用户停留时长的一把利器.那么如何快速实现移动端短视频功能呢?前两篇我们介绍了盒马 ...

  2. Redis哨兵机制的实现及与SpringBoot的整合

    1. 概述 前面我们聊过Redis的读写分离机制,这个机制有个致命的弱点,就是主节点(Master)是个单点,如果主节点宕掉,整个Redis的写操作就无法进行服务了. 为了解决这个问题,就需要依靠&q ...

  3. Activiti 学习(二)—— Activiti 流程定义和部署

    概述 在这一节,我们将创建一个 Activit 工作流,并启动这个流程,主要包含以下几个步骤: 定义流程,按照 BPMN 的规范,使用流程定义工具,用流程符号把整个流程描述出来 部署流程,把画好的流程 ...

  4. ubuntu安装glusterFS

    以2台服务器为例: node1: 172.18.1.10 node2: 172.18.1.20 1) 修改主机名,修改hosts文件添加IP地址映射 hostname node1/node2vim / ...

  5. 2.设计模式常用的UML图分析(用例图、类图与时序图)

    1-用例图 概述 展现了一组用例.参与者以及他们之间的关系. 用例图从用户角度描述系统的静态使用情况,用于建立需求模型. 用例特征 保证用例能够正确捕捉功能性需求,判断用例是否准确的依据. 用例是动宾 ...

  6. jquery播放视频事件

    $('video').trigger('play'); $('video').trigger('pause'); 判断video播放器的播放状态,并进行切换播放,需要这样 let video = $( ...

  7. 华为云计算IE面试笔记-桌面云中的用户组、虚拟机模板、模板虚拟机、虚拟机组和桌面组的关系及区别。发放完整复制和链接克隆虚拟机时,步骤有什么区别,要怎么选择桌面组?

    概念解释: 模板虚拟机:FC上创建的裸虚拟机,用于制作不同类型的虚拟机模板. 虚拟机模板:用于创建虚拟机的模板,对裸虚拟机(模板虚拟机)进行配置或自定义安装软件后,转为模板.虚拟机模板类型有完整复制, ...

  8. Windows 10 64位操作系统 下安装、连接测试sqlite3 sql基本操作 增删改

    一.下载sqlite安装包 1:详细下载安装版本可见官网:https://www.sqlite.org/download.html 2:百度盘分享连接:https://pan.baidu.com/s/ ...

  9. 一文让你彻底理解SQL连接查询

    表结构 内连接 笛卡尔积问题 普通内连接:inner join on 隐式内连接: 外连接 内连接与外连接查询的区别 内连接查询是查询两张表交集的数据,主外键关联的数据. 左连接查询是查询左表中的所有 ...

  10. 千位分隔符的JS实现

    $.extend({ //千位分割符 MoneySeparator: function numFormat(num){ if(num==null){ return num; }else { num=n ...