Jason Wang's Playground


  • Home

  • Archives

  • Tags

  • 公益404

Dive into Java -- (5) Basics

Posted on 2016-07-23

Java 基本知识

Java 基本类型

基本数据类型

类型 字长 说明
byte 1 字节型
short 2 短整型
int 4 整型
long 8 长整型
float 4 单精度浮点数
double 8 双精度浮点数
char 字符型
boolean 布尔型

long 和 double 在内存中占两个 SLOT,其它类型在内存中占一个 SLOT。

String 不是基本数据类型,它是一种特殊的类。

Object

java.lang.Object

java.lang 包在使用的时候无需显示导入,编译时由编译器自动导入。 Object 类是类层次结构的根,Java 中所有的类从根本上都继承自这个类。 Object 类是 Java 中唯一没有父类的类。其他所有的类,包括标准容器类,比如数组,都继承了 Object 类中的方法。

Object 类中的方法如下:

protected Object clone()

Creates and returns a copy of this object.

  • 会抛出 CloneNotSupportedException

使用这个方法的类必须实现 java.lang.Cloneable 接口,否则会抛出CloneNotSupportedException 异常。而 Cloneable 接口其实只是个空接口,不需要写任何方法。

  • 这个方法是 protected 修饰的 重写 clone() 方法的时候需要写成 public,以让类外部的代码调用。

boolean equals(Object obj)

Indicates whether some other object is “equal to” this one.

equals() 方法需要具有如下特点:

  • 自反性(reflexive):任何非空引用 x,x.equals(x) 返回为true。

  • 对称性(symmetric):任何非空引用 x 和 y,x.equals(y) 返回 true 当且仅当y.equals(x) 返回 true。

  • 传递性(transitive):任何非空引用 x 和 y,如果 x.equals(y) 返回true,并且y.equals(z) 返回 true,那么 x.equals(z) 返回 true。

  • 一致性(consistent):两个非空引用 x 和 y,x.equals(y) 的多次调用应该保持一致的结果(前提条件是在多次比较之间没有修改 x 和 y 用于比较的相关信息)。

约定:

  • 对于任何非空引用 x,x.equals(null) 应该返回为 false。
  • 重写 equals() 方法时,应该同时重写 hashCode() 方法。

== VS equals()

  • == 运算符判断两个引用是否指向同一个对象。
  • equals() 方法应当用来判断两个对象内容是否相等。

Object 类中的 equals() 方法代码如下:

1
2
3
4
public boolean equals(Object obj)
{

return (this == obj);
}

即 Object 类中的 equals() 方法等价于 == 。

如果需要判断两个对象是否内容相等时,应当重写 equals() 方法。

int hashCode()

Returns a hash code value for the object.

当重写 equals() 方法时,必须重写 hashCode() 方法。

hashCode 需要满足的条件如下:

(1) 在 Java 应用的一次执行过程中,如果对象用于 equals() 比较的信息没有被修改,那么同一个对象多次调用 hashCode() 方法应该返回同一个整型值。

应用的多次执行中,这个值不需要保持一致,即每次执行都是保持着各自不同的值。

(2) 如果 equals() 判断两个对象相等,那么它们的 hashCode() 方法应该返回同样的值。

(3) 如果 equals() 判断两个对象不相等,那么它们的 hashCode() 方法并不需要不同。

即,两个对象用 equals() 方法比较返回 false ,它们的 hashCode 可以相同也可以不同。但是,为不相等的对象产生不同的 hashCode 可以改善哈希表的性能。

Object 类默认的 hashCode() 方法返回对象的地址。

String toString()

Returns a string representation of the object.

Object 类中的 toString() 方法定义如下:

1
2
3
4
public String toString()
{

return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

Integer

Integer 类是 int 类型的装箱类

int

  1. int 是基本数据类型
  2. 使用时无需使用 new 创建对象,也不需要额外的引用空间保存辅助信息
  3. 由于不是对象,没有相应的方法,在某些场合使用受限

Integer:

  1. Integer 是一种封装类,它用 final 关键字修饰,不能被继承
  2. Integer 弥补了基本数据类型在面向对象方面的欠缺,它可以实例化对象,且该类中还提供了多个用于处理 int 型数据的功能方法。
  3. 可以通过自动的拆箱和封箱在 int 和 Integer 类型之间自动转换。

String

String 是一种特殊的类,String 类初始化后是不可变的(immutable)。

在进行连接操作时,String 每次返回一个新的 String 实例,会产生很多临时变量,因此,大量字符串拼接时,最好使用 StringBuffer 或 StringBuilder。

StringBuffer/StringBuilder

StringBuffer 和 StringBuilder 都可以用来拼接字符串,两者都不会生成临时变量。

区别:

StringBuilder 是非线程安全版 StringBuffer,性能优于 StringBuffer。

hashCode()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/** 
* Returns a hash code for this string. The hash code for a
* <code>String</code> object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using <code>int</code> arithmetic, where <code>s[i]</code> is the
* <i>i</i>th character of the string, <code>n</code> is the length of
* the string, and <code>^</code> indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/

public int hashCode() {
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value;
int len = count;

for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}

语法糖

foreach 是一种 Iterator 语法糖,其原理如下:

foreach 用法:

1
2
3
for (String s : strings) {
System.out.println(s);
}

类似于:

1
2
3
4
5
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
System.out.println(s);
}

修饰符

访问控制

修饰符 说明
private 只能在同一个类中访问
默认 只能在同一个包中访问
protected 可以在同一个包和子类中访问
public 不限制访问

static

  • 修饰变量
  • 修饰方法
  • 修饰静态块
1
2
3
4
5
6
class A {
public static String foobar;
static {
foobar = "hello";
}
}
  • 修饰静态内部类
1
2
3
4
5
6
7
8
class Outer {
static class Inner {
//...
}
}

// 实例化
Outer.Inner in = new Outer.Inner();
  • 静态导入

使用静态导入可以使被导入类的静态变量和静态方法在当前类直接可见,使用这些静态成员无需再给出他们的类名。

1
2
3
4
5
6
7
import static java.lang.System.out; //导入java.lang包下的System类的静态变量out;

public class HelloWorld {
public static void main(String[] args) {
out.print("Hello World!"); //不用再写成System.out.println("Hello World!")了,因为已经导入了这个静态变量out。
}
}

final

final 关键字可用于修饰类、变量和方法,用于表示它所修饰的类、变量和方法不可改变。

修饰对象 说明
变量 表示该变量一旦获得了初始值之后就不可被改变,final 既可修饰成员变量(包括类变量和实例变量),也可以修饰局部变量、形参。
方法 方法不可被重写,如果不希望子类重写父类的某个方法,则可以使用 final 修饰该方法。
类 不可以有子类

例子

1
2
3
4
5
final StringBuffer a = new StringBuffer("foo"); 
final StringBuffer b = new StringBuffer("bar");

a = b; // 编译失败
a.append("bar"); // 编译通过

由此可见,final 只对引用的值(即内存地址)有效,引用只能指向初始指向的那个对象,之后无法改变它的指向。但是 final 无法阻止它所指向的对象的变化。

对比 finally 和 finalize

finally 关键字用于异常处理机制,往往跟在 try-catch 语句块之后,finally 语句所包含的代码几乎在任何情况下都会被执行,且在 try-catch 语句块中只能有一个 finally 语句块。

finalize,垃圾回收器要回收对象的时候,首先要调用这个类的 finalize 方法。 利用 finalization 机制,用户可以定义一些特殊的操作,这些操作在一个对象将要被垃圾回收程序释放时执行。 但是 finalize 的执行是不确定的,既不确定由哪个线程执行,也不确定执行的顺序。因此不推荐使用此方法。

transient

变量修饰符,如果用 transient 声明一个实例变量,当对象存储时,它的值不需要维持。也就是说,用 transient 关键字标记的成员变量不参与序列化过程。

Reference

  1. Java™ Platform, Standard Edition 8 API Specification. Oracle. http://docs.oracle.com/javase/8/docs/api/overview-summary.html
  2. Java Object类. 圣骑士wind. http://www.cnblogs.com/mengdd/archive/2013/01/03/2842809.html

Dive into Java -- (4) Object-Oriented

Posted on 2016-07-18

面向对象

面向对象的特征有三方面:

  • Encapsulation (封装)
  • Inheritence (继承)
  • Polymorphism (多态)

继承

Java 只支持单继承,但可以实现多个接口

abstract class VS interface

abstract class interface
设计目的 在代码实现方面发挥作用,可以实现代码的重用 在系统框架设计方法发挥作用,主要定义模块之间的通信
初始化块 可以 不可以
构造方法 可以
但是不能用于创建对象,而是让子类调用
不可以
方法 没有限制,可以同时包含抽象和非抽象的方法 只能并且默认是 public abstract 方法
属性 静态常量属性和普通属性 只能并且默认是 public static final 属性
继承/实现 一个类是只能继承一个抽象类 一个类可以可以实现多个接口
一个接口不能实现另一个接口,但它可以继承多个其他接口。
方法实现 重写父类的抽象方法或者将子类定义为抽象类 必须要实现接口声明的所有方法

示例:

1
2
3
4
interface A {}
interface B {}

interface C extends A, B {} // 接口可以继承多个接口, A 和 B 都是接口

多态

Java 中实现多态的机制是:

  1. Override(覆盖)
  2. Overload(重载)

对比:

Override(覆盖) Overloade(重载)
Scope 父类与子类之间多态性的一种表现 一个类中多态性的一种表现
基本概念 子类中定义的方法与其父类中的方法有相同的名称、参数列表、返回值类型 一个类中具有相同方法名的方法的参数列表在顺序、个数、类型上存在不同

(与方法的返回值类型无关)

Override (覆盖)

  1. 方法的参数签名和返回值类型必须相同,访问控制修饰符可以不同,但是子类方法不能缩小父类方法的访问权限。
  2. 子类方法抛出的异常必须和父类方法抛出的异常相同,或者是父类方法抛出的异常类的子类。
  3. 父类的静态方法是不能被子类覆盖为非静态方法。父类的非静态方法不能被子类覆盖为静态方法。
  4. 子类可以定义与父类的静态方法同名的静态方法,以便在子类中隐藏父类的静态方法。区别:运行时,JVM 把静态方法和所属的类绑定,而把实例方法和所属的实例绑定。
  5. 父类的私有方法不能被覆盖
  6. 父类的非抽象方法可以被覆盖为抽象方法

JVM 实现

原则:“编译看左边,运行看右边”,即:在编译时,看引用类型的类是否有此方法;在运行时,看对象所在的类是否有调用的方法。

1) 对于一个引用类型的变量,Java编译器按照它声明的类型来处理。

1
2
Base base = new Sub();
base.subMethod();

编译出错,编译器认为 base 是 Base 类型的引用变量,Base 类没有 subMethod() 方法。必须使用强制转换。

1
(Sub)(base).subMethod();

2) 对于一个引用类型的变量,运行时 JVM 按照它实际引用的对象来处理。

1
2
Base base = new Base();
Sub sub = (Sub)base;

编译通过,但是运行时抛出 ClassCastException。在运行时,子类的引用类型变量可以转换为父类的引用类型,而相反的过程却不可以。

3) 在运行时环境中,通过引用类型变量来访问所引用对象的方法和属性时,JVM 采用以下的绑定规则。

  1. 实例方法与引用变量实际引用的对象的方法绑定,即动态绑定。
  2. 静态方法与引用变量所声明的类型的方法绑定,即静态绑定。 编译阶段即绑定完成。
  3. 成员变量(静态变量和实例变量) 与引用类型所声明的类型的成员变量绑定。静态绑定。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Base {
public String a = "Base";

public static void hello() {
System.out.println("Base.hello");
}

public void say() {
this.hello();
System.out.println("Base.x = " + this.a);
}
}

public class Sub extends Base {
public String a = "Sub";

public static void hello() {
System.out.println("Sub.hello");
}

public void say() {
this.hello();
System.out.println("Sub.x = " + this.a);
}
}

public class Test {
public static void main(String[] args) {
Sub sub = new Sub();

Base base = (Base)sub;

// 覆盖测试:普通方法
sub.say();
base.say();

// 覆盖测试:静态方法
sub.hello();
base.hello();

// 覆盖测试:属性
System.out.println(sub.a);
System.out.println(base.a);
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
Sub.hello
Sub.x = Sub

Sub.hello // 动态绑定中调用的静态方法,也变成动态绑定
Sub.x = Sub // 普通方法,动态绑定

Sub.hello
Base.hello // 静态方法,静态绑定

Sub
Base // 成员变量,静态绑定

Overload(重载)

如果在一个类中定义了多个重载的方法,只需根据所传参数列表的不同来调用不同的重载方法。

Java Hashmap

Posted on 2016-07-17

概述

本文翻译自 JDK 1.8 HashMap 的文档。

术语

英文 中文
Hash 哈希
Key 键
Value 值
Bucket 桶
Capacity 容量
Load Factor 负载因子
Iteration 迭代
Initial Capacity 初始容量
Rehashing 重哈希

HashMap

HashMap 的说明

java.util

public class HashMap

extends java.util.AbstractMap

implements java.util.Map, java.lang.Cloneable, java.io.Serializable

基于哈希表的 Map 接口实现。本实现提供了所有可选的 map 操作,允许 null 值和 null 键。(HashMap 类大致相当于 Hashtable,除了它是非同步的并且允许 null)。这个类不保证 map 的顺序,特别的,它也不保证该顺序随着时间保持不变。

假设哈希函数能使元素正确地分散在桶中,本实现将提供常量时间性能的基本操作(get 和 put)。迭代集合时所需的时间与哈希表的“容量”(桶的数量)加上哈希表的大小(键-值对数量)成比例。因此非常重要的一点是,如果迭代的性能非常重要,那么初始容量不应设得太高(或负载因子太低)。

一个 HashMap 实例有两个参数影响其性能:初始容量和负载因子。容量指的是哈希表中桶的数量,初始容量也就是创建哈希表时的容量。负荷因子是用来衡量哈希表容量自动增长之前允许表中数据有多满。当哈希表中的 Entry 的数量超过负载因子与当前容量的乘积时,哈希表将会被重哈希(也就是内部数据结构会被重建),以使哈希表有大约两倍数量的桶。

作为一般规则,默认的负载因子(.75)提供了一个良好的时间和空间成本之间的权衡。更高的值会减少的空间开销,但会增加查找成本(影响大多数 HashMap 类的操作,包括 get 和 put)。设置初始容量时,应当考虑 map 中预期的 Entry 数量和负载因子。如果初始容量大于最大的 Entry 数量除以负载因子,将不会发生重哈希。

如果需要将非常多的键值对存储在 HashMap 实例中,创建一个足够大容量的哈希表将会使存储过程效率更高,相比于扩大哈希表时自动执行的重哈希。需要注意的是使用许多相同 hashCode() 结果的键必然会减缓哈希表的性能。为了减小影响,当键是 Comparable 时,本类将对键使用比较顺序以帮助减少默认顺序造成的不良影响。

注意,本实现是非同步的。如果多个线程并发访问一个哈希表,并且至少有一个线程改变 map 结构,则必须在外部程序中实现同步。(结构改动是指增加或删除一个或多个映射;仅仅改变表中已有的键对应的值并不算结构改动。)这通常是由某个包含了 map 的对象的同步来实现的。

如果不存在这样的对象,应当使用 Collections.synchronizedMap 方法 “封装” map。最好是在创建时完成,以避免对 map 非同步的访问:

Map m = Collections.synchronizedMap(new HashMap(...));

本类所有的“集合视图方法”返回的迭代器都是 “fail-fast” 的。迭代器创建后,如果 map 发生了结构改动(除了迭代器自己的 remove 方法的其他任何方式),迭代器将抛出 ConcurrentModificationException 异常。因此,面对并发修改,迭代器会干净利落地产生失败,而不用冒着未来不确定的时间发生不确定的行为的风险。

注意,迭代器的 fail-fast 行为是无法保证的,一般来说,发生未同步的并发修改时,任何保证都是不可能的。fail-fast 迭代器将尽量抛出 ConcurrentModificationException。因此,编写一个正确性依赖于这个异常的程序,是完全错误的:fail-fast 行为应该只用来检测 bug。

本类属于 Java Collections Framework。

Since: 1.2

See Also: Object.hashCode(), Collection, Map, TreeMap, Hashtable

Type parameters:

  • <K> - 此 map 中维护的键的类型
  • <V> - 映射中值的类型

Implementation Notes

本实现的一些注意点

本 map 通常作为桶式哈希表,但当桶内的数据太多时,它们将会被转换成 TreeNode 桶,与 java.util.TreeMap 的结构类似。大部分方法会尽量使用普通的桶,但如果可用时,会切换到 TreeNode 方法(只需要简单地通过检查结点的 instanceof 结果)。TreeNode 桶可以像其它结点一样遍历和使用,但是额外支持数量过多时更快地查找。但是,由于正常使用时绝大多数的桶不会数量过多,因此检查树型桶的工作会推迟到表方法时。

树型桶(即桶内的元素都是 TreeNode)主要是按 hashCode 排序,但是在冲突的情况下(两个元素属于同样的 “class C implements Comparable” 类型),元素的 compareTo 方法将会用来排序。(我们谨慎地通过反射来检查泛型类型以验证这种情况,见方法 comparableClassFor)。当键有不同的哈希或者可以排序时,为了在最坏的情况下提供 O(logn)的操作,树型桶带来的复杂性增加是值得的。因此,当 hashCode() 方法的返回值分散较差,或许多键的 hashCode 相同,不管是意外还是恶意的,只要它们是可比较的,性能可以优雅地降级。(如果这种情况没有发生,相比于不采取任何预防措施,我们可能会损失时间或空间。但是唯一已知的这种情况源自较差的用户编程实践,因此这点损失也不会有太大的区别。)

因为 TreeNode 大约是正常结点的两倍大,因此,我们仅会在有足够多的结点时才会使用它们(见 TREEIFY_THRESHOLD)。当它们变得太小时(由于移除或重新调整大小),它们会被转换回普通的桶。在使用分散度很好的 hashCode 时,树型桶很少会被使用。理想的随机 hashCode 条件下,桶中结点的频度符合泊松分布(http://en.wikipedia.org/wiki/Poisson_distribution),在默认的负载因子为 0.75 时,泊松分布参数的平均值为 0.5,虽然由于调整粒度会有一个较大的方差。如果忽略方差,表大小 k 的期望值是 (exp(-0.5) * pow(0.5, k) / factorial(k))。最初几个的值是:

  • 0: 0.60653066
  • 1: 0.30326533
  • 2: 0.07581633
  • 3: 0.01263606
  • 4: 0.00157952
  • 5: 0.00015795
  • 6: 0.00001316
  • 7: 0.00000094
  • 8: 0.00000006
  • 更大: 小于 1/10000000

一般情况下,树型桶的根是它的第一个结点。但是,有时(目前仅当执行 Iterator.remove 时),根可能会在其它地方,但是会在父结点链接时(TreeNode.root() 方法),恢复正常。

当桶中的表被转换成树,或者被分裂,或者被转换回普通的桶时,我们会保持访问和遍历的相对顺序(如 Node.next)不变,以更好地保持局部性,并且在调用 iterator.remove 时稍微简化对分裂和遍历的处理。当在插入时使用比较器时,为了保持总体顺序,我们会比较类和 identityHashCode。

类似“并发编程”的“基于SSA”的编程风格有助于避免复杂的指针操作时的对齐错误。

初始值

类中的一些初始值说明

  • DEFAULT_INITIAL_CAPACITY

1 << 4, 也就是16,默认的初始容量,必须是 2 的幂

  • MAXIMUM_CAPACITY

1 << 30,最大容量

  • DEFAULT_LOAD_FACTOR

0.75,默认的负载因子

  • TREEIFY_THRESHOLD

8,桶中元素个数的阈值,当超过这个值时,桶会被转换成树

  • UNTREEIFY_THRESHOLD

6,桶中元素个数的阈值,当小于这个值时,桶中的树结构会被转换回正常的结构。这个值必须小于 TREEIFY_THRESHOLD,并且最大为 6。

  • MIN_TREEIFY_CAPACITY

64,可以被转换成树的最小的容量。如果容量小于这个值,当桶中元素过多时,表会被扩大,而不是转换成树。这个值至少为 4 * TREEIFY_THRESHOLD。

Principles of Object Oriented Design

Posted on 2016-07-13

面向对象设计的六大原则

1. 问题

软件的复用性和可维护性对于提高软件的开发效率、质量,节约开发成本有着至关重要的作用。

软件大师 Robert C.Martin 认为一个可维护性较低的软件设计,通常由于如下四个原因造成:

  • 过于僵硬(Rigidity)
  • 过于脆弱(Fragility)
  • 复用率低(Immobility)
  • 黏度过高(Viscosity)

软件工程和建模大师 Peter Coad 认为,一个好的系统设计应该具备如下三个性质:

  • 可扩展性(Extensibility)
  • 灵活性(Flexibility)
  • 可插入性(Pluggability)

现实是,在软件开发过程中,我们经常会碰到如下问题:

  1. 架构无法适应业务的发展,架构的生命周期短
  2. 内聚和耦合的标准是什么
  3. 如何提高模块复用性
  4. 如何实现高可扩展性和易维护性

如何解决这些问题呢?在面向对象的设计时,有一些基本原则,遵循这些设计原则可以有效地提高系统的复用性和可维护性。

2. 概述

常用的面向对象设计原则包括6个,这些原则并不是孤立存在的,它们相互依赖,相互补充。

原则 说明 重要性
单一职责原则(Single Responsibility Principle, SRP) 类的职责要单一,不能将太多的职责放在一个类中。 ★★★★☆
里氏代换原则(Liskov Substitution Principle, LSP) 在软件系统中,一个可以接受基类对象的地方必然可以接受一个子类对象。 ★★★★☆
依赖倒置原则(Dependency Inversion Principle, DIP) 要针对抽象层编程,而不要针对具体类编程。 ★★★★★
接口隔离原则(Interface Segregation Principle, ISP) 使用多个专门的接口来取代一个统一的接口。 ★★☆☆☆
迪米特法则(Law of Demeter, LoD) 一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互。 ★★★☆☆
开闭原则(Open-Closed Principle, OCP) 软件实体对扩展开放,对修改关闭 ★★★★★

3. 单一职责原则

定义:一个类只负责一个功能领域中的相应职责。对一个类而言,应该仅有一个引起它变化的原因。

分析:

  • 一个类承担的职责越多,它被复用的可能性越小。

  • 类的职责主要包括两个方面:数据职责和行为职责,数据职责通过其属性来体现,而行为职责通过其方法来体现。

  • 如果一个类承担的职责过多,相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。

  • 单一职责原则是实现高内聚、低耦合的指导原则

  • 简单但又难以运用的原则,需要设计人员发现并分享类的不同职责。

好处:

  • 降低类的复杂性。因为类的职责明确
  • 提高可读性
  • 提高可维护性
  • 降低变更引起的风险

4. 里氏代换原则

定义一:如果对每一个类型为S的对象 o1,都有类型为T的对象 o2,使得以 T 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有变化,那么类型 S 是类型 T 的子类型。

定义二:所有引用基类(父类)的地方必须能透明地使用其子类的对象。

分析:

  • 用子类替换基类,程序将正常运行
  • 里氏代换原则是实现开闭原则的重要方式
  • 由于使用基类对象的地方都可以使用子类对象,因此应尽量使用基类类型来定义对象,而在运行时再用子类对象来替换基类对象。

5. 依赖倒置原则

定义一:高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

定义二:要针对接口编程,不要针对实现编程

分析:

  • 代码依赖于抽象的类,而不要依赖于具体的类
  • 针对接口或抽象类编程,而不是针对具体类编程
  • 依赖倒置原则是面向对象设计的主要手段
  • 常用实现方式是: 在代码中使用抽象类,通过配置文件使用具体类

6. 接口隔离原则

定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

分析

接口隔离原则是指使用多个专门的接口,而不使用单一的总接口。

使用此原则时,需要注意以下问题:

1)接口尽量小,但是要有限度。

如果接口过于粗化,则达不到隔离的原则;如果接口过于细化,则会造成接口过多,设计过于复杂。

2) 一个接口仅仅提供客户端需要的行为(方法),隐藏它不需要的方法。

3)提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

7. 迪米特法则

迪米特法则又称为最少知识原则(Least Knowledge Principle,LKP)。

定义:一个对象应该对其他对象保持最少的了解。

分析

  • 类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大
  • 采用迪米特法则,当一个模块修改时,就会尽量少的影响其它模块,扩展会相对容易
  • 迪米特法则是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度

使用

  • 在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用
  • 在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限;
  • 在类的设计上,只要有可能,一个类型应当设计成不变类;
  • 在对其他类的引用上,一个对象对其他对象的引用应当降到最低。

虽然使用迪米特法则时,由于每个类都减少了不必要的依赖,的确可以降低耦合关系。但是过分地使用迪米特法则,会产生大量的中介和传递类,导致系统复杂度变大。

所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

8. 开闭原则

定义:Software entities like classes, modules and functions should be open for extension but closed for modifications

软件实体应当对扩展开放,对修改关闭。

分析

  • 开闭原则主要含义:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
  • 抽象化是开闭原则的关键。
  • 开闭原则是一个务虚的原则,是面向对象设计中最基础的设计原则,是其它原则的总纲。

绝大部分的设计模式都符合开闭原则,在对每一个模式进行优缺点评价时都会以开闭原则作为一个重要的评价依据,以判断基于该模式设计的系统是否具备良好的灵活性和可扩展性。

9. 总结

六大原则相互依赖,相互补充,指导我们用抽象构建框架,用实现扩展细节。

单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合;开闭原则告诉我们要对扩展开放,对修改关闭。

对这六个原则的遵守并不是是和否的问题,而是多和少的问题。我们需要在实际应用中做一些权衡,根据实际情况灵活运用。

过度使用原则,并不达到预期的效果。一个良好的设计通常是在合理的范围遵守这些原则。

10. Reference

  1. 设计模式六大原则, zhengzhb, 2012/11/2, http://www.uml.org.cn/sjms/201211023.asp

Dive into Java -- (3) JVM Garbage Collection

Posted on 2016-06-30

本文转载自:

  1. Java垃圾回收机制, http://www.cnblogs.com/dolphin0520/p/3783345.html
  2. Java系列笔记(3) - Java 内存区域和GC机制, http://www.cnblogs.com/zhguang/p/3257367.html

以下部分转载自:1. Java垃圾回收机制, http://www.cnblogs.com/dolphin0520/p/3783345.html

一.如何确定某个对象是“垃圾”?

  在这一小节我们先了解一个最基本的问题:如果确定某个对象是“垃圾”?既然垃圾收集器的任务是回收垃圾对象所占的空间供新的对象使用,那么垃圾收集器如何确定某个对象是“垃圾”?—即通过什么方法判断一个对象可以被回收了。

  在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。

  这种方式的特点是实现简单,而且效率较高,但是它无法解决循环引用的问题,因此在Java中并没有采用这种方式(Python采用的是引用计数法)。看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();

object1.object = object2;
object2.object = object1;

object1 = null;
object2 = null;
}
}

class MyObject{
public Object object = null;
}

  最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数都不为0,那么垃圾收集器就永远不会回收它们。

  为了解决这个问题,在Java中采取了 可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

  至于可达性分析法具体是如何操作的我暂时也没有看得很明白,如果有哪位朋友比较清楚的话请不吝指教。

  下面来看个例子:

1
2
3
4
5
6
7
Object aobj = new Object ( ) ;
Object bobj = new Object ( ) ;
Object cobj = new Object ( ) ;
aobj = bobj;
aobj = cobj;
cobj = null;
aobj = null;

  第几行有可能会使得某个对象成为可回收对象?第7行的代码会导致有对象会成为可回收对象。至于为什么留给读者自己思考。

  再看一个例子:

1
2
3
String str = new String("hello");
SoftReference<String> sr = new SoftReference<String>(new String("java"));
WeakReference<String> wr = new WeakReference<String>(new String("world"));

  这三句哪句会使得String对象成为可回收对象?第2句和第3句,第2句在内存不足的情况下会将String对象判定为可回收对象,第3句无论什么情况下String对象都会被判定为可回收对象。

  最后总结一下平常遇到的比较常见的将对象判定为可回收对象的情况:

  1)显示地将某个引用赋值为null或者将已经指向某个对象的引用指向新的对象,比如下面的代码:

1
2
3
4
5
Object obj = new Object();
obj = null;
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = obj2;

  2)局部引用所指向的对象,比如下面这段代码:

1
2
3
4
5
6
7
8
void fun() {

.....
for(int i=0;i<10;i++) {
Object obj = new Object();
System.out.println(obj.getClass());
}
}

  循环每执行完一次,生成的Object对象都会成为可回收的对象。

  3)只有弱引用与其关联的对象,比如:

1
WeakReference<String> wr = new WeakReference<String>(new String("world"));

二.典型的垃圾收集算法

  在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。

  1.Mark-Sweep(标记-清除)算法

  这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:

  从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

  2.Copying(复制)算法

  为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

  这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。

  很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

  3.Mark-Compact(标记-整理)算法

  为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

  4.Generational Collection(分代收集)算法

  分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

  目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

  而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

  注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。



以下转载自:2. Java系列笔记(3) - Java 内存区域和GC机制, http://www.cnblogs.com/zhguang/p/3257367.html

目录

Java垃圾回收概况
Java内存区域
Java对象的访问方式
Java内存分配机制
Java GC机制
垃圾收集器

Java垃圾回收概况

  Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对JVM(Java Virtual Machine)中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,防止出现内存泄露和溢出问题。

关于JVM,需要说明一下的是,目前使用最多的Sun公司的JDK中,自从1999年的JDK1.2开始直至现在仍在广泛使用的JDK6,其中默认的虚拟机都是HotSpot。2009年,Oracle收购Sun,加上之前收购的EBA公司,Oracle拥有3大虚拟机中的两个:JRockit和HotSpot,Oracle也表明了想要整合两大虚拟机的意图,但是目前在新发布的JDK7中,默认的虚拟机仍然是HotSpot,因此本文中默认介绍的虚拟机都是HotSpot,相关机制也主要是指HotSpot的GC机制。

  Java GC机制主要完成3件事:确定哪些内存需要回收,确定什么时候需要执行GC,如何执行GC。经过这么长时间的发展(事实上,在Java语言出现之前,就有GC机制的存在,如Lisp语言),Java GC机制已经日臻完善,几乎可以自动的为我们做绝大多数的事情。然而,如果我们从事较大型的应用软件开发,曾经出现过内存优化的需求,就必定要研究Java GC机制。

  学习Java GC机制,可以帮助我们在日常工作中排查各种内存溢出或泄露问题,解决性能瓶颈,达到更高的并发量,写出更高效的程序。

  我们将从4个方面学习Java GC机制,

  1. 内存是如何分配的
  2. 如何保证内存不被错误回收(即:哪些内存需要回收)
  3. 在什么情况下执行GC以及执行GC的方式
  4. 如何监控和优化GC机制。

Java内存区域

  了解Java GC机制,必须先清楚在JVM中内存区域的划分。在Java运行时的数据区里,由JVM管理的内存区域分为下图几个模块:

Java 运行时内存划分

其中:

1,程序计数器(Program Counter Register)

程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。

  每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。

  如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。

2,虚拟机栈(JVM Stack)

一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。

  局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。

  虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。

  每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。

3,本地方法栈(Native Method Statck)

本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。

  本地方法栈也是线程私有的。

4,堆区(Heap)

堆区是理解Java GC机制最重要的区域,没有之一。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。

  一般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。

  关于堆区的内容还有很多,将在下节“Java内存分配机制”中详细介绍。

5,方法区(Method Area)

在Java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实上,方法区并不是堆(Non-Heap);另外,不少人的博客中,将Java GC的分代收集机制分为3个代:青年代,老年代,永久代,这些作者将方法区定义为“永久代”,这是因为,对于之前的HotSpot Java虚拟机的实现方式中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。不过,除HotSpot之外的多数虚拟机,并不将方法区当做永久代,HotSpot本身,也计划取消永久代。本文中,由于笔者主要使用Oracle JDK6.0,因此仍将使用永久代一词。

  方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。

  方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。一般的,方法区上执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(HotSpot),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。

  在方法区上进行垃圾收集,条件苛刻而且相当困难,效果也不令人满意,所以一般不做太多考虑,可以留作以后进一步深入研究时使用。

  在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。

  运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用

(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译)

  运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。

6,直接内存(Direct Memory) 直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。

Java对象的访问方式

一般来说,一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。

  以最简单的本地变量引用:Object obj = new Object()为例:

  • Object obj表示一个本地引用,存储在JVM栈的本地变量表中,表示一个reference类型数据;
  • new Object()作为实例对象数据存储在堆中;
  • 堆中还记录了Object类的类型信息(接口、方法、field、对象类型等)的地址,这些地址所执行的数据存储在方法区中;

在Java虚拟机规范中,对于通过reference类型引用访问具体对象的方式并未做规定,目前主流的实现方式主要有两种:

1,通过句柄访问(图来自于《深入理解Java虚拟机:JVM高级特效与最佳实现》):

通过句柄访问的实现方式中,JVM堆中会专门有一块区域用来作为句柄池,存储相关句柄所执行的实例数据地址(包括在堆中地址和在方法区中的地址)。这种实现方法由于用句柄表示地址,因此十分稳定。

2,通过直接指针访问:(图来自于《深入理解Java虚拟机:JVM高级特效与最佳实现》)

通过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包含了在方法区中的相应类型数据。这种方法最大的优势是速度快,在HotSpot虚拟机中用的就是这种方式。

Java内存分配机制

这里所说的内存分配,主要指的是在堆上的分配,一般的,对象的内存分配都是在堆上进行,但现代技术也支持将对象拆成标量类型(标量类型即原子类型,表示单个值,可以是基本类型或String等),然后在栈上分配,在栈上分配的很少见,我们这里不考虑。

  Java内存分配和回收的机制概括的说,就是:分代分配,分代回收。对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。如下图(来源于《成为JavaGC专家part I》,http://www.importnew.com/1993.html):

Memory Generation

  年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消亡的),这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。

  年轻代上的内存分配是这样的,年轻代可以分为3个区域:Eden区(伊甸园,亚当和夏娃偷吃禁果生娃娃的地方,用来表示内存首次分配的区域,再贴切不过)和两个存活区(Survivor 0 、Survivor 1)。内存分配过程为(来源于《成为JavaGC专家part I》,http://www.importnew.com/1993.html):

Memory GC by Generation
  1. 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;

  2. 最初一次,当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);

  3. 下次Eden区满了,再执行一次Minor GC,将消亡的对象清理掉,将存活的对象复制到Survivor1中,然后清空Eden区;

  4. 将Survivor0中消亡的对象清理掉,将其中可以晋级的对象晋级到Old区,将存活的对象也复制到Survivor1区,然后清空Survivor0区;

  5. 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代,但这只是个最大值,并不代表一定是这个值)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。

  从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下高效,如果在老年代采用停止复制,则挺悲剧的。

  在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内存。

  年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。     可以使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。

  如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。

用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。

  可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。

Java GC机制

GC机制的基本算法是:分代收集,这个不用赘述。下面阐述每个分代的收集方法。

  年轻代:

  事实上,在上一节,已经介绍了新生代的主要垃圾回收方法,在新生代中,使用“停止-复制”算法进行清理,将新生代内存分为2部分,1部分 Eden区较大,1部分Survivor比较小,并被划分为两个等量的部分。每次进行清理时,将Eden区和一个Survivor中仍然存活的对象拷贝到 另一个Survivor中,然后清理掉Eden和刚才的Survivor。

这里也可以发现,停止复制算法中,用来复制的两部分并不总是相等的(传统的停止复制算法两部分内存相等,但新生代中使用1个大的Eden区和2个小的Survivor区来避免这个问题)

  由于绝大部分的对象都是短命的,甚至存活不到Survivor中,所以,Eden区与Survivor的比例较大,HotSpot默认是 8:1,即分别占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下来的内存超过了10%,则需要将一部分对象分配到 老年代。

用-XX:SurvivorRatio参数来配置Eden区域Survivor区的容量比值,默认是8,代表Eden:Survivor1:Survivor2=8:1:1.

  老年代:

  老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-整理算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。 在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC,否则,就查看是否设置了-XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行Full GC(这代表着如果设置-XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。

  方法区(永久代):

  永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:

  1. 类的所有实例都已经被回收
  2. 加载类的ClassLoader已经被回收
  3. 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)

    永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。

HotSpot提供-Xnoclassgc进行控制

使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看类加载和卸载信息

-verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;

-XX:+TraceClassUnLoading需要fastdebug版HotSpot支持

垃圾收集器

在GC机制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具体实现,Java虚拟机规范中对于垃圾收集器没有任何规定,所以不同厂商实现的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器如下图(图来源于《深入理解Java虚拟机:JVM高级特效与最佳实现》,图中两个收集器之间有连线,说明它们可以配合使用):

HotSpot GC

在介绍垃圾收集器之前,需要明确一点,就是在新生代采用的停止复制算法中,“停 止(Stop-the-world)”的意义是在回收内存时,需要暂停其他所 有线程的执行。这个是很低效的,现在的各种新生代收集器越来越优化这一点,但仍然只是将停止的时间变短,并未彻底取消停止。

  • Serial收集器:新生代收集器,使用停止复制算法,使用一个线程进行GC,串行,其它工作线程暂停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值)

  • ParNew收集器:新生代收集器,使用停止复制算法,Serial收集器的多线程版,用多个线程进行GC,并行,其它工作线程暂停,关注缩短垃圾收集时间。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。

  • Parallel Scavenge 收集器:新生代收集器,使用停止复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适 合用户交互,提高用户体验)。使用-XX:+UseParallelGC开关控制使用Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效),用开关参数-XX:+UseAdaptiveSizePolicy可以进行动态控制,如自动调整Eden/Survivor比例,老年代对象年龄,新生代大小等,这个参数在ParNew下没有。

  • Serial Old收集器:老年代收集器,单线程收集器,串行,使用标记整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停(注意,在老年代中进行标记整理算法清理,也需要暂停其它线程),在JDK1.5之前,Serial Old收集器与ParallelScavenge搭配使用。

  • Parallel Old收集器:老年代收集器,多线程,并行,多线程机制与Parallel Scavenge差不错,使用标记整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。

  • CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS(原因见后面),当用户线程内存不足时,采用备用方案Serial Old收集。

CMS收集的执行过程是:初始标记(CMS-initial-mark) -> 并发标记(CMS-concurrent-mark) –>预清理(CMS-concurrent-preclean)–>可控预清理(CMS-concurrent-abortable-preclean)-> 重新标记(CMS-remark) -> 并发清除(CMS-concurrent-sweep) ->并发重设状态等待下次CMS的触发(CMS-concurrent-reset)

具体的说,先2次标记,1次预清理,1次重新标记,再1次清除。

1,首先jvm根据-XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingOccupancyOnly来决定什么时间开始垃圾收集;

2,如果设置了-XX:+UseCMSInitiatingOccupancyOnly,那么只有当old代占用确实达

3,如果没有设置-XX:+UseCMSInitiatingOccupancyOnly,那么系统会根据统计数据自行决定什么时候触发cms gc;因此有时会遇到设置了80%比例才cms gc,但是50%时就已经触发了,就是因为这个参数没有设置的原因;

4,当cms gc开始时,首先的阶段是初始标记(CMS-initial-mark),是stop the world阶段,因此此阶段标记的对象只是从root集最直接可达的对象; CMS-initial-mark:961330K(1572864K),指标记时,old代的已用空间和总空间

5,下一个阶段是并发标记(CMS-concurrent-mark),此阶段是和应用线程并发执行的,所谓并发收集器指的就是这个,主要作用是标记可达的对象,此阶段不需要用户停顿。 此阶段会打印2条日志:CMS-concurrent-mark-start,CMS-concurrent-mark

6,下一个阶段是CMS-concurrent-preclean,此阶段主要是进行一些预清理,因为标记和应用线程是并发执行的,因此会有些对象的状态在标记后会改变,此阶段正是解决这个问题因为之后的Rescan阶段也会stop the world,为了使暂停的时间尽可能的小,也需要preclean阶段先做一部分工作以节省时间 此阶段会打印2条日志:CMS-concurrent-preclean-start,CMS-concurrent-preclean

7,下一阶段是CMS-concurrent-abortable-preclean阶段,加入此阶段的目的是使cms gc更加可控一些,作用也是执行一些预清理,以减少Rescan阶段造成应用暂停的时间 此阶段涉及几个参数: -XX:CMSMaxAbortablePrecleanTime:当abortable-preclean阶段执行达到这个时间时才会结束 -XX:CMSScheduleRemarkEdenSizeThreshold(默认2m):控制abortable-preclean阶段什么时候开始执行, 即当eden使用达到此值时,才会开始abortable-preclean阶段 -XX:CMSScheduleRemarkEdenPenetratio(默认50%):控制abortable-preclean阶段什么时候结束执行 此阶段会打印一些日志如下: CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean, CMS:abort preclean due to time XXX

8,再下一个阶段是第二个stop the world阶段了,即Rescan阶段,此阶段暂停应用线程,停顿时间比并发标记小得多,但比初始标记稍长。对对象进行重新扫描并标记; YG occupancy:964861K(2403008K),指执行时young代的情况 CMS remark:961330K(1572864K),指执行时old代的情况 此外,还打印出了弱引用处理、类卸载等过程的耗时

9,再下一个阶段是CMS-concurrent-sweep,进行并发的垃圾清理

10,最后是CMS-concurrent-reset,为下一次cms gc重置相关数据结构

有2种情况会触发CMS 的悲观full gc,在悲观full gc时,整个应用会暂停

A,concurrent-mode-failure:预清理阶段可能出现,当cms gc正进行时,此时有新的对象要进行old代,但是old代空间不足造成的。其可能性有:1,O区空间不足以让新生代晋级,2,O区空间用完之前,无法完成对无引用的对象的清理。这表明,当前有大量数据进入内存且无法释放。

B,promotion-failed:新生代young gc可能出现,当进行young gc时,有部分young代对象仍然可用,但是S1或S2放不下,因此需要放到old代,但此时old代空间无法容纳此。

影响cms gc时长及触发的参数是以下2个: -XX:CMSMaxAbortablePrecleanTime=5000 -XX:CMSInitiatingOccupancyFraction=80 解决也是针对这两个参数来的,根本的原因是每次请求消耗的内存量过大 解决方式:

A,针对cms gc的触发阶段,调整-XX:CMSInitiatingOccupancyFraction=50,提早触发cms gc,就可以缓解当old代达到80%,cms gc处理不完,从而造成concurrent mode failure引发full gc

B,修改-XX:CMSMaxAbortablePrecleanTime=500,缩小CMS-concurrent-abortable-preclean阶段的时间

C,考虑到cms gc时不会进行compact,因此加入-XX:+UseCMSCompactAtFullCollection (cms gc后会进行内存的compact)和-XX:CMSFullGCsBeforeCompaction=4(在full gc4次后会进行compact)参数

在CMS清理过程中,只有初始标记和重新标记需要短暂停顿,并发标记和并发清除都不需要暂停用户线程,因此效率很高,很适合高交互的场合。

CMS也有缺点,它需要消耗额外的CPU和内存资源,在CPU和内存资源紧张,CPU较少时,会加重系统负担(CMS默认启动线程数为(CPU数量+3)/4)。

另外,在并发收集过程中,用户线程仍然在运行,仍然产生内存垃圾,所以可能产生“浮动垃圾”,本次无法清理,只能下一次Full GC才清理,因此在GC期间,需要预留足够的内存给用户线程使用。所以使用CMS的收集器并不是老年代满了才触发Full GC,而是在使用了一大半(默认68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction来设置)的时候就要进行Full GC,如果用户线程消耗内存不是特别大,可以适当调高-XX:CMSInitiatingOccupancyFraction以降低GC次数,提高性能,如果预留的用户线程内存不够,则会触发Concurrent Mode Failure,此时,将触发备用方案:使用Serial Old 收集器进行收集,但这样停顿时间就长了,因此-XX:CMSInitiatingOccupancyFraction不宜设的过大。

还有,CMS采用的是标记清除算法,会导致内存碎片的产生,可以使用-XX:+UseCMSCompactAtFullCollection来设置是否在Full GC之后进行碎片整理,用-XX:CMSFullGCsBeforeCompaction来设置在执行多少次不压缩的Full GC之后,来一次带压缩的Full GC。

  • G1收集器:在JDK1.7中正式发布,与现状的新生代、老年代概念有很大不同,目前使用较少,不做介绍。

注意并发(Concurrent)和并行(Parallel)的区别: 并发是指用户线程与GC线程同时执行(不一定是并行,可能交替,但总体上是在同时执行的),不需要停顿用户线程(其实在CMS中用户线程还是需要停顿的,只是非常短,GC线程在另一个CPU上执行); 并行收集是指多个GC线程并行工作,但此时用户线程是暂停的;

所以,Serial是串行的,Parallel收集器是并行的,而CMS收集器是并发的.

关于JVM参数配置和内存调优实例,见我的下一篇博客(编写中:Java系列笔记(4) - JVM监控与调优),本来想写在同一篇博客里的,无奈内容太多,只好另起一篇。

说明:   本文是Java系列笔记的第3篇,这篇文章写了很久,主要是Java内存和GC机制相对复杂,难以理解,加上本人这段时间项目和生活中耗费的时间很多,所以进度缓慢。文中大多数笔记内容来源于我在网络上查到的博客和《深入理解Java虚拟机:JVM高级特效与最佳实现》一书。   本人能力有限,如果有错漏,请留言指正。

参考资料

《JAVA编程思想》,第5章

《Java深度历险》,Java垃圾回收机制与引用类型

《深入理解Java虚拟机:JVM高级特效与最佳实现》,第2-3章

成为JavaGC专家Part II — 如何监控Java垃圾回收机制, http://www.importnew.com/2057.html

JDK5.0垃圾收集优化之–Don’t Pause,http://calvin.iteye.com/blog/91905

【原】java内存区域理解-初步了解,http://iamzhongyong.iteye.com/blog/1333100

关于施用full gc频繁的分析及解决:http://www.07net01.com/zhishi/383213.html

Introduction of .NET

Posted on 2016-06-26

.NET

.NET 简介

.NET 是微软用来实现 XML, Web Services, SOA(面向服务的体系结构 Service-Oriented Architecture)和敏捷性的技术。它是微软的新一代技术平台,为敏捷商务构建互联互通的应用系统,这些系统是基于标准的,联通的,适应变化的,稳定的和高性能的。

从技术的角度看,一个 .NET 应用是一个运行于 .NET Framework 之上的应用程序。(更准确地说,一个 .NET 应用是一个使用 .NET Framework 类库来编写,并运行于公共语言运行时Common Language Runtime 之上的应用程序。)

.NET Framework 是一个多语言组件开发和执行环境,它提供了一个跨语言的统一编程环境。

.NET Framework 是以一种采用系统虚拟机运行的编程平台,以 CLR 为基础,支持多种语言(C#, VB.NET, C++, Python等)的开发库。

.NET 结构框图

图:.NET 结构框图

CLS

Common language Specification(公共语言规范),定义了提供给公共语言基础的接口,例如对于枚举类型的隐含表示类型的协定。

CTS

Common Type System(公共类型系统),类型是 CLR 的基础,CTS 是一个正式的描述类型定义和行为的规范,如类型的字段、属性、事件、方法、作用范围等。

FCL

Framework Class Library(框架类库),它向开发人员提供了大量类型。分为两部分:

1. BCL

Base Class Library(基础类库):它是一组标准函数库,包括:IO,String,Threading等。

2. 其他类

这些类库基本上都是引用 BCL,在这些类库的基础上做的扩充。如:ADO.NET,ASP.NET,Windows.Forms 等。

CIL

Common Intermediate Language(公共中间语言)。

CLR

Common Language Runtime(公共语言运行时),CLR 是 CLI 标准的实现。该运行时用于执行已编译的 .NET 应用程序。

CLI

Common Language Intrastructure(公共语言基础),是一套标准(ECMA335)。CLI 定义了构成 .NET Framework 基础结构的可执行码以及代码的运行时环境的规范,它定义了一个语言无关的跨体系结构的运行环境,这使得开发者可以用规范内定义的各种高级语言来开发软件,并且无需修正即可将软件运行在不同的计算机体系结构上。

CLR

图:CLR 结构图

C# 所具有的许多特点都是由 CLR 提供的, 如类型安全(Type Checker)、垃圾回收(Garbage Collector)、异常处理(Exception Manager)、向下兼容(COM Marshaler)等。

.NET 上的 CLR 为开发者提供如下的服务:

◆ 平台无关:CLR实际上是提供了一项使用了虚拟机技术的产品,他构架在操作系统之上,并不要求程序的运行平台是 Windows 系统,只要是能够支持它的运行库的系统,都可以在上面运行 .NET 应用。所以,一个完全由托管代码组成的应用程序,只要编译一次,就可以在任何支持 .NET 的平台上运行.

◆ 跨语言集成:CLR 允许开发者以任何语言进行开发,用这些语言开发的代码,可以在 CLR 环境下紧密无缝的进行交叉调用,例如,可以用 VB 声明一个基类对象,然后在 C# 代码中直接创建次基类的派生类。

◆ 自动内存管理:CLR 提供了垃圾收集机制,可以自动管理内存。当对象或变量的生命周期结速后,CLR 会自动释放他们所占用的内存.

◆ 版本控制

◆ .NET 安全

◆ 简单的组件互操作性。

◆ 自描述组件

自描述组件是指将所有数据和代码都放在一个文件中的执行文件。自描述组件可以大大简化系统的开发和配置,并且改进系统的可靠性。

CLI

CLI(Common Language Infrastructure,CLI)是 CLR 的一个子集,也就是 .NET 中最终对编译成 MSIL 代码的应用程序的运行环境进行管理的那一部分。

在 CLR 结构图中 CLI 位于下半部分,主要包括类加载器 (Class Loader)、实时编译器 (IL To Native Compilers) 和一个运行时环境的垃圾收集器 (Garbage Collector)。CLI 是 .NET 和 CLR 的核心,CLI 为 IL 代码提供运行的环境,可以将任何语言编写的代码通过对应的编译器转换为 IL 代码运行其上。

根据 C# 与 CLI 这两项标准,你也可以自己写出能够运行于任何操作系统上的 C#/.NET 平台, 如著名的 Mono 项目。

.NET编译运行流程

图:.NET编译运行流程

开发者使用高级编程语言编写程序,由对应的编译器将代码编译成中间语言(IL),运行的时候CLR 通过内置的实时编译器,将 IL 代码转换为操作系统的原生代码(Native Code)运行在操作系统之上。

C#

C# 语言是一种现代、面向对象的语言,它简化了 C++ 语言在类、命名空间、方法重载和异常处理等方面的操作,它摒弃了 C++ 的复杂性,更易使用,更少出错。它使用组件编程,容易使用。

C# 语法和 C++ 和 Java 语法非常相似,如果具有 C++ 和 Java 的基础,学习 C# 语言应该比较轻松。

C# 语言的特点

通用语言规范
自动内存管理
交叉语言处理
增加安全
版本支持
完全面向对象

C# Hello World

1) 编辑文本:HelloWorld.cs

using System;
class HelloWorld
{ 
    static void Main(){
        Console.WirteLine("HelloWorld!");
    }
}

2) 运行命令:

$ C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe HelloWorld.cs

将生成 CLR 可执行文件: HelloWorld.exe

3) 运行程序:HelloWorld.exe

控制台输出:Hello World!

Mono

Mono 简介

Mono 是一个跨平台的、开源的 .NET 实现,由 Xamarin 公司所主持的自由开放源代码项目。该项目的目标是创建一系列符合 ECMA 标准(Ecma-334 和 Ecma-335)的 .NET 工具,包括 C# 编译器和通用语言架构。

Mono项目包括三个核心的部分:

编译器(C#)

负责生成符合公共语言规范的映射代码,即 CIL。

类库

CLR

Mono 的运行时将运行 CIL 代码。

在Java的世界中,这项工作是由 SUN 公司完成的,SUN 针对不同的操作系统开发出相应的Java 虚拟机,以便让一个由 Java 开发的应用程序运行在不同的操作系统上

Mono 使用示例

Console Hello World

1) 编辑 hello.cs

using System;
public class HelloWorld
{
    static public void Main ()
    {
        Console.WriteLine ("Hello Mono World");
    }
}

2) 编译

与使用 Microsoft.NET 不同,要使用 Mono 的编译程序

$ mcs hello.cs

将生成 hello.exe 文件。

3) 运行

使用 mono 运行 hello.exe

$ mono hello.exe

将输出:Hello Mono World

Winforms Hello World

与上面类似,但是会通过 Winforms 库来显示图形界面。

1) 编辑 hello.cs

1
2
3
4
5
6
7
8
using System;
using System.Windows.Forms;
public class HelloWorld : Form{
static public void Main () {
Application.Run (new HelloWorld ()); }
public HelloWorld () {
Text = "Hello Mono World“; }
}

2) 编译

需要添加 dotnet 包。

$ mcs hello.cs -pkg:dotnet  

将生成 hello.exe 文件。

3) 运行

使用 mono 运行 hello.exe

$ mono hello.exe

将输出一个带文字 “Hello Mono World” 的窗口。

进一步了解可以参考:

  1. Install Mono: http://www.mono-project.com/docs/getting-started/install
  2. Mono Reference: http://www.mono-project.com/docs/getting-started/mono-basics
  3. Class Status | Mono vs .NET http://go-mono.com/status

Dive into Java -- (2) JVM Runtime Data Area

Posted on 2016-06-25

概述

不同于 C/C++ 程序,JVM 拥有自动内存管理机制,使得开发人员不需要手动地释放内存。但是对开发人员来说,还是应当了解 JVM 是如何使用内存的。

JVM 执行 Java 程序时,会根据 Java 虚拟机规范,将它所管理的内存分为几个区域,每个区域有各自的用途和功能。这些区域被称为运行时数据区域(Runtime Data Area)。其框图如下:

图:JVM Runtime Data Area

程序计数器(Program Counter Register)

程序计数器用于存储每个线程执行的 JVM 字节指令的位置。其工作方式类似于真实 CPU 中的 PC 寄存器。读取当前指令、分支、跳转等都依赖于程序计数器完成。

每一个线程都有独立的程序计数器,彼此互不干扰。因此,此数据区域是“线程私有”的。

另外,当运行 Native 方法时,则此数据区域中不存储信息。

JVM 栈(JVM Stack)

图:JVM 栈和堆示意图

JVM 栈中用于存放当前线程中的一些数据:

1) 局部变量(基本类型)

Java 中定义的八种基本类型:boolean、char、byte、short、int、long、float、double。

除了 double 型数据占用两个 Slot(局部变量空间)外,其它数据在栈中占用一个 Slot。

2) 对象引用

对象的在堆中的地址或者可以得到对象的句柄。

3) returnAddress类型

指向一条字节码指令的地址。

4) 栈帧(Stack Frame)

栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法从调用到执行完成的过程,对应于一个栈帧在 JVM 栈中入栈到栈的过程。方法调用时,栈帧入栈;方法返回时,栈帧出栈。

与程序计数器类似,JVM 栈也是线程私有的,每个线程创建的同时都会创建各自的 JVM 栈。

此数据区域上定义了两种 Error:StackOverflowError 和 OutOfMemoryError。如果线程请求的栈深度超过 JVM 所允许的最大深度,将抛出 StackOverflowError;当 JVM 栈需要扩展,但无法申请到足够的内存时,将抛出 OutOfMemoryError。

堆(Heap)

堆是 JVM 用来存储对象实例以及数组值的区域,在虚拟机启动时创建,一般情况下,堆是 JVM 所管理的最大的一块内存区域。

Java 中所有通过 new 创建的对象的内存都在堆中分配,堆是 JVM 中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了 new 对象的开销是比较大的。

堆是 GC 工作的主要区域。

Heap中的对象的内存需要等待GC进行回收。由于 GC 基本都采用分代收集算法,所以堆中还可以细分为:新生代(Young)和老年代(Tenured)。还可以细分为:Eden, From, To 空间。

Sun Hotspot JVM 中,为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间 TLAB(Thread Local Allocation Buffer),其大小由 JVM 根据运行的情况计算而得,在 TLAB 上分配对象时不需要加锁,因此 JVM 在给线程的对象分配内存时会尽量的在 TLAB 上分配,在这种情况下 JVM 中分配对象内存的性能比较高,但如果对象过大的话则仍然是直接使用堆空间分配。

当堆无法获取足够的内存时,会抛出 OutOfMemoryError。对于 Hotspot JVM,可以通过参数 -Xms -Xmx 调整堆的大小。

方法区(Method Area)

方法区用于存放已被虚拟机所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为 final 类型的常量、类中的 Field 信息、类中的方法信息。当开发人员在程序中通过 Class 对象中的 getName、isInterface 等方法来获取信息时,这些数据都来自于方法区。

同时方法区域也是全局共享的,在一定的条件下它也会被 GC ,当方法区域需要使用的内存超过其允许的大小时,会抛出 OutOfMemoryError。对于 Hotspot JVM,可以通过 -XX:MaxPermSize 调整方法区大小的上限。

运行时常量池(Runtime Constant Pool)

存放的为类中的固定的常量信息、方法和 Field 的引用信息等,其空间从方法区域中分配。

本地方法栈(Native Method Stacks)

JVM 采用本地方法栈来支持 Native 方法的执行,此区域用于存储每个 Native 方法调用的状态。其与 JVM 栈的功能类似,同样的,也会抛出 StackOverflowError 和 OutOfMemoryError。

Reference

  1. Jvm工作原理学习笔记 - OPEN 开发经验库. http://www.open-open.com/lib/view/open1408453806147.html

  2. 舒の随想日记 » 浅析Java虚拟机结构与机制. http://blog.hesey.net/2011/04/introduction-to-java-virtual-machine.html

Dive into Java -- (1) JVM introduction

Posted on 2016-06-23

June 23, 2016 / Jason Wang

JVM 简介

JVM 简介

JVM 是 Java Virtual Machine(Java虚拟机)的缩写,JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

Java 语言的一个非常重要的特点就是与平台的无关性。而使用 Java 虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后, Java 语言在不同平台上运行时不需要重新编译。 Java 语言使用 Java 虚拟机屏蔽了与具体平台相关的信息,使得 Java 语言编译程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。 Java 虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是 Java 程序能够“一次编译,到处运行”的原因。

JVM 规格

JVM 的设计目标是提供一个基于抽象规格描述的计算机模型,为解释程序开发人员提供很好的灵活性,同时也确保 Java 代码可在符合该规范的任何系统上运行。JVM 对其实现的某些方面给出了具体的定义,特别是对 Java 可执行代码,即字节码(Bytecode)的格式给出了明确的规格。这一规格包括操作码和操作数的语法和数值、标识符的数值表示方式、以及 Java 类文件中的 Java 对象、常量缓冲池在 JVM 的存储映象。这些定义为 JVM 解释器开发人员提供了所需的信息和开发环境。Java 的设计者希望给开发人员以随心所欲使用 Java 的自由。

JVM 定义了控制 Java 代码解释执行和具体实现的五种规格,它们是:

JVM 指令系统
JVM 寄存器
JVM 栈结构
JVM 碎片回收堆
JVM 存储区

因此,只要是符合 JVM 规格的实现,都可以执行 Java 程序。大多情况下,我们使用的是 Sun(2009 年 4 月被 Oracle 收购)JDK 提供的 JVM。但除此之外,还有其它的 JVM 实现,如 Open JDK,BEA 的 JRockit。

JVM 结构

JVM 结构图

JVM 的主要结构如下:

可以看出,JVM 主要由三部分组成:

  1. Class Loader(类装载器:用来装载.class文件)
  2. Execution Engine (执行引擎:执行字节码或者执行本地方法)
  3. Runtime Data Area (运行时数据区:方法区、堆、Java 栈、PC寄存器、本地方法栈)

还可以看出,在运行时数据区中,方法区和堆是所有 Java 线程共享的,而Java栈、本地方法栈、PC寄存器则是线程私有的。

Runtime Data Area部分将另外单独说明。

JVM ClassLoader

Class Loader 分类

a) Bootstrap Class Loader(启动类加载器)

JVM 的根 ClassLoader,JVM 启动时初始化此 ClassLoader,Java的核心类都是由该ClassLoader加载。

在 Sun JDK 的实现中,由此 ClassLoader 完成 $JAVA_HOME 中 jre/lib/rt.jar 中所有 class 文件的加载,这个jar中包含了java规范定义的所有接口以及实现。这个类加载器是由C++实现的,并且在Java语言中无法获得它的引用。

b) Extension Class Loader(扩展类加载器)

负责加载一些扩展功能的jar包。

c) System Class Loader(系统类加载器)

负责加载启动参数中指定的 Classpath 中的 jar 包及目录,通常我们自己写的Java类也是由该ClassLoader 加载。在Sun JDK中,系统类加载器的名字叫 AppClassLoader。

d) User Defined Class Loader(用户自定义类加载器)

User Defined Class Loader 是开发者继承 ClassLoader 抽象类自行实现的ClassLoader,可用于加载非 Classpath 中的 jar 以及目录。由用户自定义类的加载规则,可以手动控制加载过程中的步骤。

ClassLoader 抽象类的关键方法

1) loadClass

负责加载指定名字的类,它接受一个全类名,然后返回一个Class类型的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {  
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

loadClass的过程为:为先从已经加载的类中寻找,如没有则继续从 parent ClassLoader 中寻找,如仍然没找到,则从 System ClassLoader 中寻找,最后再调用 findClass 方法来寻找,如要改变类的加载顺序,则可覆盖此方法

2) findLoadedClass

负责从当前 ClassLoader 实例对象的缓存中寻找已加载的类,调用的为 native 的方法。

3) findClass

直接抛出 ClassNotFoundException,因此需要通过覆盖 loadClass 或此方法来以自定义的方式加载相应的类。

1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

4) findSystemClass

负责从 System ClassLoader 中寻找类,如未找到,则继续从 Bootstrap ClassLoader 中寻找,如仍然为找到,则返回 null 。

5) defineClass

负责将二进制的字节码转换为 Class 对象

6) resolveClass

负责完成 Class 对象的链接,如已链接过,则会直接返回。

类加载过程

完整的JVM 类加载过程的步骤如下:

1) 装载

装载过程负责找到编译好的 .class 字节码并加载至 JVM 中。

JVM 通过 ClassLoader 根据类名和类所在的包名,来完成类的加载。已加载的类使用如下标识:

类名 + 包名 + ClassLoader 实例ID

JVM 的类加载子系统支持运行时动态加载,动态加载的优点有很多,例如可以节省内存空间、灵活地从网络上加载类,动态加载的另一好处是可以通过命名空间的分隔来实现类的隔离,增强了整个系统的安全性。

2) 链接

链接过程负责:校验二进制字节码的格式,初始化装载类中的静态变量以及解析类中调用的接口、类。

完成校验后,JVM 初始化类中的静态变量,并将其值赋为默认值。

最后对类中的所有属性、方法进行验证,以确保其需要调用的属性、方法存在,以及具备应的权限(例如 public、private 域权限等),可能会抛出 NoSuchMethodError、NoSuchFieldError 等错误信息。

3) 初始化

初始化过程即为执行类中的静态初始化代码、构造器代码以及静态属性的初始化,JVM规范严格定义了何时需要对类进行初始化:

a、通过 new 关键字、反射、clone、反序列化机制实例化对象时。

b、调用类的静态方法时。

c、使用类的静态字段或对其赋值时。

d、通过反射调用类的方法时。

e、初始化该类的子类时(初始化子类前其父类必须已经被初始化)。

f、JVM启动时被标记为启动类的类(简单理解为具有main方法的类)。

JVM Execution Engine

指令执行

在执行方法时 JVM 提供了四种指令来执行:

  1. invokestatic:调用类的static方法

  2. invokevirtual:调用对象实例的方法

  3. invokeinterface:将属性定义为接口来进行调用

  4. invokespecial:JVM 对于初始化对象(Java 构造器的方法为:)以及调用对象实例中的私有方法时。

主要执行技术

由于 JVM 执行的是平台无关的字节指令,因此需要先将字节指令转换为特定的处理器硬件平台对应的指令代码才能在平台上运行。

1) 解释模式

由 interpreter 将每个 Java 指令都转译成对等的微处理器指令,并根据转译后的指令先后次序依序执行,由于一个 Java 指令可能被转译成十几或数十几个对等的微处理器指令,这种模式执行的速度会比较慢。

2) JIT 模式

Just In Time(即时编译)。

当 Java 执行 runtime 环境时,每遇到一个新的类,JIT 编译器在此时就会针对这个类进行编译(compile)作业。经过编译后的程序,被优化成更为精简的原生指令码(native code),因此执行速度更快。以少许的编译时间来节省稍后相当长的执行时间。虽然 JIT 可以提高效率,但是并未达到最佳性能,因为某些极少执行到的 Java 指令在编译时所额外花费的时间可能比转译器在执行时的时间还长。

3) 自适应优化

使用动态编译器仅针对较常被执行的程序码进行编译,其余部分仍使用解释模式来执行。动态编译器通过某种机制,对每个类进行分析,然后决定使用哪种方式。动态编译器针对程序的特性或者是让程序执行几个循环,再根据结果决定是否编译这段程序码。动态编译器会根据“历史数据”做决策,所以程序执行的时间愈长,判断正确的机率就愈高。以整个结果来看,动态编译器的执行速度超越以前的 JIT 技术,平均速度可提高至50%。

(Sun 的 HotspotJVM 采用这种技术)

JVM 生命周期

一个 JVM 实例对应一个独立运行的 Java 程序, 它属于进程级别。

而 JVM 执行引擎(Execution Engine)实例则是线程级别的。

JVM 的生命周期如下:

1) 启动

启动一个 Java 程序时,一个 JVM 实例就产生了,任何一个拥有 public static void main(String[] args) 函数的 class 都可以作为 JVM 实例运行的起点。

b) 运行

main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM 内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由 JVM 自己使用,Java 程序也可以标明自己创建的线程是守护线程。

c) 终止

当程序中的所有非守护线程都终止时,JVM 才退出;若安全管理器允许,程序也可以使用 Runtime 类或者 System.exit() 来退出。

Reference

  1. Jvm工作原理学习笔记 - OPEN 开发经验库. http://www.open-open.com/lib/view/open1408453806147.html

  2. 舒の随想日记 » 浅析Java虚拟机结构与机制. http://blog.hesey.net/2011/04/introduction-to-java-virtual-machine.html

Configure Maven

Posted on 2016-05-29

May 29, 2016 / Jason Wang

配置 Maven

1 Maven 配置文件

默认情况下, Maven 的配置文件为配置目录下的 settings.xml 文件。

Linux -– /etc/maven2/settings.xml
Windows –- {maven}\conf\settings.xml

可以通过修改此文件来配置 Maven 。

通常需要修改如下默认配置:

1) 本地仓库 (Local Repository)

Maven的本地仓库用来存储所有项目的依赖关系。第三方 jar 文件和其他文件会被 Maven下载到本地仓库。 当建立一个 Maven 项目时,Maven 会将所有相关文件和依赖库下载存储在本地仓库中。

默认情况下,Maven 的本地仓库为 .m2 文件夹:

Linx -- ~/.m2/
Windows -- C:\Documents and Settings\{your-username}\.m2\

2) 远程镜像 (Mirror)

当遇到无法下载依赖库的情况时(被墙或速度慢),可以通过添加镜像来解决。

2 配置本地仓库

如果不想把仓库放在默认的系统盘中,可以修改 localRepository 配置。

在 settings.xml 文件中找到 配置段:

<!-- localRepository
| The path to the local repository maven will use to store artifacts.
|
| Default: ~/.m2/repository
<localRepository>/path/to/local/repo</localRepository>
-->

去掉注释,将 /path/to/local/repo 修改为想要的目录即可。

3 添加远程镜像

在 settings.xml 文件中找到 配置段:

<mirrors>
  <mirror>
    <id>mirrorId</id>
    <mirrorOf>repositoryId</mirrorOf>
    <name>Human Readable Name for this Mirror.</name>
    <url>http://my.repository.com/repo/path</url>
  </mirror>
</mirrors>

此配置是样板配置,在此段注释下按格式添加 Mirror 配置。

常用的镜像如下:

国内镜像

oschina.net

<mirror>
  <id>nexus-osc</id>
  <mirrorOf>*</mirrorOf>
  <name>Nexus osc</name>
  <url>http://maven.oschina.net/content/groups/public/</url>
</mirror>

net.cn

<mirror>
    <id>net-cn</id>
    <mirrorOf>central</mirrorOf>
    <name>Human Readable Name for this Mirror.</name>
    <url>http://maven.net.cn/content/groups/public/</url>   
</mirror>

国外镜像

ibiblio.org

<mirror>  
  <id>ibiblio</id>  
  <mirrorOf>central</mirrorOf>  
  <name>ibiblio Mirror of http://repo1.maven.org/maven2/</name>  
  <url>http://mirrors.ibiblio.org/pub/mirrors/maven2/</url>  
</mirror>  

jboss

<mirror>  
  <id>jboss-public-repository-group</id>  
  <mirrorOf>central</mirrorOf>  
  <name>JBoss Public Repository Group</name>  
  <url>http://repository.jboss.org/nexus/content/groups/public</url>  
</mirror>

repo2

<mirror>
  <id>repo2</id>
  <mirrorOf>central</mirrorOf>
  <name>Human Readable Name for this Mirror.</name>
  <url>http://repo2.maven.org/maven2/</url>
</mirror>

uk.maven.org

<mirror>
  <id>ui</id>
  <mirrorOf>central</mirrorOf>
  <name>Human Readable Name for this Mirror.</name>
  <url>http://uk.maven.org/maven2/</url>
</mirror>

4 安装本地 jar 包

如果已经下载好的 jar 包,可以通过如下命令安装到 Maven 的本地仓库

1
mvn install:install-file -DgroupId=<groupId> -DartifactId=<artifactId> -Dversion=<version> -Dpackaging=jar -Dfile=/path/to/jar/file

参数说明:

-DgroupId= 包的 group Id

-DartifactId= 包的 artifact Id

-Dversion= 包的版本号

例如,安装已下载好的 myBatis 包:

1
mvn install:install-file -DgroupId=org.mybatis -DartifactId=mybatis -Dversion=3.4.0 -Dpackaging=jar -Dfile=mybatis-3.4.0.jar

Programmer Competency Matrix

Posted on 2016-03-26

March 26, 2016 / Jason Wang

此文翻译自 Sijin Joseph的“Programmer Competency Matrix”

程序员能力矩阵

Programmer Competency Matrix

注意每个层次的知识是累积的。层次 n 意味着你也掌握了低于层次 n 的知识

Computer Science
(计算机科学)
Domain 2n (Level 0) n2 (Level 1) n (Level 2) log(n) (Level 3) Comments
(注释)
data structures
(数据结构)
不知道数组和链表的区别 在编程实践中,能够解释并使用数组、链表、字典等 知道基本数据结构的空间和时间的权衡,比如数组与链表的对比
能够解释如何实现哈希表,并能处理冲突
知道优先级队列及其实现
知道高级数据结构,如:B-tree、二项堆和斐波那契堆、AVL树、红黑树、伸展树、跳转表、字典树等
algorithms
(算法)
不会找出数组元素的平均值 基本的排序、搜索、数据结构遍历和检索算法 树、图、简单贪婪和分治算法
能够理解此能力矩阵每层的相关性
能够识别并编写动态规划程序
非常了解图相关的算法
非常了解数值计算算法
能够识别 NP 问题等
Working with someone who has a good topcoder ranking would be an unbelievable piece of luck!
(与高等级的程序员一起工作是一种难以置信的幸运)
systems programming
(系统编程)
不知道什么是编译器、链接器、解释器 基本理解编译器、链接器和解释器
知道什么是汇编代码,知道硬件层是如何工作的
知道一些虚拟内存和分页的知识
理解内核模式和用户模式
理解多线程,同步原语以及它们是如何实现的
能够阅读汇编代码
理解网络是如何工作的
理解网络协议和socket编程
理解整个编程知识栈
理解硬件(CPU + 内存 + 中断 + 微代码)
理解二进制代码、汇编、静态和动态链接、编译、解释、JIT编译、垃圾收集、堆、栈、内存寻址
Software Engineering
(软件工程)
2n(Level 0) n2(Level 1) n (Level 2) log(n) (Level 3) Comments
source code version control
(代码版本控制)
按日期备份文件夹 VSS,初级 CVS/SVN 用户 熟练使用CVS、SVN特性
知道如何建立分支和合并
知道如何使用patch等
分布式VCS系统的知识。
使用过Bzr/Mercurial/Darcs/Git
build automation
(构建自动化)
只知道如何通过 IDE 构建 知道如何通过命令行构建系统 知道如何通过配置脚本来构建基本系统 能够配置脚本来构建系统,生成文档、安装包、Release Note
会为代码打tag
automated testing
(自动化测试)
认为测试是tester的事 写过自动化单元测试的代码
能为所写的代码提出好的单元测试用例
以TDD(Test-Driven Development)的方式写过代码 理解并能够建立自动化的 函数、负载/性能和 UI 测试
Programming
(编程)
2n (Level 0) n2 (Level 1) n (Level 2) log(n) (Level 3) Comments
problem decomposition
(问题分解)
只会一行一行地码字,通过“复制粘贴”的方式重用代码 能够将问题分解为多个函数 能够想出可复用的函数/对象来解决整个问题 能使用合适的数据结构和算法,并能写出通用的、面向对象的代码,用以封装问题中需要变化的部分
systems decomposition
(系统分解)
无法想象比单个文件/类更高的层次 能够为同一平台、技术的问题设计解决方案 能够设计跨技术、跨平台系统 能够理解和设计带有多个产品线,并需要集成外部系统的复杂系统。
同时能够设计运行支持系统,如系统监控、报告、故障恢复等。
communication
(沟通)
不能表达出想法。糟糕的拼写和语法 同事能了解你在说什么。好的拼写和语法 能与同事有效沟通 能够以明确的方式理解和交流思想/设计/创意/规格,能够根据场景调整交流方式 这是一个往往被低估,却是一个判断程序员非常关键的标准。随着非英语地区外包的增加,这个问题变得更加突出。我知道的一些项目由于程序员不能理解意图而导致失败
code organization within a file
(文件内代码组织)
没有任何组织依据 方法按逻辑或可访问性分组 代码按区域分组,并有良好的注释,包含对其他源文件的引用 文件有License头、摘要,良好的注释,以及一致的空白使用。
文件应当规整。
2n (Level 0) n2 (Level 1) n (Level 2) log(n) (Level 3) Comments
code organization across files
(跨文件代码组织)
没想过跨文件组织代码 相关的文件归入同一个文件夹 每个物理文件只有一个单一目的,如单个类定义、单一特性的实现等 物理层面的代码组织与设计相匹配。通过文件名和目录结构可以看出设计
source tree organization
(源代码树组织)
所有代码全在一个文件夹里 只有一些按逻辑区分的代码分隔 没有循环依赖。
二进制文件、库、文档、生成文件、第三方代码被组织在合适的文件夹中。
源代码物理上与逻辑架构相匹配。
通过目录名和目录组织可以看出系统设计
The difference between this and the previous item is in the scale of organization, source tree organization relates to the entire set of artifacts that define the system.
code readability
(代码可读性)
单音节名字 对文件、变量、方法有好的命名 没有长函数,通过注释说明:不常见的代码、bug修复和代码假设 使用断言验证代码假设,自然的代码流没有深度嵌套的条件和方法
defensive coding
(保护性编码)
不知道这个概念 检查所有参数,并对关键的代码假设启用断言 确保检查返回值和由可能失败的代码引起的异常 有自己的库来帮助编写保护性编程,编写单元测试来模拟故障
2n (Level 0) n2 (Level 1) n (Level 2) log(n) (Level 3) Comments
error handling
(错误处理)
只编写愉快的case 对可能抛出异常或生成错误的代码进行基本的错误处理 确保正确地退出错误或异常,退出时正确地清理Resources(资源)、Connection(连接)和内存 编码时检测可能出现的异常,在代码的所有层面,维持一致的异常处理策略,提出整个系统的错误处理准则。
IDE 大部分用来编辑代码 了解IDE界面的功能,能够高效地通过菜单使用IDE 了解常用操作的快捷方式 写过自定义宏
API 需要经常查看文档 记住最常用的API 广泛而深度地了解API 编写过基于已有API的库,来简化常用任务或填补API的空白 E.g. of API can be Java library, .net framework or the custom API for the application
frameworks
(框架)
没有用过核心平台以外的任何框架 听过但没有用过平台上可用的常用框架 精通并使用过不止一个框架,熟悉框架的设计理念 框架作者
2n (Level 0) n2 (Level 1) n (Level 2) log(n) (Level 3) Comments
requirements
(需求)
按需求和规格编码 对规格中的遗漏提出疑问 理解完整的图景,并提出应当提供spec的所有范围 根据经验,针对所给的需求提出更好的可选方案和流程
scripting
(脚本化)
不具备脚本化工具的知识 批处理文件/shell脚本 Perl/Python/Ruby/VBScript/Powershell 写过并且发表过可重用的代码
database
(数据库)
认为Excel就是数据库 知道数据库的基本概念:规范化、ACID(原子性Atomicity、一致性Consistency、隔离性Isolation、持久性Durability)、事务化。
能写简单的select语句。
能够设计良好的、规范化的数据库模式。
精通用户视图,存储过程,触发器和用户定义类型。
知道聚集与非聚集索引之间的差异。
精通ORM(Object Relational Mapping对象关系映射)工具的使用。
能做基本的数据库管理,性能优化,索引优化,编写高级的select查询。
能够使用相关sql来替换游标。
理解数据是如何存储在内部的。
了解如何镜像、复制数据库。
了解“两阶段提交”是如何工作的。
Experience
(经验)
2n (Level 0) n2 (Level 1) n (Level 2) log(n) (Level 3) Comments
languages with professional experience
(专业语言经验)
命令式和面向对象式 命令式,面向对象式和声明式(SQL)。
如果了解静态类型 vs 动态类型,弱类型 vs 强类型以及静态推断类型则可加分。
函数式,如果了解 lazy evaluation(惰性求值),currying, continuations则可加分 并行式 (Erlang, Oz) 和逻辑型 (Prolog)
platforms with professional experience
(专业平台经验)
1 2-3 4-5 6+
years of professional experience
(专业经验年限)
1 2-5 6-9 10+
domain knowledge
(领域知识)
没有该领域的知识 在该领域中至少为一个产品工作过 在同一领域中为多个产品工作过 领域专家。
在该领域设计和实现数种产品/方案。
精通该领域使用的标准条款和协议。
Knowledge
(知识)
2n (Level 0) n2 (Level 1) n (Level 2) log(n) (Level 3) Comments
tool knowledge
(工具知识)
仅限于主要IDE (VS.Net, Eclipse等) 知道一些流行和标准的工具的可选工具 对编辑器、调试器、IDE、开源的备选工具有很好的了解。
使用过ORM工具。
实际地编写过工具和脚本,如果发布过这个工具,则有加分
languages exposed to
(语言使用)
命令式和面向对象式 命令式,面向对象式和声明式(SQL)。
如果了解静态类型 vs 动态类型,弱类型 vs 强类型以及静态推断类型则可加分。
函数式,如果了解 lazy evaluation(惰性求值),currying, continuations则可加分 并行式 (Erlang, Oz) 和逻辑型 (Prolog)
codebase knowledge
(代码库知识)
从来没有了解过代码库 基本的代码层知识,了解如何构建系统 良好的代码库工作知识,修复过一些 bug,或者完成过一些小feature 实现过代码库中的多个大的feature。
能够很容易地将大部分feature或bug的变化具像化。
knowledge of upcoming technologies
(新技术知识)
从未听说过新技术 听说过领域内的新技术 下载过预览版、CTP、beta版,并读取一些文章或手册 使用过预览版,而且使用它生成过一些东西,如果共享给其他人的话则有加分
2n (Level 0) n2 (Level 1) n (Level 2) log(n) (Level 3) Comments
platform internals
(平台内部)
对平台内部一无所知 了解平台内部工作的基本知识 深入了解平台内部,并能够理解平台如何将程序转换成可执行代码。 编写过增强平台或者为平台内部提供信息的工具。比如,反汇编器,反编译器,调试器等。
books
(书)
Unleashed series, 21 days series, 24 hour series, dummies series… Code Complete, Don’t Make me Think, Mastering Regular Expressions Design Patterns, Peopleware, Programming Pearls, Algorithm Design Manual, Pragmatic Programmer, Mythical Man month Structure and Interpretation of Computer Programs, Concepts Techniques, Models of Computer Programming, Art of Computer Programming, Database systems , by C. J Date, Thinking Forth, Little Schemer
blogs
(博客)
听说过,但从来都没有时间 阅读科技、编程、软件工程的博客,并且经常收听播客 维护一个博客,包含收集博主收藏的一些有用的文章和工具的链接 维护一个博客,包含关于编程的一些个人见解和思考

Reference:

[1]. [译文]程序员能力矩阵 Programmer, http://blog.sina.com.cn/s/blog_53c05cad0100ha6m.html

12
Jason Wang

Jason Wang

Play Fun

20 posts
30 tags
© 2016 Jason Wang
Powered by Hexo
Theme - NexT.Pisces