Trembling ! Java类的加载过程详解(加载验证准备解析初始化使用卸载)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Trembling ! Java类的加载过程详解(加载验证准备解析初始化使用卸载)

【1】类的生命周期

一个类从加载进内存到卸载出内存为止,一共经历7个阶段:


加载—>验证—>准备—>解析—>初始化—>使用—>卸载


其中,类加载包括5个阶段:


加载—>验证—>准备—>解析—>初始化


在类加载的过程中,以下3个过程称为连接:


验证—>准备—>解析


因此,JVM的类加载过程也可以概括为3个过程:


加载—>连接—>初始化


C/C++在运行前需要完成预处理、编译、汇编、连接。而在Java中,类加载(加载、连接、初始化)是在程序运行期间完成的。


在程序运行期间进行类加载会稍微增加程序的开销,但随之会带来更大的好处-----提高程序的灵活性。


Java语言的灵活性体现在它可以在运行期间动态扩展,所谓动态扩展就是在运行期间动态加载和动态连接。


【2】类加载的时机


① 类加载过程中每个步骤的顺序


我们已经知道,类加载的过程包括:加载、连接、初始化。连接又分为:验证、准备、解析。所以说类加载的过程一共分为5步:加载、验证、准备、解析、初始化。


其中加载、验证、准备、初始化的开始顺序是依次进行的,这些步骤开始之后的过程可能会有重叠。


而解析过程会发生在初始化过程中。


② 类加载过程中“初始化”开始的时机


JVM规范中只定义了类加载过程中初始化过程开始的时机,加载、连接过程都应该在初始化之前开始(解析除外)。这些过程具体在何时开始,JVM规范并没有定义,不同的虚拟机可以根据具体的需求自定义。


初始化开始的时机


在运行过程中遇到如下字节码指令时,如果类尚未初始化,那就要进行初始化:new、getstatic、putstatic、invokestatic等。这四个指定对应的Java代码场景是:

通过new创建对象;

读取、设置一个类的静态成员变量(不包括final修饰的静态变量);

调用一个类的静态成员函数,即静态方法;

使用java.lang.reflect进行反射调用的时候,如果类没有初始化,那就需要初始化。


当初始化一个类的时候,若其父类尚未初始化,那就先要让其父类初始化,然后再初始化本类。


当虚拟机启动时,虚拟机会首先初始化带有main方法的类,即主类。


③ 主动引用与被动引用


JVM规范中要求在程序运行过程中,“当且仅当”出现上述4个条件之一的情况才会初始化一个类。如果间接满足上述初始化条件是不会初始化类的。


其中,直接满足上述初始化条件的情况叫做主动引用;间接满足上述初始化过程的情况叫做被动引用。


那么,只有当程序在运行过程中满足主动引用的时候才会初始化一个类,若满足被动引用就不会初始化一个类。


④ 被动引用的场景示例示例一:

public class Fu{
  public static String name="柴毛毛";
  static{
    System.out.println("父类被初始化!");
  }
}
public class Zi extends Fu{
  static{
    System.out.println("子类被初始化!");
  }
}
public TestClass{
  public void static main(String[] args){
    System.out.println(Zi.name);
  }
}


测试结果:

父类被初始化!
柴毛毛



原因分析:


本示例看似满足初始化时机的第一条:当要获取某一个类的静态成员变量的时候如果该类尚未初始化,则对该类进行初始化。


但由于这个静态成员变量属于Fu类,Zi类只是间接调用Fu类中的静态成员变量,因此Zi类调用name属性属于间接引用。而Fu类调用name属性属于直接引用,由于JVM只初始化直接引用的类,因此只有Fu类被初始化。


这里需要注意的是,main方法在其他类中,如果在Zi类中执行main方法,JVM同样会初始化Zi类:

public class Zi extends Fu {
    static{
        System.out.println("子类被初始化!");
    }
    public static void main(String[] args){
        System.out.println(Zi.name);
    }
}


此时输出结果:

父类被初始化!
子类被初始化!
柴毛毛


示例二:

public class A {
    public static void main(String[] args){
        Fu[] arr = new Fu[10];
    }
}

输出结果:为空,并没有输出“父类被初始化!”。

原因分析:

这个过程看似满足初始化时机的第一条:遇到new创建对象时若类没被初始化,则初始化该类。

但现在通过new要创建的是一个数组对象,而非Fu类对象,因此也属于间接引用,不会初始化Fu类。


示例三:

public class Fu {
    public static final String name="柴毛毛";
    static{
        System.out.println("父类被初始化!");
    }
}
public class A {
    public static void main(String[] args){
        System.out.println(Fu.name);
    }
}

输出结果:

柴毛毛

原因分析:


本示例看似满足类初始化时机的第一个条件:获取一个类静态成员变量的时候若类尚未初始化则初始化类。


但是Fu类的静态成员变量被final修饰,它已经是一个常量。


被final修饰的常量在Java代码编译(java源文件编译成class字节码)的过程中就会被放入引用它的class文件的常量池中(这里是A的常量池)。所以程序在运行期间如果需要调用这个常量,直接去当前类的常量池中取,而不需要初始化这个类。


有说法称之为“常量传播优化”。




示例四:访问以下final修饰的static常量会触发类的初始化。

public class LFim {
    /*
     * 在复杂类型中只有字符串不会触发初始化过程!!
     */
    public static final String STRING="LangShen";
    //不会触发初始化过程!!,没有经历自动装箱,
    //字符串是编译期直接保存在常量池中的!!!
    //获取值时处理方法和它们不一样,它是一个常量表,
    //一个字符序列对应一个对象来获取!!!
    /*
     * 以下都会触发初始化过程!!
     * 
     * 这些初始化指的是类的初始化!!!
     */
    public static final ClassA CLASSA=null;
//自定义类,会触发初始化过程!,当然你就更别说new 一个对象了,new 一个也会触发初始化过程!!
    public static final Integer INTEGER=45;
    //会触发初始化过程!!,因为经历了自动装箱
    public static final Character CHARACTER='X';
    //会触发初始化过程!!因为经历了自动装箱
    public static final String STRING2=new String("456");
    //会触发初始化过程!!,是new 出来的!!!
    static{
        System.out.println("初始化静态代码块!!");
    }
}

⑤ 接口的初始化

接口和类都需要初始化,接口和类的初始化过程基本一样。

不同点在于:类初始化时,如果发现父类尚未被初始化,则先要初始化父类,然后再初始化自己;但接口初始化时,并不要求父接口已经全部初始化,只有程序在运行过程中用到父接口中的东西时,才初始化父接口。


【3】类加载的过程1—加载

通过之前的介绍可知,类加载过程共有5个步骤,分别是:加载、验证、准备、解析、初始化。其中,验证、准备、解析称为连接。下面详细介绍这5个过程JVM所做的工作。


“加载”是类加载过程的第一步。


① 加载的过程


在加载过程中,JVM主要做3件事:


通过一个类的全限定名来获取这个类的二进制字节流,即class文件:

在程序运行过程中,当要访问一个类时,若发现这个类尚未被加载,并满足类初始化时机的条件时,就根据要被初始化的这个类的全限定名找到该类的二进制字节流,开始加载过程。


将二进制字节流的存储结构转化为特定的数据结构,存储在方法区中;


在内存中创建一个java.lang.Class类型的对象:

接下来程序在运行过程中所有对该类的访问都通过这个类对象,也就是这个class类型的类对象是提供给外界访问该类的接口。


② 从哪里加载?


JVM规范对于加载过程给予了较大的宽松度。一般二进制字节流都从已经编译好的本地class文件中读取,此外还可以从以下地方读取:


从压缩包中读取

如:jar、War、Ear等

从其他文件中动态生成

如:从JSP文件中生成Class类

从数据库中读取

将二进制字节流存储进数据库中,然后在加载的时候从数据库中读取。有些中间件会这么做,用来实现代码在集群间分发。

从网络中获取

从网络中获取二进制字节流,典型的就是Applet。

③ 类和数组加载过程的区别数组也有类型,称为“数组类型”。如:

String[] str = new String[10];


这个数组的数组类型是Ljava.lang.String,而String只是这个数组中元素的类型。


当程序在运行过程中遇到new关键字创建一个数组时,由JVM直接创建数组类。再由类加载器创建数组中的元素类。


而普通类的加载由类加载器完成。既可以使用系统提供的引导类加载器,也可以使用用户自定义的类加载器。


④ 加载过程的注意点


JVM规范并未给出类在方法区中存放的数据结构

类完成加载后,二进制字节流就以特定的数据结构存储在方法区中,但存储的数据结构是由虚拟机自己定义的,JVM规范并没有指定。


JVM规范并没有指定Class对象存放的位置

在二进制字节流以特定格式存储在方法区以后,JVM会创建一个java.lang.Class类型的对象,作为本类的外部接口。既然是对象就应该存放在堆内存中,不过JVM规范并没有给出限制,不同的虚拟机根据自己的需求存放这个对象。HotSpot将Class对象存放在方法区。


加载阶段和连接阶段是交叉的

通过之前的介绍可知,类加载过程中每个步骤的开始顺序都有严格限制,但每个步骤的结束顺序没有限制。也就是说,类加载过程中,必须按照以下顺序开始:加载、验证、准备、解析、初始化,但结束顺序无所谓。因此由于每个步骤处理时间的长短不一,就会导致有些步骤会出现交叉。


【4】类加载的过程2—验证

验证阶段比较耗时,它非常重要但不一定必要,如果所运行的代码已经被反复使用和验证过,那么可以使用-Xverify:none参数关闭,以缩短类加载时间。



① 验证的目的


验证是为了确保二进制字节流中信息符合虚拟机规范,并没有安全问题。


② 为什么需要验证


虽然Java语言是一门安全语言,它能确保程序员无法访问数组边界以外的内存、避免让一个对象转换成任意类型,避免跳转到不存在的代码行;如果出现这些情况,编译无法通过。也就是说,Java语言的安全性是通过编译器来保证的。


但是我们知道,编译器(如javac)和虚拟机是两个独立的东西,虚拟机只认二进制字节流,它不会管所获得的二进制字节流是哪来的。当然,如果是编译器给它的,那就相对安全。但如果是从其他途径获得的,那么无法确保该二进制字节流是安全的。


通过上文可知,虚拟机规范中没有限制二进制字节流的来源,那么任意来源的二进制字节流虚拟机都能接受,为了防止字节流中有安全问题,因此需要验证。


③ 验证的过程


文件格式验证

这个阶段主要验证输入的二进制字节流是否符合class文件的结构规范。二进制字节流只有通过了本阶段的验证,才会被允许存入到方法区中。


本验证阶段是基于二进制字节流的,而后面的三个验证阶段都是在方法区中进行,并基于类特定的数据结构的。


通过上文可知,加载开始前,二进制字节流还没有进入方法区,而加载完成后,二进制字节流已经存入方法区。而在文件格式验证前,二进制字节流尚未进入方法区,文件格式验证通过之后才进入方法区。


也就是说,加载开始后,立即启动了文件格式验证,本阶段验证通过后,二进制字节流被转换成特定数据结构存储在方法区中,继而开始下阶段的验证和创建Class对象等操作。这个过程印证了:加载和验证是交叉进行的。


元数据验证

本阶段对方法区中的字节码描述信息进行语义分析,确保其符合Java语法规范。


字节码验证

本阶段是验证过程的最复杂的一个阶段。本阶段对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件。


符号引用验证

本阶段验证发生在解析阶段,确保解析能正常执行。

【5】类加载的过程3—准备和解析

① 准备


准备阶段完成两件事情:

  • 为已经在方法区中的类中的静态成员变量分配内存
    类的静态成员变量也存储在方法区中(如果是常量,则编译时存放进引用其的class的常量池中,加载时存入方法区的运行时常量池)。
  • 为静态成员变量设置初始值
    初始值为0、false、null等。


示例一:

public static String name="柴毛毛";


在准备阶段,JVM会在方法区中为name分配内存空间,并赋上初始值null。给name赋上“柴毛毛”是在初始化阶段完成的。

示例二:

public static final String name="柴毛毛";


final 用于声明属性、方法和类,分别表示属性一旦被分配内存空间就必须初始化并且以后不可变;方法一旦定义必须有实现代码并且子类里不可被覆盖;类一旦定义不能被定义为抽象类或是接口,因为不可被继承。


被final修饰的常量如果有初始值,那么在编译阶段就会将初始值存入constantValue属性(class文件的常量池)中,在准备阶段就将constantValue的值赋给该字段。如果没有初始值而又没有被static修饰的变量,如private final String seasonName;将在类构造函数中初始化。


当这个属性被修饰为final,而非static的时候,它属于类的实例对象的资源,当类被加载进内存的时候这个属性并没有给其分配内存空间,而只是定义了一个变量a,只有当类被实例化的时候这个属性才被分配内存空间,而实例化的时候同时执行了构造函数,所以属性被初始化了,也就符合了当它被分配内存空间的时候就需要初始化,以后不再改变的条件。


如果是同时被static和final修饰的变量,则只有两个地方初始化:


定义的时候赋值;

静态代码块里面赋值


② 解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。如果不执行,符号解析要等到字节码指令使用这个引用时才会进行。


【6】类加载的过程4—初始化

初始化阶段就是执行类构造器clinit()的过程(不是init())。把类中的变量初始化成合适的值。执行静态初始化程序,把静态变量初始化成指定的值


clinit()方法由编译器自动产生,收集类中static{}代码块中类变量赋值语句和类中静态成员变量的赋值语句。此时将会执行静态代码块和静态方法。

在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化。


初始化过程的注意点

  • clinit()方法中静态成员变量的赋值顺序是根据Java代码中静态成员变量的出现的顺序决定的。


  • 静态代码块能访问出现在静态代码块之前的静态成员变量,无法访问出现在静态代码块之后的成员变量。
  • 静态代码块能给出现在静态代码块之后的静态成员变量赋值。
    因为在准备阶段已经给静态成员变量进行了默认初始化。
public class Test{
  static{
    i=0;//给变量赋值可以正常编译通过
    System.out.print(i);//这句编译器会提示"非法向前引用"
  }
  static int i=1;
}

构造函数init()需要显示调用父类构造函数,而类的构造函数clinit()不需要调用父类的类构造函数,因为虚拟机会确保子类的clinit()方法执行前已经执行了父类的clinit()方法。

因此在虚拟机中第一个被执行的clinit()方法的类肯定是java.lang.Object。由于父类的clinit()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。


如果一个类/接口中没有静态代码块,也没有静态成员变量的赋值操作,那么编译器就不会生成clinit()方法。


接口也需要通过clinit()方法为接口中定义的静态成员变量显示初始化。


接口中不能使用静态代码块。


接口在执行clinit()方法前,虚拟机不会确保其父接口的clinit()方法被执行,只有当父接口中的静态成员变量被使用到时才会执行父接口的clinit()方法。


虚拟机会给clinit()方法加锁,因此当多条线程同时执行某一个类的clinit()方法时,只有一个方法会被执行,其他的方法都被阻塞。并且,只要有一个clinit()方法执行完,其它的clinit()方法就不会再被执行。因此,在同一个类加载器下,同一个类只会被初始化一次。


【7】类加载器

① 类与类加载器

  • 类加载器的作用
  • 将class文件加载进JVM的方法区,并在方法区中创建一个java.lang.Class对象作为外界访问这个类的接口。
  • java.lang.Class对象存放在哪里,JVM规范并未严格研究,具体看厂商实现。HotSpot将java.lang.Class对象存放在方法区。


类与加载器的联系

比较两个类是否相等,只有当这两个类由同一个加载器加载才有意义。否则,即使同一个class文件被不同的类加载器加载,那这两个类必定不同,即通过类的Class对象的equals执行的结果必定为false。


② 类加载器种类

JVM 提供如下三种类加载器:


启动类加载器(BootStrap ClassLoader,又称根加载器)

每次执行 java 命令时都会使用该加载器为虚拟机加载核心类。该加载器是由 native code 实现,而不是 Java 代码,加载类的路径为 <JAVA_HOME>/jre/lib。特别的 <JAVA_HOME>/jre/lib/rt.jar 中包含了 sun.misc.Launcher 类, 而 sun.misc.Launcher$ExtClassLoader 和 sun.misc.Launcher$AppClassLoader 都是 sun.misc.Launcher的内部类,所以拓展类加载器和系统类加载器都是由启动类加载器加载的。


扩展类加载器(Extension ClassLoader)

用于加载拓展库中的类。拓展库路径为<JAVA_HOME>/jre/lib/ext/。实现类为 sun.misc.Launcher$ExtClassLoader。


应用程序类加载器(System ClassLoader)

负责加载用户classpath下的class文件,又叫系统加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。实现类为 sun.misc.Launcher$AppClassLoader


此外还有用户自定义类加载器,继承自System ClassLoader。


类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载—双亲委派模型。


③ 类加载图示

连接过程包括验证、准备和解析。


④ 双亲委派模型


工作过程

如果一个类加载器收到了加载类的请求,它首先将请求交由父类加载器加载;若父类加载器加载失败,当前类加载器才会自己加载类。


作用

像java.lang.Object这些存放在rt.jar中的类,无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载,从而使得不同加载器加载的Object类都是同一个。


原理

双亲委派模型的代码在java.lang.ClassLoader类中的loadClass函数中实现,其逻辑如下:


首先检查类是否被加载;

若未加载,则调用父类加载器的loadClass方法;

若该方法抛出ClassNotFoundException异常,表示父类加载器无法加载,则当前类加载器调用findClass加载类;

若父类加载器可以加载,则直接返回Class对象。


源码示例如下:

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

【8】类/对象的静态成员变量与非静态成员变量

① 静态成员变量


通过上面可知,类的静态成员变量在准备阶段进行内存分配并默认初始化,在初始化阶段进行显示初始化。


静态变量在以后的创建对象的时候不再初始化,但是对象可以再次对静态变量进行赋值,该赋值操作将会反映到方法区中类的数据结构中。即,方法区中存储静态变量的值也会随之发生改变。


同样,修改类中静态变量的值会反映到已经创建好的对象上面。


示例如下:

public class ClassA {
    private int a =10;
    private static int b=20;
    public int getA() {
        return a;
    }
    public void setA(int a) {
        this.a = a;
    }
    public static int getB() {
        return b;
    }
    public static void setB(int b) {
        ClassA.b = b;
    }
}

测试类如下

public class TestClassA {
    public static void main(String[] args) throws InterruptedException {
    // 先实例化一个对象
        ClassA classA = new ClassA();
    // 另起一个县城
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
              //获取实例化对象的静态变量值
                System.out.println("thread:"+classA.getB());
                //重新设置实例化对象的静态变量值并获取
                classA.setB(30);
                System.out.println("thread:"+classA.getB());
                // 直接通过类设置类的静态变量值
                ClassA.setB(50);
                // 这里,获取实例化对象的静态变量值
                System.out.println("thread:"+classA.getB());
            }
        });
        thread.start();
        //再来一个对象
        ClassA classA2 = new ClassA();
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
              // 首先打印对象的静态变量值
                System.out.println("thread2:"+classA2.getB());
                // 设置头一个对象的静态变量值
                classA.setB(40);
                // 打印类中的静态变量值
                System.out.println("thread2:"+ClassA.getB());
            }
        });
        thread2.start();
    }
}

输出结果:

thread:20
thread:30
thread:50
thread2:50
thread2:40

② 非静态成员变量


非静态成员变量只有在实例化对象的时候才会分配内存并赋值,非静态成员变量随对象一起保存在堆中。


每个实例化对象的非静态成员变量只属于自己,值修改也只 针对自己,并不会影响到其他对象或者反映到方法区中类的数据结构上面。


每次实例化对象的时候,非静态变量的值都为最初存放在方法区中的值,如这里private int a =10;。每次新建对象,对象的a都为10。


测试类如下:

public class TestClassA {
    public static void main(String[] args) throws InterruptedException {
        ClassA classA = new ClassA();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread:"+classA.getA());
                classA.setA(30);
                System.out.println("thread:"+classA.getA());
            }
        });
        thread.start();
        ClassA classA2 = new ClassA();
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread2:"+classA2.getA());
                classA2.setA(40);
                System.out.println("thread2:"+classA2.getA());
            }
        });
        thread2.start();
    }
}

输出如下:

thread:10
thread:30
thread2:10
thread2:40

另外,需要注意的是类的非静态成员变量中,其中基本类型(非包装类型)变量的值,即字面量是保存在class文件的常量池中的,在加载的时候会被存放在方法区的运行时常量池。

public class A{
  private int a=10;
  private int b=1000;
  private Integer c=20;
  private Integer d=2000;
  //...
}

其中,10,1000都会被放在常量池中;而包装类型20和2000,其中20也放在常量池中,而且是一个Integer(20)对象。2000不在-128-127范围内,是一个新建对象!!!

目录
相关文章
|
7天前
|
存储 安全 Java
java.util的Collections类
Collections 类位于 java.util 包下,提供了许多有用的对象和方法,来简化java中集合的创建、处理和多线程管理。掌握此类将非常有助于提升开发效率和维护代码的简洁性,同时对于程序的稳定性和安全性有大有帮助。
34 17
|
3天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
26 4
|
3天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
11 2
|
8天前
|
存储 安全 Java
如何保证 Java 类文件的安全性?
Java类文件的安全性可以通过多种方式保障,如使用数字签名验证类文件的完整性和来源,利用安全管理器和安全策略限制类文件的权限,以及通过加密技术保护类文件在传输过程中的安全。
|
12天前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
|
12天前
|
Java API Maven
如何使用 Java 字节码工具检查类文件的完整性
本文介绍如何利用Java字节码工具来检测类文件的完整性和有效性,确保类文件未被篡改或损坏,适用于开发和维护阶段的代码质量控制。
|
11天前
|
存储 Java 编译器
java wrapper是什么类
【10月更文挑战第16天】
19 3
|
14天前
|
Java 程序员 测试技术
Java|让 JUnit4 测试类自动注入 logger 和被测 Service
本文介绍如何通过自定义 IDEA 的 JUnit4 Test Class 模板,实现生成测试类时自动注入 logger 和被测 Service。
20 5
|
15天前
|
Java
在Java多线程编程中,实现Runnable接口通常优于继承Thread类
【10月更文挑战第20天】在Java多线程编程中,实现Runnable接口通常优于继承Thread类。原因包括:1) Java只支持单继承,实现接口不受此限制;2) Runnable接口便于代码复用和线程池管理;3) 分离任务与线程,提高灵活性。因此,实现Runnable接口是更佳选择。
27 2
|
15天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
27 2

推荐镜像

更多