Shared posts

30 Apr 00:52

Java代码到字节码——第一部分

by BlankKelly

原文地址 作者:James Bloom 译者:张坤

理解在Java虚拟机中Java代码如何别被编译成字节码并执行是非常重要的,因为这可以帮助你理解你的程序在运行时发生了什么。这种理解不仅能确保你对语言特性有逻辑上的认识而且做具体的讨论时可以理解在语言特性上的妥协和副作用。

这篇文章讲解了在Java虚拟机上Java代码是如何编译成字节码并执行的。想了解JVM内部架构和在字节码执行期间不同内存区域之间的差异可以查看我的上一篇文章 JVM 内部原理

这篇文章共分为三个部分,每个部分被划分为几个小节。你可以单独的阅读某一部分,不过你可以阅读该部分快速了解一些基本的概念。每一个部分将会包含不同的Java字节码指令然后解释它们图和被编译并作为字节码指令被执行的,目录如下:

  • 第一部分-基本编程概念
    • 变量
      • 局部变量
      • 成员变量
      • 常量
      • 静态变量
    • 条件语句
      • if-else
      • switch
    • 循环语句
      • while循环
      • for循环
      • do-while循环
  • 第二部分-面向对象和安全
    • try-catch-finally
    • synchronized
    • 方法调用
    • new (对象和数组)
  • 第三部分-元编程
    • 泛型
    • 注解
    • 反射

这篇文章包含很代码示例和生成的对应字节码。在字节码中每条指令(或操作码)前面的数字指示了这个字节的位置。比如一条指令如1: iconst_1 仅一个字节的长度,没有操作数,所以,接下来的字节码的位置为2。再比如这样一条指令1: bipush 5将会占两个字节,操作码bipush占一个字节,操作数5占一个字节。在这个示例中,接下来的字节码的位置为3,因为操作数占用的字节在位置2。

变量

局部变量

Java虚拟机是基于栈的架构。当一个方法包括初始化main方法执行,在栈上就会创建一个栈帧(frame),栈帧中存放着方法中的局部变量。局部变量数组(local veriable array)包含在方法执行期间用到的所有变量包括一个引用变量this,所有的方法参数和在方法体内定义的变量。对于类方法(比如:static方法)方法参数从0开始,然而,对于实例方法,第0个slot用来存放this。

一个局部变量类型可以为:

  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

除了long和double所有的类型在本地变量数组中占用一个slot,long和double需要两个连续的slot因为这两个类型为64位类型。

当在操作数栈上创建一个新的变量来存放一个这个新变量的值。这个新变量的值随后会被村方法到本地变量数组对应的位置上。如果这个变量不是一个基本类型,对应的slot上值存放指向这个变量的引用。这个引用指向存放在堆中的一个对象。

例如:

int i = 5;

被编译为字节码为:

0: bipush 5
2: istore_0

bipush:

将一个字节作为一个整数推送到操作数栈。在这个例子中5被推送到操作数栈。

istore_0:

它是一组格式为istore_操作数的其中之一,它们都是将一个整数存储到本地变量。n为在本地变量数组中的位置,取值只能为0,1,2,或者3。另一个操作码用作值大于3的情况,为istore,它将一个操作数放到本地变量数组中合适的位置。

上面的代码在内存中执行的情况如下:

java_local_veribale_

这个类文件中对应每一个方法还包含一个本地便变量表(local veribale table),如果这段代码被包含在一个方法中,在类文件对应于这个方法的本地变量表中你将会得到下面的实体(entry):

LocalVariableTable:
    Start  Length  Slot  Name   Signature
      0      1      1     i         I

成员变量(类变量)

一个成员变量(field)被作为一个类实例(或对象)的一部分存储在堆上。关于这个成员变量的信息被存放在类文件中field_info数组中,如下:

ClassFile {
    u4          magic;
    u2          minor_version;
    u2          major_version;
    u2          constant_pool_count;
    cp_info     contant_pool[constant_pool_count – 1];
    u2          access_flags;
    u2          this_class;
    u2          super_class;
    u2          interfaces_count;
    u2          interfaces[interfaces_count];
    u2          fields_count;
    field_info      fields[fields_count];
    u2          methods_count;
    method_info     methods[methods_count];
    u2          attributes_count;
    attribute_info  attributes[attributes_count];
}

另外,如果这个变量被初始化,进行初始化操作的字节码将被添加到构造器中。

当如下的代码被编译:

public class SimpleClass{
    public int simpleField = 100;
}

一个额外的小结将会使用javap命令来演示将成员变量添加到field_info数组中。

public int simpleField;
Signature: I
flags: ACC_PUBLIC

进行初始化操作的字节码被添加到构造器中,如下:

public SimpleClass();
  Signature: ()V
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=1, args_size=1
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        100
       7: putfield      #2                  // Field simpleField:I
      10: return

 

aload_0: 将本地变量数组slot中一个对象引用推送到操作数栈栈顶。尽管,上面的代码中显示没有构造器对成员变量进行初始化,实际上,编译器会创建一个默认的构造器对成员变量进行初始化。因此,第一个局部变量实际上指向this,因此,aload_0操作码将this这个引用变量推送到操作数栈。aload_0是一组格式为aload_的操作数中其中一员,它们的作用都是将一个对象引用推送到操作数栈。其中n指的是被访问的本地变量数组中这个对象引用所在的位置,取值只能为0,1,2或3。与之类似的操作码有iload_,lload_,fload_和dload_,不过这些操作码是用来加载值而不是一个对象引用,这里的i指的是int,l指的是long,f指的是float,d指的是double。本地变量的索引大于3的可以使用iload,lload,fload,dload和aload来加载,这些操作码都需要一个单个的操作数指定要加载的本地变量的索引。

invokespecial: invokespecial指令用来调用实例方法,私有方法和当前类的父类的方法。它是一组用来以不同的方式调用方法的操作码的一部分,包括,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual。invokespecial指令在这段代码用来调用父类的构造器。

bipush: 将一个字节作为一个整数推送到操作数栈。在这个例子中100被推送到操作数栈。

putfield: 后面跟一个操作数,这个操作数是运行时常量池中一个成员变量的引用,在这个例子中这个成员变量叫做simpleField。给这个成员变量赋值,然后包含这个成员变量的对象一起被弹出操作数栈。前面的aload_0指令将包含这个成员变量的对象和前面的bipush指令将100分别推送到操作数栈顶。putfield随后将它们都从操作数栈顶移除(弹出)。最终结果就是在这个对象上的成员变量simpleFiled的值被更新为100。

上面的代码在内存中执行的情况如下:

java_class_variable_creation_byte_code

 

putfield操作码有一个单个的操作数指向在常量池中第二个位置。JVM维护了一个常量池,一个类似于符号表的运行时数据结构,但是包含了更多的数据。Java中的字节码需要数据,通常由于这种数据太大而不能直接存放在字节码中,而是放在常量池中,字节码中持有一个指向常量池中的引用。当一个类文件被创建时,其中就有一部分为常量池,如下所示:

Constant pool:
   #1 = Methodref          #4.#16         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#17         //  SimpleClass.simpleField:I
   #3 = Class              #13            //  SimpleClass
   #4 = Class              #19            //  java/lang/Object
   #5 = Utf8               simpleField
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               SimpleClass
  #14 = Utf8               SourceFile
  #15 = Utf8               SimpleClass.java
  #16 = NameAndType        #7:#8          //  "<init>":()V
  #17 = NameAndType        #5:#6          //  simpleField:I
  #18 = Utf8               LSimpleClass;
  #19 = Utf8               java/lang/Object

常量(类常量)

被final修饰的变量我们称之为常量,在类文件中我们标识为ACC_FINAL

例如:

public class SimpleClass {

    public final int simpleField = 100;

}

变量描述中多了一个ACC_FINAL参数:

public static final int simpleField = 100;
Signature: I
flags: ACC_PUBLIC, ACC_FINAL
ConstantValue: int 100

不过,构造器中的初始化操作并没有受影响:

4: aload_0
5: bipush        100
7: putfield      #2                  // Field simpleField:I

静态变量

被static修饰的变量,我们称之为静态类变量,在类文件中被标识为ACC_STATIC,如下所示:

public static int simpleField;
Signature: I
flags: ACC_PUBLIC, ACC_STATIC

在实例构造器中并没有发现用来对静态变量进行初始化的字节码。静态变量的初始化是在类构造器中,使用putstatic操作码而不是putfield字节码,是类构造器的一部分。

static {};
  Signature: ()V
  flags: ACC_STATIC
  Code:
    stack=1, locals=0, args_size=0
       0: bipush         100
       2: putstatic      #2                  // Field simpleField:I
       5: return

条件语句

条件流控制,比如,if-else语句和switch语句,在字节码层面都是通过使用一条指令来与其它的字节码比较两个值和分支。

for循环和while循环这两条循环语句也是使用类似的方式来实现的,不同的是它们通常还包含一条goto指令,来达到循环的目的。do-while循环不需要任何goto指令因为他们的条件分支位于字节码的尾部。更多的关于循环的细节可以查看 loops section

一些操作码可以比较两个整数或者两个引用,然后在一个单条指令中执行一个分支。其它类型之间的比较如double,long或float需要分为两步来实现。首先,进行比较后将1,0或-1推送到操作数栈顶。接下来,基于操作数栈上值是大于,小于还是等于0执行一个分支。

首先,我们拿if-else语句为例进行讲解,其他用来进行分支跳转的不同的类型的指令将会被包含在下面的讲解之中。

if-else

下面的代码展示了一条简单的用来比较两个整数大小的if-else语句。

public int greaterThen(int intOne, int intTwo) {
    if (intOne > intTwo) {
        return 0;
    } else {
        return 1;
    }
}

这个方法编译成如下的字节码:

0: iload_1
1: iload_2
2: if_icmple        7
5: iconst_0
6: ireturn
7: iconst_1
8: ireturn  

首先,使用iload_1和iload_2将两个参数推送到操作数栈。然后,使用if_icmple比较操作数栈栈顶的两个值。如果intOne小于或等于intTwo,这个操作数分支变成字节码7。注意,在Java代码中if条件中的测试与在字节码中是完全相反的,因为在字节码中如果if条件语句中的测试成功执行,则执行else语句块中的内容,而在Java代码,如果if条件语句中的测试成功执行,则执行if语句块中的内容。换句话说,if_icmple指令是在测试如果if条件不为true,则跳过if代码块。if代码块的主体是序号为5和6的字节码,else代码块的主体是序号为7和8的字节码。

java_if_else_byte_code

下面的代码示例展示了一个稍微复杂点的例子,需要一个两步比较:

public int greaterThen(float floatOne, float floatTwo) {
    int result;
    if (floatOne > floatTwo) {
        result = 1;
    } else {
        result = 2;
    }
    return result;
}

这个方法产生如下的字节码:

0: fload_1
 1: fload_2
 2: fcmpl
 3: ifle          11
 6: iconst_1
 7: istore_3
 8: goto          13
11: iconst_2
12: istore_3
13: iload_3
14: ireturn

在这个例子中,首先使用fload_1和fload_2将两个参数推送到操作数栈栈顶。这个例子与上一个例子不同在于这个需要两步比较。fcmpl首先比较floatOne和floatTwo,然后将结果推送到操作数栈栈顶。如下所示:

floatOne > floatTwo -> 1

floatOne = floatTwo -> 0

floatOne < floatTwo -> -1 floatOne or floatTwo= Nan -> 1

接下来,如果fcmpl的结果是<=0,ifle用来跳转到索引为11处的字节码。

这个例子和上一个例子的不同之处还在于这个方法的尾部只有一个单个的return语句,而在if语句块的尾部还有一条goto指令用来防止else语句块被执行。goto分支对应于序号为13处的字节码iload_3,用来将本地变量表中第三个slot中存放的结果推送扫操作数栈顶,这样就可以由retrun语句来返回。

java_if_else_byte_code_extra_goto

和存在进行数值比较的操作码一样,也有进行引用相等性比较的操作码比如==,与null进行比较比如 == null和 != null,测试一个对象的类型比如 instanceof。

if_cmp eq ne lt le gt ge 这组操作码用于操作数栈栈顶的两个整数并跳转到一个新的字节码处。可取的值有:

  • eq – 等于
  • ne – 不等于
  • lt – 小于
  • le – 小于或等于
  • gt – 大于
  • ge – 大于或等于

if_acmp eq ne  这两个操作码用于测试两个引用相等(eq)还是不相等(ne),然后跳转到由操作数指定的新一个新的字节码处。

ifnonnull/ifnull 这两个字节码用于测试两个引用是否为null或者不为null,然后跳转到由操作数指定的新一个新的字节码处。

lcmp 这个操作码用于比较在操作数栈栈顶的两个整数,然后将一个值推送到操作数栈,如下所示:

  • 如果 value1 > value2 -> 推送1
  • 如果 value1 = value2 -> 推送0
  • 如果 value1 < value2 -> 推送-1

fcmp l g / dcmp l g 这组操作码用于比较两个float或者double值,然后将一个值推送的操作数栈,如下所示:

  • 如果 value1 > value2 -> 推送1
  • 如果 value1 = value2 -> 推动0
  • 如果value1 < value2 -> 推送-1

以l或g类型操作数结尾的差别在于它们如何处理NaN。fcmpg和dcmpg将int值1推送到操作数栈而fcmpl和dcmpl将-1推送到操作数栈。这就确保了在测试时如果两个值中有一个为NaN(Not A Number),测试就不会成功。比如,如果x > y(这里x和y都为doube类型),x和y中如果有一个为NaN,fcmpl指令就会将-1推送到操作数栈。接下来的操作码总会是一个ifle指令,如果这是栈顶的值小于0,就会发生分支跳转。结果,x和y中有一个为NaN,ifle就会跳过if语句块,防止if语句块中的代码被执行到。

instanceof 如果操作数栈栈顶的对象一个类的实例,这个操作码将一个int值1推送到操作数栈。这个操作码的操作数用来通过提供常量池中的一个索引来指定类。如果这个对象为null或者不是指定类的实例则int值0就会被推送到操作数栈。

if eq ne lt le gt ge 所有的这些操作码都是用来将操作数栈栈顶的值与0进行比较,然后跳转到操作数指定位置的字节码处。如果比较成功,这些指令总是被用于更复杂的,不能用一条指令完成的条件逻辑,例如,测试一个方法调用的结果。

switch

一个Java switch表达式允许的类型可以为char,byte,short,int,Character,Byte,Short.Integer,String或者一个enum类型。为了支持switch语句,Java虚拟机使用两个特殊的指令:tableswitchlookupswitch,它们背后都是通过整数值来实现的。仅使用整数值并不会出现什么问题,因为char,byte,short和enum类型都可以在内部被提升为int类型。在Java7中添加对String的支持,背后也是通过整数来实现的。tableswitch通过速度更快,但是通常占用更多的内存。tableswitch通过列举在最小和最大的case值之间所有可能的case值来工作。最小和最大值也会被提供,所以如果switch变量不在列举的case值的范围之内,JVM就会立即跳到default语句块。在Java代码没有提供的case语句的值也会被列出,不过指向default语句块,确保在最小值和最大值之间的所有值都会被列出来。例如,执行下面的swicth语句:

public int simpleSwitch(int intOne) {
    switch (intOne) {
        case 0:
            return 3;
        case 1:
            return 2;
        case 4:
            return 1;
        default:
            return -1;
    }
}

这段代码产生如下的字节码:

0: iload_1
1: tableswitch   {
         default: 42
             min: 0
             max: 4
               0: 36
               1: 38
               2: 42
               3: 42
               4: 40
    }
36: iconst_3
37: ireturn
38: iconst_2
39: ireturn
40: iconst_1
41: ireturn
42: iconst_m1
43: ireturn

ableswitch指令拥有值0,1和4去匹配Java代码中提供的case语句,每一个值指向它们对应的代码块的字节码。tableswitch指令还存在值2和3,它们并没有在Java代码中作为case语句提供,它们都指向default代码块。当这些指令被执行时,在操作数栈栈顶的值会被检查看是否在最大值和最小值之间。如果值不在最小值和最大值之间,代码执行就会跳到default分支,在上面的例子中它位于序号为42的字节码处。为了确保default分支的值可以被tableswitch指令发现,所以它总是位于第一个字节处(在任何需要的对齐补白之后)。如果值位于最小值和最大值之间,就用于索引tableswitch内部,寻找合适的字节码进行分支跳转。例如,值为,则代码执行会跳转到序号为38处的字节码。 下图展示了这个字节码是如何执行的:

java_switch_tableswitch_byte_code

如果在case语句中的值”离得太远“(比如太稀疏),这种方法就会不太可取,因为它会占用太多的内存。当switch中case比较稀疏时,可以使用lookupswitch来替代tableswitchlookupswitch会为每一个case语句例举出分支对应的字节码,但是不会列举出所有可能的值。当执行lookupswitch时,位于操作数栈栈顶的值会同lookupswitch中的每一个值进行比较,从而决定正确的分支地址。使用lookupswitch,JVM会查找在匹配列表中查找正确的匹配,这是一个耗时的操作。而使用tableswitch,JVM可以快速定位到正确的值。当一个选择语句被编译时,编译器必须在内存和性能二者之间做出权衡,决定选择哪一种选择语句。下面的代码,编译器会使用lookupswitch:

public int simpleSwitch(int intOne) {
    switch (intOne) {
        case 10:
            return 1;
        case 20:
            return 2;
        case 30:
            return 3;
        default:
            return -1;
    }
}

这段代码产生的字节码,如下:

0: iload_1
1: lookupswitch  {
         default: 42
           count: 3
              10: 36
              20: 38
              30: 40
    }
36: iconst_1
37: ireturn
38: iconst_2
39: ireturn
40: iconst_3
41: ireturn
42: iconst_m1
43: ireturn

为了更高效的搜索算法(比线性搜索更高效),lookupswitch会提供匹配值个数并对匹配值进行排序。下图显示了上述代码是如何被执行的:

java_switch_lookupswitch_byte_code

String switch

在Java7中,switch语句增加了对字符串类型的支持。虽然现存的实现switch语句的操作码仅支持int类型且没有新的操作码加入。字符串类型的switch语句分为两个部分完成。首先,比较操作数栈栈顶和每个case语句对应的值之间的哈希值。这一步可以通过lookupswitch或者tableswitch来完成(取决于哈希值的稀疏度)。这也会导致一个分支对应的字节码去调用String.equals()进行一次精确地匹配。一个tableswitch指令将利用String.equlas()的结果跳转到正确的case语句的代码处。

public int simpleSwitch(String stringOne) {
    switch (stringOne) {
        case "a":
            return 0;
        case "b":
            return 2;
        case "c":
            return 3;
        default:
            return 4;
    }
}

这个字符串switch语句将会产生如下的字节码:

0: aload_1
 1: astore_2
 2: iconst_m1
 3: istore_3
 4: aload_2
 5: invokevirtual #2                  // Method java/lang/String.hashCode:()I
 8: tableswitch   {
         default: 75
             min: 97
             max: 99
              97: 36
              98: 50
              99: 64
       }
36: aload_2
37: ldc           #3                  // String a
39: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
42: ifeq          75
45: iconst_0
46: istore_3
47: goto          75
50: aload_2
51: ldc           #5                  // String b
53: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
56: ifeq          75
59: iconst_1
60: istore_3
61: goto          75
64: aload_2
65: ldc           #6                  // String c
67: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
70: ifeq          75
73: iconst_2
74: istore_3
75: iload_3
76: tableswitch   {
         default: 110
             min: 0
             max: 2
               0: 104
               1: 106
               2: 108
       }
104: iconst_0
105: ireturn
106: iconst_2
107: ireturn
108: iconst_3
109: ireturn
110: iconst_4
111: ireturn

这个类包含这段字节码,同时也包含下面由这段字节码引用的常量池值。了解更多关于常量池的知识可以查看JVM内部原理这篇文章的 运行时常量池 部分。

Constant pool:
  #2 = Methodref          #25.#26        //  java/lang/String.hashCode:()I
  #3 = String             #27            //  a
  #4 = Methodref          #25.#28        //  java/lang/String.equals:(Ljava/lang/Object;)Z
  #5 = String             #29            //  b
  #6 = String             #30            //  c

 #25 = Class              #33            //  java/lang/String
 #26 = NameAndType        #34:#35        //  hashCode:()I
 #27 = Utf8               a
 #28 = NameAndType        #36:#37        //  equals:(Ljava/lang/Object;)Z
 #29 = Utf8               b
 #30 = Utf8               c

 #33 = Utf8               java/lang/String
 #34 = Utf8               hashCode
 #35 = Utf8               ()I
 #36 = Utf8               equals
 #37 = Utf8               (Ljava/lang/Object;)Z

注意,执行这个switch需要的字节码的数量包括两个tableswitch指令,几个invokevirtual指令去调用 String.equals()。了解更多关于invokevirtual的更多细节可以参看下篇文章方法调用的部分。下图显示了在输入“b”时代码是如何执行的:

java_string_switch_byte_code_1

java_string_switch_byte_code_2

java_string_switch_byte_code_3

如果不同case匹配到的哈希值相同,比如,字符串”FB”和”Ea”的哈希值都是28。这可以通过像下面这样轻微的调整equlas方法流来处理。注意,序号为34处的字节码:ifeg 42 去调用另一个String.equals() 来替换上一个不存在哈希冲突的例子中的 lookupsswitch操作码。

public int simpleSwitch(String stringOne) {
    switch (stringOne) {
        case "FB":
            return 0;
        case "Ea":
            return 2;
        default:
            return 4;
    }
}

上面代码产生的字节码如下:

0: aload_1
 1: astore_2
 2: iconst_m1
 3: istore_3
 4: aload_2
 5: invokevirtual #2                  // Method java/lang/String.hashCode:()I
 8: lookupswitch  {
         default: 53
           count: 1
            2236: 28
    }
28: aload_2
29: ldc           #3                  // String Ea
31: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
34: ifeq          42
37: iconst_1
38: istore_3
39: goto          53
42: aload_2
43: ldc           #5                  // String FB
45: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
48: ifeq          53
51: iconst_0
52: istore_3
53: iload_3
54: lookupswitch  {
         default: 84
           count: 2
               0: 80
               1: 82
    }
80: iconst_0
81: ireturn
82: iconst_2
83: ireturn
84: iconst_4
85: ireturn

循环

条件流控制,比如,if-else语句和switch语句都是通过使用一条指令来比较两个值然后跳转到相应的字节码来实现的。了解更多关于条件语句的细节可以查看 conditionals section

循环包括for循环和while循环也是通过类似的方法来实现的除了它们通常一个goto指令来实现字节码的循环。do-while循环不需要任何goto指令,因为它们的条件分支位于字节码的末尾。

一些字节码可以比较两个整数或者两个引用,然后使用一个单个的指令执行一个分支。其他类型之间的比较如double,long或者float需要两步来完成。首先,执行比较,将1,0,或者-1 推送到操作数栈栈顶。接下来,基于操作数栈栈顶的值是大于0,小于0还是等于0执行一个分支。了解更多关于进行分支跳转的指令的细节可以 see above

while循环

while循环一个条件分支指令比如 if_fcmpge if_icmplt(如上所述)和一个goto语句。在循环过后就理解执行条件分支指令,如果条件不成立就终止循环。循环中最后一条指令是goto,用于跳转到循环代码的起始处,直到条件分支不成立,如下所示:

public void whileLoop() {
    int i = 0;
    while (i < 2) {
        i++;
    }
}

被编译成:

0: iconst_0
 1: istore_1
 2: iload_1
 3: iconst_2
 4: if_icmpge       13
 7: iinc            1, 1
10: goto            2
13: return

if_cmpge指令测试在位置1处的局部变量是否等于或者大于10,如果大于10,这个指令就跳到序号为14的字节码处完成循环。goto指令保证字节码循环直到if_icmpge条件在某个点成立,循环一旦结束,程序执行分支立即就会跳转到return指令处。iinc指令是为数不多的在操作数栈上不用加载(load)和存储(store)值可以直接更新一个局部变量的指令之一。在这个例子中,iinc将第一个局部变量的值加 1。

java_while_loop_byte_code_1

java_while_loop_byte_code_2

for循环

for循环和while循环在字节码层面使用了完全相同的模式。这并不令人惊讶因为所有的while循环都可以用一个相同的for循环来重写。上面那个简单的的while循环的例子可以用一个for循环来重写,并产生完全一样的字节码,如下所示:

public void forLoop() {
    for(int i = 0; i < 2; i++) {

    }
}

do-while循环

do-while循环和for循环以及while循环也非常的相似,除了它们不需要将goto指令作为条件分支成为最后一条指令用于回退到循环起始处。

public void doWhileLoop() {
    int i = 0;
    do {
        i++;
    } while (i < 2);
}

产生的字节码如下:

0: iconst_0
 1: istore_1
 2: iinc          1, 1
 5: iload_1
 6: iconst_2
 7: if_icmplt     2
10: return

java_do_while_loop_byte_code_1

java_do_while_loop_byte_code_2

更多文章

下面两篇文章将会包含下列主体:

  • 第二部分 – 面向对象和安全(下篇文章)
    • try-catch-finally
    • synchronized
    • 方法条用(和参数)
    • new (对象和数组)
  • 第三部分 – 元编程
    • 泛型
    • 注解
    • 反射 了解更多关于虚拟机内部架构和字节码运行期间不同的内存区域可以查看我的上篇文章 JVM 内部原理

 

 

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: Java代码到字节码——第一部分

29 Apr 06:24

ConcurrentHashmap 解析

by importnewzz

ConcurrentHashmap(JDK1.7)

总体描述:

concurrentHashmap是为了高并发而实现,内部采用分离锁的设计,有效地避开了热点访问。而对于每个分段,ConcurrentHashmap采用final和内存可见修饰符volatile关键字(内存立即可见:Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程“看”到。注:并不能保证对volatile变量状态有依赖的其他操作的原子性)

借用某博客对concurrentHashmap对结构图:

不难看出,concurrenthashmap采用了二次hash的方式,第一次hash将key映射到对应的segment,而第二次hash则是映射到segment的不同桶中。

为什么要用二次hash,主要原因是为了构造分离锁,使得对于map的修改不会锁住整个容器,提高并发能力。当然,没有一种东西是绝对完美的,二次hash带来的问题是整个hash的过程比hashmap单次hash要长,所以,如果不是并发情形,不要使用concurrentHashmap。

代码实现:

该数据结构中,最核心的部分是两个内部类,HashEntry和Segment

concurrentHashmap维护一个segment数组,将元素分成若干段(第一次hash)

/**
* The segments, each of which is a specialized hash table.
*/
final Segment<K,V>[] segments;

segments的每一个segment维护一个链表数组

代码:

再来看看构造方法

public ConcurrentHashMap(int initialCapacity,
    float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
    throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
    concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    while (ssize < concurrencyLevel) {
    ++sshift;
    ssize <<= 1;
    }
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;
    if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
    ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
    cap <<= 1;
    // create segments and segments[0]
    Segment<K,V> s0 =
    new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
    (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

代码28行,一旦指定了concurrencyLevel(segments数组大小)便不能改变,这样,一旦threshold超标,rehash真不会影响segments数组,这样,在大并发的情况下,只会影响某一个segment的rehash而其他segment不会受到影响

(put方法都要上锁)

HashEntry

与hashmap类似,concurrentHashmap也采用了链表作为每个hash桶中的元素,不过concurrentHashmap又有些不同

static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
      
    HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
    this.hash = hash;
    this.key = key;
    this.value = value;
    this.next = next;
    }
      
    /**
    * Sets next field with volatile write semantics. (See above
    * about use of putOrderedObject.)
    */
    final void setNext(HashEntry<K,V> n) {
    UNSAFE.putOrderedObject(this, nextOffset, n);
    }
      
    // Unsafe mechanics
    static final sun.misc.Unsafe UNSAFE;
    static final long nextOffset;
    static {
    try {
    UNSAFE = sun.misc.Unsafe.getUnsafe();
    Class k = HashEntry.class;
    nextOffset = UNSAFE.objectFieldOffset
    (k.getDeclaredField("next"));
    } catch (Exception e) {
    throw new Error(e);
    }
    }
}

HashEntry的key,hash采用final,可以避免并发修改问题,HashEntry链的尾部是不能修改的,而next和value采用volatile,可以避免使用同步造成的并发性能灾难,新版(jdk1.7)的concurrentHashmap大量使用java Unsafe类提供的原子操作,直接调用底层操作系统,提高性能(这块我也不是特别清楚)

get方法(1.6 vs 1.7)

1.6

V get(Object key, int hash) { 
    if (count != 0) { // read-volatile 
    HashEntry<K,V> e = getFirst(hash); 
    while (e != null) { 
    if (e.hash == hash && key.equals(e.key)) { 
    V v = e.value; 
    if (v != null) 
    return v; 
    return readValueUnderLock(e); // recheck 
    } 
    e = e.next; 
    } 
    } 
    return null; 
}

1.6的jdk采用了乐观锁的方式处理了get方法,在get的时候put方法正在new对象,而此时value并未赋值,这时判断为空则加锁访问

1.7

public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
    (tab = s.table) != null) {
    for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
    (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
    e != null; e = e.next) {
    K k;
    if ((k = e.key) == key || (e.hash == h && key.equals(k)))
    return e.value;
    }
    }
    return null;
}

1.7并没有判断value=null的情况,不知为何

跟同事沟通过,无论是1.6还是1.7的实现,实际上都是一种乐观的方式,而乐观的方式带来的是性能上的提升,但同时也带来数据的弱一致性,如果你的业务是强一致性的业务,可能就要考虑另外的解决办法(用Collections包装或者像jdk6中一样二次加锁获取)

http://ifeve.com/concurrenthashmap-weakly-consistent/

这篇文章可以很好地解释弱一致性问题

put方法

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

对于put,concurrentHashmap采用自旋锁的方式,不同于1.6的直接获取锁

注:个人理解,这里采用自旋锁可能作者是觉得在分段锁的状态下,并发的可能本来就比较小,并且锁占用时间又并不是特别长,因此自旋锁可以减小线程唤醒和切换的开销

关于hash

private int hash(Object k) {
        int h = hashSeed;
        if ((0 != h) && (k instanceof String)) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h ^= k.hashCode();
        // Spread bits to regularize both segment and index locations,
        // using variant of single-word Wang/Jenkins hash.
        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
    }

concurrentHashMap采用本身hashcode的同时,采用Wang/Jenkins算法对每位都做了处理,使得发生hash冲突的可能性大大减小(否则效率会很差)

而对于concurrentHashMap,segments的大小在初始时确定,此后不变,而元素所在segments桶序列由hash的高位决定

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

segmentShift为(32-segments大小的二进制长度)

总结

concurrentHashmap主要是为并发设计,与Collections的包装不同,他不是采用全同步的方式,而是采用非锁get方式,通过数据的弱一致性带来性能上的大幅提升,同时采用分段锁的策略,提高并发能力

参考:

http://www.jb51.net/article/49699.htm

http://my.oschina.net/chihz/blog/58035

http://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/

http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

相关文章

29 Apr 02:34

C语言杂谈:指针与数组 (上)

by promumu

 思维导图

 

介绍

1> 指针定义:指针是保存变量地址的变量。

2> 本文重点
>> 指针与数组之间的关系
>> 操纵指针的规则

3> 指针优点
>> 表达某个计算的唯一途径
>> 代码更高效,更紧凑

4> 指针缺点:难以理解,但是用好了,代码会非常清晰。

5> 将指针、数组和地址的算术运算集成在一起是C语言的一大优点。

指针与地址

1> 内存组织方式

 

 (1) 内存是一个个单元组成的,每一个内存单元中存放一个字节(8位)的二进制信息。
 (2) 机器中的内存单元是有序排列的。
 (3) 机器给各个内存单元规定不同地址来管理内存。这样,CPU通过地址来识别不同的内存单元,正确的对内存单元进行操作。
2> 指针与变量的关系(P:是指针变量,C:内存对象)

>>> P:保存C:中的单元首地址——这里的地址不是物理地址,而是经过地址映射后的虚拟地址,即逻辑地址。

>>> P:为指向C:的指针

3>理解指针

>>> 指针占用的内存空间大小: 32位系统占用4byte,64为8byte。

机器配置:

打印指针大小:

>>> 指针就是地址——我们可以把指针认为是用来存放地址的数据类型。不能把指针简简单单的当成一个整型数,虽然地址的值是一个整型数据。

>>> 指针是有类型的,但是这个类型不是给指针分配内存的,而是用来寻址的。

 

指针与函数参数

1.普通参数:C语言通过传值方式将值传递给被调用函数。

>> 会把变量的值复制一份给被调用函数。
>> 复制:会把变量的值赋值给一个新的变量(参数)——变量和新的变量必须有相同的存储容量。
>> 被调用函数并不能修改主调程序中的变量值,因为被调用函数使用的是一个复制过来的内存单元。

2.指针参数: 本质上跟普通参数传递是相同的,也进行了变量复制,但是传过去的值是地址。 被调用函数通过地址能够访问和修改主调程序中变量的值。

3.参数在内存消耗

普通参数:取决于申明类型。char:1个字节;short:2个字节;long:8个字节
指针参数:指针变量里存储的是地址(一般是4个字节——32位),永远是一个固定长度,不管是什么类型的指针。——除非处理器变化不是32位。

4.double *dp, atof(char *) 这里的dp是指针变量,而atof是函数

 

指针与数组

1.指针操作数组快于下标操作数组

2.数组的空间分配.如int a[10];——会在空间分配出40个相邻的内存单元来(10*4)。

 

3.指针操作数组

int *pa;
pa = &a[0];

 

4.指针移动

int *pa;
int a[10];
pa = &a[0];

pa+1将指向下一个元素a[1]:

>> 内存中的变化:”指针加1″会根据指针指定的类型int移动4个内存单元,其实本身并没有移动,只是pa+1等于第5个内存单元地址——“指针加1”中的1的大小是取决于pa的类型int的,指针类型决定指针跨内存单元的步长。

>> pa+1 等于是指向第5个内存单元——a[1]的第一个内存单元。

5.规则:

>> &a[i]和a+i含义相同,相互使用。a+i是a之后第i个元素地址。
>> 数组名代表数组第一个元素的地址。

 

地址运算符

1. 指针初始化:0或表示地址的表达式。

2. “指针加1”中的“1”的大小根据数据类型的长度按比例缩放。如果int类型占4个字节的存储空间,对应的1按4倍计算。

 

验证:

>>> 若指向char类型的指针p的内存地址是0×000000,那么p+1后的地址是0×000001。

验证过程如下:

运行结果:

 

>>> 若指向int 类型的指针p的内存地址是0×000000,那么p+1后的地址是0×000004。

运行结果:

3.指向不同数组的元素的指针之间的算术或比较运算都没有定义。

4.指针相减:如果p和q指向相同数组中的元素,且p<q,那么q-p+1就是p和q之间的元素(包括p和q)

代码验证:

运行结果:

流程变化:q-p=16 => 16/4=4 (按照int型所占内存单元等比例缩放) => 4 + 1 = 5;

 

总结

这次写关于c语言方面指针,是因为这两天看php内核文件的时候,由于C方面的欠缺,所以看着很吃力。所以想再复习下C语言。

为什么从指针入手呢?可能是因为指针在C语言中是比较难的。所以先把最难的啃下来。

本来是想一次性写完,可是指针这方面内容太多,所以决定分几批写。

我在C语言方面还是很薄弱,如果文章中有错误,希望高手们指点下。

参考文献:《C程序设计语言》

C语言杂谈:指针与数组 (上),首发于博客 - 伯乐在线

29 Apr 02:32

Java 理论与实践: 正确使用 Volatile 变量

by importnewzz

Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。本文介绍了几种有效使用 volatile 变量的模式,并强调了几种不适合使用 volatile 变量的情形。

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

Volatile 变量

Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。

出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。当使用 volatile 变量而非锁时,某些习惯用法(idiom)更加易于编码和阅读。此外,volatile 变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。

正确使用 volatile 变量的条件

您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  • 对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使 x 的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。)

大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile 变量不能像 synchronized 那样普遍适用于实现线程安全。清单 1 显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。

清单 1. 非线程安全的数值范围类
@NotThreadSafe 
public class NumberRange {
    private int lower, upper;

    public int getLower() { return lower; }
    public int getUpper() { return upper; }

    public void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException(...);
        lower = value;
    }

    public void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException(...);
        upper = value;
    }
}

这种方式限制了范围的状态变量,因此将 lower 和 upper 字段定义为 volatile 类型不能够充分实现类的线程安全;从而仍然需要使用同步。否则,如果凑巧两个线程在同一时间使用不一致的值执行 setLower 和 setUpper 的话,则会使范围处于不一致的状态。例如,如果初始状态是(0, 5),同一时间内,线程 A 调用 setLower(4) 并且线程 B 调用 setUpper(3),显然这两个操作交叉存入的值是不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是 (4, 3) —— 一个无效值。至于针对范围的其他操作,我们需要使 setLower() 和setUpper() 操作原子化 —— 而将字段定义为 volatile 类型是无法实现这一目的的。

性能考虑

使用 volatile 变量的主要原因是其简易性:在某些情形下,使用 volatile 变量要比使用相应的锁简单得多。使用 volatile 变量次要原因是其性能:某些情况下,volatile 变量同步机制的性能要优于锁。

很难做出准确、全面的评价,例如 “X 总是比 Y 快”,尤其是对 JVM 内在的操作而言。(例如,某些情况下 VM 也许能够完全删除锁机制,这使得我们难以抽象地比较 volatile 和 synchronized 的开销。)就是说,在目前大多数的处理器架构上,volatile 读操作开销非常低 —— 几乎和非 volatile 读操作一样。而 volatile 写操作的开销要比非 volatile 写操作多很多,因为要保证可见性需要实现内存界定(Memory Fence),即便如此,volatile 的总开销仍然要比锁获取低。

volatile 操作不会像锁一样造成阻塞,因此,在能够安全使用 volatile 的情况下,volatile 可以提供一些优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相比,volatile 变量通常能够减少同步的性能开销。

正确使用 volatile 的模式

很多并发性专家事实上往往引导用户远离 volatile 变量,因为使用它们要比使用锁更加容易出错。然而,如果谨慎地遵循一些良好定义的模式,就能够在很多场合内安全地使用 volatile 变量。要始终牢记使用 volatile 的限制 —— 只有在状态真正独立于程序内其他内容时才能使用 volatile —— 这条规则能够避免将这些模式扩展到不安全的用例。

模式 #1:状态标志

也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

很多应用程序包含了一种控制结构,形式为 “在还没有准备好停止程序时再执行一些工作”,如清单 2 所示:

清单 2. 将 volatile 变量作为状态标志使用
volatile boolean shutdownRequested;

...

public void shutdown() { shutdownRequested = true; }

public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

很可能会从循环外部调用 shutdown() 方法 —— 即在另一个线程中 —— 因此,需要执行某种同步来确保正确实现 shutdownRequested 变量的可见性。(可能会从 JMX 侦听程序、GUI 事件线程中的操作侦听程序、通过 RMI 、通过一个 Web 服务等调用)。然而,使用 synchronized块编写循环要比使用清单 2 所示的 volatile 状态标志编写麻烦很多。由于 volatile 简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile。

这种类型的状态标记的一个公共特性是:通常只有一种状态转换;shutdownRequested 标志从 false 转换为 true,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期不被察觉的情况下才能扩展(从 false 到 true,再转换到 false)。此外,还需要某些原子状态转换机制,例如原子变量。

模式 #2:一次性安全发布(one-time safe publication)

缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原语值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象)。

实现安全发布对象的一种技术就是将对象引用定义为 volatile 类型。清单 3 展示了一个示例,其中后台线程在启动阶段从数据库加载一些数据。其他代码在能够利用这些数据时,在使用之前将检查这些数据是否曾经发布过。

清单 3. 将 volatile 变量用于一次性安全发布
public class BackgroundFloobleLoader {
    public volatile Flooble theFlooble;

    public void initInBackground() {
        // do lots of stuff
        theFlooble = new Flooble();  // this is the only write to theFlooble
    }
}

public class SomeOtherClass {
    public void doWork() {
        while (true) { 
            // do some stuff...
            // use the Flooble, but only if it is ready
            if (floobleLoader.theFlooble != null) 
                doSomething(floobleLoader.theFlooble);
        }
    }
}

如果 theFlooble 引用不是 volatile 类型,doWork() 中的代码在解除对 theFlooble 的引用时,将会得到一个不完全构造的 Flooble

该模式的一个必要条件是:被发布的对象必须是线程安全的,或者是有效的不可变对象(有效不可变意味着对象的状态在发布之后永远不会被修改)。volatile 类型的引用可以确保对象的发布形式的可见性,但是如果对象的状态在发布后将发生更改,那么就需要额外的同步。

模式 #3:独立观察(independent observation)

安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。

使用该模式的另一种应用程序就是收集程序的统计信息。清单 4 展示了身份验证机制如何记忆最近一次登录的用户的名字。将反复使用lastUser 引用来发布值,以供程序的其他部分使用。

清单 4. 将 volatile 变量用于多个独立观察结果的发布
public class UserManager {
    public volatile String lastUser;

    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }
}

该模式是前面模式的扩展;将某个值发布以在程序内的其他地方使用,但是与一次性事件的发布不同,这是一系列独立事件。这个模式要求被发布的值是有效不可变的 —— 即值的状态在发布后不会更改。使用该值的代码需要清楚该值可能随时发生变化。

模式 #4:“volatile bean” 模式

volatile bean 模式适用于将 JavaBeans 作为“荣誉结构”使用的框架。在 volatile bean 模式中,JavaBean 被用作一组具有 getter 和/或 setter 方法 的独立属性的容器。volatile bean 模式的基本原理是:很多框架为易变数据的持有者(例如 HttpSession)提供了容器,但是放入这些容器中的对象必须是线程安全的。

在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。清单 5 中的示例展示了遵守 volatile bean 模式的 JavaBean:

清单 5. 遵守 volatile bean 模式的 Person 对象
@ThreadSafe
public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;

    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }

    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }

    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }

    public void setAge(int age) { 
        this.age = age;
    }
}

volatile 的高级模式

前面几节介绍的模式涵盖了大部分的基本用例,在这些模式中使用 volatile 非常有用并且简单。这一节将介绍一种更加高级的模式,在该模式中,volatile 将提供性能或可伸缩性优势。

volatile 应用的的高级模式非常脆弱。因此,必须对假设的条件仔细证明,并且这些模式被严格地封装了起来,因为即使非常小的更改也会损坏您的代码!同样,使用更高级的 volatile 用例的原因是它能够提升性能,确保在开始应用高级模式之前,真正确定需要实现这种性能获益。需要对这些模式进行权衡,放弃可读性或可维护性来换取可能的性能收益 —— 如果您不需要提升性能(或者不能够通过一个严格的测试程序证明您需要它),那么这很可能是一次糟糕的交易,因为您很可能会得不偿失,换来的东西要比放弃的东西价值更低。

模式 #5:开销较低的读-写锁策略

目前为止,您应该了解了 volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。

然而,如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。清单 6 中显示的线程安全的计数器使用synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

清单 6. 结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁”
@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;

    public int getValue() { return value; }

    public synchronized int increment() {
        return value++;
    }
}

之所以将这种技术称之为 “开销较低的读-写锁” 是因为您使用了不同的同步机制进行读写操作。因为本例中的写操作违反了使用 volatile 的第一个条件,因此不能使用 volatile 安全地实现计数器 —— 您必须使用锁。然而,您可以在读操作中使用 volatile 确保当前值的可见性,因此可以使用锁进行所有变化的操作,使用 volatile 进行只读操作。其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作,因此当使用 volatile 保证读代码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就像读-写操作一样。然而,要随时牢记这种模式的弱点:如果超越了该模式的最基本应用,结合这两个竞争的同步机制将变得非常困难。

结束语

与锁相比,Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件 —— 即变量真正独立于其他变量和自己以前的值 —— 在某些情况下可以使用 volatile 代替 synchronized 来简化代码。然而,使用 volatile 的代码往往比使用锁的代码更加容易出错。本文介绍的模式涵盖了可以使用 volatile 代替 synchronized 的最常见的一些用例。遵循这些模式(注意使用时不要超过各自的限制)可以帮助您安全地实现大多数用例,使用 volatile 变量获得更佳性能。

相关文章

27 Apr 06:39

封闭的苹果也一直在用开源软件,悄悄的

Image title

(本文由实习作者汪先登完成)

上周在 Cupertino 举办的工程师峰会上,苹果透露其采用了 apache 的 mesos 作为 Siri 的服务器架构。有趣的是,苹果给他自定义的 Mesos 取名为 JARVIS,或许是希望 Siri 可以像钢铁侠一样,成为真正的人工智能吧。

Mesos 由加利福利亚大学伯克利分校首先开发设计的分布式系统内核,用来更高效的运行和管理大数据中心。除此之外,Mesos 可以利用服务器的所有资源,寻找最佳的方式去完成任务。它还曾帮助 Twitter 解决了一个服务器高负载的问题,这件事为它在业界赢得了名声。

Mesos 更方便的一点是,它让 Siri 看起来就像是运行在一台电脑上,工程师可以很简便的更新 Siri,而不去考虑数以千计的服务器问题。就像在 Mesos 在其官方主页上说的一样,“让数据中心成为一个资源池”,它可以将不同的机器整合在一个逻辑计算机上面。当你拥有很多的物理资源并想构建一个巨大的静态的计算集群的时候,Mesos 就派上用场了。有很多的现代化可扩展性的数据处理应用都可以在 Mesos 上运行,包括 Hadoop、Kafka、Spark 等,同时你可以通过容器技术将所有的数据处理应用都运行在一个基础的资源池中。

一直以来,苹果给外界的都是一个很封闭的形象,但是最近一段时间以来,苹果使用越来越多的开源项目去搭建自己的应用,除了文中提到的 Mesos 之外,还发布了自有 ResearchKit 开源开发框架项目,更不用提此前收购的手势识别技术 PrimeSsense。渲染自己使用开源技术除了自身技术需要之外不排除还有其它原因:苹果近期一直不断地借 Tim Cook 之口宣传“缅怀故人”、“尊重女性”、“欢迎开放性取向”和“环保”等从前并不重视的“科技外”概念,他们正在极力地向外界表现出一种 Politically Right 的价值观,后续与美国近期的文化环境有关,也可能他们害怕自己变成曾经鄙视的 IBM “老大哥”形象。

消息来源:at cra 参考来源:TheVerge

26 Apr 07:49

JVM内部原理

by 梅小西

JVM内部原理

原文链接 原文作者:James D Bloom 翻译:梅小西(904516706)  校对:吴京润

 

这篇文章详细描述了Java虚拟机的内在结构。下面这张图来自《The Java Virtual Machine Specification Java SE 7 Edition》,它展示了一个典型的JVM的主要的内部结构。

JVM_Internal_Architecture_small
接下来的2个部分,将详细介绍这幅图中所有组成结构。 第一部分涵盖了每个线程都会生成的结构, 第二部分 涵盖了单独的每个线程生成的结构。

  • 线程
    • JVM 系统线程
    • 每个线程
    • 程序计数器 (PC)
    • 本地栈
    • 栈的限制
    • 栈帧
    • 局部变量表
    • 操作数栈
    • 动态连接
  • 线程间共享
    • 内存管理
    • 堆外内存
    • 即时(JIT)编译
    • 方法区
    • Class 文件结构
    • 类加载器
    • 快速类加载
    • 方法区在哪里
    • 类加载器的引用
    • 运行时常量池
    • 异常表
    • 符号表
    • 内部字符串 (String Table)

线程

线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行。在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。在一个Java线程准备好了所有的状态后,比如线程本地存储,缓存分配,同步的对象,栈以及程序计数器,这时一个操作系统中的本地线程也同时创建。当Java线程终止后,本地线程也会回收。操作系统因此负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,它就会调用Java线程中的run()方法。当run()方法返回,发生了未捕获异常,Java线程终止,本地线程就会决定是否JVM也应该被终止(是否是最后一个非守护线程) 。当线程终止后,本地线程和Java线程持有的资源都会被释放。

JVM 系统线程

如果你使用jconsole或者是任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用public static void main(String[])的main线程以及所有这个main线程自己创建的线程。这些主要的后台系统线程在Hotspot JVM里主要是以下几个:

虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括”stop-the-world”的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。

周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。

GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。

编译线程:这种线程在运行时会将字节码编译成到本地代码。

信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。

每个线程

每个执行线程都包含以下的部分:

程序计数器(PC)

当前非native指令(或者字节码)的地址。如果当前方法是native的,那么这个程序计数器便是无用的。所有CPU都有程序计数器,通常来说,程序计数器在每次指令执行后自增,它会维护下一个将要执行的指令的地址。JVM通过程序计数器来追踪指令执行的位置,在方法区中,程序计数器实际上是指向了一个内存地址。

每个线程都有自己的栈,它维护了在这个线程上正在执行的每个方法的栈帧。这个栈是一个后进先出的数据结构,所以当前正在执行的方法在栈的顶端,每当一个方法被调用时,一个新的栈帧就会被创建然后放在了栈的顶端。当方法正常返回或者发生了未捕获的异常,栈帧就会从栈里移除。栈是不能被直接操作的,尤其是栈帧对象的入栈和出栈,因此,栈帧对象有可能在堆里分配并且内存不需要连续。

本地栈

并不是所有的JVM都支持本地方法。不过那些支持的通常会创建出每个线程的本地方法栈。如果一个JVM已经实现了使用C-linkage 模型来支持Java本地调用,那么这个本地方法栈将会是一个C 栈。在这种情况下,参数的顺序以及返回值和传统的c程序在本地栈下几乎是一样的。一个native方法通常(取决于不同的JVM实现)会回调JVM,并且调用一个Java方法。这种native到Java的调用会发生在栈里(通常指Java栈)。这个线程会离开这个本地栈并且在栈上创建一个新的栈帧。

栈的限制

一个栈可以是动态的大小,也可以是指定的大小。如果一个线程需要一个大一点的栈,可能会导致StackOverflowError异常。如果一个线程需要一个新的栈帧而又没有足够的内存来分配,就会发生OutOfMemoryError异常。

栈帧

JVM为每个方法调用创建一个新的栈帧并推到每个方法调用的栈顶。当方法正常返回或者遇到了未捕获的异常,这个栈帧将被移除。想要了解更多的关于异常处理的可以看下面的“异常表”部分。

每个栈帧包含了:

  • 局部变量表
  • 返回值
  • 操作数栈
  • 当前方法所在的类的运行时常量池引用

局部变量表

局部变量表包含了这个方法执行期间所有用到的变量,包括this引用,所有方法参数以及其他的局部声明变量。对于类方法(比如静态方法)来说,所有方法参数下标都是从0开始,然而,对于实例方法来说这个0是留给this的。

一个局部变量可以是:

  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress

在局部变量表里,除了long和double,所有类型都是占了一个槽位,它们占了2个连续槽位,因为他们是64位宽度。

操作数栈

操作数栈用于字节码指令执行期间,就像通用寄存器在CPU里使用一样。大部分JVM的字节码各自操作出栈,入栈,复制,交换,或者执行操作,使其生产和消费各种数据。因此,在字节码里,指令把值在局部变量表和操作数栈之间频繁移动。比如,一个简单的变量初始化导致两个字节码在操作数栈里交互影响。

int i;

编译后得到下面字节码:

0:      iconst_0 // 将 0 入栈到操作数栈的顶端。

1:      istore_1 // 从操作数栈顶端弹出并保存到局部变量

想要了解更多关于局部变量表和操作数栈,运行时常量池之间的交互,请看下面的“class文件结构”。

动态链接

每个栈帧都包含了运行时常量池的引用。这个引用指向了这个栈帧正在执行的方法所在的类的常量池,它对动态链接提供了支持。

 

C/C++ 代码通常编译成一个对象文件,然后多个文件被链接起来生成一个可用的文件比如一个可执行文件或者动态链接库。在链接阶段,符号引用在每个对象文件里被替换成一个和最终执行相关的实际的内存地址。在Java里,这个链接过程在运行时是自动发生的。

当Java文件被编译时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。一个符号引用是一个逻辑引用并不是一个实际的指向一个物理内存地址的引用。不同的JVM实现能选择什么时候去解决符号引用,它通常发生在class文件加载后的验证,加载完成,立即调用或者静态解析等阶段,另外一种发生的时候是当符号引用第一次被使用,也叫做延迟或者延期解析。无论如何当每个引用第一次使用的时候,JVM必须保证解析发生,并抛出任何解析错误。绑定是一个字段,方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次,因为符号引用是完全替换的。如果符号引用关联到某个类,而这个类却还没准备好,就会引发类加载。每个直接引用被保存为偏移地址而不是和变量或者方法在运行时的位置相关的存储结构。

 

线程间共享

堆用于在运行时分配类实例和数组。数组和对象可能永远不会存储在栈上,因为一个栈帧并不是设计为在创建后会随时改变大小。栈帧仅仅保存引用,这个引用指向对象或者数组在堆中的位置。与局部变量表(每个栈帧里)中的基本数据类型和引用不同,对象总是被存储在堆里,所以他们在方法结束后不会被移除,仅仅在垃圾收集的时候才会被移除。

为了支持垃圾收集,堆被分为三个部分:

  • 年轻代
    • 常常又被划分为Eden区和Survivor区
  • 老年代(也被叫做年老代)
  • 持久代

内存管理

对象和数组不会被明确的释放,只有垃圾收集器会自动释放他们。

通常他们的工作流程如下:

  1. 新对象和数组被分配在年轻代。
  2. 年轻代会发生Minor GC。 对象如果仍然存活,将会从eden区移到survivor区。
  3. Major GC 通常会导致应用线程暂停,它会在2个区中移动对象,如果对象依然存活,将会从年轻代移到老年代。
  4. 当每次老年代进行垃圾收集的时候,会触发持久代带也进行一次收集。同样,在发生full gc的时候他们2个也会被收集一次。

堆外内存

堆外内存的对象在逻辑上是JVM的一部分,但是它却不是在堆里创建的。

堆外内存包括:

  • 持久代包含
    • 方法区
    • 内部字符串
  • 代码缓存 用于编译和保存已经被JIT编译器编译成的本地代码的方法。

即时 (JIT)编译

在JVM里,Java字节码被解释运行,但是它没有直接运行本地代码快。为了提高性能,Oracle Hotspot VM会寻找字节码的”热点”区域,它指频繁被执行的代码,然后编译成本地代码。这些本地代码会被保存在堆外内存的代码缓存区。Hotspot用这种方式,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。

 

方法区

方法区存储的是每个class的信息,例如:

  • 类加载器引用
  • 运行时常量池
    • 所有常量
    • 字段引用
    • 方法引用
    • 属性
  • 字段数据
    • 每个方法
      • 名字
      • 类型
      • 修饰符
      • 属性
    • 方法数据
      • 每个方法
        • 名字
        • 返回类型
        • 参数类型(按顺序)
        • 修饰符
        • 属性
      • 方法代码
        • 每个方法
          • 字节码
          • 操作数栈大小
          • 局部变量大小
          • 局部变量表
          • 异常表
            • 每个异常处理
              • 开始位置
              • 结束位置
              • 代码处理在程序计数器中的偏移地址
              • 被捕获的异常类的常量池索引

所有线程都共享同样的方法区,所以访问方法区的数据和动态链接的过程都是线程安全的。如果两个线程尝试访问一个类的字段或者方法而这个类还没有加载,这个类就一定会首先被加载而且仅仅加载一次,这2个线程也一定要等到加载完后才会继续执行。

 

类文件结构

一个编译好的类文件包含如下的结构:

ClassFile {

u4                         magic;

u2                         minor_version;

u2                         major_version;

u2                         constant_pool_count;

cp_info                    contant_pool[constant_pool_count – 1];

u2                         access_flags;

u2                         this_class;

u2                         super_class;

u2                        interfaces_count;

u2                         interfaces[interfaces_count];

u2                         fields_count;

field_info                 fields[fields_count];

u2                         methods_count;

method_info                methods[methods_count];

u2                         attributes_count;

attribute_info  attributes[attributes_count];

}

 

magic, minor_version, major_version:

关于类文件的固定的信息,以及这个类文件被编译的JDK版本号。

 

constant_pool:

和符号表类似,详情请看下面的“运行时常量池”

access_flags:

提供了类的修饰符清单

this_class:

指向常量池的索引,它提供了类的全限定名,如org/jamesdbloom/foo/Bar

super_class:

指向常量池的索引,它提供了一个到父类符号引用,如java/lang/Object

interfaces:

指向常量池索引集合,它提供了一个符号引用到所有已实现的接口

fields:

指向常量池索引集合,它完整描述了每个字段

methods:

指向常量池索引集合,它完整描述了每个方法的签名,如果这个方法不是抽象的或者不是native的,那么字节码中会体现出来

attributes:

不同值的集合,它提供了额外的关于这个类的信息,包括任何带有RetentionPolicy.CLASS 或者RetentionPolicy.RUNTIME的注解

 

通过使用javap这个命令,我们可以在已编译的class文件中看到字节码信息。

如果你编译下面这段代码


package org.jvminternals;

        public class SimpleClass {

                 public void sayHello() {

                         System.out.println(&amp;amp;quot;Hello&amp;amp;quot;);

                }

}

这时运行如下命令便可以看到接下来的输出

javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class

public class org.jvminternals.SimpleClass

SourceFile: “SimpleClass.java”

minor version: 0

major version: 51

flags: ACC_PUBLIC, ACC_SUPER

Constant pool:

#1 = Methodref          #6.#17         //  java/lang/Object.”<init>”:()V

#2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;

#3 = String             #20            //  “Hello”

#4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V

#5 = Class              #23            //  org/jvminternals/SimpleClass

#6 = Class              #24            //  java/lang/Object

#7 = Utf8               <init>

#8 = Utf8               ()V

#9 = Utf8               Code

#10 = Utf8               LineNumberTable

#11 = Utf8               LocalVariableTable

#12 = Utf8               this

#13 = Utf8               Lorg/jvminternals/SimpleClass;

#14 = Utf8               sayHello

#15 = Utf8               SourceFile

#16 = Utf8               SimpleClass.java

#17 = NameAndType        #7:#8          //  “<init>”:()V

#18 = Class              #25            //  java/lang/System

#19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;

#20 = Utf8               Hello

#21 = Class              #28            //  java/io/PrintStream

#22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V

#23 = Utf8               org/jvminternals/SimpleClass

#24 = Utf8               java/lang/Object

#25 = Utf8               java/lang/System

#26 = Utf8               out

#27 = Utf8               Ljava/io/PrintStream;

#28 = Utf8               java/io/PrintStream

#29 = Utf8               println

#30 = Utf8               (Ljava/lang/String;)V

{

public org.jvminternals.SimpleClass();

Signature: ()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   Lorg/jvminternals/SimpleClass;

 

public void sayHello();

Signature: ()V

flags: ACC_PUBLIC

Code:

stack=2, locals=1, args_size=1

0: getstatic      #2    // Field java/lang/System.out:Ljava/io/PrintStream;

3: ldc            #3    // String “Hello”

5: invokevirtual  #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V

8: return

LineNumberTable:

line 6: 0

line 7: 8

LocalVariableTable:

Start  Length  Slot  Name   Signature

0      9      0    this   Lorg/jvminternals/SimpleClass;

}

这个类文件说明在常量池有3个主要的部分,构造函数和sayHello方法。

  • 常量池 – 它提供了和符号表一样的信息,详细描述可以看后面的章节。
  • 方法– 每个方法包含4个区域:
    • 签名和访问标识
    • 字节码
    • 行号表 – 它为调试器提供了指向字节码关联的代码行信息,例如,sayHello方法中,字节码0代表的是第6行Java代码,字节码8代表的是第7行Java代码。
    • 局部变量表 – 栈帧里所有局部变量的集合,在所有的例子里局部变量都是指这个。

接下来介绍这个类文件中用到的字节码操作符。

 

aload_0

这个操作符是一组aload <n>格式操作符中的一种。他们加载一个对象引用到操作数栈。<n>指向局部变量集合中被访问的地址,但是值只能是0,1,2或者3。其他类似的操作符用于加载非对象引用,如iload_ <n>,lload_ <n>,fload_<n> 和dload_<n>,其中i是int类型,l是long类型,f是float类型,d是double类型。局部变量索引超过3的也可以用iload, lload, fload, dload 和aload加载。这些操作符都只加载单一的并且是明确的局部变量索引的操作数。

 

ldc

这种操作符用于将一个常量从运行时常量池推入到操作数栈。

 

getstatic

这种操作符用于将一个在运行时常量池里的静态值从静态字段列表推入到操作数栈。

 

invokespecial, invokevirtual

这种操作符是一系列方法调用操作符中的一种,比如 invokedynamic, invokeinterface,invokespecial, invokestatic, invokevirtual。在这个类文件里invokespecial 和 invokevirutal用于不同用途,invokevirutal用于调用一个基于对象的类方法,而invokespecial指令用于调用实例初始化方法,以及private方法,父类的方法。

 

return

这种操作符是一组操作符中的一种,比如ireturn, lreturn,freturn, dreturn, areturn 和return。每个操作符被指定了返回声明,他们返回不同的值,i用于int,l用于long,f用于float,d用于double,而a是对象的引用。没有return符号的将只返回void。

 

与局部变量,操作数栈以及运行时常量池交互的大部分操作数中,任何一个典型的字节码都如下所示。

.

构造函数有2个指令,第一个this被推入到操作数栈,接下来父类的构造函数被调用,它使用了this,并从操作数栈里弹出。

2
这个sayHello() 方法会更复杂,它必须通过运行时常量池将符号引用转成实际的引用,就像之前介绍的那样。第一个操作符getstatic将一个引用从System类里移出并推入到操作数栈的静态字段。接下来的操作符ldc将字符串”Hello”推入操作数栈。最后一个操作符invokevirtual调用System.out的println方法,把从操作数栈弹出字符串”Hello”作为参数并且创建一个当前线程的新的栈帧。

3

类加载器

JVM启动的时候通过引导类加载器加载一个初始类。这个类在调用public static void main(String[])方法之前被链接和初始化。这个方法的执行将依次导致所需的类的加载,链接和初始化。

 

加载 是一个通过指定的名字来查找当前类和接口并把它读取到一个字节数组的过程。下一步这个字节数组将被解析成一个确定的并带有major version和minor version的类对象。任何被直接父类指定了名字的类或者接口都会被加载。一旦这个过程完成,一个类或者接口对象便通过一个二进制表示的数据来创建完成。

 

链接 是一个类或接口验证以及类型、直接父类和父接口准备的过程。链接包含了3步,验证,准备以及部分解析。

 

        验证 是一个确定类或者接口是否是正确结构以及是否遵从Java语言和JVM规定的语法的过程。比如下面:

  1. 符号表中一致的,正确的格式
  2. final 方法 / 类没有被重写
  3. 方法遵从访问控制关键字
  4. 方法有正确的参数个数和类型
  5. 字节码没有不正确的操作栈结构
  6. 变量在使用前已经初始化
  7. 变量有正确的类型值

验证阶段的这些检查意味着它们不需要在运行的时候才进行。链接阶段的验证虽然拖慢了加载速度,但是它避免了在字节码运行时还需要进行各种检查。

 

        准备  涉及到静态存储的内存分配以及JVM会用到的任何的数据结构比如方法表。静态字段被创建和初始化为默认值,然而,在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。

 

        解析 是一个可选阶段,它通过加载引用的类或者接口来检查符号引用,以及检查引用的正确性。如果这时没有发生符号引用的解析,它会被延期到字节码指令使用之前进行。

 

初始化  一个类或者接口的初始化包含了执行类或者接口的初始化方法<clinit>
4

在JVM里有许多不同角色的类加载器。每个类加载器委托给父类加载器去加载,除了最顶层的引导类加载器(bootstrap classloader)。

引导类加载器(Bootstrap Classloader )通常被本地代码实现,因为它在JVM里是最早被实例化的。这个引导类加载器(bootstrap classloader)负责加载最基本的Java APIs,包括rt.jar。它仅仅加载启动的classpath里找到的拥有最高信任的类,这也就导致它会跳过很多给普通类进行的验证工作。

 

扩展类加载器(Extension Classloader )它加载Java标准扩展APIs类,比如security扩展类。

 

系统类加载器(System Classloader系统类加载器是默认的应用加载器,它加载classpath下的应用类。

 

用户定义类加载器(User Defined Classloaders) 是一个可替换的用于加载应用类的类加载器。一个用户自定义的类加载器一般用于多种特殊原因包括运行时重加载或者通常在web 服务器中将所需要的已加载的类分成不同组,比如Tomcat。

3

快速类加载

在HotSpot JVM 5.0版本中介绍了一种叫做类数据共享(CDS)的特性。在JVM安装过程中,它会加载一些关键的JVM类到内存映射共享存档里,比如rt.jar。CDS减少了加载这些类的时间,提高了JVM启动的速度,并且允许这些类在不同的JVM实例之间共享,降低了内存占用。

 

方法区在哪里

The Java Virtual Machine Specification Java SE 7 Edition 明确说明: “尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。” 相反,在Oracle JVM 的jconsole里会发现这个方法区(以及代码缓存)并不是堆的一部分。在OpenJDK代码里可以看到这个CodeCache在虚拟机里和ObjectHeap是不同的字段。

类加载器的引用

所有被加载的类都包含了一个指向加载他们自己的类加载器的引用。同样这个类加载器也包含了他自己加载的所有类的引用。

 

运行时常量池

JVM维护了每个类型的常量池,一个运行时数据结构和符号表很相似,尽管它包含了更多的数据。Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池,上面部分有介绍。

几种在常量池内存储的数据类型包括:

  • 数量值
  • 字符串值
  • 类引用
  • 字段引用
  • 方法引用

例如下面这段代码:

Object foo = new Object();

将会被编译成如下字节码:

0:      new #2               // Class java/lang/Object

1:      dup

2:      invokespecial #3    // Method java/ lang/Object “<init>”( ) V

这个new操作符(操作数代码)  后面紧跟#2 操作符。这个操作符是一个指向常量池的索引,因此它指向的是常量池的第2个入口。第2个入口是一个类引用,这个入口接下来引用的是另一个常量池入口,它包含类的名字,是一个UTF8常量字符串,内容为// Class java/lang/Object ,这个符号连接可以用于查找java.lang.Object这个类。new操作符创建了一个类实例并且实例化了它的值。一个指向新的类实例的引用会被加入到操作数栈。dup操作符这时会创建一个操作数栈最顶层元素的额外的拷贝,并且把它再次加入到操作数栈的顶部。最后在第2行通过invokespecial调用一个实例初始化方法。这个操作数同样包含一个指向常量池的引用。这个初始化方法从操作数池的顶端弹出一个元素并把它作为参数传给方法。最后便生成了一个指向这个新创建并被初始化的对象的引用。

如果你编译下面这个简单类:


package org.jvminternals;

public class SimpleClass {

        public void sayHello() {

                 System.out.println(&amp;amp;quot;Hello&amp;amp;quot;);

       }

}

这个已生成的类文件中的常量池像如下这样:

Constant pool:

#1 = Methodref          #6.#17         //  java/lang/Object.”<init>”:()V

#2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;

#3 = String             #20            //  “Hello”

#4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V

#5 = Class              #23            //  org/jvminternals/SimpleClass

#6 = Class              #24            //  java/lang/Object

#7 = Utf8               <init>

#8 = Utf8               ()V

#9 = Utf8               Code

#10 = Utf8               LineNumberTable

#11 = Utf8               LocalVariableTable

#12 = Utf8               this

#13 = Utf8               Lorg/jvminternals/SimpleClass;

#14 = Utf8               sayHello

#15 = Utf8               SourceFile

#16 = Utf8               SimpleClass.java

#17 = NameAndType        #7:#8          //  “<init>”:()V

#18 = Class              #25            //  java/lang/System

#19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;

#20 = Utf8               Hello

#21 = Class              #28            //  java/io/PrintStream

#22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V

#23 = Utf8               org/jvminternals/SimpleClass

#24 = Utf8               java/lang/Object

#25 = Utf8               java/lang/System

#26 = Utf8               out

#27 = Utf8               Ljava/io/PrintStream;

#28 = Utf8               java/io/PrintStream

#29 = Utf8               println

#30 = Utf8               (Ljava/lang/String;)V

这个常量池包含如下类型:

Integer

一个4字节的int类型常量

Long

一个8字节的long类型常量

Float

一个4字节的float类型常量

Double

一个8字节的double类型常量

String

一个字符串常量,指向另一个UTF8入口,在这个常量池里包含了实际的字节数据。

UTF8

一个代表UTF8编码字符序列的字节流。

Class

一个指向另一个UTF8入口的类常量 , 在这个常量池内部包含了JVM内部格式化的类名字的完全限定符(在动态链接过程里用到)。

NameAndType

冒号分隔的一对值,每个值指向另一个常量池的入口。第1个值(冒号前面)指向一个UTF8字符串入口,这个字符串是一个方法名或者字段名。第2个值指向一个UTF8入口,它代表一种类型,当前面是字段的时候,它就是字段类的全限定名,如果是方法,它就是每个参数类型的全限定名集合。

Fieldref, Methodref, InterfaceMethodref

逗号分隔的一对值,每个值指向另一个常量池的入口。第1个值(逗号前面)指向一个类入口。第2个值指向一个NameAndType入口。

异常表

异常表保存了每个异常处理信息比如:

  • 起始位置
  • 结束位置
  • 程序计数器记录的代码处理的偏移地址
  • 被捕获的异常类在常量池中的索引

如果一个方法定义了一个try-catch 或者try-finally的异常处理,就会创建一个异常表。它包含了每个异常处理或者finally块的信息,这些信息包括异常的处理范围,被处理的异常类型以及处理代码的位置。当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。

.

不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标(校对者注:此处原文似乎不正确,至少从JDK6开始,finally块会在每个方法返回前的分支复制一份,而不会发生跳转)。

符号表

在持久代里,除了有各种类型的运行时常量池外,JVM还维护了一个符号表。这个符号表是一个哈希表,它从符号指针映射到符号(比如Hashtable<Symbol*, Symbol>),并且还包含了一个指向所有符号的指针,包括每个类的运行时常量池中维护的符号。

 

引用计数器用于控制当一个符号从符号表移除的时候。比如当一个类被卸载时,所有在运行时常量池中维护的符号的引用计数将减少。当符号表里的一个符号引用计数器变成0,这个符号表就知道这个符号将不再被引用,并且这个符号会从符号表里卸载。对符号表和字符串表来说,为了提高效率和保证每个入口只出现一次,所有的入口被维护在一个标准化的格式里。

 

内部字符串 (String Table)

Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列,并且必须是同样的字符串实例。另外,如果String.intern() 被一个String 实例调用,它应该返回一个相同的引用。如果一个字符串是一个固定的字面量,那么下面会是返回true。

(“j” + “v” + “m”).intern() == “jvm”

在Hotspot JVM里,字符串表维护了内部的字符串,它是一个哈希表结构,从对象指针映射到符号(例如Hashtable<oop, Symbol>),并且是维护在持久代里。对符号表和字符串表来说,为了提高效率和保证每个入口只出现一次,所有的入口被维护在一个标准化的格式里。

 

类被加载的时候,字符串的字面量由编译器自动的内部化,并且加入到符号表里。此外,通过调用String.intern()方法,String类的实例能够明确的被内部化。调用String.intern()方法时,如果这个符号表里已经包含了这个字符串,那么将返回指向它的引用,如果不包含,那这个字符串就会被加入到字符串表并且返回它的引用。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: JVM内部原理

26 Apr 01:21

我是如何拿到Facebook Offer的

by scsecrystal

写在前面

2014年10月,我有幸通过了Facebook的电面,参加了在Palo Alto的on-site面试,并最终成功拿到了offer。期间有很多经历的东西想要记录下来,以做备忘。同时在当时准备的时候,发现国内对于Facebook面试经历的资料和分享时少之又少。因此,也想以记录的方式和大家分享经验,让更多的华人成为Facebook的一员!整个记录会分为三个大部分:面试流程,面试题集锦,入职流程和生活准备。前两部分为了避免误人子弟,我尽可能的客观描述,如非特别需要减少主观的理解在其中。

由于我申请的职位是MySQL Database Administrator,相对比较冷门。为了使得读者受众面更广,我尽可能的挑选面试中对于程序员和其他IT岗位能普遍试用的经验来作重点描述。如果你受不了博主的流水账叙述形式,可以直接跳到最后一节,获取简要通关秘籍。:)

基础要求

我们来看看要成为一个Facebook的潜在员工候选人,需要有哪些硬条件。要求的远比你想到的要简单很多:

  • 学历 由于拿到offer后,办理工作签证时出示本科成绩单。所以本科学历是最低要求的。
  • 英语 英语没有硬性要求,不需要雅思托福成绩。个人觉得能无障碍的听懂youtube上的技术分享,会一些基本日常语法加上相关专业词汇,就能比较顺利的完成电面和人肉面。
  • 专业经验 没有硬性的相关领域证书要求,当然如果你没有内推渠道,有个把证可以增加通过简历过滤器脱引而出的机会。
  • 技术经验 是否有能力维护设计Facebook服务器量级的系统是一个重要考察点。当然不要求你一定要经历过这么大的量级经验(毕竟这样的公司不多)。
  • 家庭 “一人Offer,全家受益”是我对Facebook Relocation的总结。拿到Offer后的所有环节,Facebook都会把你的家庭(配偶和子女)作为一个整体考虑进去。所以只要家人支持,家庭不会成为入职的羁绊。
  • 国外生活经历 博主在去Facebook前,除了一次自助蜜月游,从来没有出过国。也证明这方面没有硬性要求。个人觉得生活就像学游泳,扔进水里了,扑腾几下怎么样都会了。
  • 会翻墙 呵呵。。。

看了那么多,是不是觉得自己也是个合格的FB准候选人呢? :) 火速进入网申阶段。

第0阶段:网申

其实因为我cenalulu本来就安排在2014年9月底的时候去一次旧金山。所以一开始是报着试试看想法,并带着万一要on-site面试,我还可以省个机票钱的心态,通过Linkedin找了几家正在招募MySQL DBA职位的硅谷公司。从中挑选了几个巨头投了简历,包括:Google,EA,Apple,Linkedin,Twitter,Facebook(后简称:FB)。悲剧的是,只有FB通过了简历筛选,并得到了邮件回复。之后从已经在这些公司工作过的朋友那边了解到,海投简历确实是一个效率比较低下的方式。很有可能因为简历关键字匹配不成功就直接失去了后续面试机会。所以,如此悲催的首轮通过率也不足为奇了。他们强烈建议如果想从硅谷公司的简历筛选中脱颖而出,还是尽可能找公司员工做内推。{: style=”color: red”}
不过,相较于国内公司,硅谷巨头做的比较好的一点就是无论你简历通过与否,都会在两天内给到邮件结果。所以,网投时留得邮箱建议是能够每天查收一次的。下面是我众多悲剧(被拒)信中的一封:

Thank you for your interest in a role at Twitter. We have reviewed your experience in regards to this open position, and unfortunately do not see a strong match for you at this time. We will also keep your resume on file should a future match become available. We realize it is a time commitment to engage any company in the application process and we sincerely appreciate your efforts.

关于职位的具体信息,各个巨头都有自己的招聘主页。也可以通过Linkedin搜索。FB家的职位具体信息可以通过:https://www.facebook.com/careers?_rdr查找。什么?博主!这个链接打不开啊!好吧,翻墙对于大陆应聘者是一道“面试题”。
此外,走整个申请面试流程前,确保自己已经准备好了。因为,FB的效率非常高,从网申到第一次电面最短会只有两三天的时间。一旦闯关失败,距离下一次申请需要有至少半年的冷却时间。

第1阶段:电面

在和HR互通几封邮件后我们商定了电话面试时间。由于时差问题,面试定在了北京时间凌晨1点。Facebook的工作时间是当地时间早上9点到晚上6点,对应的北京时间是凌晨1点到第二天早上10点,如果是美国夏令时的话,那么时差从16个小时减少到15个小时。所以对于大陆应聘者来说面试时间有两个选择:一个是熬夜到凌晨,一个是早起面试。博主由于当时还是在前公司任职,早上10点在公司面试并不是一个很稳妥的选择。因此几次电面时间都是定的凌晨在家面试

如果进行顺利的话总共会有3-4次电面(我进行了3次,具体数量按照岗位要求和面试质量决定)。电面采用直接淘汰制。电面结果直接决定了你能够进入下一轮(不是多次面试成绩取平均)。所以,对于每一轮电面都要格外的重视。每次电面时间都控制在45钟内,技术电面无论题目是否完成,都会用足45分钟。电面方式可选电话(由面试官座机打过来)或者skype。博主建议使用手机,Skype通话质量不太稳定。我的电面过程中,两种都用过。相比之下电话的体验更好些,因为可以把电话内容录下来之后做回顾。每次电面前,我都会提前半小时呆在一个安静的房间,做一些简单的面经操练进行热身。然后确保电话畅通,耳机音量OK,Skype在线。一般来说面试官的来电时间非常准时,我的三次电面来电时间都和约定时间误差不超过5分钟!这也是FB严谨的招聘风格的一部分。电面全程使用英语。每次电面的一开始,面试官会有简短的自我介绍,并对本次电话面试的时间和内容安排做一个约有5分钟的详细介绍。这部分是你适应面试官口音的一个绝佳环节。我的运气较好所有电面都是英语的native speaker。如果你对于印度口音承受力较差的话建议先在youtube上搜一些印度哥们的技术分享视频研究下

以上就是每次电面共性的内容,下面按照电面轮次,逐一详细介绍:

注:本文只介绍流程,所有技术面的题目会在后续文章中分享

1.1 第一次电面

第一次电面全程是Recruiter(即国内常说的HR)。整个招聘流程中从电面到入职,除了技术考官外,他是你唯一的联系接口人。所以给他留下一个好印象当然是成功的第一步。简单的流程介绍后,就要求我进行一个自我介绍。然后会根据几个和应聘职位相关的工作经历提几个具体的非技术问题。之后会进行客观基础题的问答。所谓客观基础题就是有唯一标准答案的问答题。例如:Linux上HTTP上的端口是多少? MySQL和Linux基础题各20题。答题期间,recruiter不会告诉你正确与否。因此,在答题结束前是可以修正之前的答案的。我就这么修正过一次。所有40题答完之后,面试官会告知答错了几题。我当时是错了一题,面试官暗示成绩应该足以让我进入下一轮面试(具体的及格线我不清楚)。答完题以后就是,Q&A环节。问了两个事先准备的套路问题,一个关于职位本身,一个关于公司文化。最后,互相感谢,等待面试官先挂断电话。

1.2 第二次电面

第二次电面是coding技术面,由将来的team内部的员工全程主持。coding技术面的形式是,45分钟内,面试官会给出4道技术题,让面试者在 Stypi上进行答题。Stypi是一个在线协同代码编辑网站,即你的实时代码编写和修改都会在面试官那边展现出来(可以理解是网页版的远程桌面)。每道题都会通过Stypi贴在编辑区域内,然后答题者在编辑区域内当场进行coding。每次出完题后,如果觉得题目表述不清楚或者觉得模棱两可的地方可以即使询问面试官。每一题答完后,面试官如果决定有明显的bug或者效率比较低的地方会提出,让答题者进行修改,或者口述改进方案(具体根据时间进度而定)。
下面是一个Stypi界面的截图:

1.3 第三次电面

由于应聘的是MySQL Database Administrator,电面也必然免不了进行MySQL技术面试。第三面就是另一个来自将来同事的面试。该轮面试是问答形式,因此也就没有用到电脑。题目由浅入深,考察的都是MySQL的一些基础知识。同时也会根据简历上的自我介绍和项目经验进行深入的提问。

1.4 第四次电面

本来在第一次电面中Recruiter提到会有一共会有四轮电面,且第四轮电面是故障排查演练。也许是进度原因(因为离我出发去旧金山只有一周了),又也许是之前的考察已经达到了目的。Recruiter邮件告知我,接下来就直接去Palo Alto总部面试了。Bravo!


第2阶段 On-site面试

2.0 面试前夕

确认有on-site面试资格后,面试官会确认具体面试时间。同时,让候选人办理入境签证。由于我因为之前的出国计划,已经有了B1/B2的visa所以就略过了这一步。一般来说美国签证的周期在两周以上,包括材料准备,提交,大使馆面签,护照快递等。按照之后的经验,所有这些流程都会有FB指定的代理商BAL跟踪协助。所以,整个流程会非常的省心,非常的人性化!

由于我的个人安排,机票和住宿是自己搞定的。实际上根据FB的政策,所有面试的来回机票费用和住宿费用都是全包的。按照之后的经验,机票会由FB的指定代理CWT代为下单,商务舱标准。酒店是五星级,时间一般最长三天,即:面试前夜,面试当天和面试后的调整日。这两部分的钱都是FB支付。此外,FB允许报销面试期间发生的生活费用,每天150美元。包括:来回酒店机场的出租票,伙食费和基本生活用品。这部分的费用在面试结束后一个月内,通过系统上传发票(Receipt),最后通过银行转账的方式打给面试者。所以最好能提前拥有一张支持国际汇款(有SWIFT CODE)的银行的银行卡。

2.1 面试当天

面试约定在Hacker Way Site早上9:45进行。搭乘宾馆提供的直达车,早早的到了FB总部。在前台进行访客登记,等了约10分钟我的接口Recruiter就来接我了。由于来的较早,他先带我大致参观了下园区,当然免不了show一下 FB引以为傲的十几个餐厅。随后,在某个休息室匆匆的抓了杯咖啡就到了面试室。所谓的面试室就是预先book了一天的会议室,候选人整个一天的面试都会在这里进行。

on-site面试总共是5轮,每轮严格控制在45分钟(答不完就结束,有空余则继续聊),每轮一位面试官。按照我当时的情况和之前的面经来看5轮的分工都比较明确,分别是:coding、实战经验、未来的manager、未来的teamate、未来的兄弟team组员。由于今天我们主要讲流程,在这里我就大致过一下每个人的面试题倾向。具体的面试题内容会在后续博文中分享。

  • coding部分:和之前的电面题类型大致一致,只是形式变成了FB著名的white-board coding,即在一块大白板上写代码。也就是说:没有高亮!没有自动补全!重度依赖IDE的童鞋在没有准备的情况可能会有些吃紧。
  • 实战经验部分:由于我是面试MYSQL DBA方向,因此内容和MySQL内部原理密切相关。按照Recruiter的说法是:问到你不会为止(找到知识的边界)。一般来说这一面都会是技术专家出面进行。形式为问答+白板的伪代码。
  • 未来manager部分:0.5人文+0.5技术。主要考察团队合作能力,以及过去的一些项目中遇到的困难和如何解决的。如果简历上没有撒谎,并且实战经验丰富的话,这一关会是非常轻松的。
  • 未来的teamate:全技术,这部分的深度会没有技术专家面的那么深。个人猜测这一关除了做技术能力的double check外,也是为了确保候选人能够很好的与将来的同事交流。
  • 未来的兄弟team成员:这部分主要考察周围知识面的触及程度。例如:作为MySQL DBA了解Linux相关知识就是必要的;对于programmer来说,了解一些产品设计原理,或者前端知识也是必要的,等等。此外,也考察部分跨团队交流的能力。

由于是从上午开始的面试,在前两面结束后就是一个一小时的午休时间。当然,这段时间就是好好享受FB奢华的饭菜调整状态的时候啦。Recruiter当时和我说,每天最烦恼的时候就是午饭时刻,因为“去哪个食堂吃,吃什么”是最困难的问题。为此,他们内部还有一个APP,用于展示每个食堂当天提供伙食菜单,ORZ…

全天面试完成后,没有特殊安排的话,Recruiter会询问你是否想继续逛下。否则就陪同离开园区,完成一天的面试执行。

2.2 面试后的结果

大约在面试当周的周五都会有个候选人PK会,每位面试官会表明自己的看法。在这个会上就会有一个候选人是否通过的结论。我当时在面试后5天左右就收到了录用结果。之后Recruiter就会起草offer,谈工资(具体工资我就不说啦,如果想知道一个大概的业界标准可以上Glassdoor),邮件确认,走流程。Offer搞定后,就开始启动relocation项目了。所有的relocation相关你能够想到的问题,FB都有指定的代理回来帮助你,实在是非常的周到!例如:签证,搬家海运,临时落脚点,机票等。关于华人relocation和国外生活的部分,我也会在日后的博文中陆续更新。

Tips

如果你将要或准备参加FB电面/面试的话,下面是一些我个人感觉比较需要注意的点

  • 没有做过的或者不清楚的知识千万不要写在简历中,任何信息都有可能在电面中被考察到
  • 申请时留的邮箱,保持畅通可用,建议每天查收新邮件。
  • 电面环境建议安静,温度合适,电话信号良好。
  • 电面准备一条有麦克的耳机(普通手机的通话耳机就行)。
  • 注意保证手机电量充足
  • 王淮的《打造Facebook》一定要看,我的大部分面试流程的疑问都在书里得到了解答(PS:我真的不是出版社的托!觉得我是托的可以看PDF。PPS:出版社别打我)
  • coding电面之前,建议先通过stypi练习一些简单的算法题
  • 关于白板题目去哪里找:LeetcodeTopCoderCodeforcesProject Euler 都是不错的选择
  • 关于薪资范围, 可以参考Glassdoor上给出的标准基本上很准
  • 关于家庭
    • 收入:以Facebook的待遇,一个人养活一家三口基本不是问题,会有少许结余。
    • 签证:Facebook的指定代理会帮一家三口搞定一切(但是不包括申请人的家长)

Q&A

这部分我将会持续更新大家感兴趣的问题。如果你对于来FB面试,工作有什么样的疑问,都可以来我的博客原文下留言

http://cenalulu.github.io/mysql/2015-03-02-how-i-become-a-facebook-dba/

或者给我发Email: cenalulu@gmail.com 所有共性的问题我都会在文章下面作答或者邮件回复。
当然,如果你在看了本文以后有了来FB工作的念头的话,也可以把英语简历和想要应聘的职位及应聘所在地发到我的Email,我会筛选后在平时空闲的时间帮忙走内推流程。
具体职位见:https://www.facebook.com/careers?_rdr

我是如何拿到Facebook Offer的,首发于博客 - 伯乐在线

25 Apr 01:58

Coding for SSDs

25 Apr 00:59

如何写简历

by 崔凯

这周又看了200多份简历
气喘吁吁之余,分享一下我期望收到的简历是什么样的。

先说标题

很多人的简历命名为【简历】或者【个人简历】
我们的HR通常一天会收集几十份简历一起发过来。如果简历合适,需要约面的时候,会直接回复HR的邮件,“请安排【崔凯】的面试。”如果简历的名称是【崔凯的简历】,便于理解。

另外,我们团队目前在招聘的岗位包含iOS/Mac/Android/Windows/Web/UI各种岗位,这些岗位会对应不同的面试官,如果简历的标题是【崔凯Web开发】,便于HR理解,应聘者是做Web开发的,这样会安排一个做Web的面试官。

还有就是现在所处的位置,通常外地的应聘者会被安排电话面试,如果简历里能在比较显眼的位置注明地理位置,【崔凯-UI设计-北京】,这样前期有个电面,再过来双方都有个了解。

加减分

我个人特别喜欢简历中附带了github或者blog的地址,也通常都会点开看看。
如果里面内容很丰富,通常都会加分。
但是,像我这样四年都没提交的,放上去反而减分 https://github.com/cuikai/
也或者两年才积攒了5篇的博客,更让人没有读的欲望。

项目经验

其实不妨去看一下职位描述里所列的加分项,再整理一下自己做过的项目。
有些团队期望招一个nodejs,如果能在第一眼就看到应聘者有node经验,后面很多字就不用看了。

这些项目,如果还在线上,最好能给出URL
例如最近在面的一位iOS开发,给出了应用的名称,但没有给地址。我猜它还在线上,我也确实很需要iOS的人,所以才会自己去AppStore上搜一下,但如果不是特别紧缺的岗位,简历写的又不怎么样,我实在不愿意在搜索上再花时间。例如某UI设计师(对,就是你),作品直接丢过来40多M的压缩包,如果不是内推,谁会下载半天再解压缩去看。

工作经历

社招还好,谁没换过几次工作。
校招里挺可怕的是2个月换一次实习的公司。
其实对实习生要求不多,稳定是最重要的,培养成本折腾不起。

总结

我倒不反对用html写简历,我自己也是html版本的,只是期望顺便能写一下 @media print 的样式
很绚烂的简历,用黑白色打印出来,一片黑,很废墨的

其实核心思想还是换位思考。自己幻想一下拿到简历的对方,彼此都舒服一点,基本上这事就成了。毕竟各个公司都缺人。如果有身边的朋友在换工作,不妨推荐给我 cuikai#uicss.cn

相关日志:

25 Apr 00:55

文章: Java字节码忍者禁术

by Ben Evans

Java语言本身是由Java语言规格说明(JLS)所定义的,而Java虚拟机的可执行字节码则是由一个完全独立的标准,即Java虚拟机规格说明(通常也被称为VMSpec)所定义的。

JVM字节码是通过javac对Java源代码文件进行编译后生成的,生成的字节码与原本的Java语言存在着很大的不同。比方说,在Java语言中为人熟知的一些高级特性,在编译过程中会被移除,在字节码中完全不见踪影。

这方面最明显的一个例子莫过于Java中的各种循环关键字了(for、while等等),这些关键字在编译过程中会被消除,并替换为字节码中的分支指令。这就意味着在字节码中,每个方法内部的流程控制只包含if语句与jump指令(用于循环)。

在阅读本文前,我假设读者对于字节码已经有了基本的了解。如果你需要了解一些基本的背景知识,请参考《Java程序员修炼之道》(Well-Grounded Java Developer)一书(作者为Evans与Verburg,由Manning于 2012年出版),或是来自于RebelLabs的这篇报告(下载PDF需要注册)。

让我们来看一下这个示例,它对于还不熟悉的JVM字节码的新手来说很可能会感到困惑。该示例使用了javap工具,它本质上是一个Java字节码的反汇编工具,在下载的JDK或JRE中可以找到它。在这个示例中,我们将讨论一个简单的类,它实现了Callable接口:

public class ExampleCallable implements Callable {
    public Double call() {
        return 3.1415;
    }
}

我们可以通过对javap工具进行最简单形式的使用,对这个类进行反汇编后得到以下结果:

$ javap kathik/java/bytecode_examples/ExampleCallable.class
Compiled from "ExampleCallable.java"
public class kathik.java.bytecode_examples.ExampleCallable 
       implements java.util.concurrent.Callable {
  public kathik.java.bytecode_examples.ExampleCallable();
  public java.lang.Double call();
  public java.lang.Object call() throws java.lang.Exception;
}

这个反汇编后的结果看上去似乎是错误的,毕竟我们只写一个call方法,而不是两个。而且即使我们尝试手工创建这两个方法,javac也会提示,代码中有两个具有相同名称和参数的方法,它们仅有返回类型的不同,因此这段代码是无法编译的。然而,这个类确确实实是由上面那个真实的、有效的Java源文件所生成的。

这个示例能够清晰地表明在使用Java中广为人知的一种限制:不可对返回类型进行重载,其实这只是Java语言的一种限制,而不是JVM字符码本身的强制要求。javac确实会在代码中插入一些不存在于原始的类文件中的内容,如果你为此感到担忧,那大可放心,因为这种事每时每刻都在发生!每一位Java程序员最先学到的一个知识点就是:“如果你不提供一个构造函数,那么编译器会为你自动添加一个简单的构造函数”。在javap的输出中,你也能看到其中有一个构造函数存在,而它并不存在于我们的代码中。

这些额外的方法从某种程度上表明,语言规格说明的需求比VM规格说明中的细节更为严格。如果我们能够直接编写字节码,就可以实现许多“不可能”实现的功能,而这种字节码虽然是合法的,却没有任何一个Java编译器能够生成它们。

举例来说,我们可以创建出完全不含构造函数的类。Java语言规格说明中要求每个类至少要包含一个构造函数,而如果我们在代码中没有加入构造函数,javac会自动加入一个简单的void构造函数。但是,如果我们能够直接编写字节码,我们完全可以忽略构造函数。这种类是无法实例化的,即使通过反射也不行。

我们的最后一个例子已经接近成功了,但还是差一口气。在字节码中,我们可以编写一个方法,它将试图调用一个其它类中定义的私有方法。这段字节码是有效的,但如果任何程序打算加载它,它将无法正确地进行链接。这是因为在类型加载器中(classloader)的校验器会检测出这个方法调用的访问控制限制,并且拒绝这个非法访问。

介绍ASM

如果我们打算在创建的代码中实现这些超越Java语言的行为,那就需要完全手动创建这样的一个类文件。由于这个类文件的格式是两进制的,因此可以选择使用某种类库,它能够让我们对某个抽象的数据结构进行操作,随后将其转换为字节码,并通过流方式将其写入磁盘。

具备这种功能的类库有多个选择,但在本文中我们将关注于ASM。这是一个非常常见的类库,在Java 8分发包中有一个以内部API的形式提供的版本(其内容稍有不同)。对于用户代码来说,我们选择使用通用的开源类库,而不是JDK中提供的版本,毕竟我们不应当依赖于内部API来实现所需的功能。

ASM的核心功能在于,它提供了一种API,虽然它看上去有些神秘莫测(有时也会显得有些粗糙),但能够以一种直接的方式反映出字节码的数据结构。

我们看到的Java运行时是由多年之前的各种设计决策所产生的结果,而在后续各个版本的类文件格式中,我们能够清晰地看到各种新增的内容。

ASM致力于尽量使构建的类文件接近于真实形态,因此它的基础API会分解为一系列相对简单的方法片段(而这些片段正是用于建模的二进制所关注的)。

如果程序员打算完全手动编写类文件,就必需理解类文件的整体结构,而这种结构是会随时改变的。幸运的是,ASM能够处理多个不同Java版本中的类文件格式之间的细微差别,而Java平台本身对于可兼容性的高要求也侧面帮助了我们。

一个类文件依次包含以下内容:

  • 某个特殊的数字(在传统的Unix平台上,Java中的特殊数字是这个历史悠久的、人见人爱的0xCAFEBABE)
  • 正在使用中的类文件格式版本号
  • 常量
  • 访问控制标记(例如类的访问范围是public、protected还是package等等)
  • 该类的类型名称
  • 该类的超类
  • 该类所实现的接口
  • 该类拥有的字段(处于超类中的字段上方)
  • 该类拥有的方法(处于超类中的方法上方)
  • 属性(类级别的注解)

可以用下面这个方法帮助你记忆JVM类文件中的主要部分:

ASM中提供了两个API,其中最简单的那个依赖于访问者模式。在常见的形式中,ASM只包含最简单的字段以及ClassWrite类(当已经熟悉了ASM的使用和直接操作字节码的方式之后,许多开发者会发现CheckClassAdapter是一个很实用的起点,作为一个ClassVisitor,它对代码进行检查的方式,与Java的类加载子系统中的校验器的工作方式非常想像。)

让我们看几个简单的类生成的例子,它们都是按照常规的模式创建的:

  • 启动一个ClassVisitor(在我们的示例中就是一个ClassWriter)
  • 写入头信息
  • 生成必要的方法和构造函数
  • 将ClassVisitor转换为字节数组,并写入输出

示例

public class Simple implements ClassGenerator {
 // Helpful constants
 private static final String GEN_CLASS_NAME = "GetterSetter";
 private static final String GEN_CLASS_STR = PKG_STR + GEN_CLASS_NAME;

 @Override
 public byte[] generateClass() {
   ClassWriter cw = new ClassWriter(0);
   CheckClassAdapter cv = new CheckClassAdapter(cw);
   // Visit the class header
   cv.visit(V1_7, ACC_PUBLIC, GEN_CLASS_STR, null, J_L_O, new String[0]);
   generateGetterSetter(cv);
   generateCtor(cv);
   cv.visitEnd();
   return cw.toByteArray();
 }

 private void generateGetterSetter(ClassVisitor cv) {
   // Create the private field myInt of type int. Effectively:
   // private int myInt;
   cv.visitField(ACC_PRIVATE, "myInt", "I", null, 1).visitEnd();

   // Create a public getter method
   // public int getMyInt();
   MethodVisitor getterVisitor = 
      cv.visitMethod(ACC_PUBLIC, "getMyInt", "()I", null, null);
   // Get ready to start writing out the bytecode for the method
   getterVisitor.visitCode();
   // Write ALOAD_0 bytecode (push the this reference onto stack)
   getterVisitor.visitVarInsn(ALOAD, 0);
   // Write the GETFIELD instruction, which uses the instance on
   // the stack (& consumes it) and puts the current value of the
   // field onto the top of the stack
   getterVisitor.visitFieldInsn(GETFIELD, GEN_CLASS_STR, "myInt", "I");
   // Write IRETURN instruction - this returns an int to caller.
   // To be valid bytecode, stack must have only one thing on it
   // (which must be an int) when the method returns
   getterVisitor.visitInsn(IRETURN);
   // Indicate the maximum stack depth and local variables this
   // method requires
   getterVisitor.visitMaxs(1, 1);
   // Mark that we've reached the end of writing out the method
   getterVisitor.visitEnd();

   // Create a setter
   // public void setMyInt(int i);
   MethodVisitor setterVisitor = 
       cv.visitMethod(ACC_PUBLIC, "setMyInt", "(I)V", null, null);
   setterVisitor.visitCode();
   // Load this onto the stack
   setterVisitor.visitVarInsn(ALOAD, 0);
   // Load the method parameter (which is an int) onto the stack
   setterVisitor.visitVarInsn(ILOAD, 1);
   // Write the PUTFIELD instruction, which takes the top two 
   // entries on the execution stack (the object instance and
   // the int that was passed as a parameter) and set the field 
   // myInt to be the value of the int on top of the stack. 
   // Consumes the top two entries from the stack
   setterVisitor.visitFieldInsn(PUTFIELD, GEN_CLASS_STR, "myInt", "I");
   setterVisitor.visitInsn(RETURN);
   setterVisitor.visitMaxs(2, 2);
   setterVisitor.visitEnd();
 }

 private void generateCtor(ClassVisitor cv) {
   // Constructor bodies are methods with special name 
   MethodVisitor mv = 
       cv.visitMethod(ACC_PUBLIC, INST_CTOR, VOID_SIG, null, null);
   mv.visitCode();
   mv.visitVarInsn(ALOAD, 0);
   // Invoke the superclass constructor (we are basically 
   // mimicing the behaviour of the default constructor 
   // inserted by javac)
   // Invoking the superclass constructor consumes the entry on the top
   // of the stack.
   mv.visitMethodInsn(INVOKESPECIAL, J_L_O, INST_CTOR, VOID_SIG);
   // The void return instruction
   mv.visitInsn(RETURN);
   mv.visitMaxs(2, 2);
   mv.visitEnd();
 }

 @Override
 public String getGenClassName() {
   return GEN_CLASS_NAME;
 }
}

这段代码使用了一个简单的接口,用一个单一的方法生成类的字节,一个辅助方法以返回生成的类名,以及一些实用的常量:

interface ClassGenerator {
public byte[] generateClass();

public String getGenClassName();

// Helpful constants
public static final String PKG_STR = "kathik/java/bytecode_examples/";
public static final String INST_CTOR = "";
public static final String CL_INST_CTOR = "";
public static final String J_L_O = "java/lang/Object";
public static final String VOID_SIG = "()V";
}

为了驾驭生成的类,我们需要使用一个harness类,它叫做Main。Main类提供了一个简单的类加载器,并且提供了一种反射式的方式对生成类中的方法进行回调。为了简便起见,我们将生成的类定入Maven的目标文件夹的正确位置,让IDE中的classpath能够顺利地找到它:

public class Main {
public static void main(String[] args) {
   Main m = new Main();
   ClassGenerator cg = new Simple();
   byte[] b = cg.generateClass();
   try {
     Files.write(Paths.get("target/classes/" + PKG_STR +
       cg.getGenClassName() + ".class"), b, StandardOpenOption.CREATE);
   } catch (IOException ex) {
     Logger.getLogger(Simple.class.getName()).log(Level.SEVERE, null, ex);
   }
   m.callReflexive(cg.getGenClassName(), "getMyInt");
}

下面的类提供了一种方法,能够对受保护的defineClass()进行访问,这样一来我们就能够将一个字节数组转换为某个类对象,以便在反射中使用。

private static class SimpleClassLoader extends ClassLoader {
 public Class simpleDefineClass(byte[] clazzBytes) {
   return defineClass(null, clazzBytes, 0, clazzBytes.length);
 }
}

private void callReflexive(String typeName, String methodName) {
 byte[] buffy = null;
 try {
   buffy = Files.readAllBytes(Paths.get("target/classes/" + PKG_STR +
     typeName + ".class"));
   if (buffy != null) {
     SimpleClassLoader myCl = new SimpleClassLoader();
     Class newClz = myCl.simpleDefineClass(buffy);
     Object o = newClz.newInstance();
     Method m = newClz.getMethod(methodName, new Class[0]);
     if (o != null && m != null) {
       Object res = m.invoke(o, new Object[0]);
       System.out.println("Result: " + res);
     }
   }
 } catch (IOException | InstantiationException | IllegalAccessException | 
         NoSuchMethodException | SecurityException | 
         IllegalArgumentException | InvocationTargetException ex) {
   Logger.getLogger(Simple.class.getName()).log(Level.SEVERE, null, ex);
 }
}

有了这个类以后,我们只要通过细微的改动,就可以方便地测试各种不同的类生成器,以此对字节码生成器的各个方面进行探索。

实现无构造函数的类的方式也很相似。举例来说,以下这种方式可以在生成的类中仅包含一个静态字段,以及它的getter和setter(生成器不会调用generateCtor()方法):

private void generateStaticGetterSetter(ClassVisitor cv) {
// Generate the static field
  cv.visitField(ACC_PRIVATE | ACC_STATIC, "myStaticInt", "I", null,
     1).visitEnd();

  MethodVisitor getterVisitor = cv.visitMethod(ACC_PUBLIC | ACC_STATIC, 
                                         "getMyInt", "()I", null, null);
  getterVisitor.visitCode();
  getterVisitor.visitFieldInsn(GETSTATIC, GEN_CLASS_STR, "myStaticInt", "I");

  getterVisitor.visitInsn(IRETURN);
  getterVisitor.visitMaxs(1, 1);
  getterVisitor.visitEnd();

  MethodVisitor setterVisitor = cv.visitMethod(ACC_PUBLIC | ACC_STATIC, "setMyInt", 
                                         "(I)V", null, null);
  setterVisitor.visitCode();
  setterVisitor.visitVarInsn(ILOAD, 0);
  setterVisitor.visitFieldInsn(PUTSTATIC, GEN_CLASS_STR, "myStaticInt", "I");

}

setterVisitor.visitInsn(RETURN);setterVisitor.visitMaxs(2,2);setterVisitor.visitEnd();

请留意一下该方法在生成时使用了ACC_STATIC标记,此外还请注意方法的参数是位于本地变量列表中的最前面的(这里使用的ILOAD 0 模式暗示了这一点 —— 而在生成实例方法时,此处应该改为ILOAD 1,这是因为实例方法中的“this”引用存储在本地变量表中的偏移量为0)。

通过使用javap,我们就能够确认在生成的类中确实不包括任何构造函数:

$ javap -c kathik/java/bytecode_examples/StaticOnly.class 
public class kathik.StaticOnly {
public static int getMyInt(); Code:
0: getstatic    #11                // Field myStaticInt:I
3: ireturn

public static void setMyInt(int); Code:
0: iload_0
1: putstatic    #11                // Field myStaticInt:I
4: return
}

使用生成的类

目前为止,我们是使用反射的方式调用我们通过ASM所生成的类的。这有助于保持这个示例的自包含性,但在很多情况下,我们希望能够将这些代码生成在常规的Java文件中。要实现这一点非常简单。以下示例将生成的类保存在Maven的目标目录下,写法很简单:

$ cd target/classes
$ jar cvf gen-asm.jar kathik/java/bytecode_examples/GetterSetter.class kathik/java/bytecode_examples/StaticOnly.class
$ mv gen-asm.jar ../../lib/gen-asm.jar

这样一来我们就得到了一个JAR文件,可以作为依赖项在其它代码中使用。比方说,我们可以这样使用这个GetterSetter类:

import kathik.java.bytecode_examples.GetterSetter;
public class UseGenCodeExamples {
 public static void main(String[] args) {
   UseGenCodeExamples ugcx = new UseGenCodeExamples();
   ugcx.run();
 }

 private void run() {
   GetterSetter gs = new GetterSetter();
   gs.setMyInt(42);
   System.out.println(gs.getMyInt());
 }
}

这段代码在IDE中是无法通过编译的(因为GetterSetter类没有配置在classpath中)。但如果我们直接使用命令行,并且在classpath中指向正确的依赖,就可以正确地运行了:

$ cd ../../src/main/java/
$ javac -cp ../../../lib/gen-asm.jar kathik/java/bytecode_examples/withgen/UseGenCodeExamples.java
$ java -cp .:../../../lib/gen-asm.jar kathik.java.bytecode_examples.withgen.UseGenCodeExamples
42

结论

在本文中,我们通过使用ASM类库中所提供的简单API,学习了完全手动生成类文件的基础知识。我们也为读者展示了Java语言和字节码有哪些不同的要求,并且了解到Java中的某些规则其实只是语言本身的规范,而不是运行时所强制的要求。我们还看到,一个正确编写的手工类文件可以直接在语言中使用,与通过javac生成的文件没有区别。这一点也是Java与其它非Java语言,例如Groovy或Scala进行互操作的基础。

这方面的应用还有许多高级技巧,通过本文的学习,读者应该已经掌握了基本的知识,并且能够进一步深入研究JVM的运行时,以及如何对它进行各种操作的技术。

关于作者

Ben Evans是Java/JVM性能分析初创公司jClarity的CEO。在业余时间他是伦敦Java社区的领导者之一并且是Java社区进程执行委员会的一员。之前的项目经验包括谷歌IPO的性能测试,金融交易系统,为90年代一些最大的电影编写备受好评的网站,以及其他。

查看英文原文:Secrets of the Bytecode Ninjas

25 Apr 00:51

文章: 携程App的网络性能优化实践

by 陈浩然

编者按:在4月23日~25日举行的QCon全球软件开发大会(北京站)上,携程无线开发总监陈浩然分享了《移动开发网络性能优化实践》,总结了携程在App网络性能优化方面的一些实践经验。在2014年接手携程无线App的框架和基础研发工作之后,陈浩然面对的首要工作就是App客户端性能优化,尤其是网络服务性能,这是所有App优化工作的重中之重。以下为正文。

首先介绍一下携程App的网络服务架构。由于携程业务众多,开发资源导致无法全部使用Native来实现业务逻辑,因此有相当一部分频道基于Hybrid实现。网络通讯属于基础&业务框架层中基础设施的一部分,为App提供统一的网络服务:

  • Native端的网络服务

Native模块是携程的核心业务模块(酒店、机票、火车票、攻略等),Native模块的网络服务主要通过TCP连接实现,而非常见的Restful HTTP API那种HTTP连接,只有少数轻量级服务使用HTTP接口作为补充。
    
TCP连接网络服务模块使用了长连接+短连接机制,即有一个长连接池保持一定数目长连接,用于减少每次服务额外的连接,服务完成后会将该连接Socket放回长连接池,继续保持连接状态(一段时间空闲后会被回收);短连接作为补充,每次服务完成后便会主动关闭连接。
    
TCP网络服务的Payload使用的是自定义的数据及序列化协议;HTTP服务的Payload比较简单,即常见的JSON数据格式。

  • Hybrid端的网络服务

Hybrid模块由于是在WebView中展示本地或者直连的H5页面,页面逻辑发起的网络服务都是通过系统WebView的HTTP请求实现的。少量业务场景(需要加密和支付等)以Hybrid桥接接口形式的Native TCP通道来完成网络服务。

下图是网络服务的部署架构图:

携程App所有网络服务,无论是TCP还是HTTP都会先连接到一个API Gateway服务器。如果是TCP服务,会先连接上TCP Gateway,TCP Gateway会负责将请求通过HTTP转发到后端的SOA服务接口。HTTP Gateway的原理与之类似。TCP Gateway和HTTP Gateway的区别仅仅在客户端到服务端的连接方式不同而已。Gateway的作用除了业务请求还有流量控制和熔断。

要发现常见网络性能问题,先来看看一个网络服务做了哪些事情:

1.DNS Lookup

2.TCP Handshake

3.TLS Handshake

4.TCP/HTTP Request/Response

首先会是DNS解析,然后TCP连接握手,TLS连接握手(如果有的话),连接成功后再发送TCP或HTTP请求以及收到响应。如果能够将这些过程逐一梳理并确保不会存在明显的性能问题,那么基本可以确保获得不错的网络性能。网络服务里有一个重要的性能标准,即RTT(Round-Trip Time),往返时延,它表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认)所间隔的时间。理想情况下可以假设4G网络RTT为100ms,3G网络RTT为200ms,很容易就能计算出我们的App网络服务耗时的下限,当然还要加上服务器处理时间。

常见的网络性能问题有如下几种:

  • 问题一:DNS问题

DNS出问题的概率其实比大家感觉的要大,首先是DNS被劫持或者失效,2015年初业内比较知名的就有Apple内部DNS问题导致App Store、iTunes Connect账户无法登录;京东因为CDN域名付费问题导致服务停摆。携程在去年11月也遇到过DNS问题,主域名被国外服务商误列入黑名单,导致主站和H5等所有站点无法访问,但是App客户端的Native服务都正常,原因后面介绍。

另一个常见问题就是DNS解析慢或者失败,例如国内中国运营商网络的DNS就很慢,一次DNS查询的耗时甚至都能赶上一次连接的耗时,尤其2G网络情况下,DNS解析失败是很常见的。因此如果直接使用DNS,对于首次网络服务请求耗时和整体服务成功率都有非常大的影响。

  • 问题二:TCP连接问题

DNS成功后拿到IP,便可以发起TCP连接。HTTP协议的网络层也是TCP连接,因此TCP连接的成功和耗时也成为网络性能的一个因素。我们发现常见的问题有TCP端口被封(例如上海长宽对非HTTP常见端口80、8080、443的封锁),以及TCP连接超时时长问题。端口被封,直接导致无法连接;连接超时时长过短,在低速网络上可能总是无法连接成果;连接超时过长,又有可能导致用户长时间等待,用户体验差。很多时候尽快失败重新发起一次连接会很快,这也是移动网络带宽不稳定情况下的一个常见情况。

  • 问题三:Write/Read问题

DNS Lookup和TCP连接成功后,就会开始发送Request,服务端处理后返回Response,如果是HTTP连接,业内大部分App是使用第三方SDK或者系统提供的API来实现,那么只能设置些缓存策略和超时时间。iOS上的NSURLConnection超时时间在不同版本上还有不同的定义,很多时候需要自己设置Timer来实现;如果是直接使用TCP连接实现网络服务,就要自己对读写超时时间负责,与网络连接超时时长参数类似,太小了在低速网络很容易读写失败,太大了又可能影响用户体验,因此需要非常小心地处理。

我们还遇到另一类问题,某些酒店Wi-Fi对使用非80、8080和443等常见HTTP端口的服务进行了限制,即使发送Request是正常的,服务端能够正常收到,但是Response却被酒店网络proxy或防火墙拦截,客户端最终会等待读取超时。

移动网络和传统网络另一个很大的区别是Connection Migration问题。定义一个Socket连接是四元组(客户端IP,客户端Port,服务端IP,服务端Port),当用户的网络在WIFI/4G/3G/2G类型中切换时,其客户端IP会发生变化,如果此时正在进行网络服务通讯,那么Socket连接自身已经失效,最终也会导致网络服务失败。

  • 问题四:传输Payload过大

传的多就传的慢,如果没做过特别优化,传输Payload可能会比实际所需要的大很多,那么对于整体网络服务耗时影响非常大。

  • 问题五:复杂的国内外网络情况

国内运营商互联和海外访问国内带宽低传输慢的问题也令人难非常头疼。

看下携程App用户的网络类型分布:

 

Wi-Fi用户占比已超过60%,4G用户量正接近3G用户量,2G用户在逐步减少,用户的网络越来越好。4G/3G/2G网络的带宽和延迟差别很大,而带宽和延迟是网络性能的重要指标:

 

针对携程App用户的网络带宽和延迟,我们采样了海内外各8个城市的数据:

 

 

注意网络带宽和延迟并没有直接相关性,带宽高不意味着延迟低,延迟再低也不可能快过光速。从上图我们可以看到海内外带宽相差很大,但是延迟基本一致。

针对上面这些问题,在网络复杂环境和国内运营商互通状况无能为力的情况下,就针对性地逐一优化,以期达到目标:连得上、连得快、传输时间短。

  • 优化实践一:优化DNS解析和缓存

由于我们的App网络服务主要基于TCP连接,为了将DNS时间降至最低,我们内置了Server IP列表,该列表可以在App启动服务中下发更新。App启动后的首次网络服务会从Server IP列表中取一个IP地址进行TCP连接,同时DNS解析会并行进行,DNS成功后,会返回最适合用户网络的Server IP,那么这个Server IP会被加入到Server IP列表中被优先使用。

Server IP列表是有权重机制的,DNS解析返回的IP很明显具有最高的权重,每次从Server IP列表中取IP会取权重最高的IP。列表中IP权重也是动态更新的,根据连接或者服务的成功失败来动态调整,这样即使DNS解析失败,用户在使用一段时间后也会选取到适合的Server IP。

  • 优化实践二:网络质量检测(根据网络质量来改变策略)

针对网络连接和读写操作的超时时间,我们提出了网络质量检测机制。目前做到的是根据用户是在2G/3G/4G/Wi-Fi的网络环境来设置不同的超时参数,以及网络服务的并发数量。2G/3G/4G网络环境对并发TCP连接的数量是有限制的(2G网络下运营商经常只能允许单个Host一个TCP连接),因此网络服务重要参数能够根据网络质量状况来动态设定对性能和体验都非常重要。 

不过目前携程App网络00质量检测的粒度还比较粗,我们正在做的优化就是能够测算到用户当前的网络RTT,根据RTT值来设置参数,那会更加准确。Facebook App的做法是HTTP网络服务在HTTP Response的Header中下发了预估的RTT值,客户端根据这个RTT值便能够设计不同的产品和服务策略。

  • 优化实践三:提供网络服务优先级和依赖机制

由于网络对并发TCP连接的限制,就需要能够控制不必要的网络服务数量,因此我们在通讯模块中加入了网络服务优先级和依赖机制。发送一个网络服务,可以设置它的优先级,高优先级的服务优先使用长连接, 低优先级的就是用短连接。长连接由于是从长连接池中取到的TCP连接,因此节省了TCP连接时间。

网络服务依赖机制是指可以设置数个服务的依赖关系,即主从服务。假设一个App页面要发多个服务,主服务成功的情况下,才去发子服务,如果主服务失败了,自服务就无需再关心成功或者失败,会直接被取消。如果主服务成功了,那么子服务就会自动触发。

  • 优化实践四:提供网络服务重发机制

移动网络不稳定,如果一次网络服务失败,就立刻反馈给用户你失败了,体验并不友好。我们提供了网络服务重发机制,即当网络服务在连接失败、写Request失败、读Response失败时自动重发服务;长连接失败时就用短连接来做重发补偿,短连接服务失败时当然还是用短连接来补偿。这种机制增加了用户体验到的服务成功概率。

当然不是所有网络服务都可以重发,例如当下订单服务在读取Response失败时,就不能重发,因为下单请求可能已经到达服务器,此时重发服务可能会造成重复订单,所以我们添加了重发服务开关,业务段可以自行控制是否需要。

  • 优化实践五:减少数据传输量

我们优化了TCP服务Payload数据的格式和序列化/反序列化算法,从自定义格式转换到了Protocol Buffer数据格式,效果非常明显。序列化/反序列算法也做了调整,如果大家使用JSON数据格式,选用一个高效的反序列化算法,针对真实业务数据进行测试,收益明显。

图片格式优化在业界已有成熟的方案,例如Facebook使用的WebP图片格式,已经被国内众多App使用。

  • 优化实践六:优化海外网络性能

海外网络性能的优化手段主要是通过花钱,例如CDN加速,提高带宽,实现动静资源分离,对于App中的Hybrid模块优化效果非常明显。

经过上面的优化手段,携程App的网络性能从优化之初的V5.9版本到现在V6.4版本,服务成功率已经有了大幅提升,核心服务成功率都在99%以上。注意这是客户端采集的服务成功率,即用户感知到的网络服务成功率,失败量中包含了客户端无网络和服务端的错误。网络服务平均耗时下降了150-200ms。我们的目标是除2G网络外,核心业务的网络服务成功率都能够达到三个九。

数据格式优化的效果尤其明显,采用新的Protocol Buffer数据格式+Gzip压缩后的Payload大小降低了15%-45%。数据序列化耗时下降了80%-90%。

经历了这半年的网络性能优化,体会最深的就是Logging基础设施的重要性。如果我们没有完整端到端监控和统计的能力,性能优化只能是盲人摸象。Logging基础设施需要包括客户端埋点采集、服务端T+0处理、后期分析、Portal展示、自动告警等多种功能,这也不是单纯的客户端框架团队可以完成的,而需要公司多个部门合作完成。

携程基于Elastic Search开发了网络实时监控Portal,能够实时监控所有的网络服务,包括多种维度,可以跟踪到单个目标用户的所有网络请求信息。
用户的性能数据都被喷到Haddop和Hive大数据平台,我们可以轻松制定并分析网络性能KPI,例如服务成功率、服务耗时、连接成功率和连接耗时等,我们做到了在时间、网络类型、城市、长短连接、服务号等多纬度的分析。下图是我们的网络性能KPI Portal,可以查看任一服务的成功率,服务耗时、操作系统、版本等各种信息,对于某个服务的性能分析非常有用。

最后看看业界网络性能优化的新技术方向,目前最有潜力的是Google推出的SPDY和QUIC协议。

SDPY已成为HTTP/2.0 Draft,有希望成为未来HTTP协议的新标准。HTTP/2.0提供了很多诱人的特性(多路复用、请求优先级、支持服务端推送、压缩HTTP Header、强制SSL传输、对服务端程序透明等)。国内很多App包括美团、淘宝、支付宝都已经在尝试使用SDPY协议,Twitter的性能测试表明可以降低30%的网络延迟,携程也做了性能测试,由于和TCP性能差距不明显,暂未在生产上使用。

QUIC是基于UDP实现的新网络协议,由于TCP协议实现已经内置在操作系统和路由器内核中,Google无法直接改进TCP,因此基于无连接的UDP协议来设计全新协议可以获得很多好处。首先能够大幅减少连接时间,QUIC可以在发送数据前只需要0 RTT时间,而传统TCP/TLS连接至少需要1-3 RTT时间才能完成连接(即使采用Fast-Open TCP或TLS Snapshot);其次可以解决TCP Head-of-Line Blocking问题,通常前一个TCP Packet发送成功前会拥塞后面的Packet发送,而QUIC可以避免这样的问题;QUIC也有更好的移动网络环境下拥塞控制算法;新的连接方式也大幅减少了Connectiont Migration问题的影响。

随着这些新协议的逐渐成熟,相信未来能够进一步提高移动端的网络服务性能,值得大家保持关注。

 

感谢臧秀涛对本文的审校。

22 Apr 00:28

Linux Shell脚本面试25问

by changqi

Q:1 Shell脚本是什么、它是必需的吗?

答:一个Shell脚本是一个文本文件,包含一个或多个命令。作为系统管理员,我们经常需要使用多个命令来完成一项任务,我们可以添加这些所有命令在一个文本文件(Shell脚本)来完成这些日常工作任务。

Q:2 什么是默认登录shell,如何改变指定用户的登录shell

答:在Linux操作系统,“/bin/bash”是默认登录shell,是在创建用户时分配的。使用chsh命令可以改变默认的shell。示例如下所示:

# chsh <username> -s <new_default_shell>
# chsh linuxtechi -s /bin/sh

Q:3 可以在shell脚本中使用哪些类型的变量?

答:在shell脚本,我们可以使用两种类型的变量:

  • 系统定义变量
  • 用户定义变量

系统变量是由系统系统自己创建的。这些变量通常由大写字母组成,可以通过“set”命令查看。

用户变量由系统用户来生成和定义,变量的值可以通过命令“echo $<变量名>”查看。

Q:4 如何将标准输出和错误输出同时重定向到同一位置?

答:这里有两个方法来实现:

方法一:

2>&1 (# ls /usr/share/doc > out.txt 2>&1 )

方法二:

&> (# ls /usr/share/doc &> out.txt )

Q:5 shell脚本中“if”语法如何嵌套?

答:基础语法如下:

if [ Condition ]
then
command1
command2
…..
else
if [ condition ]
then
command1
command2
….
else
command1
command2
…..
fi
fi

Q:6 shell脚本中“$?”标记的用途是什么? ?

答:在写一个shell脚本时,如果你想要检查前一命令是否执行成功,在if条件中使用“$?”可以来检查前一命令的结束状态。简单的例子如下:

root@localhost:~# ls /usr/bin/shar
/usr/bin/shar
root@localhost:~# echo $?
0

如果结束状态是0,说明前一个命令执行成功。

root@localhost:~# ls /usr/bin/share
ls: cannot access /usr/bin/share: No such file or directory
root@localhost:~# echo $?
2

如果结束状态不是0,说明命令执行失败。

Q:7 在shell脚本中如何比较两个数字 ?

答:在if-then中使用测试命令( -gt 等)来比较两个数字,例子如下:

#!/bin/bash
x=10
y=20
if [ $x -gt $y ]
then
echo “x is greater than y”
else
echo “y is greater than x”
fi

Q:8 shell脚本中break命令的作用 ?

答:break命令一个简单的用途是退出执行中的循环。我们可以在while和until循环中使用break命令跳出循环。

Q:9 shell脚本中continue命令的作用 ?

答:continue命令不同于break命令,它只跳出当前循环的迭代,而不是整个循环。continue命令很多时候是很有用的,例如错误发生,但我们依然希望继续执行大循环的时候。

Q:10 告诉我shell脚本中Case语句的语法 ?

答:基础语法如下:

case word in
value1)
command1
command2
…..
last_command
!!
value2)
command1
command2
……
last_command
;;
esac

Q:11 shell脚本中while循环语法 ?

答:如同for循环,while循环只要条件成立就重复它的命令块。不同于for循环,while循环会不断迭代,直到它的条件不为真。基础语法:

while [ test_condition ]
do
commands…
done

Q:12 如何使脚本可执行 ?

答:使用chmod命令来使脚本可执行。例子如下:

# chmod a+x myscript.sh

Q:13 “#!/bin/bash”的作用 ?

答:#!/bin/bash是shell脚本的第一行,称为释伴(shebang)行。这里#符号叫做hash,而! 叫做 bang。它的意思是命令通过 /bin/bash 来执行。

Q:14 shell脚本中for循环语法 ?

答:for循环的基础语法:

for variables in list_of_items
do
command1
command2
….
last_command
done

Q:15 如何调试shell脚本 ?

答:使用’-x’参数(sh -x myscript.sh)可以调试shell脚本。另一个种方法是使用‘-nv’参数( sh -nv myscript.sh)。

Q:16 shell脚本如何比较字符串?

答:test命令可以用来比较字符串。测试命令会通过比较字符串中的每一个字符来比较。

Q:17 Bourne shell(bash) 中有哪些特殊的变量 ?

答:下面的表列出了Bourne shell为命令行设置的特殊变量。

内建变量

解释

$0

命令行中的脚本名字

$1

第一个命令行参数

$2

第二个命令行参数

…..

…….

$9

第九个命令行参数

$#

命令行参数的数量

$*

所有命令行参数,以空格隔开

Q:18 How to test files in a shell script ?

Q:18 在shell脚本中,如何测试文件 ?

答:test命令可以用来测试文件。基础用法如下表格:

Test

用法

-d 文件名

如果文件存在并且是目录,返回true

-e 文件名

如果文件存在,返回true

-f 文件名

如果文件存在并且是普通文件,返回true

-r 文件名

如果文件存在并可读,返回true

-s 文件名

如果文件存在并且不为空,返回true

-w 文件名

如果文件存在并可写,返回true

-x 文件名

如果文件存在并可执行,返回true

Q:19 在shell脚本中,如何写入注释 ?

答:注释可以用来描述一个脚本可以做什么和它是如何工作的。每一行注释以#开头。例子如下:

#!/bin/bash
# This is a command
echo “I am logged in as $USER”

Q:20 如何让 shell 就脚本得到来自终端的输入?

答:read命令可以读取来自终端(使用键盘)的数据。read命令得到用户的输入并置于你给出的变量中。例子如下:

# vi /tmp/test.sh
#!/bin/bash
echo ‘Please enter your name’
read name
echo “My Name is $name”
# ./test.sh
Please enter your name
LinuxTechi
My Name is LinuxTechi

Q:21 如何取消变量或取消变量赋值 ?

答:“unset”命令用于取消变量或取消变量赋值。语法如下所示:

# unset <Name_of_Variable>

Q:22 如何执行算术运算 ?

答:有两种方法来执行算术运算:

使用expr命令(# expr 5 + 2) 2.用一个美元符号和方括号($[ 表达式 ])例如:test=$[16 + 4] ; test=$[16 + 4]

Q:23 do-while语句的基本格式 ?

答:do-while语句类似于while语句,但检查条件语句之前先执行命令(LCTT 译注:意即至少执行一次。)。下面是用do-while语句的语法

do
{
statements
} while (condition)

Q:24 在shell脚本如何定义函数呢 ?

答:函数是拥有名字的代码块。当我们定义代码块,我们就可以在我们的脚本调用函数名字,该块就会被执行。示例如下所示:

$ diskusage () { df -h ; }

Q:25 如何在shell脚本中使用BC(bash计算器) ?

答:使用下列格式,在shell脚本中使用bc:

variable=`echo “options; expression” | bc`

Linux Shell脚本面试25问,首发于博客 - 伯乐在线

21 Apr 00:31

NGINX open sources TCP load balancing

21 Apr 00:27

Redis 3.0.0正式版发布,全新的分布式高可用数据库

by changqi
Redis 3.0.0 正式版终于到来了!最重要的新特性是集群(Redis Cluster),提供Redis功能子集(比如不支持多数据库)的分布式、容错的实现(最多支持1000结点)。Salvatore ‘antirez’ Sanfilippo在Google Groups里表示,这是Redis的重要时刻。“我相信今天的Redis 3.0.0将以某种方式完全改变Redis的面貌。”他强调,人们将认识到Redis是一个全新的东西,它的自动扩展、容错和高可用性都大大提高,从此能够在更大范围承担更关键的任务。(我总结一下老大的意思吧:Redis翻开了历史新的篇章……)antirez还透露,内置的集群功能持续干了很多年,虽然能找到一些时间密集开发,但也不时被其他特性完全打断,现在终于完成了。他预计社区能用好这些功能,积累必要的经验,还要一到两年。

他还说,Redis 3.0.0实际上标志着一个新阶段和新的开发模式的开始。以后,大量已经开发的新功能将不再急于进入稳定版本,实际上Redis 3.0.0就放弃了很多新功能,回退到2.8,以保证新的稳定版本用户能够马上使用。

他在帖子里重点提及的其他更新包括:

  • 新的”embedded string”对象编码,提升缓存命中率。在某些工作负载(尤其是管道化的高负载)下速度大幅提高。
  • 大大改进了回收键的LRU近似算法。
  • AOF重写功能被完全重新开发了,以减少进程最终将积累的缓冲写入时,由于硬盘速度慢而导致的延迟。

而在发布声明中还列出了如下更新(相对于2.8):

  • WAIT command to block waiting for a write to be transmitted to the specified number of slaves.
  • MIGRATE connection caching. Much faster keys migraitons.
  • MIGARTE new options COPY and REPLACE.
  • CLIENT PAUSE command: stop processing client requests for a specified amount of time.
  • BITCOUNT performance improvements.
  • CONFIG SET accepts memory values in different units (for example you can use “CONFIG SET maxmemory 1gb”).
  • Redis log format slightly changed reporting in each line the role of the instance (master/slave) or if it’s a saving child log.
  • INCR performance improvements.

详情可以点击 这里 查看。

ITEye上powersoft同学之前翻译了Redis 3.0的文档,虽然还没有来得及更新,但还是有参考价值的:http://www.iteye.com/blogs/subjects/redis3

Hacker News上antirez回答了社区提出的一些问题,颇有价值,整理翻译如下。

Redis之外还有什么其他更好的选择啊?

(这问题让antirez怎么答,总不能不谦虚吧。仔细听,他回答得很好。) 这得看使用场景,基本上还是就事论事、具体情况具体分析。程序员的本事不就体现在选择正确的技术,然后在不同情况下优化嘛。你要考虑数据模型是否匹配所要解决的问题,运维因素,持久化保证,性能(需要多少个结点),可扩展性,是否简单(搞这么复杂以后会不会老要我来支持啊),等等。

其他同学提到了memcached,有人评论:现在memcached已经只相当于Redis最简单的功能了,只能作为缓存。Redis不仅能缓存,还能承担很多存储任务。此外还有人提及HyperDex,但其ACID特性实现Warp是专有的产品。

此前的这个大型NoSQL比较文章,仍然有一定参考价值: http://kkovacs.eu/cassandra-vs-mongodb-vs-couchdb-vs-redis

有了Cluster,Sentinel是不是就废啦。

还没那么快,Sentinel还在与Cluster并行继续开发中。目前单实例场景下需要HA的话,它还是最佳选择。但长远(可能很长远哦)看,我们会用Cluster解决Sentinel的使用场景,不过在那之前我们会很早就告诉大家的。

谁能给我更详细地讲讲”embedded string”对象编码是啥,它针对什么工作负荷?能找到的文档都太老了。

这事儿简单。一般Redis里会有包含类型字段的对象结构,还有一个指针指向实际的对象表示。假设类型是REDIS_STRING,就得有指针指向一个”sds”字符串(sds是字符串库用的名字)。

现在有了embedded string之后,就提供了一种特殊的字符串对象,用一个位置保持对象结构和字符串本身。这样内存利用更有效,而且能够大大改进内存本地性,所以差不多所有使用字符串对象的东西(字符串,或者比较大的要用字符串对象作为集合值的集合对象)性能都更好。

这种特殊字符串只用于小字符串(工作负荷里大多数字符串都不大)。

Redis
Redis是一个开源的高级key-value(键-值)缓存与存储,以高性能著称。它也常被称为数据结构服务器,因为其中的键可以存各种数据结构包括字符串、散列、列表、集合、有序集合、位图和hyperloglog。Redis的出现,很大程度补偿了memcached这类KV数据库的不足。不仅可以用于缓存,也可以用于一些场景的存储,在很多情况下是关系数据库很好的补充。它提供了Python,Ruby,Erlang,PHP客户端,使用非常方便。

Redis 3.0.0正式版发布,全新的分布式高可用数据库,首发于博客 - 伯乐在线

21 Apr 00:25

用 LDA 做主题模型:当 MLlib 邂逅 GraphX

by 东狗

主题模型可以从一系列文章中自动推测讨论的主题。这些主题可以被用作总结和整理文章,也可以在机器学习流程的后期阶段用于特征化和降维。

在Spark 1.3中,MLlib现在支持最成功的主题模型之一,隐含狄利克雷分布(LDA)。LDA也是基于GraphX上构建的第一个MLlib算法。在这篇博文中,我们概述LDA和及其用例,并且解释GraphX是实现它最自然的方式。

主题模型

抽象地说,主题模型旨在一系列文章中找到一种结构。学习到这种“结构”之后,一个主题模型能回答以下这样的问题:X文章讨论的是什么?X文章和Y文章有多相似?如果我对Z文章感兴趣,我应该先读哪些文章?

LDA

主题模型是一个比较广的领域。Spark 1.3加入了隐含狄利克雷分布(LDA),差不多是现今最成功的主题模型。最初被开发用于文本分析和群体遗传学,LDA之后被不断拓展,应用到从时间序列分析到图片分析等问题。首先,我们从文本分析的角度描述LDA。

什么是主题?主题不是LDA的输入,所以LDA必须要从纯文本中推断主题。LDA将主题定义为词的分布。例如,当我们在一个20个新闻组的文章数据集上运行MLlib的LDA,开始的几个主题是:

看下三个主题中的高权重词语,我们可以很快了解每个主题在说什么:运动,空间探索和电脑。LDA的成功很大程度上源自它产生可解释主题的能力。

用例

除了推断出这些主题,LDA还可以推断每篇文章在主题上的分布。例如,X文章大概有60%在讨论“空间探索”,30%关于“电脑”,10%关于其他主题。

这些主题分布可以有多种用途:

  • 聚类: 主题是聚类中心,文章和多个类簇(主题)关联。聚类对整理和总结文章集合很有帮助。
    • 参看Blei教授和Lafferty教授对于Science杂志的文章生成的总结。点击一个主题,看到该主题下一系列文章。
  • 特征生成:LDA可以生成特征供其他机器学习算法使用。如前所述,LDA为每一篇文章推断一个主题分布;K个主题即是K个数值特征。这些特征可以被用在像逻辑回归或者决策树这样的算法中用于预测任务。
  • 降维:每篇文章在主题上的分布提供了一个文章的简洁总结。在这个降维了的特征空间中进行文章比较,比在原始的词汇的特征空间中更有意义。

在MLlib中使用LDA

我们给出一个使用LDA的小例子。我们在这儿描述这个过程,实际的代码在这个Github gist上。本例首先读取并预处理文章。预处理最重要的部分是选择词典。在本例中,我们将文本拆成词,之后去除(a)非字母词 (b)4个字符一下的短词 (c)最常见的20个词(停用词)。一般来说,在你自己的数据集上调整这个预处理步骤很重要。

我们运行LDA,使用10个主题和10轮迭代。根据你的数据集选择主题的数量很重要。其他参数设成默认,我们在Spark文档的Markdown文件(spark/docs/*.md)上训练LDA。

我们得到10个主题。下面是5个人工挑选出来的主题,每个主题配以最重要的5个词语。请注意每个主题有多么清晰地对应到Spark的一个组件!(打引号的主题标题是为了更清晰手动加的)

在Spark 1.3中LDA有Scala和Java的API。Python的API很快会加入。

实现:GraphX

有许多算法可以训练一个LDA模型。我们选择EM算法,因为它简单并且快速收敛。因为用EM训练LDA有一个潜在的图结构,在GraphX之上构建LDA是一个很自然的选择。

LDA主要有两类数据:词和文档。我们把这些数据存成一个偶图(如下所示),左边是词节点,右边是文档节点。每个词节点存储一些权重值,表示这个词语和哪个主题相关;类似的,每篇文章节点存储当前文章讨论主题的估计。

每当一个词出现在一篇文章中,图中就有一个边连接对应的词节点和文章节点。例如,在上图中,文章1包含词语“hockey” 和“system”

这些边也展示了这个算法的流通性。每轮迭代中,每个节点通过收集邻居数据来更新主题权重数据。下图中,文章2通过从连接的词节点收集数据来更新它的主题估计。

GraphX因此是LDA自然的选择。随着MLlib的成长,我们期望未来可以有更多图结构的学习算法!

可扩展性

LDA的并行化并不直观,已经有许多研究论文提出不同的策略来实现。关键问题是所有的方法都需要很大量的通讯。这在上图中很明显:词和文档需要在每轮迭代中用新数据更新相邻节点,而相邻节点太多了。

我们选择了EM算法的部分原因就是它通过很少轮的迭代就能收敛。更少的迭代,更少的通讯。

在Spark中加入LDA之前,我们在一个很大的Wikipedia数据集上做了测试。下面是一些数字:

  • 训练集规模:460万文档
  • 词典规模:110万词汇
  • 训练集规模:11亿词(大约239词/文章)
  • 100个主题
  • 16个 worker节点的EC2集群
  • 计时结果:10轮迭代中平均176秒/迭代

接下来是?

Spark的贡献者正在开发更多LDA算法:在线变分贝叶斯(一个快速近似算法)和吉布斯采样(一个更慢但是有时更准确的算法)。我们也在增加帮助模块,例如用于自动数据准备的Tokenizers和更多预测方法。

想开始用LDA,今天下载Spark 1.3

查看例子,了解API的细节,查看MLlib文档

致谢

LDA的开发是许多Spark贡献者的合作结果。他们是:Joseph K. Bradley、Joseph Gonzalez、David Hall、Guoqiang Li、Xiangrui Meng、Pedro Rodriguez、Avanesov Valeriy 和 Xusen Yin。

更多资源

通过这些综述学习更多关于主题模型和LDA的内容:

从这些研究论文中获得深入了解:

用 LDA 做主题模型:当 MLlib 邂逅 GraphX,首发于博客 - 伯乐在线

20 Apr 06:31

Nginx 战斗准备 —— 优化指南

by promumu

大多数的Nginx安装指南告诉你如下基础知识——通过apt-get安装,修改这里或那里的几行配置,好了,你已经有了一个Web服务器了!而且,在大多数情况下,一个常规安装的nginx对你的网站来说已经能很好地工作了。然而,如果你真的想挤压出nginx的性能,你必须更深入一些。在本指南中,我将解释Nginx的那些设置可以微调,以优化处理大量客户端时的性能。需要注意一点,这不是一个全面的微调指南。这是一个简单的预览——那些可以通过微调来提高性能设置的概述。你的情况可能不同。

 

基本的 (优化过的) 配置

我们将修改的唯一文件是nginx.conf,其中包含Nginx不同模块的所有设置。你应该能够在服务器的/etc/nginx目录中找到nginx.conf。首先,我们将谈论一些全局设置,然后按文件中的模块挨个来,谈一下哪些设置能够让你在大量客户端访问时拥有良好的性能,为什么它们会提高性能。本文的结尾有一个完整的配置文件。

 

高层的配置

nginx.conf文件中,Nginx中有少数的几个高级配置在模块部分之上。

user www-data;

pid /var/run/nginx.pid;

worker_processes auto;

worker_rlimit_nofile 100000;

userpid应该按默认设置 – 我们不会更改这些内容,因为更改与否没有什么不同。

worker_processes 定义了nginx对外提供web服务时的worder进程数。最优值取决于许多因素,包括(但不限于)CPU核的数量、存储数据的硬盘数量及负载模式。不能确定的时候,将其设置为可用的CPU内核数将是一个好的开始(设置为“auto”将尝试自动检测它)。

worker_rlimit_nofile 更改worker进程的最大打开文件数限制。如果没设置的话,这个值为操作系统的限制。设置后你的操作系统和Nginx可以处理比“ulimit -a”更多的文件,所以把这个值设高,这样nginx就不会有“too many open files”问题了。

 

Events模块

events模块中包含nginx中所有处理连接的设置。

events {
    worker_connections 2048;
    multi_accept on;
    use epoll;
}

worker_connections设置可由一个worker进程同时打开的最大连接数。如果设置了上面提到的worker_rlimit_nofile,我们可以将这个值设得很高。
记住,最大客户数也由系统的可用socket连接数限制(~ 64K),所以设置不切实际的高没什么好处。

multi_accept 告诉nginx收到一个新连接通知后接受尽可能多的连接。

use 设置用于复用客户端线程的轮询方法。如果你使用Linux 2.6+,你应该使用epoll。如果你使用*BSD,你应该使用kqueue。想知道更多有关事件轮询?看下维基百科吧(注意,想了解一切的话可能需要neckbeard和操作系统的课程基础)

(值得注意的是如果你不知道Nginx该使用哪种轮询方法的话,它会选择一个最适合你操作系统的)

 

HTTP 模块

HTTP模块控制着nginx http处理的所有核心特性。因为这里只有很少的配置,所以我们只节选配置的一小部分。所有这些设置都应该在http模块中,甚至你不会特别的注意到这段设置。

http {

    server_tokens off;

    sendfile on;

    tcp_nopush on;
    tcp_nodelay on;

    ...
}

server_tokens 并不会让nginx执行的速度更快,但它可以关闭在错误页面中的nginx版本数字,这样对于安全性是有好处的。

sendfile可以让sendfile()发挥作用。sendfile()可以在磁盘和TCP socket之间互相拷贝数据(或任意两个文件描述符)。Pre-sendfile是传送数据之前在用户空间申请数据缓冲区。之后用read()将数据从文件拷贝到这个缓冲区,write()将缓冲区数据写入网络。sendfile()是立即将数据从磁盘读到OS缓存。因为这种拷贝是在内核完成的,sendfile()要比组合read()和write()以及打开关闭丢弃缓冲更加有效(更多有关于sendfile)

tcp_nopush 告诉nginx在一个数据包里发送所有头文件,而不一个接一个的发送

tcp_nodelay 告诉nginx不要缓存数据,而是一段一段的发送–当需要及时发送数据时,就应该给应用设置这个属性,这样发送一小块数据信息时就不能立即得到返回值。

access_log off;
error_log /var/log/nginx/error.log crit;

access_log设置nginx是否将存储访问日志。关闭这个选项可以让读取磁盘IO操作更快(aka,YOLO)

error_log 告诉nginx只能记录严重的错误

keepalive_timeout 10;

client_header_timeout 10;
client_body_timeout 10;

reset_timedout_connection on;
send_timeout 10;

keepalive_timeout 给客户端分配keep-alive链接超时时间。服务器将在这个超时时间过后关闭链接。我们将它设置低些可以让ngnix持续工作的时间更长。

client_header_timeout 和 client_body_timeout 设置请求头和请求体(各自)的超时时间。我们也可以把这个设置低些。

reset_timeout_connection告诉nginx关闭不响应的客户端连接。这将会释放那个客户端所占有的内存空间。

send_timeout 指定客户端的响应超时时间。这个设置不会用于整个转发器,而是在两次客户端读取操作之间。如果在这段时间内,客户端没有读取任何数据,nginx就会关闭连接。

limit_conn_zone $binary_remote_addr zone=addr:5m;
limit_conn addr 100;

limit_conn_zone设置用于保存各种key(比如当前连接数)的共享内存的参数。5m就是5兆字节,这个值应该被设置的足够大以存储(32K*5)32byte状态或者(16K*5)64byte状态。

limit_conn为给定的key设置最大连接数。这里key是addr,我们设置的值是100,也就是说我们允许每一个IP地址最多同时打开有100个连接。

include /etc/nginx/mime.types;

default_type text/html;

charset UTF-8;

include只是一个在当前文件中包含另一个文件内容的指令。这里我们使用它来加载稍后会用到的一系列的MIME类型。
default_type设置文件使用的默认的MIME-type。

charset设置我们的头文件中的默认的字符集.

以下两点对于性能的提升在伟大的WebMasters StackExchange中有解释。

gzip on;
gzip_disable "msie6";

# gzip_static on;
gzip_proxied any;
gzip_min_length 1000;
gzip_comp_level 4;

gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

gzip是告诉nginx采用gzip压缩的形式发送数据。这将会减少我们发送的数据量。

gzip_disable为指定的客户端禁用gzip功能。我们设置成IE6或者更低版本以使我们的方案能够广泛兼容。

gzip_static告诉nginx在压缩资源之前,先查找是否有预先gzip处理过的资源。这要求你预先压缩你的文件(在这个例子中被注释掉了),从而允许你使用最高压缩比,这样nginx就不用再压缩这些文件了(想要更详尽的gzip_static的信息,请点击这里)。

gzip_proxied允许或者禁止压缩基于请求和响应的响应流。我们设置为any,意味着将会压缩所有的请求。

gzip_min_length设置对数据启用压缩的最少字节数。如果一个请求小于1000字节,我们最好不要压缩它,因为压缩这些小的数据会降低处理此请求的所有进程的速度。

gzip_comp_level设置数据的压缩等级。这个等级可以是1-9之间的任意数值,9是最慢但是压缩比最大的。我们设置为4,这是一个比较折中的设置。

gzip_type设置需要压缩的数据格式。上面例子中已经有一些了,你也可以再添加更多的格式。

# cache informations about file descriptors, frequently accessed files
# can boost performance, but you need to test those values
open_file_cache max=100000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;

##
# Virtual Host Configs
# aka our settings for specific servers
##

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;

open_file_cache打开缓存的同时也指定了缓存最大数目,以及缓存的时间。我们可以设置一个相对高的最大时间,这样我们可以在它们不活动超过20秒后清除掉。

open_file_cache_valid 在open_file_cache中指定检测正确信息的间隔时间。

open_file_cache_min_uses 定义了open_file_cache中指令参数不活动时间期间里最小的文件数。

open_file_cache_errors指定了当搜索一个文件时是否缓存错误信息,也包括再次给配置中添加文件。我们也包括了服务器模块,这些是在不同文件中定义的。如果你的服务器模块不在这些位置,你就得修改这一行来指定正确的位置。

 

一个完整的配置

user www-data;
pid /var/run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 100000;

events {
    worker_connections 2048;
    multi_accept on;
    use epoll;
}

http {
    server_tokens off;
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    access_log off;
    error_log /var/log/nginx/error.log crit;

    keepalive_timeout 10;
    client_header_timeout 10;
    client_body_timeout 10;
    reset_timedout_connection on;
    send_timeout 10;

    limit_conn_zone $binary_remote_addr zone=addr:5m;
    limit_conn addr 100;

    include /etc/nginx/mime.types;
    default_type text/html;
    charset UTF-8;

    gzip on;
    gzip_disable "msie6";
    gzip_proxied any;
    gzip_min_length 1000;
    gzip_comp_level 6;
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

    open_file_cache max=100000 inactive=20s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

编辑完配置后,确认重启nginx使设置生效。

sudo service nginx restart

 

后记

就这样!你的Web服务器现在已经就绪,之前困扰你的众多访问者的问题来吧。这并不是加速网站的唯一途径,很快我会写更多介绍其他加速网站方法的文章的。

 

Nginx 战斗准备 —— 优化指南,首发于博客 - 伯乐在线

20 Apr 01:04

JVM Internals (2013)

20 Apr 01:00

JVM内幕: Java虚拟机详解

by newseditors

本文解释了Java虚拟机(JVM)的内部架构。附图显示了依从Java虚拟机规范Java SE 7版本的典型JVM的关键内部组件。本文J对线程和线程间的共享在JVM底层运作机制做了详细的阐述。Java高手进阶必读!(@网路冷眼 推荐)

The post JVM内幕: Java虚拟机详解 appeared first on 头条 - 伯乐在线.

20 Apr 01:00

Java NIO与IO

by 程序员学架构

Java中NIO和IO的区别?各自的使用场景以及对程序设计的影响?本文提供使用NIO/IO的设计方案。 阅读原文 »

The post Java NIO与IO appeared first on 头条 - 伯乐在线.

20 Apr 01:00

Java 接口 (interface) and Scala 特质 (trait)

by Ken

本文将简要介绍Java中的接口(interface),Java 8中接口default方法,以及Scala中的特质(trait),同时会比较Java接口与Scala特质的相似与差异。 阅读原文 »

The post Java 接口 (interface) and Scala 特质 (trait) appeared first on 头条 - 伯乐在线.

20 Apr 00:55

关于垃圾回收被误解的7件事

by 蒋 生武
对Java垃圾回收最大的误解是什么?它实际又是什么样的呢?

当 我还是小孩的时候,父母常说如果你不好好学习,就只能去扫大街了。但他们不知道的是,清理垃圾实际上是很棒的一件事。可能这也是即使在Java的世界中, 同样有很多开发者对GC算法产生误解的原因——包括它们怎样工作、GC是如何影响程序运行和你能对它做些什么。因此我们找到了Java性能调优专家Haim Yadid,并把名为Java performance tuning guide的文章发表在Takipi的博客上。

最新博文:关于垃圾回收被误解的7件事
http://t.co/3QJLJuKCRqpic.twitter.com/aqQEF0zTkK
— Takipi (@takipid) April 6, 2015

带着对性能调优指南浓厚的兴趣,我们决定在这篇后续的博文中收集一些关于垃圾回收的流行观点,并且指出为什么它们完全是错误的。

来看看前7名:

1. 只有一个垃圾回收器

不,并且4也是错误的答案。HotSpot JVM一共有4个垃圾回收器:Serial, Parallel / Throughput. CMS, and the new kid on the block G1。别急,另外还有一些非标准的垃圾回收器和更大胆的实现,比如Shenandoah或 者其他JVM使用的回收器(C4——Azul开发的无停顿回收器)。HotSpot默认使用Parallel / Throughput回收器,但它常常不是你运行程序的最佳选择。比如CMS和G1会使GC停顿(GC pause)发生的频率降低,但是对于每次停顿所花费的时间,很可能比Parallel回收器更长。另一方面来说,在使用相同大小堆内存的情况下,Parallel回收器能带来更高的吞吐量。

结论:根据你的需求(可接受的GC停顿频率和持续时间)选择合适的垃圾回收器。

2. 并行(Parallel) = 并发(Concurrent)

一个GC周期(Garbage Collection cycle)可以以STW(Stop-The-World)的形式出现,这会发生一次GC停顿,也可以并发地执行从而无需暂停应用程序。更进一步来 讲,GC算法本身可以是串行的(单线程),也可以是并行的(多线程)。因此当我们提到并发的GC时,并不代表它是并行完成的,相反当提到串行GC时,也并 不意味着就一定会出现GC停顿。在GC的世界中,并发和并行是两个完全不同的概念。并发针对的是GC周期,而并行针对GC算法自身。

结论:垃圾回收的过程实际上有两步,启动GC周期和GC自身运行,这是不同的两件事。

3. G1能解决所有问题

经过一系列修正和改 进,Java 7中引入了G1回收器,它是JVM垃圾回收器中最新的组件。G1最大的优势就是解决了CMS中常见的内存碎片问题:GC周期会从老年代(Old Generation)中释放内存块,结果内存变得像瑞士奶酪那样千疮百孔,直到JVM对其无从下手了,才不得不停下来处理这些碎片。但是故事没这么简 单,某些情况下其他回收器可能比G1有更好的表现,这完全取决于你的需求。

结论:没有一个奇迹般的回收器能解决所有GC问题,你应该通过具体实验来选择合适的回收器。

4. 平均事务时间是最需要被关注的指标

如 果你仅仅监控服务器的平均事务时间,那么很可能错过一些异常值。这些异常的情况可能对用户来说是毁灭性的,而人们没有意识到它的重要性。比如一个事务在正常情况下耗时100ms,但受到GC停顿的影响,花了1分钟才完成。除了用户没人会注意到这个问题,因为你只观察了平均事务时间。试想有1%或者更多的用 户经历了这个场景,如果只关注平均值,它就太容易被忽略了。想了解更多和延迟相关的问题和怎样正确处理,可以在这里阅读Gil Tene的博客。

结论:留心那些异常值,你可以知道系统最后那1%的状况。(可不是这个1%

5. 降低新对象的分配率可以改善GC的运行状况

我们可以 粗略地把系统中的对象分为三种:长命(long-lived)对象,对它们我们一般做不了什么;中等寿命(mid-lived)对象,最大的问题可能出现在这;短命(short-lived)对象,它们的释放和回收通常都很快,在下个GC周期来临时就会消失。专注于中等寿命对象的分配率可以带来有益的结 果,这对短命和长命的对象却不是那么有效。另外,控制中等寿命对象往往是一项困难的工作。

结论:给服务器带来压力的并不单纯是对象的分配率,在运行过程中这些对象的种类才是一切麻烦的根源。

6. 调优可以解决所有事

如果你的程序需要保存大量被频繁修改的状态,对JVM堆内存进行调优就无法带来很好的收益。较长的GC停顿是不可避免的。一个解决办 法是对架构进行改善,保证一个对响应时间有决定性影响或者造成瓶颈的过程中,不包含大量状态。大量状态和响应能力是难以良好共存的,因此将它们分开处理才 是上上之选。

结论:不是所有的问题都可以通过调整JVM参数解决,有时你只需要回顾自己的绘图板。(译注:重新审视程序的设计)

7. GC日志会导致巨大的系统开销

简单来说,这是错的,尤 其在默认的日志配置下。日志数据是极为有价值的,Java 7中还引入了钩子来控制它们的大小,保证硬盘空间不被用尽。如果不收集GC日志,那么你会失去这几乎是唯一的,知晓JVM垃圾回收器在生产环境中工作状态 的方法。一般可接受的GC开销以5%作为上限,如果你能知道系统为GC停顿付出的代价,也能对最小化这个代价采取行动,这种程度的开销是不值一提的。

结论:在能力范围内,尽可能多地获取系统在生产环境中的运行数据,你会发现那是一个全新的世界。

总结

希望上面的结论能帮助你们更好地把握Java垃圾回收器的工作。在你们的程序中出现过类似问题吗?你们周围还有没有其他对GC常见的误解?请在下面的评论区留言。

相关文章

20 Apr 00:54

Stackoverflow问答:Java是传值还是传引用?

by 叶文海

原文地址 译者:叶文海(yewenhai@gmail.com)
译者注:这是一篇在Stackoverflow上面的一个经典问题,也是Java开发者容易混淆的一个问题,我节选了其中两个vote最高的回复进行翻译。
问题:我一直认为Java的参数是按引用传递,然而我看过一些文章里说Java的参数并不是按引用传递的,比如这篇,这让我很迷惑。Java中的参数到底是按引用传递还是按值传递?

回答1:

在Java里参数是按值来传递的。比较难理解的可能是Java传递的是对象的引用,但这些引用是按值传递。
比如:

public static void main( String[] args ){
    Dog aDog = new Dog("Max");
    foo(aDog);
    if( aDog.getName().equals("Max") ){ //true
        System.out.println( "Java passes by value." );
    }else if( aDog.getName().equals("Fifi") ){
        System.out.println( "Java passes by reference." );
    }
}
public static void foo(Dog d) {
    d.getName().equals("Max"); // true
    d = new Dog("Fifi");
    d.getName().equals("Fifi"); // true
}

在这个例子里面,执行完foo()方法之后,在main方法里再调用aDog.getName()方法依然会返回”Max”,在main方法中的
aDog并没有因为foo()的执行而被重写,这说明了参数是按值来进行传递的。如果是按照引用来传递的话在执行完foo()
方法之后aDog.getName()将会返回”Fifi”。
就像这样:

Dog aDog = new Dog("Max");
foo(aDog);
aDog.getName().equals("Fifi"); // true

public void foo(Dog d) {
    d.getName().equals("Max"); // true
    d.setName("Fifi");
}

回答2:

我刚刚发现你引用了我的文章 ;)(译者注:这位是提问者引用文章的作者)
在Java的规范里说明了在Java中一切参数都是按值传递的,根本就没有引用传递这一说。
理解这个概念的关键是要明白

Dog myDog;

这里声明的并不是一个Dog对象,而是一个指向Dog对象的指针。
这是什么意思呢,就是当你执行

Dog myDog = new Dog("Rover");
foo(myDog);

本质上是你把创建好的Dog对象的地址传递给foo方法。(我说的‘本质上’其实是因为Java中的指针并不是直接的地址,不过可以简单的理解成这样)。
假设Dog对象在内存中的地址是42。那我们就是把42这个值传递给了foo方法。
如果foo方法的定义如下:

public void foo(Dog someDog) {
    someDog.setName("Max");     // AAA
    someDog = new Dog("Fifi");  // BBB
    someDog.setName("Rowlf");   // CCC
}

让我们来看看执行的时候会发生些什么。
1. someDog的值设置为42。
2. 在AAA行
      a.someDog指向一个内存地址为42的Dog对象。
      b.把Dog(内存地址为42)对象的name属性改为Max。
3. 在BBB行
      a.一个新的Dog对象被创建,我们假设它的内存地址是74。
      b.把这个74的内存地址值赋给someDog。
4. 在CCC行
      a.someDog指向一个内存地址为74的Dog对象。
      b.把Dog(内存地址为74)对象的name属性改为Rowlf。
5. 方法执行完毕。
现在让我们来想想在这个方法外面发生了什么:
myDog改变了吗?
这个问题的关键在于:
要明确myDog是一个指针,而不是一个实际的Dog对象。所以答案是它没有改变,myDog的值还是42;它指向的还是最开始的那个Dog对象(虽然在foo方法中的AAA行把它指向对象的name属性改成了Max,但是它指向的还是那个最初的Dog对象)。
这验证了改变所指对象的属性,但没有改变其指向。
Java的运行机制跟C很像。你可以给一个指针赋值,然后把这个指针传递给一个方法,之后在这个方法中你可以改变这个指针指向对象的数据,但是你不能改变这个指针的指向。

在C++,Ada,Pascal以及其他支持引用传递的语言中你可以直接改变传递的参数。如果Java是引用传递的话,那么在执行上面定义的foo方法的BBB行的时候someDog的指向就会被改变。
可以把引用参数当成被传递参数的别名,当这个别名被赋值的时候就相当于被传递的参数被赋值。
这对你有帮助吗?(我会把这个回答补充到我的文章里面去)。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: Stackoverflow问答:Java是传值还是传引用?

17 Apr 12:08

IntelliJ IDEA的光芒会盖过Eclipse吗

by Dony.Xu

作为一个资深的Eclipse用户,我想对IntelliJ IDEA做一个更为严谨的审视。JetBrains的工作人员非常的友善,并为Podcastpedia.orgCodingpedia.org这两个 工程给予了我一个开放源码的许可证。在这片文章中,我列出来Eclipse中常用且与IntelliJ等同的一些操作。写这篇文章为了以后遗忘时能够再用做个记录,也为或许能帮助到其他的人。

快捷键

要事先说!下表中列出了在两个IDE之中我最常用的快捷键:

描述

Eclipse

IntelliJ

代码补全

Ctrl+space

ctrl+space

打开类或者接口

(两个IDE都支持使用“驼峰字符”前缀的方式来过滤查找列表,进而轻松完成搜索;比如:可以使用“PoDI”来检索PodcastDaoImpl类)

 Ctrl+Shift +T

Ctrl+N

快速打开文件/资源

Ctrl+Shift+R

Shift+F6

打开声明

F3

Ctrl+B

查看Javadoc/详情

鼠标滑过(F2聚焦)

Ctrl+Q

快速修复

Alt+1

Alt+Enter

导入所有须要的包

Ctrl+Shift+O

Ctrl+Alt+O

保存文件/保存所有文件

Ctrl+S/Ctrl+Shift+S

自动保存

当前文件快速定位弹出框(成员,方法)

Ctrl+O

Ctrl+F12

源码(生成getter和setter,构造器等)

Alt+Up /Alt+Down

Alt+Insert

当前语法补全

if,do-while,try-catch,return(方法调用)等正确的语法构造(如:添加括号)

Ctrl+Shift+Enter

抽取常量

Ctrl+1->抽取常亮

Ctrl+Alt+C

抽取变量

Ctrl+1->抽取变量

Ctrl+Alt+V

增加、删除以及移动数行代码

在当前插入符添加一行

Shift+Enter

Shift+Enter

复制一行或代码段

Ctrl+Alt+Up/Down

Ctrl+D

删除一行代码

Ctrl+D

Ctrl+Y

选中代码向上或者向下移动

Alt+Up/Down

Shift+Alt+Up/Down

查找/搜索

查找类/变量在工作区或工程中使用

Ctrl+Shift+G

Alt+F7

在工程或者工作区中查找文本

Ctrl+H (选择文件搜索)

Ctrl+Shift+F

导航

回退(撤消最后导航操作)

Alt+Left

Ctrl+Alt+Left

标签/编辑之间的导航

Ctrl + Page Down / Up

Alt + Left/Alt + Right

跳转某一行

Ctrl+L

Ctrl+G

导航到最近的文件

Ctrl + E

Ctrl + E

在编辑器之间快速切换方法

Alt + Up / Down

调试

运行一行

F6

F8

进入下一次计算

F5

F7

运行到下一个断点

F7

Shift+F8

回复运行

F8

F9

链接编辑器

很多时候我们在编辑一个文件,同时还需要编辑其他的文件。假如FF类是一个经常编辑的类,同时又需要对同一个包中的其他类进行编辑—通过链接编辑器的功能,可以迅速在同包的类之间进行切换。这个功能为我们提供了什么样的便利?每当编辑了一个文件,它会立即显示其所在包浏览器视图/项目视图中的位置。如果使用展开式的包视图,它会按功能对类划分并显示,而不使用分层(dao层, service层等)的方式来展示类。这也是我强烈推荐的展示方式,因为真的很方便。

Eclipse

在工程浏览视图或者包浏览视图可以看到并使用链接编辑器(Link to Editor)的按钮。

如果不想使用该功能,依然可以使用Alt+Shift+W快捷键来查看包视图或工程视图并设置其显示位置。

 IntelliJ

在工程视图或者包视图中选择设置,然后勾选根据源码自动滚动(Autoscroll From Source)功能;

如果不想使用该功能,依然可以使用快捷键Alt+F1来导航并设置显示的位置;

IntelliJ的魅力之处

默认设置了许多的功能

IntelliJ本身就自带了众多的功能(如:GitHub的集成)。当然,在Eclipse你也可以通过选择不同版本的插件来获取到足够的功能,只是需要自己来配置这些插件。

使用鼠标滚轮改变字体大小

在IntelliJ中,可以使用鼠标滚轮来改变字体大小(我在浏览器中经常使用该功能)。但是这个功能需要手动激活。

  1. 打开IDE的设置(Ctrl+Shift+S或点击 文件菜单>Setting)
  2. 在编辑器页面(在搜索框中输入“Editor”),确保Change font size (Zoom) with Ctrl+MouseWheel这个选项被选中。

在IDE中直接启动命令行终端

使用快捷键: Alt + F12

灵活易用的模板

输入p,然后使用快捷键Ctrl+J,就可以获取以下选项:

  • psf – public static final
  • psfi – public static final int
  • psfs – public static final String
  • psvm – main method declaration

对JavaScript、HTML5的强力支持

商业版的IntelliJ应该包含了对 HTML5CSS3SASSLESSJavaScriptCoffeeScriptNode.jsActionScript以及其他语言的代码辅助功能。我将尽快地确认这些内容。

相比Eclipse IntelliJ的不足之处

无法最大化控制台

在Eclipse中,可以使用Ctrl+M快捷键或者双击标签来最大化当前的控制台。但是在IntelliJ中并没有类似的方式来。

鼠标悬停显示Javadoc

当然,在IntelliJ中可以使用Ctrl+Q快捷键来获取上述的功能。但当鼠标悬停代码就能看到部分Javadoc的功能在Eclipse中显得是那么的友好。

总结

在我看来,每一个IDE都很棒,IntelliJ看起来更加的现代,但有时候我又喜欢经典版的Eclipse,这可能是因为过去经常使用Eclipse。以后可能会继续受这个因素的影响。

到这里就是我全部的经验,后续将继续添加一些在使用Eclipse和IntelliJ遇到的功能以及功能上的差异,敬请期待。

相关文章

17 Apr 01:59

颠覆大数据分析之Spark弹性分布式数据集

by 我是谁

颠覆大数据分析之Spark弹性数据集

译者:黄经业    购书

Spark中迭代式机器学习算法的数据流可以通过图2.3来进行理解。将它和图2.1中Hadoop MR的迭代式机器学习的数据流比较一下。你会发现在Hadoop MR中每次迭代都会涉及HDFS的读写,而在Spark中则要简单得多。它仅需从HDFS到Spark中的分布式共享对象空间的一次读入——从HDFS文件中创建RDD。RDD可以重用,在机器学习的各个迭代中它都会驻留在内存里,这样能显著地提升性能。当检查结束条件发现迭代结束的时候,会将RDD持久化,把数据写回到HDFS中。后续章节会对Spark的内部结构进行详细介绍——包括它的设计,RDD,以及世系等等。

图2.3  Spark中进行迭代式计算的数据共享

Spark的弹性分布式数据集

RDD这个概念跟我们讨论到的Spark的动机有关——就是能让用户操作分布式系统上的Scala集合。Spark中的这个重要的集合就是RDD。RDD可以通过在其它RDD或者稳态存储中的数据(比如说,HDFS中的文件)上执行确定性操作来进行创建。创建RDD的另一种方式就是将Scala集合并行化。RDD的创建也就是Spark中的转换操作。RDD上除了转换操作,还有其它的一些操作,比如说动作(action)。像map, filter以及join这些都是常见的转换操作。RDD有意思的一点在于它可以将自己的世系或者说创建它所需的转换序列,以及它上面的动作给存储起来。这意味着Spark程序只能拥有一个RDD引用——它知道自己的世系,包括它是如何创建的,上面执行过哪些操作。世系为RDD提供了容错性——即使它丢失了,只要世系本身被持久化或者复制了,就仍能重建整个RDD。RDD的持久化以及分块可以由程序员来指定。比如说,你可以基于记录的主键来进行分块。

在RDD上可以执行许多操作。包括count,collect以及save,它们分别可以用来统计元素总数,返回记录,以及保存到磁盘或者HDFS中。世系图中存储了RDD的转换以及动作。表2.1中列举了一系列的转换及动作。

表2.1

转换 描述
Map(function f1) 把RDD中的每个元素并行地传递给f1,并返回结果的RDD
Filter(function f2) 选取出那些传递给函数f2并返回true的RDD元素
flatMap(function f3) 和map类似,但f3返回的是一个序列,它能将单个输入映射成多个输出。
Union(RDD r1) 返回RDD r1和自身的并集
Sample(flag, p, seed) 返回RDD的百分之p的随机采样(使用种子seed)
动作 描述
groupByKey(noTasks) 只能在键值对数据上进行调用——返回的数据按值进行分组。并行任务的数量通过一个参数来指定(默认是8)
reduceByKey(function f4,noTasks) 对相同key元素上应用函数f4的结果进行聚合。第二个参数是并行的任务数
Join(RDD r2, noTasks) 将RDD r2和对象自身进行连接——计算出指定key的所有可能的组合
groupWith(RDD r3, noTasks) 将RDD r3与对象自身进行连接,并按key进行分组
sortByKey(flag) 根据标记值将RDD自身按升序或降序来进行排序
动作 描述
Reduce(function f5) 使用函数f5来对RDD的所有元素进行聚合
Collect() 将RDD的所有元素作为一个数组来返回
Count() 计算RDD的元素总数
take(n) 获取RDD的第n个元素
First() 等价于take(1)
saveAsTextFile(path) 将RDD持久化成HDFS或者其它Hadoop支持的文件系统中路径为path的一个文件
saveAsSequenceFile(path) 将RDD持久化为Hadoop的一个序列文件。只能在实现了Hadoop写接口或类似接口的键值对类型的RDD上进行调用。
动作 描述
foreach(function f6) 并行地在RDD的元素上运行函数f6

下面将通过一个例子来介绍下如何在Spark环境中进行RDD的编程。这里是一个呼叫数据记录(CDR)——基于影响力分析的应用程序——通过CDR来构建用户的关系图,并识别出影响力最大的K个用户。CDR结构包括id,调用方,接收方,计划类型,呼叫类型,持续时长,时间,日期。具体做法是从HDFS中获取CDR文件,接着创建出RDD对象并过滤记录,然后再在上面执行一些操作,比如说通过查询提取出特定的字段,或者执行诸如count的聚合操作。最终写出的Spark代码如下:

val spark = new SparkContext();

Call_record_lines = spark.textFile(“HDFS://….”);

Plan_a_users = call_record_lines.filter(_.

CONTAINS(“plana”)); // RDD上的过滤操作.

Plan_a_users.cache(); // 告诉Spark运行时,如果仍有空间,就将这个RDD缓存到内存里Plan_a_users.count();

%% 呼叫数据集处理中.

 

RDD可以表示成一张图,这样跟踪RDD在不同转换/动作间的世系变化会简单一些。RDD接口由五部分信息组成,详见表2.2。

表2.2  RDD接口

信息 HadoopRDD FilteredRDD JoinedRDD
分区类型 每个HDFS块一个分区 和父RDD一致 每个reduce任务一个
依赖类型 无依赖 和父RDD是一对一的依赖 在每一个父RDD上进行shuffle
基于父RDD来计算数据集的函数 读取对应块的数据 计算父RDD并进行过滤 读取洗牌后的数据并进行连接
位置元数据(preferredLocations) 从命名节点中读取HDFS块的位置信息 无(从父RDD中获取)
分区元数据(partitioningScheme) HashPartitioner

Spark的实现

Spark是由大概20000行Scala代码写就的,核心部分大概是14000行。Spark可以运行在Mesos, Nimbus或者YARN等集群管理器之上。它使用的是未经修改的Scala解释器。当触发RDD上的一个动作时,一个被称为有向无环图(DAG)调度器的Spark组件就会去检查RDD的世系图,同时会创建各阶段的DAG。每个阶段内都只会出现窄依赖,宽依赖所需的洗牌操作就是阶段的边界。调度器在DAG的不同阶段启动任务来计算出缺失的分区,以便重构整个RDD对象。它将各阶段的任务对象提交给任务调度器(Task Scheduler, TS)。任务对象是一个独立的实体,它由代码和转换以及所需的元数据组成。调度器还负责重新提交那些输出丢失了的阶段。任务调度器使用一个被称为延迟调度(Zaharia等 2010)的调度算法来将任务分配给各个节点。如果RDD中有指定了优先区域的话,任务会被传送给这些节点,否则会被分配到那些有分区在请求内存任务的节点上。对于宽依赖而言,中间记录会在那些包含父分区的节点上生成。这样会使得错误恢复变得简单,Hadoop MR中map输出的物化也是类似的。

Spark中的Worker组件会负责接收任务对象并在一个线程池中调用它们的run方法。它将异常或者错误报告给TaskSetManager(TSM)。TSM是任务调度器管理的一个实体——每个任务集都会对应一个TSM,用于跟踪任务的执行过程。TS是按先进先出的顺序来轮询TSM集的。通过插入不同的策略或者算法,这里仍有一定的优化空间。执行器会与其它的组件进行交互,比如说块管理器(BM),通信管理器(CM),Map输出跟踪器(MOT)。块管理器是节点用于缓存RDD并接收洗牌数据的组件。它也可以看作是每个worker中只写一次的K-V存储。块管理器和通信管理器进行通信以便获取到远端的块数据。通信管理器是一个异步网络库。MOT这个组件会负责跟踪每个map任务都在哪运行并把这些信息返回给归约器——Worker会缓存这个信息。当映射器的输出丢失了的话,会使用一个“分代ID”来将这个缓存置为无效。Spark中各组件的交互如图2.4中所示。

图2.4  Spark集群中的组件

RDD的存储可以通过下面这三种方式来完成:

  1. 作为Java虚拟机中反序列化的Java对象:由于对象就在JVM内存中,这样做的性能会更佳。
  2. 作为内存中序列化的Java对象:这么表示内存的使用率会更高,但却牺牲了访问速度。
  3. 存储在磁盘上:这样做性能最差,但是如果RDD太大以至于无法存放到内存中的话就只能这么做了。

一旦内存满了,Spark的内存管理会通过最近最少使用(LRU)策略来回收RDD。然而,属于同一个RDD的分区是无法剔除的——因为通常来说,一个程序可能会在一个大的RDD上进行计算,如果将同一个RDD中的分区剔除的话则会出现系统颠簸。

世系图拥有足够的信息来重建RDD的丢失分区。然而,考虑到效率的因素(重建整个RDD可能会需要很大的计算量),检查点仍是必需的——用户可以自主控制哪个RDD作为检查点。使用了宽依赖的RDD可以使用检查点,因为在这种情况下,计算丢失的分区会需要显著的通信及计算量。而对于只拥有窄依赖的RDD而言,检查点则不太适合。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: 颠覆大数据分析之Spark弹性分布式数据集

16 Apr 06:32

GNU Hurd 0.6发布

by WinterIsComing
Thomas Schwinge宣布发布GNU Hurd 0.6。GNU Hurd基于GNU Mach微内核,设计替代Unix内核,最早的开发始于1986年,一度开发停滞,可能与Linux内核流行有关,最近几年该项目才再次活跃起来。上个版本GNU Hurd 0.4是在2013年9月发布的。GNU Hurd目前只支持32位x86架构, 支持64位x86架构的版本正在开发之中,而其它处理器架构的支持则还处于寻找开发者的阶段。






16 Apr 06:27

简述负载均衡和CDN技术

by promumu

曾经见到知乎上有人问“为什么像facebook这类的网站需要上千个工程师维护?”,下面的回答多种多样,但总结起来就是:一个高性能的web系统需要从无数个角度去考虑他,大到服务器的布局,小到软件中某个文件的实现,甚至于某个循环内的运算如果出现不严谨都可能导致全盘崩溃。

上面提到web性能优化需要多个角度去考虑,我们无法考虑到所有的优化细节,但可以从我们已知的层面去优化,我们就先从网络层面说起。

 

1.网络请求路径:

 

————————————————————————————————————————|

(客户端输入URL定位符)→(DNS服务器寻找映射)→(进入服务器,处理数据)→(返回数据至客户端)

在这个用例中我们可以很清晰的看出网络请求到返回的过程,虽然非常抽象,但足够我们以他为基础来进行优化了。

————————————————————————————————————————|

 

1)负载均衡 

BOSS一次给了小明好多项任务,小明发现怎么安排时间也做不完,于是乎他盯上了在旁边偷偷看电影的小强,小强突然觉得背后有一股凉气,一回头小明一脸坏笑看着他,“这几个任务交给你,晚上请你吃饭,要不然…嘿嘿嘿”,小强虽然不情愿,但是在小明的请求(要挟)下,只能服从。第二天,小明顺利的完成了任务,给小强买了袋辣条。

在计算机上负载均衡也类似如此,我们的大BOSS客户端将请求发送至服务器,然而一台服务器是无法承受很高的并发量的,我们就会将请求转发到其他服务器,当然真正的负载均衡架构并不是由一台server转发的另一台server,而在客户端与服务器端中间加入了一个负责分配请求的负载均衡硬件(软件)。

 

DNS

名词:DNS是客户端发送请求中一个非常重要的中转,他的作用是将用户请求的URL映射为具体的IP地址,全世界有13台根服务器,但通常为我们进行域名解析的并不是根服务器,而是直接访问我们的 LDNS(Local DNS Server),通常由网络运营商维护。

最早的负载均衡就是利用搭建本地DNS服务器实现的,实现方式简单易懂,为同一个主机名分配多个映射 ,可采用轮循,随机等方式分配请求。看上去没什么问题,但是在使用过程中会发现,如果其中一个地址down机,我们是无法及时发现的,如果有用户被分配到这个主机就会出现访问失败的状况,同时我们也无法判断每个server的负载,可能会出现,某个server几乎闲置,另外一个server负载压力极高的情况。

 

 

 硬件设备

名词:负载均衡器(Load Balancer),负载均衡器通常作为独立的硬件置于客户端与服务器之间。

负载均衡设备拥有非常好的负载均衡性能,他拥有众多的负载均衡策略(权重,动态比率,最快模式,最小连接数等),可以保证以相对较优的方式分配请求,不过好的东西总是有代价的,那就是价格,一台负载均衡器的售价往往高达十几万甚至几十万,许多企业并不愿意为它买单。

 

反向代理

名词:Nginx。高性能,轻量级,已经成了人们对Nginx的第一印象,Nginx可作为HTTP服务器,在处理高并发请求的时候拥有比现在主流的Apache服务器更高的性能,同时Nginx也是一个优秀的反向代理服务器。

第一次听到“反向代理”,可能有些陌生,但如果了解与之对应的正向代理就很好理解了,正向代理通常由客户端主动链接,比如我们的科学上网方式就是使用正向代理,以达到间接访问网站的目的,而反向代理在服务器端,无需主动链接,当我们访问拥有反向代理的网站时,实际访问的是其反向代理服务器,而非真正的服务器,当请求到达反向代理服务器时,反向代理服务器再将请求转发至服务器。反向代理是实现负载均衡的主流手段之一,通常使用Nginx等服务器搭建,Nginx同样拥有众多的分配策略,以保证平均分配压力。

 

Nginx反向代理:

 

BIGIP(硬件)负载均衡:

 

2)CDN

视频总在缓冲,图片各种加载不出来,几年前是再正常不过的事了,在当时大家也没觉得是回事,但把这种情况放在现在,我想人们绝对直接就小红叉了吧,那么我们如何避免这样的情况呢?这就是我要说的,内容分发网络(Content Delivery Network),简称:CDN

CDN简单的来说就是存储一些静态文件的一台或多台服务器,通过复制,缓存等方式,将文件保存其中。

1.哪些是静态文件?

css,html,图片,媒体都属于静态文件,也就是说用户发送的请求不会影响静态文件的内容,而jsp,php等文件就不属于静态文件,因为他们的内容会因我们的请求而发生改变。

2.CDN如何实现加速?

通常情况下,我们所要的数据都是从主服务器中获取,但假如我们的主服务器在南方,而访问用户在北方,那么访问速度就会相对变慢,变慢的原因有很多,例如传输距离,运营商,带宽等等因素,而使用CDN技术的话,我们会将CDN节点分布在各地,当用户发送请求到达服务器时,服务器会根据用户的区域信息,为用户分配最近的CDN服务器。

3.CDN数据从哪里来?

复制,缓存,CDN服务器可以在用户请求后缓存文件,也可以主动抓取主服务器内容。

分布在各地的CDNS:

简述负载均衡和CDN技术,首发于博客 - 伯乐在线

16 Apr 06:27

深入浅出 Git

by wanqu

讲了一些应用场景,不全面,但足以应付大部分use case了。

顺便推荐一个我每天用许多次、爱不释手的命令(如果你以前不知道的话):git grep

16 Apr 01:33

Spark 已经 5 岁了

by Allen

今天,我们庆祝一下Spark项目的一个重要里程碑——Spark已经开源5年了。当我们第一次决定发布我们在加州大学伯克利分校的研究代码时,没有人知道Spark能走多远,但我们相信我们已经建立了一些想要分享给全世界的真正优雅的技术。五年来,我们一直很敬佩众多的贡献者和用户,是他们使Spark变为目前处于领先的计算框架。事实上,据我们所知,Spark现在已经成为大数据领域最活跃的开源项目(看每个月的贡献者或每个月的提交量)。除了贡献者以外,Spark已经根据流处理的批量分析,建立了数以百计的生产实例

为了庆祝Spark的5岁生日,我想做的事情是强调一些如何构建该项目的核心理念,它们在今天仍然适用。为了这一点,我又查看了Spark的第一个公开版本

首先要注意的是这个版本非常地小:它仅仅包含3900行代码,其中包含1300行的Scala解释器,600行的例子和300行的测试。值得欣喜的是,从2010年3月以来,我们的测试覆盖率大幅上涨。然而,Spark的大小反映了一个重要的事实:从一开始,我们就试图保持Spark引擎小而紧凑,这样使许多开发人员更容易理解,也使我们更容易修改和升级。即使在今天,Spark核心引擎也只有50000行代码。对比第一个版本,主要添加的是对“shuffle”操作的支持,它需要新的网络代码和一个DAG调度程序,同时添加了对多个后端调度程序的支持,例如YARN。但是,即使在今天我们仍可以定期地对核心引擎进行大的改变,以提高所有Spark应用程序的性能和稳定性。例如,在我们去年大规模排序的工作中,Databricks的多个开发人员最终改写了几乎所有的Spark网络层。

关于Spark,其次要注意的事是它可以做什么:对于今天的Spark来讲,即使2000行的引擎也可以处理最重要的两个工作,迭代算法和交互式查询。回到2010年,我们是唯一支持交互式使用的集群计算引擎,但我们是通过修改Scala解释器以提交代码到Spark集群的。我们不断寻求改善这种体验的方法,使得能通过Spark的Python APIDataFrames等特性来实现真正的交互式数据科学。此外,即使是2010年版本的Spark,也能够运行逻辑回归等迭代算法,而且逻辑回归的运行效率比MapReduce快20到30倍(后续的改进将效率提高到了100 倍)。

我们思考该项目的最后一个重要的元素是我们专注于简单、稳定的APIs。2010年的Spark附带的代码示例,比如逻辑回归和计算圆周率,和今天的Spark代码几乎相同(查看逻辑回归pi)。我们尽量定义稳定的APIs,从而使开发者可以在未来几年中重复使用,以最小化他们为了跟上Spark更新所必须做的工作量。从Spark1.0开始,这些兼容性保证在所有主要的Spark组件中有效。

2010年的Spark就足够使用了,从那之后它是如何发展的呢?尽管在Spark的所有领域都有巨大的发展,包括支持更多的编程语言(Java、Python,马上支持的R),数据源,代码优化等,但对Spark的最大改变是其标准库。这几年以来,Spark已经有了四个高级库——Spark StreamingMllibGraphXSpark SQL——他们都运行在核心引擎之上,并彼此之间轻松高效地进行交互。今天这些库的代码占Spark代码的大部分—大约200000行,而核心引擎也只有50000行。他们也代表了可用于大数据的单一的最大标准库,从而使得编写那些跨数据生命周期所有阶段的应用程序更加简单。不过,这些库仍然很新,他们中的大多数是最近两年被添加进来的。在未来几年内,我希望这些库能显著成长,目的是为大数据建立一个工具集,同时建立可用于小数据的同等规模的库。你可以从Spark Summit 2015上我的幻灯片中,找到一些Databricks正在维护相关库的领域。

最后,类似于任何5岁的事物,Spark有时仍然会在没有监督的情况下陷入困境,有时也很难理解。在Databricks,我们正通过在Spark代码库和周边支持材料上的努力,尽量使Spark比以往任何时候都更容易使用和运行。我们在Spark上的所有的工作,都是开源的,直接发送给Apache。此外,我们已经提供了大量的免费在线培训材料,以及培训课程书籍。最后,我们建立了一个名为Databricks Cloud的服务,使得Spark更容易运行。我们很享受建立Spark的过程,我们希望无论在哪一种环境中运行它,你也都能很享受使用它的过程。

Spark 已经 5 岁了,首发于博客 - 伯乐在线

16 Apr 01:19

随手记之TCP Keepalive笔记

by nieyong

零。前言

TCP是无感知的虚拟连接,中间断开两端不会立刻得到通知。一般在使用长连接的环境下,需要心跳保活机制可以勉强感知其存活。业务层面有心跳机制,TCP协议也提供了心跳保活机制。

一。TCP Keepalive解读

长连接的环境下,人们一般使用业务层面或上层应用层协议(诸如MQTT,SOCKET.IO等)里面定义和使用。一旦有热数据需要传递,若此时连接已经被中介设备断开,应用程序没有及时感知的话,那么就会导致在一个无效的数据链路层面发送业务数据,结果就是发送失败。

无论是因为客户端意外断电、死机、崩溃、重启,还是中间路由网络无故断开、NAT超时等,服务器端要做到快速感知失败,减少无效链接操作。

1. 交互过程

2. 协议解读

下面协议解读,基于RFC1122#TCP Keep-Alives

  1. TCP Keepalive虽不是标准规范,但操作系统一旦实现,默认情况下须为关闭,可以被上层应用开启和关闭。
  2. TCP Keepalive必须在没有任何数据(包括ACK包)接收之后的周期内才会被发送,允许配置,默认值不能够小于2个小时
  3. 不包含数据的ACK段在被TCP发送时没有可靠性保证,意即一旦发送,不确保一定发送成功。系统实现不能对任何特定探针包作死连接对待
  4. 规范建议keepalive保活包不应该包含数据,但也可以包含1个无意义的字节,比如0x0。
  5. SEG.SEQ = SND.NXT-1,即TCP保活探测报文序列号将前一个TCP报文序列号减1。SND.NXT = RCV.NXT,即下一次发送正常报文序号等于ACK序列号;总之保活报文不在窗口控制范围内 有一张图,可以很容易说明,但请仔细观察Tcp Keepalive部分:

  1. 不太好的TCP堆栈实现,可能会要求保活报文必须携带有1个字节的数据负载
  2. TCP Keepalive应该在服务器端启用,客户端不做任何改动;若单独在客户端启用,若客户端异常崩溃或出现连接故障,存在服务器无限期的为已打开的但已失效的文件描述符消耗资源的严重问题。但在特殊的NFS文件系统环境下,需要客户端和服务器端都要启用Tcp Keepalive机制。
  3. TCP Keepalive不是TCP规范的一部分,有三点需要注意:
    • 在短暂的故障期间,它们可能引起一个良好连接(good connection)被释放(dropped)
    • 它们消费了不必要的宽带
    • 在以数据包计费的互联网消费(额外)花费金钱

二。Tcp keepalive 如何使用

以下环境是在Linux服务器上进行。应用程序若想使用,需要设置SO_KEEPALIVE套接口选项才能够生效。

1. 系统内核参数配置

  1. tcp_keepalive_time,在TCP保活打开的情况下,最后一次数据交换到TCP发送第一个保活探测包的间隔,即允许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为7200s(2h)。
  2. tcp_keepalive_probes 在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包次数,默认值为9(次)
  3. tcp_keepalive_intvl,在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为75s。

发送频率tcp_keepalive_intvl乘以发送次数tcp_keepalive_probes,就得到了从开始探测到放弃探测确定连接断开的时间

若设置,服务器在客户端连接空闲的时候,每90秒发送一次保活探测包到客户端,若没有及时收到客户端的TCP Keepalive ACK确认,将继续等待15秒*2=30秒。总之可以在90s+30s=120秒(两分钟)时间内可检测到连接失效与否。

以下改动,需要写入到/etc/sysctl.conf文件:

net.ipv4.tcp_keepalive_time=90
net.ipv4.tcp_keepalive_intvl=15
net.ipv4.tcp_keepalive_probes=2

保存退出,然后执行sysctl -p生效。可通过 sysctl -a | grep keepalive 命令检测一下是否已经生效。

针对已经设置SO_KEEPALIVE的套接字,应用程序不用重启,内核直接生效。

2. Java/netty服务器如何使用

只需要在服务器端一方设置即可,客户端完全不用设置,比如基于netty 4服务器程序:

ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .option(ChannelOption.SO_BACKLOG, 100)
             .childOption(ChannelOption.SO_KEEPALIVE, true)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ch.pipeline().addLast(
                             new EchoServerHandler());
                 }
             });

            // Start the server.
            ChannelFuture f = b.bind(port).sync();

            // Wait until the server socket is closed.
            f.channel().closeFuture().sync();

Java程序只能做到设置SO_KEEPALIVE选项,至于TCP_KEEPCNT,TCP_KEEPIDLE,TCP_KEEPINTVL等参数配置,只能依赖于sysctl配置,系统进行读取。

3. C语言如何设置

下面代码摘取自libkeepalive源码,C语言可以设置更为详细的TCP内核参数。

int socket(int domain, int type, int protocol)
{
  int (*libc_socket)(int, int, int);
  int s, optval;
  char *env;

  *(void **)(&libc_socket) = dlsym(RTLD_NEXT, "socket");
  if(dlerror()) {
    errno = EACCES;
    return -1;
  }

  if((s = (*libc_socket)(domain, type, protocol)) != -1) {
    if((domain == PF_INET) && (type == SOCK_STREAM)) {
      if(!(env = getenv("KEEPALIVE")) || strcasecmp(env, "off")) {
        optval = 1;
      } else {
        optval = 0;
      }
      if(!(env = getenv("KEEPALIVE")) || strcasecmp(env, "skip")) {
        setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));
      }
#ifdef TCP_KEEPCNT
      if((env = getenv("KEEPCNT")) && ((optval = atoi(env)) >= 0)) {
        setsockopt(s, SOL_TCP, TCP_KEEPCNT, &optval, sizeof(optval));
      }
#endif
#ifdef TCP_KEEPIDLE
      if((env = getenv("KEEPIDLE")) && ((optval = atoi(env)) >= 0)) {
        setsockopt(s, SOL_TCP, TCP_KEEPIDLE, &optval, sizeof(optval));
      }
#endif
#ifdef TCP_KEEPINTVL
      if((env = getenv("KEEPINTVL")) && ((optval = atoi(env)) >= 0)) {
        setsockopt(s, SOL_TCP, TCP_KEEPINTVL, &optval, sizeof(optval));
      }
#endif
    }
  }

   return s;
}

4. 针对已有程序没有硬编码KTTCP EEPALIVE实现

完全可以借助于第三方工具libkeepalive,通过LD_PRELOAD方式实现。比如

LD_PRELOAD=/the/path/libkeepalive.so java -jar /your/path/yourapp.jar &

这个工具还有一个比较方便的地方,可以直接在程序运行前指定TCP保活详细参数,可以省去配置sysctl.conf的麻烦:

LD_PRELOAD=/the/path/libkeepalive.so \
  > KEEPCNT=20 \
  > KEEPIDLE=180 \
  > KEEPINTVL=60 \
  > java -jar /your/path/yourapp.jar &

针对较老很久不更新的程序,可以尝试一下嘛。

三。Linux内核层面对keepalive处理

参数和定义

#define MAX_TCP_KEEPIDLE     32767
#define MAX_TCP_KEEPINTVL     32767
#define MAX_TCP_KEEPCNT          127
#define MAX_TCP_SYNCNT          127

#define TCP_KEEPIDLE          4     /* Start keeplives after this period */
#define TCP_KEEPINTVL          5     /* Interval between keepalives */
#define TCP_KEEPCNT          6     /* Number of keepalives before death */

net/ipv4/Tcp.c,可以找到对应关系:

     case TCP_KEEPIDLE:
          val = (tp->keepalive_time ? : sysctl_tcp_keepalive_time) / HZ;
          break;
     case TCP_KEEPINTVL:
          val = (tp->keepalive_intvl ? : sysctl_tcp_keepalive_intvl) / HZ;
          break;
     case TCP_KEEPCNT:
          val = tp->keepalive_probes ? : sysctl_tcp_keepalive_probes;
          break;

初始化:

 case TCP_KEEPIDLE:
      if (val < 1 || val > MAX_TCP_KEEPIDLE)
           err = -EINVAL;
      else {
           tp->keepalive_time = val * HZ;
           if (sock_flag(sk, SOCK_KEEPOPEN) &&
               !((1 << sk->sk_state) &
                 (TCPF_CLOSE | TCPF_LISTEN))) {
                __u32 elapsed = tcp_time_stamp - tp->rcv_tstamp;
                if (tp->keepalive_time > elapsed)
                     elapsed = tp->keepalive_time - elapsed;
                else
                     elapsed = 0;
                inet_csk_reset_keepalive_timer(sk, elapsed);
           }
      }
      break;
 case TCP_KEEPINTVL:
      if (val < 1 || val > MAX_TCP_KEEPINTVL)
           err = -EINVAL;
      else
           tp->keepalive_intvl = val * HZ;
      break;
 case TCP_KEEPCNT:
      if (val < 1 || val > MAX_TCP_KEEPCNT)
           err = -EINVAL;
      else
           tp->keepalive_probes = val;
      break;

这里可以找到大部分处理逻辑,net/ipv4/Tcp_timer.c:

static void tcp_keepalive_timer (unsigned long data)
{
     struct sock *sk = (struct sock *) data;
     struct inet_connection_sock *icsk = inet_csk(sk);
     struct tcp_sock *tp = tcp_sk(sk);
     __u32 elapsed;

     /* Only process if socket is not in use. */
     bh_lock_sock(sk);
     if (sock_owned_by_user(sk)) {
          /* Try again later. */
          inet_csk_reset_keepalive_timer (sk, HZ/20);
          goto out;
     }

     if (sk->sk_state == TCP_LISTEN) {
          tcp_synack_timer(sk);
          goto out;
     }
    // 关闭状态的处理
     if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {
          if (tp->linger2 >= 0) {
               const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;

               if (tmo > 0) {
                    tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
                    goto out;
               }
          }
          tcp_send_active_reset(sk, GFP_ATOMIC);
          goto death;
     }

     if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE)
          goto out;

     elapsed = keepalive_time_when(tp);

     /* It is alive without keepalive 8) */
     if (tp->packets_out || sk->sk_send_head)
          goto resched;

     elapsed = tcp_time_stamp - tp->rcv_tstamp;

     if (elapsed >= keepalive_time_when(tp)) {
          if ((!tp->keepalive_probes && icsk->icsk_probes_out >= sysctl_tcp_keepalive_probes) ||
               (tp->keepalive_probes && icsk->icsk_probes_out >= tp->keepalive_probes)) {
               tcp_send_active_reset(sk, GFP_ATOMIC);
               tcp_write_err(sk); // 向上层应用汇报连接异常
               goto out;
          }
          if (tcp_write_wakeup(sk) <= 0) {
               icsk->icsk_probes_out++; // 这里仅仅是计数,并没有再次发送保活探测包
               elapsed = keepalive_intvl_when(tp);
          } else {
               /* If keepalive was lost due to local congestion,
               * try harder.
               */
               elapsed = TCP_RESOURCE_PROBE_INTERVAL;
          }
     } else {
          /* It is tp->rcv_tstamp + keepalive_time_when(tp) */
          elapsed = keepalive_time_when(tp) - elapsed;
     }

     TCP_CHECK_TIMER(sk);
     sk_stream_mem_reclaim(sk);

resched:
     inet_csk_reset_keepalive_timer (sk, elapsed);
     goto out;

death:    
     tcp_done(sk);

out:
     bh_unlock_sock(sk);
     sock_put(sk);
}

keepalive_intvl_when 函数定义:

static inline int keepalive_intvl_when(const struct tcp_sock *tp)
{
    return tp->keepalive_intvl ? : sysctl_tcp_keepalive_intvl;
}

四。TCP Keepalive 引发的错误

启用TCP Keepalive的应用程序,一般可以捕获到下面几种类型错误

  1. ETIMEOUT 超时错误,在发送一个探测保护包经过(tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes)时间后仍然没有接收到ACK确认情况下触发的异常,套接字被关闭
    java.io.IOException: Connection timed out
    
  2. EHOSTUNREACH host unreachable(主机不可达)错误,这个应该是ICMP汇报给上层应用的。
    java.io.IOException: No route to host
    
  3. 链接被重置,终端可能崩溃死机重启之后,接收到来自服务器的报文,然物是人非,前朝往事,只能报以无奈重置宣告之。
    java.io.IOException: Connection reset by peer
    

五。常见的使用模式

  1. 默认情况下使用keepalive周期为2个小时,如不选择更改,属于误用范畴,造成资源浪费:内核会为每一个连接都打开一个保活计时器,N个连接会打开N个保活计时器。 优势很明显:
  • TCP协议层面保活探测机制,系统内核完全替上层应用自动给做好了
  • 内核层面计时器相比上层应用,更为高效
  • 上层应用只需要处理数据收发、连接异常通知即可
  • 数据包将更为紧凑
  1. 关闭TCP的keepalive,完全使用业务层面心跳保活机制 完全应用掌管心跳,灵活和可控,比如每一个连接心跳周期的可根据需要减少或延长
  2. 业务心跳 + TCP keepalive一起使用,互相作为补充,但TCP保活探测周期和应用的心跳周期要协调,以互补方可,不能够差距过大,否则将达不到设想的效果。朋友的公司所做IM平台业务心跳2-5分钟智能调整 + tcp keepalive 300秒,组合协作,据说效果也不错。

虽然说没有固定的模式可遵循,那么有以下原则可以参考:

  • 不想折腾,那就弃用TCP Keepalive吧,完全依赖应用层心跳机制,灵活可控性强
  • 除非可以很好把控TCP Keepalive机制,那就可以根据需要自由使用吧

六。注意和 HTTP的Keep-Alive区别

  • HTTP协议的Keep-Alive意图在于连接复用,同一个连接上串行方式传递请求-响应数据
  • TCP的keepalive机制意图在于保活、心跳,检测连接错误。

七。引用

  1. 我来说说TCP保活
  2. TCP Keepalive HOWTO


nieyong 2015-04-14 17:08 发表评论
13 Apr 01:41

并发环境下HashMap引起的full gc排查

by ol_beta

作者:佐井    原文地址

现象

最近上线一个需求,完成需求的过程对代码进行了一次重构。应用发布后半个小时左右,发现一个机器报警,load过高。登陆机器看CPU使用情况,发现load已经正常,看下CPU使用情况,发现有一个核跑满,其他CPU使用率很低。大概一个小时后,其他机器陆续报警,发现同样的问题,紧急回滚应用。

应用运行在16G内存的虚机上,整个JVM11G内存,其中新生代3G,CMS gc,JDK7。

第一反应是JVM可能在进行full gc,因为只有一个线程跑满,其他线程被JVM暂停了。先去应用日志看下应用运行情况,果然日志已经没有任何输出。jstat -gcutil查看JVM内存使用情况,发现Old区使用已经100%。

摘掉服务

考虑到full gc导致RT变得超长,去ateye摘掉应用注册的HSF服务,但是操作失败,整个JVM已经没有响应。

保留现场

jmap -F -dump:format=b,file=atw.bin `jid` 

把整个堆dump到本地,dump失败,JVM已经僵死。

jmap -histo  `jid` > histo.log

保留histo内存快照成功;)

jstack `jid` > stack.log

JVM线程信息保存成功:)

现场保存完毕,重启应用。

初步分析

首先看下JVM线程栈信息,看看下是否有应用线程阻赛,一般情况下,如果大量线程阻赛,每个线程都持有一定量的内存,很可能导致内存吃紧,而这些阻塞的线程又没有处理完请求,占用的heap空间不能被minor gc回收掉,导致产生full gc,

cat stack.log | grep atw | sort | uniq -c | sort -nr | head -10

结果如下(重新排版过):

 177    at ...service.impl...searchInterProduct(AtwSearchServiceImpl.java:505)
 104    at ...service.impl..searchOneWay(AtwSearchServiceImpl.java:260)
  80    at ...service.impl.executor...execute(OneWayCommonAgentSearchExecutor.java:419)
  70    at ...service.impl.executor...handleFlights(AbstractSearchExecutor.java:689)
  47    at ...service.impl...searchOneWay(AtwSearchServiceImpl.java:257)
  31    at ...service.impl.executor...getFlightInfoAndStock(AbstractSearchExecutor.java:1073)
  30    at ...service.impl.executor...getFlightInfoAndStock(AbstractSearchExecutor.java:1087)
  22    at ...util.stlog.FarePolicyCounter.addFail(FarePolicyCounter.java:249)
  20    at ...service.impl.executor...execute(OneWayCommonAgentSearchExecutor.java:424)
  20    at ...service.impl.executor...getAllFares(AbstractSearchExecutor.java:892)

HSF线程开了200个,应用相关的正在运行的线程最多的是com.taobao.trip.atw.service.impl.AtwSearchServiceImpl.searchInterProduct(AtwSearchServiceImpl.java:505),一共177个,小于HSF线程数,属于正常,其他线程数量也在正常范围内。线程的锁和其他信息也未发现异常。

接下来看下histo.log:

num     #instances         #bytes  class name
----------------------------------------------
  1:        204258     4368429800  [B
  2:       6812683      926524888  com.taobao.trip.atw.domain.AtwInterFareDO
  3:      22639343      724458976  java.util.HashMap$Entry
  4:      22304135      538457776  [S
  5:      21614962      518759088  java.lang.Long
  6:      13867918      443773376  com.taobao.trip.atw.util.LongReferenceConcurrentHashMap$HashEntry
  7:       6812439      326997072  com.taobao.trip.atw.domain.AtwInterFareSegmentDO
  8:        421442      211696296  [J
  9:        557827      199825568  [Ljava.util.HashMap$Entry;
 10:       6812439      163498536  [Lcom.taobao.trip.atw.domain.AtwInterFareSegmentDO;

发现最大的内存对象是byte数组,204258个实例大约占用4G堆空间(整个堆11G),平均每个20k。

初步猜测可能本次上线代码还有new byte[]的地方,即查看代码,发现本次新增功能没有这样的代码。而且整个应用的代码也无可疑地方产生了byte数组。

继续分析可能是依赖的二方或者三方jar包引起,重新申请分支,提发布单,查看发布包变化:

image

通过对比发现,本次发布涉及jar包变更很小,而且无三方包变更,只有内部包发生变化。对变化的包进行分析没有找到new byte[]地方。

继续分析histo.log,找到一台线上正常机器,生成histo,用故障机器数据减去正常值,得到差值如下(top 10):

class instances bytes
[B 47404 4275481936
java.util.HashMap$Entry 19442009 622144288
java.lang.Long 19711584 473078016
[Ljava.util.HashMap$Entry; 239216 164734560
com.taobao.at.dal.common.dataobject.AtwMessage 359668 20141408
java.util.HashMap 215770 10356960
java.util.concurrent.LinkedBlockingQueue$Node 421036 10104864
com.taobao.trip.atw.metaq.service.common.LocalMessageReactor$1 359769 8634456
com.alibaba.rocketmq.common.message.MessageExt 65151 6775704

除了byte[] 外,java.util.HashMap$Entry比正常机器多2kw,查看代码也没有明显证据能解释HashMap和byte[]同时增大的场景。

至此,分析思路阻塞,需要找到新的线索。

通过GC日志找到新线索

通过上面的分析,已经找到现象:应用出现了full gc,而且伴随大量byte[]和java.util.HashMap$Entry不能回收。

然而,full gc最直接的产物gc.log还没有被挖掘。根据full gc时间点,发现新线索(重新排版过):)

==WARNNING==  allocating large array--thread_id[0x00007f71211b0800]--thread_name[owc--425027705]--array_size[2132509912 bytes]--array_length[2132509891 elememts]
prio=10 tid=0x00007f71211b0800 nid=0x3f43e runnable
    at com.alibaba.dubbo.common.io.Bytes.copyOf(Bytes.java:59)
    at com.alibaba.dubbo.common.io...write(UnsafeByteArrayOutputStream.java:64)
    at com.taobao.hsf.com.caucho.hessian.io...flushBuffer(Hessian2Output.java:1553)
    at com.taobao.hsf.com.caucho.hessian.io...printString(Hessian2Output.java:1466)
    at com.taobao.hsf.com.caucho.hessian.io...writeString(Hessian2Output.java:987)
    at com.taobao.hsf.com.caucho.hessian.io...writeObject(BasicSerializer.java:149)
    at com.taobao.hsf.com.caucho.hessian.io...writeObject(Hessian2Output.java:421)
    at com.taobao.hsf.com.caucho.hessian.io...writeObject(MapSerializer.java:99)
    at com.taobao.hsf.com.caucho.hessian.io...writeObject(Hessian2Output.java:421)
    at com.taobao.hsf.com.caucho.hessian.io...serialize(UnsafeSerializer.java:293)
    at com.taobao.hsf.com.caucho.hessian.io...writeInstance(UnsafeSerializer.java:212)
    at com.taobao.hsf.com.caucho.hessian.io...writeObject(UnsafeSerializer.java:171)
    at com.taobao.hsf.com.caucho.hessian.io.H..writeObject(Hessian2Output.java:421)
    at com.taobao.hsf.remoting.serialize...encode(Hessian2Encoder.java:23)
    at com.taobao.hsf.remoting.server.output...writeHSFResponse(RpcOutput.java:47)
    at com.taobao.hsf.remoting.provider...handleRequest(ProviderProcessor.java:202)
    at com.taobao.hsf.remoting.server...handleRequest(RPCServerHandler.java:47)
    at com.taobao.hsf.remoting.server..r.handleRequest(RPCServerHandler.java:25)
    ...

阿里定制的JVM增加了许多自己的新特性,其中一个就是在full gc不能回收的情况下,会把当前分配最大内存的线程信息和分配的内存信息打印出来!

==WARNNING== allocating large array–thread_id[0x00007f71211b0800]–thread_name[owc–425027705]–array_size[2132509912 bytes]–array_length[2132509891 elememts]

线程owc--425027705,这是一个应用自己处理HSF请求的线程,它在分配一个巨大的数据组!通过gc日志的堆栈信息发现当前这个线程正在处理byte[]的拷贝:

at com.alibaba.dubbo.common.io.Bytes.copyOf(Bytes.java:59)

这个拷贝过程一般是,应用处理好HSF请求后,把处理结果序列化成byte[],然后通过网路传输到调用机器上。

至此找到了byte[]产生的原因,还有java.util.HashMap$Entry未解决。

根据线程名字owc--425027705去JVM的线程日志查找信息,发现owc--425027705是处理请求的主线程,下面有四个子线程都在处理这样的堆栈:

"owc--425027705-344" daemon prio=10 tid=0x00007f710278f800 nid=0x3f414 runnable [0x0000000051906000]
   java.lang.Thread.State: RUNNABLE
    at java.util.HashMap.getEntry(HashMap.java:469)
    at java.util.HashMap.get(HashMap.java:421)
    at com.taobao.trip.atw.result.impl.DefaultPriceMergerOW.processHeightQuality(DefaultPriceMergerOW.java:327)
    at com.taobao.trip.atw.result.impl.DefaultPriceMergerOW.extendedProductProcess(DefaultPriceMergerOW.java:179)
    at com.taobao.trip.atw.result.impl.DefaultPriceMergerOW.mergeOneWay(DefaultPriceMergerOW.java:137)
    at com.taobao.trip.atw.result.PriceMergerProxy.mergeOneWay(PriceMergerProxy.java:184)
    ...

子线程都在从HashMap中get数据!由于之前遇到过HashMap多线程操作导致成环形数据结构,继而get操作成死循环的教训,这里断定是HashMap问题!

HashMap多线程下成死循环原因

简短的说,多线程下对HashMap的put操作,会导致内部的Entry链表形成环形数据结构。
首先,put操作会检查容量是否充足,如果不足,会resize内部数组。

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

问题就在于resize内部会遍历Entry的链表:

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

这样的代码在多线程情况下,会出现环。

对于成环的Map,get遍历Entry链表时会导致死循环:

final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }

    int hash = (key == null) ? 0 : hash(key);
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}        

为什么会出现多个线程同时操作一个HashMap?

处理逻辑

主线程收到请求后,会分配4个子线程去计算结果,然后由主线程去完成对结果的合并。如果子线程处理失败或者超时,那么这个子线程的结果会被丢弃,不会被合并。

从日志上看,4个子线程的处理都已经超时,但是由于HashMap并发操作造成死循环,4个子线程仍然在运行,主线程丢弃了子线程的结果,那数量量应该非常小才对,为何会产生如此大的byte[]?

追根溯源,从主线程分配任务找到了端倪。4个子线程处理计算的结果对象都是从主线程拷贝过来的:

BeanUtils.copyProperties(main, rsp);

这次修改在结果对象上增加了一个HashMap:

    private Map<Long,Map<ItemGroupType,ItemDO>> agentItemGroup;
    
    public Map<Long, Map<ItemGroupType, ItemDO>> getAgentItemGroup() {
        if (agentItemGroup == null) {
            agentItemGroup = new HashMap<Long, Map<ItemGroupType, ItemDO>>();
        }
        return agentItemGroup;
    }

    public void setAgentItemGroup(Map<Long, Map<ItemGroupType, ItemDO>> agentItemGroup) {
        this.agentItemGroup = agentItemGroup;
    }

agentItemGroup的get方法会判断是否null,如果是的话,会生成一个新的map。

org.springframework.beans.BeanUtils#copyProperties(java.lang.Object, java.lang.Object)方法中,对象属性的赋值会调用get/set方法,(参考:org.springframework.beans.BeanUtils#copyProperties(java.lang.Object, java.lang.Object, java.lang.Class, java.lang.String[]))这样就导致4个子线程用的map跟主线程是同一个map,而且就算子线程的结果被放弃了,主线程的map已经被搞坏。

com.taobao.hsf.com.caucho.hessian.io.MapSerializer.writeObject(MapSerializer.java:99)

HSF在对Map的序列化时候,对遍历Map,进行序列化:

    public void writeObject(Object obj, AbstractHessianOutput out) throws IOException {
        if(!out.addRef(obj)) {
            Map map = (Map)obj;
            Class cl = obj.getClass();
            if(!cl.equals(HashMap.class) && this._isSendJavaType && obj instanceof Serializable) {
                out.writeMapBegin(obj.getClass().getName());
            } else {
                out.writeMapBegin((String)null);
            }

            Iterator iter = map.entrySet().iterator();

            while(iter.hasNext()) {
                Entry entry = (Entry)iter.next();
                out.writeObject(entry.getKey());
                out.writeObject(entry.getValue());
            }

            out.writeMapEnd();
        }
    }

由于主线程的map已经成环形数据结构,遍历的迭代器会死循环执行。

至此,full gc现象全部排查完毕,解决方案,一行代码到搞定:

BeanUtils.copyProperties(main, rsp);
rsp.setAgentItemGroup(new HashMap<Long, Map<ItemGroupType, ItemDO>>()); 

总结

并发环境下被HashMap坑不止一次,很多时候,写代码没有考虑并发场景,熟知写的代码已经是在并发环境运行了。这样就酿成大错,其实后来想想,HashMap也可以做一下改进,get中如果循环超过size次了,抛出个异常,也不会导致死循环和full gc了 :)

但这并不能根治问题,写代码还是要多想想,加强reivew!