十一月 的个人资料NOVEMBRE日志列表留言簿更多 工具 帮助

日志


6月3日

Java类文件

 一、什么是Java类文件
   Java类文件是Java程序的二进制表示形式。每一个类文件代表一个类或者接口。不可能在一个类文件中放入多个类或者接口。这样就使得无论类文件是在哪一种平台上生成,都可以在任何主机上执行。
   虽然类文件是Java体系结构的一部分,但是他并不是与Java语言不可分的。你可以将其他语言的程序编译为类文件,也可以将Java程序文件编译为其他二进制形式。
  Java类文件是一个基于8-bit字节的二进制流。数据块顺序的、无分割符的、big-endian的形式存储。
  二、类文件的内容
   Java的类文件中包含了所有Java虚拟机所需要的关于类和接口的信息。所有类文件中的信息都以以下的四种基本类型的存储:
   Table 6-1. Class file "primitive types"
   u1 a single unsigned byte
   u2 two unsigned bytes
   u4 four unsigned bytes
   u8 eight unsigned bytes
   类文件中的主要部分以表6-2的顺序存储:
   Table 6-2. Format of a ClassFile Table
   Type& #9;Name Count
   u4 magic 1
   u2 minor_version 1
   u2 major_version 1
   u2 constant_pool_count 1
   cp_info constant_pool constant_pool_count-1
   u2 access_flags 1
   u2 this_class 1
   u2 super_class 1
   u2 interfaces_count 1
   u2 interfaces interfaces_count
   u2 fields_count 1
   field_info fields fields_count
   u2 methods_count 1
   method_info methods methods_count
   u2 attributes_count 1
   attribute_info attributes attributes_count
   1、魔术编码(magic)
   每一个Java类文件的开头四个字节都是魔术编码(OxCAFEBABE)。通过魔术编码可以很容易识别类文件。
   2、副版本号和主版本号(minor_version and major_version)
   剩下的四个字节是副版本号和主版本号。但Java技术在进化时,一些新的特性可能会被加入到类文件中。每一次类文件格式的变化,都会相应的改变版本号。虚 拟机通过版本号来识别自己能够处理的类文件。Java虚拟机往往只能处理一个给定的主版本号和其下的一些副版本号。虚拟机必须拒绝那些不再处理范围内的类 文件。
   3、常量个数和常量池(constant_pool_count and constant_pool)
   接下来的就是常量池,常量池中包含了哪些被类或者接口访问过的常量,比如:字符串,常量(final variable values),类名,方法名。常量池作为一个列表存储。列表中常量的个数就是之前保存的“constant_pool_count”。
   很多常量池中的常量都引用了常量池中的其他常量,那些引用常量池常量的引用最终也会转换为对常量池中常量的直接引用。虽然常量列表中的索引是从1开始的,但是常量个数还是包含了0,比如一个常量列表中有15个常量,那么它的常量个数就为16。
   每一个常量开头都会有一个标志,以表示他的类型。当虚拟机读取这个标志时,就会知道这个常量的具体类型了。表6-3列举了这些标志:
   Table 6-3. Constant pool tags
   Entry Type Tag Value Description
   CONSTANT_Utf8 1 A UTF-8 encoded Unicode string
   CONSTANT_Integer 3 An int literal value
   CONSTANT_Float 4 A float literal value
   CONSTANT_Long 5 A long literal value
   CONSTANT_Double 6 A double literal value
   CONSTANT_Class 7 A symbolic reference to a class or interface
   CONSTANT_String 8 A String literal value
   CONSTANT_Fieldref 9 A symbolic reference to a field
   CONSTANT_Methodref 10 A symbolic reference to a method declared in a class
   CONSTANT_InterfaceMethodref 11 A symbolic reference to a method declared in an interface
   CONSTANT_NameAndType 12 Part of a symbolic reference to a field or method
   表6-3中的每一个标志都会有一个相应的表格,用来描述这个标志的所表示的一些详细信息,这些对应的标志都会以标志名+_INFO来结尾。比如CONSTANT_CLASS标志对应的就是CONSTANT_CLASS_INFO。
   常量池在程序的动态链接中扮演了很重要的角色。除了上边所说的各种常量值以外,常量池中包含了一下三种符号引用:类和接口的全名,字段名和描述符,方法名 和描述符。一个字段是一个类或者接口中的实例或者类变量,字段描述符是字段的类型。方法的描述符是方法和返回值和参数的个数、顺序和类型。在虚拟机将这个 类或者接口链接到其他类或者接口时用到这些全名。因为类文件不包含任何关于内存结构的信息,所以这个链接只能以符号引用的形式存在。虚拟机在执行时将这些 符号引用转换为实际的地址。具体的信息参见第八章“The Linking Model”。
   4、访问标志(access_flags)
   紧接在常量池后面的两个字节就是访问标志,表示这个类或者接口的几方面信息,他有如下几种值:
   Table 6-4. Flag bits in the access_flags item of ClassFile tables
   Flag Name Value Meaning if Set Set By
   ACC_PUBLIC 0x0001 Type is public Classes and interfaces
   ACC_FINAL 0x0010 Class is final Classes only
   ACC_SUPER 0x0020 Use new invokespecial semanticsClasses and interfaces
   ACC_INTERFACE 0x0200 Type is an interface, not a class All interfaces, no classes
   ACC_ABSTRACT 0x0400 Type is abstract All interfaces, some classes
   ACC_SUPER标志是为了兼容以前SUN的老式的编译器。所有没有使用的访问标志,必须设置为0。
   5、类名(this_class)
   接下来的两个字节保存了一个常量池的索引。这个常量池中的实体必须是CONSTANT_CLSS_INTO类型的,他包含了标志和名字索引。标志就是 CONSTATN_CLASS,那个名字索引应该是一个保存了这个类或者接口全名的CONSTANT_UTF8_INFO类型的索引。
   6、父类(super_class)
   this_class之后的就是两个字节的super_class,他也是一个常量池的索引,其中保存了父类的全名,处理this_class一样。当父 类是java.lang.Object时,super_class都应该是0。对于接口super_class都是0。
   7、(interfaces_count and interfaces)
   interfaces_count中保存了父接口的个数,interfaces中以数组形式保存了一些常量池的索引。每一个索引都指向了一个 CONSTANT_CLASS_INFO的常量,其中保存了每一个父接口的全名。这个数组的顺序就是父接口出现在在implements、extends 语句中从左到右的顺序。
   8、(fields_count and fields)
   字段被保存在一个field_info的列表中,fields_count是这个列表的长度。Field_info列表中保存的只是类或者接口中的申明的变量,从父类或者父接口中继承的字段不保存在这里。
  每一个field_info表中的一条都描述了一个字段的信息,包括:字段名,描述符,访问权限。如果一个字段被申明为final,那么这个字段的信息即会保存在field_info表中,也会保存在常量池中。
   9、(methods_count and methods)
   方法的信息都保存在method_info表中,mehtods_count是表的长度。Method_info表中只保存类或者接口中申明的方法,不保存从父类或者接口中继承的方法。
  Method_info 表中保存了方法名和描述符(返回值和参数类型)。如果不是抽象方法,还会保存用于堆栈的大小(保存本地变量用的)、操作数堆栈的最大值、捕捉的异常列表、 方法的字节码、可选的行号和本地变量表。如果方法抛出一些被检查的异常,method_info还会包含一个被检查异常的列表。
   10、(attributes_count and attributes)
   类文件中最后的就是属性个数和atribute_info列表。Atribute_info表中保存了一些指向常量池中constant_utf8_info的索引,其中保存了属性的名字。Java虚拟机规范中定义了两种类型的属性:源代码和内部类。
  三、特定字符串(Special Strings)
   常量池中的字符引用包含三种特定字符串:全限定名,简单名,描述符。一个类或者接口的所有字符引用都必须包含一个全限定名。每一个字段或者方法都有一个简 单名和描述符作为全限定名的补充。这些特定字符串用来表示文件中定义的类和接口,包换类名,父类名,父接口名,每个字段和方法的简单名和描述符。
   1、全名(Fully Qualified Names)
   当常量池中引用了类和接口时,就会提供这个类或者接口的权限定名,比如java.lang.Object。
   2、简单名(Simple Names)
   字段和方法都以简单名的形式保存在常量池中。比如常量池中有一个java.lang.Object类的String toString()方法的引用,就会保存“toString”;java.lang.System类的java.io.PrintStream out字段,被保存为“out”。
   3、描述符(Descripters)
   字段和方法的字符引用都会包含一个描述符。字段的描述符提供了字段的类型。方法的描述符提供了方法的返回值、参数个数、参数类型。所有描述符得类型列表:
   FieldDescriptor:
   FieldType
   ComponentType:
   FieldType
   FieldType:
   BaseType
   ObjectType
   ArrayType
   BaseType:
   Terminal Type
   B byte
   C char
   D double
   F float
   I int
   J long
   S short
   Z boolean
   ObjectType:
   L<classname>;
   ArrayType:
   [ ComponentType
   ParameterDescriptor:
   FieldType
   MethodDescriptor:
   ( ParameterDescriptor* ) ReturnDescriptor
   ReturnDescriptor:
   FieldType
   V
   Table 6-6. Examples of field descriptors
   Descriptor Field Declaration
   I int i;
   [[J long[][] windingRoad;
   [Ljava/lang/Object; java.lang.Object[] stuff;
   Ljava/util/Hashtable; java.util.Hashtable ht;
   [[[Z boolean[][][] isReady;
   Table 6-7. Examples of method descriptors
   Descriptor Method Declaration
   ()I int getSize();
   ()Ljava/lang/String; String toString();
   ([Ljava/lang/String;)V void main(String[] args);
   ()V void wait()
   (JI)V void wait(long timeout, int nanos)
   (ZILjava/lang/String;II)Z boolean regionMatches(boolean ignoreCase, int toOffset, String other, int ooffset, int len);
   ([BII)I int read(byte[] b, int off, int len);
  四、常量池
   常量池被组织成为一个cp_info类型的列表,表6-8显示了cp_info表的结构
   Table 6-8. General form of a cp_info table
   Type Name Count
   u1 tag 1
   u1 info depends on tag value
   1、The CONSTANT_Utf8_info Table
   2、The CONSTANT_Integer_info Table
   3、The CONSTANT_Float_info Table
   4、The CONSTANT_Long_info Table
   5、The CONSTANt_Double_info Table
   6、The CONSTANT_Class_info Table
   7、The CONSTANT_String_info Table
   8、The CONSTANT_Fieldref_info Table
   9、The CONSTANT_Methodref_info Table
   10、The COMSTANT_InterfaceMethodref_info Table
   11、The CONSTANT_NameAndType_info Table
  五、Fields
  六、Methods
  七、Attributes

一、什么是Java类文件 Java类文件是Java程序的二进制表示形式。每一个类文件代表一个类或者接口。不可能在一个类文件中放入多个类或者接口。这样就使得无论类文件是在哪一种平台上生成,都可以在任何主机上执行。 虽然类文件是Java体系结构的一部分,但是他并不是与Java语言不可分的。你可以将其他语言的程序编译为类文件,也可以将Java程序文件编译为其他二进制形式。 Java类文件是一个基于8-bit字节的二进制流。数据块顺序的、无分割符的、big-endian的形式存储。二、类文件的内容 Java的类文件中包含了所有Java虚拟机所需要的关于类和接口的信息。所有类文件中的信息都以以下的四种基本类型的存储: Table 6-1. Class file "primitive types" u1 a single unsigned byte u2 two unsigned bytes u4 four unsigned bytes u8 eight unsigned bytes 类文件中的主要部分以表6-2的顺序存储: Table 6-2. Format of a ClassFile Table Type& #9;Name Count u4 magic 1 u2 minor_version 1 u2 major_version 1 u2 constant_pool_count 1 cp_info constant_pool constant_pool_count-1 u2 access_flags 1 u2 this_class 1 u2 super_class 1 u2 interfaces_count 1 u2 interfaces interfaces_count u2 fields_count 1 field_info fields fields_count u2 methods_count 1 method_info methods methods_count u2 attributes_count 1 attribute_info attributes attributes_count 1、魔术编码(magic) 每一个Java类文件的开头四个字节都是魔术编码(OxCAFEBABE)。通过魔术编码可以很容易识别类文件。 2、副版本号和主版本号(minor_version and major_version) 剩下的四个字节是副版本号和主版本号。但Java技术在进化时,一些新的特性可能会被加入到类文件中。每一次类文件格式的变化,都会相应的改变版本号。虚 拟机通过版本号来识别自己能够处理的类文件。Java虚拟机往往只能处理一个给定的主版本号和其下的一些副版本号。虚拟机必须拒绝那些不再处理范围内的类 文件。 3、常量个数和常量池(constant_pool_count and constant_pool) 接下来的就是常量池,常量池中包含了哪些被类或者接口访问过的常量,比如:字符串,常量(final variable values),类名,方法名。常量池作为一个列表存储。列表中常量的个数就是之前保存的“constant_pool_count”。 很多常量池中的常量都引用了常量池中的其他常量,那些引用常量池常量的引用最终也会转换为对常量池中常量的直接引用。虽然常量列表中的索引是从1开始的,但是常量个数还是包含了0,比如一个常量列表中有15个常量,那么它的常量个数就为16。 每一个常量开头都会有一个标志,以表示他的类型。当虚拟机读取这个标志时,就会知道这个常量的具体类型了。表6-3列举了这些标志: Table 6-3. Constant pool tags Entry Type Tag Value Description CONSTANT_Utf8 1 A UTF-8 encoded Unicode string CONSTANT_Integer 3 An int literal value CONSTANT_Float 4 A float literal value CONSTANT_Long 5 A long literal value CONSTANT_Double 6 A double literal value CONSTANT_Class 7 A symbolic reference to a class or interface CONSTANT_String 8 A String literal value CONSTANT_Fieldref 9 A symbolic reference to a field CONSTANT_Methodref 10 A symbolic reference to a method declared in a class CONSTANT_InterfaceMethodref 11 A symbolic reference to a method declared in an interface CONSTANT_NameAndType 12 Part of a symbolic reference to a field or method 表6-3中的每一个标志都会有一个相应的表格,用来描述这个标志的所表示的一些详细信息,这些对应的标志都会以标志名+_INFO来结尾。比如CONSTANT_CLASS标志对应的就是CONSTANT_CLASS_INFO。 常量池在程序的动态链接中扮演了很重要的角色。除了上边所说的各种常量值以外,常量池中包含了一下三种符号引用:类和接口的全名,字段名和描述符,方法名 和描述符。一个字段是一个类或者接口中的实例或者类变量,字段描述符是字段的类型。方法的描述符是方法和返回值和参数的个数、顺序和类型。在虚拟机将这个 类或者接口链接到其他类或者接口时用到这些全名。因为类文件不包含任何关于内存结构的信息,所以这个链接只能以符号引用的形式存在。虚拟机在执行时将这些 符号引用转换为实际的地址。具体的信息参见第八章“The Linking Model”。 4、访问标志(access_flags) 紧接在常量池后面的两个字节就是访问标志,表示这个类或者接口的几方面信息,他有如下几种值: Table 6-4. Flag bits in the access_flags item of ClassFile tables Flag Name Value Meaning if Set Set By ACC_PUBLIC 0x0001 Type is public Classes and interfaces ACC_FINAL 0x0010 Class is final Classes only ACC_SUPER 0x0020 Use new invokespecial semanticsClasses and interfaces ACC_INTERFACE 0x0200 Type is an interface, not a class All interfaces, no classes ACC_ABSTRACT 0x0400 Type is abstract All interfaces, some classes ACC_SUPER标志是为了兼容以前SUN的老式的编译器。所有没有使用的访问标志,必须设置为0。 5、类名(this_class) 接下来的两个字节保存了一个常量池的索引。这个常量池中的实体必须是CONSTANT_CLSS_INTO类型的,他包含了标志和名字索引。标志就是 CONSTATN_CLASS,那个名字索引应该是一个保存了这个类或者接口全名的CONSTANT_UTF8_INFO类型的索引。 6、父类(super_class) this_class之后的就是两个字节的super_class,他也是一个常量池的索引,其中保存了父类的全名,处理this_class一样。当父 类是java.lang.Object时,super_class都应该是0。对于接口super_class都是0。 7、(interfaces_count and interfaces) interfaces_count中保存了父接口的个数,interfaces中以数组形式保存了一些常量池的索引。每一个索引都指向了一个 CONSTANT_CLASS_INFO的常量,其中保存了每一个父接口的全名。这个数组的顺序就是父接口出现在在implements、extends 语句中从左到右的顺序。 8、(fields_count and fields) 字段被保存在一个field_info的列表中,fields_count是这个列表的长度。Field_info列表中保存的只是类或者接口中的申明的变量,从父类或者父接口中继承的字段不保存在这里。每一个field_info表中的一条都描述了一个字段的信息,包括:字段名,描述符,访问权限。如果一个字段被申明为final,那么这个字段的信息即会保存在field_info表中,也会保存在常量池中。 9、(methods_count and methods) 方法的信息都保存在method_info表中,mehtods_count是表的长度。Method_info表中只保存类或者接口中申明的方法,不保存从父类或者接口中继承的方法。 Method_info 表中保存了方法名和描述符(返回值和参数类型)。如果不是抽象方法,还会保存用于堆栈的大小(保存本地变量用的)、操作数堆栈的最大值、捕捉的异常列表、 方法的字节码、可选的行号和本地变量表。如果方法抛出一些被检查的异常,method_info还会包含一个被检查异常的列表。 10、(attributes_count and attributes) 类文件中最后的就是属性个数和atribute_info列表。Atribute_info表中保存了一些指向常量池中constant_utf8_info的索引,其中保存了属性的名字。Java虚拟机规范中定义了两种类型的属性:源代码和内部类。三、特定字符串(Special Strings) 常量池中的字符引用包含三种特定字符串:全限定名,简单名,描述符。一个类或者接口的所有字符引用都必须包含一个全限定名。每一个字段或者方法都有一个简 单名和描述符作为全限定名的补充。这些特定字符串用来表示文件中定义的类和接口,包换类名,父类名,父接口名,每个字段和方法的简单名和描述符。 1、全名(Fully Qualified Names) 当常量池中引用了类和接口时,就会提供这个类或者接口的权限定名,比如java.lang.Object。 2、简单名(Simple Names) 字段和方法都以简单名的形式保存在常量池中。比如常量池中有一个java.lang.Object类的String toString()方法的引用,就会保存“toString”;java.lang.System类的java.io.PrintStream out字段,被保存为“out”。 3、描述符(Descripters) 字段和方法的字符引用都会包含一个描述符。字段的描述符提供了字段的类型。方法的描述符提供了方法的返回值、参数个数、参数类型。所有描述符得类型列表: FieldDescriptor:
       FieldDescriptor:
   FieldType
   ComponentType:
   FieldType
   FieldType:
   BaseType
   ObjectType
   ArrayType
   BaseType:
   Terminal Type
   B byte
   C char
   D double
   F float
   I int
   J long
   S short
   Z boolean
   ObjectType:
   L<classname>;
   ArrayType:
   [ ComponentType
   ParameterDescriptor:
   FieldType
   MethodDescriptor:
   ( ParameterDescriptor* ) ReturnDescriptor
   ReturnDescriptor:
   FieldType
   V
   Table 6-6. Examples of field descriptors
   Descriptor Field Declaration
   I int i;
   [[J long[][] windingRoad;
   [Ljava/lang/Object; java.lang.Object[] stuff;
   Ljava/util/Hashtable; java.util.Hashtable ht;
   [[[Z boolean[][][] isReady;
   Table 6-7. Examples of method descriptors
   Descriptor Method Declaration
   ()I int getSize();
   ()Ljava/lang/String; String toString();
   ([Ljava/lang/String;)V void main(String[] args);
   ()V void wait()
   (JI)V void wait(long timeout, int nanos)
   (ZILjava/lang/String;II)Z boolean regionMatches(boolean ignoreCase, int toOffset, String other, int ooffset, int len);
   ([BII)I int read(byte[] b, int off, int len);
  四、常量池
   常量池被组织成为一个cp_info类型的列表,表6-8显示了cp_info表的结构
   Table 6-8. General form of a cp_info table
   Type Name Count
   u1 tag 1
   u1 info depends on tag value
   1、The CONSTANT_Utf8_info Table
   2、The CONSTANT_Integer_info Table
   3、The CONSTANT_Float_info Table
   4、The CONSTANT_Long_info Table
   5、The CONSTANt_Double_info Table
   6、The CONSTANT_Class_info Table
   7、The CONSTANT_String_info Table
   8、The CONSTANT_Fieldref_info Table
   9、The CONSTANT_Methodref_info Table
   10、The COMSTANT_InterfaceMethodref_info Table
   11、The CONSTANT_NameAndType_info Table
  五、Fields
  六、Methods
  七、Attributes

Java虚拟机

一、什么是Java虚拟机
   当你谈到Java虚拟机时,你可能是指:
   1、抽象的Java虚拟机规范
   2、一个具体的Java虚拟机实现
   3、一个运行的Java虚拟机实例
  二、Java虚拟机的生命周期
   一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。程序开始执行时他才运行,程序结束时他就停止。你在同一台机器上运行三个程序,就会有三个运行中的Java虚拟机。
   Java虚拟机总是开始于一个main()方法,这个方法必须是公有、返回void、直接受一个字符串数组。在程序执行时,你必须给Java虚拟机指明这个包换main()方法的类名。
   Main()方法是程序的起点,他被执行的线程初始化为程序的初始线程。程序中其他的线程都由他来启动。Java中的线程分为两种:守护线程 (daemon)和普通线程(non-daemon)。守护线程是Java虚拟机自己使用的线程,比如负责垃圾收集的线程就是一个守护线程。当然,你也可 以把自己的程序设置为守护线程。包含Main()方法的初始线程不是守护线程。
   只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会停止。如果有足够的权限,你可以调用exit()方法终止程序。
  三、Java虚拟机的体系结构
   在Java虚拟机的规范中定义了一系列的子系统、内存区域、数据类型和使用指南。这些组件构成了Java虚拟机的内部结构,他们不仅仅为Java虚拟机的实现提供了清晰的内部结构,更是严格规定了Java虚拟机实现的外部行为。
   每一个Java虚拟机都由一个类加载器子系统(class loader subsystem),负责加载程序中的类型(类和接口),并赋予唯一的名字。每一个Java虚拟机都有一个执行引擎(execution engine)负责执行被加载类中包含的指令。
   程序的执行需要一定的内存空间,如字节码、被加载类的其他额外信息、程序中的对象、方法的参数、返回值、本地变量、处理的中间变量等等。Java虚拟机将 这些信息统统保存在数据区(data areas)中。虽然每个Java虚拟机的实现中都包含数据区,但是Java虚拟机规范对数据区的规定却非常的抽象。许多结构上的细节部分都留给了 Java虚拟机实现者自己发挥。不同Java虚拟机实现上的内存结构千差万别。一部分实现可能占用很多内存,而其他以下可能只占用很少的内存;一些实现可 能会使用虚拟内存,而其他的则不使用。这种比较精炼的Java虚拟机内存规约,可以使得Java虚拟机可以在广泛的平台上被实现。
   数据区中的一部分是整个程序共有,其他部分被单独的线程控制。每一个Java虚拟机都包含方法区(method area)和堆(heap),他们都被整个程序共享。Java虚拟机加载并解析一个类以后,将从类文件中解析出来的信息保存与方法区中。程序执行时创建的 对象都保存在堆中。
   当一个线程被创建时,会被分配只属于他自己的PC寄存器“pc register”(程序计数器)和Java堆栈(Java stack)。当线程不掉用本地方法时,PC寄存器中保存线程执行的下一条指令。Java堆栈保存了一个线程调用方法时的状态,包括本地变量、调用方法的 参数、返回值、处理的中间变量。调用本地方法时的状态保存在本地方法堆栈中(native method stacks),可能再寄存器或者其他非平台独立的内存中。
   Java堆栈有堆栈块(stack frames (or frames))组成。堆栈块包含Java方法调用的状态。当一个线程调用一个方法时,Java虚拟机会将一个新的块压到Java堆栈中,当这个方法运行结束时,Java虚拟机会将对应的块弹出并抛弃。
   Java虚拟机不使用寄存器保存计算的中间结果,而是用Java堆栈在存放中间结果。这是的Java虚拟机的指令更紧凑,也更容易在一个没有寄存器的设备上实现Java虚拟机。
   图中的Java堆栈中向下增长的,PC寄存器中线程三为灰色,是因为它正在执行本地方法,他的下一条执行指令不保存在PC寄存器中。
  四、数据类型(Data Types)
   所有Java虚拟机中使用的数据都有确定的数据类型,数据类型和操作都在Java虚拟机规范中严格定义。Java中的数据类型分为原始数据类型 (primitive types)和引用数据类型(reference type)。引用类型依赖于实际的对象,但不是对象本身。原始数据类型不依赖于任何东西,他们就是本身表示的数据。
  所有Java程序语言中的原始 数据类型,都是Java虚拟机的原始数据类型,除了布尔型(boolean)。当编译器将Java源代码编译为自己码时,使用整型(int)或者字节型 (byte)去表示布尔型。在Java虚拟机中使用整数0表示布尔型的false,使用非零整数表示布尔型的true,布尔数组被表示为字节数组,虽然他 们可能会以字节数组或者字节块(bit fields)保存在堆中。
   除了布尔型,其他Java语言中的原始类型都是Java虚拟机中的数据类型。在Java中数据类型被分为:整形的byte,short,int,long;char和浮点型的float,double。Java语言中的数据类型在任何主机上都有同样的范围。
   在Java虚拟机中还存在一个Java语言中不能使用的原始数据类型返回值类型(returnValue)。这种类型被用来实现Java程序中的“finally clauses”,具体的参见18章的“Finally Clauses”。
   引用类型可能被创建为:类类型(class type),接口类型(interface type),数组类型(array type)。他们都引用被动态创建的对象。当引用类型引用null时,说明没有引用任何对象。
   Java虚拟机规范只定义了每一种数据类型表示的范围,没有定义在存储时每种类型占用的空间。他们如何存储由Java虚拟机的实现者自己决定。关于浮点型更多信息参见14章“Floating Point Arithmetic”。
  TypeRange
  byte8-bit signed two's complement integer (-27 to 27 - 1, inclusive)
  short16-bit signed two's complement integer (-215 to 215 - 1, inclusive)
  int32-bit signed two's complement integer (-231 to 231 - 1, inclusive)
  long64-bit signed two's complement integer (-263 to 263 - 1, inclusive)
  char16-bit unsigned Unicode character (0 to 216 - 1, inclusive)
  float32-bit IEEE 754 single-precision float
  double64-bit IEEE 754 double-precision float
  returnValueaddress of an opcode within the same method
  referencereference to an object on the heap, or null
  五、字节长度
   Java虚拟机中最小的数据单元式字(word),其大小由Java虚拟机的实现者定义。但是一个字的大小必须足够容纳byte,short,int, char,float,returnValue,reference;两个字必须足够容纳long,double。所以虚拟机的实现者至少提供的字不能小 于31bits的字,但是最好选择特定平台上最有效率的字长。
   在运行时,Java程序不能决定所运行机器的字长。字长也不会影响程序的行为,他只是在Java虚拟机中的一种表现方式。
  六、类加载器子系统
   Java虚拟机中的类加载器分为两种:原始类加载器(primordial class loader)和类加载器对象(class loader objects)。原始类加载器是Java虚拟机实现的一部分,类加载器对象是运行中的程序的一部分。不同类加载器加载的类被不同的命名空间所分割。
   类加载器调用了许多Java虚拟机中其他的部分和java.lang包中的很多类。比如,类加载对象就是java.lang.ClassLoader子类 的实例,ClassLoader类中的方法可以访问虚拟机中的类加载机制;每一个被Java虚拟机加载的类都会被表示为一个 java.lang.Class类的实例。像其他对象一样,类加载器对象和Class对象都保存在堆中,被加载的信息被保存在方法区中。
   1、加载、连接、初始化(Loading, Linking and Initialization)
  类加载子系统不仅仅负责定位并加载类文件,他按照以下严格的步骤作了很多其他的事情:(具体的信息参见第七章的“类的生命周期”)
   1)、加载:寻找并导入指定类型(类和接口)的二进制信息
   2)、连接:进行验证、准备和解析
   ①验证:确保导入类型的正确性
   ②准备:为类型分配内存并初始化为默认值
   ③解析:将字符引用解析为直接饮用
   3)、初始化:调用Java代码,初始化类变量为合适的值
   2、原始类加载器(The Primordial Class Loader)
   每个Java虚拟机都必须实现一个原始类加载器,他能够加载那些遵守类文件格式并且被信任的类。但是,Java虚拟机的规范并没有定义如何加载类,这由 Java虚拟机实现者自己决定。对于给定类型名的类型,原始莱加载器必须找到那个类型名加“.class”的文件并加载入虚拟机中。
   3、类加载器对象
   虽然类加载器对象是Java程序的一部分,但是ClassLoader类中的三个方法可以访问Java虚拟机中的类加载子系统。
   1)、protected final Class defineClass(…):使用这个方法可以出入一个字节数组,定义一个新的类型。
   2)、protected Class findSystemClass(String name):加载指定的类,如果已经加载,就直接返回。
   3)、protected final void resolveClass(Class c):defineClass()方法只是加载一个类,这个方法负责后续的动态连接和初始化。
   具体的信息,参见第八章“连接模型”( The Linking Model)。
   4、命名空间
   当多个类加载器加载了同一个类时,为了保证他们名字的唯一性,需要在类名前加上加载该类的类加载器的标识。具体的信息,参见第八章“连接模型”( The Linking Model)。
  七、方法区(The Method Area)
   在Java虚拟机中,被加载类型的信息都保存在方法区中。这写信息在内存中的组织形式由虚拟机的实现者定义,比如,虚拟机工作在一个“little- endian”的处理器上,他就可以将信息保存为“little-endian”格式的,虽然在Java类文件中他们是以“big-endian”格式保 存的。设计者可以用最适合并地机器的表示格式来存储数据,以保证程序能够以最快的速度执行。但是,在一个只有很小内存的设备上,虚拟机的实现者就不会占用 很大的内存。
   程序中的所有线程共享一个方法区,所以访问方法区信息的方法必须是线程安全的。如果你有两个线程都去加载一个叫Lava的类,那只能由一个线程被容许去加载这个类,另一个必须等待。
   在程序运行时,方法区的大小是可变的,程序在运行时可以扩展。有些Java虚拟机的实现也可以通过参数也订制方法区的初始大小,最小值和最大值。
   方法区也可以被垃圾收集。因为程序中的内由类加载器动态加载,所有类可能变成没有被引用(unreferenced)的状态。当类变成这种状态时,他就可 能被垃圾收集掉。没有加载的类包括两种状态,一种是真正的没有加载,另一个种是“unreferenced”的状态。详细信息参见第七章的类的生命周期 (The Lifetime of a Class)。
   1、类型信息(Type Information)
   每一个被加载的类型,在Java虚拟机中都会在方法区中保存如下信息:
   1)、类型的全名(The fully qualified name of the type)
   2)、类型的父类型的全名(除非没有父类型,或者弗雷形式java.lang.Object)(The fully qualified name of the typeís direct superclass)
   3)、给类型是一个类还是接口(class or an interface)(Whether or not the type is a class )
   4)、类型的修饰符(public,private,protected,static,final,volatile,transient等)(The typeís modifiers)
   5)、所有父接口全名的列表(An ordered list of the fully qualified names of any direct superinterfaces)
   类型全名保存的数据结构由虚拟机实现者定义。除此之外,Java虚拟机还要为每个类型保存如下信息:
   1)、类型的常量池(The constant pool for the type)
   2)、类型字段的信息(Field information)
   3)、类型方法的信息(Method information)
   4)、所有的静态类变量(非常量)信息(All class (static) variables declared in the type, except constants)
   5)、一个指向类加载器的引用(A reference to class ClassLoader)
   6)、一个指向Class类的引用(A reference to class Class)
   1)、类型的常量池(The constant pool for the type)
   常量池中保存中所有类型是用的有序的常量集合,包含直接常量(literals)如字符串、整数、浮点数的常量,和对类型、字段、方法的符号引用。常量池 中每一个保存的常量都有一个索引,就像数组中的字段一样。因为常量池中保存中所有类型使用到的类型、字段、方法的字符引用,所以它也是动态连接的主要对 象。详细信息参见第六章“The Java Class File”。
   2)、类型字段的信息(Field information)
   字段名、字段类型、字段的修饰符(public,private,protected,static,final,volatile,transient等)、字段在类中定义的顺序。
   3)、类型方法的信息(Method information)
   方法名、方法的返回值类型(或者是void)、方法参数的个数、类型和他们的顺序、字段的修饰符(public,private,protected,static,final,volatile,transient等)、方法在类中定义的顺序
   如果不是抽象和本地本法还需要保存
   方法的字节码、方法的操作数堆栈的大小和本地变量区的大小(稍候有详细信息)、异常列表(详细信息参见第十七章“Exceptions”。)
   4)、类(静态)变量(Class Variables)
   类变量被所有类的实例共享,即使不通过类的实例也可以访问。这些变量绑定在类上(而不是类的实例上),所以他们是类的逻辑数据的一部分。在Java虚拟机使用这个类之前就需要为类变量(non-final)分配内存
   常量(final)的处理方式于这种类变量(non-final)不一样。每一个类型在用到一个常量的时候,都会复制一份到自己的常量池中。常量也像类变 量一样保存在方法区中,只不过他保存在常量池中。(可能是,类变量被所有实例共享,而常量池是每个实例独有的)。Non-final类变量保存为定义他的 类型数据(data for the type that declares them)的一部分,而final常量保存为使用他的类型数据(data for any type that uses them)的一部分。详情参见第六章“The Java Class FileThe Java Class File”
   5)、指向类加载器的引用(A reference to class ClassLoader)
   每一个被Java虚拟机加载的类型,虚拟机必须保存这个类型是否由原始类加载器或者类加载器加载。那些被类加载器加载的类型必须保存一个指向类加载器的引 用。当类加载器动态连接时,会使用这条信息。当一个类引用另一个类时,虚拟机必须保存那个被引用的类型是被同一个类加载器加载的,这也是虚拟机维护不同命 名空间的过程。详情参见第八章“The Linking Model”
   6)、指向Class类的引用(A reference to class Class)
   Java虚拟机为每一个加载的类型创建一个java.lang.Class类的实例。你也可以通过Class类的方法:
  public static Class forName(String className)来查找或者加载一个类,并取得相应的Class类的实例。通过这个Class类的实例,我们可以访问Java虚拟机方法区中的信息。具体参照Class类的JavaDoc。
   2、方法列表(Method Tables)
   为了更有效的访问所有保存在方法区中的数据,这些数据的存储结构必须经过仔细的设计。所有方法区中,除了保存了上边的那些原始信息外,还有一个为了加快存 取速度而设计的数据结构,比如方法列表。每一个被加载的非抽象类,Java虚拟机都会为他们产生一个方法列表,这个列表中保存了这个类可能调用的所有实例 方法的引用,报错那些父类中调用的方法。详情参见第八章“The Linking Model”
  八、堆
   当Java程序创建一个类的实例或者数组时,都在堆中为新的对象分配内存。虚拟机中只有一个堆,所有的线程都共享他。
   1、垃圾收集(Garbage Collection)
   垃圾收集是释放没有被引用的对象的主要方法。它也可能会为了减少堆的碎片,而移动对象。在Java虚拟机的规范中没有严格定义垃圾收集,只是定义一个Java虚拟机的实现必须通过某种方式管理自己的堆。详情参见第九章“Garbage Collection”。
   2、对象存储结构(Object Representation)
   Java虚拟机的规范中没有定义对象怎样在堆中存储。每一个对象主要存储的是他的类和父类中定义的对象变量。对于给定的对象的引用,虚拟机必须嫩耨很快的 定位到这个对象的数据。另为,必须提供一种通过对象的引用方法对象数据的方法,比如方法区中的对象的引用,所以一个对象保存的数据中往往含有一个某种形式 指向方法区的指针。
   一个可能的堆的设计是将堆分为两个部分:引用池和对象池。一个对象的引用就是指向引用池的本地指针。每一个引用池中的条目都包含两个部分:指向对象池中对 象数据的指针和方法区中对象类数据的指针。这种设计能够方便Java虚拟机堆碎片的整理。当虚拟机在对象池中移动一个对象的时候,只需要修改对应引用池中 的指针地址。但是每次访问对象的数据都需要处理两次指针。下图演示了这种堆的设计。在第九章的“垃圾收集”中的HeapOfFish Applet演示了这种设计。
   另一种堆的设计是:一个对象的引用就是一个指向一堆数据和指向相应对象的偏移指针。这种设计方便了对象的访问,可是对象的移动要变的异常复杂。下图演示了这种设计
   当程序试图将一个对象转换为另一种类型时,虚拟机需要判断这种转换是否是这个对象的类型,或者是他的父类型。当程序适用instanceof语句的时候也 会做类似的事情。当程序调用一个对象的方法时,虚拟机需要进行动态绑定,他必须判断调用哪一个类型的方法。这也需要做上面的判断。
   无论虚拟机实现者使用哪一种设计,他都可能为每一个对象保存一个类似方法列表的信息。因为他可以提升对象方法调用的速度,对提升虚拟机的性能非常重要,但 是虚拟机的规范中比没有要求必须实现类似的数据结构。下图描述了这种结构。图中显示了一个对象引用相关联的所有的数据结构,包括:
   1)、一个指向类型数据的指针
   2)、一个对象的方法列表。方法列表是一个指向所有可能被调用对象方法的指针数组。方法数据包括三个部分:操作码堆栈的大小和方法堆栈的本地变量区;方法的字节码;异常列表。
   每一个Java虚拟机中的对象必须关联一个用于同步多线程的lock(mutex)。同一时刻,只能有一个对象拥有这个对象的锁。当一个拥有这个这个对象 的锁,他就可以多次申请这个锁,但是也必须释放相应次数的锁才能真正释放这个对象锁。很多对象在整个生命周期中都不会被锁,所以这个信息只有在需要时才需 要添加。很多Java虚拟机的实现都没有在对象的数据中包含“锁定数据”,只是在需要时才生成相应的数据。除了实现对象的锁定,每一个对象还逻辑关联到一 个“wait set”的实现。锁定帮组线程独立处理共享的数据,不需要妨碍其他的线程。“wait set”帮组线程协作完成同一个目标。“wait set”往往通过Object类的wait()和notify()方法来实现。
   垃圾收集也需要堆中的对象是否被关联的信息。Java虚拟机规范中指出垃圾收集一个运行一个对象的finalizer方法一次,但是容许 finalizer方法重新引用这个对象,当这个对象再次不被引用时,就不需要再次调用finalize方法。所以虚拟机也需要保存finalize方法 是否运行过的信息。更多信息参见第九章的“垃圾收集”
   3、数组的保存(Array Representation)
  在Java 中,数组是一种完全意义上的对象,他和对象一样保存在堆中、有一个指向Class类实例的引用。所有同一维度和类型的数组拥有同样的Class,数组的长 度不做考虑。对应Class的名字表示为维度和类型。比如一个整型数据的Class为“[I”,字节型三维数组Class名为“[[[B”,两维对象数据 Class名为“[[Ljava.lang.Object”。
  多维数组被表示为数组的数组,如下图:
   数组必须在堆中保存数组的长度,数组的数据和一些对象数组类型数据的引用。通过一个数组引用的,虚拟机应该能够取得一个数组的长度,通过索引能够访问特定 的数据,能够调用Object定义的方法。Object是所有数据类的直接父类。更多信息参见第六章“类文件”。
  九、PC寄存器(程序计数器)(The Program Counter)
   每一个线程开始执行时都会被创建一个程序计数器。程序计数器只有一个字长(word),所以它能够保存一个本地指针和returnValue。当线程执行 时,程序计数器中存放了正在执行指令的地址,这个地址可以使一个本地指针,也可以使一个从方法字节码开始的偏移指针。如果执行本地方法,程序计数器的值没 有被定义。
  十、Java堆栈(The Java Stack)
   当一个线程启动时,Java虚拟机会为他创建一个Java堆栈。Java堆栈用一些离散的frame类纪录线程的状态。Java虚拟机堆Java堆栈的操作只有两种:压入和弹出frames。
   线程中正在执行的方法被称为当前方法(current method),当前方法所对应的frame被称为当前帧(current frame)。定义当前方法的类被称为当前类(current class),当前类的常量池被称为当前常量池(current constant pool.)。当线程执行时,Java虚拟机会跟踪当前类和当前常量池。但线程操作保存在帧中的数据时,他只操作当前帧的数据。
   当线程调用一个方法时,虚拟机会生成一个新的帧,并压入线程的Java堆栈。这个新的帧变成当前帧。当方法执行时,他使用当前帧保存方法的参数、本地变 量、中间结构和其他数据。方法有两种退出方式:正常退出和异常推出。无论方法以哪一种方式推出,Java虚拟机都会弹出并丢弃方法的帧,上一个方法的帧变 为当前帧。
   所有保存在帧中的数据都只能被拥有它的线程访问,线程不能访问其他线程的堆栈中的数据。所以,访问方法的本地变量时,不需要考虑多线程同步。
   和方法区、堆一样,Java堆栈不需要连续的内存空间,它可以被保存在一个分散的内存空间或者堆上。堆栈具体的数据和长度都有Java虚拟机的实现者自己定义。一些实现可能提供了执行堆栈最大值和最小值的方法。
  十一、堆栈帧(The Stack Frame)
   堆栈帧包含三部分:本地变量、操作数堆栈和帧数据。本地变量和操作数堆栈的大小都是一字(word)为单位的,他们在编译就已经确定。帧数据的大小取决于 不同的实现。当程序调用一个方法时,虚拟机从类数据中取得本地变量和操作数堆栈的大小,创建一个合适大小和帧,然后压入Java堆栈中。
   1、本地变量(Local Variables)
   本地变量在Java堆栈帧中被组织为一个从0计数的数组,指令通过提供他们的索引从本地变量区中取得相应的值。Int,float,reference, returnValue占一个字,byte,short,char被转换成int然后存储,long和doubel占两个字。
   指令通过提供两个字索引中的前一个来取得long,doubel的值。比如一个long的值存储在索引3,4上,指令就可以通过3来取得这个long类型的值。
   本地变量区中包含了方法的参数和本地变量。编译器将方法的参数以他们申明的顺序放在数组的前面。但是编译器却可以将本地变量任意排列在本地变量数组中,甚至两个本地变量可以公用一个地址,比如,当两个本地变量在两个不交叠的区域内,就像循环变量i,j。
   虚拟机的实现者可以使用任何结构来描述本地变量区中的数据,虚拟机规范中没有定义如何存储long和doubel。
   2、操作数堆栈(Operand Stack)
   向本地变量一样,操作数堆栈也被组织为一个以字为单位的数组。但是不像本地变量那样通过索引访问,而是通过push和pop值来实现访问的。如果一个指令push一个值到堆栈中,那么下一个指令就可以pop并且使用这个值。
   操作数堆栈不像程序计数器那样不可以被指令直接访问,指令可以直接访问操作数堆栈。Java虚拟机是一个以堆栈为基础,而不是以寄存器为基础的,因为它的 指令从堆栈中取得操作数,而不是同寄存器中。当然,指令也可以从其他地方去的操作数,比如指令后面的操作码,或者常量池。但是Java虚拟机指令主要是从 操作数堆栈中取得他们需要的操作数。
   Java虚拟机将操作数堆栈视为工作区,很多指令通过先从操作数堆栈中pop值,在处理完以后再将结果push回操作数堆栈。一个add的指令执行过程如 下图所示:先执行iload_0和iload_1两条指令将需要相加的两个数,从本地方法区中取出,并push到操作数堆栈中;然后执行iadd指令,现 pop出两个值,相加,并将结果pusp进操作数堆栈中;最后执行istore_2指令,pop出结果,赋值到本地方法区中。
   3、帧数据(Frame Data)
   处理本地变量和操作数堆栈以外,java堆栈帧还包括了为了支持常量池,方法返回值和异常分发需要的数据,他们被保存在帧数据中。
   当虚拟机遇到使用指向常量池引用的指令时,就会通过帧数据中指向常量区的指针来访问所需要的信息。前面提到过,常量区中的引用在最开始时都是符号引用。即使当虚拟机检查这些引用时,他们也是字符引用。所以虚拟机需要在这时转换这个引用。
   当一个方法正常返回时,虚拟机需要重建那个调用这个方法的方法的堆栈帧。如果执行完的方法有返回值,虚拟机就需要将这个值push进调用方法的哪个操作数堆栈中。
   帧数据中也包含虚拟机用来处理异常的异常表的引用。异常表定义了一个被catch语句保护的一段字节码。每一个异常表中的个体又包含了需要保护的字节玛的 范围,和异常被捕捉到时需要执行的字节码的位置。当一个方法抛出一个异常时,Java虚拟机就是用异常表去判断如何处理这个异常。如果虚拟机找到了一个匹 配的catch,他就会将控制权交给catch语句。如果没有找到匹配的catch,方法就会异常返回,然后再调用的方法中继续这个过程。
   除了以上的三个用途外,帧数据还可能包含一些依赖于实现的数据,比如调试的信息。
  十二、本地方法堆栈
   本地方法区依赖于虚拟机的不同实现。虚拟机的实现者可以自己决定使用哪一种机制去执行本地方法。
   任何本地方法接口(Native Method Interface)都使用某种形式的本地方法堆栈。
  十三、执行引擎
   一个java虚拟机实现的核心就是执行引擎。在Java虚拟机规范,执行引擎被描述为一系列的指令。对于每一个指令,规范都描述了他们应该做什么,但是没有说要如何去做。
   1、指令集
   在Java虚拟机中一个方法的字节码流就是一个指令的序列。每一个指令由一个字节的操作码(Opcode)和可能存在的操作数(Operands)。操作 码指示去做什么,操作数提供一些执行这个操作码可能需要的额外的信息。一个抽象的执行引擎每次执行一个指令。这个过程发生在每一个执行的线程中。
  有时,执行引擎可能会遇到一个需要调用本地方法的指令,在这种情况下,执行引擎会去试图调用本地方法,但本地方法返回时,执行引擎会继续执行字节码流中的下一个指令。本地方法也可以看成对Java虚拟机中的指令集的一种扩充。
   决定下一步执行那一条指令也是执行引擎工作的一部分。执行引擎有三种方法去取得下一条指令。多数指令会执行跟在他会面的指令;一些像goto, return的指令,会在他们执行的时候决定他们的下一条指令;当一个指令抛出异常时,执行引擎通过匹配catch语句来决定下一条应该执行的指令。
   平台独立性、网络移动性、安全性左右了Java虚拟机指令集的设计。平台独立性是指令集设计的主要影响因素之一。基于堆栈的结构使得Java虚拟机可以在 更多的平台上实现。更小的操作码,紧凑的结构使得字节码可以更有效的利用网络带宽。一次性的字节码验证,使得字节码更安全,而不影响太多的性能。
   2、执行技术
   许多种执行技术可以用在Java虚拟机的实现中:解释执行,及时编译(just-in-time compiling),hot-spot compiling,native execution in silicon。
   3、线程
   Java虚拟机规范定义了一种为了在更多平台上实现的线程模型。Java线程模型的一个目标时可以利用本地线程。利用本地线程可以让Java程序中的线程能过在多处理器机器上真正的同时执行。
   Java线程模型的一个代价就是线程优先级,一个Java线程可以在1-10的优先级上运行。1最低,10最高。如果设计者使用了本地线程,他们可能将这 10个优先级映射到本地优先级上。Java虚拟机规范只定义了,高一点优先级的线程可以却一些cpu时间,低优先级的线程在所有高优先级线程都堵塞时,也 可以获取一些cpu时间,但是这没有保证:低优先级的线程在高优先级线程没有堵塞时不可以获得一定的cpu时间。因此,如果需要在不同的线程间协作,你必 须使用的“同步(synchronizatoin)”。
   同步意味着两个部分:对象锁(object locking)和线程等待、激活(thread wait and notify)。对象锁帮助线程可以不受其他线程的干扰。线程等待、激活可以让不同的线程进行协作。
   在Java虚拟机的规范中,Java线程被描述为变量、主内存、工作内存。每一个Java虚拟机的实例都有一个主内存,他包含了所有程序的变量:对象、数组合类变量。每一个线程都有自己的工作内存,他保存了哪些他可能用到的变量的拷贝。规则:
   1)、从主内存拷贝变量的值到工作内存中
   2)、将工作内存中的值写会主内存中
   如果一个变量没有被同步化,线程可能以任何顺序更新主内存中的变量。为了保证多线程程序的正确的执行,必须使用同步机制。
  十四、本地方法接口(Native Method Interface)
   Java虚拟机的实现并不是必须实现本地方法接口。一些实现可能根本不支持本地方法接口。Sun的本地方法接口是JNI(Java Native Interface)。
  十五、现实中的机器(The Real Machine)
  十六、数学方法:仿真(Eternal Math : A Simulation)
4月28日

使用platformRequest()自动更新MIDlet套件

MIDP 2.0提供了一系列的新特性,其中一个就是调用平台的应用。例如可以调用WAP浏览器访问特定的网址,也可以调用电话应用程序呼叫某个号码。上述两个功能都可以使用MIDlet类的platformRequest()方法实现,platformRequest()接受一个String类型的参数url,如果是呼叫电话号码则url的形式为tel:13810011001。如果是调用WAP浏览器则url的形式类似于http://www.j2medev.com/wap.wml

本文介绍如何使用platformRequest()方法自动更新MIDlet套件,在MIDP的文档中说明。如果url的形式为一个指定的MIDlet套件,可以是JAD文件,也可以是jar文件。例如http://www.j2medev.com/helloworld.jad。这个时候,此请求会被认为是安装MIDlet套件来对待,这样用户可以控制安装的过程,就像我们直接从WAP浏览器输入地址安装MIDlet套件一样。如果请求的MIDlet套件是当前正在运行的应用程序的升级版,则当前的程序需要先退出,然后执行更新操作。

如果想让发布的MIDlet套件具备自动升级的功能,那么需要提供一个服务器端程序,服务器端程序能够检测是不是有更新版本的应用程序下载,并且可以将这个结果告诉给客户端。通常这样的服务器端可以实现为Web应用程序,用servletMIDlet通信。当用户运行MIDlet的时候,MIDlet首先连接指定的服务器获取信息,如果没有新版软件则正常运行,如果有新版本的软件则是用platformRequest()方法请求安装应用程序。

                     if (update == NEED_UPDATE) {

                            try {

                                   platformRequest("http://www.j2medev.com/wap/autoupdate.jar");

                                   destroyApp(true);

                                   notifyDestroyed();

                            } catch (ConnectionNotFoundException ex) {

                                   ex.printStackTrace();

                            }

                           

                     }else{

                            //do something else

                     }

这里我们在代码中执行destroyApp(true),让MIDlet套件主动退出。如果想做的更为专业,可以在检测到有新版本的软件的时候,显示一个对话框让用户选择是否自动更新,根据用户的选择执行相关的操作。下面的代码UpdateMIDlet可以用于演示此项功能。

/**

 * author mingjava

 * Created on 2006-3-8

 */

package com.j2medev.autoupdate;

 

import java.util.Random;

import javax.microedition.io.ConnectionNotFoundException;

import javax.microedition.lcdui.Display;

import javax.microedition.lcdui.Form;

import javax.microedition.midlet.MIDlet;

import javax.microedition.midlet.MIDletStateChangeException;

 

public class UpdateMIDlet extends MIDlet {

 

       public static int NEED_UPDATE = 0;

 

       public static int NO_UPDATE = 1;

 

       private Display display = null;

 

       protected void startApp() throws MIDletStateChangeException {

              if (display == null) {

                     display = Display.getDisplay(this);

                     int update = (new Random().nextInt() >>> 1) % 2;

                     if (update == NEED_UPDATE) {

                            try {

                                   platformRequest("http://www.j2medev.com/wap/autoupdate.jar");

                                   destroyApp(true);

                                   notifyDestroyed();

                            } catch (ConnectionNotFoundException ex) {

                                   ex.printStackTrace();

                            }

                           

                     }else if(update == NO_UPDATE){

                            Form form = new Form("Test");

                            form.append("No update is needed");

                            display.setCurrent(form);

                     }

              }

       }

 

       protected void pauseApp() {

 

       }

 

       protected void destroyApp(boolean arg0) throws MIDletStateChangeException {

 

       }

 

}

       为了模拟是否有升级软件的可能,我们在startApp()中随机生成一个随机数update。如果update等于0则代表有升级版本,如果update等于1则代表不需要升级。为了简单起见,这里我们没有编写联网检测升级版本的程序。此程序在Nokia 7610上测试通过,可以自动升级MIDlet套件。

4月25日

[转]Java程序员的推荐阅读书籍

JavaEye (http://www.javaeye.com)
范凯(http://robbin.javaeye.com)
作为Java程序员来说,最痛苦的事情莫过于可以选择的范围太广,可以读的书太多,往往容易无所适从。我想就我自己读过的技术书籍中挑选出来一些,按照学习的先后顺序,推荐给大家,特别是那些想不断提高自己技术水平的Java程序员们。
一、Java编程入门类
对于没有Java编程经验的程序员要入门,随便读什么入门书籍都一样,这个阶段需要你快速的掌握Java基础语法和基本用法,宗旨就是“囫囵吞枣不求甚解”,先对Java熟悉起来再说。用很短的时间快速过一遍Java语法,连懵带猜多写写代码,要“知其然”。
1、《Java编程思想》

在有了一定的Java编程经验之后,你需要“知其所以然”了。这个时候《Java编程思想》是一本让你知其所以然的好书,它对于基本的面向对象知识有比较清楚的交待,对Java基本语法,基本类库有比较清楚的讲解,可以帮你打一个良好的Java编程基础。这本书的缺点是实在太厚,也比较罗嗦,不适合现代人快节奏学习,因此看这本书要懂得取舍,不是每章每节都值得一看的,挑重点的深入看就可以了。
2、《Agile Java》中文版

这本书是出版社送给我的,我一拿到就束之高阁,放在书柜一页都没有翻过,但是前两天整理书柜的时候,拿出来一翻,竟然发现这绝对是一本好书!这本书一大特点是以单元测试和TDD来贯穿全书的,在教你Java各种重要的基础知识的过程中,潜移默化的影响你的编程思维走向敏捷,走向TDD。另外这本书成书很新,以JDK5.0的语法为基础讲解,要学习JDK5.0的新语法也不错。还有这本书对于内容取舍也非常得当,Java语言毕竟类库庞大,可以讲的内容太多,这本书选择的内容以及内容的多寡都很得当,可以让你以最少的时间掌握Java最重要的知识,顺便培养出来优秀的编程思路,真是一本不可多得的好书。
虽然作者自己把这本书定位在入门级别,但我不确定这本书用来入门是不是稍微深了点,我自己也准备有空的时候翻翻这本书,学习学习。
二、Java编程进阶类
打下一个良好的Java基础,还需要更多的实践经验积累,我想没有什么捷径。有两本书值得你在编程生涯的这个阶段阅读,培养良好的编程习惯,提高你的代码质量。
1、《重构 改善既有代码的设计》

这本书名气很大,不用多介绍,可以在闲暇的时候多翻翻,多和自己的实践相互印证。这本书对你产生影响是潜移默化的。
2、《测试驱动开发 by Example》

本书最大特点是很薄,看起来没有什么负担。你可以找一个周末的下午,一边看,一边照做,一个下午就把书看完,这本书的所有例子跑完了。这本书的作用是通过实战让你培养TDD的思路。
三、Java架构师之路
到这个阶段,你应该已经非常娴熟的运用Java编程,而且有了一个良好的编程思路和习惯了,但是你可能还缺乏对应用软件整体架构的把握,现在就是你迈向架构师的第一步。
1、《Expert One-on-One J2EE Design and Development》

这本书是Rod Johnson的成名著作,非常经典,从这本书中的代码诞生了springframework。但是好像这本书没有中译本。
2、《Expert One-on-One J2EE Development without EJB》

这本书由gigix组织翻译,多位业界专家参与,虽然署名译者是JavaEye,其实JavaEye出力不多,实在是忝居译者之名。
以上两本书都是RodJohnson的经典名著,Java架构师的必读书籍。在我所推荐的这些书籍当中,是我看过的最仔细,最认真的书,我当时读这本书几乎是废寝忘食的一气读完的,有小时候挑灯夜读金庸武侠小说的劲头,书中所讲内容和自己的经验知识一一印证,又被无比精辟的总结出来,读完这本书以后,我有种被打通经脉,功力爆增的感觉。
但是后来我看过一些其他人的评价,似乎阅读体验并没有我那么high,也许是因为每个人的知识积累和经验不同导致的。我那个时候刚好是经验知识积累已经足够丰富,但是还没有系统的整理成型,让这本书一梳理,立刻形成完整的知识体系了。
3、《企业应用架构模式》

Martin的又一本名著,但这本书我只是泛泛的看了一遍,并没有仔细看。这本书似乎更适合做框架的人去看,例如如果你打算自己写一个ORM的话,这本书是一定要看的。但是做应用的人,不看貌似也无所谓,但是如果有空,我还是推荐认真看看,会让你知道框架为什么要这样设计,这样你的层次可以晋升到框架设计者的角度去思考问题。Martin的书我向来都是推崇,但是从来都没有像Rod Johnson的书那样非常认真去看。
4、《敏捷软件开发 原则、模式与实践》

Uncle Bob的名著,敏捷的经典名著,这本书比较特别,与其说是讲软件开发过程的书,不如说讲软件架构的书,本书用了很大篇幅讲各种面向对象软件开发的各种模式,个人以为看了这本书,就不必看GoF的《设计模式》了。
四、软件开发过程
了解软件开发过程不单纯是提高程序员个人的良好编程习惯,也是增强团队协作的基础。
1、《UML精粹》

UML其实和软件开发过程没有什么必然联系,却是软件团队协作沟通,撰写软件文档需要的工具。但是UML真正实用的图不多,看看这本书已经足够了,完全没有必要去啃《UML用户指南》之类的东西。要提醒大家的是,这本书的中译本翻译的非常之烂,建议有条件的看英文原版。
2、《解析极限编程 拥抱变化》XP

这是Kent Beck名著的第二版,中英文对照。没什么好说的,必读书籍。
3、《统一软件开发过程》UP

其实UP和敏捷并不一定冲突,UP也非常强调迭代,测试,但是UP强调的文档和过程驱动却是敏捷所不取的。不管怎么说,UP值得你去读,毕竟在中国真正接受敏捷的企业很少,你还是需要用UP来武装一下自己的,哪怕是披着UP的XP。
4、《敏捷建模》AM

ScottAmbler的名著,这本书非常的progmatic,告诉你怎么既敏捷又UP,把敏捷和UP统一起来了,又提出了很多progmatic的建议和做法。你可以把《解析极限编程拥抱变化》、《统一软件开发过程》和《敏捷建模》这三本书放在一起读,看XP和UP的不同点,再看AM是怎么统一XP和UP的,把这三种理论融为一炉,形成自己的理论体系,那么你也可以去写书了。
五、软件项目管理
如果你突然被领导提拔为项目经理,而你完全没有项目管理经验,你肯定会心里没底;如果你觉得自己管理项目不善,很想改善你的项目管理能力,那么去考PMP肯定是远水不解近渴的。
1、《快速软件开发》

这也是一本名著。可以这样说,有本书在手,你就有了一个项目管理的高级参谋给你出谋划策,再也不必担心自己不能胜任的问题了。这本书不是讲管理的理论的,在实际的项目管理中,讲这些理论是不解决问题的,这本书有点类似于“软件项目点子大全”之类的东西,列举了种种软件项目当中面临的各种问题,以及应该如何解决问题的点子,你只需要稍加变通,找方抓药就行了。
六、总结
在这份推荐阅读书籍的名单中,我没有列举流行的软件框架类学习书籍,例如Struts,Hibernate,Spring之类,也没有列举AJAX方面的书籍。是因为这类书籍容易过时,而上述的大半书籍的生命周期都足够长,值得你去购买和收藏。

郝瑞——Java学习之路:不走弯路,就是捷径(转载)

 

 

0.引言

软件开发之路是充满荆棘与挑战之路,也是充满希望之路。Java学习也是如此,没有捷径可走。梦想像《天龙八部》中虚竹一样被无崖子醍醐灌顶而轻松获得一甲子功力,是很不现实的。每天仰天大叫"天神啊,请赐给我一本葵花宝典吧",殊不知即使你获得了葵花宝典,除了受自宫其身之苦外,你也不一定成得了"东方不败",倒是成"西方失败"的几率高一点。

"不走弯路,就是捷径",佛经说的不无道理。

1.如何学习程序设计?

Java是一种平台,也是一种程序设计语言,如何学好程序设计不仅仅适用于Java,对C++等其他程序设计语言也一样管用。有编程高手认为,Java也好C也好没什么分别,拿来就用。为什么他们能达到如此境界?我想是因为编程语言之间有共通之处,领会了编程的精髓,自然能够做到一通百通。如何学习程序设计理所当然也有许多共通的地方。

1.1 培养兴趣

兴趣是能够让你坚持下去的动力。如果只是把写程序作为谋生的手段的话,你会活的很累,也太对不起自己了。多关心一些行业趣事,多想想盖茨。不是提倡天天做白日梦,但人要是没有了梦想,你觉得有味道吗?可能像许多深圳本地农民一样,打打麻将,喝喝功夫茶,拜拜财神爷;每个月就有几万十几万甚至更多的进帐,凭空多出个"食利阶层"。你认为,这样有味道吗?有空多到一些程序员论坛转转,你会发现,他们其实很乐观幽默,时不时会冒出智慧的火花。

1.2 慎选程序设计语言

男怕入错行,女怕嫁错郎。初学者选择程序设计语言需要谨慎对待。软件开发不仅仅是掌握一门编程语言了事,它还需要其他很多方面的背景知识。软件开发也不仅仅局限于某几个领域,而是已经渗透到了各行各业几乎每一个角落。

如果你对硬件比较感兴趣,你可以学习C语言/汇编语言,进入硬件开发领域。如果你对电信的行业知识及网络比较熟悉,你可以在C/C++等之上多花时间,以期进入电信软件开发领域。如果你对操作系统比较熟悉,你可以学习C/Linux等等,为Linux内核开发/驱动程序开发/嵌入式开发打基础。

如果你想介入到应用范围最广泛的应用软件开发(包括电子商务电子政务系统)的话,你可以选择J2EE或.NET,甚至LAMP组合。每个领域要求的背景知识不一样。做应用软件需要对数据库等很熟悉。总之,你需要根据自己的特点来选择合适你的编程语言。

1.3 要脚踏实地,快餐式的学习不可取

先分享一个故事。

有一个小朋友,他很喜欢研究生物学,很想知道那些蝴蝶如何从蛹壳里出来,变成蝴蝶便会飞。有一次,他走到草原上面看见一个蛹,便取了回家,然后看着,过了几天以后,这个蛹出了一条裂痕,看见里面的蝴蝶开始挣扎,想抓破蛹壳飞出来。 这个过程达数小时之久,蝴蝶在蛹里面很辛苦地拼命挣扎,怎么也没法子走出来。这个小孩看着看着不忍心,就想不如让我帮帮它吧,便随手拿起剪刀在蛹上剪开,使蝴蝶破蛹而出。 但蝴蝶出来以后,因为翅膀不够力,变得很臃肿,飞不起来。

这个故事给我们的启示是:欲速则不达。

浮躁是现代人最普遍的心态,能怪谁?也许是贫穷落后了这么多年的缘故,就像当年的大跃进一样,都想大步跨入***主义社会。现在的软件公司、客户、政府、学校、培训机构等等到处弥漫着浮躁之气。就拿我比较熟悉的大连大工IT职业培训来说吧,居然打广告宣称"20多年的计算机职业教育,辽宁省十佳学校",殊不知中国计算机发展才几年,软件发展才几年,居然去报名的学生不少,简直是藐视天下程序员。培训出来的“程序员”大多不知道OO,OP为何物?社会环境如是,我们不能改变,只能改变自己,闹市中的安宁,弥足珍贵。许多初学者C++/Java没开始学,立马使用VC/JBuilder,会使用VC/JBuilder开发一个Hello World程序,就忙不迭的向世界宣告,"我会软件开发了",简历上也大言不惭地写上"精通VC/Java"。结果到软件公司面试时要么被三两下打发走了,要么被驳的体无完肤,无地自容。到处碰壁之后才知道捧起《C++编程思想》《Java编程思想》仔细钻研,早知如此何必当初呀。

"你现在讲究简单方便,你以后的路就长了",好象也是佛经中的劝戒。

1.4 多实践,快实践

彭端淑的《为学一首示子侄》中有穷和尚与富和尚的故事。

从前,四川边境有两个和尚,一个贫穷,一个有钱。一天,穷和尚对富和尚说:"我打算去南海朝圣,你看怎么样?"富和尚说:"这里离南海有几千里远,你靠什么去呢?"穷和尚说:"我只要一个水钵,一个饭碗就够了。"富和尚为难地说:"几年前我就打算买条船去南海,可至今没去成,你还是别去吧!" 一年以后,富和尚还在为租赁船只筹钱,穷和尚却已经从南海朝圣回来了。

这个故事可解读为:任何事情,一旦考虑好了,就要马上上路,不要等到准备周全之后,再去干事情。假如事情准备考虑周全了再上路的话,别人恐怕捷足先登了。软件开发是一门工程学科,注重的就是实践,"君子动口不动手"对软件开发人员来讲根本就是错误的,他们提倡"动手至上",但别害怕,他们大多温文尔雅,没有暴力倾向,虽然有时候蓬头垢面的一副"比尔盖茨"样。有前辈高人认为,学习编程的秘诀是:编程、编程、再编程,笔者深表赞同。不仅要多实践,而且要快实践。我们在看书的时候,不要等到你完全理解了才动手敲代码,而是应该在看书的同时敲代码,程序运行的各种情况可以让你更快更牢固的掌握知识点。

1.5 多参考程序代码

程序代码是软件开发最重要的成果之一,其中渗透了程序员的思想与灵魂。许多人被《仙剑奇侠传》中凄美的爱情故事感动,悲剧的结局更有一种缺憾美。为什么要以悲剧结尾?据说是因为写《仙剑奇侠传》的程序员失恋而安排了这样的结局,他把自己的感觉融入到游戏中,却让众多的仙剑迷扼腕叹息。

多多参考代码例子,对Java而言有参考文献[4.3],有API类的源代码(JDK安装目录下的src.zip文件),也可以研究一些开源的软件或框架。

1.6 加强英文阅读能力

对学习编程来说,不要求英语, 但不能一点不会,。最起码像Java API文档(参考文献[4.4])这些东西还是要能看懂的,连猜带懵都可以;旁边再开启一个"金山词霸"。看多了就会越来越熟练。在学Java的同时学习英文,一箭双雕多好。另外好多软件需要到英文网站下载,你要能够找到它们,这些是最基本的要求。英语好对你学习有很大的帮助。口语好的话更有机会进入管理层,进而可以成为剥削程序员的"周扒皮"。

1.7 万不得已才请教别人

笔者在Martix与Java论坛的在线辅导系统中解决学生问题时发现,大部分的问题学生稍做思考就可以解决。请教别人之前,你应该先回答如下几个问题。

你是否在google中搜索了问题的解决办法?

你是否查看了Java API文档?

你是否查找过相关书籍?

你是否写代码测试过?

如果回答都是"是"的话,而且还没有找到解决办法,再问别人不迟。要知道独立思考的能力对你很重要。要知道程序员的时间是很宝贵的。

1.8 多读好书

书中自有颜如玉。比尔盖茨是一个饱读群书的人。虽然没有读完大学,但九岁的时候比尔盖茨就已经读完了所有的百科全书,所以他精通天文、历史、地理等等各类学科,可以说比尔?茨不仅是当今世界上金钱的首富,而且也可以称得上是知识的巨富。

笔者在给学生上课的时候经常会给他们推荐书籍,到后来学生实在忍无可忍开始抱怨,"天呐,这么多书到什么时候才能看完了","学软件开发,感觉上了贼船"。这时候,我的回答一般是,"别着急,什么时候带你们去看看我的书房,到现在每月花在技术书籍上的钱400元,这在软件开发人员之中还只能够算是中等的",学生当场晕倒。(注:这一部分学生是刚学软件开发的)

1.9 使用合适的工具

工欲善其事必先利其器。软件开发包含各种各样的活动,需求收集分析、建立用例模型、建立分析设计模型、编程实现、调试程序、自动化测试、持续集成等等,没有工具帮忙可以说是寸步难行。工具可以提高开发效率,使软件的质量更高BUG更少。组合称手的武器。到飞花摘叶皆可伤人的境界就很高了,无招胜有招,手中无剑心中有剑这样的境界几乎不可企及。

2.软件开发学习路线

两千多年的儒家思想孔孟之道,中庸的思想透入骨髓,既不冒进也不保守并非中庸之道,而是找寻学习软件开发的正确路线与规律。

从软件开发人员的生涯规划来讲,我们可以大致分为三个阶段,软件工程师→软件设计师→架构设计师或项目管理师。不想当元帅的士兵不是好士兵,不想当架构设计师或项目管理师的程序员也不是好的程序员。我们应该努力往上走。让我们先整理一下开发应用软件需要学习的主要技术。

A.基础理论知识,如操作系统、编译原理、数据结构与算法、计算机原理等,它们并非不重要。如不想成为计算机科学家的话,可以采取"用到的时候再来学"的原则。

B.一门编程语言,现在基本上都是面向对象的语言,Java/C++/C#等等。如果做WEB开发的话还要学习HTML/JavaScript等等。

C.一种方法学或者说思想,现在基本都是面向对象思想(OOA/OOD/设计模式)。由此而衍生的基于组件开发CBD/面向方面编程AOP等等。

D.一种关系型数据库,ORACLE/SqlServer/DB2/MySQL等等

E.一种提高生产率的IDE集成开发环境JBuilder/Eclipse/VS.NET等。

F.一种UML建模工具,用ROSE/VISIO/钢笔进行建模。

G.一种软件过程,RUP/XP/CMM等等,通过软件过程来组织软件开发的众多活动,使开发流程专业化规范化。当然还有其他的一些软件工程知识。

H.项目管理、体系结构、框架知识。

正确的路线应该是:B→C→E→F→G→H。

还需要补充几点:

1).对于A与C要补充的是,我们应该在实践中逐步领悟编程理论与编程思想。新技术虽然不断涌现,更新速度令人眼花燎乱雾里看花;但万变不离其宗,编程理论与编程思想的变化却很慢。掌握了编程理论与编程思想你就会有拨云见日之感。面向对象的思想在目前来讲是相当关键的,是强势技术之一,在上面需要多投入时间,给你的回报也会让你惊喜。

2).对于数据库来说是独立学习的,这个时机就由你来决定吧。

3).编程语言作为学习软件开发的主线,而其余的作为辅线。

4).软件工程师着重于B、C、E、D;软件设计师着重于B、C、E、D、F;架构设计师着重于C、F、H。

3.如何学习Java?

3.1 Java学习路线

3.1.1 基础语法及Java原理

基础语法和Java原理是地基,地基不牢靠,犹如沙地上建摩天大厦,是相当危险的。学习Java也是如此,必须要有扎实的基础,你才能在J2EE、J2ME领域游刃有余。参加SCJP(SUN公司认证的Java程序员)考试不失为一个好方法,原因之一是为了对得起你交的1200大洋考试费,你会更努力学习,原因之二是SCJP考试能够让你把基础打得很牢靠,它要求你跟JDK一样熟悉Java基础知识;但是你千万不要认为考过了SCJP就有多了不起,就能够获得软件公司的青睐,就能够获取高薪,这样的想法也是很危险的。获得"真正"的SCJP只能证明你的基础还过得去,但离实际开发还有很长的一段路要走。

3.1.2 OO思想的领悟

掌握了基础语法和Java程序运行原理后,我们就可以用Java语言实现面向对象的思想了。面向对象,是一种方法学;是独立于语言之外的编程思想;是CBD基于组件开发的基础;属于强势技术之一。当以后因工作需要转到别的面向对象语言的时候,你会感到特别的熟悉亲切,学起来像喝凉水这么简单。

使用面向对象的思想进行开发的基本过程是:

●调查收集需求。

●建立用例模型。

●从用例模型中识别分析类及类与类之间的静态动态关系,从而建立分析模型。

●细化分析模型到设计模型。

●用具体的技术去实现。

●测试、部署、总结。

3.1.3 基本API的学习

进行软件开发的时候,并不是什么功能都需要我们去实现,也就是经典名言所说的"不需要重新发明轮子"。我们可以利用现成的类、组件、框架来搭建我们的应用,如SUN公司编写好了众多类实现一些底层功能,以及我们下载过来的JAR文件中包含的类,我们可以调用类中的方法来完成某些功能或继承它。那么这些类中究竟提供了哪些方法给我们使用?方法的参数个数及类型是?类的构造器需不需要参数?总不可能SUN公司的工程师打国际长途甚至飘洋过海来告诉你他编写的类该如何使用吧。他们只能提供文档给我们查看,Java DOC文档(参考文献4.4)就是这样的文档,它可以说是程序员与程序员交流的文档。

基本API指的是实现了一些底层功能的类,通用性较强的API,如字符串处理/输入输出等等。我们又把它成为类库。熟悉API的方法一是多查Java DOC文档(参考文献4.4),二是使用JBuilder/Eclipse等IDE的代码提示功能。

3.1.4 特定API的学习

Java介入的领域很广泛,不同的领域有不同的API,没有人熟悉所有的API,对一般人而言只是熟悉工作中要用到的API。如果你做界面开发,那么你需要学习Swing/AWT/SWT等API;如果你进行网络游戏开发,你需要深入了解网络API/多媒体API/2D3D等;如果你做WEB开发,就需要熟悉Servlet等API啦。总之,需要根据工作的需要或你的兴趣发展方向去选择学习特定的API。

3.1.5 开发工具的用法

在学习基础语法与基本的面向对象概念时,从锻炼语言熟练程度的角度考虑,我们推荐使用的工具是Editplus/JCreator+JDK,这时候不要急于上手JBuilder/Eclipse等集成开发环境,以免过于关注IDE的强大功能而分散对Java技术本身的注意力。过了这一阶段你就可以开始熟悉IDE了。

程序员日常工作包括很多活动,编辑、编译及构建、调试、单元测试、版本控制、维持模型与代码同步、文档的更新等等,几乎每一项活动都有专门的工具,如果独立使用这些工具的话,你将会很痛苦,你需要在堆满工具的任务栏上不断的切换,效率很低下,也很容易出错。在JBuilder、Eclipse等IDE中已经自动集成编辑器、编译器、调试器、单元测试工具JUnit、自动构建工具ANT、版本控制工具CVS、DOC文档生成与更新等等,甚至可以把UML建模工具也集成进去,又提供了丰富的向导帮助生成框架代码,让我们的开发变得更轻松。应该说IDE发展的趋势就是集成软件开发中要用到的几乎所有工具。

从开发效率的角度考虑,使用IDE是必经之路,也是从一个学生到一个职业程序员转变的里程碑。

Java开发使用的IDE主要有Eclipse、JBuilder、JDeveloper、NetBeans等几种;而Eclipse、JBuilder占有的市场份额是最大的。JBuilder在近几年来一直是Java集成开发环境中的霸主,它是由备受程序员尊敬的Borland公司开发,在硝烟弥漫的Java IDE大战中,以其快速的版本更新击败IBM的Visual Age for Java等而成就一番伟业。IBM在Visual Age for Java上已经无利可图之下,干脆将之贡献给开源社区,成为Eclipse的前身,真所谓"柳暗花明又一村"。浴火重生的Eclipse以其开放式的插件扩展机制、免费开源获得广大程序员(包括几乎所有的骨灰级程序员)的青睐,极具发展潜力。

3.1.6 学习软件工程

对小型项目而言,你可能认为软件工程没太大的必要。随着项目的复杂性越来越高,软件工程的必要性才会体现出来。参见"软件开发学习路线"小节。

3.2学习要点

确立的学习路线之后,我们还需要总结一下Java的学习要点,这些要点在前文多多少少提到过,只是笔者觉得这些地方特别要注意才对它们进行汇总,不要嫌我婆婆妈妈啊。

3.2.1勤查API文档

当程序员编写好某些类,觉得很有成就感,想把它贡献给各位苦难的同行。这时候你要使用"Javadoc"工具(包含在JDK中)生成标准的Java DOC文档,供同行使用。J2SE/J2EE/J2ME的DOC文档是程序员与程序员交流的工具,几乎人手一份,除了菜鸟之外。J2SE DOC文档官方下载地址:http://Java.sun.com/j2se/1.5.0/download.jsp,你可以到google搜索CHM版本下载。也可以在线查看:http://Java.sun.com/j2se/1.5.0/docs/api/index.html

对待DOC文档要像毛主席语录,早上起床念一遍,吃饭睡觉前念一遍。

当需要某项功能的时候,你应该先查相应的DOC文档看看有没有现成的实现,有的话就不必劳神费心了直接用就可以了,找不到的时候才考虑自己实现。使用步骤一般如下:

●找特定的包,包一般根据功能组织。

●找需要使用类,类命名规范的话我们由类的名字可猜出一二。

●选择构造器,大多数使用类的方式是创建对象。

●选择你需要的方法。

3.2.2 查书/google->写代码测试->查看源代码->请教别人

当我们遇到问题的时候该如何解决?

这时候不要急着问别人,太简单的问题,没经过思考的问题,别人会因此而瞧不起你。可以先找找书,到google中搜一下看看,绝大部分问题基本就解决了。而像"某些类/方法如何使用的问题",DOC文档就是答案。对某些知识点有疑惑是,写代码测试一下,会给你留下深刻的印象。而有的问题,你可能需要直接看API的源代码验证你的想法。万不得已才去请教别人。

3.2.3学习开源软件的设计思想

Java领域有许多源代码开放的工具、组件、框架,JUnit、ANT、Tomcat、Struts、Spring、Jive论坛、PetStore宠物店等等多如牛毛。这些可是前辈给我们留下的瑰宝呀。入宝山而空手归,你心甘吗?对这些工具、框架进行分析,领会其中的设计思想,有朝一日说不定你也能写一个XXX框架什么的,风光一把。分析开源软件其实是你提高技术、提高实战能力的便捷方法。

3.2.4 规范的重要性

没有规矩,不成方圆。这里的规范有两层含义。第一层含义是技术规范,多到http://www.jcp.org下载JSRXXX规范,多读规范,这是最权威准确最新的教材。第二层含义是编程规范,如果你使用了大量的独特算法,富有个性的变量及方法的命名方式;同时,没给程序作注释,以显示你的编程功底是多么的深厚。这样的代码别人看起来像天书,要理解谈何容易,更不用说维护了,必然会被无情地扫入垃圾堆。Java编码规范到此查看或下载http://Java.sun.com/docs/codeconv/,中文的也有,啊,还要问我在哪,请参考3.2.2节。

3.2.5 不局限于Java

很不幸,很幸运,要学习的东西还有很多。不幸的是因为要学的东西太多且多变,没时间陪老婆家人或女朋友,导致身心疲惫,严重者甚至导致抑郁症。幸运的是别人要抢你饭碗绝非易事,他们或她们需要付出很多才能达成心愿。

Java不要孤立地去学习,需要综合学习数据结构、OOP、软件工程、UML、网络编程、数据库技术等知识,用横向纵向的比较联想的方式去学习会更有效。如学习Java集合的时候找数据结构的书看看;学JDBC的时候复习数据库技术;采取的依然是"需要的时候再学"的原则。

4.结束语

需要强调的是,学习软件开发确实有一定的难度,也很辛苦,需要付出很多努力,但千万不要半途而废。本文如果能对一直徘徊在Java神殿之外的朋友有所帮助的话,笔者也欣慰了。哈哈,怎么听起来老气横秋呀?没办法,在电脑的长期辐射之下,都快变成小老头了。最后奉劝各位程序员尤其是MM程序员,完成工作后赶快远离电脑,据《胡播乱报》报道,电脑辐射会在白皙的皮肤上面点缀一些小黑点,看起来鲜艳无比…… (文章提到的大连大工IT培训。本人没有任何攻击的意思)

4月24日

Eclipse及其插件介绍和下载

一:Eclipse及其插件介绍和下载

0.Eclipse下载
EMF,GEF - Graphical Editor Framework,UML2,VE - Visual Editor都在这里下载
http://www.eclipse.org/downloads/index.php
 
0.5.lomboz J2EE插件,开发JSP,EJB
http://forge.objectweb.org/projects/lomboz
1.MyEclipse J2EE开发插件,支持SERVLET/JSP/EJB/数据库操纵等
http://www.myeclipseide.com
 
2.Properties Editor  编辑java的属性文件,并可以自动存盘为Unicode格式
http://propedit.sourceforge.jp/index_en.html
 
3.Colorer Take  为上百种类型的文件按语法着色
http://colorer.sourceforge.net/
 
4.XMLBuddy 编辑xml文件
http://www.xmlbuddy.com
 
5.Code Folding  加入多种代码折叠功能(比eclipse自带的更多)
http://www.coffee-bytes.com/servlet/PlatformSupport
 
6.Easy Explorer  从eclipse中访问选定文件、目录所在的文件夹
http://easystruts.sourceforge.net/
 
7.Fat Jar 打包插件,可以方便的完成各种打包任务,可以包含外部的包等
http://fjep.sourceforge.net/
 
8.RegEx Test 测试正则表达式
http://brosinski.com/stephan/archives/000028.php
 
9.JasperAssistant 报表插件(强,要钱的)
http://www.jasperassistant.com/
 
10.Jigloo GUI Builder JAVA的GUI编辑插件
http://cloudgarden.com/jigloo/
 
11.Profiler 性能跟踪、测量工具,能跟踪、测量BS程序
http://sourceforge.net/projects/eclipsecolorer/
 
12.AdvanQas 提供对if/else等条件语句的提示和快捷帮助(自动更改结构等)
http://eclipsecolorer.sourceforge.net/advanqas/index.html
 
13.Log4E Log4j插件,提供各种和Log4j相关的任务,如为方法、类添加一个logger等
http://log4e.jayefem.de/index.php/Main_Page
 
14.VSSPlugin VSS插件
http://sourceforge.net/projects/vssplugin
 
15.Implementors 提供跳转到一个方法的实现类,而不是接中的功能(实用!)
http://eclipse-tools.sourceforge.net/implementors/
 
16.Call Hierarchy 显示一个方法的调用层次(被哪些方法调,调了哪些方法)
http://eclipse-tools.sourceforge.net/call-hierarchy/index.html
 
17.EclipseTidy 检查和格式化HTML/XML文件
http://eclipsetidy.sourceforge.net/
 
18.Checkclipse 检查代码的风格、写法是否符合规范
http://www.mvmsoft.de/content/plugins/checkclipse/checkclipse.htm
 
19.Hibernate Synchronizer Hibernate插件,自动映射等
http://www.binamics.com/hibernatesync/
 
20.VeloEclipse  Velocity插件
http://propsorter.sourceforge.net/
 
21.EditorList 方便的列出所有打开的Editor
http://editorlist.sourceforge.net/
 
22.MemoryManager 内存占用率的监视
http://cloudgarden.com/memorymanager/
 
23.swt-designer java的GUI插件
http://www.swt-designer.com/
 
24.TomcatPlugin 支持Tomcat插件
http://www.sysdeo.com/eclipse/tomcatPlugin.html
 
25.XML Viewer
http://tabaquismo.freehosting.net/ignacio/eclipse/xmlview/index.html
 
26.quantum 数据库插件
http://quantum.sourceforge.net/
 
27.Dbedit 数据库插件
http://sourceforge.net/projects/dbedit
 
28.clay.core 可视化的数据库插件
http://www.azzurri.jp/en/software/index.jsp
http://www.azzurri.jp/eclipse/plugins
 
29.hiberclipse hibernate插件
http://hiberclipse.sourceforge.net
http://www.binamics.com/hibernatesync
 
30.struts-console Struts插件
http://www.jamesholmes.com/struts/console/
 
31.easystruts Struts插件
http://easystruts.sourceforge.net
 
32.veloedit Velocity插件
http://veloedit.sourceforge.net/
 
33.jalopy 代码整理插件
http://jalopy.sourceforge.net/
 
34.JDepend 包关系分析
http://andrei.gmxhome.de/jdepend4eclipse/links.html
 
35.Spring IDE Spring插件
http://springide-eclip.sourceforge.net/updatesite/
 
36.doclipse 可以产生xdoclet 的代码提示
http://beust.com/doclipse/
0.Eclipse下载
EMF,GEF - Graphical Editor Framework,UML2,VE - Visual Editor都在这里下载
http://www.eclipse.org/downloads/index.php
 
0.5.lomboz J2EE插件,开发JSP,EJB
http://forge.objectweb.org/projects/lomboz
1.MyEclipse J2EE开发插件,支持SERVLET/JSP/EJB/数据库操纵等
http://www.myeclipseide.com
 
2.Properties Editor  编辑java的属性文件,并可以自动存盘为Unicode格式
http://propedit.sourceforge.jp/index_en.html
 
3.Colorer Take  为上百种类型的文件按语法着色
http://colorer.sourceforge.net/
 
4.XMLBuddy 编辑xml文件
http://www.xmlbuddy.com
 
5.Code Folding  加入多种代码折叠功能(比eclipse自带的更多)
http://www.coffee-bytes.com/servlet/PlatformSupport
 
6.Easy Explorer  从eclipse中访问选定文件、目录所在的文件夹
http://easystruts.sourceforge.net/
 
7.Fat Jar 打包插件,可以方便的完成各种打包任务,可以包含外部的包等
http://fjep.sourceforge.net/
 
8.RegEx Test 测试正则表达式
http://brosinski.com/stephan/archives/000028.php
 
9.JasperAssistant 报表插件(强,要钱的)
http://www.jasperassistant.com/
 
10.Jigloo GUI Builder JAVA的GUI编辑插件
http://cloudgarden.com/jigloo/
 
11.Profiler 性能跟踪、测量工具,能跟踪、测量BS程序
http://sourceforge.net/projects/eclipsecolorer/
 
12.AdvanQas 提供对if/else等条件语句的提示和快捷帮助(自动更改结构等)
http://eclipsecolorer.sourceforge.net/advanqas/index.html
 
13.Log4E Log4j插件,提供各种和Log4j相关的任务,如为方法、类添加一个logger等
http://log4e.jayefem.de/index.php/Main_Page
 
14.VSSPlugin VSS插件
http://sourceforge.net/projects/vssplugin
 
15.Implementors 提供跳转到一个方法的实现类,而不是接中的功能(实用!)
http://eclipse-tools.sourceforge.net/implementors/
 
16.Call Hierarchy 显示一个方法的调用层次(被哪些方法调,调了哪些方法)
http://eclipse-tools.sourceforge.net/call-hierarchy/index.html
 
17.EclipseTidy 检查和格式化HTML/XML文件
http://eclipsetidy.sourceforge.net/
 
18.Checkclipse 检查代码的风格、写法是否符合规范
http://www.mvmsoft.de/content/plugins/checkclipse/checkclipse.htm
 
19.Hibernate Synchronizer Hibernate插件,自动映射等
http://www.binamics.com/hibernatesync/
 
20.VeloEclipse  Velocity插件
http://propsorter.sourceforge.net/
 
21.EditorList 方便的列出所有打开的Editor
http://editorlist.sourceforge.net/
 
22.MemoryManager 内存占用率的监视
http://cloudgarden.com/memorymanager/
 
23.swt-designer java的GUI插件
http://www.swt-designer.com/
 
24.TomcatPlugin 支持Tomcat插件
http://www.sysdeo.com/eclipse/tomcatPlugin.html
 
25.XML Viewer
http://tabaquismo.freehosting.net/ignacio/eclipse/xmlview/index.html
 
26.quantum 数据库插件
http://quantum.sourceforge.net/
 
27.Dbedit 数据库插件
http://sourceforge.net/projects/dbedit
 
28.clay.core 可视化的数据库插件
http://www.azzurri.jp/en/software/index.jsp
http://www.azzurri.jp/eclipse/plugins
 
29.hiberclipse hibernate插件
http://hiberclipse.sourceforge.net
http://www.binamics.com/hibernatesync
 
30.struts-console Struts插件
http://www.jamesholmes.com/struts/console/
 
31.easystruts Struts插件
http://easystruts.sourceforge.net
 
32.veloedit Velocity插件
http://veloedit.sourceforge.net/
 
33.jalopy 代码整理插件
http://jalopy.sourceforge.net/
 
34.JDepend 包关系分析
http://andrei.gmxhome.de/jdepend4eclipse/links.html
 
35.Spring IDE Spring插件
http://springide-eclip.sourceforge.net/updatesite/
 
36.doclipse 可以产生xdoclet 的代码提示
http://beust.com/doclipse/

二:

After installing Eclipse 3 and Tomcat Plugin,the plugin is not loaded!

Lanching Eclipse 3 with "-clean" option can fix this problem.

Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=540513


Eclips使用秘技(绝对经典)

Eclipse优点:免费、程序代码排版功能、有中文化包、可增 

设许多功能强大的外挂、支持多种操作系统(Windows、 

Linux、Solaris、Mac OSX)..等等。 

 

开此篇讨论串的目的,是希望能将Eclipse的一些使用技巧集 

合起来...欢迎大家继续补充下去...由于Eclipse的版本众多 

,希望补充的先进们能顺便说明一下您所使用的版本~ 

 

Eclipse网站:http://www.eclipse.org/ ;

Eclipse中文化教学:JavaWorld站内文章参考 

 

(使用版本:Eclipse 2.1.2 Release + 中文化) 

 

热键篇: 

Template:Alt + / 

修改处:窗口->喜好设定->工作台->按键->编辑->内容辅助。 

个人习惯:Shift+SPACE(空白)。 

简易说明:编辑程序代码时,打sysout +Template启动键,就 

会自动出现:System.out.println(); 。 

设定Template的格式:窗口->喜好设定->Java->编辑器->模板。 

 

程序代码自动排版:Ctrl+Shift+F 

修改处:窗口->喜好设定->工作台->按键->程序代码->格式。 

个人习惯:Alt+Z。 

自动排版设定:窗口->喜好设定->Java->程序代码格式制作程序。 

样式页面->将插入tab(而非空格键)以内缩,该选项取消勾选 

,下面空格数目填4,这样在自动编排时会以空格4作缩排。 

 

快速执行程序:Ctrl + F11 

个人习惯:ALT+X 

修改处:窗口->喜好设定->工作台->按键->执行->启动前一次的启动作业。 

简易说明:第一次执行时,它会询问您执行模式, 

设置好后,以后只要按这个热键,它就会快速执行。 

<ALT+Z(排版完)、ATL+X(执行)>..我觉得很顺手^___^ 

 

自动汇入所需要的类别:Ctrl+Shift+O 

简易说明: 

假设我们没有Import任何类别时,当我们在程序里打入: 

 

BufferedReader buf = 

new BufferedReader(new InputStreamReader(System.in)); 

 

此时Eclipse会警示说没有汇入类别,这时我们只要按下Ctrl+Shift+O 

,它就会自动帮我们Import类别。 

 

查看使用类别的原始码:Ctrl+鼠标左键点击 

简易说明:可以看到您所使用类别的原始码。 

 

将选取的文字批注起来:Ctrl+/ 

简易说明:Debug时很方便。 

修改处:窗口->喜好设定->工作台->按键->程序代码->批注 

 

视景切换:Ctrl+F8 

个人习惯:Alt+S。 

修改处:窗口->喜好设定->工作台->按键->窗口->下一个视景。 

简易说明:可以方便我们快速切换编辑、除错等视景。 

 

密技篇: 

一套Eclipse可同时切换,英文、繁体、简体显示: 

1.首先要先安装完中文化包。 

2.在桌面的快捷方式后面加上参数即可, 

英文-> -nl "zh_US" 

繁体-> -nl "zh_TW" 

简体-> -nl "zh_CN"。 

(其它语系以此类推) 

像我2.1.2中文化后,我在我桌面的Eclipse快捷方式加入参数-n1 "zh_US"。 

"C:Program Fileseclipseeclipse.exe" -n "zh_US" 

接口就会变回英文语系噜。 

 

利用Eclipse,在Word编辑文书时可不必将程序代码重新编排: 

将Eclipse程序编辑区的程序代码整个复制下来(Ctrl+C),直接贴(Ctrl+V)到 

Word或WordPad上,您将会发现在Word里的程序代码格式,跟Eclipse 

所设定的完全一样,包括字型、缩排、关键词颜色。我曾试过JBuilder 

、GEL、NetBeans...使用复制贴上时,只有缩排格式一样,字型、颜 

色等都不会改变。 

 

外挂篇: 

外挂安装:将外挂包下载回来后,将其解压缩后,您会发现features、 

plugins这2个数据夹,将里面的东西都复制或移动到Eclipse的features 

、plugins数据夹内后,重新启动Eclipse即可。 

 

让Eclipse可以像JBuilderX一样使用拖拉方式建构GUI的外挂: 

1.Jigloo SWT/Swing GUI Builder : 

http://cloudgarden.com/jigloo/index.html ;

下载此版本:Jigloo plugin for Eclipse (using Java 1.4 or 1.5) 

安装后即可由档案->新建->其它->GUI Form选取要建构的GUI类型。 

 

2.Eclipse Visual Editor Project: 

http://www.eclipse.org/vep/ ;

点选下方Download Page,再点选Latest Release 0.5.0进入下载。 

除了VE-runtime-0.5.0.zip要下载外,以下这2个也要: 

EMF build 1.1.1: (build page) (download zip) 

GEF Build 2.1.2: (build page) (download zip) 

 

3.0 M8版本,请下载: 

EMF build I200403250631 

GEF Build I20040330 

VE-runtime-1.0M1 

 

安装成功后,便可由File->New->Visual Class开始UI设计。 

安装成功后,即可由新建->Java->AWT与Swing里选择 

所要建构的GUI类型开始进行设计。VE必须配合着对应 

版本,才能正常使用,否则即使安装成功,使用上仍会 

有问题。 

 

使用Eclipse来开发JSP程序: 

外挂名称:lomboz(下载页面) 

http://forge.objectweb.org/project/showfiles.php?group_id=97 ;

请选择适合自己版本的lomboz下载,lomboz.212.p1.zip表示2.1.2版, 

lomboz.3m7.zip表示M7版本....以此类推。 

lomboz安装以及设置教学: 

Eclipse开发JSP-教学文件 

 

Java转exe篇: 

实现方式:Eclipse搭配JSmooth(免费)。 

1.先由Eclipse制作包含Manifest的JAR。 

制作教学 

2.使用JSmooth将做好的JAR包装成EXE。 

JSmooth下载页面: 

http://jsmooth.sourceforge.net/index.php ;

3.制作完成的exe文件,可在有装置JRE的Windows上执行。 

 

Eclipse-Java编辑器最佳设定: 

编辑器字型设定:工作台->字型->Java编辑器文字字型。 

(建议设定Courier New -regular 10) 

 

编辑器相关设定:窗口->喜好设定->Java->编辑器 

 

外观:显示行号、强调对称显示的方括号、强调显示现行行、 

显示打印边距,将其勾选,Tab宽度设4,打印编距字段设80。 

程序代码协助:采预设即可。 

语法:可设定关键词、字符串等等的显示颜色。 

附注:采预设即可。 

输入:全部字段都勾选。 

浮动说明:采预设即可。 

导览:采预设即可。 

 

使自动排版排出来的效果,最符合Java设计惯例的设定: 

自动排版设定:窗口->喜好设定->Java->程序代码制作格式。 

 

换行:全部不勾选。 

分行:行长度上限设:80。 

样式:只将强制转型后插入空白勾选。 

内缩空格数目:设为4。 

 

Eclipse的教学文件: 

Eclipse 3.0系列热键表 - 中英对照解说版 (by sungo) ~New~ 

Window+GCC+CDT用Eclipse开发C、C++ (by sungo) ~New~ 

 

其它: 

扩充Eclipse的Java 开发工具(中文) 

使用Eclipse开发J2EE 应用程序(中文) 

使用Eclipse平台进行除错(中文) 

用Eclipse进行XML 开发(中文) 

开发Eclipse外挂程序(中文) 

国际化您的Eclipse外挂程序(英文) 

将Swing编辑器加入Eclipse(英文) 

如何测试你的Eclipse plug-in符合国际市场需求(英文) 

 

Eclipse的相关网站: 

http://eclipse-plugins.2y.net/eclipse/index.jsp ;

http://www.eclipseplugincentral.com/ ;

Eclipse相关教学[简体] 

 

  

 

  

 

写程序写到很累了,想休息一下??玩玩小Game是 

不错的选择,下面介绍使用Eclipse玩Game的Plug-in。 

 

补充外挂篇: 

Eclipse-Games: 

http://eclipse-games.sourceforge.net/ ;

版本选:Latest Release 3.0.1 (Release Notes) Sat, 3 Jan 2004 

 

外挂安装完后,重新开启Eclipse。 

窗口->自订视景->其它->勾选Game Actions。 

再将Eclipse关闭,重新再启动,就可以开始玩噜。 

(共有4种:采地雷I、采地雷II、贪食蛇、仓库番。) 

 

(Eclipse 2.1.2 +中文化 玩Game -仓库番)   

 

补充:(于Eclipse使用assertion机制) 

Eclipse版本:2.1.3 release。 

 

JDK1.4版新加入的assertion机制(关键词:assert),由于JDK1.4编译器 

预设是兼容1.3,所以要使用assert必须在编译时加上-source 1.4的参数。 

 

C:>javac -source 1.4 XXX.java

执行时则必须加-ea 或-enableassertions参数启动。 

 

C:>java -ea XXX

 

如要在Eclipse中使用assertion机制,请作以下设定: 

设定一:(编译设定) 

Windows->Preferance->Java->Compiler->Compliance and Classfiles 

页面。将..JDK Compliance level->Compiler compliance level调成1.4。 

 

设定二:(执行设定) 

Run->Run->(x)=Arguments页面,在VM arguments加入-da参数,按下 

Run button便可看到启动assertion后的执行结果。 

 

(Eclipse 2.1.3 release + assertion测试) 

<assert判别为false,所以show出AssertionError>   

 

新版(m8+)的eclipse可以设vm arguments 

另外提供一种设法,是在eclipse启动时加入vm arguments(跟加大eclipse预设内存大小的方式一样) 

这样就不用每次run都得需去设vm arguments  
 



更多资源:
参与论坛讨论: http://www.java-cn.com/forum/index.jsp
更多技术文章: http://www.java-cn.com/technology/index.jsp
更多JAVA博客: http://www.54bk.com/
JAVA中文站:  我们要做中国最好的JAVA门户网站,记住我们的域名: JAVA-CN.COM | JAVA-CN.NET | JAVA-CN.ORG
本文网址: http://www.java-cn.com/technology/technology_detail.jsp?id=4015
声    明: 转载此文章,须在显著位置注明 JAVA中文站 的原文网址;
          若是本站的转载文章,请标明原文出处,并保留作者信息

Eclipse下J2ME开发环境的配置

Java~J2ME安装文件说明:

jdk1.5.4.exe-------------------------------Java运行环境安装文件
eclipsesdk_win32.zip-----------------------eclipse安装文件
eclipse3.2.1中文包编程工具.zip--------------eclipse汉化包
eclipseme.feature_1.6.0_site.zip---------eclipse的J2ME插件安装文件
eclipseme.feature_1.6.0.src.zip-----eclipse的J2ME插件源代码(不用)
proguard3.7.zip----------------------------------混淆器
j2me_wireless_toolkit-2_2-ml-windows.exe----------WTK模拟器安装
j2me_wireless_toolkit-2_2-update_1-ml-windows.zip---WTK更新
j2me_wireless_toolkit-2_2-update_2-ml-windows.zip-------WTK更新
Kwyshell MidpX Emulator手机Java模拟器.rar----------JAR文件模拟器
手机顽童.rar------------------------------------JAR文件模拟器

配置eclipse

1. 安装jdk1.5.4.exe(安装在任何目录都可),同时配置Java环境(可以不配置,配置后可以使用JDK工具包内

工具)

2. 安装j2me_wireless_toolkit-2_2-ml-windows.exe (WTK模拟器),按顺序解压两个更新包到所装目录上

3. 解压proguard3.7.zip到某个目录下

4. 解压eclipsesdk_win32.zip(绿色程序)到一个目录,不要这时就打开eclipse,先解压eclipse汉化包,到

该目录下,否则会导致只有部分内容被汉化

5. 运行eclipse,配置自己学习的工作路径,选择 帮助—>软件更新—>查找并安装—>搜索要安装的新功能部件

6. 选择 新建已归档的站点,选择eclipseme.feature_1.6.0_site.zip文件,安装,重启eclipse

7. 选择 首选项—>J2ME—>Device Management—>Import 在Specify search directory下,选择你所装的WTK模

拟器的目录

8. 选择 Refresh ,后按完成,使用DefaultColorPhone为默认模式,这时已为eclipse配置上了模拟器

9. 再到 “首选项”找到,J2ME—>Packaging—>obfuscation,在Proguard Root Directory右边的框中,选择

刚才解压的proguard3.7的文件夹,应用,这时为eclipse配置上了混淆器

10. 再到 “首选项”找到,Java—>调试,将“发生未不捕获到的异常时暂挂执行”与“在发生编译错误时暂挂

执行”这两个选项调为“未选中”状态,再把下面的调试器超时(毫秒)的右侧数值设置为15000

11. 配置完毕

使用eclipse进行开发控制台程序

1. 新建 Java项目,填写项目名,完成

2. 新建 类,填写名称,选中public static void main(String[] args) 完成

3. 在填写代码区,System.out.print(“hello”);

4. 在快捷键找到“运行”,选择“Java应用程序”,按“新建启动配置”(或双击Java应用程序),为该程序

新建一个启动的配置,填写Main类

5. 这时在JAVA应用下,出现“新建配置”,运行

6. 在代码输入区下方的控制台标签下,显示”hello”,完成程序


使用eclipse进行J2ME开发

1. 新建 J2ME下的J2ME Midlet Suite,填写项目名,下一步,完成

2. 新建 J2ME下的J2ME Midlet,填写名称,完成

3. 点击运行,新建一个Wireless Toolkit Emulator的运行配置,运行,出现手机样式,运行成功


在WTK上运行所编写的J2ME程序

1. 运行“开始”-“所有程序”-“J2ME Wireless Toolkit 2.2”-“KToolbar”程序

2. 新建项目,填写项目名字,和MIDlet类名(必须为继承MIDlet的那个类的名字),确定

3. 到该程序目录下的apps目录中,找到你所建立的项目目录,把程序文件放在src目录下,然后返回KToolbar界

面,选择运行,显示手机样式,运行成功


混淆出错:(参照J2ME中文教程234页)
注意:
很多初次使用的朋友会发现即便指定路径之后,依然不能顺利创建混淆,并且往往得到类
似下面的警告信息。
出现这个错误的原因与JDK 路径有关。我们在安装了JDK 之后(以1.4.2 为例),系统环境
变量中存在两种JDK,一种是JDK SDK,一种是运行时环境(runtime)。Eclipse 在解压安装时选
择的是后者,而启动Proguard3.0.1 需要的是前者。
修复这个问题很简单,在“首选项 / java / 已安装的JRE”中把你的JRE 从指向运行时更

改为指向SDK(即JDK 的安装目录)
此时JRE 将拥有完整JDK 库文件,再次运行创建混淆,我们会发现在Hello World 子目录
deployed 中包括了HelloWorld.jar,HelloWorld_base.jar, HelloWorld_base_obf.jar。他们分别是
混淆后,混淆前等不同版本的jar 包。 

两种免费的Java Obfuscator比较

alilo 发表于 2006-04-07
点击数:799 评论数:6 评价:15/6
关键词:Java Obfuscator 混淆

摘要:

Java的bytecode很容易通过JAD等反编译工具搞出源代码, 目前最有效的保护方法是obfuscate类名和方法名.本文从几个不同的方面比较了两种Free的Java Obfuscator的优缺点..
Java的bytecode很容易通过JAD等反编译工具搞出源代码,  目前最有效的保护方法是obfuscate类名和方法名
注意: 用obfuscate防盗版是根本不可能, 连汇编这种东西都能被破解掉, 破解java代码简直就是小菜
用obfuscate主要是为了保护源代码的知识产权, 别人无法用反编译的源代码做事情.

本文从几个不同的方面比较了两种Free的Java Obfuscator的优缺点..


两种Free的Java Obfuscator: yguardProguard.

yguard: http://www.yworks.com/en/products_yguard_about.htm

Proguard: http://proguard.sourceforge.net/


下面列出两者的不同和优缺点

1. 速度
  yguard的速度远远高于proguard, 主要区别在于proguard不管用没有用, 上来就扫描jre\lib\rt.jar和其他所有用到的library 。  这个回合yguard胜出

2. 是否Opensource以及遵循协议
    yguard: ywork公司的产品,免费使用, 但不是Open source
    Proguard是GPL(这里GPL只是指Proguard本身, 对于obfuscate出来的jar没有限制), 可以从sourceforge.net下载源代码 但是作者并不希望别人参与,没有CVS,所以也无从知道作者的最新进展.  
    这个回合proguard胜出

3. 是否支持Package Name obfuscate
    yguard支持Package Name obfuscate
    Proguard不支持

   这其实是很重要的一个特性, 一个好的Java程序往往在一个package里只有10个以下Class,根据package Name很容易猜出各个Class是干什么的, 比如:
   com.mycompany.license.a   com.mycompany.license.b  com.mycompany.license.c
   虽然obfuscate了Class Name,但是因为这个license只有3个Class,很容易猜出来是干什么的
  但是如果你obfuscate成:
  com.mycompany.a.a  com.mycompany.a.b com.mycompany.a.c
  则大大增加了困难, 尤其大型software, 有几十个package的情况下,  会呈几何级数增加反编译的难度

  这个回合yguard胜出, 但是Proguard已经准备在4.0里推出这个特性

4. 增量obfuscate
proguard支持增量obfuscate , yguard不支持.
也就是在obfuscate a.jar时, 记住所有的名字映射关系(比如MyUtil->a), 然后在obfuscate b.jar的时候, 使用那个映射关系(所有调用MyUtil改成调用a)

   假设a.jar里面有一个MyUtil被b.jar里面的Main使用, 在没有Incremental Obfuscate的情况下, 你必须指定不obfuscate a.jar里的MyUtil.

   没有增量obfuscate 坏处是很显然的: 第一obfuscate的配置复杂了, 第二,暴露了某些Class

    这个回合Proguard胜出

5. 其它
其他不太重要的特性:
- Proguard可以用一篇文章中的词(比如莎士比亚的剧本)作为变量名字, 呵呵
- Proguard可以删除所有没有用到的Class或者方法(叫做Shrink)
- Yguard能够obfuscate资源名称, 比如Messages.properties->a.properties并且修改相应的ResourceBundle类
- Yguard可以replace指定文本文件里的Class Name,比如修改eclipse plugin.xml里的Class名字
   我个人不太赞成这种做法

总结
对于小型java应用(只有一个package,一个jar文件), 两者区别很小, (但是小型的java应用值得obfuscate么?呵呵) 目前因为没有obfuscate package name这个重要特性, 所以目前我偏向用yguard, 但是yguard没有incremental obufscate, 真伤脑筋, 现在就等Proguard4.0出来, 马上移植到Proguard4.0上去
-------------
【虎.无名】这些对于简单入口程序比较有用,但是对于底层库的开发人员来说,用处不大,因为很多包名、类名、方法名都是不能修改的。我用的是Apusc的一个混淆器joc2.jar,对代码进行混淆,而保留所有public的类、方法等。

几个Java混淆器的网站

【http://www.leesw.com/】smokescreen java obfuscator的website
【http://www.retrologic.com/retroguard-main.html】RetroGuard java obfuscator的website
【http://www.codingart.com/codeshield.html】CodeShield Java Bytecode Obfuscator
【http://www.meurrens.org/ip-Links/Java/CodeEngineering/jobe-doc.html】JOBE
【http://www.preemptive.com/tools/】DashO(不是共享软件)
【http://www.condensity.com/】Condensity(不是共享软件,不过有5天试用版)
【http://www.force5.com/servlet/EvalDownload】Jcloak
【http://www.jproof.com/downloads.html】1stBarrier
【http://www.2lkit.com/products/2LKitObf/index.htm】2lKit Obfuscator
【http://www.duckware.com/jobfuscate.html】Jobfuscate
【http://www.meurrens.org/ip-Links/Java/CodeEngineering/zelixDoc/documentation/index.html】Zelix
【http://www.drjava.de/obfuscator/】Marvin
【http://www.e-t.com/jshrink.html】Jshrink
 

职场新人必看:如何在三个月掌握三年的经验(转载)

很多资料在网上都是可以找到的,只是看你具备不具备足够的信息收集与处理能力,而这个收集与处理信息的过程,也能极大的提升你的职业能力。

  我一直有个感觉,在“模仿中成长,在创新中成功”,其实在真正的职业工作中,大多数的工作都是模仿重复,强调的是工作效率,而不是创新。对于企业而言,过度的创新必然导致过多的失败,以及效率的低下。

  以下方式是我的成长中曾经做过的,也是我用来训练新员工的方案。你们也可以试试。

  看到很多谈应聘技巧的帖子,其实并不实用,有菜谱并不代表能做出好菜,能不能做出好菜仍要看你天天炒,日日炒,炒出来的本事。

  所以,我这里要强调的一点是,你收集到的任何资料都不能只是看看,而必须自己手把手,动手去整理、去归类,去建立新的结构,这个信息收集与处理的过程甚至比你最后总结成文的文字更重要。

  何谓“学习”?学习学习,学而习,习而成习惯。光学不习,那知识还只是书上的,老师教的,不是你自己的,只有你重复练习了,经过量变,才会有质变,当你形成条件反射时,你就真正掌握这个东西了。

  这个过程需要维持两至三个月的时间,一定要坚持下去,你会看到自己的变化的。否则,你会用你最青春的两三年来慢慢沉淀出这些你两三个月就能掌握的东西。

  一切一切,其实,你们比的不是其它的东西,只是比的速度。

  这也是为什么我那么强调基本功的原因。
    
    
    1. 职业分析:
    A. 分析性格——分析长处和短处——分析大家都有的长处——确定自己最终发展的专业
    B. 确定兴趣——分析竞争的激烈程度和发展的空间大小——寻找相对优势—确定自己最终进入的行业
    C. 确定行业内自己的专业方向,继续保持自身的专业优势。
     /* 性格决定专业,兴趣决定行业,行业>专业,某个行业会包括很多专业。 */

    2. 编写行业报告——着重对行业全面性的把握。
    A. 通过上网查询和购买行业报刊,收集不少于三十万字的行业、重点企业的有效资料,在电脑中进行资料分析、分类、汇总。
    B. 参考同类行业书籍,确定写作提纲,确定文章结构和逻辑方向,培养文字表达能力和逻辑能力,以及熟练的电脑使用技能。
    C. 将三十万字资料浓缩成十至十五万字,写成一本符合出版行文格式要求的行业报告。如果选题好,还真的有出版的可能性。如果有一定的独特见解,也可以写成文章争取在专业刊物上发表,树立个人专业形象。
    
    3. 编写讲座报告——着重对专业系统性的把握。
    A. 根据你希望从事的专业岗位,从报告中选择两到三个重点,将书稿压缩成两万字的讲座稿(按每分钟150字的演讲速度,即两个小时)。
    B. 将演讲稿再浓缩成两千字的提纲和重要内容,使用PPT软件编成演讲用演示文件,并根据相关内容配以精彩图片。
    C. 培养职业化的公众表达能力和表达方式,练习普通话,使用讲座稿进行互动讲座和演讲练习,只到脱口而出。
    

  告诉大家两个名人是这么成长的.
  
  一个是教英语的李阳,他读大学时成绩不好,英语不及格,然后他做什么去了?他跑到没人的地方大声喊英语去了.
  
  一个是做广告的叶茂中,他卖广告卖不出去了,他跑回家写书.别人看到的和他自己说的是拿着书出版出了名,发达了.其实做过这个事的人才会知道,当他把这本书写出来时,能不能出版已经不重要的,因为他知道他变化了.
  
  我当时也是没办法了,把所有的钱买了台电脑,在家里做了三个月这个事,三个月后的变化是惊人的,我的父母、我兼职的公司的老总,最重要的是我自己,都感觉到了自己的变化。
  
  完全不同了。

  其实我写的已经不是理论了,其实什么都没有技巧的,只是多看书,然后多做,硬磕,坚持下去,刚开始觉得没变化,没感觉,很累,坚持不下去,然后做着做着,就越来越快了,然后慢慢的有变化.

  而且有意思的是,我在家呆了三个月,做的事其实根本与我所从事的工作没有一点关系.只是这三个月的训练,对于我的逻辑、结构、全局性、文字表达能力、口头表达能力有了极大的提升。

  至于收入翻5翻,当年一个月也就八百块钱,然后做完这个训练后整个人的状态都变了,有自信了,然后写了一个方案去应聘,结果进了一家大公司,当然,开始我还不想去,因为对方只给我800/月,还要自己租房子,吃饭,觉得不好,但是对方连续四个月三次打电话找我,于是我去了,结果去了就后悔了,真正好的公司根本不在乎工资的,重要的是你自己的能力。第一个月,我就挣了八千块,我以前想都不敢想的。然后两个月就转了正,而有一个有关系的同事,呆了一年还没能转正。然后每个月的收入超过工资几倍,还有年终奖两万,出国旅游,其实也不累,我到这个家公司的同时,还到另一家广告公司兼职,呵呵,很回忆的过去。

  现在看到太多的人谈工资,我确实不喜欢,我这几年都不和老板谈工资的,因为说出来好笑,帐面工资高了,还要多扣税.

  我只在意公司的分配方式,怎么样算提成和奖金,年薪.
  
  上个月有一个和我同龄的名牌大学MBA来我现在所在的小公司应聘,不愿意和人事小姐谈,老板不在,我就来谈了,我说好呀,以你的资历我不能和你谈给谁做副手的问题了,我跟你谈谈公司的分配方式吧,其实我们公司普通员工的收入都不高的,长沙平均水平,只是不忙,周末休两天,工作满一年还有一个星期的年休假.
  
  但是公司几个部门负责人还是有钱的,象我三十岁,一年18万左右的年薪,其它的我就不清楚了,有几个我一个星期才见一次的,比我还小,只怕拿得比我还多.你应该也是这样的吧.
  
  他要求6千一个月的月薪,我说这倒不重要,重要的是公司不会给你安排业务的,你自己找业务回来,公司给你平台,给你配团队,能挣多少钱是你的本事.
  
  我说完了,问,你有什么想法吗?他说没想法,起身走人.
  
  太有意思了,你在长沙想拿六千一个月,你等别人找事给你做,你为什么不能自己找到项目呀?六千是底薪呀,差不多7万2千的底薪,如果是这样的,那我自己算我应该拿到二十五万以上的年薪了.
  
  从来拿底薪和拿年薪的人就是不一样的.
  
  如果你不敢拿年薪,你就不要想着谈什么老板给你少了.
  
  企业是要盈利的,资本家是要剥削的.问题是,如果你是一个真正能创造价值的人,你自己所创造的价值你是可以拿到手的.
  
  大学毕业生,如果什么经验也没有,只有知识,没有技能,能找到一个给你几百块钱,让你在这里呆着学东西的企业就应该感谢了,如果你觉得这种企业不是你所向往的,你在上大学时就老老实实努力学,少玩,多练.
  
  我工作有一个总结,钱永远不会是目标,但是它会是结果.

  谈到职业规划,有人说过职业可以规划的,我也相信未来可以计划的,问题是,你是不是这个能不能计划出你未来的人,以及,你身边有没有熟悉你的高 人 指 点,如果没有,那你自己都不会明白你自己的未来是什么的,就象象你去做所谓的性向测试,说不定是你自己在自欺欺人了,这种事多了,没人会把自己算成一个坏人的。

  所以重要的还是那一句话,复杂的生活简单过,简单的事情重复做。
  
  你是中文系的,如果你的年纪还不是很大,建议你凭你自己的能力,哪怕是工资少点,你都要进最好的广告公司,去呆上一年半载,按我说的方法偷师,基本能力提升了,慢慢的你会遇到一些贵人的,还有你会涉及一些行业,慢慢的,你会发觉你内心深处喜欢的行业。
  
  呵呵,特别是哦,女孩子,只有努力才能进大公司,只有进了大公司才能遇到优秀的男生。好男生都关在写字楼里上班下班加班的,呵呵。生活圈子都小的,你选择的工作圈在你努力的阶段就是你的生活圈。

  在你的成长过程中,有五个人非常重要。
    
  第一个,导师,教练。
  他教给你实用的技巧、一定的工作经验,而不是知识。他可以给你指明方向。
  这个人可能是你的上司、前辈、学长。
    
  第二个,陪练,同路人。
  任何人的成长都不是学出来的,而是学而习,习而成习惯,练出来的。在这个练的过程中,是一件很苦的过程,是一系列简单动作的重复重复再重复,由量变到质变的过程,在这个过程中,一个人很难坚持下来,这时你需要一个同路人。
  他可以是和你共同兴趣,共同目标的朋友,最好是你生命中所爱的人。
    
  第三个,榜样,他是你人生的标杆。
  在你一生中,在不同阶段,会有不同的标杆,你向他学习,受他鼓舞,一步一步向他靠扰。
  最重要的是那个你看得到摸得着的人,你知道,不需要通过机遇,只需要通过努力就可以达到的榜样。
    
  第四个,敌人,看不起你的人,拒绝过你的人。
  人不到绝境是不会有斗志的,你要证明他是错的,他会给你真正的动力。
    
  第五个,最重要的是第五个,你们觉得第五个人是你自己。
  世界上没有救世主,任何希望当别人救世主的人不是疯子就是傻子,只有自己才可以救自己。
  这个世界上,失败的人除了天分太差之外,只有以下几点,懒,方向不对,方法不对,没有坚持。
  如果你自己做不到,你不要怪别人。

  基本功是你自己的,细节所积累下来的,能让你迅速融入新环境.
  /* 人和人最终的差距就是在基本功上面,是否迅速。 */
  
  不知道怎么跟大家谈基本功这个问题.

  很多东西大家都没把它当基本功了.

  比如说,我想要的人,他打字很快,他很少很少写错别字,有丰富的词汇量,逻辑很清晰,用词很准确,这些看上去难不难?
  
  但是在我这两年见过的应聘的策划文案来看,只有两个人做到了.一个是做了三年文案的女孩子,慢慢磨的.一个是中文硕士生,还没毕业.
  
  其实大学到底教给大家什么了?
  知识?
  
  大学阶段必须打好你的基本功,这些决定了你就业后的学习能力,阶层简单工作的工作效率.
  
  如果谁还说打字、排版是文员做的事,那只能说他是真正不明白真正的职场需要。
  
  你们在大学所学到的知识,都是同质化的了,如果将知识变为通用的、标准化的技能才是重要的。
  
  既然学的东西没用,那在大学还要不要认真学习呢?
  
  当然要,因为这些东西是系统性的,这个学习过程能培养你的学习能力。
  
  知识不能改变你的命运了,但是它可以改变你的气质。
  
  如果你读个四年大学出来,你的气质还不能好一点,那你的大学就真的白读了。
  
  经常有人在问面试穿什么衣服呀?
  
  穿什么衣服重要吗?
  
  重要的是什么人在穿这些衣服。
  
  重要的是你的精气神,你的气质。
  
  有一天有一个应聘文案的来了,我叫设计总监先和他聊聊。
  
  聊完了,我说这个人不行吧,设计总监说为什么?
  
  我说我们调性不符,我们多少都有点书卷气,而他是一脸的江湖气。
  
  果然,呵呵。
  
  招聘方当然是要看应聘者的外形条件的,但并不是丑的就不招,重要的是能力和你的气质,是不是符合公司要求的。

  重要的是兴趣。
  
  然后是狂练基本功,简单重复积累。
  
  学打拳,你先站三个月桩再说。
  
  面对新人,我说很多东西,你会发现,每个字你都认识,每句话你都看得懂,但是你理解吗?
  
  领悟,是教不了的。
  
  自己努力吧,自己重复做,再会明白自己最想要的是什么。
  
  你考公员员如果死活考不上,那你应该去想想,这种机械性的考试你都过不了,那是不是学习方法,或者兴趣不对呀?
  
  做销售,同样的,从基阶做起吧。
  
  你的财政学对你有没有帮助?
  
  当然有,你对销售的认识会不同的。
  
  象十年前我卖保险,人人都跟银行比,算利息,都算得没有银行高,只能说死了人有赔了。
  
  而我是怎么算呢?我用递增,还是增减年金公式算,呵呵,比银行高呢。
  
  另外,别人说死了人有赔,最多是说得婉转点。
  
  我可没把它当死人卖呀,我把它当礼物卖,当成父母送给孩子的礼物卖,卖得可好了,呵呵。
  
  现在哪个做人寿险的人敢说他一年做两百多单?
  
  呵呵,我好象一年做了二百四十单左右,全是年缴哦。

  这个世界上最穷的和最富的人都在做销售.
  
  做销售的人底薪很低的,大多数人拼的只是体力罢了,如果你想做好,你多花心思就可以了.多想多跑,还是在一个行业里多坚持,找到高手做师父带你.

  我说说当年我混日子的时候怎么过来的.
  
  那年头电脑还紧俏,我只要一有机会就到别人电脑上练东西,终于练成了今天的电脑基本功,一方面要多学,一方面要多用心.
  
  然后,我每天做记录,记下工作的流程,记下别人说过的工作中重要的话,其实什么叫行业经验,很多老手随便说的话,都是行话了,有它的意思的,听了就要想,就要去查,很多东西就知道了.
  
  为什么要记录,因为什么叫职业化?职业化就是标准化、流程化,模式化,你多看多记多想就能明白了,这些东西在很多地方都是通用的。
  /* 职业精神 */

  有一点,如果这里收入还可以的话,你好好学吧,任何工作都要呆一两年,你才会有认识的,跳来跳去的对你不好,真的,你还在磨性情的时候,只要你保持学习的能力,别下班玩去了就可以了,有压力才有动力,好好留心心仪的公司招聘的要求,按那个要求去做一个一年的训练与学习计划,一年后,那个公司在等你。

内存管理内幕

 

Jonathan Bartlett (johnnyb@eskimo.com), 技术总监, New Media Worx

2004 年 11 月 29 日

本文将对 Linux™ 程序员可以使用的内存管理技术进行概述,虽然关注的重点是 C 语言,但同样也适用于其他语言。文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半手工地管理内存,以及如何使用垃圾收集自动管理内存。

为什么必须管理内存

内存管理是计算机编程最为基本的领域之一。在很多脚本语言中,您不必担心内存是如何管理的,这并不能使得内存管理的重要性有一点点降低。对实际编程来说,理解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 C 和 C++,您必须进行内存管理。本文将介绍手工的、半手工的以及自动的内存管理实践的基本概念。

追溯到在 Apple II 上进行汇编语言编程的时代,那时内存管理还不是个大问题。您实际上在运行整个系统。系统有多少内存,您就有多少内存。您甚至不必费心思去弄明白它有多少内存,因为每一台机器的内存数量都相同。所以,如果内存需要非常固定,那么您只需要选择一个内存范围并使用它即可。

不过,即使是在这样一个简单的计算机中,您也会有问题,尤其是当您不知道程序的每个部分将需要多少内存时。如果您的空间有限,而内存需求是变化的,那么您需要一些方法来满足这些需求:

  • 确定您是否有足够的内存来处理数据。
  • 从可用的内存中获取一部分内存。
  • 向可用内存池(pool)中返回部分内存,以使其可以由程序的其他部分或者其他程序使用。

 

实现这些需求的程序库称为 分配程序(allocators),因为它们负责分配和回收内存。程序的动态性越强,内存管理就越重要,您的内存分配程序的选择也就更重要。让我们来了解可用于内存管理的不同方法,它们的好处与不足,以及它们最适用的情形。




回页首


C 风格的内存分配程序

C 编程语言提供了两个函数来满足我们的三个需求:

  • malloc:该函数分配给定的字节数,并返回一个指向它们的指针。如果没有足够的可用内存,那么它返回一个空指针。
  • free:该函数获得指向由 malloc 分配的内存片段的指针,并将其释放,以便以后的程序或操作系统使用(实际上,一些 malloc 实现只能将内存归还给程序,而无法将内存归还给操作系统)。

 

物理内存和虚拟内存

要理解内存在程序中是如何分配的,首先需要理解如何将内存从操作系统分配给程序。计算机上的每一个进程都认为自己可以访问所有的物理内存。显然,由于同时在运行多个程序,所以每个进程不可能拥有全部内存。实际上,这些进程使用的是 虚拟内存

只是作为一个例子,让我们假定您的程序正在访问地址为 629 的内存。不过,虚拟内存系统不需要将其存储在位置为 629 的 RAM 中。实际上,它甚至可以不在 RAM 中 —— 如果物理 RAM 已经满了,它甚至可能已经被转移到硬盘上!由于这类地址不必反映内存所在的物理位置,所以它们被称为虚拟内存。操作系统维持着一个虚拟地址到物理地址的转换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 RAM 中,那么操作系统将暂时停止您的进程,将其他内存转存到硬盘中,从硬盘上加载被请求的内存,然后再重新启动您的进程。这样,每个进程都获得了自己可以使用的地址空间,可以访问比您物理上安装的内存更多的内存。

在 32-位 x86 系统上,每一个进程可以访问 4 GB 内存。现在,大部分人的系统上并没有 4 GB 内存,即使您将 swap 也算上, 每个进程所使用的内存也肯定少于 4 GB。因此,当加载一个进程时,它会得到一个取决于某个称为 系统中断点(system break)的特定地址的初始内存分配。该地址之后是未被映射的内存 —— 用于在 RAM 或者硬盘中没有分配相应物理位置的内存。因此,如果一个进程运行超出了它初始分配的内存,那么它必须请求操作系统“映射进来(map in)”更多的内存。(映射是一个表示一一对应关系的数学术语 —— 当内存的虚拟地址有一个对应的物理地址来存储内存内容时,该内存将被映射。)

基于 UNIX 的系统有两个可映射到附加内存中的基本系统调用:

  • brk: brk() 是一个非常简单的系统调用。还记得系统中断点吗?该位置是进程映射的内存边界。 brk() 只是简单地将这个位置向前或者向后移动,就可以向进程添加内存或者从进程取走内存。
  • mmap: mmap(),或者说是“内存映像”,类似于 brk(),但是更为灵活。首先,它可以映射任何位置的内存,而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将它们映射到文件和文件位置,这样,读写内存将对文件中的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的内存的能力。 munmap() 所做的事情与 mmap() 相反。

 

如您所见, brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。在我们的例子中将使用 brk(),因为它更简单,更通用。

实现一个简单的分配程序

如果您曾经编写过很多 C 程序,那么您可能曾多次使用过 malloc()free()。不过,您可能没有用一些时间去思考它们在您的操作系统中是如何实现的。本节将向您展示 mallocfree 的一个最简化实现的代码,来帮助说明管理内存时都涉及到了哪些事情。

要试着运行这些示例,需要先 复制本代码清单,并将其粘贴到一个名为 malloc.c 的文件中。接下来,我将一次一个部分地对该清单进行解释。

在大部分操作系统中,内存分配由以下两个简单的函数来处理:

  • void *malloc(long numbytes):该函数负责分配 numbytes 大小的内存,并返回指向第一个字节的指针。
  • void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该函数会将分配的空间归还给进程的“空闲空间”。

malloc_init 将是初始化内存分配程序的函数。它要完成以下三件事:将分配程序标识为已经初始化,找到系统中最后一个有效内存地址,然后建立起指向我们管理的内存的指针。这三个变量都是全局变量:

清单 1. 我们的简单分配程序的全局变量

            int has_initialized = 0;
            void *managed_memory_start;
            void *last_valid_address;
            

如前所述,被映射的内存的边界(最后一个有效地址)常被称为系统中断点或者 当前中断点。在很多 UNIX® 系统中,为了指出当前系统中断点,必须使用 sbrk(0) 函数。 sbrk 根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。使用参数 0 只是返回当前中断点。这里是我们的 malloc 初始化代码,它将找到当前中断点并初始化我们的变量:

清单 2. 分配程序初始化函数

            /* Include the sbrk function */
            #include <unistd.h>
            void malloc_init()
            {
            /* grab the last valid address from the OS */
            last_valid_address = sbrk(0);
            /* we don't have any memory to manage yet, so
            *just set the beginning to be last_valid_address
            */
            managed_memory_start = last_valid_address;
            /* Okay, we're initialized and ready to go */
            has_initialized = 1;
            }
            

现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构:

清单 3. 内存控制块结构定义

            struct mem_control_block {
            int is_available;
            int size;
            };
            

现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。

在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码:

清单 4. 解除分配函数

            void free(void *firstbyte) {
            struct mem_control_block *mcb;
            /* Backup from the given pointer to find the
            * mem_control_block
            */
            mcb = firstbyte - sizeof(struct mem_control_block);
            /* Mark the block as being available */
            mcb->is_available = 1;
            /* That's It!  We're done. */
            return;
            }
            

如您所见,在这个分配程序中,内存的释放使用了一个非常简单的机制,在固定时间内完成内存释放。分配内存稍微困难一些。以下是该算法的略述:

清单 5. 主分配程序的伪代码

            1. If our allocator has not been initialized, initialize it.
            2. Add sizeof(struct mem_control_block) to the size requested.
            3. start at managed_memory_start.
            4. Are we at last_valid address?
            5. If we are:
            A. We didn't find any existing space that was large enough
            -- ask the operating system for more and return that.
            6. Otherwise:
            A. Is the current space available (check is_available from
            the mem_control_block)?
            B. If it is:
            i)   Is it large enough (check "size" from the
            mem_control_block)?
            ii)  If so:
            a. Mark it as unavailable
            b. Move past mem_control_block and return the
            pointer
            iii) Otherwise:
            a. Move forward "size" bytes
            b. Go back go step 4
            C. Otherwise:
            i)   Move forward "size" bytes
            ii)  Go back to step 4
            

我们主要使用连接的指针遍历内存来寻找开放的内存块。这里是代码:

清单 6. 主分配程序

            void *malloc(long numbytes) {
            /* Holds where we are looking in memory */
            void *current_location;
            /* This is the same as current_location, but cast to a
            * memory_control_block
            */
            struct mem_control_block *current_location_mcb;
            /* This is the memory location we will return.  It will
            * be set to 0 until we find something suitable
            */
            void *memory_location;
            /* Initialize if we haven't already done so */
            if(! has_initialized) 	{
            malloc_init();
            }
            /* The memory we search for has to include the memory
            * control block, but the users of malloc don't need
            * to know this, so we'll just add it in for them.
            */
            numbytes = numbytes + sizeof(struct mem_control_block);
            /* Set memory_location to 0 until we find a suitable
            * location
            */
            memory_location = 0;
            /* Begin searching at the start of managed memory */
            current_location = managed_memory_start;
            /* Keep going until we have searched all allocated space */
            while(current_location != last_valid_address)
            {
            /* current_location and current_location_mcb point
            * to the same address.  However, current_location_mcb
            * is of the correct type, so we can use it as a struct.
            * current_location is a void pointer so we can use it
            * to calculate addresses.
            */
            current_location_mcb =
            (struct mem_control_block *)current_location;
            if(current_location_mcb->is_available)
            {
            if(current_location_mcb->size >= numbytes)
            {
            /* Woohoo!  We've found an open,
            * appropriately-size location.
            */
            /* It is no longer available */
            current_location_mcb->is_available = 0;
            /* We own it */
            memory_location = current_location;
            /* Leave the loop */
            break;
            }
            }
            /* If we made it here, it's because the Current memory
            * block not suitable; move to the next one
            */
            current_location = current_location +
            current_location_mcb->size;
            }
            /* If we still don't have a valid location, we'll
            * have to ask the operating system for more memory
            */
            if(! memory_location)
            {
            /* Move the program break numbytes further */
            sbrk(numbytes);
            /* The new memory will be where the last valid
            * address left off
            */
            memory_location = last_valid_address;
            /* We'll move the last valid address forward
            * numbytes
            */
            last_valid_address = last_valid_address + numbytes;
            /* We need to initialize the mem_control_block */
            current_location_mcb = memory_location;
            current_location_mcb->is_available = 0;
            current_location_mcb->size = numbytes;
            }
            /* Now, no matter what (well, except for error conditions),
            * memory_location has the address of the memory, including
            * the mem_control_block
            */
            /* Move the pointer past the mem_control_block */
            memory_location = memory_location + sizeof(struct mem_control_block);
            /* Return the pointer */
            return memory_location;
            }
            

这就是我们的内存管理器。现在,我们只需要构建它,并在程序中使用它即可。

运行下面的命令来构建 malloc 兼容的分配程序(实际上,我们忽略了 realloc() 等一些函数,不过, malloc()free() 才是最主要的函数):

清单 7. 编译分配程序

            gcc -shared -fpic malloc.c -o malloc.so
            

该程序将生成一个名为 malloc.so 的文件,它是一个包含有我们的代码的共享库。

在 UNIX 系统中,现在您可以用您的分配程序来取代系统的 malloc(),做法如下:

清单 8. 替换您的标准的 malloc

            LD_PRELOAD=/path/to/malloc.so
            export LD_PRELOAD
            

LD_PRELOAD 环境变量使动态链接器在加载任何可执行程序之前,先加载给定的共享库的符号。它还为特定库中的符号赋予优先权。因此,从现在起,该会话中的任何应用程序都将使用我们的 malloc(),而不是只有系统的应用程序能够使用。有一些应用程序不使用 malloc(),不过它们是例外。其他使用 realloc() 等其他内存管理函数的应用程序,或者错误地假定 malloc() 内部行为的那些应用程序,很可能会崩溃。ash shell 似乎可以使用我们的新 malloc() 很好地工作。

如果您想确保 malloc() 正在被使用,那么您应该通过向函数的入口点添加 write() 调用来进行测试。

我们的内存管理器在很多方面都还存在欠缺,但它可以有效地展示内存管理需要做什么事情。它的某些缺点包括:

  • 由于它对系统中断点(一个全局变量)进行操作,所以它不能与其他分配程序或者 mmap 一起使用。
  • 当分配内存时,在最坏的情形下,它将不得不遍历 全部进程内存;其中可能包括位于硬盘上的很多内存,这意味着操作系统将不得不花时间去向硬盘移入数据和从硬盘中移出数据。
  • 没有很好的内存不足处理方案( malloc 只假定内存分配是成功的)。
  • 它没有实现很多其他的内存函数,比如 realloc()
  • 由于 sbrk() 可能会交回比我们请求的更多的内存,所以在堆(heap)的末端会遗漏一些内存。
  • 虽然 is_available 标记只包含一位信息,但它要使用完整的 4-字节 的字。
  • 分配程序不是线程安全的。
  • 分配程序不能将空闲空间拼合为更大的内存块。
  • 分配程序的过于简单的匹配算法会导致产生很多潜在的内存碎片。
  • 我确信还有很多其他问题。这就是为什么它只是一个例子!

 

其他 malloc 实现

malloc() 的实现有很多,这些实现各有优点与缺点。在设计一个分配程序时,要面临许多需要折衷的选择,其中包括:

  • 分配的速度。
  • 回收的速度。
  • 有线程的环境的行为。
  • 内存将要被用光时的行为。
  • 局部缓存。
  • 簿记(Bookkeeping)内存开销。
  • 虚拟内存环境中的行为。
  • 小的或者大的对象。
  • 实时保证。

 

每一个实现都有其自身的优缺点集合。在我们的简单的分配程序中,分配非常慢,而回收非常快。另外,由于它在使用虚拟内存系统方面较差,所以它最适于处理大的对象。

还有其他许多分配程序可以使用。其中包括:

  • Doug Lea Malloc:Doug Lea Malloc 实际上是完整的一组分配程序,其中包括 Doug Lea 的原始分配程序,GNU libc 分配程序和 ptmalloc。 Doug Lea 的分配程序有着与我们的版本非常类似的基本结构,但是它加入了索引,这使得搜索速度更快,并且可以将多个没有被使用的块组合为一个大的块。它还支持缓存,以便更快地再次使用最近释放的内存。 ptmalloc 是 Doug Lea Malloc 的一个扩展版本,支持多线程。在本文后面的 参考资料部分中,有一篇描述 Doug Lea 的 Malloc 实现的文章。
  • BSD Malloc:BSD Malloc 是随 4.2 BSD 发行的实现,包含在 FreeBSD 之中,这个分配程序可以从预先确实大小的对象构成的池中分配对象。它有一些用于对象大小的 size 类,这些对象的大小为 2 的若干次幂减去某一常数。所以,如果您请求给定大小的一个对象,它就简单地分配一个与之匹配的 size 类。这样就提供了一个快速的实现,但是可能会浪费内存。在 参考资料部分中,有一篇描述该实现的文章。
  • Hoard:编写 Hoard 的目标是使内存分配在多线程环境中进行得非常快。因此,它的构造以锁的使用为中心,从而使所有进程不必等待分配内存。它可以显著地加快那些进行很多分配和回收的多线程进程的速度。在 参考资料部分中,有一篇描述该实现的文章。

 

众多可用的分配程序中最有名的就是上述这些分配程序。如果您的程序有特别的分配需求,那么您可能更愿意编写一个定制的能匹配您的程序内存分配方式的分配程序。不过,如果不熟悉分配程序的设计,那么定制分配程序通常会带来比它们解决的问题更多的问题。要获得关于该主题的适当的介绍,请参阅 Donald Knuth 撰写的 The Art of Computer Programming Volume 1: Fundamental Algorithms 中的第 2.5 节“Dynamic Storage Allocation”(请参阅 参考资料中的链接)。它有点过时,因为它没有考虑虚拟内存环境,不过大部分算法都是基于前面给出的函数。

在 C++ 中,通过重载 operator new(),您可以以每个类或者每个模板为单位实现自己的分配程序。在 Andrei Alexandrescu 撰写的 Modern C++ Design 的第 4 章(“Small Object Allocation”)中,描述了一个小对象分配程序(请参阅 参考资料中的链接)。

基于 malloc() 的内存管理的缺点

不只是我们的内存管理器有缺点,基于 malloc() 的内存管理器仍然也有很多缺点,不管您使用的是哪个分配程序。对于那些需要保持长期存储的程序使用 malloc() 来管理内存可能会非常令人失望。如果您有大量的不固定的内存引用,经常难以知道它们何时被释放。生存期局限于当前函数的内存非常容易管理,但是对于生存期超出该范围的内存来说,管理内存则困难得多。而且,关于内存管理是由进行调用的程序还是由被调用的函数来负责这一问题,很多 API 都不是很明确。

因为管理内存的问题,很多程序倾向于使用它们自己的内存管理规则。C++ 的异常处理使得这项任务更成问题。有时好像致力于管理内存分配和清理的代码比实际完成计算任务的代码还要多!因此,我们将研究内存管理的其他选择。




回页首


半自动内存管理策略

引用计数

引用计数是一种 半自动(semi-automated)的内存管理技术,这表示它需要一些编程支持,但是它不需要您确切知道某一对象何时不再被使用。引用计数机制为您完成内存管理任务。

在引用计数中,所有共享的数据结构都有一个域来包含当前活动“引用”结构的次数。当向一个程序传递一个指向某个数据结构指针时,该程序会将引用计数增加 1。实质上,您是在告诉数据结构,它正在被存储在多少个位置上。然后,当您的进程完成对它的使用后,该程序就会将引用计数减少 1。结束这个动作之后,它还会检查计数是否已经减到零。如果是,那么它将释放内存。

这样做的好处是,您不必追踪程序中某个给定的数据结构可能会遵循的每一条路径。每次对其局部的引用,都将导致计数的适当增加或减少。这样可以防止在使用数据结构时释放该结构。不过,当您使用某个采用引用计数的数据结构时,您必须记得运行引用计数函数。另外,内置函数和第三方的库不会知道或者可以使用您的引用计数机制。引用计数也难以处理发生循环引用的数据结构。

要实现引用计数,您只需要两个函数 —— 一个增加引用计数,一个减少引用计数并当计数减少到零时释放内存。

一个示例引用计数函数集可能看起来如下所示:

清单 9. 基本的引用计数函数

            /* Structure Definitions*/
            /* Base structure that holds a refcount */
            struct refcountedstruct
            {
            int refcount;
            }
            /* All refcounted structures must mirror struct
            * refcountedstruct for their first variables
            */
            /* Refcount maintenance functions */
            /* Increase reference count */
            void REF(void *data)
            {
            struct refcountedstruct *rstruct;
            rstruct = (struct refcountedstruct *) data;
            rstruct->refcount++;
            }
            /* Decrease reference count */
            void UNREF(void *data)
            {
            struct refcountedstruct *rstruct;
            rstruct = (struct refcountedstruct *) data;
            rstruct->refcount--;
            /* Free the structure if there are no more users */
            if(rstruct->refcount == 0)
            {
            free(rstruct);
            }
            }
            

REFUNREF 可能会更复杂,这取决于您想要做的事情。例如,您可能想要为多线程程序增加锁,那么您可能想扩展 refcountedstruct,使它同样包含一个指向某个在释放内存之前要调用的函数的指针(类似于面向对象语言中的析构函数 —— 如果您的结构中包含这些指针,那么这是 必需的)。

当使用 REFUNREF 时,您需要遵守这些指针的分配规则:

  • UNREF 分配前左端指针(left-hand-side pointer)指向的值。
  • REF 分配后左端指针(left-hand-side pointer)指向的值。

 

在传递使用引用计数的结构的函数中,函数需要遵循以下这些规则:

  • 在函数的起始处 REF 每一个指针。
  • 在函数的结束处 UNREF 第一个指针。

 

以下是一个使用引用计数的生动的代码示例:

清单 10. 使用引用计数的示例

            /* EXAMPLES OF USAGE */
            /* Data type to be refcounted */
            struct mydata
            {
            int refcount; /* same as refcountedstruct */
            int datafield1; /* Fields specific to this struct */
            int datafield2;
            /* other declarations would go here as appropriate */
            };
            /* Use the functions in code */
            void dosomething(struct mydata *data)
            {
            REF(data);
            /* Process data */
            /* when we are through */
            UNREF(data);
            }
            struct mydata *globalvar1;
            /* Note that in this one, we don't decrease the
            * refcount since we are maintaining the reference
            * past the end of the function call through the
            * global variable
            */
            void storesomething(struct mydata *data)
            {
            REF(data); /* passed as a parameter */
            globalvar1 = data;
            REF(data); /* ref because of Assignment */
            UNREF(data); /* Function finished */
            }
            

由于引用计数是如此简单,大部分程序员都自已去实现它,而不是使用库。不过,它们依赖于 mallocfree 等低层的分配程序来实际地分配和释放它们的内存。

在 Perl 等高级语言中,进行内存管理时使用引用计数非常广泛。在这些语言中,引用计数由语言自动地处理,所以您根本不必担心它,除非要编写扩展模块。由于所有内容都必须进行引用计数,所以这会对速度产生一些影响,但它极大地提高了编程的安全性和方便性。以下是引用计数的益处:

  • 实现简单。
  • 易于使用。
  • 由于引用是数据结构的一部分,所以它有一个好的缓存位置。

 

不过,它也有其不足之处:

  • 要求您永远不要忘记调用引用计数函数。
  • 无法释放作为循环数据结构的一部分的结构。
  • 减缓几乎每一个指针的分配。
  • 尽管所使用的对象采用了引用计数,但是当使用异常处理(比如 trysetjmp()/ longjmp())时,您必须采取其他方法。
  • 需要额外的内存来处理引用。
  • 引用计数占用了结构中的第一个位置,在大部分机器中最快可以访问到的就是这个位置。
  • 在多线程环境中更慢也更难以使用。

 

C++ 可以通过使用 智能指针(smart pointers)来容忍程序员所犯的一些错误,智能指针可以为您处理引用计数等指针处理细节。不过,如果不得不使用任何先前的不能处理智能指针的代码(比如对 C 库的联接),实际上,使用它们的后果通实比不使用它们更为困难和复杂。因此,它通常只是有益于纯 C++ 项目。如果您想使用智能指针,那么您实在应该去阅读 Alexandrescu 撰写的 Modern C++ Design 一书中的“Smart Pointers”那一章。

内存池

内存池是另一种半自动内存管理方法。内存池帮助某些程序进行自动内存管理,这些程序会经历一些特定的阶段,而且每个阶段中都有分配给进程的特定阶段的内存。例如,很多网络服务器进程都会分配很多针对每个连接的内存 —— 内存的最大生存期限为当前连接的存在期。Apache 使用了池式内存(pooled memory),将其连接拆分为各个阶段,每个阶段都有自己的内存池。在结束每个阶段时,会一次释放所有内存。

在池式内存管理中,每次内存分配都会指定内存池,从中分配内存。每个内存池都有不同的生存期限。在 Apache 中,有一个持续时间为服务器存在期的内存池,还有一个持续时间为连接的存在期的内存池,以及一个持续时间为请求的存在期的池,另外还有其他一些内存池。因此,如果我的一系列函数不会生成比连接持续时间更长的数据,那么我就可以完全从连接池中分配内存,并知道在连接结束时,这些内存会被自动释放。另外,有一些实现允许注册 清除函数(cleanup functions),在清除内存池之前,恰好可以调用它,来完成在内存被清理前需要完成的其他所有任务(类似于面向对象中的析构函数)。

要在自己的程序中使用池,您既可以使用 GNU libc 的 obstack 实现,也可以使用 Apache 的 Apache Portable Runtime。GNU obstack 的好处在于,基于 GNU 的 Linux 发行版本中默认会包括它们。Apache Portable Runtime 的好处在于它有很多其他工具,可以处理编写多平台服务器软件所有方面的事情。要深入了解 GNU obstack 和 Apache 的池式内存实现,请参阅 参考资料部分中指向这些实现的文档的链接。

下面的假想代码列表展示了如何使用 obstack:

清单 11. obstack 的示例代码

            #include <obstack.h>
            #include <stdlib.h>
            /* Example code listing for using obstacks */
            /* Used for obstack macros (xmalloc is
            a malloc function that exits if memory
            is exhausted */
            #define obstack_chunk_alloc xmalloc
            #define obstack_chunk_free free
            /* Pools */
            /* Only permanent allocations should go in this pool */
            struct obstack *global_pool;
            /* This pool is for per-connection data */
            struct obstack *connection_pool;
            /* This pool is for per-request data */
            struct obstack *request_pool;
            void allocation_failed()
            {
            exit(1);
            }
            int main()
            {
            /* Initialize Pools */
            global_pool = (struct obstack *)
            xmalloc (sizeof (struct obstack));
            obstack_init(global_pool);
            connection_pool = (struct obstack *)
            xmalloc (sizeof (struct obstack));
            obstack_init(connection_pool);
            request_pool = (struct obstack *)
            xmalloc (sizeof (struct obstack));
            obstack_init(request_pool);
            /* Set the error handling function */
            obstack_alloc_failed_handler = &allocation_failed;
            /* Server main loop */
            while(1)
            {
            wait_for_connection();
            /* We are in a connection */
            while(more_requests_available())
            {
            /* Handle request */
            handle_request();
            /* Free all of the memory allocated
            * in the request pool
            */
            obstack_free(request_pool, NULL);
            }
            /* We're finished with the connection, time
            * to free that pool
            */
            obstack_free(connection_pool, NULL);
            }
            }
            int handle_request()
            {
            /* Be sure that all object allocations are allocated
            * from the request pool
            */
            int bytes_i_need = 400;
            void *data1 = obstack_alloc(request_pool, bytes_i_need);
            /* Do stuff to process the request */
            /* return */
            return 0;
            }
            

基本上,在操作的每一个主要阶段结束之后,这个阶段的 obstack 会被释放。不过,要注意的是,如果一个过程需要分配持续时间比当前阶段更长的内存,那么它也可以使用更长期限的 obstack,比如连接或者全局内存。传递给 obstack_free()NULL 指出它应该释放 obstack 的全部内容。可以用其他的值,但是它们通常不怎么实用。

使用池式内存分配的益处如下所示:

  • 应用程序可以简单地管理内存。
  • 内存分配和回收更快,因为每次都是在一个池中完成的。分配可以在 O(1) 时间内完成,释放内存池所需时间也差不多(实际上是 O(n) 时间,不过在大部分情况下会除以一个大的因数,使其变成 O(1))。
  • 可以预先分配错误处理池(Error-handling pools),以便程序在常规内存被耗尽时仍可以恢复。
  • 有非常易于使用的标准实现。

 

池式内存的缺点是:

  • 内存池只适用于操作可以分阶段的程序。
  • 内存池通常不能与第三方库很好地合作。
  • 如果程序的结构发生变化,则不得不修改内存池,这可能会导致内存管理系统的重新设计。
  • 您必须记住需要从哪个池进行分配。另外,如果在这里出错,就很难捕获该内存池。




回页首


垃圾收集

垃圾收集(Garbage collection)是全自动地检测并移除不再使用的数据对象。垃圾收集器通常会在当可用内存减少到少于一个具体的阈值时运行。通常,它们以程序所知的可用的一组“基本”数据 —— 栈数据、全局变量、寄存器 —— 作为出发点。然后它们尝试去追踪通过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可以被销毁并重新使用这些无用的数据。为了有效地管理内存,很多类型的垃圾收集器都需要知道数据结构内部指针的规划,所以,为了正确运行垃圾收集器,它们必须是语言本身的一部分。

收集器的类型

  • 复制(copying): 这些收集器将内存存储器分为两部分,只允许数据驻留在其中一部分上。它们定时地从“基本”的元素开始将数据从一部分复制到另一部分。内存新近被占用的部分现在成为活动的,另一部分上的所有内容都认为是垃圾。另外,当进行这项复制操作时,所有指针都必须被更新为指向每个内存条目的新位置。因此,为使用这种垃圾收集方法,垃圾收集器必须与编程语言集成在一起。
  • 标记并清理(Mark and sweep):每一块数据都被加上一个标签。不定期的,所有标签都被设置为 0,收集器从“基本”的元素开始遍历数据。当它遇到内存时,就将标签标记为 1。最后没有被标记为 1 的所有内容都认为是垃圾,以后分配内存时会重新使用它们。
  • 增量的(Incremental):增量垃圾收集器不需要遍历全部数据对象。因为在收集期间的突然等待,也因为与访问所有当前数据相关的缓存问题(所有内容都不得不被页入(page-in)),遍历所有内存会引发问题。增量收集器避免了这些问题。
  • 保守的(Conservative):保守的垃圾收集器在管理内存时不需要知道与数据结构相关的任何信息。它们只查看所有数据类型,并假定它们 可以全部都是指针。所以,如果一个字节序列可以是一个指向一块被分配的内存的指针,那么收集器就将其标记为正在被引用。有时没有被引用的内存会被收集,这样会引发问题,例如,如果一个整数域中包含一个值,该值是已分配内存的地址。不过,这种情况极少发生,而且它只会浪费少量内存。保守的收集器的优势是,它们可以与任何编程语言相集成。

 

Hans Boehm 的保守垃圾收集器是可用的最流行的垃圾收集器之一,因为它是免费的,而且既是保守的又是增量的,可以使用 --enable-redirect-malloc 选项来构建它,并且可以将它用作系统分配程序的简易替代者(drop-in replacement)(用 malloc/ free 代替它自己的 API)。实际上,如果这样做,您就可以使用与我们在示例分配程序中所使用的相同的 LD_PRELOAD 技巧,在系统上的几乎任何程序中启用垃圾收集。如果您怀疑某个程序正在泄漏内存,那么您可以使用这个垃圾收集器来控制进程。在早期,当 Mozilla 严重地泄漏内存时,很多人在其中使用了这项技术。这种垃圾收集器既可以在 Windows® 下运行,也可以在 UNIX 下运行。

垃圾收集的一些优点:

  • 您永远不必担心内存的双重释放或者对象的生命周期。
  • 使用某些收集器,您可以使用与常规分配相同的 API。

 

其缺点包括:

  • 使用大部分收集器时,您都无法干涉何时释放内存。
  • 在多数情况下,垃圾收集比其他形式的内存管理更慢。
  • 垃圾收集错误引发的缺陷难于调试。
  • 如果您忘记将不再使用的指针设置为 null,那么仍然会有内存泄漏。

 




回页首


结束语

一切都需要折衷:性能、易用、易于实现、支持线程的能力等,这里只列出了其中的一些。为了满足项目的要求,有很多内存管理模式可以供您使用。每种模式都有大量的实现,各有其优缺点。对很多项目来说,使用编程环境默认的技术就足够了,不过,当您的项目有特殊的需要时,了解可用的选择将会有帮助。下表对比了本文中涉及的内存管理策略。

表 1. 内存分配策略的对比

策略 分配速度 回收速度 局部缓存 易用性 通用性 实时可用 SMP 线程友好
定制分配程序 取决于实现 取决于实现 取决于实现 很难 取决于实现 取决于实现
简单分配程序 内存使用少时较快 很快 容易
GNU malloc 容易
Hoard 容易
引用计数 N/A N/A 非常好 是(取决于 malloc 实现) 取决于实现
非常快 极好 是(取决于 malloc 实现) 取决于实现
垃圾收集 中(进行收集时慢) 几乎不
增量垃圾收集 几乎不
增量保守垃圾收集 容易 几乎不


参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文

Web 上的文档

基本的分配程序 池式分配程序 智能指针和定制分配程序
  • Loki C++ Library 有很多为 C++ 实现的通用模式,包括智能指针和一个定制的小对象分配程序。
垃圾收集器 关于现代操作系统中的虚拟内存的文章 关于 malloc 的文章 关于定制分配程序的文章 关于垃圾收集的文章 Web 上的通用参考资料 书籍 来自 developerWorks
  • 自我管理数据缓冲区内存 (developerWorks,2004 年 1 月)略述了一个用于管理内存的自管理的抽象数据缓存器的伪 C (pseudo-C)实现。

  • A framework for the user defined malloc replacement feature (developerWorks,2002 年 2 月)展示了如何利用 AIX 中的一个工具,使用自己设计的内存子系统取代原有的内存子系统。

  • 掌握 Linux 调试技术 (developerWorks,2002 年 8 月)描述了可以使用调试方法的 4 种不同情形:段错误、内存溢出、内存泄漏和挂起。

  • 处理 Java 程序中的内存漏洞 (developerWorks,2001 年 2 月)中,了解导致 Java 内存泄漏的原因,以及何时需要考虑它们。

  • developerWorks Linux 专区中,可以找到更多为 Linux 开发人员准备的参考资料。

  • 从 developerWorks 的 Speed-start your Linux app 专区中,可以下载运行于 Linux 之上的 IBM 中间件产品的免费测试版本,其中包括 WebSphere® Studio Application Developer、WebSphere Application Server、DB2® Universal Database、Tivoli® Access Manager 和 Tivoli Directory Server,查找 how-to 文章和技术支持。

  • 通过参与 developerWorks blogs 加入到 developerWorks 社区。

  • 可以在 Developer Bookstore Linux 专栏中定购 打折出售的 Linux 书籍


关于作者

 

Jonathan Bartlett 是 Programming from the Ground Up 一书的作者,这本书介绍的是 Linux 汇编语言编程。Jonathan Bartlett 是 New Media Worx 的总开发师,负责为客户开发 Web、视频、kiosk 和桌面应用程序。您可以通过 johnnyb@eskimo.com 与 Jonathan 联系。

4月23日

一个老工程师给年轻工程师的十大忠告

[1]好好规划自己的路,不要跟着感觉走!根据个人的理想决策安排,绝大部分人并不指望成为什么院士或教授,而是希望活得滋润一些,爽一些。那么,就需要慎重安排自己的轨迹。从哪个行业入手,逐渐对该行业深入了解,不要频繁跳槽,特别是不要为了一点工资而转移阵地,从长远看,这点钱根本不算什么,当你对一个行业有那么几年的体会,以后钱根本不是问题。频繁地动荡不是上策,最后你对哪个行业都没有摸透,永远是新手!
[2]可以做技术,切不可沉湎于技术。千万不可一门心思钻研技术!给自己很大压力,如果你的心思全部放在这上面,那么注定你将成为孔乙己一类的人物!适可而止为之,因为技术只不过是你今后前途的支柱之一,而且还不是最大的支柱,除非你只愿意到老还是个工程师!
[3]不要去做技术高手,只去做综合素质高手!在企业里混,我们时常瞧不起某人,说他“什么都不懂,凭啥拿那么多钱,凭啥升官!”这是普遍的典型的工程师的迂腐之言。8051很牛吗?人家能上去必然有他的本事,而且是你没有的本事。你想想,老板搞经营那么多年,难道见识不如你这个新兵?人家或许善于管理,善于领会老板意图,善于部门协调等等。因此务必培养自己多方面的能力,包括管理,亲和力,察言观色能力,攻关能力等,要成为综合素质的高手,则前途无量,否则只能躲在角落看示波器!技术以外的技能才是更重要的本事!!从古到今,美国*本,一律如此!
[4]多交社会三教九流的朋友!不要只和工程师交往,认为有共同语言,其实更重要的是和其他类人物交往,如果你希望有朝一*当老板或高层管理,那么你整*面对的就是这些人。了解他们的经历,思维习惯,爱好,学习他们处理问题的模式,了解社会各个角落的现象和问题,这是以后发展的巨大的本钱,没有这些以后就会笨手笨脚,跌跌撞撞,遇到重重困难,交不少学费,成功的概率大大降低!
[5]知识涉猎不一定专,但一定要广!多看看其他方面的书,金融,财会,进出口,税务,法律等等,为以后做一些积累,以后的用处会更大!会少交许多学费!!
[6]抓住时机向技术管理或市场销售方面的转变!要想有前途就不能一直搞开发,适当时候要转变为管理或销售,前途会更大,以前搞技术也没有白搞,以后还用得着。搞管理可以培养自己的领导能力,搞销售可以培养自己的市场概念和思维,同时为自己以后发展积累庞大的人脉!应该说这才是前途的真正支柱!!!
[7]逐渐克服自己的心里弱点和性格缺陷!多疑,敏感,天真(贬义,并不可爱),犹豫不决,胆怯,多虑,脸皮太薄,心不够黑,教条式思维。。。这些工程师普遍存在的性格弱点必须改变!很难吗?只在床上想一想当然不可能,去帮朋友守一个月地摊,包准有效果,去实践,而不要只想!不克服这些缺点,一切不可能,甚至连项目经理都当不好--尽管你可能技术不错!
[8]工作的同时要为以后做准备!建立自己的工作环境!及早为自己配置一个工作环境,装备电脑,示波器(可以买个二手的),仿真器,编程器等,业余可以接点活,一方面接触市场,培养市场感觉,同时也积累资金,更重要的是准备自己的产品,咱搞技术的没有钱,只有技术,技术的代表不是学历和证书,而是产品,拿出象样的产品,就可技术转让或与人合作搞企业!先把东西准备好,等待机会,否则,有了机会也抓不住!
[9]要学会善于推销自己!不仅要能干,还要能说,能写,善于利用一切机会推销自己,树立自己的品牌形象,很必要!要创造条件让别人了解自己,不然老板怎么知道你能干?外面的投资人怎么相信你?提早把自己推销出去,机会自然会来找你!搞个个人主页是个好注意!!特别是培养自己在行业的名气,有了名气,高薪机会自不在话下,更重要的是有合作的机会...
[10]该出手时便出手!永远不可能有100%把握!!!条件差不多就要大胆去干,去闯出自己的事业,不要犹豫,不要彷徨,干了不一定成功,但至少为下一次冲击积累了经验,不干永远没出息,而且要干成必然要经历失败。不经历风雨,怎么见彩虹,没有人能随随便便成功!
4月18日

J2ME游戏优化秘密

本文章描述了代码优化在为移动设备写运行起来速度快的游戏中扮演的角色。我会用例子说明如何、什么时候和为什么要优化你的代

码,来榨干兼容MIDP的手机的每一滴性能。我们将要讨论为什么优化是必要的和为什么有时候最好不要优化。我将解释高级优化和低

级优化的差别,然后我们会知道如何使用J2ME无线开发包(WTK)自带的Profile程序来发现到哪里去优化你的代码。这篇文章最后揭

示了很多让你的MIDlet运行的技术。
为什么优化?
 计算机游戏可以分为两大类: 实时的和输入驱动的. 输入驱动的游戏显示游戏的当前运行状态,并在继续之前无限地等待用户的输入.

扑克牌游戏属于这一类,同样,大多数的猜谜游戏、过关游戏和文字冒险游戏都属于这一类。实时游戏,有时候被称为技能或动作游戏

,不等待用户,他们不停地运行直到游戏结束。
 技能和动作游戏经常以大量的屏幕上运东为特征(想想Galaga游戏和Robotron游戏)。刷新率必须至少有10fps(每秒的帧数)并且要

有足够的动作来保持玩家的挑战性。它们需要玩家快速的反应和好的手眼配合,所以就强迫S&A(技能和动作)游戏必须对玩家的输入

有很强的响应能力。在快速响应玩家案件的同时提供高帧数的图形动作,这是实时游戏的代码必须运行起来快的原因。在用J2ME开发

的时候,挑战性就更大了。
 Java 2 Micro Edition(J2ME)是java的一个分解版本。 适用于有限功能的小型设备,比如手机和PDA。J2ME设备有:
 *有限的输入能力(没有键盘!)(译者注:这里键盘特指个人电脑的键盘)
 *小的显示尺寸
 *有限的内存容量和堆大小
 *慢速的CPU
 在J2ME平台上写出快的游戏-------写出在比桌面电脑里的慢得多的CPU上运行的代码更是挑战了开发者。
什么时候不优化
 如果你不是在写一个技能或者动作游戏,那么可能不需要优化。如果玩家已经为自己的下一步考虑了几秒钟抑或几分钟,她可能不会

介意如果你的游戏响应花掉了几百微秒。这个规则的一个例外是,如果这个游戏在决定下一步如何运行的时候有大量的工作要处理,比

如搜索一百万个可能的象棋片组合。这种情况下,你可能想要优化你的代码,从而在几秒钟内计算出电脑的下一步,而不是几分钟。
 就算你正在写这种类型的游戏,优化也可能是危险的。许多这样的技术伴随着一个代价--他们表示着好”的程序设计这个通常概念飞

过来的时候,同时使你的代码更难读懂。有些是一个权衡,需要开发者大大增加程序的大小来得到性能上一点点的改进。J2ME开发者

们对于保持他们的JAR尽可能的小这个挑战再熟悉不过了。这里是一些不优化的理由:
        *优化是一个增加bug的好手
 *有些技术会降低你的代码的移植性
 *你可能要花费大量的努力来得到微小的或者没有改进
 *优化是困难的
 最后一点需要一些阐述。优化是一个活动目标,在Java平台上更是这样,而且在J2ME上就更加突出,因为其运行环境是那样的多变。

你优化后的代码可能在一个模拟器上运行得更快,但却在实际设备上更慢,或者相反。为一部手机优化可能会降低其在另一部上的性能


 不过还是有希望。有两条路径你可以做优化,高层的和底层的。第一条基本上会在所有的平台上增加执行性能,甚至会改进你代码的

整个质量。第二条是可能会让你头疼的,但是那些底层技术是很容易创造的,而且更加容易消去如果你不想使用它们。最起码,他们看

起来很有趣。
 
 我们将用系统的timer在实际设备上剖析你的代码,这可以帮助你测量出那些技术在你所开发的硬件上到底有多有效。
  最后一点:
  *优化是有趣的
 
 一个反面例子:
 让我们来看一看这个包含两个类的简单的应用程序,首先,是Midlet...
  import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class OptimizeMe extends MIDlet implements CommandListener {
  private static final boolean debug = false;
  private Display display;
  private OCanvas oCanvas;
  private Form form;
  private StringItem timeItem = new StringItem( "Time: ", "Unknown" );
  private StringItem resultItem =
                            new StringItem( "Result: ", "No results" );
  private Command cmdStart = new Command( "Start", Command.SCREEN, 1 );
  private Command cmdExit = new Command( "Exit", Command.EXIT, 2 );
  public boolean running = true;
  public OptimizeMe() {
    display = Display.getDisplay(this);
    form = new Form( "Optimize" );
    form.append( timeItem );
    form.append( resultItem );
    form.addCommand( cmdStart );
    form.addCommand( cmdExit );
    form.setCommandListener( this );
    oCanvas = new OCanvas( this );
  }
  public void startApp() throws MIDletStateChangeException {
    running = true;
    display.setCurrent( form );
  }
  public void pauseApp() {
    running = false;
  }
  public void exitCanvas(int status) {
    debug( "exitCanvas - status = " + status );
    switch (status) {
      case OCanvas.USER_EXIT:
        timeItem.setText( "Aborted" );
        resultItem.setText( "Unknown" );
      break;
      case OCanvas.EXIT_DONE:
        timeItem.setText( oCanvas.elapsed+"ms" );
        resultItem.setText( String.valueOf( oCanvas.result ) );
      break;
    }
    display.setCurrent( form );
  }
  public void destroyApp(boolean unconditional)
                          throws MIDletStateChangeException {
    oCanvas = null;
    display.setCurrent ( null );
    display = null;
  }
  public void commandAction(Command c, Displayable d) {
    if ( c == cmdExit ) {
      oCanvas = null;
      display.setCurrent ( null );
      display = null;
      notifyDestroyed();
    }
    else {
      running = true;
      display.setCurrent( oCanvas );
      oCanvas.start();
    }
  }
  public static final void debug( String s ) {
    if (debug) System.out.println( s );
  }
}
Second, the OCanvas class that does most of the work in this example...
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.Random;
public class OCanvas extends Canvas implements Runnable {
  public static final int USER_EXIT = 1;
  public static final int EXIT_DONE = 2;
  public static final int LOOP_COUNT = 100;
  public static final int DRAW_COUNT = 16;
  public static final int NUMBER_COUNT = 64;
  public static final int DIVISOR_COUNT = 8;
  public static final int WAIT_TIME = 50;
  public static final int COLOR_BG = 0x00FFFFFF;
  public static final int COLOR_FG = 0x00000000;
  public long elapsed = 0l;
  public int exitStatus;
  public int result;
  private Thread animationThread;
  private OptimizeMe midlet;
  private boolean finished;
  private long started;
  private long frameStarted;
  private long frameTime;
  private int[] numbers;
  private int loopCounter;
  private Random random = new Random( System.currentTimeMillis() );
  public OCanvas( OptimizeMe _o ) {
    midlet = _o;
    numbers = new int[ NUMBER_COUNT ];
    for ( int i = 0 ; i < numbers.length ; i++ ) {
      numbers[i] = i+1;
    }
  }
  public synchronized void start() {
    started = frameStarted = System.currentTimeMillis();
    loopCounter = result = 0;
    finished = false;
    exitStatus = EXIT_DONE;
    animationThread = new Thread( this );
    animationThread.start();
  }
  public void run() {
    Thread currentThread = Thread.currentThread();
    try {
      while ( animationThread == currentThread && midlet.running
                                               && !finished ) {
        frameTime = System.currentTimeMillis() - frameStarted;
        frameStarted = System.currentTimeMillis();
        result += work( numbers );
        repaint();
        synchronized(this) {
          wait( WAIT_TIME );
        }
        loopCounter++;
        finished = ( loopCounter > LOOP_COUNT );
      }
    }
    catch ( InterruptedException ie ) {
      OptimizeMe.debug( "interrupted" );
    }
    elapsed = System.currentTimeMillis() - started;
    midlet.exitCanvas( exitStatus );
  }
  public void paint(Graphics g) {
    g.setColor( COLOR_BG );
    g.fillRect( 0, 0, getWidth(), getHeight() );
    g.setColor( COLOR_FG );
    g.setFont( Font.getFont( Font.FACE_PROPORTIONAL,
         Font.STYLE_BOLD | Font.STYLE_ITALIC, Font.SIZE_SMALL ) );
    for ( int i  = 0 ; i < DRAW_COUNT ; i ++ ) {
      g.drawString( frameTime + " ms per frame",
                    getRandom( getWidth() ),
                    getRandom( getHeight() ),
                    Graphics.TOP | Graphics.HCENTER );
    }
  }
  private int divisor;
  private int r;
  public synchronized int work( int[] n ) {
    r = 0;
    for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) {
      for ( int i = 0 ; i < n.length ; i++ ) {
        divisor = getDivisor(j);
        r += workMore( n, i, divisor );
      }
    }
    return r;
  }
  private int a;
  public synchronized int getDivisor( int n ) {
    if ( n == 0 ) return 1;
    a = 1;
    for ( int i = 0 ; i < n ; i++ ) {
      a *= 2;
    }
    return a;
  }
  public synchronized int workMore( int[] n, int _i, int _d ) {
    return n[_i] * n[_i] / _d + n[_i];
  }
  public void keyReleased(int keyCode) {
    if ( System.currentTimeMillis() - started > 1000l ) {
      exitStatus = USER_EXIT;
      midlet.running = false;
    }
  }
  private int getRandom( int bound )
  {  // return a random, positive integer less than bound
    return Math.abs( random.nextInt() % bound );
  }
}
 这个程序是一个模拟一个简单游戏循环的MIDlet:
  *work          执行
  *draw          绘制
  *poll for user input        等待用户输入
  *repeat          重复
  对于快速游戏,这个循环一定要尽可能的紧凑和快速。我们的循环持续一个有限的次数(LOOP_COUNT=100),并且用系统timer来计

算整个作业花费了多少毫秒,我们就可以测量并改善它的性能。时间和执行的结果会显示在一个简单的窗口上。用Start命令来开启测

试。按任意键会提前退出循环,退出按钮用来结束程序。-
  在大多数游戏里面,主游戏循环中的作业会更新整个游戏状态-----移动所有的角色,检测并处理冲突,更新分数,等等。在这个例

子里面,我们并没有做什么特别有用的事。程序仅仅是在一个数组之间做一些算数运算,然后
  把这些结果加起来。
  run()函数计算了每次循环所花费的时间。每一帧,OCanvas.paint()方法都在屏幕上的16个随机的地方显示这个数。一般的,

你可以用这个方法在你的游戏里面画出你的图像元素,我们的代码在该过程中作了一些有用的摹写。
  不管这些代码看起来有多么的无意义,它给了我们足够的机会去优化它的性能。
 
******************第二页
哪里去优化 -- 90/10规则
在苛求性能的游戏里面,有90%的时间是在执行其中%10的代码。我们的优化努力就应该针对这10%的代码。我们用一个Profier来定位

这 10%. 要运行J2ME无线开发包中的profier工具,选择edit菜单下的preferences选项. 这将会显示preferences窗口.选择

Monitoring这一栏,将"Enable Profiling"悬赏,然后点ok按钮。什么也没有出现。这是对的,在Profier窗口显示之前,我们需要

在模拟器中运行我们的程序然后退出。现在就做.
图1显示了如何打开Profiler工具。

 
 
 

我的模拟器(运行在Windows XP下,Inter P4 2.4GHz的CPU)报告我100次这个循环用了6,407毫秒,或者说6又1/2秒。这个程序报

告说62或者63毫秒每帧。在硬件(一个 motorola的i85s)上运行会慢得多。 一帧的时间大约是500毫秒,整个循环用了52460毫秒。

在本文这一课中,我们将试着改善这个数据。
当你退出这个程序时,profiler窗口就会出现,然后你会看见一个文件夹浏览器中有一些东西,在左边的面板上会有一个熟悉的树形

部件。方法间的联系会在这个结构列表中显示。每一个文件夹是一个方法,打开一个文件夹会显示它所调用过的方法。在该树中选择一

个方法会显示那个方法的profiling信息并在右边的面板显示所有被它调用过的方法。注意在每一个元素旁边显示了一个百分数。这就

是该方法在整个执行过程中所占的执行时间的百分比。我们必须翻遍这棵树,来寻找时间都到哪里去了,并对占用百分比最高的方法进

行优化,如果可能的话。
 
图2 -- Profiler程序调用的图

 
 
 

对这个profiler,有几点需要说明。首先你的百分比多半会和我的不一样,但是他们的比例会比较相似--总是在最大的数之后。我的

数据在被次运行的时候都会改变。为了保持情况一致,你可能希望关掉所有的后台程序,像Email客户端,并在你测试的时候保持你正

在进行的任务最少。还有,不要在用profiler之前混淆(obfuscate)你的代码,不然你的方法会被神秘的标示为b或者a或者ff。最

后profiler不会因为你运行模拟器的设备的差别而改变,它和硬件是完全独立的。
打开最高百分比的那个文件夹,我们看到有66.8%的时间在执行一个被称为

"com.sun.kvem.midp.lcdui.EmulEventHandler$EventLoop.run"的方法,这个对我们并没有什么帮助。用类似的方法,再往下寻

找更深层次的方法,持续下去,你就会找到一个大的百分比停留在serviceRepaints()上,最后到了我们的 OCanvas.paint()方法.另

外有30%的时间在OCanvas.run()方法里.这两个方法都在我们的主程序循环中,这并不奇怪.我们不会在我们的MIDlet类中花任何时间

做优化,同样地我们不会对游戏的主循环外的任何代码做优化.
在我们的例子程序中的百分比的划分在真实的游戏中并不是完全的没有特性. 你多半会在一个真实的视觉游戏中发现这个大的执行时

间的比例是在paint()方法中. 相比于非图形化程序,图形化程序总是要花很长的时间. 不幸的是,我们的图形程序已经被写在了J2ME

API这一层下,对于改善它们的性能,我们没有多少可以做的.我们可以做的是在用哪个和如何用它们之间做出聪明的决定.

高级vs低级优化

我们在该文章随后的地方会看到一些低级代码优化的技术.你会看见它们很容易被嵌入到现有代码中,并且在改善性能的同时相应的降

低其可读性. 在我们使用那些技术之前,最好还是继续在我们的代码和算法的设计上下功夫.这是高级优化.
Michael Abrash,"Quake"的一位开发者,一次写道,"the best optimizer is between your ears"(最好的游戏器就在你的两耳之

间).这有不只一种方法而且如果如果实现花更多的时间来思考正确的做事的方式,你会得到极大的回报. 使用正确的算法所带来的性能

提升,会比用低级优化技术在普通算法上作优化得到的提升大很多. 你用低级技术可能会得到几点百分比的提升,但是请首先从最上层

开始并且使用你的大脑(你可以在你的两耳之间找到它).
现在让我们来看一看我们在paint()方法中作了什么.每次在屏幕上打印消息"n ms per frame"时,我们调用了

Graphics.drawString() 16次. 我们不知道drawString的任何内部作业,但是我们知道它用掉了大量时间,所以让我们试试其它的方

式.让我们直接将这个字符串画到一个图片实例上, 然后再画16次这个图片.
public void paint(Graphics g) {
    g.setColor( COLOR_BG );
    g.fillRect( 0, 0, getWidth(), getHeight() );
    Font font = Font.getFont( Font.FACE_PROPORTIONAL,
                              Font.STYLE_BOLD | Font.STYLE_ITALIC,
                              Font.SIZE_SMALL );
    String msMessage = frameTime + "ms per frame";
    Image stringImage =
         Image.createImage( font.stringWidth( msMessage ),
                            font.getBaselinePosition() );
    Graphics imageGraphics = stringImage.getGraphics();
    imageGraphics.setColor( COLOR_BG );
    imageGraphics.fillRect( 0, 0, stringImage.getWidth(),
                            stringImage.getHeight() );
    imageGraphics.setColor( COLOR_FG );
    imageGraphics.setFont( font );
    imageGraphics.drawString( msMessage, 0, 0,
                              Graphics.TOP | Graphics.LEFT );
    for ( int i  = 0 ; i < DRAW_COUNT ; i ++ ) {
      g.drawImage( stringImage, getRandom( getWidth() ),
                   getRandom( getHeight() ),
                   Graphics.VCENTER | Graphics.HCENTER );
    }
  }
当我们运行这个版本的软件时,我们看到我们的paint()方法占用的时间百分比减少了一点点.往里看,我们看到drawString()方法只被

调用了101次,而且现在是敌人啊我Image方法执的次数最多,被调用了1616次。虽然我们做了更多的工作,,但是程序运行得快了一点

,因为我们所用的graphics调用要快一点。
你或许发现了吧一个字符串画到一个图片上会影响显示,因为J2MEbing不支持图片的透明,所以大量的背景被重写了。这是一个

weruhe优化可能导致你重新审核程序需求的例子。如果你真的需要与文字重合,你可能被迫要用更少的时间来处理。
这个代码或许好了一点点,但是它仍然有很大的可改进空间。让我们来看一看我们的第一个低级优化技术。
 

第三页
循环之外?
循环多少次,在for()内部的代码就会执行多少次。要改善性能,那么,我们想要尽可能的把循环中的代码移动到循环外。我们可以在

profiler中看到paint()被调用了101次,并且在它之中的循环又循环了16次。在这两个循环中有哪些我们可以移出来呢?让我们从他

们的定义说明开始,每当调用paint()时,我们声明了一个字体,一个字符串,一个图片对象和一个图形对象.我们将要把它们移出到该

类的最前面.
public static final Font font =
  Font.getFont( Font.FACE_PROPORTIONAL,
                Font.STYLE_BOLD | Font.STYLE_ITALIC,
                Font.SIZE_SMALL);
public static final int graphicAnchor =
                   Graphics.VCENTER | Graphics.HCENTER;
public static final int textAnchor =
      Graphics.TOP | Graphics.LEFT;
private static final String MESSAGE = " ms per frame";
private String msMessage = "000" + MESSAGE;
private Image stringImage;
private Graphics imageGraphics;
private long oldFrameTime;

你会发现,我把Font对象变成了一个公共的常量.这一点在你的程序中通常是有用的,你可以把你所要用到的字体声明都集中到一个地方

.我发现anchor也一样,所以我也把文本和图像坐标放到了一起.对这些的预处理,保持了这些运算--虽然不怎么重要--在循环之外了.
我把MESSAGE也变成了一个常量.那是因为Java喜欢到处创建字符串对象.字符串如果没有被控制,它们可能导致大量的内存消耗.不要

把它们留给自动回收,否则你很可能会遇到内存泄露,那最终会影响你的程序性能,特别是当垃圾回收器被调用得过于频繁时.字符串创

造垃圾,而垃圾不好.用一个字符串常量减少了这类问题.稍后我们会看到如何运用一个StringBuffer来完全的阻止字符串滥用带来的

内存流失.
既然我们把那些变成了实例变量,我们需要在构造函数里面添加这些代码:
stringImage = Image.createImage( font.stringWidth( msMessage ),
                                 font.getBaselinePosition() );
imageGraphics = stringImage.getGraphics();
imageGraphics.setFont( font );
另一个很酷的对于图形对象的大写字符的事是,我们可以设置一次字体然后就可以忘掉它了,不用每次在循环中都设置一次. 每次我们

还需要用fillRect()擦去图片对象. 热情的编码者可能会发现这里有一个机会从同一个图片创建两个图形对象,然后为fillRect()的

调用预设其中一个的颜色为COLOR_BG,并为 drawString()的调用预设另一个的颜色为COLOR_FG.不幸地,对同一个图片的多次调用

getGraphics()没有被定义,在不同的平台上不一样,于是你的技巧可能在Motorola上有效但在NOKIA上不行.如果不确定,就不做.
还有另一种改进我们的paint()的方法.再次使用我们的大脑我们认识到,如果从上次调用以来frameTime的值改变了,那么我们只需要

重画这个字符串.那是我们的新变量oldFrameTime到来的地方,下面是新的方法:
public void paint(Graphics g) {
  g.setColor( COLOR_BG );
  g.fillRect( 0, 0, getWidth(), getHeight() );
  if ( frameTime != oldFrameTime ) {
    msMessage = frameTime + MESSAGE;
    imageGraphics.setColor( COLOR_BG );
    imageGraphics.fillRect( 0, 0, stringImage.getWidth(),
                            stringImage.getHeight() );
    imageGraphics.setColor( COLOR_FG );
    imageGraphics.drawString( msMessage, 0, 0, textAnchor );
  }
  for ( int i  = 0 ; i < DRAW_COUNT ; i ++ ) {
    g.drawImage( stringImage, getRandom( getWidth() ),
                 getRandom( getHeight() ), graphicAnchor );
  }
  oldFrameTime = frameTime;
}
现在Profiler显示OCanvas的paint总共所花费的时间百分比已经降低为42.01%了.对比结果frameTime在 paint()中的调用,对

drawString()和fillRect()的调用次数已经从101变为69了.那时一个不错的节约,没有多少可以做的了,现在是该认真的时候了.你优

化得越多,它就变得越困难.现在我们要去挖掉最后几块循环中的代码.我们现在正在剃去非常小的百分比或者说百分比的碎片了,但是

我们比较幸运,他们加起来还是比较可观的.
让我们从一些简单的开始.让我们调用那些函数一次并且把结果暂存在循环之外,而不是每次都调用getHeight()和getWidth(). 下一

步,我们将停止使用字符串并手动使用StringBuffer来做所有事.依靠在Graphics.setClip()的调用中限制绘画区域,我们将剃掉一些

对drawImage()的调用.最后,我们将避免在循环中对java.util.Random.nextInt()的调用.
这是些新的变量...
  private static final String MESSAGE = "ms per frame:";
  private int iw, ih, dw, dh;
  private StringBuffer stringBuffer;
  private int messageLength;
  private int stringLength;
  private char[] stringChars;
  private static final int RANDOMCOUNT = 256;
  private int[] randomNumbersX = new int[RANDOMCOUNT];
  private int[] randomNumbersY = new int[RANDOMCOUNT];
  private int ri;
...这里是我们构造函数里的新代码:
iw = stringImage.getWidth();
ih = stringImage.getHeight();
dw = getWidth();
dh = getHeight();
for ( int i = 0 ; i < RANDOMCOUNT ; i++ ) {
  randomNumbersX[i] = getRandom( dw );
  randomNumbersY[i] = getRandom( dh );
}
ri = 0;
stringBuffer = new StringBuffer( MESSAGE+"000" );
messageLength = MESSAGE.length();
stringLength = stringBuffer.length();
stringChars = new char[stringLength];
stringBuffer.getChars( 0, stringLength, stringChars, 0 );

你现在可以看到我们在预处理显示(Display)和图片(Image).我们也在暂存512次getRandom()的调用的结果,有了StringBuffer也不

再需要msMessage这个字符串.当然,肉依然在paint()方法中:
  public void paint(Graphics g) {
    g.setColor( COLOR_BG );
    g.fillRect( 0, 0, dw, dh );
    if ( frameTime != oldFrameTime ) {
      stringBuffer.delete( messageLength, stringLength );
      stringBuffer.append( (int)frameTime );
      stringLength = stringBuffer.length();
      stringBuffer.getChars( messageLength,
                             stringLength,
                             stringChars,
                             messageLength );
      iw = font.charsWidth( stringChars, 0, stringLength );
      imageGraphics.setColor( COLOR_BG );
      imageGraphics.fillRect( 0, 0, iw, ih );
      imageGraphics.setColor( COLOR_FG );
      imageGraphics.drawChars( stringChars, 0,
                               stringLength, 0, 0, textAnchor );
    }
    for ( int i  = 0 ; i < DRAW_COUNT ; i ++ ) {
      g.setClip( randomNumbersX[ri], randomNumbersY[ri], iw, ih );
      g.drawImage( stringImage, randomNumbersX[ri],
                   randomNumbersY[ri], textAnchor );
      ri = (ri+1) % RANDOMCOUNT;
    }
    oldFrameTime = frameTime;
  }
我们现在正在用一个StringBuffer来写我们的消息中的字符.相比于在开头插入一个字符,在StringBuffer的后面添加要容易得多,所

以我把字符显示的顺序调换了,现在frameTime在消息的最后了,比如:"ms per frame:120".我们每次重写最后的几位frameTime字符

,保持消息的一部分不变. 像这样明白的运用StringBuffer会节约paint()方法内系统从创建到销毁字符串的时间.它是额外的工作,但

值得做.注意,我在把 frameTimer强制转换为一个整数.我发现用append(long) 导致了一个内存泄露.我不知道为什么,但这是一个为

什么你需要用软件注意事情的例子.
我们用font.charsWidth()来计算消息图片的宽度,以让我们可以画得最少.我们使用均衡字符,来使"ms per frame:1"的宽度比这绘

制它图片小,我们用Graphics.setClip(),所以我们就不需要画更多的. 这同样意味着我们只需要填充一个足够遮掩我们需要的区域那

么大的一个矩形.我们希望绘图省下的时间比调用font.charWidth()花去的时间多.
在这里这可能不会带来多大区别,但它确实是一个绘制玩家的分数到屏幕上的不错的技术.那种情况下,在绘制0分和150,000,000分之

间有着巨大的差别. 这多少是因为font.getBaselinePosition()的不正确的返回值,这个值好像和font.getHeight()的返回值一样,

啊! (叹气)
最后,我们刚刚搞定了我们的两个数组中预先计算的"随机"数,这节约了我们产生随机数的一些调用.注意用取模运算来实现一个循环数

组的用法.注意我们用同一个TextAnchor绘制图片和字符串,所以现在setClip()工作正常.
我们已经在一个灰色地带,怀着对这个版本的代码产生的数据。Profiler高速我们这个代码比没有这些改变的代码在paint()方法里

多花了大概7%多一点的时间。对font.charsWidth()的调用可能是个原因,它占了4.6%。(这并不多,但它可以被减少。注意我们每

次都会获取 MESSAGE字符串的宽度。我们可以简单的在循环体之间计算它,并简单的把它加到frameTime的宽度上。)同样,新的对

setClip()的调用被标识为0.85%,而且看起来大大的增加了drawImage所占用的时间百分比(从27.58%到33.94%)。
到这一点了,看起来所有额外的代码肯定会使执行慢下来,但是程序产生的书记和这个假设矛盾了。在模拟器上的数据上下波动,看起

来好像没有长时间的测试,是不能下决定了,但是我的i85s报告说额外的代码比不加要快一点点,在没有对setClip()或者

charsWidth()时数据是 37130毫秒,而两个都有的时候是36540。我做了我的耐心所能忍受的那么多次,结果都一致。这使执行环境

差异这一点的影响突出起来。一旦你到了一个你不能确定会不会有进展的地方,你可能会被迫继续所有在硬件上的测试,这需要大量的

对JAR文件的安装和卸载。
看起来我们已经从我们的图形程序段压榨出了大量的性能。现在是对我们的work()方法进行同样的高级和低级优化的时候了。让我们

来回顾一下那个方法:
  public synchronized int work( int[] n ) {
    r = 0;
    for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) {
      for ( int i = 0 ; i < n.length ; i++ ) {
        divisor = getDivisor(j);
        r += workMore( n, i, divisor );
      }
    }
    return r;
  }
每次在run()中的循环,我们都传递一个数组参数。在work()中的循环外计算了我们的除数,然后调用workMore()来做这个除法。这

里所有事都错了,你可能也发现了。因为一开始,程序员已经把getDivisor()的调用放到了循环内。如果j的值在循环内部没有改变,

那么除数是不变的,真的属于内循环外面。
但是让我们多想一想,这个调用本身就是完全不必要的。下面的代码做了同样的事情...
  public synchronized int work( int[] n ) {
    r = 0;
    divisor = 1;
    for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) {
      for ( int i = 0 ; i < n.length ; i++ ) {
        r += workMore( n, i, divisor );
      }
      divisor *= 2;
    }
    return r;
  }
...没有对getDivisor()的调用。现在我们的profiler告诉我们run()方法花了23.72%的时间,对应于我们做这些改进以前的38.78%

。请总是在使用低级优化技术之前首先使用你的大脑来优化。接下来,让我们看一看它们中的一些技术。
 
第四页
低级优化
所有的程序员都对子程序和函数---为了避免在多个地方重复,把程序中共用的代码从应用程序中提出来--的概念很熟悉。不幸地,这

个通常的 “好”变成习惯会影响性能,因为方法的调用会带来一定量的开销。最简单的减少对一个方法的调用所耗费的时间的方法是

,仔细地挑选他们的声明修饰语。我们的程序员已经很小心了,已经把他的work()和workMore()方法同步了,以防万一其它的线程同

时调用了它们。这很不错,但是如果我们对性能很看重,我们常常要做出一些牺牲,今天我们的牺牲是安全的。
好了,我们知道不会有其它人会调用这些方法,那么我们可以不怎么担心地把他们同步起来。还有什么其它的可以做?让我们来看一看

这个方法类型的列表:
*synchronized 该方法是最慢的,因为需要获取一个对象锁
*interface 该方法是次慢的
*instance 这个方法居中
*final 该方法比较快
*static 该方法是最快的
所以我们不应该让所有的都是synchronized,而且看起来我们甚至可以把work()和workMore标示为final static方法。这样做会减

少1%模拟器中run()方法所花的时间。
另一个影响方法调用性能的因素是,传递给该方法的参数的个数。我们调用了workMore()51712次,每次都传递一个整形数组和两个

整形变量给这个参数并返回一个整形变量。在这个有点微不足道的例子中,可以很容易地把workMore()方法拆散到work()的方法体中

来完全地避免这个调用。在真实世界中,这是个很难的决定,特别是当这意味着需要把代码复制到你程序周围的时候。在profiler上

测试一下来看和没做这一步之前到底有多大的差别。如果你不能把所有的方法去掉,试着减少你传递的参数的个数。参数越多,开销就

越大。
  public final static int work( int[] n ) {
    divisor = 1;
    r = 0;
    for ( int j = 0 ; j < DIVISOR_COUNT ; j++ ) {
      for ( int i = 0 ; i < n.length ; i++ ) {
        r += n[i] * n[i] / divisor + n[i];
      }
      divisor *= 2;
    }
    return r;
  }
哇哦!去掉workMore()的调用把run中的时间开销砍到了9.96%。现在开始,一路上都会上升。让我们来看一看两个最基本的优化技术

---降低强度(strength reduction)和解开循环。
降低强度就是将一个慢一点的操作用一个相对快一点的完成同样的工作的去替换。最普通的就是使用位移运算符,它和对2的乘除运算

是相等的。比如说,x>>2和x/4是相等的(2的2次幂),x<<10和x*1024是相等的(2的10次幂)。一个令人惊讶的巧合,我们的除数

总是2的幂方(是不是很幸运!),所以我们可以用位移来替换那些除法。
解开循环减少了代码控制流的开销,但在循环中做更多的操作,少执行几次循环,或者完全把循环去掉。由于我们的DIVISOR——

COUNT只是8,解开我们的循环变得简单起来。
public final static int work( int[] n ) {
  r = 0;
  for ( int i = 0 ; i < n.length ; i++ ) { r +=  n[i] * n[i]  + n[i]; }
  for ( int i = 0 ; i < n.length ; i++ ) { r +=  (n[i] * n[i] >> 1) + n[i]; }
  for ( int i = 0 ; i < n.length ; i++ ) { r +=  (n[i] * n[i] >> 2) + n[i]; }
  for ( int i = 0 ; i < n.length ; i++ ) { r +=  (n[i] * n[i] >> 3) + n[i]; }
  for ( int i = 0 ; i < n.length ; i++ ) { r +=  (n[i] * n[i] >> 4) + n[i]; }
  for ( int i = 0 ; i < n.length ; i++ ) { r +=  (n[i] * n[i] >> 5) + n[i]; }
  for ( int i = 0 ; i < n.length ; i++ ) { r +=  (n[i] * n[i] >> 6) + n[i]; }
  for ( int i = 0 ; i < n.length ; i++ ) { r +=  (n[i] * n[i] >> 7) + n[i]; }
  return r;
}
有两个重点,第一,你会发现解开我们的循环需要我们复制一些代码。这是你在J2ME中想要的最后一件事,程序员们总是在与JAR作战

,但要记得 JARing(打包)过程包括了压缩,而压缩工作对重复的代码最有效,所以上面的代码可能不会像你所想的那样对你的jar

文件大小产生大的影响。再者,这都是代价交换。你的代码可以很小,很快,易读,任意选择其中的两个。第二点是位移操作符合乘除

运算的优先级不一样,所以你常常需要在表达式周围放置括号,而乘除运算符则不需要。
解开循环和使用位移操作符提升了1%多一点,不算坏。现在让我们把注意力集中到数组访问上。数组在C中是快速的数据结构,但因为

那个原因他们也很危险---如果你的代码访问了超过数组尾部的地址,那么你就重写了你不应该访问的内存区域,而且结果通常都是可

怕的。
相比之下,java是一个很安全的语言---像那样执行到数组尾部以外会简单地抛出一个 ArrayIndexOutOfBoundsException(一个数组

地址越界异常)。每次访问数组的时候系统都检查数组的下标是否有效,这使得数组的访问比C中要慢。再者,对于java内部对于数组

的处理我们没有什么可以做的,但是我们可以在它周围作一些聪明的决定。在上面的代码中,举例来说,我们访问了n[i]24次。我们

可以通过把n[i]的值存储于一个变量来省略掉很多那样的数组访问。稍微高级一点的想法同样揭示了我们可以用聪明的多的方式重新

安排他们,像这样...
  private static int divisor;
  private static int r;
  private static int ni;
  public final static int work( int[] n ) {
    r = 0;
    for ( int i = 0 ; i < n.length ; i++ )  {
      ni = n[i];
      r +=  ni * ni + ni;
      r +=  (ni * ni >> 1) + ni;
      r +=  (ni * ni >> 2) + ni;
      r +=  (ni * ni >> 3) + ni;
      r +=  (ni * ni >> 4) + ni;
      r +=  (ni * ni >> 5) + ni;
      r +=  (ni * ni >> 6) + ni;
      r +=  (ni * ni >> 7) + ni;
    }
    return r;
  }
...把run()中耗费的时间减少到了6.18%,一点也不坏。在我们继续之前,让我多说一点关于数组的事。一个稍微高级的优化(也就是

“thought”)可能揭示了数组可能不是这里必要的正确的数据结构。想想一个链表或者一些其他的结构,如果他们将提升性能。第二

点,如果你将要使用一个数组而且也需要复制它的内容到另一个数组,请总是使用System.arraycopy()。完成同样的工作,它会比自

己写的函数要快一点。最后,数组的性能比java.util.Vector对象的性能要好一点。如果你需要一种Vectors提供的功能,想想自己

写代码并测试一下,确保你的代码要快一点。
好了,我们真的把优化的事情做完了。我们刚刚介绍了暂存数组变量和该变量的平房的另一对变量。你可能在想为什么那些变量在方法

体的声明之外就被声明了。他们在循环外是因为每次定义一个整形数都有一点开销,而且我们需要保持他们在循环外也有效,对么?错


什么都不假设,我们确实为整形变量的声明节约了时间,但是如果那些变量在方法内部的定义为局部的,代码实际上可能会更慢。这是

因为局部变量表现得更好,因为JVM解释一个在方法外声明的变量会花更长的时间。所以让我们把他们变为局部变量。
最后,我们可以微微改变一下for()循环。计算机处理和零比较比处理和其他的非零数比较要快。那意味着我们可以改变我们的循环的

顺序并像这样重写方法,我们就可以和零比较:
public final static int work( int[] n ) {
    int r = 0;
    int ni;
    int nis;
    int i;
    for ( i = n.length ; --i >= 0 ; ) {
      ni = n[i];
      nis = ni * ni;
      r +=  nis  + ni;
      r +=  (nis >> 1) + ni;
      r +=  (nis >> 2) + ni;
      r +=  (nis >> 3) + ni;
      r +=  (nis >> 4) + ni;
      r +=  (nis >> 5) + ni;
      r +=  (nis >> 6) + ni;
      r +=  (nis >> 7) + ni;
    }
    return r;
  }
就是它了!这个代码可能会快一点,但是 profiler的结果不是那么的明显,清楚地是这个方法变得难懂了。或许这里还有更多可改进

的空间,但是让我们再看一下paint()方法,看看我们所学的里面有没有什么在这儿可以介绍的。
记住我们所学的关于局部变量的么?如果你被迫要用一个实例变量,而且你在一个方法中引用了那个变量多次,它可能值得你创建一个

局部变量来让 JVM只处理那个引用一次。你将引入一个声明和一个赋值,这会让程序变慢,但根据经验,如果一个变量被引用了超过

两次,我们将会使用这个技术。
我们同样可以在我们的paint()方法中运用降低强度,用一个循环移位计数器来代替取模运算符。这只有在我们的随机数暂存数组的长

度是2的倍数(令人惊讶地)的时候是可能的。最后,我们可以把我们的比较运算改成总是和零比较。这里是新的改进过的paint()方

法:
  public void paint(Graphics g) {
    StringBuffer sb = stringBuffer;
    Graphics ig = imageGraphics;
    char[] sc = stringChars;
    int sl;
    int ml = messageLength;
    int ril = ri;
    int iw = 0;
    g.setColor( COLOR_BG );
    g.fillRect( 0, 0, dw, dh );
    if ( frameTime - oldFrameTime != 0  ) {
      sb.delete( ml, stringLength );
      sb.append( (int)frameTime );
      sl = stringLength = sb.length();
      sb.getChars( ml, sl, sc, ml );
      iw = font.charsWidth( sc, 0, sl );
      ig.setColor( COLOR_BG );
      ig.fillRect( 0, 0, iw, ih );
      ig.setColor( COLOR_FG );
      ig.drawChars( sc, 0, sl, 0, 0, textAnchor );
    }
    for ( int i  = DRAW_COUNT ; --i >=0  ; ) {
      g.setClip( randomNumbersX[ril], randomNumbersY[ril], iw, ih );
      g.drawImage( stringImage, randomNumbersX[ril],
                   randomNumbersY[ril], textAnchor );
      ril++;
      ril &= 255;
    }
    ri = ril;
    oldFrameTime = frameTime;
  }
此外,怀着对profiler结果的敬意,我们已经在路的尽头了。这些改变并不影响图形函数被调用的次数,所以最好情况下这个差别也

会很小。但是当我们把所有对我们的work()方法的改变组合起来并且装在新的JAR到设备上时,这个差别是很大的。我的motorola

i85s现在在14030毫秒内完成了测试----快了两倍多!
这个代码还有最后一点需要改变。我把它放到最后是因为它没有书写得特别好,并且我的经验是它的表现在不同的实现间不一样。看着

OCanvas中的start()和run()方法,你可以看到我已经用了一个单独的动画线程。这是Java中处理动画的传统方法。在游戏中用这个

技术的一个问题是,每当重复循环时,我们被迫等待系统事件,比如说按键或者一个命令被传输了。毕竟我们在一个同步块中调用

wait()方法等待。这是艰辛的优化代码。毕竟我们的困难工作优化了其他所有事情,但我们在最激烈的时候实际上什么正确的事情也

没能做。更坏的是,为WAIT_TIME得到一个好的数据并不简单。如果我们 wait()太长,游戏就变慢了。如果我们没有wait()足够的时

间,按键可能被错过然后游戏停止了对用户输入的响应。
J2ME提供了一个这个问题的解决方案,用Display.callSerially()方法。API声明callSerially (Runnable r)"导致了在repaint

周期完成不久之后,为了和事件流同步,Runnable对象r让其run()被推迟调用“[原文是:"causes the Runnable object r to

have its run() method called later, serialized with the event stream, soon after completion of the repaint

cycle"]。通过使用callSerially(),我们可以完全的取消对wait()的调用。系统会保证我们的work()和paint()方法和用户输入程

序同步地被调用,那样游戏就会保持可响应性。这里是一些新的方法...
  public void start() {
    started = frameStarted = System.currentTimeMillis();
    loopCounter = result = 0;
    finished = false;
    exitStatus = EXIT_DONE;
    run();
  }
  public void run() {
    frameTime = System.currentTimeMillis() - frameStarted;
    frameStarted = System.currentTimeMillis();
    if ( midlet.running && !finished ) {
      result += work( numbers );
      repaint();
      display.callSerially(this);
      loopCounter++;
      finished = ( loopCounter > LOOP_COUNT );
    }
    else {
      elapsed = System.currentTimeMillis() - started;
      midlet.exitCanvas( exitStatus );
    }
  }
...此外我们还需要声明并得到一个Display的句柄:
   Display display = Display.getDisplay( midlet );
没有了对wait()的调用,现在我的i85s在10180毫秒内完成了代码的运行---差不多省了40%。你可能希望更大的性能提升,在得知我

们刚刚消除了100次50毫秒的对wait()的调用,但是请记住这个技术也是关于用户输入响应的。
再次,让我强调一下这个动画方法需要小心使用。在使他在NOKIA上工作的时候我遇到的麻烦,而且他们所有的示例代码(即使是游戏

代码)都使用了wait()技术。即使是在Motorola上,我在动画Canvas上添加了命令对象的时候,使用callSerially()的时候也遇到

了问题。在你在家尝试这个之前请小心测试。
 
第五页

其他的技术
一个我不能在我的示例程序中包含的技术是,最佳的使用switch()。Switch非常普遍的用于实现有限状态自动机(Finite State

Machines),在为非玩家角色的行为控制做人工智能的代码时。在你使用switch的时候,像这样写代码是一个好的编程习惯:
  public static final int STATE_RUNNING = 1000;
  public static final int STATE_JUMPING = 2000;
  public static final int STATE_SHOOTING = 3000;
  switch ( n ) {
    case STATE_RUNNING:
      doRun();
    case STATE_JUMPING:
      doJump();
    case STATE_SHOOTING:
      doShoot();
  }
这没有什么不对的,这些变量很不错而且离得很远,万一我们想要加一个在RUNNING和JUMPING之间的变量,像 STATE_DUCKING =

2500。但是显然switch选项可以被编译为一个两字节的代码,如果所用的整数紧靠在一起那么这个两字节的代码会更快,所以这会更

好:
  public static final int STATE_RUNNING = 1;
  public static final int STATE_JUMPING = 2;
  public static final int STATE_SHOOTING = 3;
在使用定点数学库(Fixed Point math library)的时候,有一些优化你可以做。首先,如果你除了一个相同的数很多次,你应该计

算出那个数的倒数然后把运算改变为执行一个乘法。乘法要比除法快一点。所以不是...
  int fpP = FP.Div( fpX, fpD );
  int fpQ = FP.Div( fpY, fpD );
  int fpR = FP.Div( fpZ, fpD );
...你应该把它重新写成这样:
  int fpID = FP.Div( 1, fpD );
  int fpP = FP.Mul( fpX, fpID );
  int fpQ = FP.Mul( fpY, fpID );
  int fpR = FP.Mul( fpZ, fpID );
如果你在每一帧要做数百次的除法,这会有所帮助。第二点,不要默认你的FP数学函数库不错。don't take your FP math library

for granted.  如果你有它的源代码,打开它然后看一下里面发生了什么。保证所有的方法都被声明为final static并看看有没有机

会优化它的代码。比如,你可能发现这个乘法方法需要把int强制转换为long然后再转换回来:
public static final int Mul (int x, int y) {
  long z = (long) x * (long) y;
  return ((int) (z >> 16));
}
那些转换要花时间。冲突检测使用边界圆球或者半球(bounding circles or spheres)包括将int的平方相加。那会产生一些大的

可能会溢出你的int数据类型的最大值的数字。要避免这个,你可以写下自己的返回一个long型数的平方函数:
    public static final long Sqr (int x) {
      long z = (long) x;
      z *= z;
      return (z >> 16);
    }
这个优化的方法避免了两个转换。如果你要做大量的定点计算,你可能要考虑把所有的主游戏循环中的调用替换为long型的。那会节

约大量的方法调用和参数传递。你可能也发现当这个计算被手动写出的时候,你可以减少类型转换所需要的次数。如果你嵌套一些对你

的库的调用,这是尤其正确的。比如:
    int fpA = FP.Mul( FP.toInt(5),
                    FP.Mul( FP.Div( 1 / fpB ),
                    FP.Mul( FP.Div( fpC, fpD ),
                    FP.toInt( 13 ) ) ) );
花一些时间Take the time来拆开这些像这样的嵌套调用,然后看你是否能减少类型转换的次数。另一个方式是避免到long类型的转

换,如果你知道涉及到的数字足够小以至于他们肯定不会导致溢出。
在高级优化上,你应该看一些游戏设计上的文章。大量的已知的游戏设计中问题,比如3D几何和碰撞检测已经被非常优雅和有效地解

决了。如果你找不到java源代码,你很可能会找到C源代码或者伪代码来转换。例如,边界检查是一个普通的技术,我们已经在paint

()方法中用到了。我们只需要清除帧到帧之间所改变的那部分屏幕,而不是每次都把整个屏幕清除。因为图形程序相对来说较慢,你

会发现这额外的管理的代价---需要明了屏幕上哪些部分需要被清除--值得付出。
一些手机制造商提供了一些私有的API来帮助程序员们处理一些J2ME表现出来的的限制,比如声音的欠缺,图片透明度的欠缺,等等。

例如, Motorola提供了一个浮点数学库来在芯片上做浮点运算。这个库比最快的定点数学库快很多,精度也高很多。使用这个函数会

完全破坏你代码的可移植性,当然,如果在许多不同的手机上运行不是关键,他们是一个可选项。
结论
*只优化需要的代码
*只在有价值的地方优化
*用profiler来找要优化的地方
*在具体的设备上profiler无能为力,在硬件上使用System timer
*在于用低级技术之前,总是先研究你的代码并且试着改进算法
*绘图是慢的,所以尽量节俭地使用图形调用
*在可以减少绘制区域的地方使用setClip()
*尽可能的把东西放到循环之外
*拼命地预先计算和暂存
*字符串带来垃圾,垃圾不好,所以使用StringBuffers来代替
*什么都不假设
*可能就使用static final方法,避免synchronized修饰符
*传递尽可能少的参数到经常调用的方法
*如果可能,完全地去掉函数调用
*解开循环
*对2的幂的乘除运算用位移运算代替
*你可以使用位运算符代替取模运算来实现循环
*试着用零来代替和其他数的比较
*数组访问比C要慢,所以暂存数组元素
*消去公共的子表达式
*局部变量要比引用变量快
*如果可以callSerially()就不要wait()
*在switch()中使用小的变量作选项
*检查定点数学库并且优化它
*拆开嵌套的FP调用来减少类型转换
*除法比乘法慢,所以用乘于倒数来代替除法
*用使用过和测试过的算法
*为了保护可移植性,小心地使用私有高性能API

下一步去哪里?
优化是魔法。任何的计算机的心脏都是CPU,java的心脏在虚拟CPU,JVM(java虚拟机)。要榨干虚拟机的每点性能,你需要了解大

量的表层以下的事情是如何工作的。特别地,你需要知道哪些事情JVM可以做得快,哪些慢。寻找有java里层工作的可靠信息的网站。

你不必要学习如何按字节来写程序,但是你懂得越多,就越容易跟上优化你的程序性能的新方式。
没有什么能够代替经验。你会及时地发现关于J2ME的性能特性和所开发的手机的你个人的秘密。即使你不能编出有独特特性的代码,

你可以用它设计你的下一个游戏。在开发我的游戏的时候,我发现调用5次drawImage()来分别绘制5个有25像素的图片要比调用它一

次来绘制一个五倍大小的图片慢得多。这个只是肯定会帮助我设计我的下一个游戏。
祝你好运,玩得开心!
资源:
1. J2ME's official web site contains the latest on what's happening on this front.
2. Like wireless games? Read the Wireless Gaming Review.
3. Discuss J2ME Game Development at j2me.org
4. A great site on many aspects of Java Optimization
5. Another great site on Optimization
6. Many articles on J2ME performance tuning
7. The amazing Graphics Programming Black Book by Michael Abrash
8. The Art of Computer Game Design by Chris Crawford
关于作者:
Mike Shivas has been playing video games since before the advent of the 8-bit home microcomputer. He has been

programming in Java since 1996, has consulted for MasterCard on wireless solutions and is the published

author of several J2ME video games. Readers may contact Mike at mailto:mshivas@hotmail.com?subject=FastCode.

七个优秀的开源J2ME项目

MWT

Micro Window Toolkit(MWT)是一个用于开发J2ME 用户界面(UI)的工具包。它具有友好,强大,快速,开源 等特性。因为它的"灵感"来自 AWT,Swing和SWT。可以使用bitmap fonts等来自定义组件。它专门为嵌入式开发而设计和优化。基于LGPL发布。
项目地址:http://j2me-mwt.sourceforge.net/


1 - J2ME Desktop:
Download theJad andJar or view it withJava WebStart
2 - ELP ~ RPG:
Download theJad andJar or view it withJava WebStart

3 - Progress Bar:
Download theJad andJar or view it withJava WebStart

4 - Messenger Interface:
Download theJad andJar or view it withJava WebStart

J4ME-开源的UI Logger 项目

J4ME 是一个J2ME应用程序开发包。它包括一个UI框架,一个日记框架用于帮助调试因不同品牌手机 的差异而产生的问题,一个蓝牙GPS框架让你能够利用JSR-179 Location API接口从蓝牙GPS设备获取定位信息和一个Java类包其中包含了J2ME中没有的方法比如双精度型数字的计算等。
                                            ui.gif logging.gif gps.gif

OpenBaseMovil


BaseMovil是一个J2ME应用程序开发框架。它由多个比较独立类库组成包括:

OpenBaseMovil-core:整个框架的底层基础 类库,它主要提供国际化 支持、任务控制、加密、压缩、浮点支持、properties文件支持、一个MVC框架、一个事件框架。

OpenBaseMovil-db:一个关系型数据库 引擎,其中一个表格存储的数据可上千。

OpenBaseMovil-script:一个脚本 引擎。

OpenBaseMovil-ui:一个UI工具包。
这个几类库都能够完全集成在一起使用。

OpenBaseMovil.jpg 
http://www.openbasemovil.org/

J2ME 游戏 脚本引擎/J2me game script engine

一个非常小的script脚本引擎,可以同时在j2me / j2se / c++ platform上面运行

它的目标是使用这个脚本引擎便于开发一个更加灵活的j2me 游戏。
这个脚本有点像BASIC

这东西不错,是一个开源的脚本引擎,大家可以学习里面的思想

里面其中带了一个例子,是3子棋。感觉挺不错的。这东西在SF也挺活跃的。

http://sourceforge.net/projects/j2megamescript/

例子图片3_200710301619291.jpg

Y!Go

Y!Go这是一个Yahoo Messenger J2ME客户端。

http://ygo.sourceforge.net/

EBookME

EBookME是一个用于把导入的文本文件 (HTML,DOC,PDF,…)生成J2ME电子书籍格式(JAD,JAR)的Java程序。生成的电子书 籍可以在支持MIDP1.0的手机上阅读。

http://ebookme.sourceforge.net/

jMobileCore

jMobileCore 包是一个强大的工具用来开发J2ME应用程序.jMobileCore提供支持开发简洁的,基于Canvas的图形 用户接口,快速地数据访问,可靠地通信,简化多线程midlet应用程序。jMobileCore包可工作在任何支持J2ME (MIDP1.0 和CLDC1.0)的移动电话与PDA设备.

http://jmobilecore.sourceforge.net/

还有超级经典的J2ME-POLISH

http://www.j2mepolish.org

J2ME Polish 2.0

J2ME Polish 2.0 Release Candidate is here!

design example 1

design example 2

design example 3

j2me实现图片透明效果 (转)

代码
/**
*
* @author Jagie
*
*/
public class ShadowMIDlet extends MIDlet {
Canvas c = new ShadowCanvas();
public ShadowMIDlet() {
}
protected void startApp() throws MIDletStateChangeException {
Display.getDisplay(this).setCurrent(c);
}
protected void pauseApp() {
// TODO Auto-generated method stub
}
protected void destroyApp(boolean arg0) throws MIDletStateChangeException {
// TODO Auto-generated method stub
}
}
/**
*
* @author Jagie
*
*/
class ShadowCanvas extends Canvas implements Runnable {
int w, h;
// 原始图片
Image srcImage;
// 原始图片的像素数组
int[] srcRgbImage;
// 渐变图片的像素数组
int[] shadowRgbImage;
int imgWidth, imgHeight;
int count;
public ShadowCanvas() {
w = this.getWidth();
h = this.getHeight();
try {
srcImage = Image.createImage("/av.png");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
imgWidth = srcImage.getWidth();
imgHeight = srcImage.getHeight();
// 制造原始图片的像素数组,用一个int来代表每一个像素,按位表示方式是:0xAARRGGBB
srcRgbImage = new int[imgWidth * imgHeight];
// 获取原始图片的所有像素,参见MIDP APPI文档
srcImage.getRGB(srcRgbImage, 0, imgWidth, 0, 0, imgWidth, imgHeight);
shadowRgbImage = new int[srcRgbImage.length];
System.arraycopy(srcRgbImage, 0, shadowRgbImage, 0,
shadowRgbImage.length);
// 渐变图片的所有像素已开始都是全透明的
for (int i = 0; i < shadowRgbImage.length; i++) {
shadowRgbImage &= 0x00ffffff;
}
new Thread(this).start();
}
public void paint(Graphics g) {
g.setColor(0, 0, 0);
g.fillRect(0, 0, w, h);
// 绘制渐变图片
g.drawRGB(shadowRgbImage, 0, imgWidth, (w - imgWidth) / 2,
(h - imgHeight) / 2, imgWidth, imgHeight, true);
g.setColor(0, 255, 0);
g.drawString("count=" + count, w / 2, 30, Graphics.HCENTER
| Graphics.TOP);
}
public void run() {
while (true) {
boolean changed = false;
// 改变渐变图片的每一个像素
for (int i = 0; i < shadowRgbImage.length; i++) {
// 获取渐变图片的某一像素的alpha值
int alpha = (shadowRgbImage & 0xff000000) >>> 24;
// 原始图片的对应像素的alpha值
int oldAlpha = (srcRgbImage & 0xff000000) >>> 24;
if (alpha < oldAlpha) {
// alpha值++
shadowRgbImage = ((alpha + 1) << 24) | (shadowRgbImage & 0x00ffffff);
changed = true;
}
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
count++;
repaint();
// 当所有像素的alpha值都达到原始值后,线程运行结束
if (!changed) {
System.out.println("over");
break;
}
}
}
}

内存管理工具类,支持简单的垃圾回收(转)

开发J2AVA ME 程序最需要关注的是内存的占用,开发性能高,适配多种机型的软件不是件容易的事情,我在国庆时间整理下我的开发思路写了这个类,给大家参考下


package com.gggeye.util;

import java.util.Hashtable;
import java.util.Stack;
import java.util.TimerTask;
import java.util.Vector;

import com.gggeye.demo.Logger;

 

 
 /**
  *  内存管理,工具类,主要是用于项目的内存控制,
  *  此类相对重要点,因为涉及到各个方面的内存回收
  *  此类除了提供一些常用的方法进行回收外,还还提供一个自动回收的机制,不过不建议采用,自动回收机制是是当
  *  空闲内存小于指定内存大小时候,释放内存,回收的时间是5秒回收一次<br/>
   */
public   class MemoryManager implements Runnable {
   
    /**
     * 释放内存,当内存小于addtionMemory时候,进行垃圾回收,主动回收
     * @param addtionMemory
     */
    public final static void release(long addtionMemory) {
        long freeMemory = Runtime.getRuntime().freeMemory();
        //如果空闲的内存小于指定的内存,则回收
        if(freeMemory<=addtionMemory){
            Logger.println("回收前的内存==>" +MemoryManager.freeMemoryKByte());
            System.gc();
            Logger.println("回收后的内存==>" +MemoryManager.freeMemoryKByte());
        }
    }
   
    /**
     * 释放指定缓存对象
     * @param addtionMemory
     * @param cache
     */
    public final static void release(long addtionMemory, Object cache){
        release(addtionMemory);
        if(cache != null){
            if(cache instanceof java.util.Hashtable){
                Hashtable i = (Hashtable)cache;
                i.clear();         
            }else if(cache instanceof java.util.Vector){
                Vector i = (Vector)cache;
                i.removeAllElements();
            }else if(cache instanceof java.util.Stack){
                Stack i = (Stack)cache;
                i.removeAllElements();
            }        
            cache = null;
            System.gc();
        }   
    }
   
    /**
     * 得到系统空闲内存,单位是k
     * @return 返回空闲内存的大小
     */
    public final static String freeMemoryKByte(){
        return freeMemoryByte()/1024 + "k";
    }
   
   
    public final static long  freeMemoryByte(){
        return Runtime.getRuntime().freeMemory();
    }
   
   
 

    private static MemoryManager instance ;
    private static TimerTask task;
    /**
     * 自动回收内存机制,此方法,应该在系统第一次调用的时候调用,如果重复调用则会抛出异常
     * @param time
     */
    public static void autoGC(long time) throws java.lang.RuntimeException{
        if(instance == null){
            instance  = new MemoryManager();
            task = TimerTaskManager.getInstace().create(instance, time);
        }else
            throw new RuntimeException("GC is starting...");
    }
   
    public static void colse(){
        if(instance != null) instance = null;
        if(task != null){
            task.cancel();
            task = null;
        }
       
    }

    /**
     * 实现对内存的自动化管理
     */
    public void run() {
        //小于12k内存的时候释放内存
        release(1200000);
         
    }

}

java像素级图像处理与识别方法

转载,挺不错的文章

朋友要求帮忙做一个图片识别的小程序,因为要用在特定的环境下,所以决定采用java语言实现。首先用matlab实现了识别算法的仿真,因为只是对特定的数字组合的识别,所以非常的简单,放弃采用比较复杂的识别算法,采用最普通的像素比较的识别算法。(如果背景噪声比较复杂,可以考虑先滤波后识别)在写java程序的时候发现一些问题,网上关于图片像素级操作的资料不是太多,有的还不是太正确,特此写出自己的成果与大家分享。
核心类:BufferedImage,ImageIO
ImageIO类提供图象读写接口,可以对URL,InputStream等操作,得到图像信息十分的方便。
ImageIO在javax.imageio.*的包中,属于jdk中的标准类。提供的方法有:
read() 例:BufferedImage imd=ImageIO.read(new File(file));
write() 例:ImageIO.write(imd, "JPEG", new File("C:\\test"+k+".gif"));
//具体方法可以查找jdk doc
BufferedImage类是一个Image类的子类,与Image不同的是,它是在内存中创建和修改的,你可以显示它也可以不显示它,这就看你的具体需求了。这里因为我用于图像的识别所以就不需要显示出来了。你可以通过ImageIO的方法来读取一个文件到BufferedImage,也可以将其写回一个文件中去。类似的操作可以看前面的两个方法。以及参考jdk doc
因为我要识别类似于身份验证的一个数字串图片,所以我考虑把这些数字分离出来,存在不同的图像内,这里BufferedImage类提供一个很方便的办法。
getSubimage(int left,int top,int width,int height)
例:    BufferedImage newim[]=new BufferedImage[4];
newim[0]=imd.getSubimage(4,0,10,18);
newim[1]=imd.getSubimage(13,0,10,18);
newim[2]=imd.getSubimage(22,0,10,18);
newim[3]=imd.getSubimage(31,0,10,18);
最后为了得到图像的像素,我们需要的就是得到像素的方法,这个方法有很多,这里我介绍的是
getRGB(int x,int y) 得到特定像素点的RGB值。
例: pix=new int[10*18];pix[i*(10)+j]=newim[k].getRGB(j,i);
现在我们得到了像素,可以看出像素是一个一维数组,你如果不习惯可以考虑保存在一个二维的数组中,然后就来实施你的看家算法,什么小波变换,拉普拉斯算子,尽管来吧。怎么样是不是很方便呢?什么你好像看不太懂,好给你一些源程序好了,包括像素分解和识别算法。

源代码
/*
* Created on 2005-11-29
*
* TODO To change the template for this generated file go to
* Window - Preferences - Java - Code Style - Code Templates
*/
package com.syvin.image;

import java.awt.*;
import java.awt.image.*;
import java.io.FileOutputStream;
import java.io.*;
import java.io.InputStream;
import java.net.URL;
import javax.imageio.*;
public class MyImage{
   BufferedImage imd;//待识别图像

private int iw,ih;//图像宽和高

public final static String path="D:\\jyy\\app\\tomcat\\webapps\\userlogon\\a.jpg";

static public void main(String args[]) {
   try{
   MyImage app = new MyImage();//构造一个类
  
   String s=app.getImageNum("C:\\无标题.bmp");//得到识别字符串
   System.out.println("recognize result"+s);
   byte[] by=s.getBytes();
   File f=new File("C:\\testfile.txt");
   FileOutputStream fos=new FileOutputStream(f);//写入一个结果文件
   fos.write(by);
   fos.close();
   }catch(Exception e){
    e.printStackTrace();
   }
}

//构造函数
public MyImage() throws IOException {
  
    super("Image Test");
    try{
    }catch(Exception e){
     e.printStackTrace();
    }
}
//得到图像的值
public String getImageNum(String file){
  
   StringBuffer sb=new StringBuffer("");
   try{
   imd=ImageIO.read(new File(file));//用ImageIO的静态方法读取图像
BufferedImage newim[]=new BufferedImage[4];
int []x=new int[4];
        //将图像分成四块,因为要处理的文件有四个数字。
newim[0]=imd.getSubimage(4,0,10,18);
newim[1]=imd.getSubimage(13,0,10,18);
newim[2]=imd.getSubimage(22,0,10,18);
newim[3]=imd.getSubimage(31,0,10,18);

for(int k=0;k<4;k++){

x[k]=0;

ImageIO.write(newim[k], "JPEG", new File("C:\\test"+k+".gif"));
this.iw=newim[k].getWidth(null);
this.ih=newim[k].getHeight(null);
pix=new int[iw*ih];

//因为是二值图像,这里的方法将像素读取出来的同时,转换为0,1的图像数组。
for(int i=0;i
for(int j=0;j
   pix[i*(iw)+j]=newim[k].getRGB(j,i);
   if(pix[i*(iw)+j]==-1)
    pix[i*(iw)+j]=0;
   else pix[i*(iw)+j]=1;
  
   x[k]=x[k]+pix[i*(iw)+j];

}

}
//得到像匹配的数字。
int r=this.getMatchNum(pix);
sb.append(r);
System.out.println("x="+x[k]);
}
   }catch(Exception e){
    e.printStackTrace();
   }
return sb.toString();
}
//数字模板 0-9
static int[][] value={
   //num 0;
   {0,0,0,0,0,0,0,0,0,0,
    0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,1,1,0,0,0,0,
0,0,1,1,1,1,1,0,0,0,
0,0,1,1,0,0,1,1,0,0,
0,1,1,0,0,0,0,1,1,0,
0,1,1,0,0,0,0,1,1,0,
0,1,1,0,0,0,0,1,1,0,
0,1,1,0,0,0,0,1,1,0,
0,0,1,1,0,0,1,1,0,0,
0,0,0,1,1,1,1,0,0,0,
0,0,0,0,1,1,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0
    },
   //num 1
   {0,0,0,0,0,0,0,0,0,0,
   0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,1,1,0,0,0,
0,0,0,0,1,1,1,0,0,0,
0,0,0,1,1,1,1,0,0,0,
0,0,0,0,0,1,1,0,0,0,
0,0,0,0,0,1,1,0,0,0,
0,0,0,0,0,1,1,0,0,0,
0,0,0,0,0,1,1,0,0,0,
0,0,0,0,0,1,1,0,0,0,
0,0,0,0,0,1,1,0,0,0,
1,1,1,1,1,1,1,1,1,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0
},
//num2
,
//num3
,
//num4
,
//num5
,
//num6
,
//num7
,
//num8
,
//num9
};

//图像像素相减取绝对值得到最小熵的结果。
public int getMatchNum(int[] pix){
   int result=-1;
   int temp=100;
   int x;
   for(int k=0;k<=9;k++){
     x=0;
    for(int i=0;i
     x=x+Math.abs(pix[i]-value[k][i]);
   
    }
    /*for(int a=0;a<18;a++){
     for(int b=0;b<10;b++){
      System.out.print(pix[a*10+b]+"-"+value[k][a*10+b]+"|");
    
     }
     System.out.println();
   
    }*/
   
    if(x
    {
     temp=x;
     result=k;
    }
   
   }

   return result;
}

}

4月15日

彻底明白Java的IO系统

一. Input和Output
1. stream代表的是任何有能力产出数据的数据源,或是任何有能力接收数据的接收源。在Java的IO中,所有的stream(包括Input和Out stream)都包括两种类型:
1.1 以字节为导向的stream
以字节为导向的stream,表示以字节为单位从stream中读取或往stream中写入信息。以字节为导向的stream包括下面几种类型:
1) input stream:
1) ByteArrayInputStream:把内存中的一个缓冲区作为InputStream使用
2) StringBufferInputStream:把一个String对象作为InputStream
3) FileInputStream:把一个文件作为InputStream,实现对文件的读取操作
4) PipedInputStream:实现了pipe的概念,主要在线程中使用
5) SequenceInputStream:把多个InputStream合并为一个InputStream
2) Out stream
1) ByteArrayOutputStream:把信息存入内存中的一个缓冲区中
2) FileOutputStream:把信息存入文件中
3) PipedOutputStream:实现了pipe的概念,主要在线程中使用
4) SequenceOutputStream:把多个OutStream合并为一个OutStream
1.2 以Unicode字符为导向的stream
以Unicode字符为导向的stream,表示以Unicode字符为单位从stream中读取或往stream中写入信息。以Unicode字符为导向的stream包括下面几种类型:
1) Input Stream
1) CharArrayReader:与ByteArrayInputStream对应
2) StringReader:与StringBufferInputStream对应
3) FileReader:与FileInputStream对应
4) PipedReader:与PipedInputStream对应
2) Out Stream
1) CharArrayWrite:与ByteArrayOutputStream对应
2) StringWrite:无与之对应的以字节为导向的stream
3) FileWrite:与FileOutputStream对应
4) PipedWrite:与PipedOutputStream对应
以字符为导向的stream基本上对有与之相对应的以字节为导向的stream。两个对应类实现的功能相同,字是在操作时的导向不同。如CharArrayReader:和ByteArrayInputStream的作用都是把内存中的一个缓冲区作为InputStream使用,所不同的是前者每次从内存中读取一个字节的信息,而后者每次从内存中读取一个字符。
1.3 两种不现导向的stream之间的转换
InputStreamReader和OutputStreamReader:把一个以字节为导向的stream转换成一个以字符为导向的stream。
2. stream添加属性
2.1 “为stream添加属性”的作用
运用上面介绍的Java中操作IO的API,我们就可完成我们想完成的任何操作了。但通过FilterInputStream和FilterOutStream的子类,我们可以为stream添加属性。下面以一个例子来说明这种功能的作用。
如果我们要往一个文件中写入数据,我们可以这样操作:
FileOutStream fs = new FileOutStream(“test.txt”);
然后就可以通过产生的fs对象调用write()函数来往test.txt文件中写入数据了。但是,如果我们想实现“先把要写入文件的数据先缓存到内存中,再把缓存中的数据写入文件中”的功能时,上面的API就没有一个能满足我们的需求了。但是通过FilterInputStream和FilterOutStream的子类,为FileOutStream添加我们所需要的功能。
2.2 FilterInputStream的各种类型
2.2.1 用于封装以字节为导向的InputStream
1) DataInputStream:从stream中读取基本类型(int、char等)数据。
2) BufferedInputStream:使用缓冲区
3) LineNumberInputStream:会记录input stream内的行数,然后可以调用getLineNumber()和setLineNumber(int)
4) PushbackInputStream:很少用到,一般用于编译器开发
2.2.2 用于封装以字符为导向的InputStream
1) 没有与DataInputStream对应的类。除非在要使用readLine()时改用BufferedReader,否则使用DataInputStream
2) BufferedReader:与BufferedInputStream对应
3) LineNumberReader:与LineNumberInputStream对应
4) PushBackReader:与PushbackInputStream对应
2.3 FilterOutStream的各种类型
2.2.3 用于封装以字节为导向的OutputStream
1) DataIOutStream:往stream中输出基本类型(int、char等)数据。
2) BufferedOutStream:使用缓冲区
3) PrintStream:产生格式化输出
2.2.4 用于封装以字符为导向的OutputStream
1) BufferedWrite:与对应
2) PrintWrite:与对应
3. RandomAccessFile
1) 可通过RandomAccessFile对象完成对文件的读写操作
2) 在产生一个对象时,可指明要打开的文件的性质:r,只读;w,只写;rw可读写
3) 可以直接跳到文件中指定的位置
4. I/O应用的一个例子
import java.io.*;
public class TestIO{
public static void main(String[] args)
throws IOException{
//1.以行为单位从一个文件读取数据
BufferedReader in = 
new BufferedReader(
new FileReader("F:\\nepalon\\TestIO.java"));
String s, s2 = new String();
while((s = in.readLine()) != null)
s2 += s + "\n";
in.close();

//1b. 接收键盘的输入
BufferedReader stdin = 
new BufferedReader(
new InputStreamReader(System.in));
System.out.println("Enter a line:");
System.out.println(stdin.readLine());

//2. 从一个String对象中读取数据
StringReader in2 = new StringReader(s2);
int c;
while((c = in2.read()) != -1)
System.out.println((char)c);
in2.close();

//3. 从内存取出格式化输入
try{
DataInputStream in3 = 
new DataInputStream(
new ByteArrayInputStream(s2.getBytes()));
while(true)
System.out.println((char)in3.readByte()); 
}
catch(EOFException e){
System.out.println("End of stream");
}

//4. 输出到文件
try{
BufferedReader in4 =
new BufferedReader(
new StringReader(s2));
PrintWriter out1 =
new PrintWriter(
new BufferedWriter(
new FileWriter("F:\\nepalon\\ TestIO.out")));
int lineCount = 1;
while((s = in4.readLine()) != null)
out1.println(lineCount++ + ":" + s);
out1.close();
in4.close();
}
catch(EOFException ex){
System.out.println("End of stream");
}

//5. 数据的存储和恢复
try{
DataOutputStream out2 = 
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("F:\\nepalon\\ Data.txt")));
out2.writeDouble(3.1415926);
out2.writeChars("\nThas was pi:writeChars\n");
out2.writeBytes("Thas was pi:writeByte\n");
out2.close();
DataInputStream in5 =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("F:\\nepalon\\ Data.txt")));
BufferedReader in5br =
new BufferedReader(
new InputStreamReader(in5));
System.out.println(in5.readDouble());
System.out.println(in5br.readLine());
System.out.println(in5br.readLine());
}
catch(EOFException e){
System.out.println("End of stream");
}

//6. 通过RandomAccessFile操作文件
RandomAccessFile rf =
new RandomAccessFile("F:\\nepalon\\ rtest.dat", "rw");
for(int i=0; i<10>
rf.writeDouble(i*1.414);
rf.close();

rf = new RandomAccessFile("F:\\nepalon\\ rtest.dat", "r");
for(int i=0; i<10>
System.out.println("Value " + i + ":" + rf.readDouble());
rf.close();

rf = new RandomAccessFile("F:\\nepalon\\ rtest.dat", "rw");
rf.seek(5*8);
rf.writeDouble(47.0001);
rf.close();

rf = new RandomAccessFile("F:\\nepalon\\ rtest.dat", "r");
for(int i=0; i<10>
System.out.println("Value " + i + ":" + rf.readDouble());
rf.close();
}
}
关于代码的解释(以区为单位):
1区中,当读取文件时,先把文件内容读到缓存中,当调用in.readLine()时,再从缓存中以字符的方式读取数据(以下简称“缓存字节读取方式”)。
1b区中,由于想以缓存字节读取方式从标准IO(键盘)中读取数据,所以要先把标准IO(System.in)转换成字符导向的stream,再进行BufferedReader封装。
2区中,要以字符的形式从一个String对象中读取数据,所以要产生一个StringReader类型的stream。
4区中,对String对象s2读取数据时,先把对象中的数据存入缓存中,再从缓冲中进行读取;对TestIO.out文件进行操作时,先把格式化后的信息输出到缓存中,再把缓存中的信息输出到文件中。
5区中,对Data.txt文件进行输出时,是先把基本类型的数据输出屋缓存中,再把缓存中的数据输出到文件中;对文件进行读取操作时,先把文件中的数据读取到缓存中,再从缓存中以基本类型的形式进行读取。注意in5.readDouble()这一行。因为写入第一个writeDouble(),所以为了正确显示。也要以基本类型的形式进行读取。
6区是通过RandomAccessFile类对文件进行操作

成为游戏制作者,你准备好了么?

在进入游戏业之前,以下的情况你了解么?
组织一个开发团队需要至少20人磨合6~8个月,而需要50个这样的团队才有可能产生一个世界级的制作人;
开发一个大型MMO需要3年或者更长时间;
游戏开发的核心人员与新手的薪资相差悬殊;
大型游戏的代码量往往在20万行以上,而策划文本则可能超过50万字;
制作人在MMO项目制作期内主持超过300次会议,并且累计收发5万余封的工作信件;
在我负责面试新手的过程中,只有不到20%的应聘者做好了准备,更多的人对游戏开发的了解仅限于想当然的程度,不少新人将游戏业想象成“好玩的工作”,只需要玩的技能就可以有前途的职业;有人认为自己对于游戏有很多“独特而伟大”的想法,更多人进入游戏业的最重要理由是“我从小就在玩FC”。错误估计从事游戏开发工作的难度和所需要的能力,对于一个新人,不仅仅是能否通过面试这么简单。有些人在工作中被淘汰,或者在频繁的跳槽中一无建树,其原因仅仅是:他没准备好。看看下面的对话,你准备好了么?

1
初学者问制作人:“我去面试策划的时候,提出了几十个从来没有人想到的创意,可是居然没有被录用,这是为什么呢?”
制作人笑了笑:“首先,从来没有人想到的创意是不存在的;其次,不能被执行的创意有更精确的词来表述——空谈。”

点评:在进行开发的过程中,必须考虑创意是否能够实现,或者实现的成本是否可以忍受。无法实现的创意就是不名一文的空想。而有些创意实现的成本远远超出可以忍受的范围。作为一个初学者,或许喜欢通过表现自己的崭新想法和创意来获得认同,但是,在有经验的人士看来,这些“伟大创意”往往是陈年旧话。所以在提出之前请先考虑:您的创意是否真的没有别人想到过?还是别人也想到了但是无法做到?为什么他们做不到?他们做不到的原因是否也会阻碍您创意的执行呢?

2
“制作人太固执了!”初学者抱怨道,“我就喜欢历史军事题材的游戏,可是MMO军事策略提案总是通不过!”
“你是否评估过成本?是否做过市场分析?是否调查过竞争对手?”产品经理一口气问了三个问题,而初学者频频摇头。
“那么先做这些吧!”产品经理挠了挠头,“另外,你熟悉的战役中,有哪个将军是根据个人喜好来决定战略的?”

点评:游戏本身是一种商品,游戏公司要赚钱如同将军要打胜仗。所以,没有一个成熟的制作人会忽视市场的调查分析。对于一个专业的制作者,市场的反映、玩家的调查比自己的个人喜好重要的多。专业制作者与业余选手的重要区别之一就是能否放弃个人喜好进行客观的分析问题。从个人喜好出发而决定游戏开发类型题材的制作者并非不存在,但他们要么是上世纪产业兴起之初的个人英雄,要么,这些玩票产生的产品都严重亏损,并且在上市后半年内逐渐被玩家遗忘。

3
初学者:“我玩游戏玩了10几年了,什么游戏我都玩过,所以我一定能做好游戏!”
产品经理:“我坐车有20几年了,什么型号的车我都做过,你说我是不是应该去做汽车设计?”

点评:产品经理的话一语中的,正如坐车甚至开车的人很难设计车辆、影评家未必能作导演一样。一个足够长的游戏龄只能使你在面试的考官获得非常有限的加分。考官会想:“哦,这个小伙子可能比不玩游戏的人知道的多一些。”但考官不会因此录用你,他还是一样需要考核你的专业能力和工作技能。游戏行业的公司,不管是开发、发行,还是运营和渠道,除了会玩游戏,了解游戏之外都需要更多针对性的技能,比如市场人员需要行销的技能,程序人员需要技术能力等等。认为只要游戏玩的好,什么都不用学习就可以进游戏公司是非常幼稚的想法(但是笔者发现,每3份应聘策划的简历中,至少有1位对此想法深信不疑的人)。而且,过渡沉迷于游戏的人缺乏自制力,很难在游戏开发工作中有突出的表现。

4
初学者:“我觉得策划的主要职责是哄着程序和美术干活,沟通好就可以了。”
产品经理:“那你认为你和其他几个策划该如何分工?你负责哄那个程序?你计划怎么哄?三陪?”

点评:又一个将策划工作想象的非常容易和轻松的初学者,类似的想法还有“策划其实就是文案”、“没必要写详细策划案,策划可以随时过去跟程序美术说怎么作”、“策划就是出主意的”……如此种种、不胜枚举。要知道,策划需要设计构建整个项目,控制,绝非简单工种。很多应聘者将策划想象成简单的工作其实是由于自己心里没底,对这些朋友,我的建议是尽快充电,提升自己,可以参看王世颖的《百日造就游戏制作人》。

5
初学者:你看,别人WOW做了5年,只要给我们足够的时间,我们就能超过他们,做出比他们更NB的大作。
产品经理:我实话告诉你,如果明年再不出东西,我们全家搬到你们家吃饭。

点评:类似以上的对话可能在每个公司都出现过。暴雪有足够的声望和实力,使他们可以说服投资人接受不断的延期和天文数字的开发费用。事实上,全世界有这种好运气的开发团队不超过10家。作为一个开发者,不能将项目做好的期望寄托到自己不能控制的因素上:如果咱们有“足够长的时间”、“很多资金用来雇宫本茅”、“世界一流的程序”,咱们就能……。谁都可以做梦,但是这种梦不是一个成熟的职业人该做的,有这种表现的新人往往对自己信心不足,很多时候还有推卸责任的目的。一个真实事件就是,我曾听到某家公司的美术抱怨自己公司的引擎只能支持2000面的人物,他们说“要是能买虚幻II引擎就好了,天堂2有4000面。”事实上,2000面的人物可以做的很好,他们根本没有努力,而他们的辛苦写引擎的程序同事却变成了游戏人物难看的替罪羊。

6
行政总监:“你要求的薪资是5000,作为一个刚毕业的新手,你如何评估你未来应得的薪水?”
应聘者:“我觉得要在北京生活好,衣食住行都算上至少要这个收入吧,而且我听说游戏行业工资就是高!”
行政总监:“……”

点评:游戏业的招聘启示中确实经常见到年薪20万的职位。而且很多报道中都极力渲染游戏行业的人才缺口。这给很多人造成一种游戏行业非常赚钱的感觉。实际上,有经验的人才确实非常抢手,可以拿到20万甚至40万的年薪,但是完全没有经验的新人,其薪资水准与一般IT行业相差不大。原因很简单,就是没有经验的新人很多,而有经验的高手太少。所以,对于现在还是新人的你,如果你确实决定要做游戏,那么最好的选择就是做好心理准备承受入行初期的低收入时期,逐步获得经验,提升能力,最后成长为有经验的高手。
另外,上文中的应聘者还有一个错误,他把自己的生活需求等同于自己应获得的回报,这对公司是不公平的,公司应该根据员工的贡献和能力给予员工应得的回报。

7
技术总监:“这么简单的界面bug你为何没有检查出来?”
程序初学者摇摇头:“我根本没检查。”
“为什么?!”技术总监生气了。
程序初学者困惑的看着技术总监:“如果我都检查了,要QA(质量监督部门,负责检查产品问题——作者注)干什么?”

点评:我个人认为,缺乏基本的对质量负责的态度,是目前游戏业从业者中广泛存在的问题。游戏开发是一个环节非常紧密的工作,一个人的工作完成后,会传递给下一个人进行继续的设计和加工。有的初学者没有检查自己工作的习惯,他们急匆匆做完就交给下个人,他们认为即使出现问题,下一个人也会发现并且返回来修正。这是非常危险的,一旦有错误未被发现而继续传递下去,最后的结果可能导致整个团队为了这些错误返工。我希望让初学者了解,一旦你的工作失误经常影响到与你配合的同事,那么就会使你在他们心目中的被打上不可信任或者粗心大意的标签,如果这个团队充满这种不可信任的人,那么项目就岌岌可危了。所以,在准备应聘之前先告诉你自己,你要对自己的所有工作负责。

8
技术总监:“指针非常重要,你必须搞明白才能完成下一个小游戏。”
程序初学者:“我现在没时间,我最近一直在研究多线程。”

点评:很明显,一个连指针都无法理解的程序,是很难真正搞懂多线程的。游戏开发与其他的学习一样,能力提升需要顺序渐进,没有根基的好高骛远对于个人和项目都只是浪费时间。这个浅显的道理似乎大家都很理解,但是上文中的例子却经常出现,那么只能说明很多人对自己的评价出现了问题。所以,当你认为自己已经足够强的时候,先考虑清楚,那些主管安排的初级工作,你真的已经做到最好了么?

9
应聘者:“我对D&D系统(即龙与地下城——作者注)进行了多年的研究,精通奇幻游戏设定。所以我认为我很适合这个职位。”
制作人:“在D&D设定里面,2D6是什么意思?”
应聘者出汗了:“这个……其实我主要了解D&D的各个游戏的背景。”
制作人:“异域中,印记城在整个多元宇宙中的什么位置?”
应聘者大汗:“异域我不在行,其实我主要是玩博德之门比较多,能问我个博德之门的问题么……”

点评:经过2次提问,我们很容易发现,其实这位应聘者根本没有看过龙与地下城的战役设定、玩家手册或者城主手册。他仅仅玩过使用D&D规则的一个PC游戏,就自称“精通”、“进行了多年的研究”。在考官看来,要么他是一个不诚实的人,要么就是一个非常容易自满的人,而这种人通过面试的机会自然微乎其微。为了增加应聘成功的机会,非常多的应聘者在简历中夸大自己的能力,有的人甚至在工作经历上作假,对于这种简历,大部分考官都会选择放弃。没有任何经验的新手可能有机会,但是撒谎的人就没有机会,保持诚信是非常重要的。

10
应聘者:“我有激情,我愿意为了游戏制作每天工作16个小时!”
行政经理:“很好,除了激情你还有别的么?”

点评:很多新人是由于非常喜欢游戏,非常想创造自己梦想中的游戏来进入这个行业的。在刚刚进入行业的时候,他们富有激情,能够忍受长时间的工作压力,并且把一切困难都视为对于自己的挑战。激情看上去是很好的兴奋剂,使得新手能够大踏步的进步,甚至有很多行业内的招聘者也喜欢招聘富于激情的新手,原因不仅仅是他们的上进心,还有他们更能忍受较低的薪水。
激情的最大问题在于,它只是一种短期的精神状态,不代表任何竞争中的优势,更不能取代实力、经验的重要性,也不存在任何的门槛——很显然的,任何一个应聘者都很容易将自己包装成一个“最富激情”的人。即使是发自内心的激情,也不很难维持很长的时间,在激情的“三分钟热血”过后,失落感很有可能影响到新人的工作状态。另外,激情容易使人丧失判断力,作出激进和冒险的动作。
所以,在激情之余,要理性的分析自己的优点、缺点和特长,然后再决定是否真的要进入这个行业。

上面的10段对话仅仅是作者见到新人心态不良的一小部分。不可否认,在面试和试用的过程中,任何新手都有犯错的可能,但是,多做准备可以让你增加成功的几率,在本文的最后,我列出应聘者可能会被考官Cut掉的部分表现,供各位参考:
伪造工作经历、学历,盗用他人作品,或简历叙述与能力严重不符;
拒绝进行笔试和上机考试;
对被应聘公司没有丝毫了解;
将游戏制作工作想象的非常简单;
薪资要求远远超过能力水准,而且没有合理的解释;
无法说明之前工作的离职原因;
对自己的评价远远超出客观水平;
面试迟到或者等待考官时表现不佳;