Java内存管理-掌握自定义类加载器的实现(七)

勿在流沙筑高台,出来混迟早要还的。做一个积极的人编码、改bug、提升自己我有一个乐园,面向编程,春暖花开!上一篇分析了ClassLoader的类加载相关的核心源码,也简单介绍了ClassLoader的设计思想,读源码相对来说是比较枯燥的,还是这个是必须要走的过程,学习源码中的一些思想,一些精髓,看一下大神级人物是怎么写出那么牛逼的代码。我们能够从中学到一点点东西,那也是一种进步和成长了…

大家好,又见面了,我是全栈君。

做一个积极的人

编码、改bug、提升自己

我有一个乐园,面向编程,春暖花开!

推荐阅读

第一季

0、Java的线程安全、单例模式、JVM内存结构等知识梳理
1、Java内存管理-程序运行过程(一)
2、Java内存管理-初始JVM和JVM启动流程(二)
3、Java内存管理-JVM内存模型以及JDK7和JDK8内存模型对比总结(三)
4、Java内存管理-掌握虚拟机类加载机制(四)
5、Java内存管理-掌握虚拟机类加载器(五)
6、Java内存管理-类加载器的核心源码和设计模式(六)
7、Java内存管理-掌握自定义类加载器的实现(七)
第一季总结:由浅入深JAVA内存管理 Core Story

第二季

8、Java内存管理-愚人节new一个对象送给你(八)
【福利】JVM系列学习资源无套路赠送
9、Java内存管理-”一文掌握虚拟机创建对象的秘密”(九)
10、Java内存管理-你真的理解Java中的数据类型吗(十)
11、Java内存管理-Stackoverflow问答-Java是传值还是传引用?(十一)
12、Java内存管理-探索Java中字符串String(十二)

实战

一文学会Java死锁和CPU 100% 问题的排查技巧

分享一位老师的人工智能教程。零基础!通俗易懂!风趣幽默!
大家可以看看是否对自己有帮助,点击这里查看【人工智能教程】。接下来进入正文。

勿在流沙筑高台,出来混迟早要还的。

上一篇分析了ClassLoader的类加载相关的核心源码,也简单介绍了ClassLoader的设计思想,读源码相对来说是比较枯燥的,还是这个是必须要走的过程,学习源码中的一些思想,一些精髓,看一下大神级人物是怎么写出那么牛逼的代码。我们能够从中学到一点点东西,那也是一种进步和成长了。本文基于上一篇文章内容,手把手写一个自定义类加载器,并且通过一些简单的案例(场景)让我们更加细致和静距离的体验类加载器的神奇之处。

本文地图:

Java内存管理-掌握自定义类加载器的实现(七)

一、上文回顾扩展

上一篇介绍了ClassLoader中loadClass()内的一些源码,也介绍了一些核心的API,其中有一个getParent()是没有做说明的,这里简单说明一下,方便快速理解后续的内容。

// 返回委托的父类加载器。 
ClassLoader getParent() 

这个方法是获取父类加载器,那么父类加载器是怎么初始化的。上一文也提到了,虽然类加载器的加载模式为双亲委派模型,但是真正在实现上并不是使用继承方式。

看下面源码,sun.misc.Launcher类是java的入口,在启动java应用的时候会首先创建Launcher类,创建Launcher类的时候回准备应用程序运行中需要的类加载器。

/** * 删除了一些其他代码,方便阅读 **/
public class Launcher { 
   
    // 可自行打印一下 bootClassPath ,看输入内容是什么?
    private static String bootClassPath = System.getProperty("sun.boot.class.path");// ①
    private static Launcher launcher = new Launcher(); // ②
    private ClassLoader loader;
    public Launcher() { 
   
        Launcher.ExtClassLoader var1; 
        try { 
   
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) { 
   
            throw new InternalError("Could not create extension class loader", var10);
        }
        try { 
   
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) { 
   
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);// ③
      	 // 省略其他代码....
    }
}

第一:Launcher作为JAVA应用的入口,根据我们之前所学的双亲委派模型,Laucher是由JVM创建的,它类加载器应该是BootStrapClassLoader, 这是一个C++编写的类加载器,是Java应用体系中最顶层的类加载器,负责加载JVM需要的一些类库,classpath配置 (%JAVA_HOME%/jre/lib)。下面通过简单例子进行说明:

public class TestClassLoader { 
   
    public static void main(String[] args) { 
   
		// 可以获取是哪个类加载器加载 Launcher 这个类
		ClassLoader classLoader = Launcher.class.getClassLoader();
		System.out.println("classLoader : " + classLoader);
		System.out.println("-------------------");
		String bootClassPath = System.getProperty("sun.boot.class.path");
		String[] split = bootClassPath.split(";");

		for (int i = 0; i < split.length; i++) { 
   
			System.out.println(split[i]);
		}
}

输出的结果,我本地机器上路径:

classLoader : null 
-------------------
C:\Program Files\Java\jdk1.8.0_121\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_121\jre\lib\sunrsasign.jar
C:\Program Files\Java\jdk1.8.0_121\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_121\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_121\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_121\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_121\jre\classes

第二:当初始化Launcher类的时候,遇到关键字new 进行初始化,调用构造方法。先获取到ExtClassLoader类加载器,然后在获取AppClassLoader类加载器器,然后设置ExtClassLoader做为它父类加载器。具体设置代码如下:

public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { 
   
	//删掉其他代码...
    return new Launcher.AppClassLoader(var1x, var0);
}
AppClassLoader(URL[] var1, ClassLoader var2) { 
   
    // 看 super的实现 ,var2 就是 ExtClassLoader
    super(var1, var2, Launcher.factory);
    this.ucp.initLookupCache(this);
}
/** * 在AppClassLoader的父类中,java.net.URLClassLoader#URLClassLoader * super(parent); 设置父类加载器,最后可以通过 getParent() 获取父类加载器 */
public URLClassLoader(URL[] urls, ClassLoader parent,
                      URLStreamHandlerFactory factory) { 
   
    super(parent); // 这里就不在继续往上跟踪了,在往上代码相对简单,可自行查阅
    SecurityManager security = System.getSecurityManager();
    //删掉其他代码...
}

第三:设置上下文类加载器的思考?

在Java中为什么需要上下文类加载器呢,这个就是一个非常有意思的问题。 Java中有两种方式获取类加载器:

第一种:一个类被加载的时候使用哪个类加载器来加载,也就是类.class.getClassLoader()或者对象.getClass().getClassLoader()

String str = new String();
ClassLoader classLoader1 = String.class.getClassLoader();
ClassLoader classLoader2 = str.getClass().getClassLoader();
System.out.println("String loader1 : " + classLoader1);
System.out.println("String loader2 : " + classLoader2);
-----输出为null,启动类加载器进行加载--------
String loader1 : null
String loader2 : null

第二种:通过Thread的上限文获取类加载器。为什么要通过上下文加载呢?

虽然我们都知道Java类加载的双亲委派模型,在加载一个类的时候,会优先委派给父类加载器,这样保证不会出现类被重复加载,也保证了Java一些基础类(如String类)可以稳定的存在,不会被用户自定义类顶替掉。

但是双亲委派模型并不是完美的,在一些场景下会出现一些比较难解决的问题,举个例子,在使用SPI的时候,java.util.ServiceLoader是通过BootStrapClassLoader类加载器加载的,在执行到加载用户编写的扩展类的时候,如果使用当前类的类加载器,是肯定无法加载到用户编写的类的,这个时候就无法继续执行了,所以这个时候就需要使用Thread的上下文类加载器,查看源码的时候我们就发现,在用户不主动传递ClassLoader的时候,会获取当前上下文类加载器,这样应用程序才能正常的执行。

public static <S> ServiceLoader<S> load(Class<S> var0) { 
   
    ClassLoader var1 = Thread.currentThread().getContextClassLoader();
    return load(var0, var1);
}

小总结

上面的内容是在上一篇的基础上继续的扩展,如果对还没有看过上一篇内容,请先阅读上一篇内容后,在来看这段内容。整个ClassLoader源码的的部分就分析这么多了,后面在自定义类加载器中有遇到需要分析源码的地方,还是会继续进行说明和讲解。

二、实现自定义类加载器

前面的两篇文章一直在为自定义加载器做铺垫,本文终于来引来这个神秘的嘉宾了,下面就我们用”热恋”的掌声欢迎它的出场(活跃一下气氛,因为刚才看源码太安静了)!

首先看一下JDK API中如何教我们实现从现从网络加载类的类加载器的简单示例。看下面代码:

例如,应用程序可以创建一个网络类加载器,从服务器中下载类文件。示例代码如下所示: 

ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();
. . .
网络类加载器子类必须定义方法 findClass 和 loadClassData,以实现从网络加载类。下载组成该类的字节后,它应该使用方法 defineClass 来创建类实例。示例实现如下: 

class NetworkClassLoader extends ClassLoader { 
   
    String host;
    int port;

    public Class findClass(String name) { 
   
    byte[] b = loadClassData(name);
    return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassData(String name) { 
   
    // load the class data from the connection
    . . .
    }
}

下面就“手把手”一步步教你如何实现一个自定义类加载器!

第一步: 新建一个MyClassLoader 继承 ClassLoader

public class MyClassLoader extends ClassLoader { 
   }

第二步:添加自定义的类加载器属性和 构造函数

public class MyClassLoader extends ClassLoader { 
   
    /** * 类加载器名称 */
    private String name;

    /** * 自定义加载路径 */
    private String path;
	/** * 自定义加载文件后缀类型 */
    private final String fileType = ".class";
    
    public MyClassLoader(String name,String path){ 
   
        //让系统类加载器(AppClassLoader)成为该类加载器的父类加载器
        super();
        this.name = name;
        this.path = path;
    }
    public MyClassLoader(ClassLoader parent,String name,String path){ 
   
        //显示指定该类的父类加载器
        super(parent);
        this.name = name;
        this.path = path;
    }
    
    @Override
    public String toString() { 
   
        return this.name;
    }
}

**第三步:**重写findClass方法,自定义我们自己的查询类的方式,然后通过defineClass方法将一个 byte 数组转换为 Class 类的实例。

/** * 加载我们自己定义的类,通过我们自己定义的类加载器 * @param name 二进制的文件名称 * @return Class实例对象 * @throws ClassNotFoundException */
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException { 
   
    //获取class文件的字节数组
    byte[] resultData = this.loadByteClassData(name);

    return super.defineClass(name, resultData, 0, resultData.length);
}
/** * 加载指定路径下面的class文件的字节数组 * @param name 二进制文件名称 ,例如:com.learn.classloader.Demo * @return 二进制字节数组 */
private byte[] loadByteClassData(String name) { 
   
    byte[] classData = null;
    InputStream in = null;
    ByteArrayOutputStream os = null;
    try { 
   
        // 比如 有包名 二进制文件名:com.learn.classloader.Demo
        // 转换为本地路径 com/learnclassloader/Demo.class
        name = this.path + name.replaceAll("\\.", "/") + fileType;
        File file = new File(name);
        os = new ByteArrayOutputStream();
        in = new FileInputStream(file);

        int tmp = 0;
        while ((tmp = in.read()) != -1){ 
   
            os.write(tmp);
        }
        // 文件流转为二进制字节流
        classData = os.toByteArray();
    }catch (Exception e){ 
   
        e.printStackTrace();
    }finally { 
   
        try { 
   
            // 关闭流
            if(in != null){ 
   
                in.close();
            }
            if(os != null){ 
   
                os.close();
            }
        }catch (Exception e){ 
   
            e.printStackTrace();
        }

    }
    return classData;
}

上面三步整合在一起,就实现了一个简单的类加载! 查看自定义类加载器的全部代码。

/** * 自定义类加载器 * * @author:dufyun * @version:1.0.0 * @date 2019/3/28 */
public class MyClassLoader extends ClassLoader { 
   
	/** * 类加载器名称 */
	private String name;

	/** * 自定义加载路径 */
	private String path;
	/** * 自定义加载文件后缀类型 */
	private final String fileType = ".class";

    public MyClassLoader(String name,String path){ 
   
		//让系统类加载器(AppClassLoader)成为该类加载器的父类加载器
        super();
        this.name = name;
        this.path = path;
    }
    public MyClassLoader(ClassLoader parent,String name,String path){ 
   
		//显示指定该类的父类加载器
        super(parent);
        this.name = name;
        this.path = path;
    }

	/** * 加载我们自己定义的类,通过我们自己定义的类加载器 * @param name 二进制的文件名称 * @return Class实例对象 * @throws ClassNotFoundException */
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException { 
   
		//获取class文件的字节数组
		byte[] resultData = this.loadByteClassData(name);

		return super.defineClass(name, resultData, 0, resultData.length);
	}
	/** * 加载指定路径下面的class文件的字节数组 * @param name 二进制文件名称 ,例如:com.learn.classloader.Demo * @return 二进制字节数组 */
	private byte[] loadByteClassData(String name) { 
   
		byte[] classData = null;
		InputStream in = null;
		ByteArrayOutputStream os = null;
		try { 
   
			// 比如 有包名 二进制文件名:com.learn.classloader.Demo
			// 转换为本地路径 com/learn/classloader/Demo.class
			name = this.path + name.replaceAll("\\.", "/") + fileType;
			File file = new File(name);
			os = new ByteArrayOutputStream();
			in = new FileInputStream(file);

			int tmp = 0;
			while ((tmp = in.read()) != -1){ 
   
				os.write(tmp);
			}
			// 文件流转为二进制字节流
			classData = os.toByteArray();
		}catch (Exception e){ 
   
			e.printStackTrace();
		}finally { 
   
			try { 
   
				// 关闭流
				if(in != null){ 
   
					in.close();
				}
				if(os != null){ 
   
					os.close();
				}
			}catch (Exception e){ 
   
				e.printStackTrace();
			}

		}
		return classData;
	}

    @Override
    public String toString() { 
   
        return this.name;
    }
}

第五: 简单的测试自定义类加载器!

MyClassLoader myClassLoaderA = new MyClassLoader("aflyun", "F:\\tmp\\");
System.out.println("myClassLoaderA :" + myClassLoaderA.getParent().getClass().getName());

// 设置父类加载器 为null ,启动类加载器
MyClassLoader myClassLoaderB = new MyClassLoader(null, "aflyun", "F:\\tmp\\");
System.out.println("myClassLoaderB :" + myClassLoaderB.getParent());

------------
myClassLoaderA :sun.misc.Launcher$AppClassLoader
myClassLoaderB :null

看到这里,你如果对本篇第一小节理解的话,这里肯定会好奇,MyClassLoader是怎么设置AppClassLoader为父类加载器的,在代码中只写了一个super();系统类加载器(AppClassLoader)成为该类加载器的父类加载器。还是看源码 ,使用super();在初始化的时候调用CLassLoader的构造函数,在此构造函数中有一个getSystemClassLoader()获取的就是 AppClassLoader

protected ClassLoader() { 
   
    this(checkCreateClassLoader(), getSystemClassLoader());
}

我们在看一下 getSystemClassLoader()的实现,其中有一个initSystemClassLoader()方法获取到Launcher初始化加载的ClassLoader,然后将此ClassLoader赋值给 ClassLoader类 中的 scl!具体源码如下:

// The class loader for the system
private static ClassLoader scl;
public static ClassLoader getSystemClassLoader() { 
   
    initSystemClassLoader(); // 获取系统初始化的ClassLoader
    if (scl == null) { 
   
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) { 
   
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}
private static synchronized void initSystemClassLoader() { 
   
    if (!sclSet) { 
   
        // 省略其他代码.....
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) { 
   
            Throwable oops = null;
            //从Launcher 中获取ClassLoader也就是 AppClassLoader
            scl = l.getClassLoader();
          // 省略其他代码.....
        }
        sclSet = true;
    }
}

最后this(checkCreateClassLoader(), getSystemClassLoader());调用了ClassLoader的另一个构造函数,具体看下面源码,比较简单,就不做太多说明了。

// 这段代码上一篇文章就介绍过
private ClassLoader(Void unused, ClassLoader parent) { 
   
    this.parent = parent; // 将获取的系统类加载器作为父类加载器,通过getParent()获取!
    if (ParallelLoaders.isRegistered(this.getClass())) { 
   
        parallelLockMap = new ConcurrentHashMap<>();
        // 省略其他代码.....
    } else { 
   
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
       // 省略其他代码.....
    }
}
/** * 获取父类加载器 */
public final ClassLoader getParent() { 
   
    if (parent == null)
        return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) { 
   
        checkClassLoaderPermission(parent, Reflection.getCallerClass());
    }
    return parent;
}

一张图在此说明:当前的类加载器就是自定义类加载器MyClassLoader!

MyClassLoader myClassLoaderA = new MyClassLoader("aflyun", "F:\\tmp\\");

类加载器双亲委派模型


tips1 :自定义类加载说明

在实现自定义类加载器过程中可以重写findClass 也可以重写loadClass ,但一般建议重写findClass

tips2 :自定义类加载器设置了加载路径path,其实之前介绍过的类加载器也有对应的加载路径。

// BootStrapClassLoader
String bootClassPath = System.getProperty("sun.boot.class.path");
// ExtClassLoader
String var0 = System.getProperty("java.ext.dirs");
// AppClassLoader
String var1 = System.getProperty("java.class.path");

三、多案例分析

1、相同两个类文件,名称一样,一个在磁盘F:\tmp\下(无包名)一个工程中(有包名)

F:盘中代码

Java内存管理-掌握自定义类加载器的实现(七)

public class HelloWorld { 
   
	public HelloWorld() { 
   
		System.out.println("--F: --HelloWorld--" + this.getClass().getClassLoader());
	}
}

工程中代码:

package com.learn.classloader;

public class HelloWorld { 
   
	public HelloWorld() { 
   
		System.out.println("--IDEA --HelloWorld--" + this.getClass().getClassLoader());
	}
}

执行类加载代码

MyClassLoader myClassLoader = new MyClassLoader("myClassLoader","F:/tmp/");
Class<?> cls = null;
try { 
   
    //加载类文件
    cls = myClassLoader.loadClass("HelloWorld");
    cls.newInstance();//实例化类。调用构造方法
} catch (Exception e) { 
   
    e.printStackTrace();
}

思考一下打印的结果是什么呢?

答案:--F: --HelloWorld--myClassLoader

原因分析:HelloWorld类的class文件只有在F:/tmp/下存在,所以就加载的是F盘下的HelloWorld类文件!

2、相同两个类文件,名称和包名一样,一个在磁盘F:\tmp\,一个在工程中

F:盘中代码

Java内存管理-掌握自定义类加载器的实现(七)

package com.learn.classloader;
public class HelloWorld { 
   
	public HelloWorld() { 
   
		System.out.println("--F:com.learn.classloader --HelloWorld--" + this.getClass().getClassLoader());
	}
}

工程中的代码:

package com.learn.classloader;

public class HelloWorld { 
   
	public HelloWorld() { 
   
		System.out.println("--IDEA --HelloWorld--" + this.getClass().getClassLoader());
	}
}

执行类加载代码

MyClassLoader myClassLoader = new MyClassLoader("myClassLoader","F:/tmp/");
Class<?> cls = null;
try { 
   
    cls = myClassLoader.loadClass("com.learn.classloader.HelloWorld");
    cls.newInstance();
} catch (Exception e) { 
   
    e.printStackTrace();
}

思考一下打印的结果是什么呢?

答案:--IDEA --HelloWorld--sun.misc.Launcher$AppClassLoader@18b4aac2

原因分析:F盘中HelloWorld和工程中的HelloWorld具有一样的包名,并且MyClassLoader没有设置父类加载器,那么默认的父类加载类就是AppClassLoader,根据之前所学的双亲委派模型,HelloWorld类文件会首先被父类加载器加载,也就是被AppClassLoader加载,只要父类加载器加载成功,子类加载器就不会在进行加载!

3、新增一个myClassLoaderB的实例设置父类加载器myClassLoader,加载MyHelloWorld

F: 盘 MyHelloWorld

Java内存管理-掌握自定义类加载器的实现(七)

package com.learn.classloader;
public class MyHelloWorld { 
   
	public MyHelloWorld() { 
   
		System.out.println("--F:com.learn.classloader --MyHelloWorld--" + this.getClass().getClassLoader());
	}
}

D:盘 MyHelloWorld

Java内存管理-掌握自定义类加载器的实现(七)

package com.learn.classloader;
public class MyHelloWorld { 
   
	public MyHelloWorld() { 
   
		System.out.println("--D:com.learn.classloader --MyHelloWorld--" + this.getClass().getClassLoader());
	}
}

此时新增一个myClassLoaderB,加载MyHelloWorld!

加载的代码如下:

MyClassLoader myClassLoader = new MyClassLoader("myClassLoader","F:/tmp/");
MyClassLoader myClassLoaderB = new MyClassLoader(myClassLoader,"myClassLoader","D:/tmp/");
Class<?> cls = null;
try { 
   
    cls = myClassLoaderB.loadClass("com.learn.classloader.MyHelloWorld");
    cls.newInstance();
} catch (Exception e) { 
   
    e.printStackTrace();
}

思考一下打印的结果是什么呢?

答案:--F:com.learn.classloader --MyHelloWorld--myClassLoader

原因分析:myClassLoaderB父类加载器是myClassLoader,此时在加载 MyHelloWorld,首先会被父类加载器myClassLoader 加载!

4、新增myClassLoaderC设置父类加载器为null(启动类加载器),加载MyHelloWorld!

代码示例和示例3一样!只是修改类加载器!代码如下:

MyClassLoader myClassLoaderC = new MyClassLoader(null,"myClassLoaderC","D:/tmp/");
Class<?> cls = null;
try { 
   
    cls = myClassLoaderC.loadClass("com.learn.classloader.MyHelloWorld");
    cls.newInstance();
} catch (Exception e) { 
   
    e.printStackTrace();
}

思考一下打印的结果是什么呢?

答案:--D:com.learn.classloader --MyHelloWorld--myClassLoaderC

原因分析:myClassLoaderC父类加载器是启动类加载器,在启动类加载器中找不到MyHelloWorld,转一圈回来还是需要myClassLoaderC自己去加载!

四、本文总结

本文实现了一个自定义的类加载器,并且通过简单的案例进行讲解和说明,让我们更加深入的了解类加载器的双亲委派模式和实现原理。

对类加载器有了比较深入的学习和思考之后,会对我们以后写Java代码会有一定帮助,并且在遇到一些Java的异常如ClassNotFoundException能够快速知道原因。 其实类加载的知识还有很多,在这里先抛出两个问题:

问题1、Java热部署如何实现 ? 修改一个Java文件后,不需要启动服务,就可以动态生效! (目前的主流开发工具都支持,如IDEA 在windows下 Ctrl+Shirt+F9,动态编译,动态加载!或者 SpringBoot通过配置devtools实现热部署的原理是什么?)

本质上是更新clas文件内容! 不需要重新启动服务!

问题2、之前一直提的类加载模式: 双亲模式模式!但是在Tomcat中你知道 WebappClassLoader 的加载机制吗?

说明:WebappClassLoader 会先加载自己的Class ,找不到在委托给parent,破坏双亲委派模式!

后续有时间会去整理,如果你对这两个问题感兴趣,也欢迎在文末留言,一起探讨!

五、参考资料

《JDK API 文档》

《深入理解Java虚拟机》

备注: 由于本人能力有限,文中若有错误之处,欢迎指正。


谢谢你的阅读,如果您觉得这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到!祝你每天开心愉快!



Java编程技术乐园:一个分享编程知识的公众号。跟着老司机一起学习干货技术知识,每天进步一点点,让小的积累,带来大的改变!


扫描关注,后台回复【资源】,获取珍藏干货! 99.9%的伙伴都很喜欢

image.png | center| 747x519


©
每天都在变得更好的阿飞云

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/121030.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 最速下降法收敛速度快还是慢_最速下降法是全局收敛算法吗

    最速下降法收敛速度快还是慢_最速下降法是全局收敛算法吗\qquad已知设步长为α\alphaα,下降方向为ddd,f(xk+αd)f(x_{k}+\alphad)f(xk​+αd)在xkx_{k}xk​的TaylorTaylorTaylor展示为f(xk+1)=f(xk+αd)=f(xk)+αgkTd+O(∣∣αd∣∣2)f(x_{k+1})=f(x_{k}+\alphad)=f(x_{k})+\alphag_{k}^{T}d+O(||\…

  • Linux Oracle 创建用户

    Linux Oracle 创建用户1、启动Oracle(Oracle没启动的情况下)su-oracle,切换成oracle用户lsnrctlstartsqlplus/nologconn/assysdbastartup(若数据库处于启动状态,则无需再次启动)2、查询临时表空间和表空间的存储位置selectnamefromv$tempfile;selectnamefromv$datafile从结果可以看出有…

  • 三维浮雕软件 linux,做3D浮雕圆雕模型用哪个软件好?3Dcoat这款软件是不错的选择。…「建议收藏」

    三维浮雕软件 linux,做3D浮雕圆雕模型用哪个软件好?3Dcoat这款软件是不错的选择。…「建议收藏」#以下是我整理了这款软件的几个优点:优点1,先进的智能转化功能,可以把彩色的平面图片生成3D浮雕模型图,也可以把灰度图生成3D浮雕图,例如在木雕家具效果图设计行业,3DCAOT制作的家具设计贴浮雕效果图优点2,它有先进的局部精细化功能,特别是用于表面精细的浮雕类工艺品设计,可以在产品的表面制作各种效果的浮雕效果。优点3,用于3D扫描抄数的后期处理,修图,对于扫描文件的表面处理,精修等。优点4,指定…

  • 浅谈Vue响应式原理

    浅谈Vue响应式原理一、Vue中的响应式Vue最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的JavaScript对象。而当你修改它们时,视图会进行更新。二、响应式的基本原理1.通过Object.definePropert来实现监听数据的改变和读取(属性中的getter和setter方法)实现数据劫持2.观察者模式(发布者-订阅者)观察者(订阅者)–Watcher:update():当事件发生时,具体要做的事情目标(发布者)–Dep:①subs数组:存储所有的观察者②

  • lscpu详解

    lscpu详解1 lscpu[centos@localhost~]$lscpuArchitecture:x86_64CPUop-mode(s):32-bit,64-bitByteOrder:LittleEndianCPU(s):16On-lineCPU(s)list:0-15Thr

    2022年10月30日
  • java InputStreamReader_InputStream

    java InputStreamReader_InputStream简介OutputStreamWriter和InputStreamReader是字节流和字符流转化之间桥梁,OutputStreamWriter继承自Writer接口,而InputStreamReader继承自接口Reader.需要了解的一点是字符流的写入和读取的方式.字符输出流:将字符通过指定编码方式转化成字节数据,然后存储到文件中. 字符输入流:读取文件的字节数据通过相同的编码方式转化…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号