Java 字节码指令是 JVM 体系中非常难啃的一块硬骨头,我估计有些读者会有这样的疑惑,“Java 字节码难学吗?我能不能学会啊?”

讲良心话,不是我谦虚,一开始学 Java 字节码和 Java 虚拟机方面的知识我也感觉头大!但硬着头皮学了一阵子之后,突然就开窍了,觉得好有意思,尤其是明白了 Java 代码在底层竟然是这样执行的时候,感觉既膨胀又飘飘然,浑身上下散发着自信的光芒!

我在 博客园 共输出了 100 多篇 Java 方面的文章,总字数超过 30 万字, 内容风趣幽默、通俗易懂,收获了很多初学者的认可和支持,内容包括 Java 语法、Java 集合框架、Java 并发编程、Java 虚拟机等核心内容


为了帮助更多的 Java 初学者,我“一怒之下”就把这些文章重新整理并开源到了 GitHub,起名《教妹学 Java》,听起来是不是就很有趣?

GitHub 开源地址(欢迎 star):https://github.com/itwanger/jmx-java

Java 官方的虚拟机 Hotspot 是基于栈的,而不是基于寄存器的。

基于栈的优点是可移植性更好、指令更短、实现起来简单,但不能随机访问栈中的元素,完成相同功能所需要的指令数也比寄存器的要多,需要频繁的入栈和出栈。

基于寄存器的优点是速度快,有利于程序运行速度的优化,但操作数需要显式指定,指令也比较长。

Java 字节码由操作码和操作数组成。

  • 操作码(Opcode):一个字节长度(0-255,意味着指令集的操作码总数不可能超过 256 条),代表着某种特定的操作含义。
  • 操作数(Operands):零个或者多个,紧跟在操作码之后,代表此操作需要的参数。

由于 Java 虚拟机是基于栈而不是寄存器的结构,所以大多数指令都只有一个操作码。比如 aload_0(将局部变量表中下标为 0 的数据压入操作数栈中)就只有操作码没有操作数,而 invokespecial #1(调用成员方法或者构造方法,并传递常量池中下标为 1 的常量)就是由操作码和操作数组成的。

01、加载与存储指令

加载(load)和存储(store)相关的指令是使用最频繁的指令,用于将数据从栈帧的局部变量表和操作数栈之间来回传递。

1)将局部变量表中的变量压入操作数栈中

  • xload_(x 为 i、l、f、d、a,n 默认为 0 到 3),表示将第 n 个局部变量压入操作数栈中。
  • xload(x 为 i、l、f、d、a),通过指定参数的形式,将局部变量压入操作数栈中,当使用这个指令时,表示局部变量的数量可能超过了 4 个

解释一下。

x 为操作码助记符,表明是哪一种数据类型。见下表所示。


像 arraylength 指令,没有操作码助记符,它没有代表数据类型的特殊字符,但操作数只能是一个数组类型的对象。

大部分的指令都不支持 byte、short 和 char,甚至没有任何指令支持 boolean 类型。编译器会将 byte 和 short 类型的数据带符号扩展(Sign-Extend)为 int 类型,将 boolean 和 char 零位扩展(Zero-Extend)为 int 类型。

举例来说。

private void load(int age, String name, long birthday, boolean sex) {
    System.out.println(age + name + birthday + sex);
}

通过 jclasslib 看一下 load() 方法(4 个参数)的字节码指令。


  • iload_1:将局部变量表中下标为 1 的 int 变量压入操作数栈中。
  • aload_2:将局部变量表中下标为 2 的引用数据类型变量(此时为 String)压入操作数栈中。
  • lload_3:将局部变量表中下标为 3 的 long 型变量压入操作数栈中。
  • iload 5:将局部变量表中下标为 5 的 int 变量(实际为 boolean)压入操作数栈中。

通过查看局部变量表就能关联上了。


2)将常量池中的常量压入操作数栈中

根据数据类型和入栈内容的不同,此类又可以细分为 const 系列、push 系列和 Idc 指令。

const 系列,用于特殊的常量入栈,要入栈的常量隐含在指令本身。


push 系列,主要包括 bipush 和 sipush,前者接收 8 位整数作为参数,后者接收 16 位整数。

Idc 指令,当 const 和 push 不能满足的时候,万能的 Idc 指令就上场了,它接收一个 8 位的参数,指向常量池中的索引。

  • Idc_w:接收两个 8 位数,索引范围更大。
  • 如果参数是 long 或者 double,使用 Idc2_w 指令。

举例来说。

public void pushConstLdc() {
    // 范围 [-1,5]
    int iconst = -1;
    // 范围 [-128,127]
    int bipush = 127;
    // 范围 [-32768,32767]
    int sipush= 32767;
    // 其他 int
    int ldc = 32768;
    String aconst = null;
    String IdcString = "沉默王二";
}

通过 jclasslib 看一下 pushConstLdc() 方法的字节码指令。


  • iconst_m1:将 -1 入栈。范围 [-1,5]。
  • bipush 127:将 127 入栈。范围 [-128,127]。
  • sipush 32767:将 32767 入栈。范围 [-32768,32767]。
  • ldc #6 <32768>:将常量池中下标为 6 的常量 32768 入栈。
  • aconst_null:将 null 入栈。
  • ldc #7 <沉默王二>:将常量池中下标为 7 的常量“沉默王二”入栈。

3)将栈顶的数据出栈并装入局部变量表中

主要是用来给局部变量赋值,这类指令主要以 store 的形式存在。

  • xstore_(x 为 i、l、f、d、a,n 默认为 0 到 3)
  • xstore(x 为 i、l、f、d、a)

明白了 xload_ 和 xload,再看 xstore_ 和 xstore 就会轻松得多,作用反了一下而已。

大家来想一个问题,为什么要有 xstore_ 和 xload_ 呢?它们的作用和 xstore n、xload n 不是一样的吗?

xstore_ 和 xstore n 的区别在于,前者相当于只有操作码,占用 1 个字节;后者相当于由操作码和操作数组成,操作码占 1 个字节,操作数占 2 个字节,一共占 3 个字节。

由于局部变量表中前几个位置总是非常常用,虽然 xstore_<n>xload_<n> 增加了指令数量,但字节码的体积变小了!

举例来说。

public void store(int age, String name) {
    int temp = age + 2;
    String str = name;
}

通过 jclasslib 看一下 store() 方法的字节码指令。


  • istore_3:从操作数中弹出一个整数,并把它赋值给局部变量表中索引为 3 的变量。
  • astore 4:从操作数中弹出一个引用数据类型,并把它赋值给局部变量表中索引为 4 的变量。

通过查看局部变量表就能关联上了。


02、算术指令

算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新压入操作数栈。可以分为两类:整型数据的运算指令和浮点数据的运算指令。

需要注意的是,数据运算可能会导致溢出,比如两个很大的正整数相加,很可能会得到一个负数。但 Java 虚拟机规范中并没有对这种情况给出具体结果,因此程序是不会显式报错的。所以,大家在开发过程中,如果涉及到较大的数据进行加法、乘法运算的时候,一定要注意!

当发生溢出时,将会使用有符号的无穷大 Infinity 来表示;如果某个操作结果没有明确的数学定义的话,将会使用 NaN 值来表示。而且所有使用 NaN 作为操作数的算术操作,结果都会返回 NaN。

举例来说。

public void infinityNaN() {
    int i = 10;
    double j = i / 0.0;
    System.out.println(j); // Infinity

    double d1 = 0.0;
    double d2 = d1 / 0.0;
    System.out.println(d2); // NaN
}
  • 任何一个非零的数除以浮点数 0(注意不是 int 类型),可以想象结果是无穷大 Infinity 的。
  • 把这个非零的数换成 0 的时候,结果又不太好定义,就用 NaN 值来表示。

Java 虚拟机提供了两种运算模式

  • 向最接近数舍入:在进行浮点数运算时,所有的结果都必须舍入到一个适当的精度,不是特别精确的结果必须舍入为可被表示的最接近的精确值,如果有两种可表示的形式与该值接近,将优先选择最低有效位为零的(类似四舍五入)。
  • 向零舍入:将浮点数转换为整数时,采用该模式,该模式将在目标数值类型中选择一个最接近但是不大于原值的数字作为最精确的舍入结果(类似取整)。

我把所有的算术指令列一下:

  • 加法指令:iadd、ladd、fadd、dadd
  • 减法指令:isub、lsub、fsub、dsub
  • 乘法指令:imul、lmul、fmul、dmul
  • 除法指令:idiv、ldiv、fdiv、ddiv
  • 求余指令:irem、lrem、frem、drem
  • 自增指令:iinc

举例来说。

public void calculate(int age) {
    int add = age + 1;
    int sub = age - 1;
    int mul = age * 2;
    int div = age / 3;
    int rem = age % 4;
    age++;
    age--;
}

通过 jclasslib 看一下 calculate() 方法的字节码指令。


  • iadd,加法
  • isub,减法
  • imul,乘法
  • idiv,除法
  • irem,取余
  • iinc,自增的时候 +1,自减的时候 -1

03、类型转换指令

可以分为两种:

1)宽化,小类型向大类型转换,比如 int–>long–>float–>double,对应的指令有:i2l、i2f、i2d、l2f、l2d、f2d。

  • 从 int 到 long,或者从 int 到 double,是不会有精度丢失的;
  • 从 int、long 到 float,或者 long 到 double 时,可能会发生精度丢失;
  • 从 byte、char 和 short 到 int 的宽化类型转换实际上是隐式发生的,这样可以减少字节码指令,毕竟字节码指令只有 256 个,占一个字节。

2)窄化,大类型向小类型转换,比如从 int 类型到 byte、short 或者 char,对应的指令有:i2b、i2s、i2c;从 long 到 int,对应的指令有:l2i;从 float 到 int 或者 long,对应的指令有:f2i、f2l;从 double 到 int、long 或者 float,对应的指令有:d2i、d2l、d2f。

  • 窄化很可能会发生精度丢失,毕竟是不同的数量级;
  • 但 Java 虚拟机并不会因此抛出运行时异常。

举例来说。

public void updown() {
    int i = 10;
    double d = i;

    float f = 10f;
    long ong = (long)f;
}

通过 jclasslib 看一下 updown() 方法的字节码指令。


  • i2d,int 宽化为 double
  • f2l, float 窄化为 long

04、对象的创建和访问指令

Java 是一门面向对象的编程语言,那么 Java 虚拟机是如何从字节码层面进行支持的呢?

1)创建指令

数组也是一种对象,但它创建的字节码指令和普通的对象不同。创建数组的指令有三种:

  • newarray:创建基本数据类型的数组
  • anewarray:创建引用类型的数组
  • multianewarray:创建多维数组

普通对象的创建指令只有一个,就是 new,它会接收一个操作数,指向常量池中的一个索引,表示要创建的类型。

举例来说。

public void newObject() {
    String name = new String("沉默王二");
    File file = new File("无愁河的浪荡汉子.book");
    int [] ages = {};
}

通过 jclasslib 看一下 newObject() 方法的字节码指令。


  • new #13 ,创建一个 String 对象。
  • new #15 ,创建一个 File 对象。
  • newarray 10 (int),创建一个 int 类型的数组。

2)字段访问指令

字段可以分为两类,一类是成员变量,一类是静态变量(static 关键字修饰的),所以字段访问指令可以分为两类:

  • 访问静态变量:getstatic、putstatic。
  • 访问成员变量:getfield、putfield,需要创建对象后才能访问。

举例来说。

public class Writer {
    private String name;
    static String mark = "作者";

    public static void main(String[] args) {
        print(mark);
        Writer w = new Writer();
        print(w.name);
    }

    public static void print(String arg) {
        System.out.println(arg);
    }
}

通过 jclasslib 看一下 main() 方法的字节码指令。


  • getstatic #2 ,访问静态变量 mark
  • getfield #6 ,访问成员变量 name

05、方法调用和返回指令

方法调用指令有 5 个,分别用于不同的场景:

  • invokevirtual:用于调用对象的成员方法,根据对象的实际类型进行分派,支持多态。
  • invokeinterface:用于调用接口方法,会在运行时搜索由特定对象实现的接口方法进行调用。
  • invokespecial:用于调用一些需要特殊处理的方法,包括构造方法、私有方法和父类方法。
  • invokestatic:用于调用静态方法。
  • invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法,并执行。

举例来说。

public class InvokeExamples {
    private void run() {
        List ls = new ArrayList();
        ls.add("难顶");

        ArrayList als = new ArrayList();
        als.add("学不动了");
    }

    public static void print() {
        System.out.println("invokestatic");
    }

    public static void main(String[] args) {
        print();
        InvokeExamples invoke = new InvokeExamples();
        invoke.run();
    }
}

我们用 javap -c InvokeExamples.class 来反编译一下。

Compiled from "InvokeExamples.java"
public class com.itwanger.jvm.InvokeExamples {
  public com.itwanger.jvm.InvokeExamples();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  private void run();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #4                  // String 难顶
      11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      16: pop
      17: new           #2                  // class java/util/ArrayList
      20: dup
      21: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
      24: astore_2
      25: aload_2
      26: ldc           #6                  // String 学不动了
      28: invokevirtual #7                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
      31: pop
      32: return

  public static void print();
    Code:
       0: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #9                  // String invokestatic
       5: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #11                 // Method print:()V
       3: new           #12                 // class com/itwanger/jvm/InvokeExamples
       6: dup
       7: invokespecial #13                 // Method "<init>":()V
      10: astore_1
      11: aload_1
      12: invokevirtual #14                 // Method run:()V
      15: return
}

InvokeExamples 类有 4 个方法,包括缺省的构造方法在内。

1)InvokeExamples() 构造方法中

缺省的构造方法内部会调用超类 Object 的初始化构造方法:

`invokespecial #1 // Method java/lang/Object."<init>":()V`

2)成员方法 run()

invokeinterface #5,  2  // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

由于 ls 变量的引用类型为接口 List,所以 ls.add() 调用的是 invokeinterface 指令,等运行时再确定是不是接口 List 的实现对象 ArrayList 的 add() 方法。

invokevirtual #7 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z

由于 als 变量的引用类型已经确定为 ArrayList,所以 als.add() 方法调用的是 invokevirtual 指令。

3)main() 方法中

invokestatic  #11 // Method print:()V

print() 方法是静态的,所以调用的是 invokestatic 指令。

方法返回指令根据方法的返回值类型进行区分,常见的返回指令见下图。


06、操作数栈管理指令

常见的操作数栈管理指令有 pop、dup 和 swap。

  • 将一个或两个元素从栈顶弹出,并且直接废弃,比如 pop,pop2;
  • 复制栈顶的一个或两个数值并将其重新压入栈顶,比如 dup,dup2,dup_×1,dup2_×1,dup_×2,dup2_×2;
  • 将栈最顶端的两个槽中的数值交换位置,比如 swap。

这些指令不需要指明数据类型,因为是按照位置压入和弹出的。

举例来说。

public class Dup {
    int age;
    public int incAndGet() {
        return ++age;
    }
}

通过 jclasslib 看一下 incAndGet() 方法的字节码指令。


  • aload_0:将 this 入栈。
  • dup:复制栈顶的 this。
  • getfield #2:将常量池中下标为 2 的常量加载到栈上,同时将一个 this 出栈。
  • iconst_1:将常量 1 入栈。
  • iadd:将栈顶的两个值相加后出栈,并将结果放回栈上。
  • dup_x1:复制栈顶的元素,并将其插入 this 下面。
  • putfield #2: 将栈顶的两个元素出栈,并将其赋值给字段 age。
  • ireturn:将栈顶的元素出栈返回。

07、控制转移指令

控制转移指令包括:

  • 比较指令,比较栈顶的两个元素的大小,并将比较结果入栈。
  • 条件跳转指令,通常和比较指令一块使用,在条件跳转指令执行前,一般先用比较指令进行栈顶元素的比较,然后进行条件跳转。
  • 比较条件转指令,类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。
  • 多条件分支跳转指令,专为 switch-case 语句设计的。
  • 无条件跳转指令,目前主要是 goto 指令。

1)比较指令

比较指令有:dcmpg,dcmpl、fcmpg、fcmpl、lcmp,指令的第一个字母代表的含义分别是 double、float、long。注意,没有 int 类型。

对于 double 和 float 来说,由于 NaN 的存在,有两个版本的比较指令。拿 float 来说,有 fcmpg 和 fcmpl,区别在于,如果遇到 NaN,fcmpg 会将 1 压入栈,fcmpl 会将 -1 压入栈。

举例来说。

public void lcmp(long a, long b) {
    if(a > b){}
}

通过 jclasslib 看一下 lcmp() 方法的字节码指令。


lcmp 用于两个 long 型的数据进行比较。

2)条件跳转指令


这些指令都会接收两个字节的操作数,它们的统一含义是,弹出栈顶元素,测试它是否满足某一条件,满足的话,跳转到对应位置。

对于 long、float 和 double 类型的条件分支比较,会先执行比较指令返回一个整形值到操作数栈中后再执行 int 类型的条件跳转指令。

对于 boolean、byte、char、short,以及 int,则直接使用条件跳转指令来完成。

举例来说。

public void fi() {
    int a = 0;
    if (a == 0) {
        a = 10;
    } else {
        a = 20;
    }
}

通过 jclasslib 看一下 fi() 方法的字节码指令。


3 ifne 12 (+9) 的意思是,如果栈顶的元素不等于 0,跳转到第 12(3+9)行 12 bipush 20

3)比较条件转指令


前缀“if_”后,以字符“i”开头的指令针对 int 型整数进行操作,以字符“a”开头的指令表示对象的比较。

举例来说。

public void compare() {
    int i = 10;
    int j = 20;
    System.out.println(i > j);
}

通过 jclasslib 看一下 compare() 方法的字节码指令。


11 if_icmple 18 (+7) 的意思是,如果栈顶的两个 int 类型的数值比较的话,如果前者小于后者时跳转到第 18 行(11+7)。

4)多条件分支跳转指令

主要有 tableswitch 和 lookupswitch,前者要求多个条件分支值是连续的,它内部只存放起始值和终止值,以及若干个跳转偏移量,通过给定的操作数 index,可以立即定位到跳转偏移量位置,因此效率比较高;后者内部存放着各个离散的 case-offset 对,每次执行都要搜索全部的 case-offset 对,找到匹配的 case 值,并根据对应的 offset 计算跳转地址,因此效率较低。

举例来说。

public void switchTest(int select) {
    int num;
    switch (select) {
        case 1:
            num = 10;
            break;
        case 2:
        case 3:
            num = 30;
            break;
        default:
            num = 40;
    }
}

通过 jclasslib 看一下 switchTest() 方法的字节码指令。


case 2 的时候没有 break,所以 case 2 和 case 3 是连续的,用的是 tableswitch。如果等于 1,跳转到 28 行;如果等于 2 和 3,跳转到 34 行,如果是 default,跳转到 40 行。

5)无条件跳转指令

goto 指令接收两个字节的操作数,共同组成一个带符号的整数,用于指定指令的偏移量,指令执行的目的就是跳转到偏移量给定的位置处。

前面的例子里都出现了 goto 的身影,也很好理解。如果指令的偏移量特别大,超出了两个字节的范围,可以使用指令 goto_w,接收 4 个字节的操作数。


巨人的肩膀:

https://segmentfault.com/a/1190000037628881

除了以上这些指令,还有异常处理指令和同步控制指令,我打算吊一吊大家的胃口,大家可以期待一波~~

(骚操作)

路漫漫其修远兮,吾将上下而求索

想要走得更远,Java 字节码这块就必须得硬碰硬地吃透,希望二哥的这些分享可以帮助到大家~

叨逼叨

二哥在 博客园 上写了很多 Java 方面的系列文章,有 Java 核心语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等,也算是体系完整了。


为了能帮助到更多的 Java 初学者,二哥把自己连载的《教妹学Java》开源到了 GitHub,尽管只整理了 50 篇,发现字数已经来到了 10 万+,内容更是没得说,通俗易懂、风趣幽默、图文并茂

GitHub 开源地址(欢迎 star):https://github.com/itwanger/jmx-java

如果有帮助的话,还请给二哥点个赞,这将是我继续分享下去的最强动力!

硬核万字长文,深入理解 Java 字节码指令(建议收藏)的更多相关文章

  1. 大话+图说:Java字节码指令——只为让你懂

    前言 随着Java开发技术不断被推到新的高度,对于Java程序员来讲越来越需要具备对更深入的基础性技术的理解,比如Java字节码指令.不然,可能很难深入理解一些时下的新框架.新技术,盲目一味追新也会越 ...

  2. 【JVM源码解析】模板解释器解释执行Java字节码指令(上)

    本文由HeapDump性能社区首席讲师鸠摩(马智)授权整理发布 第17章-x86-64寄存器 不同的CPU都能够解释的机器语言的体系称为指令集架构(ISA,Instruction Set Archit ...

  3. Java字节码指令收集大全

    Java字节码指令大全 常量入栈指令 指令码 操作码(助记符) 操作数 描述(栈指操作数栈) 0x01 aconst_null null值入栈. 0x02 iconst_m1 -1(int)值入栈. ...

  4. 从1+1=2来理解Java字节码从1+1=2来理解Java字节码

    编译"1+1"代码 首先我们需要写个简单的小程序,1+1的程序,学习就要从最简单的1+1开始,代码如下: 写好java类文件后,首先执行命令javac TestJava.java ...

  5. Java字节码指令

    1. 简介 Java虚拟机的指令由一个字节长度的.代表着某种特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成. 由于Java虚拟机采用面向操作数栈而不是寄存 ...

  6. 万字长文深入理解java中的集合-附PDF下载

    目录 1. 前言 2. List 2.1 fail-safe fail-fast知多少 2.1.1 Fail-fast Iterator 2.1.2 Fail-fast 的原理 2.1.3 Fail- ...

  7. 在Myeclipse下查看Java字节码指令信息

         在实际项目开发中,有时为了了解Java编译器内部的一些工作,需要查看Java文件对应的具体的字节码指令集,这里提供两种方式供参考. 一.使用javap命令      javap是JDK提供的 ...

  8. 从Java源码到Java字节码

    Java最主流的源码编译器,javac,基本上不对代码做优化,只会做少量由Java语言规范要求或推荐的优化:也不做任何混淆,包括名字混淆或控制流混淆这些都不做.这使得javac生成的代码能很好的维持与 ...

  9. 在Eclipse里查看Java字节码

    要理解 Java 字节码,比较推荐的方法是自己尝试编写源码对照字节码学习.其中阅读 Java 字节码的工具必不可少.虽然javap可以以可读的形式展示出.class 文件中字节码,但每次改动源码都需调 ...

随机推荐

  1. LTDC_DMA2D驱动实验

    STM32F429芯片使用LTDC.DMA2D.及RAM存储器,构成了一个完整的液晶控制器.LTDC负责不断刷新液晶屏(将数据从显存搬运到液晶屏),DMA2D用于图像数据搬运.混合及格式转换(将数据搬 ...

  2. 进程与线程 .Net Core系列-多线程

    进程与线程 进程: 狭义定义:进程是正在运行的程序的实例 广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动.它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分 ...

  3. ffmpeg入门到实战-ffmpeg是怎么转码的?

    阅读目录 视频是怎么被播放的? ffmpeg命令的格式 ffmpeg转码输出的过程 视频是怎么被播放的? 我们知道,当下大多数播放器都是基于ffmpeg二次开发的.你有没有想过,你用播放器打开一个视频 ...

  4. moment常用方法

    1.subtract方法,时间加减处理 console.log(moment().format("YYYY-MM-DD HH:mm:ss")); //当前时间 console.lo ...

  5. Redmine部署中遇到的问题

    Redmine部署文章: 第一篇:Redmine部署 第二篇:Redmine部署中遇到的问题 上一篇文章我写了Redmine怎样部署(点这里直达上一篇文章),这一篇就写一下在Redmine部署中遇到过 ...

  6. gRPC(2):四种基本通信模式

    在 gRPC(1):入门及简单使用(go) 中,我们实现了一个简单的 gRPC 应用程序,其中双方通信是简单的请求-响应模式,没发出一个请求都会得到一个响应,然而,借助 gRPC 可以实现不同的通信模 ...

  7. kustomize简单使用

    1.背景 在Kubernetes v1.14版本的发布说明中,kustomize 成为了 kubectl 内置的子命令,并说明了 kustomize 使用 Kubernetes 原生概念帮助用户创作并 ...

  8. 谁知道百会CRM跟Zoho是一家公司吗?

    说到ZohoCRM,无论是搜索引擎还是信息网站,总会有无数的身影.很多人不知道这两家公司的关系,甚至认为百会和Zoho是一家公司.那么,百会CRM和Zoho属于同一类公司吗?它们之间有什么关系?今天小 ...

  9. CentOS-Docker安装MySQL(单点)

    下载镜像 $ docker pull mysql 创建相关目录和文件 $ mkdir -p /usr/mysql/conf /usr/mysql/data $ chmod -R 755 /usr/my ...

  10. Linux:监测收集linux服务器性能数据工具Sysstat的使用与安装

    Sysstat是一个工具集,包括sar.pidstat.iostat.mpstat.sadf.sadc.其中sar是其中最强大,也是最能符合我们测试要求的工具,同时pidstat也是非常有用的东东,因 ...