Java JVM类加载阶段 双亲委派模式

类加载阶段

加载

将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:

  • _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
  • _super 即父类
  • _fields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法表

如果这个类还有父类没有加载,先加载父类

加载和链接可能是交替运行的

instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中,可以通过前面介绍的 HSDB 工具查看

image

链接

验证

验证类是否符合 JVM规范,安全性检查

用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行

image

准备

为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
解析

将常量池中的符号引用解析为直接引用

public class Load2 {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        ClassLoader classloader = Load2.class.getClassLoader();
        // loadClass 方法不会导致类的解析和初始化
        Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
        // 这里就会解析 C类了,C类声明了D类的成员变量,所以也会加载D类
        // new C();
        System.in.read();
    }
}

class C {
    D d = new D();
}

class D {
}

初始化

<cinit>()V 方法

初始化即调用 <cinit>()V ,虚拟机会保证这个类的『构造方法』的线程安全

发生的时机

概括得说,类初始化是【懒惰的】

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致类初始化的情况

  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass 方法
  • Class.forName 的参数 2 为 false 时
class A {
    static int a = 0;
    static {
        System.out.println("a init");
    }
}

class B extends A {
    final static double b = 5.0;
    static boolean c = false;
    static {
        System.out.println("b init");
    }
}

验证(实验时请先全部注释,每次只执行其中一个)

public class Load3 {
    static {
        System.out.println("main init");
    }

    public static void main(String[] args) throws ClassNotFoundException {
        // 1. 静态常量(基本类型和字符串)不会触发初始化
        System.out.println(B.b);
        // 2. 类对象.class 不会触发初始化
        System.out.println(B.class);
        // 3. 创建该类的数组不会触发初始化
        System.out.println(new B[0]);
        // 4. 不会初始化类 B,但会加载 B、A
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        cl.loadClass("cn.itcast.jvm.t3.B");
        // 5. 不会初始化类 B,但会加载 B、A
        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
        Class.forName("cn.itcast.jvm.t3.B", false, c2);

        // 1. 首次访问这个类的静态变量或静态方法时
        System.out.println(A.a);
        // 2. 子类初始化,如果父类还没初始化,会引发
        System.out.println(B.c);
        // 3. 子类访问父类静态变量,只触发父类初始化
        System.out.println(B.a);
        // 4. 会初始化类 B,并先初始化类 A
        Class.forName("cn.itcast.jvm.t3.B");
    }
}
懒惰初始化单例模式
public final class Singleton {
    private Singleton() { }
    // 内部类中保存单例
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }

    // 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

以上的实现特点是:

  • 懒惰实例化
  • 初始化时的线程安全是有保障的

类加载器

以 JDK 8 为例:

名称加载哪的类说明
Bootstrap ClassLoaderJAVA_HOME/jre/lib无法直接访问
Extension ClassLoaderJAVA_HOME/jre/lib/ext上级为 Bootstrap,显示为 null
Application ClassLoaderclasspath上级为 Extension
自定义类加载器自定义上级为 Application

启动类加载器

创建一个类:

package cn.itcast.jvm.t3.load;
public class F {
    static {
        System.out.println("bootstrap F init");
    }
}

执行:

package cn.itcast.jvm.t3.load;
public class Load5_1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
        System.out.println(aClass.getClassLoader());
    }
}

输出:

E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5
bootstrap F init
null

设置JVM参数:

-Xbootclasspath 表示设置 bootclasspath,其中 /a:. 表示将当前目录追加至 bootclasspath 之后

可以用这个办法替换核心类

  • java -Xbootclasspath:
  • java -Xbootclasspath/a:<追加路径>
  • java -Xbootclasspath/p:<追加路径>

扩展类加载器

package cn.itcast.jvm.t3.load;
public class G {
    static {
        System.out.println("classpath G init");
    }
}

执行:

public class Load5_2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
        System.out.println(aClass.getClassLoader());
    }
}

输出:

classpath G init
sun.misc.Launcher$AppClassLoader@18b4aac2

写一个同名的类

package cn.itcast.jvm.t3.load;
public class G {
    static {
        System.out.println("ext G init");
    }
}

打个 jar 包

E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)

将 jar 包拷贝到 JAVA_HOME/jre/lib/ext

重新执行 Load5_2

输出

ext G init
sun.misc.Launcher$ExtClassLoader@29453f44

没打jar包的时候,用的是应用类加载器,打了jar包后用的是扩展类加载器,并且只加载了jar包中的类,没有加载应用中自己写的类,类加载时会先看上级有没有加载过,有的话就不会再加载了。

双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则

这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查该类是否已经加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 2. 有上级的话,委派上级 loadClass
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }

            if (c == null) {
                long t1 = System.nanoTime();
                // 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
                c = findClass(name);
                // 5. 记录耗时
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }

        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

例如:

public class Load5_3 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Load5_3.class.getClassLoader().loadClass("cn.itcast.jvm.t3.load.H");
        System.out.println(aClass.getClassLoader());
    }
}

执行流程为:

  1. sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
  2. sun.misc.Launcher$AppClassLoader // 2 处,委派上级 sun.misc.Launcher$ExtClassLoader.loadClass()
  3. sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
  4. sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader查找
  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
  6. sun.misc.Launcher$ExtClassLoader // 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到 sun.misc.Launcher$AppClassLoader的 // 2 处
  7. 继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的 findClass 方法,在classpath 下查找,找到了

线程上下文类加载器

在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?

让我们追踪一下源码:

public class DriverManager {

// 注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

// 初始化驱动
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

先不看别的,看看 DriverManager 的类加载器:

System.out.println(DriverManager.class.getClassLoader());

打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

继续看 loadInitialDrivers() 方法:

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }

    // 1)使用 ServiceLoader 机制加载驱动,即 SPI
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
                // Do nothing
            }
            return null;
        }
    });
    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    // 2)使用 jdbc.drivers 定义的驱动名加载驱动
    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
            Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

image

这样就可以使用

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
    iter.next();
}

来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet 初始化器
  • Spring 容器
  • Dubbo(对 SPI 进行了扩展)

接着看 ServiceLoader.load 方法:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类LazyIterator 中:

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();

    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,"Provider " + cn + " not found");
    }

    if (!service.isAssignableFrom(c)) {
        fail(service, "Provider " + cn + " not a subtype");
    }

    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service, "Provider " + cn + " could not be instantiated", x);
    }
    throw new Error(); // This cannot happen
}

自定义类加载器

问问自己,什么时候需要自定义类加载器

  1. 想加载非 classpath 随意路径中的类文件
  2. 都是通过接口来使用实现,希望解耦时,常用在框架设计
  3. 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

  1. 继承 ClassLoader 父类
  2. 要遵从双亲委派机制,重写 findClass 方法,注意不是重写 loadClass 方法,否则不会走双亲委派机制
  3. 读取类文件的字节码
  4. 调用父类的 defineClass 方法来加载类
  5. 使用者调用该类加载器的 loadClass 方法

示例:

准备好两个类文件放入 E:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下:

image

image

image

输出 : true false

两个类对象相等的前提是类加载器也相等

运行期优化

即时编译

分层编译

(TieredCompilation)

public class JIT1 {
    public static void main(String[] args) {
        for (int i = 0; i < 200; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                new Object();
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\n",i,(end - start));
        }
    }
}

部分输出:

0 96426
1 52907
2 44800
3 119040
4 65280
5 47360
6 45226
7 47786
59 57600
60 83200
61 7024204
62 49493
63 20907
64 20907
65 20053
66 20906
67 20907
132 20053
133 15360
134 136533
135 43093
136 853
137 853
138 853
198 853
199 854

JVM 将执行状态分成了 5 个层次:

  • 0 层,解释执行(Interpreter)
  • 1 层,使用 C1 即时编译器编译执行(不带 profifiling)
  • 2 层,使用 C1 即时编译器编译执行(带基本的 profifiling)
  • 3 层,使用 C1 即时编译器编译执行(带完全的 profifiling)
  • 4 层,使用 C2 即时编译器编译执行

profifiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等

即时编译器(JIT)与解释器的区别

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT 会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由来),优化之

刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析,再运行刚才的示例观察结果

参考资料:https://docs.oracle.com/en/java/javase/12/vm/java-hotspot-virtual-machine-performance-enhancements.html#GUID-D2E3DC58-D18B-4A6C-8167-4A1DFB4888E4

方法内联

(Inlining)

private static int square(final int i) {
    return i * i;
}
System.out.println(square(9));

如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置:

System.out.println(9 * 9);

还能够进行常量折叠(constant folding)的优化

System.out.println(81);

实验:

public class JIT2 {

    // -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印inlining 信息
    // -XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining
    // -XX:+PrintCompilation 打印编译信息

    public static void main(String[] args) {
        int x = 0;
        for (int i = 0; i < 500; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                x = square(9);
            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\t%d\n",i,x,(end - start));
        }
    }

    private static int square(final int i) {
        return i * i;
    }
}
字段优化

JMH 基准测试请参考:http://openjdk.java.net/projects/code-tools/jmh/

创建 maven 工程,添加依赖如下

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>${jmh.version}</version>
</dependency>

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>${jmh.version}</version>
    <scope>provided</scope>
</dependency>

编写基准测试代码:

package test;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
public class Benchmark1 {

    int[] elements = randomInts(1_000);

    private static int[] randomInts(int size) {
        Random random = ThreadLocalRandom.current();
        int[] values = new int[size];
        for (int i = 0; i < size; i++) {
            values[i] = random.nextInt();
        }
        return values;
    }

    @Benchmark
    public void test1() {
        for (int i = 0; i < elements.length; i++) {
            doSum(elements[i]);
        }
    }

    @Benchmark
    public void test2() {
        int[] local = this.elements;
        for (int i = 0; i < local.length; i++) {
            doSum(local[i]);
        }
    }

    @Benchmark
    public void test3() {
        for (int element : elements) {
            doSum(element);
        }
    }

    static int sum = 0;

    @CompilerControl(CompilerControl.Mode.INLINE)
    static void doSum(int x) {
        sum += x;
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
            .include(Benchmark1.class.getSimpleName())
            .forks(1)
            .build();
        new Runner(opt).run();
    }
}

首先启用 doSum 的方法内联,测试结果如下(每秒吞吐量,分数越高的更好):

Benchmark             Mode     Samples     Score         Score error     Units
t.Benchmark1.test1    thrpt     5         2420286.539     390747.467     ops/s
t.Benchmark1.test2    thrpt     5         2544313.594     91304.136      ops/s
t.Benchmark1.test3    thrpt     5         2469176.697     450570.647     ops/s

接下来禁用 doSum 方法内联

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) {
    sum += x;
}

测试结果如下:

Benchmark             Mode     Samples     Score         Score error     Units
t.Benchmark1.test1    thrpt     5         296141.478     63649.220       ops/s
t.Benchmark1.test2    thrpt     5         371262.351     83890.984       ops/s
t.Benchmark1.test3    thrpt     5         368960.847     60163.391       ops/s

在刚才的示例中,doSum 方法是否内联会影响 elements 成员变量读取的优化:

如果 doSum 方法内联了,刚才的 test1 方法会被优化成下面的样子(伪代码):

@Benchmark
public void test1() {
    // elements.length 首次读取会缓存起来 -> int[] local
    for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
        sum += elements[i]; // 1000 次取下标 i 的元素 <- local
    }
}

可以节省 1999 次 Field 读取操作

但如果 doSum 方法没有内联,则不会进行上面的优化

反射优化

package cn.itcast.jvm.t3.reflect;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Reflect1 {
    public static void foo() {
        System.out.println("foo...");
    }

    public static void main(String[] args) throws Exception {
        Method foo = Reflect1.class.getMethod("foo");
        for (int i = 0; i <= 16; i++) {
            System.out.printf("%d\t", i);
            foo.invoke(null);
        }
        System.in.read();
    }
}

foo.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现

package sun.reflect;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import sun.reflect.misc.ReflectUtil;

class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method method) {
        this.method = method;
    }

    public Object invoke(Object target, Object[] args)
            throws IllegalArgumentException, InvocationTargetException {
        // inflationThreshold 膨胀阈值,默认 15
        if (++this.numInvocations > ReflectionFactory.inflationThreshold()
                && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
            // 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
            MethodAccessorImpl generatedMethodAccessor = (MethodAccessorImpl)
                (new MethodAccessorGenerator())
                    .generateMethod(
                        this.method.getDeclaringClass(),
                        this.method.getName(),
                        this.method.getParameterTypes(),
                        this.method.getReturnType(),
                        this.method.getExceptionTypes(),
                        this.method.getModifiers()
                    );
            this.parent.setDelegate(generatedMethodAccessor);
        }

        // 调用本地实现
        return invoke0(this.method, target, args);
    }

    void setParent(DelegatingMethodAccessorImpl parent) {
        this.parent = parent;
    }

    private static native Object invoke0(Method method, Object target, Object[] args);
}

当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到类名为 sun.reflflect.GeneratedMethodAccessor1

可以使用阿里的 arthas 工具:

java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.1.1
[INFO] Found existing java process, please choose one and hit RETURN.
* [1]: 13065 cn.itcast.jvm.t3.reflect.Reflect1

选择 1 回车表示分析该进程

image

再输入【jad + 类名】来进行反编译

image

image

注意

通过查看 ReflectionFactory 源码可知 sun.reflflect.noInflflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)

sun.reflflect.inflflationThreshold 可以修改膨胀阈值

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/348759.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

WWDG喂狗

3F 是0111111 40 是1000000 0X7F 127 0X5F 95 127-9532 注意:中断是在0x40,在0x40喂狗则程序不会复位 在0x5F之前喂狗会复位,减小到63以下也会复位 在0x5F与0x3F之间喂狗会继续执行,不会复位 WWDG_HandleTypeDef WWDG_Handler; //窗口看门狗句柄//初始化窗口看门狗…

2024 高级前端面试题之 HTML 「精选篇」

该内容主要整理关于 HTML 的相关面试题&#xff0c;其他内容面试题请移步至 「最新最全的前端面试题集锦」 查看。 HTML模块精选篇 1. 如何理解HTML语义化2. H5的新特性有哪些3. 说一下 HTML5 Drag API4. iframe有那些缺点5. 如何实现浏览器内多个标签页之间的通信6. 简述一下s…

EF core连接数据库的前期完整配置流程-开发环境搭建

EF core连接数据库完整流程-开发环境搭建 前置&#xff1a;.net6 core webapi不勾选任何配置 声明&#xff1a;这里是以两个配置类来做的&#xff0c;一个T_Books表&#xff0c;一个T_Person表 Book&#xff1a;创建属性及类型 BookConfig&#xff1a;对创建的进行属性数据表…

坚持刷题 | 平衡二叉树

文章目录 题目考察点代码实现实现总结对实现进一步改进扩展提问 坚持刷题&#xff0c;老年痴呆追不上我&#xff0c;今天继续二叉树&#xff1a;平衡二叉树 题目 110.平衡二叉树 考察点 递归能力&#xff1a; 能否使用递归来解决问题。树的基本操作&#xff1a;能否正确地访…

【算法练习】leetcode算法题合集之动态规划篇

普通动规系列 LeetCode343. 整数拆分 LeetCode343. 整数拆分 将10的结果存在索引为10的位置上&#xff0c;需要保证数组长度是n1&#xff0c;索引的最大值是n&#xff0c;索引是从0开始的。 n的拆分&#xff0c;可以拆分为i和n-i&#xff0c;当然i可以继续拆分。而且拆分为n-…

Vue基础知识

Vue Vue基础知识 v-bind:动态绑定属性值 Vue 修改&#xff0c;标签内也修改 在methods 中可以定义很多函数 在 data 中可以定义很多变量 v-if / v-show&#xff1a;对符合条件的元素进行展示 v-for:把数据遍历出现在网页中 案例 <!DOCTYPE html><html lang"e…

【论文阅读笔记】Towards Universal Unsupervised Anomaly Detection in Medical Imaging

Towards Universal Unsupervised Anomaly Detection in Medical Imaging arxiv&#xff0c;19 Jan 2024 【开源】 【核心思想】 本文介绍了一种新的无监督异常检测方法—Reversed Auto-Encoders (RA)&#xff0c;旨在提高医学影像中病理检测的准确性和范围。RA通过生成类似健…

C语言关键字

关键字的分类 C语言一共多少个关键字呢&#xff1f;一般的书上&#xff0c;都是32个,但是这个都C90(C89) 的标准。其实 C99 后又新增了5个关键字。不过&#xff0c;目前主流的编译器&#xff0c;对 C99 支持的并不好&#xff0c;默认使用 C90 &#xff0c;即&#xff0c;认为3…

【大数据】Flink 中的数据传输

Flink 中的数据传输 1.基于信用值的流量控制2.任务链接 在运行过程中&#xff0c;应用的任务会持续进行数据交换。TaskManager 负责将数据从发送任务传输至接收任务。它的网络模块在记录传输前会先将它们收集到 缓冲区 中。换言之&#xff0c;记录并非逐个发送的&#xff0c;而…

活字格V9获取图片失败bug,报错404,了解存储路径,已改为批量上传和批量获取

项目场景&#xff1a; 问题描述 原因分析&#xff1a; 解决方案&#xff1a; 完成了批量上传功能&#xff0c;这插件真的很方便 于是写了个批量获取附件的js代码&#xff0c;我真厉害 项目场景&#xff1a; 活字格V9版本获取图片链接Upload 【9.0.103.0】图片上传的存储路…

java web 研究生信息管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 java Web研究生信息管理系统是一套完善的java web信息管理系统 &#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境 为TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为My…

Keycloak - docker 运行 前端集成

Keycloak - docker 运行 & 前端集成 这里的记录主要是跟我们的项目相关的一些本地运行/测试&#xff0c;云端用的 keycloak 版本不一样&#xff0c;不过本地我能找到的最简单的配置是这样的 docker 配置 & 运行 keycloak keycloak 有官方(Red Hat Inc.)的镜像&#…

助力工业产品质检,基于YOLOv7【tiny/l/x】不同系列参数模型开发构建智能PCB电路板质检分析系统

AI助力工业质检智能生产制造已经有很多成功的实践应用了&#xff0c;在我们前面的系列博文中也有很多对应的实践&#xff0c;感兴趣的话可以自行移步阅读前面的博文即可&#xff1a; 《助力质量生产&#xff0c;基于目标检测模型MobileNetV2-YOLOv3-Lite实现PCB电路板缺陷检测…

python:socket基础操作(4)-《tcp客户端基础》

tcp就和udp不一样了&#xff0c;tcp是客户端和服务器端&#xff0c;如果想通过tcp发送数据&#xff0c;要先让tcp进行连接服务器端 tcp客户端 先让服务器端进行启动 import socketdef main():# 创建套接字tcp_client_socket socket.socket(socket.AF_INET,socket.SOCK_STREAM…

【原理图PCB专题】OrCAD Capture CIS关闭开始界面

17.4版本 在打开OrCAD Capture CIS时会发现打开Start Page页面&#xff0c;那么如何将他关闭再也不看这个界面呢&#xff1f; 在窗口中输入SetOptionBool EnableStartPage 0 回车 重启软件后就再也不会弹出Start Page页面 如果没有发现Command Window那么将菜单栏view->C…

Cesium绘制流动管线

目录 一、第一种方式 二、第二种方式 1.安装gsap 2.引入 一、第一种方式 使用viewer.clock Clock文档 viewer.clock文档 vec4 colorImage texture2D(image, vec2(fract(st.s - time), st.t));这里&#xff0c;因为使用的是高版本的cesium1.108,直接写texture2D会报错&am…

计数指针:shared_ptr (共享指针)与函数 笔记

推荐B站视频&#xff1a; 4.shared_ptr计数指针_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV18B4y187uL?p4&vd_sourcea934d7fc6f47698a29dac90a922ba5a3 5.shared_ptr与函数_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV18B4y187uL?p5&vd_sourcea…

JavaScript基础之输入输出与变量常量详解

输入和输出 输出和输入也可理解为人和计算机的交互&#xff0c;用户通过键盘、鼠标等向计算机输入信息&#xff0c;计算机处理后再展示结果给用户&#xff0c;这便是一次输入和输出的过程。 举例说明&#xff1a;如按键盘上的方向键&#xff0c;向上/下键可以滚动页面&#x…

C语言实现插入排序算法(附带源代码)

插入排序 插入排序&#xff08;英语&#xff1a;Insertion Sort&#xff09;是一种简单直观的排序算法。它的工作原理是通过构建有序序列&#xff0c;对于未排序数据&#xff0c;在已排序序列中从后向前扫描&#xff0c;找到相应位置并插入。插入排序在实现上&#xff0c;通常…

opencv012 滤波器04 中值滤波,双边滤波

中值滤波 取中位数&#xff0c;可以处理椒盐噪音 CV自带medianBlur函数dst cv2.medianBlur(src, ksize) 参数说明&#xff1a;1.src: 需要滤波的图片&#xff1b;2.ksize&#xff1a;核大小&#xff0c;必须是比1大的奇数【举个例子&#xff1a;3&#xff0c;5&#xff0c;7……