目录

前提: 运行环境

1. 类加载的过程

  1.1 类加载器初始化的过程

  1.2 类加载的过程

  1.3 类的懒加载

2. jvm核心类加载器

3. 双亲委派机制

4. 自定义类加载器

5. tomcat类加载机制


运行环境:

我是在mac上操作的. 先找到mac的java地址. 从~/.bash_profile中可以看到

java的home目录是: /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home


一. 类加载的过程

1.1 类加载器初始化的过程

假如现在有一个java类 com.lxl.jvm.Math类, 里面有一个main方法,

package com.lxl.jvm;

public class Math {
public static int initData = 666;
public static User user = new User(); public int compute() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
} public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}

这个方法很简单, 通常我们直接执行main方法就ok, 可以运行程序了, 那么点击运行main方法, 整个过程是如何被加载的呢? 为什么点击执行main, 就能得到结果呢?

先来看看答题的类加载流程, 如下图:

备注:

1. windows上的java启动程序是java.exe, mac下,就是java

2. c语言部分,我们做了解, java部门是需要掌握的部分.

第一步: java调用底层的jvm.dll文件创建java虚拟机(这一步由C++实现) . 这里java是c++写的代码, 调用的jvm.dll也是c++底层的一个函数. 通过调用jvm.dll文件, 会创建java虚拟机. java虚拟机的启动都是c++程序实现的. dll文件相当于java的jar包. 开启虚拟机以后,会做哪些操作呢? 看第二步

第二步:在启动虚拟机的过程中, 会创建一个引导类加载器的实例. 这个引导类的加载器是C语言实现的. 然后jvm虚拟机就启动起来了. jvm启动起来了,记下来干嘛呢?

第三步: 接下来,C++语言会调用java的启动程序.刚刚只是创建了java虚拟机, java虚拟机里面还有很多启动程序. 其中有一个程序叫做Launcher. 类全称是sun.misc.Launcher. 通过启动这个java类, 会由这个类引导加载器加载并创建很多其他的类加载器. 而这些加载器才是真正启动并加载磁盘上的字节码文件.

第四步:真正的去加载本地磁盘的字节码文件,然后启动执行main方法.

第五步:main方法执行完毕, 引导类加载器会发起一个c++调用, 销毁JVM

以上就是启动一个main方法, 这个类加载的全部过程

下面, 我们重点来看一下, 我们的类com.lxl.Math是怎么被加载到java虚拟机里面去的?  

1.2 类加载的过程

上面的com.lxl.jvm.Math类最终会生成clas字节码文件. 字节码文件是怎么被加载器加载到JVM虚拟机的呢?

类加载有五步:加载, 验证, 准备, 解析, 初始化. 那么这五步都是干什么的呢?我们来看一下

我们的类在哪里呢? 在磁盘里(比如: target文件夹下的class文件), 我们先要将class类加载到内存中. 加载到内存区域以后, 不是简简单单的转换成二进制字节码文件,他会经过一系列的过程. 比如: 验证, 准备, 解析, 初始化等. 把这一些列的信息转变成内元信息, 放到内存里面去. 我们来看看具体的过程

第一步: 加载.  将class类加载到java虚拟机的内存里去, 在加载到内存之前, 会有一些列的操作, 第一个是验证字节码, 比如:打开一个字节码文件

打眼一看, 感觉像是乱码, 实际上不是的. 其实,这里面每个字符串都有对应的含义. 那么文件里面的内容我们能不能替换呢?当然不能, 一旦替换, 就不能执行成功了. 所以, 第一步:验证, 验证什么呢?

验证字节码加载是否正确: 格式是否正确. 验证, 验证的就是里面的内容, 是否符合java虚拟机的规范. 验证完了, 接下来是准备. 准备干什么呢? 比如我们的类Math, 他首先会给Math里的静态变量赋值一个初始值. 比如我们Math里有两个静态遍历

public static int initData = 666;
public static User user = new User();

在准备的过程中, 就会给这两个变量赋初始值, 这个初始值并不是真实的值, 比如initData的初始值是0. 如果是boolean类型, 就赋值为false.  也就是说, 准备阶段赋的值是jvm固定的, 不是我们定义的值.如果一个final的常量, 比如public static final int name="zhangsan", 那么他在初始化的时候, 是直接赋初始值"zhangsan"的. 这里只是给静态变量赋初始值

记下来说说解析的过程. 解析的过程略微复杂, 解析是将"符号引用"转变为直接引用.

什么是符号引用呢?

比如我们的程序中的main方法. 写法是固定的, 我们就可以将main当成一个符号. 比如上面的initData, int, static, 我们都可以将其称之为符号, java虚拟机内部有个专业名词,把他叫做符号. 这些符号被加载到内存里都会对应一个地址. 将"符号引用"转变为直接引用, 指的就是, 将main, initData, int等这些符号转变为对应的内存地址. 这个地址就是代码的直接引用. 根据直接引用的值,我们就可以知道代码在什么位置.然后拿到代码去真正的运行.

将符号引用转变为"内存地址", 这种有一个专业名词, 叫静态链接. 上面的解析过程就相当于静态链接的过程. 类加载期间,完成了符号到内存地址的转换. 有静态链接, 那么与之对应的还有动态链接.

什么是动态链接呢?

public static void main(String[] args) {
Math math = new Math();
math.compute();
}

比如:上面这段代码, 只有当我运行到math.compute()这句话的时候, 才回去加载compute()这个方法. 也就是说, 在加载的时候, 我不一定会把compute()这个方法解析成内存地址. 只有当运行到这行代买的时候, 才会解析.

我们来看看汇编代码

javap -v Math.class
Classfile /Users/luoxiaoli/Downloads/workspace/project-all/target/classes/com/lxl/jvm/Math.class
Last modified 2020-6-27; size 777 bytes
MD5 checksum a6834302dc2bf4e93011df4c0b774158
Compiled from "Math.java"
public class com.lxl.jvm.Math
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#35 // java/lang/Object."<init>":()V
#2 = Class #36 // com/lxl/jvm/Math
#3 = Methodref #2.#35 // com/lxl/jvm/Math."<init>":()V
#4 = Methodref #2.#37 // com/lxl/jvm/Math.compute:()I
#5 = Fieldref #2.#38 // com/lxl/jvm/Math.initData:I
#6 = Class #39 // com/lxl/jvm/User
#7 = Methodref #6.#35 // com/lxl/jvm/User."<init>":()V
#8 = Fieldref #2.#40 // com/lxl/jvm/Math.user:Lcom/lxl/jvm/User;
#9 = Class #41 // java/lang/Object
#10 = Utf8 initData
#11 = Utf8 I
#12 = Utf8 user
#13 = Utf8 Lcom/lxl/jvm/User;
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 Lcom/lxl/jvm/Math;
#21 = Utf8 compute
#22 = Utf8 ()I
#23 = Utf8 a
#24 = Utf8 b
#25 = Utf8 c
#26 = Utf8 main
#27 = Utf8 ([Ljava/lang/String;)V
#28 = Utf8 args
#29 = Utf8 [Ljava/lang/String;
#30 = Utf8 math
#31 = Utf8 MethodParameters
#32 = Utf8 <clinit>
#33 = Utf8 SourceFile
#34 = Utf8 Math.java
#35 = NameAndType #14:#15 // "<init>":()V
#36 = Utf8 com/lxl/jvm/Math
#37 = NameAndType #21:#22 // compute:()I
#38 = NameAndType #10:#11 // initData:I
#39 = Utf8 com/lxl/jvm/User
#40 = NameAndType #12:#13 // user:Lcom/lxl/jvm/User;
#41 = Utf8 java/lang/Object
{
public static int initData;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC public static com.lxl.jvm.User user;
descriptor: Lcom/lxl/jvm/User;
flags: ACC_PUBLIC, ACC_STATIC public com.lxl.jvm.Math();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lxl/jvm/Math; public int compute();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn
LineNumberTable:
line 8: 0
line 9: 2
line 10: 4
line 11: 11
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/lxl/jvm/Math;
2 11 1 a I
4 9 2 b I
11 2 3 c I public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/lxl/jvm/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: return
LineNumberTable:
line 15: 0
line 16: 8
line 17: 13
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 args [Ljava/lang/String;
8 6 1 math Lcom/lxl/jvm/Math;
MethodParameters:
Name Flags
args static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: sipush 666
3: putstatic #5 // Field initData:I
6: new #6 // class com/lxl/jvm/User
9: dup
10: invokespecial #7 // Method com/lxl/jvm/User."<init>":()V
13: putstatic #8 // Field user:Lcom/lxl/jvm/User;
16: return
LineNumberTable:
line 4: 0
line 5: 6
}
SourceFile: "Math.java"

使用这个指令, 就可以查看Math的二进制文件. 其实这个文件,就是上面那个二进制代码文件.

看看这里面有什么东西?

类的名称, 大小,修改时间, 大版本,小版本, 访问修饰符等等

 Last modified 2020-6-27; size 777 bytes
MD5 checksum a6834302dc2bf4e93011df4c0b774158
Compiled from "Math.java"
public class com.lxl.jvm.Math
minor version: 0
major version: 52

还有一个Constant pool 常量池. 这个常量池里面有很多东西. 我们重点看中间哪一行. 第一列表示一个常量的标志符, 这个标识符可能在其他地方会用到.  第二列就表示常量内容.

Constant pool:
#1 = Methodref #9.#35 // java/lang/Object."<init>":()V
#2 = Class #36 // com/lxl/jvm/Math
#3 = Methodref #2.#35 // com/lxl/jvm/Math."<init>":()V
#4 = Methodref #2.#37 // com/lxl/jvm/Math.compute:()I
#5 = Fieldref #2.#38 // com/lxl/jvm/Math.initData:I
#6 = Class #39 // com/lxl/jvm/User
#7 = Methodref #6.#35 // com/lxl/jvm/User."<init>":()V
#8 = Fieldref #2.#40 // com/lxl/jvm/Math.user:Lcom/lxl/jvm/User;
#9 = Class #41 // java/lang/Object
#10 = Utf8 initData
#11 = Utf8 I
#12 = Utf8 user
#13 = Utf8 Lcom/lxl/jvm/User;
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 Lcom/lxl/jvm/Math;
#21 = Utf8 compute

这些标识符在后面都会被用到, 比如main方法

 public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/lxl/jvm/Math
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method compute:()I
12: pop
13: return
LineNumberTable:
line 15: 0
line 16: 8
line 17: 13
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 args [Ljava/lang/String;
8 6 1 math Lcom/lxl/jvm/Math;
MethodParameters:
Name Flags
args

这里面就用到了#2 #3 #4 ,这都是标识符的引用.

第一句: new了一个Math(). 我们看看汇编怎么写的?

         0: new           #2                  // class com/lxl/jvm/Math

new + #2. #2是什么呢? 去常量池里看, #2代表的就是Math类

   #2 = Class              #36            // com/lxl/jvm/Math

这里要说的还是math.compute()这个方法, 不是在类加载的时候就被加载到内存中去了, 而是运行main方法的时候, 执行到这行代码才被加载进去, 这个过程叫做动态链接.

类加载的时候, 我们可以把"解析"理解为静态加载的过程. 一般像静态方法(例如main方法), 获取其他不变的静态方法会被直接加载到内存中, 因为考虑到性能, 他们加载完以后就不会变了, 就直接将其转变为在内存中的代码位置.

而像math.compute()方法, 在加载过程中可能会变的方法(比如compute是个多态,有多个实现), 那么在初始化加载的时候, 我们不会到他会调用谁, 只有到运行时才能知道代码的实现, 所以在运行的时候在动态的去查询他在内存中的位置, 这个过程就是动态加载

下一个步骤: 初始化: 对类的静态变量初始化为指定的值. 执行静态代码块. 比如代码

public static int initData = 666;

在准备阶段将其赋值为0, 而在初始化阶段, 会将其赋值为设定的666  

2. 验证

3. 准备

4. 解析

5. 初始化

1.3 类的懒加载

类被加载到方法区中以后,主要包含:运行时常量池, 类型信息, 字段信息, 方法信息, 类加载器的引用, 对应class实例的引用等信息.

什么意思呢? 就是说, 当一个类被加载到内存, 这个类的常量,有常量名, 类型, 域信息等; 方法有方法名, 返回值类型, 参数类型, 方法作用域等符号信息都会被加载放入不同的区域.

注意: 如果主类在运行中用到其他类,会逐步加载这些类, 也就是说懒加载. 用到的时候才加载.

package com.lxl.jvm;

public class TestDynamicLoad {
static {
System.out.println("********Dynamic load class**************");
} public static void main(String[] args) {
new A();
System.out.println("*********load test*****************");
B b = null; // 这里的b不会被加载, 除非new B();
}

} class A {
static {
System.out.println("********load A**************");
} public A(){
System.out.println("********initial A**************");
}
} class B {
static {
System.out.println("********load B**************");
} public B(){
System.out.println("********initial B**************");
}
}

这里定义了两个类A和B, 当使用到哪一个的时候, 那个类才会被加载, 比如:main方法中, B没有被用到, 所以, 他不会被加载到内存中.

运行结果

********Dynamic load class**************
********load A**************
********initial A**************
*********load test*****************

总结几点如下:

1. 静态代码块在构造方法之前执行

2. 没有被真正使用的类不会被加载

二. jvm核心类加载器

类主要通过类加载器来加载, java里面有如下几种类加载器

1. 引导类加载器:负责加载支撑JVM运行的, 位于jre目录的lib目录下的核心类. 比如:rt.jar, charset.jar等,

2. 扩展类加载器: 负责加载支撑JVM运行的, 位于jre目录的lib/ext扩展目录中的jar包

3. 应用程序类加载器: 负责加载classPath路径下的类包, 主要加载自己写的类

4. 自定义类加载器: 负责加载用户自定义路径下的类包

引导类加载器是由C++帮我们实现的, 然后c++语言会通过一个Launcher类将扩展类加载器(ExtClassLoader)和应用程序类加载器(AppClassLoader)构造出来, 并且把他们之间的关系构建好.

例1:

package com.lxl.jvm;

import sun.misc.Launcher;

import java.net.URL;

public class TestJDKClassLoader {

    public static void main(String[] args) {
/**
* 第一个: String 是jdk自身自带的类, 所以, 他的类加载器是引导类加载器
* 第二个: 加密类的classloader, 这是jdk扩展包的一个类
* 第三个: 是我们当前自己定义的类, 会被应用类加载器加载
*/
System.out.println(String.class.getClassLoader());
System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
}
}

我们来看这个简单的代码, 运行结果:

null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
 第一个: String 是jdk自身自带的类, 所以, 他的类加载器是引导类加载器
第二个: 加密类的classloader, 这是jdk扩展包的一个类, jdk扩展包里面使用的是extClassLoader类加载器加载的
第三个: 是我们当前自己定义的类, 会被AppClassLoader应用程序加载器加载.

我们看到ExtClassLoader和AppClassLoader都是Launcher类的一部分. 那Launcher类是什么东西呢?

上面有提到, Launcher类是jvm启动的时候由C++调用启动的一个类.

那么,第一个bootstrap引导类加载器, 那引导类加载器返回的为什么是null呢?

因为bootstrap引导类加载器, 他不是java的对象, 他是c++生成的对象, 所以这里是看不到的

例2: ExtClassLoader和AppClassLoader是怎么生成的?

从这个图中我们可以看出,C++调用java创建JVM启动器, 其中一个启动器是Launcher,他实际是调用了sun.misc.Launcher的getLauncher()方法. 那我们就从这个方法入手看看到底是如何运行的?

我们看到Lanucher.java类是在核心的rt.jar包里的

我们看到getLauncher()类直接返回了launcher. 而launcher是一个静态对象变量, 这是一个单例模式

C++通过getLauncher()-->直接返回了lanucher对象, 而launcher对象是在构建类的时候就已经初始化好了. 那么,初始化的时候做了哪些操作呢?接下来看看他的构造方法.

在构造方法里, 首先定义了一个ExtClassLoader. 这是一个扩展类加载器, 扩展类加载器调用的是getExtClassLoader(). 接下来看一看getExtClassLoader这个方法做了什么?

在这里, 判断当前对象是否初始化过, 如果没有, 那么就创建一个ExtClassLoader()对象, 看看createExtClassLoader()这个方法做了什么事呢?

doPrivileged是一个权限校验的操作, 我们可以先不用管, 直接看最后一句, return new Launcher.ExtClassLoader(var1). 直接new了一个ExtClassLoader, 其中参数是var1, 代表的是ext扩展目录下的文件.

在ExtClassLoader(File[] var1)这个方法中,  这里第一步就是调用了父类的super构造方法. 而ExtClassLoader继承了谁呢? 我么你可以看到他继承了URLClassLoader.

而URLClassLoader是干什么用的呢? 其实联想一下大概能够猜数来, 这里有一些文件路径, 通过文件路径加载class类.

我们继续看调用的super(parent), 我们继续往下走, 就会看到一下内容

这里设置了ExtClassLoader的parent是谁? 注意看的话,我们发现, ExtClassLoader的parent类是null.

这就是传递过来的parent类加载器, 那么这里的parent类加载器为什么是null呢? 因为, ExtClassLoader的父类加载器是谁呢? 他是Bootstrap ClassLoader. 而BootStrap ClassLoader是C++的类加载器, 我们不能直接调用它, 所以, 设置为null.

其实, ExtClassLoader在初始化阶段就是调用了ExtClassLoader方法, 初始化了ExtClassLoader类

接下来,我们回到Launcher的构造方法, 看看Launcher接下来又做了什么?

可以看到, 接下来调了AppClassLoader的getAppClassLoader(var1), 这个方法. 需要注意一下的是var1这个参数. var1是谁呢? 向上看, 可以看到var1是ExtClassLoader.

这是AppClassLoader, 应用程序类加载器, 这个类是加载我们自己定义的类的类加载器. 他也是继承自URLClassLoader.

我们来看看getAppClassLoader(final ClassLoader var0)方法. 这个方法的参数就是上面传递过来的ExtClassLoader

这里第一句话就是获取当前项目的class 文件路径, 然后将其转换为URL. 并调用了Launcher.AppClassLoader(var1x, var0), 其中var1x是class类所在的路径集合, var0是扩展的类加载器ExtClassLoader, 接下来, 我们进入到这个方法里看一看

AppClassLoader直接调用了其父类的构造方法, 参数是class类路径集合, 和ExtClassLoader

最后, 我们看到, 将ExtClassLoader传递给了parent变量. 这是定义在ClassLoader中的属性, 而ClassLoader类是所有类加载器的父类. 因此, 我们也可以看到AppClassLoader的父类加载器是ExtClassLoader

同时, 我们也看到了, C++在启动JVM的时候, 调用了Launcher启动类, 这个启动类同时加载了ExtClassLoader和AppClassLoader.

public static void main(String[] args) {

  ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
  ClassLoader extClassLoader = appClassLoader.getParent();
  ClassLoader bootstrapClassLoad = extClassLoader.getParent();   System.out.println("bootstrap class loader: " + bootstrapClassLoad);
  System.out.println("ext class loader " + extClassLoader);
  System.out.println("app class loader "+ appClassLoader);
}

通过这个demo, 我们也可以看出, appClassLoader的父类是extClassLoader, extClassLoader的父类是bootstrapClassLoader

输出结果:

bootstrap class loader: null
ext class loader sun.misc.Launcher$ExtClassLoader@2a84aee7
app class loader sun.misc.Launcher$AppClassLoader@18b4aac2 

1.1 jvm核心类加载器--jdk源码剖析的更多相关文章

  1. jdk源码剖析:Synchronized

    开启正文之前,先说一下源码剖析这一系列,就以"死磕到底"的精神贯彻始终,最少追踪到JVM指令(再往下C语言实现了). =========正文分割线===========  Sync ...

  2. jdk源码剖析三:锁Synchronized

    一.Synchronized作用 (1)确保线程互斥的访问同步代码 (2)保证共享变量的修改能够及时可见 (3)有效解决重排序问题.(Synchronized同步中的代码JVM不会轻易优化重排序) 二 ...

  3. 2018-08-27 使用JDT核心库解析JDK源码后初步分析API命名

    源自术语词典API项目 · Issue #85 · program-in-chinese/overview, 打算先用早先的代码提取JDK API中的类/方法/参数名, 看看有哪些词需要翻译. 源码在 ...

  4. jdk源码剖析一:OpenJDK-Hotspot源码包目录结构

    开启正文之前,先说一下源码剖析这一系列,就以“死磕到底”的精神贯彻始终,JDK-->JRE-->JVM(以openJDK代替) 最近想看看JDK8源码,但JDK中JVM(安装在本地C:\P ...

  5. JVM 类加载器ClassLoader源码学习笔记

    类加载 在Java代码中,类型的加载.连接与初始化过程都是在程序运行期间完成的. 类型可以是Class,Interface, 枚举等. Java虚拟机与程序的生命周期 在如下几种情况下,Java虚拟机 ...

  6. 1. JVM核心类加载器及类加载的全过程

    运行环境: 下面说明一下我的运行环境.我是在mac上操作的. 先找到mac的java地址. 从~/.bash_profile中可以看到 java的home目录是: /Library/Java/Java ...

  7. 第五章 类加载器ClassLoader源码解析

    说明:了解ClassLoader前,先了解 第四章 类加载机制 1.ClassLoader作用 类加载流程的"加载"阶段是由类加载器完成的. 2.类加载器结构 结构:Bootstr ...

  8. 类加载器ClassLoader源码解析

    1.ClassLoader作用 类加载流程的"加载"阶段是由类加载器完成的. 2.类加载器结构 结构:BootstrapClassLoader(祖父)-->ExtClassL ...

  9. jdk源码剖析五:JDK8-废弃永久代(PermGen)迎来元空间(Metaspace)

    目录 1.背景 2.为什么废弃永久代(PermGen) 3.深入理解元空间(Metaspace) 4.总结 ========正文分割线===== 一.背景 1.1 永久代(PermGen)在哪里? 根 ...

随机推荐

  1. tcphdr结构

    包含在/usr/src/linux/include/linux/tcp.h 1 struct tcphdr { 2 __be16 source; 3 __be16 dest; 4 __be32 seq ...

  2. Map 综述(四):彻头彻尾理解 HashTable

    摘要: Hashtable与HashMap都是Map族中较为常用的实现,也都是Java Collection Framework 的重要成员,它们的本质都是 链表数组.本文深入JDK源码并从定义.构造 ...

  3. T-SQL——关于跨库连接查询

    目录 0. 同一台服务器不同数据库 1. 使用跨库查询函数--OpenDataSource() 2. 使用链接服务器(Linking Server) 3. 使用OpenDataSource()函数和链 ...

  4. 2021年哪个低代码平台更值得关注?T媒体盘点国内主流低代码厂商

    2020年圣诞前夜,国内知名创投科技媒体T媒体旗下的T研究发布了2020中国低代码平台指数测评报告.报告除了对国内低代码行业现状进行总结外,还对主流低代码厂商的市场渗透和曝光进行测评. 报告认为,低代 ...

  5. 【Elasticsearch】.NetCore中Elasticsearch组件NEST的使用

    .NetCore中Elasticsearch组件NEST的使用 1. 安装Docker # 安装Docker curl -fsSL https://get.docker.com | bash -s d ...

  6. 查看所有日志命令:journalctl

    journalctl命令作用:实时查看所有日志(内核日志和应用日志) 语法格式: journalctl [参数] 常用参数:-k 查看内核日志-b 查看系统本次启动的日志-u 查看指定服务的日志-n ...

  7. WebService学习总结(五)--CXF的拦截器

    拦截器是Cxf的基础,Cxf中很多的功能都是由内置的拦截器来实现的,拦截器在Cxf中由Interceptor表示.拦截器的作用类似axis2中handle.Cxf的拦截器包括入拦截器和出拦截器,所有的 ...

  8. JS_DOM操作之操作标签

    <标签名 属性1="属性值1" 属性2="属性值2"-->文本</标签名> 1 - 文本操作 <div class="c ...

  9. rtsp->rtmp 推流直播 Plan B

    上篇文章我们谈到使用 EasyDarwin 推流后 前端HTML播放器 播放无画面的情况,找了各种播放器都服务正常解决,但使用VLC却能正常播放的问题,我们尝试了很久最后另辟蹊径,找到 nginx安装 ...

  10. Android线程池使用介绍

    本文主要使用kotlin,讨论Android开发中的线程池用法. 我们想使用线程的时候,可以直接创建子线程并启动 Thread { Log.d("rfDev", "rus ...