深入理解java虚拟机(4)---类加载机制

  类加载的过程包括:

  加载class到内存,数据校验,转换和解析,初始化,使用using和卸载unloading过程。

除了解析阶段,其他过程的顺序是固定的。解析可以放在初始化之后,目的就是为了支持动态加载。

从java开发者来讲,我们并不关心具体细节,只要知道整个流程以及每个流程大体干了那些事情。

每个流程具体对开发代码会有那些影响就可以了。

一:类的加载流程

1.加载loading

  在加载过程中,虚拟机需要完成3件事情:

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

2)将这个直接流的静态存储结构转化为方法区的运行时数据结构。

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

2.验证

验证是虚拟机非常重要的一步,其目的是为了确保class文件的字节流符合java虚拟机自身的要求,不会导致虚拟机崩溃。

java语言本身是比较安全的语言,它没有数组越界等情况的发生。But,class语言并不是一定由java语言产生的。甚至于,

可以直接使用16进制工具编写class文件。而这些文件就不能保证class文件的规范性。

大致分成4个阶段的验证过程:文件格式验证元数据验证字节码验证符号引用验证

文件格式验证:

比如是否以魔数开头,主次版本号是否在虚拟机可处理范围之内,常量池是否有不支持类型等。

经过这个阶段的验证之后,字节流才会进入内存的方法区进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构进行的

元数据验证:

对字节码描述的信息进行语义分析,以保证其描述的信息符合JAVA语言规范的要求,这个阶段可能包括的验证点有:

这个类是否有父类,父类是否集成了不允许继承的类,如果不是抽象类是否实现了其父类或接口中要求实现的所有方法,类中的字段和父类是否有矛盾

字节码验证:

最复杂的一个解读那,主要工作是进行数据流和控制流分析。这阶段对类的方法体进行校验分析,保证该方法在运行时不会做出危害JVM安全的行为,例如:

保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,保证跳转指令不会跳转到方法体以外的字节码指令上,保证方法体中的类型转换是有效的。。。

这个验证并不能保证一定安全(停机问题,通过程序去校验程序逻辑是无法做到绝对准确的

1.6加入StackMapTable功能对这个阶段做了优化,提高速度,但这个StackMapTable也可能被篡改,可以通过启动参数来关闭这个选项。

符号引用验证:

这个阶段发生在虚拟机将符号引用转化为直接引用的时候。这个转化动作将在连接的第三个阶段----解析阶段中发生

可以看作是对类自身以外的信息进行匹配性的校验。

比如:符号引用中通过字符串描述的全限定名是否能找到对应的类,是否存在所描述的方法和字段。。。。

如果无法通过符号验证,将会抛出一个Java.lang.IncompatibleClassChangeError异常的子类,比如Java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSuchMethodError

可以使用启动参数来关闭大部分类验证措施,缩短虚拟机类加载时间

3.准备

准备阶段就是为类的变量正式分配内存并设置初始值。这个初始值与初始化不是同一个概念。

比如

public static int value = 12;

这个阶段value的值为0 而不是12。value赋值为12的阶段

是在初始化的过程中出现的。

java所有的基本类型都赋值为零值。(简单来说就是0 or null,0.0f,false等)

4.解析

解析是java语言面向对象的基础。

解析的过程是将常量池里面的字符引用替换为直接引用的过程。

符号引用是 一组以符号来描述所引用的目标。各种虚拟机的内存布局可以各不相同,但是字面量的形式有虚拟机规范严格规定。

直接引用就是对虚拟机内存布局的直接描述。

所以引用的目标必须已经加载到内存里面了。

1).类或接口的解析

类和接口的解析:假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成整个解析的过程需要以下三个步骤:

如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C

如果C是数组类型,并且数组的元素类型是对象,则按照1的情况处理。如果元素类型不是对象,则由虚拟机生成一个代表此数组维度和元素的数组对象

如果上述步骤没有异常,C在虚拟机中世纪已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认C是否具有对D的访问权限,如果没有则抛出java.lang.IllegalAccessError异常。

2).字段解析

大体情况如下:

class D{

  public D(C c)
    {
         string a = c.a;
    }
}    

D 需要加载C.a 字段,首先,需要加载的是C类的解析内容。然后关键部分就是java语言继承的东东了。

如果C类本生就含有a的字段,直接返回a的直接引用。

搜索C类的接口,按照继承关系从上到下搜索各个接口已经父接口,直到找到a字段。如果没有

if C is not the java.lang.Object,同上,搜索C类的父类,如果有,就使用该字段的直接引用。如果没有

也就是C类及其相关类or接口没有这个字段,查找失败。

如果找到,还需要进行权限验证。

如果接口 & 类 都包含相同名的字段,java程序员有时候会无法判断到底使用的是哪个字段。

所以编译器一般会拒绝这种情况的发生。

以下是使用androidstudio 实验的结果:

public interface ICoo {
    public static int A = 1;
}

public abstract class CooAbstruct {
    public int A = 22;
}

public class Coo extends CooAbstruct implements ICoo {

//    public int geta()
//    {
//        return A;
//    }
}
public class Doo {

    public Doo(Coo c)
    {
        int a  = c.A;
    }
}

E:\GitHub\jvmdemo\app\src\main\java\com\joyfulmath\myapplication\Doo.java:13: 错误: 对A的引用不明确, CooAbstruct中的变量 A和ICoo中的变量 A都匹配
int a = c.A;
^
1 个错误

可以看到,编译器明确 无法区分A到底是使用哪个字段。

在C++的多继承中,类似的情况在使用时需要明确到底是使用哪个子类的字段。

3)类的方法加载:

同样使用C类来描述这个过程:

类方法和接口方法 常量类型是分开的。所以如果C类方法发现是一个接口的方法的话,直接回抛出异常。类型检测。

直接在C类里面寻找是否有匹配的字符描述的方法。没有就继续

在C类的父类里面递归寻找,没有就继续

在C类的接口里面递归寻找,找到,说明本方法未被实现,C类是抽象类。抛出异常

都没有找到,nosuchmethod。

如果找到有效的匹配方法后,检查权限。

4)接口的加载方法

过程同类的方法基本一致。只是不需要进行权限检查。

二.初始化

初始化和准备阶段是不同的过程,而且是java程序员最关系的部分。

1.必须初始化的情况

java虚拟机规范 规定了5种 (有且仅有)情况下,必须进行初始化的操作。

1)遇到new,getstatic,putstatic,invokestatic 这4条指令的时候。对应场景:

实例化一个类,读取或者设置一个类的静态字段,调用一个类的静态方法时候。

2)使用反射方法调用的时候,需要先初始化。

3)当初始化一个类时,需要先初始化父类。

4)当虚拟机启动时,需要指定一个启动类(main类),虚拟机会首先初始化这个类。

5)当使用jdk1.7动态语言时候,具体情况本文不做分析。

一下使用几个demo来说明我们容易误解的地方:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TraceLog.i(String.valueOf(SubClass.value));
    }

}
public class SubClass extends SuperClass {
    static {

        TraceLog.i("subclass init!");
    }
}
public class SuperClass {
    static {
        TraceLog.i("SuperClass init!");
    }

    public static int value = 12;
}

结果log:

05-08 10:10:33.783 868-868/com.joyfulmath.myapplication I/SuperClass: <clinit>: SuperClass init! [at (SuperClass.java:13)]
05-08 10:10:33.783 868-868/com.joyfulmath.myapplication I/MainActivity: onCreate: 12 [at (MainActivity.java:19)]

是的,只有父类被初始化了,子类没有初始化,why?

应为value定义的父类,所以只需要初始化父类就可以的。

public class SuperClass {
    static {
        TraceLog.i("SuperClass init!");
    }

    public static int value = 12;

    public SuperClass()
    {
        TraceLog.i("SuperClass construct");
    }
}

实例化construction函数没有走到,所以没有实例被创建!!!but,我们在看log,<clinit> 这个是神马?这个就是打印SuperClass.init所在的函数!!!

这个等到下面在讲,我们继续我们的demo。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
//        TraceLog.i(String.valueOf(SubClass.value));
        TraceLog.i();
        SuperClass[] a  = new SuperClass[10];
    }

05-08 10:22:33.100 12438-12438/com.joyfulmath.myapplication I/MainActivity: onCreate:  [at (MainActivity.java:21)]

what? 对于SuperClass 没有一行log,也就是根本没有初始化SuperClass。

它触发了一个类为“[xxx.Superclass“ , 这是SuperClass对应的数组类,是由虚拟机自动生成的。

 TraceLog.i(a[0].toString());
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method ‘java.lang.String java.lang.Object.toString()‘ on a null object reference
                                                                                  at com.joyfulmath.myapplication.MainActivity.onCreate(MainActivity.java:23)
                                                                                  at android.app.Activity.performCreate(Activity.java:5961)
                                                                                  at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1129)
                                                                                  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2364)

a[0] 居然是null? 是的,数组a里面都是null。对的a只是一个数组,a的类型为”[xxx.Superclass“ 不是SuperClass。所以数组不会自动初始化元数据。

常量。

常量存放在常量池里面,所以对常量的引用在编译阶段就已经被优化。

下面我们来讲讲<clinit> 这个东东。

静态代码块+所有类的变量的赋值动作。

这里有一点需要强调:编译器收集的顺序与由源代码在文件中的顺序是一致的。

<clinit>()方法是由编译器自动收集类中的所有类变量的复制动作和静态语句块中的语句合并而成。编译器收集的顺序和语句在源文件中出现的顺序一致,静态语句块中只能访问到定义在它之前的变量,定义在它之后的变量,只能赋值,不能访问

<clinit>()方法与类的构造函数<init>()不同,不需要显式的调用父类构造器,虚拟机会保证父类的<clinit>()在子类的之前完成。因此,虚拟机执行的第一个<clinit>()方法肯定是java.lang.Object.

由于父类<clinit>()方法先执行,也就意味着父类中定义的静态语句要优先于子类的变量赋值操作。

<clinit>()方法并不是必须的,如果一个类没有静态语句块也没有对变量赋值操作,就不会生成

接口中不能使用静态语句块,但仍有变量初始化赋值的操作,因此也会生成<clinit>()方法,但与类不同的是,接口的<clinit>()方法不需要执行父接口的<clinit>()方法。只有当父几口中定义的变量被使用时,父接口才初始化,另外,接口的实现类在初始化时一样不会执行接口的<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都会阻塞,直到该方法执行完,如果在一个类的<clinit>()方法中有耗时很长的操作,可能会造成多个进程阻塞,在实际应用中,这种阻塞往往很隐蔽。

参考:

《深入理解java虚拟机》 周志明著

时间: 2024-10-13 23:24:15

深入理解java虚拟机(4)---类加载机制的相关文章

【深入理解Java虚拟机】类加载机制

本文内容来源于<深入理解Java虚拟机>一书,非常推荐大家去看一下这本书. 本系列其他文章: [深入理解Java虚拟机]Java内存区域模型.对象创建过程.常见OOM [深入理解Java虚拟机]垃圾回收机制 1.类加载机制概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 在java中,类型的加载.连接和初始化过程都是在程序运行期间完成的,这种策略虽然会带来一些性能开销,但是却为jav

java虚拟机之类加载机制

注:文中所说的 Class 文件并不是特指存在于具体磁盘中的文件,而是一串二进制字节流,无论是以何种形式存在的都可以. 1. 引言 java 类被虚拟机编译之后成为一个 Class 的字节码文件,该字节码文件中包含各种描述信息,最终都需要加载到虚拟机中之后才能运行和使用.那么虚拟机是如何加载这些 Class 文件?Class 文件中的信息进入虚拟机之后会发生什么变化?接下来我们一个一个探讨. 2. 类加载的时机 类的整个生命周期包括:加载.验证.准备.解析.初始化.使用和卸载七个阶段,其中验证.

【java虚拟机】java虚拟机的类加载机制

这篇博文主要来总结一下java虚拟机加载一个类的过程,部分参考自<深入理解Java虚拟机>.为了避免枯燥的解说,为了让读者在读完本文后能彻底理解类加载的过程,首先来看一段java代码,我们从一个例子入手: //ClassLoaderProcess.java文件 class Singleton { private static Singleton singleton = new Singleton(); public static int count_1; public static int c

Java虚拟机的类加载机制

Java虚拟机类加载过程是把Class类文件加载到内存,并对Class文件中的数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程. 在加载阶段,java虚拟机需要完成以下3件事: a.通过一个类的全限定名来获取定义此类的二进制字节流. b.将定义类的二进制字节流所代表的静态存储结构转换为方法区的运行时数据结构. c.在java堆中生成一个代表该类的java.lang.Class对象,作为方法区数据的访问入口. Java虚拟机的类加载是通过类加载器实现的, Java中

阿里P7架构师对Java虚拟机、类加载机制是怎么理解的?

概述 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载 (Loading).验证(Verification).准备(Preparation).解析(Resolution).初始化 (Initialization).使用(Using)和卸载(Unloading)7 个阶段.其中验证.准备.解析 3 个部分统称为连接(Linking) 于初始化阶段,虚拟机规范则是严格规定了有且只有 5 种情况必须立即对类进行“初始 化”(而加载.验证.准备自然需要在此之前开始): 1)遇到

深入JAVA虚拟机之类加载机制

前言: 前面学习了类Class文件格式和里面具体的内容,也已经学习了运行时数据区的各部分区域的内容.接下来就是学习JVM是如何把Class文件中记录的信息加载到运行时内存中的,以及class文件中各个部分的信息分别存放在运行时数据区的什么地方.从这篇文字中我们能获得什么? 1.虚拟机是如何加载Class文件的 2.Class文件信息进入JVM后有那些变化 3.进一步理解运行时数据区.Class文件信息.以及类加载过程中都做了那些操作. java语言特性的根基 类加载机制 虚拟机把描述类的数据从C

深入理解Java虚拟机—内存管理机制

前面说过了类的加载机制,里面讲到了类的初始化中时用到了一部分内存管理的知识,这里让我们来看下Java虚拟机是如何管理内存的. 先让我们来看张图 有些文章中对线程隔离区还称之为线程独占区,其实是一个意思了.下面让我们来详细介绍下这五部分: 运行时数据区 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都拥有自己的用途,并随着JVM进程的启动或者用户线程的启动和结束建立和销毁. 先让我们了解下进程和线程的区别: 进程是资源分配的最小单位,线程是程序执行的

深入理解Java虚拟机笔记---类加载过程

一.加载 "加载"(Loading)阶段是"类加载"(Class Loading)过程的一个阶段.在加载阶段,虚拟机需要完成以下三件事情: a.通过一个类的全限制名来获取定义此类的二进制字节流. b.将这个字节流所代表的静态存储结构转化为方法区的运行进数据结构. c.在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口. 虚拟机规范的这三点要求实际上并不具体,因此虚拟机实现与具体应用的灵活度相当大.例如"通过一

深入理解Java虚拟机笔记---类加载时机

类从被加载到虚拟机内存中开始,到缷载出内存为止,它的整个生命周期包括了:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using),缷载(Unloading)七个阶段.其中验证,准备,解析三个阶段统称为连接(Linking)阶段,这七个阶段的发生顺序如下图: 加载,验证,准备,初始化和缷载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:

深入理解java虚拟机,类加载

1,通过使用静态字段,只有真正定义这个字段的类才会被初始化,(子类不初始化,父类初始化 2,new数组,不初始化 3,通过类来调用一些类的常量,可以不初始化类,(常量在编译期,被优化到NotInitialization类,成为了他的引用,所以可以不初始化 4,类和接口的初始化必要条件不同的是,接口可以不初始化父类的接口 5,加载: 1)通过一个类的全限定名获得一个类的二进制字节流 2)把这个类的静态存储结构转存在方法区的运行时数据结构(运行时常量池? 3)在java.lang.Object初始化