JAVA虚拟机 类加载机制理解

深入探索前端调试神器vConsole vConsole作为一款前端调试神器,为前端开发者提供了一个便捷的调试解决方案。通过本文的介绍和示例,相信你已经对vConsole有了更深入的了解。然而,vConsole的功能远不止于此,它还有许多高级用法和技巧等待你去发掘。讨论:你在前端开发中是如何使用vConsole的?你有没有发现vConsole的一些隐藏功能或技巧?欢迎在评论区分享你的经验和见解!💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面! 阅读详情

前些时间坐车无聊翻手机的时候,无意中看到一篇之前别人写的关于分析JVM类加载机制的文章。其中作者列举了一段很有意思的代码,当时大概看了之后,也是很有兴趣的推测了一下运行结果,在推敲的过程中开始发现一些疑问,所以本身对结果就比较没把握。后来有空也是敲了一遍代码,发现果不其然真实的运行结果与自己想象的结果并不相同,囧~~ 周末大概回忆了一下,大概就是类似下面这样的代码逻辑:

public class Test
{
    public static void main(String[] args)
    {
        staticMethod();
    }

    static Test sInstance = new Test();

    static
    {
        System.out.println("1");
    }

    {
        System.out.println("2");
    }

    Test()
    {
        System.out.println("3");
        System.out.println("a="+a+",b="+b);
    }

    public static void staticMethod(){
        System.out.println("4");
    }

    int a = 100;

    static int b = 200;
}

有兴趣的朋友也可以先自己在脑子里想一想以上代码的运行结果,然后去实际验证一下程序的运行结果是否和你推断的一样。如果你能轻松的推断出正确的答案,那么相信你对于这一块的知识点的掌握一定是足够的;而反之,也就可以思考一下自身需要补充的不足了。像我写这篇博客的原因,也是于此发现自己之前对于Java中类的初始化顺序的理解还是有问题的,而我推断的结果出现误差的原因其实主要就是对于JVM类加载机制的理解还是太浅显了。所以也是希望通过本文重新简单的总结一下关于JVM的类加载机制,加强自己的印象和理解。

推断出的错误结果可能有很多种,而对应的导致相应错误的原因自然也不同。以我自己来说,个人最初对于该段程序的运行结果推测如下:

1
4
2
3
a=100,b=200

其实造成这种错误的想法的原因并不难理解,因为通常来说我们对于JAVA当中类的初始化工作的执行顺序都是这样理解的:

  1. 父类的静态域(静态变量/静态代码块)。
  2. 子类的静态域(静态变量/静态代码块)。
  3. 父类的实例域(成员变量/构造代码块)。
  4. 父类的构造器
  5. 子类的实例域(成员变量/构造代码块)。
  6. 子类的构造器

事实上,我们并不能说以上的规则是错误的。但如果只是记忆这种规则,但不去理解究竟是什么样的内部机制衍生出这种初始化顺序,也可以说如果只是知其然但并不知其所以然,那么可能就会导致错误的理解和使用。就比如在这里讨论的用例中,如果只是套用这种初始化的顺序规则,就会发现自己很难确定该段程序究竟会输出什么样的结果。所以,这里我们的目的自然就是更深入一点,试着去了解:在JAVA当中,一个类从创建到消亡,究竟会走过怎么样的生命周期呢?而又究竟是什么样的内部机制决定着它们的走势?


回归一下本质的话,其实当我们在代码中需要使用到某个类的时候,无非就是两种情况:使用它的静态成员或是使用它的实例成员。例如:

public class MyClass {

    private void memberMethod(){

    }

    private static void staticMethod(){

    }

    public static void main(String[] args) {
        // 1.
        MyClass myClass = new MyClass();
        myClass.memberMethod();
        // 2. 
        staticMethod();
    }
}

那么我们有没有想过,就比如当我们试着调用类的某个方法时,虚拟机内部究竟是做了什么样的工作从而确保其正确执行的呢?分析一下来说,首先我们日常所做的编程工作 本质其实就是根据不同的业务需求编写对应的JAVA代码,编写好的代码最终被保存在一份对应的.java文件当中。问题在于:.java文件中的代码格式显然是面向我们编写者的,所以其实计算机显然是无法直接解析和执行其中的代码的。正是因为这个原因,所以当我们编写完成代码后,如果想要执行它,则还需要将其进行编译。编译通过后,才会得到一份能够被虚拟机解析和执行的.class文件。简单来说,这也才是我们本文中理解上的“类”的概念。

那么,其实我们可以将一个Java项目理解为现实生活中我们的一个工具箱,项目中不同的类就是工具箱内各种不同的工具。所以假设我现在想要使用“锤子”,首先应该怎么做呢?显然就是现在工具箱中找到正确的位置,将锤子从给取出来。那么对应来说,比如我们想使用这里的MyClass类,JVM首先需要做的工作就是通过相应的途径获取到这个类(通常来说就是查找到对应的.class文件进行获取)。于是,也就涉及到了我们本文中试图理解的的类加载机制。而这个所谓的类加载机制究竟是怎么一回事呢?我们接着进行分析。

显然首先我们需要理解的就是在什么样的情况下,一个类才会被进行装载?继续以我们工具箱的例子来说,如果我们目前只是需要使用锤子,那么显然我们没必要将工具箱所有的工具都给倒腾出来,这显然是一种浪费的行为。而对应于JVM也一样,一个成熟的项目可能会含有成千上万个类,如果程序开始运行,就将所有的类进行加载,那么显然这种内存开销和时间消费都是不能接受的。所以,就像我们需要使用锤子,就拿锤子一样;JVM会在某个类需要被使用的时候,对其进行加载。而如果更具体一点来说,这个所谓的“需要被使用”就是指:当一个类的静态成员被访问的时候。这个应该很好理解:

public class MyClass {

    ... 

    private static void staticMethod(){

    }

    private static int a = 1;

    public static void main(String[] args) {
        // 1.
        System.out.println(a);
        // 2. 
        staticMethod();
    }
}

这时我们就是在访问类的静态成员,当然,你可能会说更多的时候,其实我们还是访问类的实例成员的情况更多一些:

public class MyClass {

    private void memberMethod(){

    }

    ...

    private int b = 2;

    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        // 1.
        System.out.println(myClass.b);
        // 2. 
        myClass.memberMethod();
    }
}

但显然我们要明白的是:当我们需要访问一个类的实例域的时候,前提则是构建该类的实例对象。构建类的实例对象归根结底往往就离不开类的构造器。而虽然通常我们在定义构造器的时候并没有显式的将其定义为static,但构造器默认的就被设定为静态成员。所以其实我们通过构造器创建类的对象的时候,其实也是在访问类的静态成员。所以更加通俗一点来说,当我们访问一个类的静态变量,静态方法,或者构建类的对象的时候,都会触发该类的加载工作。


由此我们应该已经清楚了在什么样的条件下,类装载器ClassLoader就会开始类的加载工作。所以我们接下来关心的,自然就是在虚拟机当中,这个加载工作到底都做了什么?简单来说,通常JAVA中一个类的生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。

简单分析一下来说,由加载 → 初始化的这个阶段,其实就是从一个类从被加载到程序内存当中开始,到最后完成相应的初始化赋值工作的过程。当这个阶段的工作完成,我们就能够对类进行使用了。对应的,卸载则是指类在使用完成后,垃圾处理机制对无用的类进行垃圾回收,从而将其从内存中清理出去的工作。所以,对应于我们本文开头提出的程序代码来说,这里我们关注的重点显然就是由加载 → 初始化的这一阶段的工作,这其实也就是通常我们所说的Java当中的类加载机制。接着,我们就来看看这一过程中,每个步骤都做了哪些对应的工作。


加载

这是类加载过程中的第一个阶段,在这个阶段中会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。简单来说,这里做的工作就是:首先通过类的全限定名到相应的路径下查找到对应的.class文件获取到对应的二进制字节流(但这也不是绝对的,也有其他方式可以获取,比如:网络、其他文件、动态生成等等);然后将读取到的数据从静态存储结构的形式转化为方法区的运行时数据结构,转而在内存中生成一个代表这个类的java.lang.Class对象。

(注:java.lang.Class对象我们应该不会陌生,它又被称为class of classes。通常我们通过类字面常量”.class”或者”Class.forName()”都可以获取到这个对象。如果我们将平时使用的类的对象视为钞票,那么这个class对象就可以视为打印钞票的模板。由此来说,其实这个阶段的工作的意义也就更好理解了:比如我们想要创建某个类的对象,那么虚拟机究竟如何才能完成呢?这就好比我们想要印钞自然先需要拿到雕版一样,一个类当中包括类名,访问权限,其内部定义的字段,方法等信息都保存在编译后的字节码文件当中。所以当我们想要在程序中使用它们时,自然就需要先将这些信息读取进内存才行。而这些信息在字节码文件中的结构遵循某种数据格式,当它们被读取进内存后,虚拟机则将这些数据重新转化为自身认可的运行时的数据结构)

验证

这一阶段就不过多描述了,其主要目的是确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,且不会危害虚拟机自身的安全。

准备

这个阶段所做的工作其实就是正式为类或者接口的静态字段分配内存,并使用 默认值 初始化这些字段,而这些静态字段所使用的内存都将在方法区中进行分配。需要注意的是这时候进行内存分配的仅包括类或者接口的静态字段,但不包括类的成员(实例)字段。成员字段的内存分配工作将会在对象实例化时进行,并且我们都知道为它们分配的内存空间也不再是方法区,而是位于堆当中。另外,我们说了在这个阶段中,除了为类变量分配内存空间之外,还会为其设置一个初始值。而在这里会有一个差异点是需要我们记住的,举例来说:

public class Test{
    public static int num = 10;
}

就比如这里的静态变量num在准备阶段后,值就将会被初始化为默认值0,而不是10。但是对于被final修饰的静态变量,也就是通常所谓的静态常量来说,情况则有所不同:

public class Test{
    public static final int num = 10;
}

在这里,经过准备阶段过后,num的值将被设置为我们指定的10,而不再是被初始化为0 。那么,为什么会造成这种差异呢?我们先来看看这两种不同的方式编译后的.class文件会体现什么差异?

前后两图分别对应此前使用静态变量和静态常量的字节码文件,我们可以从中看到两者的一些差异。而其中最关键的是,可以看到用final修饰num过后,字节码文件中对于num的描述多了一个ConstantValue的字段,且这个常量值作为编译期常量 还将被写入到字节码文件的编译时常量池当中。那么书归正传,虚拟机在 准备阶段 面对这种情况,即当类的静态字段的属性含有ConstantValue时,会在准备阶段初始化为指定的值,而不再是初始化为一个默认的值。

(P.S:虽然在这里我们使用的是静态常量,但实际上不使用static而只被final修饰的成员常量其实也会带有ConstantValue属性。所以在字节码文件的常量池中仍然存储了相应的常量。那么,我们说当字节码文件被装载进方法区时,其常量池中的常量也会进入内存。于是这就很容易让人困惑,因为这样的话是不是就意味着即使我们只是访问类中的某个静态成员,但是该成员常量却依然会被分配内存呢?针对这个疑问也是在网上看了很久,但是都没看到说到点子上的。但是在《JAVA虚拟机规范(SE7版本)》一书中提到:如果 field_info 结构表示的非静态字段包含了 ConstantValue 属性,那么这个属性必须被虚拟机所忽略。所有 Java 虚拟机实现必须能够识别 ConstantValue 属性。由此来说,我们可以推测出非静态的final字段,在类的装载阶段应当是会被忽略的,它的内存分配及初始化等工作应当还是随着对象的实例化在堆中进行)

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。仅仅通过文字描述,我们可能不太好理解所谓的符号引用和直接引用究竟是啥意思。其实很简单,前面我们也截图了部分.class文件的信息,其中Constant Pool当中的内容就是这里所说的常量池,那么这里面哪些部分是所谓的符号引用呢?

  • CONSTANT_Class_info
  • CONSTANT_InterfaceMethodref_info
  • CONSTANT_Fieldref_info
  • CONSTANT_Methodref_info
  • CONSTANT_NameAndType_info

比如常量池中以上类型的常量都是指所谓的符号引用。而举例来说,比如之前我们的第一幅class文件的截图中,因为变量num在编译时,编译器并不能确定它在进入运行期被写入到内存后,具体的内存地址。所以编译器只能用Filedref以及NameAndType这样的符号引用来标示这个变量。但是当类开始装载之后,因为我们已经知道在准备阶段过后,类的静态字段就会在方法区内被分配内存。所以这个时候我们已经能够得知其在内存中的实际地址,所以自然也就应该用指向实际内存地址的直接引用来替换掉符号引用了。

初始化

这是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。这是什么意思呢?前面说到在准备阶段,变量已经经历过一次系统要求的初始值。但是到了初始化阶段,JVM才会真正根据我们实际指定的计划去初始化类变量和其他资源。而这个过程实际就是执行字节码文件中<clinit>方法的过程。

所谓的 <clinit>方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态代码块只能访问到定义在静态代码块之前的变量,定义在它之后的变量,在前面的静态代码块可以赋值,但是不能访问。当然, 如果一个类中没有对静态变量赋值也没有静态代码块,那么编译器可以不为这个类生成<clinit>方法。

另外,我们需要明白的是,虚拟机同样会保证<clinit>方法执行之前,父类的<clinit>方法已经执行完毕。这也是为什么前面说到的父类的静态域的初始化工作会在子类的静态初始化工作之前的原因。但是,是不是任何情况下,初始化这一阶段都会得到执行呢?其实并不是的,以下等情况就不会执行初始化阶段的工作:

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
public class Test {
    public static void main(String[] args) {
        System.out.println(Son.num);
    }

}

class Parent {
    public static int num = 10;
}

class Son extends Parent {
}

在如上代码中,就只有Parent会执行初始化阶段的工作,Son则不会。也就是说,如果这个时候子类Son当中还存在静态变量,那么它的值则仍然会是准备阶段的初始的默认值;如果存在静态代码块,那么静态代码块中的代码并不会执行。

  • 定义对象数组,不会触发该类的初始化。
public class Test {

    public static void main(String[] args) {
        Parent [] parents = new Parent[10];
    }

}


class Parent{
   ...
}
  • 对于访问类中的编译时常量,不会触发类的初始化。
public class Test {

    public static void main(String[] args) {
        System.out.println(Parent.a); // 会触发初始化
        System.out.println(Parent.b); // 不会触发初始化
    }

}


class Parent{
    public static int a = 10;
    public static final int b = 10;

    static{
        System.out.println("init");
    }
}

这里需要注意的就是,在上述代码中不会触发类的初始化的情况仅仅是针对于编译时常量。但如果是访问运行时常量,则仍然会触发类的初始化。其原因不难理解,这是因为我们前面说到,编译时常量在准备阶段就可以被设定为正确的值,所以不用再进行初始化阶段的工作,我们仍然能正确的访问到这个静态字段。但是反过来,运行时常量则意味着该常量的值只有等到程序真正进入运行期后才能确定,所以仍然需要进行初始化才能确定。比如:

public class Test
{
    public static void main(String[] args)
    {
        System.out.println(RuntimeConstant.NUM);
    }


}

class RuntimeConstant{
    public static final int NUM = new Random().nextInt(100);

    static{
        System.out.println("static init");
    }
}

就像这里,虽然NUM是一个常量,但因为它的值需要通过调用Random类的实例方法nextInt来获取,所以在编译期是无法确定的,只能等到程序进入运行期后被最终确定。所以,在这种情况下,仍然会导致RuntimeConstant类的初始化。

  • 通过类名获取Class对象,不会触发类的初始化。
public class Test {

    public static void main(String[] args) {
        System.out.println(Parent.class.getName());
    }

}


class Parent{
    ...
}
  • 通过Class.forName加载指定类时,如果明确指定参数initialize为false时,也不会触发类初始化。
public class Test {

    public static void main(String[] args) {
        Class<Parent> clazz = null;

        try {
            clazz = (Class<Parent>) Class.forName("Parent"); // 会触发初始化
            clazz = (Class<Parent>) Class.forName("Parent", false,Test.class.getClassLoader());  // 不会触发初始化
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println(clazz.getName());
    }

}


class Parent{
    ...
}

OK,相信到了这里,我们应该都已经了解了一个类从被类装载器加载到内存当中至初始化完毕,到最终能够被我们使用,都会经历哪些阶段的工作了。而额外值得一提的就是,前面我们已经说到通常一个类被加载的过程,就是由加载 → 验证 → 准备 → 解析 → 初始化的过程。这其中,加载、验证、准备、初始化的顺序是固定的,但解析则存在一定不确定性,它在某些情况下可以在初始化阶段之后再开始。而原因我们也不难猜到,就是为了支持Java语言的运行时绑定。举例来说:

public class Test {

    private static Parent p = new Parent();
    private static Parent p1;

    public static void main(String[] args) {
        initP(new Son());  
    }

    public static void initP(Parent parent){
        p1 = parent;
    }
}


class Parent{

}

class Son extends Parent{}

如上代码中,静态变量p和p1,它们解析阶段的工作都将在初始化阶段之后。p1可能相对更好理解,因为在编译期我们没有为静态变量p明确指定任何值,只有当程序进入运行期调用initP方法过后,变量p才会被真正的初始化为某个对象,所以显然在这之前也无法完成由符号引用转变为直接引用的解析工作。而对于p可能更容易造成疑惑,那就是之前的例子当中为什么静态变量num就是按照正常的顺序完成装载的呢?

答案其实不难得出:显然关键就在于它们的数据类型是基础数据类型int和对象类型的区别。对于int这种基础数据类型来说,因为内存中需要存放的实际本身就是它所对应的值。所以对于基础数据类型的静态变量来说,前面我们也说到它们本身就是被写入到方法区中存放,所以在准备阶段之后,我们已经能够得到它们在内存中对应的的直接引用,所以自然就能马上接着进行解析阶段的工作。

而对于对象来说,我们都知道对象是在程序运行期在堆内存中分配内存。所以对于对象类型的静态变量来说,在准备阶段虽然说是将作为变量的对象引用在方法区中分配了内存,但是因为其指向的对象还没有实例化,也就意味着内存中目前根本没有相关的对象,所以在准备阶段过后方法区其实仍然存放的是对应的符号引用。只有当初始化阶段结束后,其赋值的对象已经实例化完成,在堆中分配了内存空间之后,才有办法替换为对应的直接引用。所以这也就是为什么,这时候解析阶段的工作只能在初始化阶段之后才能够进行的原因。


案例解析

好了,有了以上基础,现在我们回到文章开头提出的那段程序当中。重新来分析一下,其中的代码究竟会如何运行?其实只要是对Java类的初始化工作顺序有一定了解,那么这段代码中容易让人出错的地方其实就是对于sInstance的初始化赋值工作。因为通常来讲,我们对于类的初始化顺序的认知,简单概括来说都是:静态域>实例域>构造器。但是这段代码中的特殊之处就在于,静态变量sInstance它的初始化赋值工作又涉及到它本身的构造器,也就是说出现了一个静态域与构造器的初始化嵌套。所以,如果我们对类的加载机制不了解的话,就确定不了它们这时究竟会按照什么顺序执行。

下面我们就一步一步来分析一下这个东西。总体来说,其实对于类的初始化我们其实可以理解为分为两个部分,即静态初始化和实例初始化,对这两部分的初始化工作其实我们要避免一些误区。在这其中,静态初始化其实就是我们本文中一直在分析的东西,当我们访问一个类的静态成员的时候,对于类的加载的工作实际上就是在做类的静态初始化工作。而需要明白的是,如果只是访问类的静态成员,那么在这期间 类的实例成员是并不会进行初始化工作的。而实例成员在什么时候会进行初始化呢?答案就是当我们试图获取一个类的实例对象的时候,最常见的就是我们试图通过new调用构造器实例化一个类的对象的时候。

但是,前面我们说到类的构造器本质上其实也是类的静态成员,所以它也可能会导致类的静态初始化。但就像我之前说的,我们需要避免的误区是,当我们试图去获取一个类的实例对象的时候,静态初始化的工作并不是绝对的!因为有了本文之前对于类加载机制的分析,我们可以知道,类的装载工作只会在该类还从未被装载过的时候执行一次,所以类的静态初始化工作也只会在这时执行唯一 一次。那么也就是说,如果某个类已经进行过装载,则即使再次实例化该类的对象,也不会再执行静态初始化了。看如下代码:

public class Test {

    {
        System.out.println("instance init");
    }

    static{
        System.out.println("static init");
    }

    public static void main(String[] args) {
        Test t = new Test();
        System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
        Test t1 = new Test();
    }

}

这段代码的运行结果为,由此也可以验证我们的推断:

static init
instance init
xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
instance init

但是相信这时我们可能会说,这似乎还是没有完全解释我们之前的疑问。没错,这也就是我们需要避免的另一个误区:那就是类的实例初始化工作并不是说绝对需要在静态初始化之后才能进行。所以,现在让我们重新来分析一下文初的代码的执行顺序到底是如何的:

  • 首先,因为静态方法main方法的调用,显然类装载器就会开始Test类的加载工作,即静态初始化工作将开始进行。

  • 接着,在准备阶段过后,静态成员“sInstance”与“b”就被分配了内存空间,并且分别被初始化为null和0。

  • 然后,当最终进入到初始化阶段后:按照定义的先后顺序,于是将首先执行“sInstance”的初始化工作。
    所以Test类的构造器就会被调用。而其实关键也就在这里,因为构造器的调用,意味着Test类的实例初始化将开始。

  • 而前面也说到,我们需要避免的一个误区就是实例初始化并不是必须在静态初始化之后才能进行。
    同时,虽然访问构造器也可能触发类的加载,但显然此时该类已经开始了一次加载工作,所以将直接开始实例初始化的工作。

  • 而对于实例化初始化来说,因为首先将按照定义的先后顺序执行成员变量的初始化赋值和构造代码块。
    所以,首先构造代码块将执行,于是输出“2”。接着,之后的成员变量“a”将执行初始化赋值工作,值被设置为100;

  • 当成员变量和构造代码块初始化执行完毕后,就轮到了类构造器的执行,由此将首先输出3,然后是a=100;

  • 不同的是,在输出“b”的值得时候,显然此时静态变量“b”还没来得及完成初始化工作。
    所以,到目前为止,它的值仍然还是在准备阶段时初始化的值 0,所以会接着输出输出b=0;

  • 由此也完成了对象的实例初始化,“sInstance”也将进入解析阶段,将符号引用替换为指向堆内存中Test对象的直接引用。

  • 接着,按照顺序,继续执行静态代码块,输出1。然后静态变量“b”也将完成它的初始化,被赋值为200。
    到此所有的初始化工作都已完成,类便正式进入了“使用”阶段,由此staticMethod得以执行,输出4。
    (并且,我们也不难想到,如果在该方法内再次试图打印“b”的值,显然就会输出200了。)

也就是说,经过分析,正确来说这段程序的运行结果应该如下,而实际运行程序加以验证,也可以发现结果完全吻合:

2
3
a=100,b=0
1
4

1. SAP Business Application Studio 里创建一个基于 CAP 模型的最简单的 OData 服务 本文基于 SAP Cloud Application Programming 编程模型的 OData 服务开发教程的第一步,简述了 SAP Business Technology Platform 里 Business Application Studio 开发工具的打开方式,创建 Dev Space 以及 CAP 项目的创建步骤,以及如何通过 schema.cds 和 service.cds 分别创建 OData 模型和 OData 服务,最后通过 ports preview 打开 OData 服务预览页 阅读详情

相关推荐

北航论文模板:解决XeLaTeX中Font shape'TU/SimSun(1)/b/n' undefined(font) using 'TU/SimSun(1)/m/n' instead

北航论文模板:解决XeLaTeX中Font shape ‘TU/SimSun(1)/b/n’ undefined(font) using ‘TU/SimSun(1)/m/n’ instead 问题 问题描述: 在使用北航论文模板时,产生了warning:Font shape 'TU/SimSun(1)/b/n' undefined(font) using 'TU/SimSun(1)/m/n' i...

qq_39560620的博客 1万+

“过一会”等于“用不”

今天看书发现了这句话“Later equals never”,发现对自己十分受用。 无论是工作还是学习、生活,我似乎都深受其害。往往对自己“一会再做”的命令,其实就是对事情的推诿和搪塞,是懒惰的体现。 这是个技术博客,但是我希望以此文提醒自己。像爸爸说的,当你想到一件事情,就马上去做。...

chenbeiyou的博客 861

【UOJ170】Picks loves segment tree VIII

【题目链接】 点击打开链接 【前置阅读】 【BZOJ4695】最假女选手 【BZOJ3064】【TYVJ1518】CPU监控【UOJ169】【UR #11】元旦老人与数列 【思路要点】 前置阅读中的前两题中,我们分别对线段树的区间取最大/最小值和线段树的历史最大/最小值进行了研究。本题中,两类问题同时出现,因此我们

cz_xuyixuan的博客 1245

勒布朗法则

稍后等于永不!Later equals never 。 —— [ LeBlanc ]

webrtc 3020

从零构建物联网监控系统:STM32+迪文屏+WiFi模组的实战解析

本文详细解析了基于STM32微控制器、迪文串口屏和WiFi模组构建物联网监控系统的完整方案。从硬件选型、系统架构设计到数据交互实现,提供了包括迪文屏GUI开发、STM32通信协议、低功耗优化等实战经验,帮助开发者快速掌握物联网监控系统的核心技术。

docker7nomad的博客 727

告别零散脚本:用Playwright+Pytest+Yaml+Allure搭建可维护的UI自动化项目

UI自动化测试是软件质量保障的关键环节,其核心在于模拟用户操作,对Web应用界面进行功能验证。传统的脚本堆砌方式常因代码耦合度高、维护成本大而难以为继。工程化解决方案通过分离关注点,将测试逻辑、数据和配置解耦,从而显著提升项目的可读性、可维护性和可扩展性。其技术价值在于构建可持续运行的自动化资产,而非一次性脚本。典型的应用场景包括Web应用的回归测试、兼容性测试及CI/CD流水线中的质量门禁。本文以Playwright、Pytest、Yaml和Allure为核心技术栈,详细阐述了如何从项目架构、页面对象模型

weixin_42529328的博客 299

勒布朗法则( LeBlanc)

  看《代码整洁之道》看到了一个概念:勒布朗法则。   咦?这个不是NBA中的勒布朗·詹姆斯法则,当然NBA中针对一些球星的Bug表现也制定了一系列的法则,如乔丹法则(乔丹太过于强大)、奥尼尔法则(待在三秒区不出来,统治力太强)、哈登法则(太能碰瓷!)。    我们要说的是这个: 稍后等于永不!Later equals never 。 —— [ LeBlanc ]   这个很好地反映...

weixin_34306676的博客 1436

嵌入式开发必看:STM32 DMA双缓冲配置全攻略(含CubeMX教程)

本文详细解析了STM32 DMA双缓冲机制的原理与优势,并提供了基于CubeMX的完整配置教程。通过硬件自动切换两个缓冲区,实现DMA传输与CPU处理的真正并行,有效降低中断频率,提升高速数据采集、音频流处理等场景下的系统效率和实时性。

weixin_29212429的博客 173

稍后等于永久

稍后等于永久(later equals never.) ------LeBlanc

在路上... 1183

60、磁悬浮主动隔振系统的LQR控制研究

本研究针对磁悬浮主动隔振系统,提出了一种结合ANN建模、LQR控制与MPGA优化的综合控制方案。通过建立系统的动力学与状态空间模型,利用混合训练算法的ANN精确描述MSVI的非线性和滞后特性,并设计基于最小力传递率的LQR控制器。为优化LQR的Q和R矩阵,引入多群体遗传算法(MPGA),有效克服传统GA的早熟收敛问题,提升搜索效率与控制性能。仿真结果表明,在单频、扫频、随机及冲击激励下,该方法显著降低传递力幅值,隔振效果远优于被动系统。研究验证了所提方法的有效性,为磁悬浮隔振技术的实际应用奠定了基础,并展望

linux6sysadmin的博客 124

Hermes 教程 05:多 Agent 编排

本文介绍了Hermes v0.13.0中的四种多Agent协作模式:1)delegate_task快速子任务模式,适合2-3个独立并行任务;2)spawn子Agent模式,实现零成本并行多步流水线;3)tmux交互子Agent模式,支持长期自主任务;4)全新Kanban看板系统,提供持久化AI团队协作框架。各模式在隔离级别、时长、工具访问和适用场景上存在差异,其中spawn模式通过独立上下文窗口实现零上下文成本。文章详细说明了每种模式的使用方法、限制条件和典型应用场景,并提供了具体代码示例和可视化流程图。K

程序猿_三产 534

java的类加载机制的学习

简单说Java字节码就是 .class 后缀的文件,里面存在Java虚拟机执行的命令。由于Java是一门跨平台的编译型语言,所以可以适用于不同的平台,不同CPU的计算机,开发者只需要将自己的代码编译一次,就可以运行在不同平台的JVM中。甚至,开发者可以用类似Scala、Kotlin这样的语言编写代码,只要你的编译器能够将代码编译成 .class 文件,都可以在JVM虚拟机中运行。当然也可以理解的更广义一些--- 所有能够恢复成一个类并在JVM虚拟机里加载的字节序列。

dayouzi的博客 1063

基于深度学习YOLOv11的布匹缺陷检测系统(Python+PySide6界面+训练代码)

YOLOv11模型训练:根据布匹缺陷数据集训练YOLOv11模型,以识别布匹中的各种缺陷,如裂纹、污渍、破损等。PySide6界面设计:设计一个图形用户界面,帮助用户上传布匹图像,并显示模型的检测结果。模型优化与精度提升:通过数据增强、超参数调优等手段提高模型的准确性。本文介绍了如何基于YOLOv11实现布匹缺陷检测系统,并通过PySide6构建了一个图形用户界面。该系统能够自动检测布匹中的各种缺陷,具有较高的效率和精度。

斌擎科技 529

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

Java虚拟机的类加载机制是Java平台灵活性和可扩展性的关键所在。通过深入理解类加载的概念、过程、类加载器、双亲委派模型以及类的卸载与重加载,开发者可以更好地掌握Java程序的运行原理,并在必要时通过自定义类加载器和热部署等技术手段,实现高度灵活的应用架构。在Java虚拟机中,类的加载是指将class文件从硬盘读取到内存中,并创建对应的Class对象的过程。1. 使用自定义类加载器动态加载类。2. 自定义类加载器的实现与优化。3. 类的卸载与重加载策略。1. 系统提供的类加载器。四、类的卸载与重加载。

N201871643的博客 762

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

《深入理解 Java 虚拟机:类加载机制》

重要性程序运行基础:类加载是Java程序运行的第一步,它负责将类的字节码加载到内存中,并进行验证、准备和初始化等操作,为程序的执行做好准备。动态性支持:Java语言的动态性很大程度上依赖于类加载机制,通过类加载机制,Java程序可以在运行时动态地加载类、解析类和初始化类,从而实现程序的动态扩展和灵活性。相关概念类与类加载器:每个类都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,类加载器负责将类的字节码加载到内存中,而类则是程序的基本执行单元。类加载过程阶段。

weixin_51105979的博客 878

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

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

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来

SmallWinWin的博客 1552

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

深入理解Java虚拟机|JVM07-虚拟机类加载机制

深入理解Java虚拟机第7章 虚拟机类加载机制7.1概述7.2 类加载的时机7.3 类加载的过程7.3.1.加载7.3.2.验证:7.3.3.准备:7.3.4.解析:7.3.5.初始化7.4 类加载器7.4.1 类与类加载器7.4.2 双亲委派机制参考 第7章 虚拟机类加载机制 7.1概述 本章内容报考虚拟机如何加载Class文件?Class文件中的信息进入到虚拟机后会发生什么变化? 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类

yanghao201607030101的博客 281

深入理解Java虚拟机-第七章、虚拟机类加载机制

文章目录第七章、虚拟机类加载机制一、类加载的时机对类初始化的5个时机(有且只有这五个)被动引用的例子通过子类引用父类的静态变量,不会导致子类初始化通过数组定义来引用类,不会触发此类的初始化调用类的常量不会被初始化(static final)二、类加载的过程1、加载2、验证阶段验证步骤1、文件格式验证验证步骤2、元数据验证验证步骤3、字节码验证验证步骤4、符号引用验证3、准备阶段4、解析阶段5、 初...

weixin_38367817的博客 575

深入理解java虚拟机——类加载机制及双亲委派模型详解

深入理解java虚拟机——类加载机制 定义   类加载机制指的是:虚拟机将描述类的数据从class文件加载到内存中,对加载的数据进行验证,解析,初始化,最后得到虚拟机认可后转化为直接可以使用的java类型的过程   类加载机制一共有七个阶段:加载,验证,准备,解析,初始化,使用,卸载。其中的验证,准备,解析合称为连接阶段。   加载,验证,准备,初始化,卸载的顺序是确定是,另两个由动态绑定等情况可...

下雨了_简的博客 1万+

深入理解Java虚拟机:(二)Java虚拟机的类加载机制

一、概述 在上一篇我么了解到 Java 源文件经过编译后成 Class 文件,在 Class 文件中描述的各种信息,最终都需要加载到虚拟机中才能运行与使用。而虚拟机如何加载这些 Class 文件的?Class 文件进入到虚拟机中间又发生了什么?这就是本篇要讲的内容。 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行验证、准备、解析和初始化,最终形成可以被虚拟机这届使用的 Java ...

riemann_的博客 837

深入理解java虚拟机类加载机制,通俗易懂

java源文件经编译后以.class的文件形式存在本地磁盘上,在Class文件中描述的各类信息最终都需要加载到虚拟机中之后才能被运行和使用 1.类加载机制在jvm中的位置 1.类加载过程 虚拟机把类的数据从.class文件加载到内存,并对class文件中的数据进行校验、转换、解析、初始化等操作后,最终形成可以被虚拟机识别并使用的Class对象的过程就叫做“虚拟机的类加载”,主要包括为3大阶段。 ...

Alphr的博客 624

深入理解Java虚拟机(类加载机制)

类的初始化阶段是类加载过程的最后一个步骤,直到初始化阶段,JVM才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。相对于类加载过程的其他阶段,非数组类型的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的阶段。如果数组的组件类型(Component Type,指的是数组去掉一个维度的类型,注意和前面的元素类型区分开来)是引用类型。验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但却不是必须要执行的阶段,因为验证阶段只有通过或者不通过的差别。

月伴飞鱼 827

深入理解Java虚拟机——第7章 虚拟机类加载机制

第7章 虚拟机类加载机制第7章 虚拟机类加载机制7.1 概述7.2 类加载的时机7.3 类加载的过程7.3.1 加载7.3.2 验证7.3.3 准备7.3.4 解析7.3.5 初始化7.4 类加载器7.4.1 类与类加载器7.4.2 双亲委派模型7.4.3 破坏双亲委派模型7.5 Java 模块化系统7.5.1 模块的兼容性7.5.2 模块化下的类加载器 第7章 虚拟机类加载机制 7.1 概述 7.2 类加载的时机 7.3 类加载的过程 7.3.1 加载 7.3.2 验证 7.3.3 准备 7.3.4 解析

zaker123的博客 394

java虚拟机类加载机制---《深入理解java虚拟机》读书笔记

Java虚拟机类加载机制是把Class类文件加载到内存,并对Class文件中的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程。     java可动态扩展的语言特性就是依赖运行期动态加载和动态链接这个特点实现的。     类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备、解析3

xulu_258的专栏 2950

《深入理解 Java 虚拟机》读书笔记:虚拟机类加载机制

正文 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。 一、类加载的时机 1、类的生命周期 加载 -> 连接(验证、准备、解析) -> 初始化 -> 使用 -> 卸载 加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按这种顺序按部就班...

惊却一目 175

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

本文内容来源于《深入理解Java虚拟机》一书,非常推荐大家去看一下这本书。本系列其他文章:【深入理解Java虚拟机】Java内存区域模型、对象创建过程、常见OOM【深入理解Java虚拟机】垃圾回收机制1、类加载机制概述虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。在java中,类型的加载、连

开心阳 1万+
上一篇: 理解Android图像处理-拍照、单/多图选择器及图像优化
LaterEqualsNever
博客等级 码龄12年 297粉丝 61原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值