
热爱分布式技术
Create an EC2 instance Sign up for AWS In Services -> EC2, click “Launch Instance” Choose the 64 bit Debian Jessie image Hit review and launch Save your SSH key pair! Install Java SSH into your instance and open /etc/apt/sources.list in your favorite editor. Add jessie-backports to the file: deb http://ftp.debian.org/debian jessie-backports main Now run sudo apt-get update, and install the following! $ sudo apt-get install openjdk-8-jre openjdk-8-jre-headless libjna-java Use Docker You need to install docker and docker compose. Then spin up the containers: $ git clone https://github.com/aphyr/jepsen && cd jepsen/docker $ ./up.sh $ docker exec -it jepsen-control bash Write your test Create a new Leiningen project: $ lein new foo && cd foo And edit src/jepsen/foo.clj — your first test! This does nothing! (ns jepsen.zookeeper (:require [jepsen.tests :as tests])) (defn zk-test [version] tests/noop-test) Edit test/jepsen/foo_test.clj to look like this: (ns jepsen.foo-test (:require [clojure.test :refer :all] [jepsen.core :as jepsen] [jepsen.foo :as foo])) (deftest a-test (is (:valid? (:results (jepsen/run! (foo/foo "3.4.5+dfsg-2")))))) That calls the test you just wrote (and passes it a version number). Finally! Run your test: $ lein test And hopefully you get something that ends with: Everything looks good! ヽ(‘ー`)ノ
Goal Kudu 主要面向 OLAP 应用,支持大规模数据存储,支持快速查询,并且支持实时数据更新。相比Hive 之类的SQL on Hadoop,性能会好不少,并且支持数据实时更新,这也是 Hive 的一个痛点;相比于一个传统的 OLAP 数据库,它所支持的数据规模可能要大一点,毕竟 Kudu 是水平扩展的。 Kudu 的paper里提到,它的一个设计目标是统一存储日志数据和线上数据,并且提供高效的查询。这也是我们团队目前想要实现的一个目标。 相关工作 目前团队使用的 Hive,Hive 能够查询大规模数据,但痛点也很明显:一来占用资源很多,经常一个 MapReduce 的Job 就能跑半个小时几十分钟,对集群资源的占用是相当大的;二来,查询延迟相当高,如果只是跑一些报表还没有太大问题,但是如果着急需要一些数据,等上半个小时可能就是想当麻烦的;三来,Hive 不支持实时数据更新,虽然 ORC 看起来能实现数据更新,但延迟、吞吐量都想当捉急,略显鸡肋。 也有调研过 Hive on HBase 之类的方案,能够实现数据实时更新,但最致命的一点是,它的效率比 Hive 本身还要低,例如在 join 两个大表的时候会用 hash join 的方式,并且赤裸裸地把其中一个表转成 hash table load 到内存里,但千万级的表根本就很难 load 到内存里。对于普通的查询来说,效率也要低不少。究其原因,还是 HBase 的scan 性能不足,对于OLAP 来说,顺序 scan 的性能相当关键,这也是 HDFS 能够胜任的原因所在。 目前看来,基于 HDFS的方案通常都能提供不错的查询性能,但对于 实时更新的要求来说就有点捉襟见肘了;基于HBase、Cassandra的方案能够支持实时数据更新,但 scan 的吞吐量不能满足。 还有一些方案,例如 Facebook 的Presto,一些商业的 OLAP 数据库,Vertica 之类,使用门槛也并不低,并且前景也不明确。 Performance 评价 Kudu,当然是先看性能。官方的 paper 上有一些性能的比较:对于顺序 scan,比parquet 格式的 HDFS 存储性能不相上下;相比于 Apache Phoenix,性能秒杀;而 Random Access 的性能,略逊色于 HBase。 对于这样的结果,应该可以说是非常赞的。不过我们还是持谨慎的态度,自己又做了一次 benchmark。考虑到使用场景,我们并没有采用 tpc-h benchmark,而是 采用了 tpc-ds ,这也是 Hive 所采用的 benchmark,能有效评价大规模数据下的表现。 机器性能一般,四台机器,12核,32GB 内存,SSD 硬盘。实际测试中发现内存和CPU 占用都不太高。 使用了10GB数据的benchmark,具体 benchmark 的结果过于冗长,简而言之,在机器配置接近的情况下(Hive 用了另一个集群跑的,性能要高一点),Kudu 的执行时间通常在 Hive 的十分之一左右。不得不服。不过也存在一些问题,不支持 bulk load,导入数据这一过程还是非常慢的,几亿行的数据要几十分钟了。 不过从目前的结果来看,基本能满足我们的要求。 Features Columar Storage 既然是面向 OLAP,那么 Kudu 还是使用列式存储,比 HBase 要好一点的是,它每列都是单独存储,几乎没有 column family 的限制。 C++ Kudu 使用 C++ 开发,相比于 Hadoop 生态众多使用 java 开发的程序来说,性能会有一定优势,并且在 GC上,算是解决了 HBase GC停顿的痛点。由于使用了 C++,Kudu 在一些细节上也做了很多优化,例如 SSE 指令,在row projection 的时候使用 LLVM 进行 JIT编译,据说这些优化带来了显著的性能提升。不过使用 C++ 也可能会存在一些问题,比如说和 Hadoop 生态的整合如何搞定呢? Hadoop Ecosystem Kudu 在开发时完全考虑 Hadoop 生态,尽量减少使用成本。首先查询引擎使用 Impala,如果一开始就使用 Impala 的话使用成本会降低很多;与 Spark 集成时,也有相应的接口,可以把 Kudu 的数据 load 到一个 RDD中进行操作,或者一个 DataFrame,用SparkSQL 进行查询。 实际使用了一番,虽然 API 确实很好用,跟开发一个普通的 Spark 程序别无二致,但是其吞吐量还是差了不少,把整个数据库的内容 load 到内存还是相当慢的。在这一点上应该没办法完全替代 HDFS。 HA Kudu 还是继承了GFS、Bigtable 的传统,集群架构跟 Bigtable 很像,分为 master、tablet server。master 存储元数据,tablet 管理数据。不过 master/slave 的 HA 做得并不好,master 通常还是存在单点问题。在这一点上,Kudu 采用了multi master 的方案:master 的数据用Raft做replication,多个master也分 leader/follower,用Raft 做 leader election。至于 tablet server,也分leader/follower,同样使用 Raft 做replication和leader election。 使用 Raft这样的一致性协议貌似已经成了共识,新出现的分布式系统很少使用简单的 master/slave 了。对于 multi master 的了解还不是很多,不知道会不会带来新的问题。 Partition Kudu 的 partition 策略有两种,并且是正交的,一种是 hash partition,另一种是range partition。range partition 比较适合时间序列数据,例如日志,可以每天划分一个partition,这样的访问效率也会比较高。 存储模型 Kudu 的存储模型类似关系模型,支持primary key,数据也是强类型的,有 int、string之类的数据类型。不过目前还不支持辅助索引,也许以后会实现。 一致性模型,支持snapshot scan,用MVCC保证,也就是说一次scan 过程中读到的数据是一致的。不过不支持多行事务,对于 AP数据来说也没有太大的必要性。 时间戳,与 HBase 不同,不支持 write 操作指定时间戳,但是在read 的时候可以指定。 存储引擎 Kudu 最大的特点还是它的存储引擎,也是它的性能保证。Kudu 的存储引擎没有像 HBase 一样基于 HDFS,而是基于单机的文件系统。(貌似现在的另一个流行趋势,就是不再基于分布式文件系统来搞分布式数据库了,可能是基于 immutable 存储来搞 mutable 确实比较累。) 单机的存储引擎整合了 LSM、B tree等经典的结构,可以看到 LevelDB、Parquet的影子。存储还是分为 memory 和 disk,数据先写入 memory和 write ahead log,再刷到 disk。整个存储抽象成 RowSet,细化为 MemRowSet 和 DiskRowSet。 MemRowSet,就是一个 B+ tree,树叶节点的大小是 1K,刚好是4块 cache-line(!!!);使用 SSE2 指令进行 scan,据说性能非常高。 对于 DiskRowSet,分为 base 和 delta,更新的数据写到 delta,定时 compact 到 base,没有像 LevelDB那样使用多级的 LSM。base 是一个经典的列式存储实现,针对不同的数据类型采用了不同的编码方案,例如字典编码、行程编码、front encoding 等等技术都用上了,尽量减少空间占用。在数据存储的基础上,使用了B+ tree 对primary key 进行索引,也用了 BloomFilter 加速查找。值得一提的是,BloomFilter 的大小是4KB,刚好又是filesystem pagecache 的大小。DiskRowSet 的设计借鉴了很多 Parquet 的思想,值得深入学习。 实际使用 部署安装相当简单,添加相应的 repository 就可以安装,配置精简,无须一些乱七八糟的配置 稳定性还可以,跑了几天,也跑了一些负载比较高的任务,没有挂掉 管理控制做的还不是很到位,命令行提供的功能相对较弱 资源占用很少,机器的负载很低 性能相当赞,碾压 Hive 的快感 总结 之后还会做一些使用跟调研,希望能够尽快在生产环境中用上 Kudu。
准备: nccat for windows/linux 都可以 通过 TCP 套接字连接,从流数据中创建了一个 Spark DStream/ Flink DataSream, 然后进行处理, 时间窗口大小为10s 因为 示例需要, 所以 需要下载一个netcat, 来构造流的输入。 代码: spark streaming package cn.kee.spark; public final class JavaNetworkWordCount { private static final Pattern SPACE = Pattern.compile(" "); public static void main(String[] args) throws Exception { if (args.length < 2) { System.err.println("Usage: JavaNetworkWordCount <hostname> <port>"); System.exit(1); } StreamingExamples.setStreamingLogLevels(); SparkConf sparkConf = new SparkConf().setAppName("JavaNetworkWordCount"); JavaStreamingContext ssc = new JavaStreamingContext(sparkConf, Durations.seconds(1)); JavaReceiverInputDStream<String> lines = ssc.socketTextStream( args[0], Integer.parseInt(args[1]), StorageLevels.MEMORY_AND_DISK_SER); JavaDStream<String> words = lines.flatMap(new FlatMapFunction<String, String>() { @Override public Iterator<String> call(String x) { return Arrays.asList(SPACE.split(x)).iterator(); } }); JavaPairDStream<String, Integer> wordCounts = words.mapToPair( new PairFunction<String, String, Integer>() { @Override public Tuple2<String, Integer> call(String s) { return new Tuple2<>(s, 1); } }).reduceByKey(new Function2<Integer, Integer, Integer>() { @Override public Integer call(Integer i1, Integer i2) { return i1 + i2; } }); wordCounts.print(); ssc.start(); ssc.awaitTermination(); } } Flink DataSream package cn.kee.flink; import org.apache.flink.api.common.functions.FlatMapFunction; import org.apache.flink.api.common.functions.ReduceFunction; import org.apache.flink.api.java.utils.ParameterTool; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.streaming.api.windowing.time.Time; import org.apache.flink.util.Collector; /** * Example :SocketWindowWordCount * @author keehang * */ public class SocketWindowWordCount { public static void main(String[] args) throws Exception { // the port to connect to final int port = 9999; /*try { final ParameterTool params = ParameterTool.fromArgs(args); port = params.getInt("port"); } catch (Exception e) { System.err.println("No port specified. Please run 'SocketWindowWordCount --port <port>'"); return; }*/ // get the execution environment final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); // get input data by connecting to the socket DataStream<String> text = env.socketTextStream("localhost", port, "\n"); // parse the data, group it, window it, and aggregate the counts DataStream<WordWithCount> windowCounts = text .flatMap(new FlatMapFunction<String, WordWithCount>() { @Override public void flatMap(String value, Collector<WordWithCount> out) { for (String word : value.split("\\s")) { out.collect(new WordWithCount(word, 1L)); } } }) .keyBy("word") .timeWindow(Time.seconds(5), Time.seconds(1)) .reduce(new ReduceFunction<WordWithCount>() { @Override public WordWithCount reduce(WordWithCount a, WordWithCount b) { return new WordWithCount(a.word, a.count + b.count); } }); // print the results with a single thread, rather than in parallel windowCounts.print().setParallelism(1); env.execute("Socket Window WordCount"); } } 结果: Spark是一种快速、通用的计算集群系统,Spark提出的最主要抽象概念是弹性分布式数据集(RDD),它是一个元素集合,划分到集群的各个节点上,可以被并行操作。用户也可以让Spark保留一个RDD在内存中,使其能在并行操作中被有效的重复使用。 Flink是可扩展的批处理和流式数据处理的数据处理平台,设计思想主要来源于Hadoop、MPP数据库、流式计算系统等,支持增量迭代计算。 总结:Spark和Flink全部都运行在Hadoop YARN上,性能为Flink > Spark > Hadoop(MR),迭代次数越多越明显,性能上,Flink优于Spark和Hadoop最主要的原因是Flink支持增量迭代,具有对迭代自动优化的功能 流式计算比较 它们都支持流式计算,Flink是一行一行处理,而Spark是基于数据片集合(RDD)进行小批量处理,所以Spark在流式处理方面,不可避免增加一些延时。Flink的流式计算跟Storm性能差不多,支持毫秒级计算,而Spark则只能支持秒级计算。 SQL支持 都支持,Spark对SQL的支持比Flink支持的范围要大一些,另外Spark支持对SQL的优化,而Flink支持主要是对API级的优化。 Spark 感觉2.x 后主要在spark sql 这里发展优势,快速Join操作,以及继续扩展sql支持。至于Flink,其对于流式计算和迭代计算支持力度将会更加增强。
本文目录 什么是Java反射,有什么用? Java Class文件的结构 Java Class加载的过程 反射在native的实现 附录 1. 什么是Java反射,有什么用? 反射使程序代码能够接入装载到JVM中的类的内部信息,允许在编写与执行时,而不是源代码中选定的类协作的代码,是以开发效率换运行效率的一种手段。这使反射成为构建灵活应用的主要工具。 反射可以: 调用一些私有方法,实现黑科技。比如双卡短信发送、设置状态栏颜色、自动挂电话等。 实现序列化与反序列化,比如PO的ORM,Json解析等。 实现跨平台兼容,比如JDK中的SocketImpl的实现 通过xml或注解,实现依赖注入(DI),注解处理,动态代理,单元测试等功能。比如Retrofit、Spring或者Dagger 2. Java Class文件的结构 在*.class文件中,以Byte流的形式进行Class的存储,通过一系列Load,Parse后,Java代码实际上可以映射为下图的结构体,这里可以用javap命令或者IDE插件进行查看。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 typedef struct { u4 magic;/*0xCAFEBABE*/ u2 minor_version; /*网上有表可查*/ u2 major_version; /*网上有表可查*/ u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; //重要 u2 fields_count; field_info fields[fields_count]; //重要 u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }ClassBlock; 常量池(constant pool):类似于C中的DATA段与BSS段,提供常量、字符串、方法名等值或者符号(可以看作偏移定值的指针)的存放 access_flags: 对Class的flag修饰 1 2 3 4 5 6 7 typedef enum { ACC_PUBLIC = 0x0001, ACC_FINAL = 0x0010, ACC_SUPER = 0x0020, ACC_INTERFACE = 0x0200, ACC_ACSTRACT = 0x0400 }AccessFlag this class/super class/interface: 一个长度为u2的指针,指向常量池中真正的地址,将在Link阶段进行符号解引。 filed: 字段信息,结构体如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 typedef struct fieldblock { char *name; char *type; char *signature; u2 access_flags; u2 constant; union { union { char data[8]; uintptr_t u; long long l; void *p; int i; } static_value; u4 offset; } u; } FieldBlock; method: 提供descriptor, access_flags, Code等索引,并指向常量池: 它的结构体如下,详细在这里 1 2 3 4 5 6 7 8 9 method_info { u2 access_flags; u2 name_index; //the parameters that the method takes and the //value that it return u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; } 以上具体内容可以参考 JVM文档 周志明的《深入理解Java虚拟机》,少见的国内精品书籍 一些国外教程的解析 3. Java Class加载的过程 Class的加载主要分为两步 第一步通过ClassLoader进行读取、连结操作 第二步进行Class的<clinit>()初始化。 3.1. Classloader加载过程 ClassLoader用于加载、连接、缓存Class,可以通过纯Java或者native进行实现。在JVM的native代码中,ClassLoader内部维护着一个线程安全的HashTable<String,Class>,用于实现对Class字节流解码后的缓存,如果HashTable中已经有了缓存,则直接返回缓存;反之,在获得类名后,通过读取文件、网络上的class字节流反序列化为JVM中native的C结构体,接着malloc内存,并将指针缓存在HashTable中。 下面是非数组情况下ClassLoader的流程 find/load: 将文件反序列化为C结构体。 Class反序列化的流程 link: 根据Class结构体常量池进行符号的解引。比如对象计算内存空间,创建方法表,native invoker,接口方法表,finalizer函数等工作。 3.2. 初始化过程 当ClassLoader加载Class结束后,将进行Class的初始化操作。主要执行<clinit()>的静态代码段与静态变量(取决于源码顺序)。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Sample { //step.1 static int b = 2; //step.2 static { b = 3; } public static void main(String[] args) { Sample s = new Sample(); System.out.println(s.b); //b=3 } } 具体参考如下: When and how a Java class is loaded and initialized? The Lifetime of a Type 在完成初始化后,就是Object的构造<init>了,本文暂不讨论。 4. 反射在native的实现 反射在Java中可以直接调用,不过最终调用的仍是native方法,以下为主流反射操作的实现。 4.1. Class.forName的实现 Class.forName可以通过包名寻找Class对象,比如Class.forName("java.lang.String")。 在JDK的源码实现中,可以发现最终调用的是native方法forName0(),它在JVM中调用的实际是findClassFromClassLoader(),原理与ClassLoader的流程一样,具体实现已经在上面介绍过了。 4.2. getDeclaredFields的实现 在JDK源码中,可以知道class.getDeclaredFields()方法实际调用的是native方法getDeclaredFields0(),它在JVM主要实现步骤如下 根据Class结构体信息,获取field_count与fields[]字段,这个字段早已在load过程中被放入了 根据field_count的大小分配内存、创建数组 将数组进行forEach循环,通过fields[]中的信息依次创建Object对象 返回数组指针 主要慢在如下方面 创建、计算、分配数组对象 对字段进行循环赋值 4.3. Method.invoke的实现 以下为无同步、无异常的情况下调用的步骤 创建Frame 如果对象flag为native,交给native_handler进行处理 在frame中执行java代码 弹出Frame 返回执行结果的指针 主要慢在如下方面 需要完全执行ByteCode而缺少JIT等优化 检查参数非常多,这些本来可以在编译器或者加载时完成 4.4. class.newInstance的实现 检测权限、预分配空间大小等参数 创建Object对象,并分配空间 通过Method.invoke调用构造函数(<init>()) 返回Object指针 主要慢在如下方面 参数检查不能优化或者遗漏 <init>()的查表 Method.invoke本身耗时 5. 附录 5.1. JVM与源码阅读工具的选择 初次学习JVM时,不建议去看Android Art、Hotspot等重量级JVM的实现,它内部的防御代码很多,还有android与libcore、bionic库紧密耦合,以及分层、内联甚至能把编译器的语义分析绕进去,因此找一个教学用的、嵌入式小型的JVM有利于节约自己的时间。因为以前折腾过OpenWrt,听过有大神推荐过jamvm,只有不到200个源文件,非常适合学习。 在工具的选择上,个人推荐SourceInsight。对比了好几个工具clion,vscode,sublime,sourceinsight,只有sourceinsight对索引、符号表的解析最准确。 5.2. 关于几个ClassLoader 参考这里 ClassLoader0:native的classloader,在JVM中用C写的,用于加载rt.jar的包,在Java中为空引用。 ExtClassLoader: 用于加载JDK中额外的包,一般不怎么用 AppClassLoader: 加载自己写的或者引用的第三方包,这个最常见 例子如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 //sun.misc.Launcher$AppClassLoader@4b67cf4d //which class you create or jars from thirdParty //第一个非常有歧义,但是它的确是AppClassLoader ClassLoader.getSystemClassLoader(); com.test.App.getClass().getClassLoader(); Class.forName("ccom.test.App").getClassLoader() //sun.misc.Launcher$ExtClassLoader@66d3c617 //Class loaded in ext jar Class.forName("sun.net.spi.nameservice.dns.DNSNameService") //null, class loaded in rt.jar String.class.getClassLoader() Class.forName("java.lang.String").getClassLoader() Class.forName("java.lang.Class").getClassLoader() Class.forName("apple.launcher.JavaAppLauncher").getClassLoader() 最后就是getContextClassLoader(),它在Tomcat中使用,通过设置一个临时变量,可以向子类ClassLoader去加载,而不是委托给ParentClassLoader 1 2 3 4 5 6 7 ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); // call some API that uses reflection without taking ClassLoader param } finally { Thread.currentThread().setContextClassLoader(originalClassLoader); } 最后还有一些自定义的ClassLoader,实现加密、压缩、热部署等功能,这个是大坑,晚点再开。 5.3. 反射是否慢? 在Stackoverflow上认为反射比较慢的程序员主要有如下看法 验证等防御代码过于繁琐,这一步本来在link阶段,现在却在计算时进行验证 产生很多临时对象,造成GC与计算时间消耗 由于缺少上下文,丢失了很多运行时的优化,比如JIT(它可以看作JVM的重要评测标准之一) 当然,现代JVM也不是非常慢了,它能够对反射代码进行缓存以及通过方法计数器同样实现JIT优化,所以反射不一定慢。 更重要的是,很多情况下,你自己的代码才是限制程序的瓶颈。因此,在开发效率远大于运行效率的的基础上,大胆使用反射,放心开发吧。
MySQL凭借着出色的性能、低廉的成本、丰富的资源,已经成为绝大多数互联网公司的首选关系型数据库。虽然性能出色,但所谓“好马配好鞍”,如何能够更好的使用它,已经成为开发工程师的必修课,我们经常会从职位描述上看到诸如“精通MySQL”、“SQL语句优化”、“了解数据库原理”等要求。我们知道一般的应用系统,读写比例在10:1左右,而且插入操作和一般的更新操作很少出现性能问题,遇到最多的,也是最容易出问题的,还是一些复杂的查询操作,所以查询语句的优化显然是重中之重。 本人从13年7月份起,一直在美团核心业务系统部做慢查询的优化工作,共计十余个系统,累计解决和积累了上百个慢查询案例。随着业务的复杂性提升,遇到的问题千奇百怪,五花八门,匪夷所思。本文旨在以开发工程师的角度来解释数据库索引的原理和如何优化慢查询。 一个慢查询引发的思考 select count(*) from task where status=2 and operator_id=20839 and operate_time>1371169729 and operate_time<1371174603 and type=2; 系统使用者反应有一个功能越来越慢,于是工程师找到了上面的SQL。 并且兴致冲冲的找到了我,“这个SQL需要优化,给我把每个字段都加上索引” 我很惊讶,问道“为什么需要每个字段都加上索引?” “把查询的字段都加上索引会更快”工程师信心满满 “这种情况完全可以建一个联合索引,因为是最左前缀匹配,所以operate_time需要放到最后,而且还需要把其他相关的查询都拿来,需要做一个综合评估。” “联合索引?最左前缀匹配?综合评估?”工程师不禁陷入了沉思。 多数情况下,我们知道索引能够提高查询效率,但应该如何建立索引?索引的顺序如何?许多人却只知道大概。其实理解这些概念并不难,而且索引的原理远没有想象的那么复杂。 MySQL索引原理 ##索引目的 索引的目的在于提高查询效率,可以类比字典,如果要查“mysql”这个单词,我们肯定需要定位到m字母,然后从下往下找到y字母,再找到剩下的sql。如果没有索引,那么你可能需要把所有单词看一遍才能找到你想要的,如果我想找到m开头的单词呢?或者ze开头的单词呢?是不是觉得如果没有索引,这个事情根本无法完成? ##索引原理 除了词典,生活中随处可见索引的例子,如火车站的车次表、图书的目录等。它们的原理都是一样的,通过不断的缩小想要获得数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是我们总是通过同一种查找方式来锁定数据。 数据库也是一样,但显然要复杂许多,因为不仅面临着等值查询,还有范围查询(>、<、between、in)、模糊查询(like)、并集查询(or)等等。数据库应该选择怎么样的方式来应对所有的问题呢?我们回想字典的例子,能不能把数据分成段,然后分段查询呢?最简单的如果1000条数据,1到100分成第一段,101到200分成第二段,201到300分成第三段......这样查第250条数据,只要找第三段就可以了,一下子去除了90%的无效数据。但如果是1千万的记录呢,分成几段比较好?稍有算法基础的同学会想到搜索树,其平均复杂度是lgN,具有不错的查询性能。但这里我们忽略了一个关键的问题,复杂度模型是基于每次相同的操作成本来考虑的,数据库实现比较复杂,数据保存在磁盘上,而为了提高性能,每次又可以把部分数据读入内存来计算,因为我们知道访问磁盘的成本大概是访问内存的十万倍左右,所以简单的搜索树难以满足复杂的应用场景。 ###磁盘IO与预读 前面提到了访问磁盘,那么这里先简单介绍一下磁盘IO和预读,磁盘读取数据靠的是机械运动,每次读取数据花费的时间可以分为寻道时间、旋转延迟、传输时间三个部分,寻道时间指的是磁臂移动到指定磁道所需要的时间,主流磁盘一般在5ms以下;旋转延迟就是我们经常听说的磁盘转速,比如一个磁盘7200转,表示每分钟能转7200次,也就是说1秒钟能转120次,旋转延迟就是1/120/2 = 4.17ms;传输时间指的是从磁盘读出或将数据写入磁盘的时间,一般在零点几毫秒,相对于前两个时间可以忽略不计。那么访问一次磁盘的时间,即一次磁盘IO的时间约等于5+4.17 = 9ms左右,听起来还挺不错的,但要知道一台500 -MIPS的机器每秒可以执行5亿条指令,因为指令依靠的是电的性质,换句话说执行一次IO的时间可以执行40万条指令,数据库动辄十万百万乃至千万级数据,每次9毫秒的时间,显然是个灾难。下图是计算机硬件延迟的对比图,供大家参考: 考虑到磁盘IO是非常高昂的操作,计算机操作系统做了一些优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计非常有帮助。 ###索引的数据结构 前面讲了生活中索引的例子,索引的基本原理,数据库的复杂性,又讲了操作系统的相关知识,目的就是让大家了解,任何一种数据结构都不是凭空产生的,一定会有它的背景和使用场景,我们现在总结一下,我们需要这种数据结构能够做些什么,其实很简单,那就是:每次查找数据时把磁盘IO次数控制在一个很小的数量级,最好是常数数量级。那么我们就想到如果一个高度可控的多路搜索树是否能满足需求呢?就这样,b+树应运而生。 ###详解b+树 如上图,是一颗b+树,关于b+树的定义可以参见B+树,这里只说一些重点,浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点只不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。 ###b+树的查找过程 如图所示,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。 ###b+树性质 1.通过上面的分析,我们知道IO次数取决于b+数的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;而m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于1时将会退化成线性表。 2.当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。 慢查询优化 关于MySQL索引原理是比较枯燥的东西,大家只需要有一个感性的认识,并不需要理解得非常透彻和深入。我们回头来看看一开始我们说的慢查询,了解完索引原理之后,大家是不是有什么想法呢?先总结一下索引的几大基本原则 建索引的几大原则 1.最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。 2.=和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式 3.尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录 4.索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’); 5.尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可 回到开始的慢查询 根据最左匹配原则,最开始的sql语句的索引应该是status、operator_id、type、operate_time的联合索引;其中status、operator_id、type的顺序可以颠倒,所以我才会说,把这个表的所有相关查询都找到,会综合分析; 比如还有如下查询 select * from task where status = 0 and type = 12 limit 10; select count(*) from task where status = 0 ; 那么索引建立成(status,type,operator_id,operate_time)就是非常正确的,因为可以覆盖到所有情况。这个就是利用了索引的最左匹配的原则 查询优化神器 - explain命令 关于explain命令相信大家并不陌生,具体用法和字段含义可以参考官网explain-output,这里需要强调rows是核心指标,绝大部分rows小的语句执行一定很快(有例外,下面会讲到)。所以优化语句基本上都是在优化rows。 慢查询优化基本步骤 0.先运行看看是否真的很慢,注意设置SQL_NO_CACHE 1.where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高 2.explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询) 3.order by limit 形式的sql语句让排序的表优先查 4.了解业务方使用场景 5.加索引时参照建索引的几大原则 6.观察结果,不符合预期继续从0分析 几个慢查询案例 下面几个例子详细解释了如何分析和优化慢查询 复杂语句写法 很多情况下,我们写SQL只是为了实现功能,这只是第一步,不同的语句书写方式对于效率往往有本质的差别,这要求我们对mysql的执行计划和索引原则有非常清楚的认识,请看下面的语句 select distinct cert.emp_id from cm_log cl inner join ( select emp.id as emp_id, emp_cert.id as cert_id from employee emp left join emp_certificate emp_cert on emp.id = emp_cert.emp_id where emp.is_deleted=0 ) cert on ( cl.ref_table='Employee' and cl.ref_oid= cert.emp_id ) or ( cl.ref_table='EmpCertificate' and cl.ref_oid= cert.cert_id ) where cl.last_upd_date >='2013-11-07 15:03:00' and cl.last_upd_date<='2013-11-08 16:00:00'; 0.先运行一下,53条记录 1.87秒,又没有用聚合语句,比较慢 53 rows in set (1.87 sec) 1.explain +----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+ | 1 | PRIMARY | cl | range | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date | 8 | NULL | 379 | Using where; Using temporary | | 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 63727 | Using where; Using join buffer | | 2 | DERIVED | emp | ALL | NULL | NULL | NULL | NULL | 13317 | Using where | | 2 | DERIVED | emp_cert | ref | emp_certificate_empid | emp_certificate_empid | 4 | meituanorg.emp.id | 1 | Using index | +----+-------------+------------+-------+---------------------------------+-----------------------+---------+-------------------+-------+--------------------------------+ 简述一下执行计划,首先mysql根据idx_last_upd_date索引扫描cm_log表获得379条记录;然后查表扫描了63727条记录,分为两部分,derived表示构造表,也就是不存在的表,可以简单理解成是一个语句形成的结果集,后面的数字表示语句的ID。derived2表示的是ID = 2的查询构造了虚拟表,并且返回了63727条记录。我们再来看看ID = 2的语句究竟做了写什么返回了这么大量的数据,首先全表扫描employee表13317条记录,然后根据索引emp_certificate_empid关联emp_certificate表,rows = 1表示,每个关联都只锁定了一条记录,效率比较高。获得后,再和cm_log的379条记录根据规则关联。从执行过程上可以看出返回了太多的数据,返回的数据绝大部分cm_log都用不到,因为cm_log只锁定了379条记录。 如何优化呢?可以看到我们在运行完后还是要和cm_log做join,那么我们能不能之前和cm_log做join呢?仔细分析语句不难发现,其基本思想是如果cm_log的ref_table是EmpCertificate就关联emp_certificate表,如果ref_table是Employee就关联employee表,我们完全可以拆成两部分,并用union连接起来,注意这里用union,而不用union all是因为原语句有“distinct”来得到唯一的记录,而union恰好具备了这种功能。如果原语句中没有distinct不需要去重,我们就可以直接使用union all了,因为使用union需要去重的动作,会影响SQL性能。 优化过的语句如下 select emp.id from cm_log cl inner join employee emp on cl.ref_table = 'Employee' and cl.ref_oid = emp.id where cl.last_upd_date >='2013-11-07 15:03:00' and cl.last_upd_date<='2013-11-08 16:00:00' and emp.is_deleted = 0 union select emp.id from cm_log cl inner join emp_certificate ec on cl.ref_table = 'EmpCertificate' and cl.ref_oid = ec.id inner join employee emp on emp.id = ec.emp_id where cl.last_upd_date >='2013-11-07 15:03:00' and cl.last_upd_date<='2013-11-08 16:00:00' and emp.is_deleted = 0 4.不需要了解业务场景,只需要改造的语句和改造之前的语句保持结果一致 5.现有索引可以满足,不需要建索引 6.用改造后的语句实验一下,只需要10ms 降低了近200倍! +----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+ | 1 | PRIMARY | cl | range | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date | 8 | NULL | 379 | Using where | | 1 | PRIMARY | emp | eq_ref | PRIMARY | PRIMARY | 4 | meituanorg.cl.ref_oid | 1 | Using where | | 2 | UNION | cl | range | cm_log_cls_id,idx_last_upd_date | idx_last_upd_date | 8 | NULL | 379 | Using where | | 2 | UNION | ec | eq_ref | PRIMARY,emp_certificate_empid | PRIMARY | 4 | meituanorg.cl.ref_oid | 1 | | | 2 | UNION | emp | eq_ref | PRIMARY | PRIMARY | 4 | meituanorg.ec.emp_id | 1 | Using where | | NULL | UNION RESULT | <union1,2> | ALL | NULL | NULL | NULL | NULL | NULL | | +----+--------------+------------+--------+---------------------------------+-------------------+---------+-----------------------+------+-------------+ 53 rows in set (0.01 sec) 明确应用场景 举这个例子的目的在于颠覆我们对列的区分度的认知,一般上我们认为区分度越高的列,越容易锁定更少的记录,但在一些特殊的情况下,这种理论是有局限性的 select * from stage_poi sp where sp.accurate_result=1 and ( sp.sync_status=0 or sp.sync_status=2 or sp.sync_status=4 ); 0.先看看运行多长时间,951条数据6.22秒,真的很慢 951 rows in set (6.22 sec) 1.先explain,rows达到了361万,type = ALL表明是全表扫描 +----+-------------+-------+------+---------------+------+---------+------+---------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+---------------+------+---------+------+---------+-------------+ | 1 | SIMPLE | sp | ALL | NULL | NULL | NULL | NULL | 3613155 | Using where | +----+-------------+-------+------+---------------+------+---------+------+---------+-------------+ 2.所有字段都应用查询返回记录数,因为是单表查询 0已经做过了951条 3.让explain的rows 尽量逼近951 看一下accurate_result = 1的记录数 select count(*),accurate_result from stage_poi group by accurate_result; +----------+-----------------+ | count(*) | accurate_result | +----------+-----------------+ | 1023 | -1 | | 2114655 | 0 | | 972815 | 1 | +----------+-----------------+ 我们看到accurate_result这个字段的区分度非常低,整个表只有-1,0,1三个值,加上索引也无法锁定特别少量的数据 再看一下sync_status字段的情况 select count(*),sync_status from stage_poi group by sync_status; +----------+-------------+ | count(*) | sync_status | +----------+-------------+ | 3080 | 0 | | 3085413 | 3 | +----------+-------------+ 同样的区分度也很低,根据理论,也不适合建立索引 问题分析到这,好像得出了这个表无法优化的结论,两个列的区分度都很低,即便加上索引也只能适应这种情况,很难做普遍性的优化,比如当sync_status 0、3分布的很平均,那么锁定记录也是百万级别的 4.找业务方去沟通,看看使用场景。业务方是这么来使用这个SQL语句的,每隔五分钟会扫描符合条件的数据,处理完成后把sync_status这个字段变成1,五分钟符合条件的记录数并不会太多,1000个左右。了解了业务方的使用场景后,优化这个SQL就变得简单了,因为业务方保证了数据的不平衡,如果加上索引可以过滤掉绝大部分不需要的数据 5.根据建立索引规则,使用如下语句建立索引 alter table stage_poi add index idx_acc_status(accurate_result,sync_status); 6.观察预期结果,发现只需要200ms,快了30多倍。 952 rows in set (0.20 sec) 我们再来回顾一下分析问题的过程,单表查询相对来说比较好优化,大部分时候只需要把where条件里面的字段依照规则加上索引就好,如果只是这种“无脑”优化的话,显然一些区分度非常低的列,不应该加索引的列也会被加上索引,这样会对插入、更新性能造成严重的影响,同时也有可能影响其它的查询语句。所以我们第4步调差SQL的使用场景非常关键,我们只有知道这个业务场景,才能更好地辅助我们更好的分析和优化查询语句。 无法优化的语句 select c.id, c.name, c.position, c.sex, c.phone, c.office_phone, c.feature_info, c.birthday, c.creator_id, c.is_keyperson, c.giveup_reason, c.status, c.data_source, from_unixtime(c.created_time) as created_time, from_unixtime(c.last_modified) as last_modified, c.last_modified_user_id from contact c inner join contact_branch cb on c.id = cb.contact_id inner join branch_user bu on cb.branch_id = bu.branch_id and bu.status in ( 1, 2) inner join org_emp_info oei on oei.data_id = bu.user_id and oei.node_left >= 2875 and oei.node_right <= 10802 and oei.org_category = - 1 order by c.created_time desc limit 0 , 10; 还是几个步骤 0.先看语句运行多长时间,10条记录用了13秒,已经不可忍受 10 rows in set (13.06 sec) 1.explain +----+-------------+-------+--------+-------------------------------------+-------------------------+---------+--------------------------+------+----------------------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+--------+-------------------------------------+-------------------------+---------+--------------------------+------+----------------------------------------------+ | 1 | SIMPLE | oei | ref | idx_category_left_right,idx_data_id | idx_category_left_right | 5 | const | 8849 | Using where; Using temporary; Using filesort | | 1 | SIMPLE | bu | ref | PRIMARY,idx_userid_status | idx_userid_status | 4 | meituancrm.oei.data_id | 76 | Using where; Using index | | 1 | SIMPLE | cb | ref | idx_branch_id,idx_contact_branch_id | idx_branch_id | 4 | meituancrm.bu.branch_id | 1 | | | 1 | SIMPLE | c | eq_ref | PRIMARY | PRIMARY | 108 | meituancrm.cb.contact_id | 1 | | +----+-------------+-------+--------+-------------------------------------+-------------------------+---------+--------------------------+------+----------------------------------------------+ 从执行计划上看,mysql先查org_emp_info表扫描8849记录,再用索引idx_userid_status关联branch_user表,再用索引idx_branch_id关联contact_branch表,最后主键关联contact表。 rows返回的都非常少,看不到有什么异常情况。我们在看一下语句,发现后面有order by + limit组合,会不会是排序量太大搞的?于是我们简化SQL,去掉后面的order by 和 limit,看看到底用了多少记录来排序 select count(*) from contact c inner join contact_branch cb on c.id = cb.contact_id inner join branch_user bu on cb.branch_id = bu.branch_id and bu.status in ( 1, 2) inner join org_emp_info oei on oei.data_id = bu.user_id and oei.node_left >= 2875 and oei.node_right <= 10802 and oei.org_category = - 1 +----------+ | count(*) | +----------+ | 778878 | +----------+ 1 row in set (5.19 sec) 发现排序之前居然锁定了778878条记录,如果针对70万的结果集排序,将是灾难性的,怪不得这么慢,那我们能不能换个思路,先根据contact的created_time排序,再来join会不会比较快呢? 于是改造成下面的语句,也可以用straight_join来优化 select c.id, c.name, c.position, c.sex, c.phone, c.office_phone, c.feature_info, c.birthday, c.creator_id, c.is_keyperson, c.giveup_reason, c.status, c.data_source, from_unixtime(c.created_time) as created_time, from_unixtime(c.last_modified) as last_modified, c.last_modified_user_id from contact c where exists ( select 1 from contact_branch cb inner join branch_user bu on cb.branch_id = bu.branch_id and bu.status in ( 1, 2) inner join org_emp_info oei on oei.data_id = bu.user_id and oei.node_left >= 2875 and oei.node_right <= 10802 and oei.org_category = - 1 where c.id = cb.contact_id ) order by c.created_time desc limit 0 , 10; 验证一下效果 预计在1ms内,提升了13000多倍! ```sql 10 rows in set (0.00 sec) 本以为至此大工告成,但我们在前面的分析中漏了一个细节,先排序再join和先join再排序理论上开销是一样的,为何提升这么多是因为有一个limit!大致执行过程是:mysql先按索引排序得到前10条记录,然后再去join过滤,当发现不够10条的时候,再次去10条,再次join,这显然在内层join过滤的数据非常多的时候,将是灾难的,极端情况,内层一条数据都找不到,mysql还傻乎乎的每次取10条,几乎遍历了这个数据表! 用不同参数的SQL试验下 select sql_no_cache c.id, c.name, c.position, c.sex, c.phone, c.office_phone, c.feature_info, c.birthday, c.creator_id, c.is_keyperson, c.giveup_reason, c.status, c.data_source, from_unixtime(c.created_time) as created_time, from_unixtime(c.last_modified) as last_modified, c.last_modified_user_id from contact c where exists ( select 1 from contact_branch cb inner join branch_user bu on cb.branch_id = bu.branch_id and bu.status in ( 1, 2) inner join org_emp_info oei on oei.data_id = bu.user_id and oei.node_left >= 2875 and oei.node_right <= 2875 and oei.org_category = - 1 where c.id = cb.contact_id ) order by c.created_time desc limit 0 , 10; Empty set (2 min 18.99 sec) 2 min 18.99 sec!比之前的情况还糟糕很多。由于mysql的nested loop机制,遇到这种情况,基本是无法优化的。这条语句最终也只能交给应用系统去优化自己的逻辑了。 通过这个例子我们可以看到,并不是所有语句都能优化,而往往我们优化时,由于SQL用例回归时落掉一些极端情况,会造成比原来还严重的后果。所以,第一:不要指望所有语句都能通过SQL优化,第二:不要过于自信,只针对具体case来优化,而忽略了更复杂的情况。 慢查询的案例就分析到这儿,以上只是一些比较典型的案例。我们在优化过程中遇到过超过1000行,涉及到16个表join的“垃圾SQL”,也遇到过线上线下数据库差异导致应用直接被慢查询拖死,也遇到过varchar等值比较没有写单引号,还遇到过笛卡尔积查询直接把从库搞死。再多的案例其实也只是一些经验的积累,如果我们熟悉查询优化器、索引的内部原理,那么分析这些案例就变得特别简单了。 写在后面的话 本文以一个慢查询案例引入了MySQL索引原理、优化慢查询的一些方法论;并针对遇到的典型案例做了详细的分析。其实做了这么长时间的语句优化后才发现,任何数据库层面的优化都抵不上应用系统的优化,同样是MySQL,可以用来支撑Google/FaceBook/Taobao应用,但可能连你的个人网站都撑不住。套用最近比较流行的话:“查询容易,优化不易,且写且珍惜!”
HBase 简介 众所周知,在 SQL 方面处于顶级的有两个公司,一个是 Oracle,他们已经积累了大量的经验,另一个是谷歌,谷歌 F1 在2012年发布了一篇论文,个人认为它是全球最优秀的 SQL OLTP 数据库。 1978年左右,数据库刚刚发展时出现了SQL RDBMS。2000年左右,国内开始流行互联网,互联网对 Oracle 数据库也产生较大的冲击。现在,传统的数据库大部分是集中在传统领域,互联网方面用得比较多的是 MySQL ,其次 HBase 等 NoSQL 也吸引了大量的用户。 为什么会出现 NoSQL?最开始所有人都用 SQL Database,那时比较高端有 Oracle,开源的还有 MySQL、PostgreSQL。可是随着业务的迅速发展,数据库成为了瓶颈,于是促使了 NoSQL 的诞生,NoSQL 将 Scale 放在第一位。如果业务快速发展,扩容会成为亟待解决的首要问题。这时,大多数人会选择放弃事务一致性。什么是一致性?比如使用微信时,如果我加你为好友,这是一个双向关系,对应到数据库中至少是两个操作,第一是在好友列表里把你加进来,第二个是你的好友列表里把我加进去。如果这两个列表的数据库放在不同的机器上,就需要保证一致性。否则可能会出现我是你的好友,但你的好友中却找不到我的这种情况。但这中间可能会出现多种情况,比如我把你加为好友,然后修改数据的时候 Crush 掉了,这个时候传统方案是会引入一个消息队列,有的还需要做一些补偿,这些问题在 NoSQL 里处理起来相对麻烦。 国内最大的 HBase 使用者是小米公司,有几个 HBase 的 Committer ,所以经过一些修改后可以支持分布式事务,于是能够解决之前的问题。为什么在面临诸多选择时,小米会选择 HBase 呢?就目前情况来说,主要还是技术选型和人才储备上的考虑。 MongoDB 大家应该不陌生,但用到一定程度后,总会出现各种问题,甚至有文章呼吁大家放弃 MongoDB 。但所有数据库都不是“十全十美”的,没有最好,选择最适合的尤为重要。 很多时候产品都有其特性,在满足其特性或者规格的情况下,使用起来可能非常顺手,否则十之八九都遇到各种麻烦。比如小米使用 HBase 就非常顺手,但其他的公司则不一定。道理很简单,如果不熟悉其使用场景,也不知道在相应场景下配什么参数,所以会出现各种各样的问题。 事实上,HBase 有非常好的特性,目前在小米公司可以每秒跑一百万 OPS ,最近 Pinterest 公布他们的 HBase 每秒可以跑三百万个 OPS ,这个数量级可以远超很多互联网公司。 HBase 在读写一致性方面非常出色,有很好的自动 Scale 的能力,通过Block Cache 和 Bloom Filters可以很好的解决查询问题,是否在磁盘上也可以通过Bloom Filters来判定。 另一方面,Oracle 把一部分逻辑会放在 CPU/硬件里,对应的 HBase 也会把一部分逻辑下推到对应的 RegionServer 上。对于一个分布系统来说,如果需要查询一个条件,可以直接把这个简单调节推到对应的 RegionServer 上执行。再比如求和运算,现在有一百亿数据,甚至一千亿条数据,分布在10个节点上,最快的求和方法是让所有节点同时运算,将这个条件下推得到所有对应数据的和,最后收集到10个数据的和即可。其实还可以继续往下推,这是比较复杂的数据库优化技术,实际情况还会更复杂。这在 HBase 里面依赖 Coprocessor 来实现。 大家应该对 MVCC 比较熟悉,也就是多版本,它的优点在于可以多次读取而不会 block。然后还有一个很好的特性,假设你用的 Database ,MVCC 在你没有做 compaction 之前可以回到任何时间的数据。现在云服务上也可以每隔半小时做一次快照,实际上如果使用 MVCC 回到任意一秒的话,可以完全不需要快照。 TiDB的优势 下面再介绍一下我们的产品 TiDB,Ti 是元素周期表里的元素。大家如果了解我们团队的程序员,就知道他们都比较 Geek,取名字要么在希腊神话里选一个神的名字,或者在数学里找一个希腊字母, 但是看了一圈,好坑都已经被占上了。于是,我们在化学元素周期表里找了一个金属作为项目名称,对于 Database 而言,它必须是高速稳定的,刚好钛金属有很强的防腐蚀性,所以选择了钛(Ti)。 因为 TiDB 的目标是谷歌 F1,所以自然会满足以上特性。首先是可以满足分布式一致,也就是说对于应用来说,不用关心后面分成多少个机器,事务的一致性是必须保证的,比如我们之前提到的 A 关注 B,两个互相加好友或者转帐,可以直接利用一条 SQL 搞定,而无需担心中间过程。另外一个特性是兼容 MySQL 协议,国内大概有70% 的互联网公司都在使用 MySQL,为了考虑大家的迁移成本,我们会兼容 MySQL 协议。同时,由于已经很多 APP 在 MySQL 上运行,为我们提供了充足的测试样本。 TiDB 的测试有五百多万个,每次提交一行代码时,后面大概有6个机器并行地跑 Test ,五百多万 Test 所需时间大约是十分钟。为了照顾各种引擎爱好者,我们还支持了 LevelDB 、RocksDB、LMDB、BoltDB 等。TiDB 主要是采用 Go 语言开发的,其代码简单、易于理解,而且性能非常高。 系统架构 任何用 MySQL 协议写的程序都可以直接使用 TiDB ,其中间是 MySQL 协议相关的内容,再往下是 SQL Layer。其次是事务 KV 层,这正是 F1 和 Spanner 构造得最为精密的地方。最底层的构造是从 KV 开始,在 KV 基础上架一个分布式的 KV 层用于支持事务,然后再让 SQL 语句直接映射到 KV 层上。 接下来,向大家介绍 现阶段 TiDB 使用的分布式事务是如何在 HBase 上实现的,早期版本中,我们参考的是 Google 的 Percolator 的模型。首先假设有一个 Client,先为其分配一个 Timestamp,在 Google 论文中叫做Time Oracle,用来分配时间戳。分配之后可以做读写操作,根据时间戳进行快照读。最后提交之前要先 Prepare ,Prepare的时候会检测是否冲突,最后提交时会得到 Commit ,如果整个过程没有任何冲突就可以提交。 上图代表了一个实例,最初帐户情况是 Bob 有10美金,而 Joe 有5美金。前面的数字代表其版本,当前是第6个版本,指向的是第5个版本,为10美金,Joe 是2美金。 假设Bob要转4美金给 Joe。第一步,要先转出去4美金,10美金变成6美金,由于被扣掉4美金,然后会标注一下自己是主锁。 Joe当前是第7个版本,因为他得到了4美金,所以余额变成了6美金,同时标记自己指向另外一个主锁 Bob。 到第八个版本时,主锁会指向现在的7,这时可以把主锁删掉。如果访问的时候发现主锁被删除,那么主锁冲突已不存在,可以进行提交。同时,它会把自己的锁删掉,中间还有一些其它的清理过程。 整个事务模型中会有单点,从 Time Oracle 分配一个时间戳,单点决定了整个系统的性能。Google 论文里有一个对应描述,可以跑到两百万每秒。因为事务开始和结束的时候都需要取一个 Timestamp ,所以他们最快读写事务的速度是一百万每秒,他们已经在论文中实现。实际上,现在有更好的方式可以提高速度,如 HLC 和一些 Time Oracle的改进算法。 关于 Spanner ,我们重点参考对象是谷歌 Spanner 和 F1 。由于 Spanner 高度依赖于时钟,所以谷歌有一套原子钟和 GPS 时钟,GPS 信号可以给出地理位置和时间。为什么需要原子钟呢?由于 GPS 时钟特别容易受到干扰,比如天气恶劣时 GPS 时钟就不能运行,而原子钟仍然适用。 上图是谷歌 F1 的一些信息,其中单独标记了谷歌 F1 的这篇论文,大家有兴趣的话不妨细读一番,目前整个 TiDB 所做的都是在实现这篇论文。假设有一千亿数据,你现在要给某一列加索引时,在传统数据库上应该如何操作?比如说在分布式环境下,你用MySQL 给一列添加一个索引,这几乎很难实现,而且还必须保证 index 的一致性。更多细节请参考论文。 TiDB 是如何从 SQL 迁移到 KV 上的呢?由基础知识可知,传统的 RDBMS 数据库底下一般是一个 B-Tree。对于分布式关系型数据库,站在更上层一点看,比如谷歌的F1,数据库底层都是 KV 层,都在 KV 层逻辑下操作。如果有一个 User Table,在 TiDB 里假设你的Table的结构是由 uid、name和 email 构成。在 TiDB 里有一个隐藏列叫做 RowID ,所有的操作包括行锁都是锁的 RowID 。假设 RowID 是1, uid 是XX,Name 是 Bob,Email 是 bob@Email.com,这都属于元信息。即便你的 Column name 很长,但最后在数据库里存储的是原信息。在 TiDB 中, 每一列都有唯一的UID。 假设 Table 的 ID 是1,uid 的 ID 是2,name 的ID是3,email 的 ID 是4。在数据库中存储为一个 KV 结构,然后对 TableID、RowID 、ColumnID 进行重新编码,直接将这个表的一行切成4个 KV 。这时候如果进行 select , Email 等于某一个值的话,于是可以直接取出来相应的值,速度非常快。 兼容 MySQL TiDB 对 MySQL 协议有很好的兼容性。有一些比较知名的 MySQL 应用和管理工具,比如WordPress、PhpMyAdmin, MySQL Workbench,都可以直接基于 TiDB 运行。而且数据可以无限扩展,不再是单机数据库。其次,TiDB 还兼容各种 ORM ,比如 XORM 、Beego ORM 等,能够支持很多 MySQL 的应用。每一次代码更新,这些 ORM Test 会自动运行一次,从而保证与 MySQL 的兼容性,虽然还有一些比较细微的特性暂时没有支持。现在已经支持异步的 Schema 变更,对于 DDL 操作,不会阻塞线上的业务。 关于社区 目前 TiDB 完全开源在 Github 上面。开源和开放的概念是两回事,很多大公司,所谓的开源只是把代码上传一下,国内比较知名的案例也挺多的,大家知道很多项目都已经放弃了维护。但是我们是打算完全以一个开放的心态来做整个事情,全部的代码,全部的讨论, Code Review,Bug Tracking,Roadmap 都是开源的,毕竟通用的分布式 OLTP 关系型数据库是一个非常前沿而且极端重要的领域,未来是云上的 DBaaS 的重要组成部分,但是在这块目前整个技术社区,即使全球来看都没有一个太成熟开源解决方案,TiDB也目前也处于早期,从架构上来看,我们将 SQL 层和 KV 层做了很彻底的分离,这也是我们希望更多开发者能根据自己的需要更方便的进行定制,我们也想得很清楚,依靠某一家公司,或者某几个人的力量是不够的,我们 PingCAP 只是将这一把火点起来,将框架搭好,制定好透明和公平的规则,吸引更多的合作公司和独立开发者,一起将 TiDB 做成中国第一个世界顶级的开源项目,实现共赢。 好的项目可以由社区进行推动,就比如 HBase,HBase 不属于任何一个公司,但是社区一直推动它进步。目前我们在 GitHub 状态是有 3200+的 Star,有 32个 Contributors,算是开了一个好头,非常感谢大家,希望大家都能参与进来。
淘宝根据自身业务需求研发了TDDL(Taobao Distributed Data Layer)框架,主要用于解决分库分表场景下的访问路由(持久层与数据访问层的配合)以及异构数据库之间的数据同步,它是一个基于集中式配置的JDBC DataSource实现,具有分库分表、Master/Salve、动态数据源配置等功能。就目前而言,许多大厂也在出一些更加优秀和社区支持更广泛的DAL层产品,比如Hibernate Shards、Ibatis-Sharding等。TDDL位于数据库和持久层之间,它直接与数据库建立交道,如图所示: 淘宝很早就对数据进行过分库的处理,上层系统连接多个数据库,中间有一个叫做DBRoute的路由来对数据进行统一访问。DBRoute对数据进行多库的操作、数据的整合,让上层系统像操作一个数据库一样操作多个库。但是随着数据量的增长,对于库表的分法有了更高的要求,例如,你的商品数据到了百亿级别的时候,任何一个库都无法存放了,于是分成2个、4个、8个、16个、32个……直到1024个、2048个。好,分成这么多,数据能够存放了,那怎么查询它?这时候,数据查询的中间件就要能够承担这个重任了,它对上层来说,必须像查询一个数据库一样来查询数据,还要像查询一个数据库一样快(每条查询在几毫秒内完成),TDDL就承担了这样一个工作。在外面有些系统也用DAL(数据访问层) 这个概念来命名这个中间件。下图展示了一个简单的分库分表数据查询策略: TDDL的主要优点: 数据库主备和动态切换 带权重的读写分离 单线程读重试 集中式数据源信息管理和动态变更 剥离的稳定jboss数据源 支持mysql和oracle数据库 基于jdbc规范,很容易扩展支持实现jdbc规范的数据源 无server,client-jar形式存在,应用直连数据库 读写次数,并发度流程控制,动态变更 可分析的日志打印,日志流控,动态变更 TDDL的体系架构 TDDL其实主要可以划分为3层架构,分别是Matrix层、Group层和Atom层。Matrix层用于实现分库分表逻辑,底层持有多个Group实例。而Group层和Atom共同组成了动态数据源, Group层实现了数据库的Master/Salve模式的写分离逻辑,底层持有多个Atom实例。最后Atom层 (TAtomDataSource)实现数据库ip,port,password,connectionProperties等信息的动态推送,以及持有原子的数据源分离的JBOSS数据源)。 持久层只关心对数据源的CRUD操作,而多数据源的访问并不应该由它来关心。也就是说TDDL透明给持久层的数据源接口应该是统一且“单一”的,至于数据库到底如何分库分表持久层无需知道也无需编写对应的SQL去实行应对策略。这个时候对TDDL一些疑问就出现了,TDDL需要对SQL进行二次解析和拼装吗?答案是不解析仅拼装。TDDL只需要从持久层拿到发出的SQL再按照一些分库分表条件,进行特定的SQL扩充以此满足访问路路由操作。 TDDL除了拿到分库分表条件外,还需要拿到order by、group by、limit、join等信息,SUM、MAX、MIN等聚合函数信息,DISTINCT信息。具有这些关键字的SQL将会在单库和多库情况下进行,语义是不同的。TDDL必须对使用这些关键字的SQL返回的结果做出合适的处理; TDDL行复制需要重新拼写SQL,带上sync_version字段; 不通过sql解析,因为TDDL遵守JDBC规范,它不可能去扩充JDBC规范里面的接口,所以只能通过SQL中加额外的字符条件(也就是HINT方式)或者ThreadLocal方式进行传递,前者使SQL过长,后者难以维护,开发debug时不容易跟踪,而且需要判定是在一条SQL执行后失效还是1个连接关闭后才失效; TDDL现在也同时支持Hint方式和ThreadLocal方式传递这些信息; 前言 在开始讲解淘宝的 TDDL(Taobao Distribute Data Layer) 技术之前,请允许笔者先吐槽一番。首先要开喷的是淘宝的社区支持做的无比的烂, TaoCode 开源社区上面,几乎从来都是有人提问,无人响应。再者版本迭代速度也同样差强人意 , 就目前而言 TDDL 的版本已经全线开源(Group、Atom、Matrix)大家可以在Github上下载源码 。 目录 一、互联网当下的数据库拆分过程 二、 TDDL 的架构原型 三、下载 TDDL 的 Atom 层和 Group 层源代码 四、 Diamond 简介 五、 Diamond 的安装和使用 六、动态数据源层的 Master/Salve 读写分离 配置与实现 七、 Matrix 层的分库分表配置与实现 一、互联网当下的数据库拆分过程 对于一个刚上线的互联网项目来说,由于前期活跃用户数量并不多,并发量也相对较小,所以此时企业一般都会选择将所有数据存放在 一个数据库 中进行访问操作。但随着后续的市场推广力度不断加强,用户数量和并发量不断上升,这时如果仅靠一个数据库来支撑所有访问压力,几乎是在 自寻死路 。所以一旦到了这个阶段,大部分 Mysql DBA 就会将数据库设置成 读写分离状态 ,也就是一个 Master节点对应多个 Salve 节点。经过 Master/Salve 模式的设计后,完全可以应付单一数据库无法承受的负载压力,并将访问操作分摊至多个 Salve 节点上,实现真正意义上的读写分离。但大家有没有想过,单一的 Master/Salve 模式又能抗得了多久呢?如果用户数量和并发量出现 量级 上升,单一的 Master/Salve 模式照样抗不了多久,毕竟一个 Master 节点的负载还是相对比较高的。为了解决这个难题,Mysql DBA 会在单一的 Master/Salve 模式的基础之上进行数据库的 垂直分区 (分库)。所谓垂直分区指的是可以根据业务自身的不同,将原本冗余在一个数据库内的业务表拆散,将数据分别存储在不同的数据库中,同时仍然保持 Master/Salve模式。经过垂直分区后的 Master/Salve 模式完全可以承受住难以想象的高并发访问操作,但是否可以永远 高枕无忧 了?答案是否定的,一旦业务表中的数据量大了,从维护和性能角度来看,无论是任何的 CRUD 操作,对于数据库而言都是一件极其耗费资源的事情。即便设置了索引, 仍然无法掩盖因为数据量过大从而导致的数据库性能下降的事实 ,因此这个时候 Mysql DBA 或许就该对数据库进行 水平分区 (分表, sharding ),所谓水平分区指的是将一个业务表拆分成多个子表,比如 user_table0 、 user_table1 、 user_table2 。子表之间通过某种契约关联在一起,每一张子表均按段位进行数据存储,比如 user_table0 存储 1-10000 的数据,而 user_table1 存储 10001-20000 的数据,最后 user_table3 存储 20001-30000 的数据。经过水平分区设置后的业务表,必然能够将原本一张表维护的海量数据分配给 N 个子表进行存储和维护,这样的设计在国内一流的互联网企业比较常见,如图 1-1 所示: 图 1-1 水平分区 上述笔者简单的讲解了数据库的分库分表原理。接下来请大家认真思考下。原本一个数据库能够完成的访问操作,现在如果按照分库分表模式设计后,将会显得非常麻烦,这种麻烦尤其体现在 访问操作 上。因为持久层需要判断出对应的数据源,以及数据源上的水平分区,这种访问方式我们称之为访问 “ 路由 ” 。按照常理来说,持久层不应该负责数据访问层 (DAL) 的工作,它应该只关心 one to one 的操作形式,所以淘宝的 TDDL 框架诞生也就顺其自然了。 二、 TDDL 的架构原型 淘宝根据自身业务需求研发了 TDDL ( Taobao Distributed Data Layer )框架,主要用于解决 分库分表场景下的访问路由(持久层与数据访问层的配合)以及异构数据库之间的数据同步 ,它是一个基于集中式配置的 JDBC DataSource 实现,具有分库分表、 Master/Salve 、动态数据源配置等功能。 就目前而言,许多大厂也在出一些更加优秀和社区支持更广泛的 DAL 层产品,比如 Hibernate Shards 、 Ibatis-Sharding 等。如果你要问笔者还为什么还要对 TDDL进行讲解,那么笔者只能很 无奈 的表示公司要这么干,因为很多时候技术选型并不是笔者说了算,而是客户说了算。当笔者费劲所有努力在 google 上寻找 TDDL的相关使用说明和介绍时,心里一股莫名的火已经开始在蔓延,对于更新缓慢(差不多一年没更新过 SVN ),几乎没社区支持(提问从不响应)的产品来说,除了蜗居在企业内部,必定走不了多远,最后的结局注定是 悲哀 的。好了,既然抱怨了一番,无论如何还是要坚持讲解完。 TDDL 位于数据库和持久层之间,它直接与数据库建立交道,如图 1-2 所示: 图 1-2 TDDL 所处领域模型定位 传说淘宝很早以前就已经对数据进行过分库分表处理,应用层连接多个数据源,中间有一个叫做 DBRoute 的技术对数据库进行 统一 的路由访问。 DBRoute 对数据进行多库的操作、数据的整合,让应用层像操作一个数据源一样操作多个数据库。但是随着数据量的增长,对于库表的分法有了更高的要求,例如,你的商品数据到了百亿级别的时候,任何一个库都无法存放了,于是分成 2 个、 4 个、 8 个、 16个、 32 个 …… 直到 1024 个、 2048 个。好,分成这么多,数据能够存放了,那怎么查询它?这时候,数据查询的中间件就要能够承担这个重任了,它对上层来说,必须像查询一个数据库一样来查询数据,还要像查询一个数据库一样快( 每条查询要求在几毫秒内完成 ), TDDL 就承担了这样一个工作( 其他 DAL 产品做得更好 ),如图 1-3 所示: 图 1-3 TDDL 分库分表查询策略 上述笔者描述了 TDDL 在分库分表环境下的查询策略,那么接下来笔者有必要从淘宝官方 copy 它们自己对 TDDL 优点的一些描述,真实性不敢保证,毕竟没完全开源,和社区零支持,大家看一看就算了,别认真。 淘宝人自定的 TDDL 优点: 1 、数据库主备和动态切换; 2 、带权重的读写分离; 3 、单线程读重试; 4 、集中式数据源信息管理和动态变更; 5 、剥离的稳定 jboss 数据源; 6 、支持 mysql 和 oracle 数据库; 7 、基于 jdbc 规范,很容易扩展支持实现 jdbc 规范的数据源; 8 、无 server,client-jar 形式存在,应用直连数据库; 9 、读写次数 , 并发度流程控制,动态变更; 10 、可分析的日志打印 , 日志流控,动态变更; 注意 : TDDL 必须要依赖 diamond 配置中心( diamond 是淘宝内部使用的一个管理持久配置的系统,目前淘宝内部绝大多数系统的配置)。 接下来,笔者将会带领各位一起分析 TDDL 的体系架构。 TDDL 其实主要可以划分为 3 层架构,分别是 Matrix 层、 Group 层和 Atom 层。 Matrix 层用于实现分库分表逻辑,底层持有多个 Group 实例。而 Group 层和 Atom 共同组成了 动态数据源 , Group 层实现了数据库的 Master/Salve 模式的写分离逻辑,底层持有多个Atom 实例。最后 Atom 层 (TAtomDataSource) 实现数据库ip,port,password,connectionProperties 等信息的动态推送 , 以及持有原子的数据源分离的 JBOSS 数据源)。 图 1-4 TDDL 体系结构 章节的最后,我们还需要对 TDDL 的原理进行一次剖析。因为我们知道持久层只关心对数据源的 CRUD 操作,而多数据源的访问,并不应该由它来关心。也就是说 TDDL 透明给持久层的数据源接口应该是统一且 “ 单一 ” 的,至于数据库 到底如何分库分表,持久层无需知道,也无需 编写对应的 SQL 去实行 应对策略 。这个时候对 TDDL 一些疑问就出现了, TDDL 需要对 SQL 进行二次解析和拼装吗?答案是 不解析仅拼装 。说白了 TDDL 只需要从持久层拿到发出的 SQL 再按照一些分库分表条件,进行特定的 SQL 扩充以此满足访问路路由操作。 以下是淘宝团队对 TDDL 的官方原理解释: 1 、 TDDL 除了拿到分库分表条件外,还需要拿到 order by 、 group by 、 limit 、join 等信息, SUM 、 MAX 、 MIN 等聚合函数信息, DISTINCT 信息。具有这些关键字的 SQL 将会在单库和多库情况下进行 , 语义是不同的。 TDDL 必须对使用这些关键字的 SQL 返回的结果做出合适的处理; 2 、 TDDL 行复制需要重新拼写 SQL, 带上 sync_version 字段; 3 、不通过 sql 解析 , 因为 TDDL 遵守 JDBC 规范 , 它不可能去扩充 JDBC 规范里面的接口 , 所以只能通过 SQL 中加额外的字符条件 ( 也就是 HINT 方式 ) 或者ThreadLocal 方式进行传递 , 前者使 SQL 过长 , 后者难以维护 , 开发 debug 时不容易跟踪 , 而且需要判定是在一条 SQL 执行后失效还是 1 个连接关闭后才失效; 4 、 TDDL 现在也同时支持 Hint 方式和 ThreadLocal 方式传递这些信息; 三、下载 TDDL 的 Atom 层和 Group 层源代码 前面我们谈及了 TDDL 的动态数据源主要由 2 部分构成,分别是 Atom 和Group 。 Group 用于实现数据库的 Master/Salve 模式的写分离逻辑,而 Atom 层则是持有数据源。非常遗憾的 TDDL 中还有一层叫做 Matrix ,该层是整个 TDDL 最为核心的地方,淘宝也并没有对这一层实现开源,而 Matrix 层主要是建立在动态数据源之上的分库分表实现。换句话说, TDDL 是基于模块化结构的,开发人员可以选用 TDDL 中的部分子集。 大家可以从淘宝的 TaoCode 上下载 TDDL 的源码带,然后进行构件的打包。TDDL 的项目主要是基于 Maven 进行管理的,所以建议大家如果不了解 Maven 的使用,还是参考下笔者的博文《 Use Maven3.x 》。 大家下载好 TDDL 的源代码后,通过 IDE 工具导入进来后可以发现,开源的TDDL 的工程结构有如下几部份组成: tddl-all – — tbdatasource — tddl-atom-datasource — tddl-common — tddl-group-datasource — tddl-interact — tddl-sample 大家可以使用 Maven 的命令“ mvn package “将 TDDL 的源代码打包成构件。如果你的电脑上并没有安装 Maven 的插件到不是没有办法实现构件打包,你可以使用eclipse 的导出命令,将源代码导出成构件形式也可以。 四、 Diamond 简介 使用任何一种框架都需要配置一些配置源信息,毕竟每一种框架都有自己的规范,使用者务必遵守这些规范来实现自己的业务与基础框架的整合。自然 TDDL 也不例外,也是有配置信息需要显式的进行配置,在 TDDL 中,配置可以基于 2 种方式,一种是基于本地配置文件的形式,另外一种则是基于 Diamond 的形式进行配置,在实际开发过程中,由于考虑到配置信息的集中管理所带来的好处,大部分开发人员愿意选择将 TDDL 的配置信息托管给 Diamond ,所以本文还是以Diamond 作为 TDDL 的配置源。 diamond 是淘宝内部使用的一个管理持久配置的系统,它的特点是简单、可靠、易用,目前淘宝内部绝大多数系统的配置,由 diamond 来进行统一管理。diamond 为应用系统提供了获取配置的服务,应用不仅可以在启动时从 diamond获取相关的配置,而且可以在运行中对配置数据的变化进行感知并获取变化后的配置数据。 五、 Diamond 的安装和使用 Diamond 和 TDDL 不同,它已经实现了完全意义上的开源。大家可以从淘宝的TaoCode 上下载 Diamond 的源代码, SVN 下载地址为http://code.taobao.org/svn/diamond/trunk 。当大家成功下载好 Diamond 的源代码后,我们接下来就需要开始 Diamond 的环境搭建工作。 首先我们需要安装好 Mysql 数据库,以 root 用户登录,建立用户并赋予权限,建立数据库,然后建表,语句分别如下: create database diamond; grant all on diamond.* to zh@’%’ identified by ‘abc’; use diamond create table config_info ( ‘ id’ bigint(64) unsigned NOT NULL auto_increment, ‘ data_id’ varchar(255) NOT NULL default ’ ’, ‘ group_id’ varchar(128) NOT NULL default ’ ’, ‘ content’ longtext NOT NULL, ‘ md5 ′ varchar(32) NOT NULL default ’ ’ , ‘ gmt_create ’ datetime NOT NULL default ’ 2010-05-05 00:00:00 ′ , ‘ gmt_modified ’ datetime NOT NULL default ’ 2010-05-05 00:00:00 ′ , PRIMARY KEY (‘id’), UNIQUE KEY ‘uk_config_datagroup’ (‘data_id’,'group_id’)); 完成后,请将数据库的配置信息( IP ,用户名,密码)添加到 diamond-server 工程的 src/resources/jdbc.properties 文件中的 db.url , db.user , db.password 属性上面,这里建立的库名,用户名和密码,必须和 jdbc.properties 中对应的属性相同。 tomcat 是 Damond 的运行容器,在 diamond-server 源代码根目录下,执行 mvn clean package -Dmaven.test.skip ,成功后会在 diamond-server/target 目录下生成diamond-server.war 。打包完成后,将 diamond-server.war 放在 tomcat 的webapps 目录下。最后启动 tomcat ,即启动了 Diamond 。 http server 用来存放 diamond server 等地址列表,可以选用任何 http server ,这里以 tomcat 为例。一般来讲, http server 和 diamond server 是部署在不同机器上的,这里简单起见,将二者部署在同一个机器下的同一个 tomcat 的同一个应用中,注意,如果部署在不同的 tomcat 中,端口号一定是 8080 ,不能修改(所以必须部署在不同的机器上)。 在 tomcat 的 webapps 中的 diamond-server 中建立文件 diamond ,文件内容是diamond-server 的地址列表,一行一个地址,地址为 IP ,例如 127.0.0.1 ,完成这些步骤后,就等于已经完成 Diamond 的安装。 六、动态数据源层的 Master/Salve 读写分离 配置与实现 其实使用 TDDL 并不复杂,只要你会使用 JDBC ,那么 TDDL 对于你来说无非就只需要将 JDBC 的操作连接替换为 TDDL 的操作连接,剩余操作一模一样。并且由于 TDDL 遵循了 JDBC 规范,所以你完全还可以使用 Spring JDBC 、 Hibernate等第三方持久层框架进行 ORM 操作。 我们来看看如何 TDDL 中配置 TDDL 的读写分离, Atom+Group 组成了 TDDL 的动态数据源,这 2 层主要负责数据库的读写分离。 TGroupDataSource 的配置 1、 配置读写分离权重: KEY : com.taobao.tddl.jdbc.group_V2.4.1_ “ groupKey ” (Matrix 中为“ dbKey ” ) VALUE : dbKey:r10w0,dbKey2:r0w10 TAtomDataSource 的配置(由 3 部分组成, global 、 app 、 user ) 1、 基本数据源信息 (global) : KEY : com.taobao.tddl.atom.global. “ dbKey ” VALUE :( ip= 数据库 IP port= 数据库端口 dbName= 数据库昵称 dbType= 数据库类型 dbStatus=RW ) 2、 数据库密码信息 (user) : KEY : com.taobao.tddl.atom.passwd. “ dbKey ” . “ dbType ” . “ dbUserName ” VALUE :数据库密码 3、 数据库连接信息( app ,如果不配置时间单位,缺省为分钟): KEY : com.taobao.tddl.atom.app. “ appName ” . “ dbKey ” VALUE :( userName= 数据库用户 minPoolSize= 最小连接数 maxPoolSize= 最大连接数 idleTimeout= 连接的最大空闲时间 blockingTimeout= 等待连接的最大时间 checkValidConnectionSQL=select 1 connectionProperties=rewriteBatchedStatements=true&characterEncoding=UTF8&connectTimeout=1000&autoReconnect=true&socketTimeout=12000 ) 应用层使用 TDDL 示例: public class UseTDDL { private static final String APPNAME = "tddl_test"; private static final String GROUP_KEY = "tddltest"; private static TGroupDataSource tGroupDataSource ; /* 初始化动态数据源 */ static { tGroupDataSource = new TGroupDataSource(); tGroupDataSource .setAppName( APPNAME ); tGroupDataSource .setDbGroupKey( GROUP_KEY ); tGroupDataSource .init(); } @Test public void testQuery() { final String LOAD_USER = "SELECT userName FROM tddl_table WHERE userName=?"; Connection conn = null ; PreparedStatement pstmt = null ; ResultSet rs = null ; try { conn = tGroupDataSource .getConnection(); pstmt = conn.prepareStatement(LOAD_USER); pstmt.setString(1, "tddl-test2"); rs = pstmt.executeQuery(); while (rs.next()) System. out .println("data: " + rs.getString(1)); } catch (Exception e) { e.printStackTrace(); } finally { try { if ( null != rs) rs.close(); if ( null != pstmt) pstmt.close(); if ( null != conn) conn.close(); } catch (Exception e) { e.printStackTrace(); } } } } 七、 Matrix 层的分库分表配置与实现 在上一章节中,笔者演示了如何在 Diamond 中配置数据库的读写分离,那么本章笔者则会演示如果配置 TDDL 的分库分表。 TDDL 的 Matrix 层是建立在动态数据源之上的,所以分库分表的配置和读写分离的基本配置也是一样的,只不过我们需要新添加 dbgroups 和 shardrule 项。dbgroups 项包含了我们所需要配置的所有 AppName 选项,而 shardrule 则是具体的分库分表规则。这里有一点需要提醒各位,在开源版本的 TDDL 中,配置TGroupDataSource 读写分离是使用 dbKey ,然而在 Matrix 中则是使用appName 。 1 、配置 Group 组: KEY : com.taobao.tddl.v1_ “ appName ” _dbgroups VALUE : appName1 , appName2 2 、配置分库分表规则: KEY : com.taobao.tddl.v1_”appName”_shardrule VALUE :( <?xml version="1.0" encoding="gb2312"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="root" class="com.taobao.tddl.common.config.beans.AppRule" init-method="init"> <property name="readwriteRule" ref="readwriteRule" /> </bean> <bean id="readwriteRule" class="com.taobao.tddl.common.config.beans.ShardRule"> <property name="dbtype" value="MYSQL" /> <property name="tableRules"> <map> <entry key="tddl_table" value-ref="tddl_table" /> </map> </property> </bean> <bean id="tddl_table" init-method="init" class="com.taobao.tddl.common.config.beans.TableRule"> <!-- 数据库组 index 号 --> <property name="dbIndexes" value="tddl_test,tddl_test2" /> <!-- 分库规则 --> <property name="dbRuleArray" value="(#id#.longValue() % 4).intdiv(2)"/> <!-- 分表规则 , 需要注意的是,因为 taobao 目前 dba 的要求是所有库内的表名必须完全不同,因此这里多加了一个映射的关系 简单来说,分表规则只会算表的 key. 俩库 4 表 : db1(tab1+tab2) db2(tab3+tab4) db1 == key: 0 value tab1 key: 1 value tab2 db2 == key: 0 value tab3 key: 1 value tab4 --> <property name="tbRuleArray" value="#id#.longValue() % 4 % 2"/> <property name="tbSuffix" value="throughAllDB:[_0-_3]" /> </bean> </beans> ) TDDL 的分库分表配置形式完全是采用 Spring 的配置形式,这一点大家应该是非常熟悉的。那么接下来我们一步一步的分析 TDDL 的分库分表规则。 在元素 <map/> 中我们可以定义我们所需要的分表,也就是说,当有多个表需要实现分表逻辑的时候,我们可以在集合中进行定义。当然我们还需要外部引用<bean/> 标签中定义的具体的表逻辑的分库分表规则。 在分库分表规则中,我们需要定义 数据库组 index 号,也就是说我们需要定义我们有多少的 appNames ,接下来我们就可以定义分库和分表规则了。 TDDL的分库分表规则完全是采用取余方式,比如 <property name="dbRuleArray" value="(#id#.longValue() % 4).intdiv(2)"/> , value 属性中包含有具体的分库规则,其中“ #id# ”作为我们的分库分表条件,此值在数据库中对应的类型必须是整类,然后进行取余后再进行 intdiv 。或许有些朋友看不太明白这个是什么意思,我们用简单的一点的话来说就是,“ #id#.longValue() % 4).intdiv(2) ”的含义是我们需要分 2个库和 4 个表,那么我们怎么知道我们的数据到底落盘到哪一个库呢?打个比方,如果我们的 id 等于 10 ,首先 10%4 等于 2 ,然后 2/2 等于 1 , TDDL 分库规则下标从 0 开始,那么我们的数据就是落盘到第 2 个库。 当大家明白 TDDL 的分库规则后,我们接下来再来分析分表规则 <property name="tbRuleArray" value="#id#.longValue() % 4 % 2"/> 。和分库规则类似的是,我们都采用取余算法首先进行运算,只不过分表尾运算也是使用取余,而不是除算。打个比方,如果我们的 id 等于 10 ,首先 10%4 等于 2 ,然后 2%2 等于 0 ,那么我们的数据就是落盘到第 2 个库的第 1 张表。 应用层使用 TDDL 示例: public class UseTDDL { private static final String APPNAME = "tddl_test"; private static final TDataSource dataSource ; /* 初始化动态数据源 */ static { dataSource = new TDataSource(); dataSource .setAppName( APPNAME ); dataSource .setUseLocalConfig( false ); dataSource .setDynamicRule( false ); dataSource .init(); } @Test public void query() { final String LOAD_USER = "SELECT userName FROM tddl_table WHERE id = ?"; Connection conn = null ; PreparedStatement pstmt = null ; ResultSet rs = null ; try { conn = dataSource .getConnection(); pstmt = conn.prepareStatement(LOAD_USER); pstmt.setLong(1, 3); rs = pstmt.executeQuery(); while (rs.next()) System. out .println("data: " + rs.getString(1)); } catch (Exception e) { e.printStackTrace(); } finally { try { if ( null != rs) rs.close(); if ( null != pstmt) pstmt.close(); if ( null != conn) conn.close(); } catch (Exception e) { e.printStackTrace(); } } } @Test public void insert() { final String LOAD_USER = "insert into tddl_table values(?, ?)"; Connection conn = null ; PreparedStatement pstmt = null ; try { conn = dataSource .getConnection(); pstmt = conn.prepareStatement(LOAD_USER); pstmt.setLong(1, 10); pstmt.setString(2, "JohnGao"); pstmt.execute(); System. out .println("insert success..."); } catch (Exception e) { e.printStackTrace(); } finally { try { if ( null != pstmt) pstmt.close(); if ( null != conn) conn.close(); } catch (Exception e) { e.printStackTrace(); } } } }
当我还年幼的时候,我很任性,复制数组也是,写一个for循环,来回倒腾,后来长大了,就发现了System.arraycopy的好处。 为了测试俩者的区别我写了一个简单赋值int[100000]的程序来对比,并且中间使用了nanoTime来计算时间差: 程序如下: int[] a = new int[100000]; for(int i=0;i<a.length;i++){ a[i] = i; } int[] b = new int[100000]; int[] c = new int[100000]; for(int i=0;i<c.length;i++){ c[i] = i; } int[] d = new int[100000]; for(int k=0;k<10;k++){ long start1 = System.nanoTime(); for(int i=0;i<a.length;i++){ b[i] = a[i]; } long end1 = System.nanoTime(); System.out.PRintln("end1 - start1 = "+(end1-start1)); long start2 = System.nanoTime(); System.arraycopy(c, 0, d, 0, 100000); long end2 = System.nanoTime(); System.out.println("end2 - start2 = "+(end2-start2)); System.out.println(); } 为了避免内存不稳定干扰和运行的偶然性结果,我在一开始的时候把所有空间申明完成,并且只之后循环10次执行,得到如下结果: end1 - start1 = 366806 end2 - start2 = 109154 end1 - start1 = 380529 end2 - start2 = 79849 end1 - start1 = 421422 end2 - start2 = 68769 end1 - start1 = 344463 end2 - start2 = 72020 end1 - start1 = 333174 end2 - start2 = 77277 end1 - start1 = 377335 end2 - start2 = 82285 end1 - start1 = 370608 end2 - start2 = 66937 end1 - start1 = 349067 end2 - start2 = 86532 end1 - start1 = 389974 end2 - start2 = 83362 end1 - start1 = 347937 end2 - start2 = 63638 可以看出,System.arraycopy的性能很不错,为了看看究竟这个底层是如何处理的,我找到openJDK的一些代码留恋了一些: System.arraycopy是一个native函数,需要看native层的代码: public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); 找到对应的openjdk6-src/hotspot/src/share/vm/prims/jvm.cpp,这里有JVM_ArrayCopy的入口: JVM_ENTRY(void, JVM_ArrayCopy(JNIEnv *env, jclass ignored, jobject src, jint src_pos, jobject dst, jint dst_pos, jint length)) JVMWrapper("JVM_ArrayCopy"); // Check if we have null pointers if (src == NULL || dst == NULL) { THROW(vmSymbols::java_lang_NullPointerException()); } arrayOop s = arrayOop(JNIHandles::resolve_non_null(src)); arrayOop d = arrayOop(JNIHandles::resolve_non_null(dst)); assert(s->is_oop(), "JVM_ArrayCopy: src not an oop"); assert(d->is_oop(), "JVM_ArrayCopy: dst not an oop"); // Do copy Klass::cast(s->klass())->copy_array(s, src_pos, d, dst_pos, length, thread); JVM_END 前面的语句都是判断,知道最后的copy_array(s, src_pos, d, dst_pos, length, thread)是真正的copy,进一步看这里,在openjdk6-src/hotspot/src/share/vm/oops/typeArrayKlass.cpp中: void typeArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d, int dst_pos, int length, TRAPS) { assert(s->is_typeArray(), "must be type array"); // Check destination if (!d->is_typeArray() || element_type() != typeArrayKlass::cast(d->klass())->element_type()) { THROW(vmSymbols::java_lang_ArrayStoreException()); } // Check is all offsets and lengths are non negative if (src_pos < 0 || dst_pos < 0 || length < 0) { THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException()); } // Check if the ranges are valid if ( (((unsigned int) length + (unsigned int) src_pos) > (unsigned int) s->length()) || (((unsigned int) length + (unsigned int) dst_pos) > (unsigned int) d->length()) ) { THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException()); } // Check zero copy if (length == 0) return; // This is an attempt to make the copy_array fast. int l2es = log2_element_size(); int ihs = array_header_in_bytes() / WordSize; char* src = (char*) ((oop*)s + ihs) + ((size_t)src_pos << l2es); char* dst = (char*) ((oop*)d + ihs) + ((size_t)dst_pos << l2es); Copy::conjoint_memory_atomic(src, dst, (size_t)length << l2es);//还是在这里处理copy } 这个函数之前的仍然是一堆判断,直到最后一句才是真实的拷贝语句。 在openjdk6-src/hotspot/src/share/vm/utilities/copy.cpp中找到对应的函数: // Copy bytes; larger units are filled atomically if everything is aligned. void Copy::conjoint_memory_atomic(void* from, void* to, size_t size) { address src = (address) from; address dst = (address) to; uintptr_t bits = (uintptr_t) src | (uintptr_t) dst | (uintptr_t) size; // (Note: We could improve performance by ignoring the low bits of size, // and putting a short cleanup loop after each bulk copy loop. // There are plenty of other ways to make this faster also, // and it's a slippery slope. For now, let's keep this code simple // since the simplicity helps clarify the atomicity semantics of // this Operation. There are also CPU-specific assembly versions // which may or may not want to include such optimizations.) if (bits % sizeof(jlong) == 0) { Copy::conjoint_jlongs_atomic((jlong*) src, (jlong*) dst, size / sizeof(jlong)); } else if (bits % sizeof(jint) == 0) { Copy::conjoint_jints_atomic((jint*) src, (jint*) dst, size / sizeof(jint)); } else if (bits % sizeof(jshort) == 0) { Copy::conjoint_jshorts_atomic((jshort*) src, (jshort*) dst, size / sizeof(jshort)); } else { // Not aligned, so no need to be atomic. Copy::conjoint_jbytes((void*) src, (void*) dst, size); } } 上面的代码展示了选择哪个copy函数,我们选择conjoint_jints_atomic,在openjdk6-src/hotspot/src/share/vm/utilities/copy.hpp进一步查看: // jints, conjoint, atomic on each jint static void conjoint_jints_atomic(jint* from, jint* to, size_t count) { assert_params_ok(from, to, LogBytesPerInt); pd_conjoint_jints_atomic(from, to, count); } 继续向下查看,在openjdk6-src/hotspot/src/cpu/zero/vm/copy_zero.hpp中: static void pd_conjoint_jints_atomic(jint* from, jint* to, size_t count) { _Copy_conjoint_jints_atomic(from, to, count); } 继续向下查看,在openjdk6-src/hotspot/src/os_cpu/linux_zero/vm/os_linux_zero.cpp中: void _Copy_conjoint_jints_atomic(jint* from, jint* to, size_t count) { if (from > to) { jint *end = from + count; while (from < end) *(to++) = *(from++); } else if (from < to) { jint *end = from; from += count - 1; to += count - 1; while (from >= end) *(to--) = *(from--); } } 可以看到,直接就是内存块赋值的逻辑了,这样避免很多引用来回倒腾的时间,必然就变快了。
Guice是由Google大牛Bob lee开发的一款绝对轻量级的java IoC容器。其优势在于: 速度快,号称比spring快100倍。 无外部配置(如需要使用外部可以可以选用Guice的扩展包),完全基于annotation特性,支持重构,代码静态检查。 简单,快速,基本没有学习成本。 Guice和spring各有所长,Guice更适合与嵌入式或者高性能但项目简单方案,如OSGI容器,spring更适合大型项目组织。 注入方式 在我们谈到IOC框架,首先我们的话题将是构造,属性以及函数注入方式,Guice的实现只需要在构造函数,字段,或者注入函数上标注@Inject,如: 构造注入 public class OrderServiceImpl implements OrderService { private ItemService itemService; private PriceService priceService; @Inject public OrderServiceImpl(ItemService itemService, PriceService priceService) { this.itemService = itemService; this.priceService = priceService; } ... } 属性注入 public class OrderServiceImpl implements OrderService { private ItemService itemService; private PriceService priceService; @Inject public void init(ItemService itemService, PriceService priceService) { this.itemService = itemService; this.priceService = priceService; } ... } 函数(setter)注入 public class OrderServiceImpl implements OrderService { private ItemService itemService; private PriceService priceService; @Inject public void setItemService(ItemService itemService) { this.itemService = itemService; } @Inject public void setPriceService(PriceService priceService) { this.priceService = priceService; } ... } Module依赖注册 Guice提供依赖配置类,需要继承至AbstractModule,实现configure方法。在configure方法中我们可以用Binder配置依赖。 Binder利用链式形成一套独具语义的DSL,如: 基本配置:binder.bind(serviceClass).to(implClass).in(Scopes.[SINGLETON | NO_SCOPE]); 无base类、接口配置:binder.bind(implClass).in(Scopes.[SINGLETON | NO_SCOPE]); service实例配置:binder.bind(serviceClass).toInstance(servieInstance).in(Scopes.[SINGLETON | NO_SCOPE]); 多个实例按名注入:binder.bind(serviceClass).annotatedWith(Names.named(“name”)).to(implClass).in(Scopes.[SINGLETON | NO_SCOPE]); 运行时注入:利用@Provides标注注入方法,相当于spring的@Bean。 @ImplementedBy:或者在实现接口之上标注@ImplementedBy指定其实现类。这种方式有点反OO设计,抽象不该知道其实现类。 对于上面的配置在注入的方式仅仅需要@Inject标注,但对于按名注入需要在参数前边加入@Named标注,如: public void configure() { final Binder binder = binder(); //TODO: bind named instance; binder.bind(NamedService.class).annotatedWith(Names.named("impl1")).to(NamedServiceImpl1.class); binder.bind(NamedService.class).annotatedWith(Names.named("impl2")).to(NamedServiceImpl2.class); } @Inject public List<NamedService> getAllItemServices(@Named("impl1") NamedService nameService1, @Named("impl2") NamedService nameService2) { } Guice也可以利用@Provides标注注入方法来运行时注入:如 @Provides public List<NamedService> getAllItemServices(@Named("impl1") NamedService nameService1, @Named("impl2") NamedService nameService2) { final ArrayList<NamedService> list = new ArrayList<NamedService>(); list.add(nameService1); list.add(nameService2); return list; } Guice实例 下面是一个Guice module的实例代码:包含大部分常用依赖配置方式。更多代码参见github . package com.github.greengerong.app; /** * *************************************** * * * Auth: green gerong * * Date: 2014 * * blog: http://greengerong.github.io/ * * github: https://github.com/greengerong * * * * **************************************** */ public class AppModule extends AbstractModule { private static final Logger LOGGER = LoggerFactory.getLogger(AppModule.class); private final BundleContext bundleContext; public AppModule(BundleContext bundleContext) { this.bundleContext = bundleContext; LOGGER.info(String.format("enter app module with: %s", bundleContext)); } @Override public void configure() { final Binder binder = binder(); //TODO: bind interface binder.bind(ItemService.class).to(ItemServiceImpl.class).in(SINGLETON); binder.bind(OrderService.class).to(OrderServiceImpl.class).in(SINGLETON); //TODO: bind self class(without interface or base class) binder.bind(PriceService.class).in(Scopes.SINGLETON); //TODO: bind instance not class. binder.bind(RuntimeService.class).toInstance(new RuntimeService()); //TODO: bind named instance; binder.bind(NamedService.class).annotatedWith(Names.named("impl1")).to(NamedServiceImpl1.class); binder.bind(NamedService.class).annotatedWith(Names.named("impl2")).to(NamedServiceImpl2.class); } @Provides public List<NamedService> getAllItemServices(@Named("impl1") NamedService nameService1, @Named("impl2") NamedService nameService2) { final ArrayList<NamedService> list = new ArrayList<NamedService>(); list.add(nameService1); list.add(nameService2); return list; } } Guice的使用 对于Guice的使用则比较简单,利用利用Guice module初始化Guice创建其injector,如: Injector injector = Guice.createInjector(new AppModule(bundleContext)); 这里可以传入多个module,我们可以利用module分离领域依赖。 Guice api方法: public static Injector createInjector(Module... modules) public static Injector createInjector(Iterable<? extends Module> modules) public static Injector createInjector(Stage stage, Module... modules) public static Injector createInjector(Stage stage, Iterable<? extends Module> modules) Guice同时也支持不同Region配置,上面的State重载,state支持 TOOL,DEVELOPMENT,PRODUCTION选项;默认为DEVELOPMENT环境。
Elasticsearch 是最近两年异军突起的一个兼有搜索引擎和NoSQL数据库功能的开源系统,基于Java/Lucene构建。最近研究了一下,感觉 Elasticsearch 的架构以及其开源的生态构建都有许多可借鉴之处,所以整理成文章分享下。本文的代码以及架构分析主要基于 Elasticsearch 2.X 最新稳定版。 Elasticsearch 看名字就能大概了解下它是一个弹性的搜索引擎。首先弹性隐含的意思是分布式,单机系统是没法弹起来的,然后加上灵活的伸缩机制,就是这里的 Elastic 包含的意思。它的搜索存储功能主要是 Lucene 提供的,Lucene 相当于其存储引擎,它在之上封装了索引,查询,以及分布式相关的接口。 Elasticsearch 中的几个概念 集群(Cluster)一组拥有共同的 cluster name 的节点。 节点(Node) 集群中的一个 Elasticearch 实例。 索引(Index) 相当于关系数据库中的database概念,一个集群中可以包含多个索引。这个是个逻辑概念。 主分片(Primary shard) 索引的子集,索引可以切分成多个分片,分布到不同的集群节点上。分片对应的是 Lucene 中的索引。 副本分片(Replica shard)每个主分片可以有一个或者多个副本。 类型(Type)相当于数据库中的table概念,mapping是针对 Type 的。同一个索引里可以包含多个 Type。 Mapping 相当于数据库中的schema,用来约束字段的类型,不过 Elasticsearch 的 mapping 可以自动根据数据创建。 文档(Document) 相当于数据库中的row。 字段(Field)相当于数据库中的column。 分配(Allocation) 将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。 分布式以及 Elastic 分布式系统要解决的第一个问题就是节点之间互相发现以及选主的机制。如果使用了 Zookeeper/Etcd 这样的成熟的服务发现工具,这两个问题都一并解决了。但 Elasticsearch 并没有依赖这样的工具,带来的好处是部署服务的成本和复杂度降低了,不用预先依赖一个服务发现的集群,缺点当然是将复杂度带入了 Elasticsearch 内部。 服务发现以及选主 ZenDiscovery 节点启动后先ping(这里的ping是 Elasticsearch 的一个RPC命令。如果 discovery.zen.ping.unicast.hosts 有设置,则ping设置中的host,否则尝试ping localhost 的几个端口, Elasticsearch 支持同一个主机启动多个节点) Ping的response会包含该节点的基本信息以及该节点认为的master节点。 选举开始,先从各节点认为的master中选,规则很简单,按照id的字典序排序,取第一个。 如果各节点都没有认为的master,则从所有节点中选择,规则同上。这里有个限制条件就是 discovery.zen.minimum_master_nodes,如果节点数达不到最小值的限制,则循环上述过程,直到节点数足够可以开始选举。 最后选举结果是肯定能选举出一个master,如果只有一个local节点那就选出的是自己。 如果当前节点是master,则开始等待节点数达到 minimum_master_nodes,然后提供服务。 如果当前节点不是master,则尝试加入master。 Elasticsearch 将以上服务发现以及选主的流程叫做 ZenDiscovery 。由于它支持任意数目的集群(1-N),所以不能像 Zookeeper/Etcd 那样限制节点必须是奇数,也就无法用投票的机制来选主,而是通过一个规则,只要所有的节点都遵循同样的规则,得到的信息都是对等的,选出来的主节点肯定是一致的。但分布式系统的问题就出在信息不对等的情况,这时候很容易出现脑裂(Split-Brain)的问题,大多数解决方案就是设置一个quorum值,要求可用节点必须大于quorum(一般是超过半数节点),才能对外提供服务。而 Elasticsearch 中,这个quorum的配置就是 discovery.zen.minimum_master_nodes 。 说到这里要吐槽下 Elasticsearch 的方法和变量命名,它的方法和配置中的master指的是master的候选节点,也就是说可能成为master的节点,并不是表示当前的master,我就被它的一个 isMasterNode 方法坑了,开始一直没能理解它的选举规则。 弹性伸缩 Elastic Elasticsearch 的弹性体现在两个方面: 1. 服务发现机制让节点很容易加入和退出。 2. 丰富的设置以及allocation API。 Elasticsearch 节点启动的时候只需要配置discovery.zen.ping.unicast.hosts,这里不需要列举集群中所有的节点,只要知道其中一个即可。当然为了避免重启集群时正好配置的节点挂掉,最好多配置几个节点。节点退出时只需要调用 API 将该节点从集群中排除 (Shard Allocation Filtering),系统会自动迁移该节点上的数据,然后关闭该节点即可。当然最好也将不可用的已知节点从其他节点的配置中去除,避免下次启动时出错。 分片(Shard)以及副本(Replica) 分布式存储系统为了解决单机容量以及容灾的问题,都需要有分片以及副本机制。Elasticsearch 没有采用节点级别的主从复制,而是基于分片。它当前还未提供分片切分(shard-splitting)的机制,只能创建索引的时候静态设置。 (elasticsearch 官方博客的图片) 比如上图所示,开始设置为5个分片,在单个节点上,后来扩容到5个节点,每个节点有一个分片。如果继续扩容,是不能自动切分进行数据迁移的。官方文档的说法是分片切分成本和重新索引的成本差不多,所以建议干脆通过接口重新索引。 Elasticsearch 的分片默认是基于id 哈希的,id可以用户指定,也可以自动生成。但这个可以通过参数(routing)或者在mapping配置中修改。当前版本默认的哈希算法是MurmurHash3。 Elasticsearch 禁止同一个分片的主分片和副本分片在同一个节点上,所以如果是一个节点的集群是不能有副本的。 恢复以及容灾 分布式系统的一个要求就是要保证高可用。前面描述的退出流程是节点主动退出的场景,但如果是故障导致节点挂掉,Elasticsearch 就会主动allocation。但如果节点丢失后立刻allocation,稍后节点恢复又立刻加入,会造成浪费。Elasticsearch的恢复流程大致如下: 集群中的某个节点丢失网络连接 master提升该节点上的所有主分片的在其他节点上的副本为主分片 cluster集群状态变为 yellow ,因为副本数不够 等待一个超时设置的时间,如果丢失节点回来就可以立即恢复(默认为1分钟,通过 index.unassigned.node_left.delayed_timeout 设置)。如果该分片已经有写入,则通过translog进行增量同步数据。 否则将副本分配给其他节点,开始同步数据。 但如果该节点上的分片没有副本,则无法恢复,集群状态会变为red,表示可能要丢失该分片的数据了。 分布式集群的另外一个问题就是集群整个重启后可能导致不预期的分片重新分配(部分节点没有启动完成的时候,集群以为节点丢失),浪费带宽。所以 Elasticsearch 通过以下静态配置(不能通过API修改)控制整个流程,以10个节点的集群为例: gateway.recover_after_nodes: 8 gateway.expected_nodes: 10 gateway.recover_after_time: 5m 比如10个节点的集群,按照上面的规则配置,当集群重启后,首先系统等待 minimum_master_nodes(6)个节点加入才会选出master, recovery操作是在 master节点上进行的,由于我们设置了 recover_after_nodes(8),系统会继续等待到8个节点加入, 才开始进行recovery。当开始recovery的时候,如果发现集群中的节点数小于expected_nodes,也就是还有部分节点未加入,于是开始recover_after_time 倒计时(如果节点数达到expected_nodes则立刻进行 recovery),5分钟后,如果剩余的节点依然没有加入,则会进行数据recovery。 搜索引擎 Search Elasticsearch 除了支持 Lucene 本身的检索功能外,在之上做了一些扩展。 1. 脚本支持 Elasticsearch 默认支持groovy脚本,扩展了 Lucene 的评分机制,可以很容易的支持复杂的自定义评分算法。它默认只支持通过sandbox方式实现的脚本语言(如lucene expression,mustache),groovy必须明确设置后才能开启。Groovy的安全机制是通过java.security.AccessControlContext设置了一个class白名单来控制权限的,1.x版本的时候是自己做的一个白名单过滤器,但限制策略有漏洞,导致一个远程代码执行漏洞。 2. 默认会生成一个 _all 字段,将所有其他字段的值拼接在一起。这样搜索时可以不指定字段,并且方便实现跨字段的检索。 3. Suggester Elasticsearch 通过扩展的索引机制,可以实现像google那样的自动完成suggestion以及搜索词语错误纠正的suggestion。 NoSQL 数据库 Elasticsearch 可以作为数据库使用,主要依赖于它的以下特性: 默认在索引中保存原始数据,并可获取。这个主要依赖 Lucene 的store功能。 实现了translog,提供了实时的数据读取能力以及完备的数据持久化能力(在服务器异常挂掉的情况下依然不会丢数据)。Lucene 因为有 IndexWriter buffer, 如果进程异常挂掉,buffer中的数据是会丢失的。所以 Elasticsearch 通过translog来确保不丢数据。同时通过id直接读取文档的时候,Elasticsearch 会先尝试从translog中读取,之后才从索引中读取。也就是说,即便是buffer中的数据尚未刷新到索引,依然能提供实时的数据读取能力。Elasticsearch 的translog 默认是每次写请求完成后统一fsync一次,同时有个定时任务检测(默认5秒钟一次)。如果业务场景需要更大的写吞吐量,可以调整translog相关的配置进行优化。 dynamic-mapping 以及 schema-free Elasticsearch 的dynamic-mapping相当于根据用户提交的数据,动态检测字段类型,自动给数据库表建立表结构,也可以动态增加字段,所以它叫做schema-free,而不是schema-less。这种方式的好处是用户能一定程度享受schema-less的好处,不用提前建立表结构,同时因为实际上是有schema的,可以做查询上的优化,检索效率要比纯schema-less的数据库高许多。但缺点就是已经创建的索引不能变更数据类型(Elasticsearch 写入数据的时候如果类型不匹配会自动尝试做类型转换,如果失败就会报错,比如数字类型的字段写入字符串”123”是可以的,但写入”abc”就不可以。),要损失一定的自由度。 另外 Elasticsearch 提供的index-template功能方便用户动态创建索引的时候预先设定索引的相关参数以及type mapping,比如按天创建日志库,template可以设置为对 log-* 的索引都生效。 这两个功能我建议新的数据库都可以借鉴下。 丰富的QueryDSL功能 Elasticsearch 的query语法基本上和sql对等的,除了join查询,以及嵌套临时表查询不能支持。不过 Elasticsearch 支持嵌套对象以及parent外部引用查询,所以一定程度上可以解决关联查询的需求。另外group by这种查询可以通过其aggregation实现。Elasticsearch 提供的aggregation能力非常强大,其生态圈里的 Kibana 主要就是依赖aggregation来实现数据分析以及可视化的。 系统架构 Elasticsearch 的依赖注入用的是guice,网络使用netty,提供http rest和RPC两种协议。 Elasticsearch 之所以用guice,而不是用spring做依赖注入,关键的一个原因是guice可以帮它很容易的实现模块化,通过代码进行模块组装,可以很精确的控制依赖注入的管理范围。比如 Elasticsearch 给每个shard单独生成一个injector,可以将该shard相关的配置以及组件注入进去,降低编码和状态管理的复杂度,同时删除shard的时候也方便回收相关对象。这方面有兴趣使用guice的可以借鉴。 ClusterState 前面我们分析了 Elasticsearch 的服务发现以及选举机制,它是内部自己实现的。服务发现工具做的事情其实就是跨服务器的状态同步,多个节点修改同一个数据对象,需要有一种机制将这个数据对象同步到所有的节点。Elasticsearch 的ClusterState 就是这样一个数据对象,保存了集群的状态,索引/分片的路由表,节点列表,元数据等,还包含一个ClusterBlocks,相当于分布式锁,用于实现分布式的任务同步。 主节点上有个单独的进程处理 ClusterState 的变更操作,每次变更会更新版本号。变更后会通过PRC接口同步到其他节点。主节知道其他节点的ClusterState 的当前版本,发送变更的时候会做diff,实现增量更新。 Rest 和 RPC Elasticsearch 的rest请求的传递流程如上图(这里对实际流程做了简化): 1. 用户发起http请求,Elasticsearch 的9200端口接受请求后,传递给对应的RestAction。 2. RestAction做的事情很简单,将rest请求转换为RPC的TransportRequest,然后调用NodeClient,相当于用客户端的方式请求RPC服务,只不过transport层会对本节点的请求特殊处理。 这样做的好处是将http和RPC两层隔离,增加部署的灵活性。部署的时候既可以同时开启RPC和http服务,也可以用client模式部署一组服务专门提供http rest服务,另外一组只开启RPC服务,专门做data节点,便于分担压力。 Elasticsearch 的RPC的序列化机制使用了 Lucene 的压缩数据类型,支持vint这样的变长数字类型,省略了字段名,用流式方式按顺序写入字段的值。每个需要传输的对象都需要实现: void writeTo(StreamOutput out) T readFrom(StreamInput in) 两个方法。虽然这样实现开发成本略高,增删字段也不太灵活,但对 Elasticsearch 这样的数据库系统来说,不用考虑跨语言,增删字段肯定要考虑兼容性,这样做效率最高。所以 Elasticsearch 的RPC接口只有java client可以直接请求,其他语言的客户端都走的是rest接口。 网络层 Elasticsearch 的网络层抽象很值得借鉴。它抽象出一个 Transport 层,同时兼有client和server功能,server端接收其他节点的连接,client维持和其他节点的连接,承担了节点之间请求转发的功能。Elasticsearch 为了避免传输流量比较大的操作堵塞连接,所以会按照优先级创建多个连接,称为channel。 recovery: 2个channel专门用做恢复数据。如果为了避免恢复数据时将带宽占满,还可以设置恢复数据时的网络传输速度。 bulk: 3个channel用来传输批量请求等基本比较低的请求。 regular: 6个channel用来传输通用正常的请求,中等级别。 state: 1个channel保留给集群状态相关的操作,比如集群状态变更的传输,高级别。 ping: 1个channel专门用来ping,进行故障检测。 (3个节点的集群连接示意,来源 Elasticsearch 官方博客) 每个节点默认都会创建13个到其他节点的连接,并且节点之间是互相连接的,每增加一个节点,该节点会到每个节点创建13个连接,而其他每个节点也会创建13个连回来的连接。 线程池 由于java不支持绿色线程(fiber/coroutine),我前面的《并发之痛》那篇文章也分析了线程池的问题,线程池里保留多少线程合适?如何避免慢的任务占用线程池,导致其他比较快的任务也得不到执行?很多应用系统里,为了避免这种情况,会随手创建线程池,最后导致系统里充塞了大的量的线程池,浪费资源。而 Elasticsearch 的解决方案是分优先级的线程池。它默认创建了10多个线程池,按照不同的优先级以及不同的操作进行划分。然后提供了4种类型的线程池,不同的线程池使用不同的类型: CACHED 最小为0,无上限,无队列(SynchronousQueue,没有缓冲buffer),有存活时间检测的线程池。通用的,希望能尽可能支撑的任务。 DIRECT 直接在调用者的线程里执行,其实这不算一种线程池方案,主要是为了代码逻辑上的统一而创造的一种线程类型。 FIXED 固定大小的线程池,带有缓冲队列。用于计算和IO的耗时波动较小的操作。 SCALING 有最小值,最大值的伸缩线程池,队列是基于LinkedTransferQueue 改造的实现,和java内置的Executors生成的伸缩线程池的区别是优先增加线程,增加到最大值后才会使用队列,和java内置的线程池规则相反。用于计算和IO耗时都不太稳定,需要限制系统承载最大任务上限的操作。 这种解决方案虽然要求每个用到线程池的地方都需要评估下执行成本以及应该用什么样的线程池,但好处是限制了线程池的泛滥,也缓解了不同类型的任务互相之间的影响。 脑洞时间 以后每篇分析架构的文章,我都最后会提几个和该系统相关的改进或者扩展的想法,称为脑洞时间,作为一种锻炼。不过只提供想法,不深入分析可行性以及实现。 支持shard-spliting 这个被人吐糟了好长时间,官方就是不愿意提供。我简单构想了下,感觉实现这个应该也不复杂。一种实现方式是按照传统的数据库sharding机制,1分2,2分4,4分8等,主要扩展点在数据迁移以及routing的机制上。但这种方式没办法实现1分3,3分5,这样的sharding。另外一个办法就是基于当前官方推荐的重建索引的机制,只是对外封装成resharding的接口,先给旧索引创建别名,客户端通过别名访问索引,然后设定新索引的sharding数目,后台创建新的索引,倒数据,等数据追上的时候,切换别名,进行完整性检查,这样整个resharding的机制可以自动化了。 支持mapreduce 认为Elasticsearch 可以借鉴 Mongo 的轻量mapreduce机制,这样可以支持更丰富的聚合查询。 支持语音以及图片检索 当前做语音和图片识别的库或者服务的开发者可以提供一个 Elasticsearch 插件,把语音以及图片转换成文本进行索引查询,应用场景应该也不少。 用ForkJoinPool来替代 Elasticsearch 当前的线程池方案 ForkJoinPool加上java8的CompletableFuture,一定程度上可以模拟coroutine效果,再加上最新版本的netty内部已经默认用了ForkJoinPool,Elasticsearch 这种任务有需要拆子任务的场景,很适合使用ForkJoinPool。 Elasticsearch 的开源产品启示 还记得10年前在大学时候捣鼓 Lucene,弄校园内搜索,还弄了个基于词典的分词工具。毕业后第一份工作也是用 Lucene 做站内搜索。当时搭建的服务和 Elasticsearch 类似,提供更新和管理索引的api给业务程序,当然没有 Elasticsearch 这么强大。当时是有想过做类似的一个开源产品的,后来发现apache已经出了 Solr(2004年的时候就创建了,2008年1.3发布,已经相对成熟),感觉应该没啥机会了。但 Elasticsearch 硬是在这种情况下成长起来了(10年创建,14年才发布1.0)。 二者的功能以及性能几乎都不相上下(开始性能上有些差距,但 Solr 有改进,差不多追上了),参看文末比较链接。 我觉得一方面是 Elasticsearch 的简单友好的分布式机制占了先机,也正好赶上了移动互联网爆发移动应用站内搜索需求高涨的时代。第一波站内搜索是web时代,也是 Lucene 诞生的时代,但web的站内搜索可以简单的利用搜索引擎服务的自定义站点实现,而应用的站内搜索就只能靠自己搭了。另外一方面是 Elasticsearch 的周边生态以及目标市场看把握的非常精准。Elasticsearch 现在的主要目标市场已经从站内搜索转移到了监控与日志数据的收集存储和分析,也就是大家常谈论的ELK。 Elasticsearch 现在主要的应用场景有三块。站内搜索,主要和 Solr 竞争,属于后起之秀。NoSQL json文档数据库,主要抢占 Mongo 的市场,它在读写性能上优于 Mongo(见文末比较链接),同时也支持地理位置查询,还方便地理位置和文本混合查询,属于歪打正着。监控,统计以及日志类时间序的数据的存储和分析以及可视化,这方面是引领者。 据说 Elasticsearch 的创始人当初创建 Elasticsearch 的时候是为了给喜欢做菜的媳妇搭建个菜谱的搜索网站,虽然菜谱搜索网站最后一直没做出来,但诞生了 Elasticsearch。所以程序员坚持一个业余项目也是很重要的,万一无心插柳就成荫了呢?
API Feature Solr 6.2.1 ElasticSearch 5.0 Format XML, CSV, JSON JSON HTTP REST API Binary API SolrJ TransportClient, Thrift (through a plugin) JMX support ES specific stats are exposed through the REST API Official client libraries Java Java, Groovy, PHP, Ruby, Perl, Python, .NET, Javascript Official list of clients Community client libraries PHP, Ruby, Perl, Scala, Python, .NET, Javascript, Go, Erlang, Clojure Clojure, Cold Fusion, Erlang, Go, Groovy, Haskell, Java, JavaScript, .NET, OCaml, Perl, PHP, Python, R, Ruby, Scala, Smalltalk, Vert.x Complete list 3rd-party product integration (open-source) Drupal, Magento, Django, ColdFusion, Wordpress, OpenCMS, Plone, Typo3, ez Publish, Symfony2, Riak (via Yokozuna) Drupal, Django, Symfony2, Wordpress, CouchBase 3rd-party product integration (commercial) DataStax Enterprise Search, Cloudera Search, Hortonworks Data Platform, MapR SearchBlox, Hortonworks Data Platform, MapR etc Complete list Output JSON, XML, PHP, Python, Ruby, CSV, Velocity, XSLT, native Java JSON, XML/HTML (via plugin) Infrastructure Feature Solr 6.2.1 ElasticSearch 5.0 Master-slave replication Only in non-SolrCloud. In SolrCloud, behaves identically to ES. Not an issue because shards are replicated across nodes. Integrated snapshot and restore Filesystem Filesystem, AWS Cloud Plugin for S3 repositories, HDFS Plugin for Hadoop environments, Azure Cloud Plugin for Azure storage repositories Indexing Feature Solr 6.2.1 ElasticSearch 5.0 Data Import DataImportHandler - JDBC, CSV, XML, Tika, URL, Flat File [DEPRECATED in 2.x] Rivers modules - ActiveMQ, Amazon SQS, CouchDB, Dropbox, DynamoDB, FileSystem, Git, GitHub, Hazelcast, JDBC, JMS, Kafka, LDAP, MongoDB, neo4j, OAI, RabbitMQ, Redis, RSS, Sofa, Solr, St9, Subversion, Twitter, Wikipedia ID field for updates and deduplication DocValues Partial Doc Updates with stored fields with _source field Custom Analyzers and Tokenizers Per-field analyzer chain Per-doc/query analyzer chain Index-time synonyms Supports Solr and Wordnet synonym format Query-time synonyms especially via hon-lucene-synonyms Technically, yes, but practically no because multi-word/phrase query-time synonyms are not supported. See ES docs and hon-lucene-synonyms blog for nuances. Multiple indexes Near-Realtime Search/Indexing Complex documents Schemaless 4.4+ Multiple document types per schema One set of fields per schema, one schema per core Online schema changes Schemaless mode or via dynamic fields. Only backward-compatible changes. Apache Tika integration Dynamic fields Field copying via multi-fields Hash-based deduplication Murmur plugin or ER plugin Searching Feature Solr 6.2.1 ElasticSearch 5.0 Lucene Query parsing Structured Query DSL Need to programmatically create queries if going beyond Lucene query syntax. Span queries via SOLR-2703 Spatial/geo search Multi-point spatial search Faceting Top N term accuracy can be controlled with shard_size Advanced Faceting New JSON faceting API as of Solr 5.x blog post Geo-distance Faceting Pivot Facets More Like This Boosting by functions Boosting using scripting languages Push Queries JIRA issue Percolation. Distributed percolation supported in 1.0 Field collapsing/Results grouping Query Re-Ranking via Rescoring or a plugin Index-based Spellcheck Phrase Suggester Wordlist-based Spellcheck Autocomplete Query elevation workaround Intra-index joins via parent-child query via has_children and top_children queries Inter-index joins Joined index has to be single-shard and replicated across all nodes. Resultset Scrolling New to 4.7.0 via scan search type Filter queries also supports filtering by native scripts Filter execution order local params and cache property Alternative QueryParsers DisMax, eDisMax query_string, dis_max, match, multi_match etc Negative boosting but awkward. Involves positively boosting the inverse set of negatively-boosted documents. Search across multiple indexes it can search across multiple compatible collections Result highlighting Custom Similarity Searcher warming on index reload Warmers API Term Vectors API Customizability Feature Solr 6.2.1 ElasticSearch 5.0 Pluggable API endpoints Pluggable search workflow via SearchComponents Pluggable update workflow via UpdateRequestProcessor Pluggable Analyzers/Tokenizers Pluggable QueryParsers Pluggable Field Types Pluggable Function queries Pluggable scoring scripts Pluggable hashing Pluggable webapps [site plugins DEPRECATED in 5.x] blog post Automated plugin installation Installable from GitHub, maven, sonatype or elasticsearch.org Distributed Feature Solr 6.2.1 ElasticSearch 5.0 Self-contained cluster Depends on separate ZooKeeper server Only Elasticsearch nodes Automatic node discovery ZooKeeper internal Zen Discovery or ZooKeeper Partition tolerance The partition without a ZooKeeper quorum will stop accepting indexing requests or cluster state changes, while the partition with a quorum continues to function. Partitioned clusters can diverge unless discovery.zen.minimum_master_nodes set to at least N/2+1, where N is the size of the cluster. If configured correctly, the partition without a quorum will stop operating, while the other continues to work. See this Automatic failover If all nodes storing a shard and its replicas fail, client requests will fail, unless requests are made with the shards.tolerant=true parameter, in which case partial results are retuned from the available shards. Automatic leader election Shard replication Sharding Automatic shard rebalancing it can be machine, rack, availability zone, and/or data center aware. Arbitrary tags can be assigned to nodes and it can be configured to not assign the same shard and its replicates on a node with the same tags. Change # of shards Shards can be added (when using implicit routing) or split (when using compositeId). Cannot be lowered. Replicas can be increased anytime. each index has 5 shards by default. Number of primary shards cannot be changed once the index is created. Replicas can be increased anytime. Shard splitting Relocate shards and replicas can be done by creating a shard replicate on the desired node and then removing the shard from the source node can move shards and replicas to any node in the cluster on demand Control shard routing shards or _route_ parameter routing parameter Pluggable shard/replica assignment Rule-based replica assignment Probabilistic shard balancing with Tempest plugin Consistency Indexing requests are synchronous with replication. A indexing request won't return until all replicas respond. No check for downed replicas. They will catch up when they recover. When new replicas are added, they won't start accepting and responding to requests until they are finished replicating the index. Replication between nodes is synchronous by default, thus ES is consistent by default, but it can be set to asynchronous on a per document indexing basis. Index writes can be configured to fail is there are not sufficient active shard replicas. The default is quorum, but all or one are also available. Misc Feature Solr 6.2.1 ElasticSearch 5.0 Web Admin interface bundled with Solr Marvel or Kibana apps Visualisation Banana (Port of Kibana) Kibana Hosting providers WebSolr, Searchify, Hosted-Solr, IndexDepot, OpenSolr, gotosolr Found, ObjectRocket, bonsai.io, Indexisto, qbox.io, IndexDepot, Compose.io Thoughts... I'm embedding my answer to this "Solr-vs-Elasticsearch" Quora question verbatim here: 1. Elasticsearch was born in the age of REST APIs. If you love REST APIs, you'll probably feel more at home with ES from the get-go. I don't actually think it's 'cleaner' or 'easier to use', but just that it is more aligned with web 2.0 developers' mindsets. 2. Elasticsearch's Query DSL syntax is really flexible and it's pretty easy to write complex queries with it, though it does border on being verbose. Solr doesn't have an equivalent, last I checked. Having said that, I've never found Solr's query syntax wanting, and I've always been able to easily write a custom SearchComponent if needed (more on this later). 3. I find Elasticsearch's documentation to be pretty awful. It doesn't help that some examples in the documentation are written in YAML and others in JSON. I wrote a ES code parser once to auto-generate documentation from Elasticsearch's source and found a number of discrepancies between code and what's documented on the website, not to mention a number of undocumented/alternative ways to specify the same config key. By contrast, I've found Solr to be consistent and really well-documented. I've found pretty much everything I've wanted to know about querying and updating indices without having to dig into code much. Solr's schema.xml and solrconfig.xml are *extensively* documented with most if not all commonly used configurations. 4. Whilst what Rick says about ES being mostly ready to go out-of-box is true, I think that is also a possible problem with ES. Many users don't take the time to do the most simple config (e.g. type mapping) of ES because it 'just works' in dev, and end up running into issues in production. And once you do have to do config, then I personally prefer Solr's config system over ES'. Long JSON config files can get overwhelming because of the JSON's lack of support for comments. Yes you can use YAML, but it's annoying and confusing to go back and forth between YAML and JSON. 5. If your own app works/thinks in JSON, then without a doubt go for ES because ES thinks in JSON too. Solr merely supports it as an afterthought. ES has a number of nice JSON-related features such as parent-child and nested docs that makes it a very natural fit. Parent-child joins are awkward in Solr, and I don't think there's a Solr equivalent for ES Inner hits. 6. ES doesn't require ZooKeeper for it's 'elastic' features which is nice coz I personally find ZK unpleasant, but as a result, ES does have issues with split-brain scenarios though (google 'elasticsearch split-brain' or see this: Elasticsearch Resiliency Status). 7. Overall from working with clients as a Solr/Elasticsearch consultant, I've found that developer preferences tend to end up along language party lines: if you're a Java/c# developer, you'll be pretty happy with Solr. If you live in Javascript or Ruby, you'll probably love Elasticsearch. If you're on Python or PHP, you'll probably be fine with either. Something to add about this: ES doesn't have a very elegant Java API IMHO (you'll basically end up using REST because it's less painful), whereas Solrj is very satisfactory and more efficient than Solr's REST API. If you're primarily a Java dev team, do take this into consideration for your sanity. There's no scenario in which constructing JSON in Java is fun/simple, whereas in Python its absolutely pain-free, and believe me, if you have a non-trivial app, your ES json query strings will be works of art. 8. ES doesn't have in-built support for pluggable 'SearchComponents', to use Solr's terminology. SearchComponents are (for me) a pretty indispensable part of Solr for anyone who needs to do anything customized and in-depth with search queries. Yes of course, in ES you can just implement your own RestHandler, but that's just not the same as being able to plug-into and rewire the way search queries are handled and parsed. 9. Whichever way you go, I highly suggest you choose a client library which is as 'close to the metal' as you can get. Both ES and Solr have *really* simple search and updating search APIs. If a client library introduces an additional DSL layer in attempt to 'simplify', I suggest you think long and hard about using it, as it's likely to complicate matters in the long-run, and make debugging and asking for help on SO more problematic. In particular, if you're using Rails + Solr, consider using rsolr/rsolr instead of sunspot/sunspot if you can help it. ActiveRecord is complex code and sufficiently magical. The last thing you want is more magic on top of that. --- To conclude, ES and Solr have more or less feature-parity and from a feature standpoint, there's rarely one reason to go one way or the other (unless your app lives/breathes JSON). Performance-wise, they are also likely to be quite similar (I'm sure there are exceptions to the rule. ES' relatively new autocomplete implementation, for example, is a pretty dramatic departure from previous Lucene/Solr implementations, and I suspect it produces faster responses at scale). ES does offer less friction from the get-go and you feel like you have something working much quicker, but I find this to be illusory. Any time gained in this stage is lost when figuring out how to properly configure ES because of poor documentation - an inevitablity when you have a non-trivial application. Solr encourages you to understand a little more about what you're doing, and the chance of you shooting yourself in the foot is somewhat lower, mainly because you're forced to read and modify the 2 well-documented XML config files in order to have a working search app. --- EDIT on Nov 2015: ES has been gradually distinguishing itself from Solr when it comes to data analytics. I think it's fair to attribute this to the immense traction of the ELK stack in the logging, monitoring and analytic space. My guess is that this is where Elastic (the company) gets the majority of its revenue, so it makes perfect sense that ES (the product) reflects this. We see this manifesting primarily in the form of aggregations, which is a more flexible and nuanced replacement for facets. Read more about aggregations here: Migrating to aggregations Aggregations have been out for a while now (since 1.4), but with the recently released ES 2.0 comes pipeline aggregations, which let you compute aggregations such as derivatives, moving averages, and series arithmetic on the results of other aggregations. Very cool stuff, and Solr simply doesn't have an equivalent. More on pipeline aggregations here: Out of this world aggregations If you're currently using or contemplating using Solr in an analytics app, it is worth your while to look into ES aggregation features to see if you need any of it.
我们在2016年五月开源了DistributedLog项目,引起了社区的广泛关注。大家常常问起的问题之一就是DistributedLog与Apache Kafka相对比,各有什么优劣。从技术上来讲DistributedLog并不是一个象Apache Kafka那么成熟的、有分区机制的广播/订阅系统。DistributedLog是一个复制日志流仓库,它用Apache BookKeeper来做日志分区仓库。它关注的是构建可靠的实时系统所需要的持久性、副本和强一致性。可以把DistributedLog用于构建或尝试各种不同的消息通信模型,比如队列、广播/订阅等。 因为两者都是处理日志,数据模型也类似,所以这篇文章主要从技术角度讨论Apache Kafka与DistributedLog的不同点。我们会尽量做到客观,但由于我们不是Apache Kafka的专家,因此我们可能会对Apache Kafka存在误解。如果发现有错,也请大家直接指出。 相关厂商内容 Amazon ECS运行应用程序所使用的范式和工具大解密 亚马逊AWS首席云计算技术顾问费良宏做客InfoQ在线课堂 如何更好地设置、管理和扩展你的Amazon ECS 【双11】京东大促通天塔——核心中间件架构演进及实践 相关赞助商 更多AWS最新精彩内容和活动,请关注AWS专区! 首先,让我们简单地介绍一下Kafka和DistributedLog的概况。 Kafka是什么? Kafka是最初由Linkedin开源出来的一套分布式消息系统,现在由Apache软件基金会管理。这是一套基于分区的发布/订阅系统。Kafka中的关键概念就是Topic。一个Topic下面会有多个分区,每个分区都有备份,分布在不同的代理服务器上。生产者会把数据记录发布到一个Topic下面的分区中,具体方式是轮询或者基于主键做分区,而消费者会处理Topic中发布出来的数据记录。所有数据都是发布给相应分区的主代理进程,再复制到从代理进程,所有的读数据请求也都是依次由主代理处理的。从代理仅仅用于数据的冗余备份,并在主代理无法继续提供服务时顶上。图一的左边部分显示了Kafka中的数据流。 DistributedLog是什么? 与Kafka不同,DistributedLog并不是一个基于分区的发布/订阅系统,它是一个复制日志流仓库。DistributedLog中的关键概念是持续的复制日志流。一个日志流会被分段成多个日志片段。每个日志片段都在Apache BookKeeper中存储成Apache BooKeeper中的一个账目,其中的数据会在多个Bookie(Bookie就是Apache BookKeeper的存储节点)之间复制和均衡分布。一个日志流的所有数据记录都由日志流的属主排序,由许多个写入代理来管理日志流的属主关系。应用程序也可以使用核心库来直接追加日志记录。这对于复制状态机一类对于顺序和排他写有着非常高要求的场景非常有用。每个追加到日志流末尾的日志记录都会被赋予一个序列号。读者可以从任何指定的序列号开始读日志流的数据。读请求也会在那个流的所有存储副本上做负载均衡。图一的右半部分显示了DistributedLog中的数据流。 图一:Apache Kafka与Apache DistributedLog Kafka与DistributedLog有什么不同? 因为同类事物才有可比较的基础,所以我们只在本文中把Kafka分区和DistributedLog流相对比。下表列出了两套系统之间最显著的不同点。 (点击放大图像) 数据模型 Kafka分区是存储在代理服务器磁盘上的以若干个文件形式存在的日志。每条记录都是一个键-值对,但对于轮询式的数据发布可以省略数据的主键。主键用于决定该条记录会被存储到哪个分区上以及用于日志压缩功能。一个分区的所有数据只存储在若干个代理服务器上,并从主代理服务器复制到从代理服务器。 DistributedLog流是以一系列日志分片的形式存在的虚拟流。每个日志分片都以一条BookKeeper账目的形式存在,并被复制到多个Bookie上。在任意时刻都只有一个活跃的日志分片接受写入请求。在特定的时间段过后,或者旧日志分片达到配置大小(由配置的日志分片策略决定)之后,或者日志的属主出故障之后,旧的日志分片会被封存,一个新的日志分片会被开启。 Kafka分区和DistributedLog流在数据分片和分布的不同点决定了它们在数据持久化策略和集群操作(比如集群扩展)上的不同。 图二显示了DistributedLog和Kafka数据模型的不同点 (点击放大图像) 图二:Kafka分区与DistributedLog流 数据持久化 一个Kafka分区中的所有数据都保存在一个代理服务器上(并被复制到别的代理服务器上)。在配置的有效期过后数据会失效并被删除。另外,也可以配置策略让Kafka的分区保留每个主键的最新值。 与Kafka相似,DistributedLog也可以为每个流配置有效期,并在超时之后将相应的日志分片失效或删除。除此之外,DistributedLog还提供了显示的截断机制。应用程序可以显式地将一个日志流截断到流的某个指定位置。这对于构建可复制的状态机非常有用,因为可复制的状态机需要在删除日志记录之前先将状态持久化。Manhattan就是一个用到了这个功能的典型系统。 操作 数据分片和分布机制的不同也导致了维护集群操作上的不同,扩展集群操作就是一个例子。 扩展Kafka集群时,通常现有分区都要做重新分布。重新分布操作会将Kafka分区挪动到不同的副本上,以此达到均衡分布。这就要把整个流的数据从一个副本拷到另一个副本上。我们也说过很多次了,执行重新分布操作时必须非常小心,避免耗尽磁盘和网络资源。 而扩展DistributedLog集群的工作方式则截然不同。DistributedLog包含两层:存储层(Apache BooKeeper)和服务层(写入和读出代理)。在扩展存储层时,我们只需要添加更多的Bookie就好了。新的Bookie马上会被写入代理发现,并立刻用于写入新的日志分片。在扩展数据存储层时不会有任何的重新分布操作。只在增加服务层时会有重新分布操作,但这个重新分布也只是移动日志流的属主权,以使网络代宽可以在各个代理之间均衡分布。这个重新分布的过程只与属主权相关,没有数据迁移操作。这种存储层和服务层的隔离不仅仅是让系统具备了自动扩展的机制,更让各种不同类型的资源可以独立扩展。 写与生产者 如图一所示,Kafka生产者把数据一批批地写到Kafka分区的主代理服务器上。而ISR(同步复制)集合中的从代理服务器会从主代理上把记录复制走。只有在主代理从所有的ISR集合中的副本上都收到了成功的响应之后,一条记录才会被认为是成功写入的。可以配置让生产者只等待主代理的响应,还是等待ISR集合中的所有代理的响应。 DistributedLog中则有两种方式把数据写入DistributedLog流,一是用一个Thrift的瘦客户端通过写代理(众所周知的多写入)写入,二是通过DistributedLog的核心库来直接与存储节点交互(众所周知的单独写入)。第一种方式很适合于构建消息系统,第二种则适用于构建复制状态机。你可以查阅DistributedLog文档的相关章节来获取更多的信息和参考,以找到你需要的方式。 日志流的属主会并发地以BookKeeper条目的形式向Bookie中写入一批记录,并等待多个Bookie的Quorum结果。Quorum的大小取决于BookKeeper账目的ack_quorum_size参数,并且可以配置到DistributedLog流的级别。它提供了和Kafka生产者相似的在持久性上的灵活性。在接下来的“复制”一节我们会对比两者在复制算法上的更多不同之处。 Kafka和DistributedLog都支持端到端的批量操作和压缩机制。但两者之间的一点微妙区别是对DistributedLog的写入操作都是在收到响应之前都先通过fsync刷到硬盘上的,而我们并没发现Kafka也提供了类似的可靠性保证。 读与消费者 Kafka消费者从主代理服务器上读出数据记录。这个设计的前提就是主代理上在大多数情况下最新的数据都还在文件系统页缓存中。从充分利用文件系统页缓存和获得高性能的角度来说这是一个好办法。 DistributedLog则采用了完全不同的方法。因为各个存储节点之间没有明确的主从关系,DistributedLog可以从任意存储着相关数据的存储节点上读出数据。为了获得可预期的低延迟,DistributedLog引入了一个推理式读机制,即在超出了配置的读操作时限之后,它会在不同的副本上再次尝试获取数据。这就可能会对存储节点导致比Kafka更高的读压力。不过,如果将读超时时间配成可以让99%的存储节点的读操作都不会超时,那就可以极大程度地解决延迟问题,只带来1%的额外读压力。 对于读的考虑和机制上的不同主要源于复制机制和存储节点的I/O系统的不同,在下文会继续讨论。 复制 Kafka用的是ISR复制算法:将一个代理服务器选为主。所有写操作都被发送到主代理上,所有处于ISR集合中的从代理都从主代理上读取和复制数据。主代理会维护一个高水位线(HW,High Watermark),即每个分区最新提交的数据记录的偏移量。高水位线会不断同步到从代理上,并周期性地在所有代理上记录检查点,以备恢复之用。在所有ISR集合中的副本都把数据写入了文件系统(并不必须是磁盘)并向主代理发回了响应之后,主代理才会更新高水位线。 ISR机制让我们可以增加或减少副本的数量,在可用性和性能之间做出权衡。可是扩大或缩小副本的集合的副作用是增大了丢失数据的可能性。 DistributedLog使用的是Quorum投票复制算法,这在Zab、Raft以及Viewstamped Replication等一致性算法中都很常见。日志流的属主会并发地把数据记录写入所有存储节点,并在得到超过配置数量的存储节点投票确认之后,才认为数据已成功提交。存储节点也只在数据被显式地调用flush操作刷入磁盘之后才会响应写入请求。日志流的属主也会维护一个日志流的最新提交的数据记录的偏移量,就是大家知道的Apache BookKeeper中的LAC(LastAddConfirmed)。LAC也会保存在数据记录中(来节省额外的RPC调用开销),并不断复制到别的存储节点上。DistributedLog中复本集合的大小是在每个流的每个日志分片级别可配置的。改变复制参数只会影响新的日志分片,不会影响已有的。 存储 每个Kafka分区都以若干个文件的形式保存在代理的磁盘上。它利用文件系统的页缓存和I/O调度机制来得到高性能。Kafka也是因此利用Java的sendfile API来高效地从代理中写入读出数据的。不过,在某些情况下(比如消费者处理不及时、随机读写等),页缓存中的数据淘汰很频繁,它的性能也有很大的不确性性。 DistributedLog用的则是不同的I/O模型。图三表示了Bookie(BookKeeper的存储节点)的I/O机制。写入(蓝线)、末尾读(红线)和中间读(紫线)这三种常见的I/O操作都被隔离到了三种物理上不同的I/O子系统中。所有写入都被顺序地追加到磁盘上的日志文件,再批量提交到硬盘上。在写操作持久化到磁盘上之后,它们就会放到一个Memtable中,再向客户端发回响应。Memtable中的数据会被异步刷新到交叉存取的索引数据结构中:记录被追加到日志文件中,偏移量则在分类账目的索引文件中根据记录ID索引起来。最新的数据肯定在Memtable中,供末尾读操作使用。中间读会从记录日志文件中获取数据。由于物理隔离的存在,Bookie节点可以充分利用网络流入带宽和磁盘的顺序写入特性来满足写请求,以及利用网络流出代宽和多个磁盘共同提供的IOPS处理能力来满足读请求,彼此之间不会相互干扰。 图三:BookKeeper的I/O隔离 小结 Kafka和DistributedLog都是设计来处理日志流相关问题的。它们有相似性,但在存储和复制机制上有着不同的设计理念,因此有了不同的实现方式。希望这篇文章能从技术角度解释清楚它们的区别,回答一些问题。我们接下来也会再多写一些文章来讲讲DistributedLog的性能指标
Elasticsearch是一个开源的分布式实时搜索与分析引擎,支持云服务。它是基于Apache Lucene搜索引擎的类库创建的,提供了全文搜索能力、多语言支持、专门的查询语言、支持地理位置服务、基于上下文的搜索建议、自动完成以及搜索片段(snippet)的能力。Elasticsearch支持RESTful的API,可以使用JSON通过HTTP调用它的各种功能,包括搜索、分析与监控。此外,它还为Java、PHP、Perl、Python以及Ruby等各种语言提供了原生的客户端类库。下面是总结了一下使用 elasticsearch所遇到的各类问题以及相关的解决方案。 1、out of memory错误问题 因为默认情况下es对字段数据缓存(Field Data Cache)大小是无限制的,查询时会把字段值放到内存,特别是facet查询,对内存要求非常高,它会把结果都放在内存,然后进行排序等操作,一直使用内存,直到内存用完,当内存不够用时就有可能出现out of memory错误。 解决方法: (1)设置es的缓存类型为Soft Reference,它的主要特点是据有较强的引用功能。只有当内存不够的时候,才进行回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引 用对象还能保证在Java抛出OutOfMemory 异常之前,被设置为null。它可以用于实现一些常用图片的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory。在es 的配置文件加上index.cache.field.type: soft即可。 (2)设置es最大缓存数据条数和缓存失效时间,通过设置index.cache.field.max_size: 50000来把缓存field的最大值设置为50000,设置index.cache.field.expire: 10m把过期时间设置成10分钟。 2、抛出异常,错误如下: 1org.elasticsearch.transport.RemoteTransportException: Failed to deserialize exception response from stream 原因:es节点之间的JDK版本不一样 解决方式:统一JDK版本和环境 3、抛出异常,错误如下: org.elasticsearch.client.transport.NoNodeAvailableException: No node available (1)端口错误 client = new TransportClient().addTransportAddress(new InetSocketTransportAddress(ipAddress, 9300)); 端口9300写成9200的报错No node available 或者查看连接的是不是本地计算机,如果是远程的话查看一下IP地址是否正确。 (2)jar包报错误的话可能是引用包不匹配,开启的服务是什么版本最好对应相应的jar包。 (3)修改了集群名称,设置了集群名字导致出现问题,设置操作如下: Settings settings = ImmutableSettings.settingsBuilder().put("cluster.name", "yoodb").build(); client = new TransportClient(settings).addTransportAddress(new InetSocketTransportAddress(ipAddress, 9300)); (4)集群超过5s没有响应,解决方式如下: 1)设置client.transport.ping_timeout超时时间,增大一些 2)代码内嵌入,如下: while (true) { try { bulk.execute().actionGet(getRetryTimeout()); break; } catch (NoNodeAvailableException cont) { Thread.sleep(5000); continue; } } 4、由gc引起节点脱离集群 因为gc时会使jvm停止工作,如果某个节点gc时间过长,master ping3次(zen discovery默认ping失败重试3次)不通后就会把该节点剔除出集群,从而导致索引进行重新分配。解决方法如下: (1)优化gc,减少gc时间。 (2)调大zen discovery的重试次数(es参数:ping_retries)和超时时间(es参数:ping_timeout)。后来发现根本原因是有个节点的系统所在硬盘满了。导致系统性能下降。 5、无法创建本地线程问题 es恢复时报错,如下: RecoverFilesRecoveryException[[index][3] Failed to transfer [215] files with total size of [9.4gb]]; nested: OutOfMemoryError[unable to create new native thread]; ]] 刚开始以为是文件句柄数限制,但想到之前报的是too many open file这个错误,并且也把数据改大了。查资料得知一个进程的jvm进程的最大线程数为:虚拟内存/(堆栈大小*1024*1024),也就是说虚拟内存越大或堆栈越小,能创建的线程越多。重新设置后还是会报那这错,按理说可创建线程数完全够用了的,就想是不是系统的一些限制。后来在网上找到说是max user processes的问题,这个值默认是1024,这个参数单看名字是用户最大打开的进程数,但看官方说明,就是用户最多可创建线程数,因为一个进程最少有一个线程,所以间接影响到最大进程数。调大这个参数后就没有报这个错了。 解决方法: (1)增大jvm的heap内存或降低xss堆栈大小(默认的是512K)。 (2)打开/etc/security/limits.d/90-nproc.conf,把soft nproc 1024这行的1024改大就行了。 6、集群状态为黄色时并发插入数据报错,错误如下: [7]: index [index], type [index], id [1569133], message [UnavailableShardsException[[index][1] [4] shardIt, [2] active : Timeout waiting for [1m], request: org.elasticsearch.action.bulk.BulkShardRequest@5989fa07]] 这是错误信息,当时集群状态为黄色,即副本没有分配。当时副本设置为2,只有一个节点,当你设置的副本大于可分配的机器时,此时如果你插入数据就有可能报上面的错,因为es的写一致性默认是使用quorum,即quorum值必须大于(副本数/2+1),我这里2/2+1=2也就是说要要至少插入到两份索引中,由于只有一个节点,quorum等于1,所以只插入到主索引,副本找不到从而报上面那个错。 解决方法:(1)去掉没分配的副本。(2)把写一致性改成one,即只写入一份索引就行。 7、错误使用api导致集群卡死 其实这个是很低级的错误。功能就是更新一些数据,可能会对一些数据进行删除,但删除时同事使用了deleteByQuery这个接口,通过构造 BoolQuery把要删除数据的id传进去,查出这些数据删除。但问题是BoolQuery最多只支持1024个条件,100个条件都已经很多了,所以这样的查询一下子就把es集群卡死了。 解决方法:用bulkRequest进行批量删除操作。 8、设置jvm锁住内存时启动警告 当设置bootstrap.mlockall: true时,启动es报警告Unknown mlockall error 0,因为linux系统默认能让进程锁住的内存为45k。 解决方法:设置为无限制,linux命令:ulimit -l unlimited 博文出处:http://www.yoodb.com/article/display/246
区别于Kylin它底层用的mpp,Palo是百度基础架构部数据团队所开发的一套面向大规模数据分析的并行数据库系统。主要目标是支撑稳定的、在线的、交互式的数据报表(Reporting)和数据多维分析(OLAP)服务。Palo 的一个很大的特色是:将会满足报表和OLAP分析这两类不同的需求。 Palo在整个分析体系中所承担的是数据库的角色,只是这个数据库是并行的、面向分析的数据库系统。然后在这个数据库系统上,通过支撑现有的(BIEE、Pentaho等)或者百度自己研发的BI应用套件来提供全套的报表和分析应用。 基本使用指南 1 数据表的创建和导入 1.1 下载palo-client 请登录Palo主页,从Download区下载最新的palo-client包,并解压, 举例如下(使用时注意更改为相应的palo-client版本): wget http://palo.baidu.com:8080/download/palo-client-1.0.3.tar.gz tar zxf palo-client-1.0.3.tar.gz 1.2 权限申请 使用palo系统前需要先申请权限,请联系Palo管理员创建用户及数据库。 管理员会给用户赋予某个数据库的读、写、创建表、删除表或者变更表schema的权限,然后使用者就可以通过palo-client进行下面的操作。 Note 管理员可以为使用者创建读、写两个用户。 1.3 运行palo-client 运行palo-client程序,参数为: -H DM(即逻辑实现架构中Frontend Master)程序的地址 -P DM(即逻辑实现架构中Frontend Master)程序的端口, 默认为8133 -u 用户名 -p 口令 示例: ./palo-client -H PALO_MASTER_HOST -P PALO_MASTER_PORT -u YOUR_USERNAME -p YOUR_PASSWORD 使用use命令切换数据库, 示例: use YOUR_DB Note 使用 help 命令查看命令列表 使用 help COMMAND 查看具体命令的详细中文帮助 使用 Tab 键自动补全命令和参数 使用向上或向下方向键显示历史命令 1.4 建表 使用 create_table_family 建立一个逻辑表, 关于 create_table_family 的详细参数可以使用 help create_table_family 查询。 下面这个例子, 建立一个名字为 ps_stats 的逻辑表, 这个逻辑表的基础表的名字为 basic 这个逻辑表有4个列: siteid, 类型是INT, 默认值为10 time, 类型是SHORT city, 类型是STRING, 长度为32, 默认值UNKNOWN pv, 类型是LONG, 默认值是100; 这是一个指标列, Palo内部会对指标列做聚合操作, 这个列的聚合方法是累加(ADD) 命令中后续几个参数 -p -h 与数据分布方式有关, 参考 数据分布方式。示例: create_table_family ps_stats basic \ siteid,INT,DEF(10) \ time,SHORT \ city,STRING,LEN(32),DEF(UNKNOWN) \ pv,LONG,AGG(ADD),DEF(100) \ -p hash \ -h 31 可以对逻辑表增加上卷表以提高性能, 例如我们需要经常查询不同 siteid 的 pv , 但不关心其他维度, 这个时候可以在 ps_stats 逻辑表内建一个只有 siteid 和 pv 的名字为 siteid_roll_up 的表。示例: create_table ps_stats siteid_roll_up siteid pv -p hash -h 11 1.5 导入数据 使用 batch_load 命令导入数据, 使用 help batch_load 命令查询该命令的详细参数. 导入Palo的数据必须在HDFS上, 并且其权限对others可读, 也可以将HDFS用户palo加入你的用户组, 并使得数据对用户组可读, 以提高数据的安全性. 每一批导入数据都需要取一个 Data Label , Data Label 最好是一个和数据有关的字符串. Palo基于该字符串对数据去重保证: 对于同一个Database内的一个Data Label 最多只能成功导入一次 示例1: 以”fc-stats-20130901”为Data Label, 使用HDFS上的文件更新ps_stats文件表: batch_load -l fc-stats-20130901 -t ps_stats -s hdfs://*:54310/*/*/input/stats/20130901/* 数据源文件内字段的顺序和个数不需要与Table Family的字段顺序一致, 在 batch_load 命令中可以指定数据源文件的字段.如果数据源中的字段在Table Family里没有出现, Palo会自动忽略. 如果Table Family中的字段在数据源中没有出现, 且字段具有默认值, Palo会导入默认值. 示例2: 以”fc-stats-new-20130902”为Data Label, 使用HDFS上的文件更新ps_stats文件表, 并指定数据源的字段, 数据源的顺序与Table Family不同,且多出了pid字段, 少了city字段: batch_load -l fc-stats-new-20130902 -t ps_stats -s hdfs://*:54310/app/*/input/stats/*/*;column_names=pid,time,siteid,pv 示例3: 对ps_stats表Label为“fc-stats-20130901”的已导入数据进行**数据恢复**: batch_load -l fc-stats-20130901-recover \ -t ps_stats -s hdfs://*.*.com:54310/*/*/input/stats/20130901/*;is_negative=true \ -t ps_stats -s hdfs://*.*.*.com:54310/good_file Note 同一个 batch_load 命令可以同时导入多个逻辑表(Table Family)的数据, 这些数据会原子生效. Palo batch_load 命令支持通配符*和? 合理设置导入任务的超时时间(使用-o来指定,默认为3600秒) 合理设置错误数据过滤比例(使用-f来指定,默认为0,代表不允许过滤,如果有过滤则任务失败;0.1代表允许10%以内的错误数据过滤) 对于失败的任务, 不用更换 Data Label ,可以再次使用 batch_load 命令导入 同一个 batch_load 命令中不能指定同一个路径导入多个不同的table_family 1.6 查询导入任务的状态 使用 show_batch_load 命令查询导入任务的状态, show_batch_load 有丰富的参数, 使用 help show_batch_load 了解该命令的详细参数. 导入任务的主要信息为: pending 导入任务尚未被调度执行 etl 正在执行ETL计算, Palo内部状态 load 正在进行加载, Palo内部状态 finished 导入任务成功完成 cancelled 导入任务失败 state 导入状态 fail_message 导入任务失败原因 creation_time 任务创建时间 done_time 任务结束时间 etl_job_id ETL任务在Hadoop上的任务Id dpp.abnorm.ALL 输入数据中被过滤掉的非法数据条数 dpp.norm.ALL 输入数据中合法的数据条数 etl_job_info ETL任务的信息, 用户可以关注其中两个字段 示例1:显示当前数据库内最后20个导入任务的状态: show_batch_load 结果显示为: 示例2:显示数据库内以”20120101”为 DataLabel 的所有任务的状态的详细信息: show_batch_load -l 20120101 -v -L 0 Note 如果任务失败,可以参考help show_batch_load中的【查错】来追查任务出错的原因。 1.7 取消导入任务 使用 cancel_batch_load 命令取消一个导入任务的执行, 被取消的任务数据不会导入Palo系统;已经处于 cancelled 或 finished 状态的任务无法被取消。 示例1:取消当前数据库中 Data Label 为”20131028-fc”的任务: cancel_batch_load 20131028-fc 2 数据的查询 一旦数据导入成功,我们可以使用任何Mysql的客户端工具或者库来连接Palo进行使用。在基本使用中,我们将使用Mysql client命令行来介绍数据表的查询。 2.1 下载mysql-client 如果你使用的机器OS环境是Redhat4u3环境,即百度当前服务器上部署最多的环境,就是那个gcc为3.4.5, glibc为2.3的环境,你可以直接下载我们已经为你编译好的Mysql Client命令行工具mysql来使用: wget http://palo.baidu.com:8080/download/mysql-client 如果你的机器OS环境不是上面所说的,建议下载mysql的源代码,然后进行源码编译: wget http://palo.baidu.com:8080/download/mysql-5.1.49.tar.gz tar zxf mysql-5.1.49.tar.gz cd mysql-5.1.49 ./configure --without-server --disable-shared make # make成功后,mysql程序就在client/目录下生成了,拷走即可使用 Note 由于我们只需要构建client,所以传入–without-server选项。 为了构建出来的mysql程序可以方便的被拷来拷去使用,构建时传入–disabled-shared,以便使得mysql使用libmysqlclient静态库,而不是动态库。 构建后不需要执行make install, 直接从源码根目录下的client目录,直接把mysql拷走即可使用。 2.2 启动mysql命令行程序 运行mysql程序, 常用参数为: -h 为了构建出来的mysql程序可以方便的被拷来拷去使用,构建时传入–disabled-shared,以便使得mysql使用libmysqlclient静态库,而不是动态库。 这里应该填写查询节点(即逻辑实现中Frontend Slave)的主机名或者ip -P 这里应该填写查询节点(即逻辑实现中Frontend Slave)的端口 -u 用户名,使用你前面申请的账户。建议:对一个database的数据管理和数据查询建议使用不同的账户,即做到账户在“读写权限”上分离 -p 口令,不输入的话,会提示你输入 示例: ./mysql -h PALO_FRONTEND_HOST -P PALO_FRONTEND_PORT -u YOUR_USERNAME -p YOUR_PASSWORD 2.3 查看数据库、表信息 查看你所拥有权限的数据库列表: mysql> show databases; +--------------------+ | Database | +--------------------+ | star | | test | +--------------------+ 2 rows in set (0.00 sec) 查看某个数据库中表的信息: mysql> use star; Database changed mysql> show tables; +----------------+ | Tables_in_star | +----------------+ | customer | | dates | | lineorder | | part | | supplier | +----------------+ 5 rows in set (0.01 sec) mysql> desc customer; +--------------+-------------+------+------+---------+-------+ | Field | Type | Null | Key | Default | Extra | +--------------+-------------+------+------+---------+-------+ | c_custkey | int(11) | NO | | NULL | | | c_name | varchar(20) | NO | | NULL | | | c_address | varchar(20) | NO | | NULL | | | c_city | varchar(20) | NO | | NULL | | | c_nation | varchar(20) | NO | | NULL | | | c_region | varchar(20) | NO | | NULL | | | c_phone | varchar(20) | NO | | NULL | | | c_mktsegment | varchar(20) | NO | | NULL | | +--------------+-------------+------+------+---------+-------+ 8 rows in set (0.01 sec)
可以看看:https://github.com/jinhang/fcn 【论文信息】 《Fully Convolutional Networks for Semantic Segmentation》 CVPR 2015 best paper Reference link: http://blog.csdn.NET/tangwei2014 http://blog.csdn.net/u010025211/article/details/51209504 概览&主要贡献 提出了一种end-to-end的做semantic segmentation的方法,简称FCN。 如下图所示,直接拿segmentation 的 ground truth作为监督信息,训练一个端到端的网络,让网络做pixelwise的prediction,直接预测label map。(笔者自己类比思想:faster rcnn中的rbn->(fc->region proposal) label map-> fast-rcnn for fine tuning) 【方法简介】 主要思路是把CNN改为FCN,输入一幅图像后直接在输出端得到dense prediction,也就是每个像素所属的class,从而得到一个end-to-end的方法来实现image semantic segmentation。 我们已经有一个CNN模型,首先要把CNN的全连接层看成是卷积层,卷积模板大小就是输入的特征map的大小,也就是说把全连接网络看成是对整张输入map做卷积,全连接层分别有4096个6*6的卷积核,4096个1*1的卷积核,1000个1*1的卷积核,如下图: 接下来就要对这1000个1*1的输出,做upsampling,得到1000个原图大小(如32*32)的输出,这些输出合并后,得到上图所示的heatmap。 【细节记录】 dense prediction 这里通过upsampling得到dense prediction,作者研究过3种方案: 1,shift-and-stitch:设原图与FCN所得输出图之间的降采样因子是f,那么对于原图的每个f*f的区域(不重叠),“shift the input x pixels to the right and y pixels down for every (x,y) ,0 < x,y < f." 把这个f*f区域对应的output作为此时区域中心点像素对应的output,这样就对每个f*f的区域得到了f^2个output,也就是每个像素都能对应一个output,所以成为了dense prediction。 2,filter rarefaction:就是放大CNN网络中的subsampling层的filter的尺寸,得到新的filter: 其中s是subsampling的滑动步长,这个新filter的滑动步长要设为1,这样的话,subsampling就没有缩小图像尺寸,最后可以得到dense prediction。 以上两种方法作者都没有采用,主要是因为这两种方法都是trad-off的,原因是: 对于第二种方法, 下采样的功能被减弱,使得更细节的信息能被filter看到,但是receptive fileds会相对变小,可能会损失全局信息,且会对卷积层引入更多运算。 对于第一种方法,虽然receptive fileds没有变小,但是由于原图被划分成f*f的区域输入网络,使得filters无法感受更精细的信息。 重点方法:反卷积层->pixel wise->bp parameters->实现把conv的前传和反传过程对调一下即可 3,这里upsampling的操作可以看成是反卷积(deconvolutional),卷积运算的参数和CNN的参数一样是在训练FCN模型的过程中通过bp算法学习得到。fusion prediction 以上是对CNN的结果做处理,得到了dense prediction,而作者在试验中发现,得到的分割结果比较粗糙,所以考虑加入更多前层的细节信息,也就是把倒数第几层的输出和最后的输出做一个fusion,实际上也就是加和: 这样就得到第二行和第三行的结果,实验表明,这样的分割结果更细致更准确。在逐层fusion的过程中,做到第三行再往下,结果又会变差,所以作者做到这里就停了。可以看到如上三行的对应的结果: 问题&解决办法 1.如何做pixelwise的prediction? 传统的网络是subsampling的,对应的输出尺寸会降低,要想做pixelwiseprediction,必须保证输出尺寸。 解决办法: (1)对传统网络如AlexNet,VGG等的最后全连接层变成卷积层。 例如VGG16中第一个全连接层是25088x4096的,将之解释为512x7x7x4096的卷积核,则如果在一个更大的输入图像上进行卷积操作(上图的下半部分),原来输出4096维feature的节点处(上图的上半部分),就会输出一个coarsefeature map。 这样做的好处是,能够很好的利用已经训练好的supervisedpre-training的网络,不用像已有的方法那样,从头到尾训练,只需要fine-tuning即可,训练efficient。 (2)加In-network upsampling layer。 对中间得到的featuremap做bilinear上采样,就是反卷积层。实现把conv的前传和反传过程对调一下即可。 2.如何refine,得到更好的结果? upsampling中步长是32,输入为3x500x500的时候,输出是544x544,边缘很不好,并且limit thescale of detail of the upsampling output。 解决办法: 采用skiplayer的方法,在浅层处减小upsampling的步长,得到的finelayer 和 高层得到的coarselayer做融合,然后再upsampling得到输出。 这种做法兼顾local和global信息,即文中说的combiningwhat and where,取得了不错的效果提升。FCN-32s为59.4,FCN-16s提升到了62.4,FCN-8s提升到62.7。可以看出效果还是很明显的。 3.训练细节 用AlexNet,VGG16或者GoogleNet训练好的模型做初始化,在这个基础上做fine-tuning,全部都fine-tuning。 采用wholeimage做训练,不进行patchwise sampling。实验证明直接用全图已经很effectiveand efficient。 对classscore的卷积层做全零初始化。随机初始化在性能和收敛上没有优势。 【实验设计】 1,对比3种性能较好的几种CNN:AlexNet, VGG16, GoogLeNet进行实验,选择VGG16 2,对比FCN-32s-fixed, FCN-32s, FCN-16s, FCN-8s,证明最好的dense prediction组合是8s 3,FCN-8s和state-of-the-art对比是最优的,R-CNN, SDS. FCN-16s 4,FCN-16s和现有的一些工作对比,是最优的 5,FCN-32s和FCN-16s在RGB-D和HHA的图像数据集上,优于state-of-the-art 【总结】 优点 1,训练一个end-to-end的FCN模型,利用卷积神经网络的很强的学习能力,得到较准确的结果,以前的基于CNN的方法都是要对输入或者输出做一些处理,才能得到最终结果。 2,直接使用现有的CNN网络,如AlexNet, VGG16, GoogLeNet,只需在末尾加上upsampling,参数的学习还是利用CNN本身的反向传播原理,"whole image training is effective and efficient." 3,不限制输入图片的尺寸,不要求图片集中所有图片都是同样尺寸,只需在最后upsampling时按原图被subsampling的比例缩放回来,最后都会输出一张与原图大小一致的dense prediction map。 缺陷 根据论文的conclusion部分所示的实验输出sample如下图: 可以直观地看出,本文方法和Groud truth相比,容易丢失较小的目标,比如第一幅图片中的汽车,和第二幅图片中的观众人群,如果要改进的话,这一点上应该是有一些提升空间的。 结果 当然是state-of-the-art的了。 感受一下:
1. 简介 物体检测的深度网络按感兴趣区域 (RoI) 池化层分为两大主流:共享计算的全卷积子网络 (每个子网络与 RoI 无关) 和 不共享计算的作用于各自 RoI 的子网络。工程分类结构 (如 Alexnet 和 VGG Nets) 造成这样的分流。而工程上的图像分类结构被设计为两个子网络——1个后缀1个空间池化层的卷积子网络和多个全连接层。因此,图像分类网络中最后的空间池化层自然变成了物体检测网络中的 RoI 池化层。 近年来,诸如残差网络和 GoogLeNets 等先进的图像分类网络为全卷积网络。类似地,自然会想到用在物体检测中用全卷积网络 (隐藏层不包含作用于 RoI 的子网络)。然而,物体检测工作中的经验表明,这样天真的解决方案的检测效果远差于该网络的分类效果。 为弥补尴尬,更快 R-CNN 检测器不自然地在两卷积层间插入RoI 池化层,这样更深的作用于各 RoI 的子网络虽精度更高,但各个 RoI 计算不共享所以速度慢。 尴尬在于:物体分类要求平移不变性越大越好 (图像中物体的移动不用区分),而物体检测要求有平移变化。所以,ImageNet 分类领先的结果证明尽可能有平移不变性的全卷积结构更受亲睐。另一方面,物体检测任务需要一些平移变化的定位表示。比如,物体的平移应该使网络产生响应,这些响应对描述候选框覆盖真实物体的好坏是有意义的。我们假设图像分类网络的卷积层越深,则该网络对平移越不敏感。 我曾看到的尴尬包括: a) Kaggle 中的白鲸身份识别。刚开始很多人尝试从图像到坐标的直接回归,到后面有几位心善的大哥分享了自己手动标定后白鲸的图像坐标,后来显著的进展大多是因为把白鲸的位置检测和身份识别问题简化为白鲸的身份识别问题。 b) Caffe 用于物体检测时的均值收敛问题。 为消除尴尬,在网络的卷积层间插入 RoI 池化层。这种具体到区域的操作在不同区域间跑时不再有平移不变性。然而,该设计因引入相当数目的按区域操作层 (region-wise layers) 而牺牲了训练和测试效率。 本文,我们为物体检测推出了基于区域的全卷积网络 (R-FCN),采用全卷积网络结构作为 FCN,为给 FCN 引入平移变化,用专门的卷积层构建位置敏感分数地图 (position-sensitive score maps)。每个空间敏感地图编码感兴趣区域的相对空间位置信息。 在FCN上面增加1个位置敏感 RoI 池化层来监管这些分数地图。 2. 方法 (1) 简介 效仿 R-CNN,采用流行的物体检测策略,包括区域建议和区域分类两步。不依赖区域建议的方法确实存在 (SSD 和 Yolo 弟兄),基于区域的系统在不同 benchmarks 上依然精度领先。用更快 R-CNN 中的区域建议网络 (RPN) 提取候选区域,该 RPN 为全卷积网络。效仿更快 R-CNN,共享 RPN 和 R-FCN 的特征。 RPN 给出感兴趣区域,R-FCN 对该感兴趣区域分类。R-FCN 在与 RPN 共享的卷积层后多加1个卷积层。所以,R-FCN 与 RPN 一样,输入为整幅图像。但 R-FCN 最后1个卷积层的输出从整幅图像的卷积响应图像中分割出感兴趣区域的卷积响应图像。 R-FCN 最后1个卷积层在整幅图像上为每类生成k2个位置敏感分数图,有C类物体外加1个背景,因此有k2(C+1)个通道的输出层。k2个分数图对应描述位置的空间网格。比如,k×k=3×3,则9个分数图编码单个物体类的 {top−left,top−center,top−right,...,bottom−right}。 R-FCN 最后用位置敏感 RoI 池化层,给每个 RoI 1个分数。选择性池化图解:看上图的橙色响应图像 (top−left),抠出橙色方块 RoI,池化橙色方块 RoI 得到橙色小方块 (分数);其它颜色的响应图像同理。对所有颜色的小方块投票 (或池化) 得到1类的响应结果。 选择性池化是跨通道的,投票部分的池化为所有通道的池化。而一般池化都在通道内。 R-FCN 最后1个卷积层的输出为什么会具有相对空间位置这样的物理意义 (top-left,top-center,…,bottom-right)? 原文为“With end-to-end training, this RoI layer shepherds the last convolutional layer to learn specialized position-sensitive score maps.”。所以,假设端到端训练后每层真有相对位置的意义,那么投票前的输入一定位置敏感。投票后面的内容用作分类。 端到端训练先自行脑补: 假设已知原图像与真实物体的边界框中心坐标和宽高,把1个物体的边界框中心坐标分成k2个网格的中心坐标,宽高缩放为物体宽高的1k倍,得到每个网格的掩码。用原图像和每类物体的网格在整幅图像中的掩码端到端训练全卷积网络。挺像图像分割~ (2) 基础结构 ResNet-101 网络有100个卷积层,1个全局平均池化层和1个1000类的全连接层。仅用ImageNet预训练的该网络的卷积层计算特征图。 (3) 位置敏感分数图 对 R-FCN 的卷积响应图像按 RPN 的结果分割出来感兴趣区域,对单通道的感兴趣区域分成k×k个网格,每个网格平均池化,然后所有通道再平均池化。 其实不是这样的~ 因为 RoI 覆盖的所有面积的橙色方片都是左上位置的响应。 “To explicitly encode position information into each RoI, we divide each RoI rectangle into k×k bins by a regular grid.” 这句话应对应下图 (对应后面效果图的黄色虚线部分): 对1个大小为w×h的 RoI,1个桶 (bin) 的大小为wk×hk,最后1个卷积层为每类产生k2个分数图。对第(i,j)个桶 (0≤i,j≤k−1),定义1个位置敏感 RoI 池化操作: rc(i,j|Θ)=1n∑(x,y)∈bin(i,j)zi,j,c(x+x0,y+y0|Θ) 其中,rc(i,j|Θ)为第c类第(i,j)个箱子的池化响应,zi,j,c为k2(C+1)个分数图中的输出,(x0,y0)为 RoI 的左上角坐标,n为桶里的像素总数,且Θ为网络的参数。 桶对应后面效果图的黄色实线部分,1个桶只抠了每类的每个相对空间位置通道中 RoI 的对应相对空间位置的分数图,其它的部分丢弃。 (4) 分类 对该 RoI 每类的所有相对空间位置的分数平均池化 (或投票)。 rc(Θ)=∑i,jrc(i,j|Θ) Softmax 回归分类。 (5) 定位 k2(C+1)维的卷积层后,增加1个4k2维的卷积层来回归边界框。每个 RoI 产生的4k2维向量经平均投票后,用快速 R-CNN 的参数化得到1个4维向量(tx,ty,tw,th)。 (6) 训练 每个 RoI 的损失函数为交叉熵损失与边界框回归损失的和。 L(s,tx,y,w,h)=Lcls(sc∗)+λ[c∗>0]Lreg(t,t∗)=−log⎛⎝erc∗(Θ)∑Cc′=0erc′(Θ)⎞⎠+λ[c∗>0]Lreg(t,t∗) 其中,c∗=0说明 RoI 的真实标签为背景。Lreg与快速 R-CNN 中的边界框损失回归相同。RPN 产生的区域建议当 RoI 与 真实边框的 IoU 超过0.5时,标定为正样本。 在线难例挖掘 (OHEM)。假设每个图像前向产生N个区域建议,计算所有建议的损失。按损失排序所有 RoIs,选择损失最高的B个 RoIs 3。 (7) 可视化 RoI 分类的可视化。RPN 刚好产生包含 person 类的 RoI。经过 R-FCN 的最后1个卷积层后产生9个相对空间位置的分数图,对 person 类的每个相对空间位置通道内的 RoI 桶平均池化得到3×3的池化分数,投票后送入分类器判断属于 person 类。当分类正确时,该类通道的位置敏感分数图 (中间) 的大多数橙色实线网格内的响应在整个 RoI 位置范围内最强。 3. 相关工作 R-CNN 证实用深度网络产生区域建议是有效的。R-CNN 在剪切变形的区域上评价卷积网络,区域间不共享计算。SPP 网络,快速 R-CNN 和更快速 R-CNN 为”半卷积” (卷积子网络在整幅图像上共享计算,另1个子网络评价各个区域)。 一些物体检测器被认为是”全卷积“模型。OverFeat 在共享卷积特征图上滑窗操作来检测物体。类似地,快速 R-CNN 等也用滑动窗口,它们的1个单尺度的滑动窗口可看作1个卷积层。更快 R-CNN 的 RPN 部分为1个预测关于多尺寸参考盒 (锚) 的边界框的全卷积检测器。更快 R-CNN 的 RPN 未知区域建议的类,但 SSD 该部分已知特定的类。 另一类物体检测器采用全连接层,在整个图像上产生整体物体的检测结果。 4. 实验 (1) PASCAL VOC 训练VOC 07 trainval 和 VOC 12 trainval,测试VOC 07 test。 a. 与其它全卷积策略比较 朴素更快 R-CNN ResNet-101 的共享特征图,最后1个卷积层后用 RoI 池化。每个 RoI 上用21类全连接层。 ResNet-101 (conv4 与 conv5 间插入 RoI 池化层),朴素更快 R-CNN (conv5 后插入 RoI 池化层)。mAP 升 7.5%。。经验证实更快 R-CNN 系统的卷积层间插入 RoI 池化层能提高相关空间信息的重要性。 特定类 RPN 训练 RPN 与 更快 R-CNN 部分相同,2类卷积分类层 (物体或背景) 改为 21类卷积分类层 (20类物体+1背景)。 特定类 RPN 类似于快速 R-CNN 的特殊形式 (用稠密的滑窗替换区域建议)。mAP 跌 8.8%。效果不如2类 RPN。 无位置敏感的 R-FCN k=1时,位置不敏感。相当于每个 RoI 全局池化。 位置敏感的 R-FCN 相对于 ResNet-101,mAP 升0.02 %~ b. 与用 ResNet-101 的更快 R-CNN 比较 所要比较的检测器为各大 Benchmark 上的最强竞争者。名字太长,后面简称暂时最强检测器。 原作者的意思可能是这样:结合 MS COCO 训练后,R-FCN 仅需多尺度训练 mAP 就能到 82%,而暂时最强检测器,除了多尺度训练,还要迭代盒回归和上下文才比 R-FCN 多 2.0% 和 1.8%;而且,即使不带 MS COCO 训练,没有上述附加的”+++”工作,R-FCN 也比暂时最强检测器的 mAP 还要至少高 3%。而且,R-FCN 快得多。 带 MS COCO 训练数据后,mAP 大涨~ P.S. 该数据集由微软发起~ c. 深度的影响 深度为50~101时 mAP 会增加,到152层时饱和。 d. 区域建议的影响 RPN 即使用选择搜索和边缘盒 (Edge Boxes) 也有 77% 以上的 mAP。 (2) MS COCO IoU 为0.5时,R-FCN和暂时最强检测器的 mAP 仅有刚过50%。说明 MS COCO 与 PASCAL VOC 相比有更大的挑战性~ 5. 小结 R-FCN 在数据集 VOC 07 和 12 上的 mAP 分别为 83.6% 和 82%,测试时每张图像耗时 170ms。微软的视觉计算组其实每年在领先的成果上改进了一点点,但原理简单,分析角度又新,实验规模也不小。该团队不仅明星云集,且力往一块使,容易出大片~
有参考:http://blog.csdn.net/u010167269/article/details/52563573 SSD: Single Shot MultiBox Detector By Wei Liu, Dragomir Anguelov, Dumitru Erhan, Christian Szegedy, Scott Reed, Cheng-Yang Fu, Alexander C. Berg. Introduction SSD is an unified framework for object detection with a single network. You can use the code to train/evaluate a network for object detection task. For more details, please refer to our arXiv paper. System VOC2007 test mAP FPS (Titan X) Number of Boxes Faster R-CNN (VGG16) 73.2 7 300 Faster R-CNN (ZF) 62.1 17 300 YOLO 63.4 45 98 Fast YOLO 52.7 155 98 SSD300 (VGG16) 72.1 58 7308 SSD300 (VGG16, cuDNN v5) 72.1 72 7308 SSD500 (VGG16) 75.1 23 20097 Citing SSD Please cite SSD in your publications if it helps your research: @article{liu15ssd, Title = {{SSD}: Single Shot MultiBox Detector}, Author = {Liu, Wei and Anguelov, Dragomir and Erhan, Dumitru and Szegedy, Christian and Reed, Scott and Fu, Cheng-Yang and Berg, Alexander C.}, Journal = {arXiv preprint arXiv:1512.02325}, Year = {2015} } Contents Installation Preparation Train/Eval Models Installation Get the code. We will call the directory that you cloned Caffe into $CAFFE_ROOT git clone https://github.com/weiliu89/caffe.git cd caffe git checkout ssd Build the code. Please follow Caffe instruction to install all necessary packages and build it. # Modify Makefile.config according to your Caffe installation. cp Makefile.config.example Makefile.config make -j8 # Make sure to include $CAFFE_ROOT/python to your PYTHONPATH. make py make test -j8 make runtest -j8 # If you have multiple GPUs installed in your machine, make runtest might fail. If so, try following: export CUDA_VISIBLE_DEVICES=0; make runtest -j8 # If you have error: "Check failed: error == cudaSuccess (10 vs. 0) invalid device ordinal", # first make sure you have the specified GPUs, or try following if you have multiple GPUs: unset CUDA_VISIBLE_DEVICES Preparation Download fully convolutional reduced (atrous) VGGNet. By default, we assume the model is stored in$CAFFE_ROOT/models/VGGNet/ Download VOC2007 and VOC2012 dataset. By default, we assume the data is stored in $HOME/data/ # Download the data. cd $HOME/data wget http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtrainval_06-Nov-2007.tar wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtest_06-Nov-2007.tar # Extract the data. tar -xvf VOCtrainval_11-May-2012.tar tar -xvf VOCtrainval_06-Nov-2007.tar tar -xvf VOCtest_06-Nov-2007.tar Create the LMDB file. cd $CAFFE_ROOT # Create the trainval.txt, test.txt, and test_name_size.txt in data/VOC0712/ ./data/VOC0712/create_list.sh # You can modify the parameters in create_data.sh if needed. # It will create lmdb files for trainval and test with encoded original image: # - $HOME/data/VOCdevkit/VOC0712/lmdb/VOC0712_trainval_lmdb # - $HOME/data/VOCdevkit/VOC0712/lmdb/VOC0712_test_lmdb # and make soft links at examples/VOC0712/ ./data/VOC0712/create_data.sh Train/Eval Train your model and evaluate the model on the fly. # It will create model definition files and save snapshot models in: # - $CAFFE_ROOT/models/VGGNet/VOC0712/SSD_300x300/ # and job file, log file, and the python script in: # - $CAFFE_ROOT/jobs/VGGNet/VOC0712/SSD_300x300/ # and save temporary evaluation results in: # - $HOME/data/VOCdevkit/results/VOC2007/SSD_300x300/ # It should reach 72.* mAP at 60k iterations. python examples/ssd/ssd_pascal.py If you don't have time to train your model, you can download a pre-trained model at here. Evaluate the most recent snapshot. # If you would like to test a model you trained, you can do: python examples/ssd/score_ssd_pascal.py Test your model using a webcam. Note: press esc to stop. # If you would like to attach a webcam to a model you trained, you can do: python examples/ssd/ssd_pascal_webcam.py Here is a demo video of running a SSD500 model trained on MSCOCO dataset. Check out examples/ssd_detect.ipynb or examples/ssd/ssd_detect.cpp on how to detect objects using a SSD model. To train on other dataset, please refer to data/OTHERDATASET for more details. We currently add support for MSCOCO and ILSVRC2016. Models Models trained on VOC0712: SSD300, SSD500 Models trained on MSCOCO trainval35k: SSD300, SSD500 Models trained on ILSVRC2015 trainval1: SSD300, SSD500 (46.4 mAP on val2) Preface 这是今年 ECCV 2016 的一篇文章,是 UNC Chapel Hill(北卡罗来纳大学教堂山分校) 的 Wei Liu 大神的新作,论文代码:https://github.com/weiliu89/caffe/tree/ssdObeject detection summary link: Object Detection 目前 voc 2012 的榜单: Abstract 这篇文章在既保证速度,又要保证精度的情况下,提出了 SSD 物体检测模型,与现在流行的检测模型一样,将检测过程整个成一个 single deep neural network。便于训练与优化,同时提高检测速度。SSD 将输出一系列 离散化(discretization) 的 bounding boxes,这些 bounding boxes 是在 不同层次(layers) 上的 feature maps 上生成的,并且有着不同的 aspect ratio。 在 prediction 阶段: 要计算出每一个 default box 中的物体,其属于每个类别的可能性,即 score,得分。如对于 PASCAL VOC 数据集,总共有 20 类,那么得出每一个 bounding box 中物体属于这 20 个类别的每一种的可能性。 同时,要对这些 bounding boxes 的 shape 进行微调,以使得其符合物体的 外接矩形。 还有就是,为了处理相同物体的不同尺寸的情况,SSD 结合了不同分辨率的 feature maps 的 predictions。 相对于那些需要 object proposals 的检测模型,本文的 SSD 方法完全取消了 proposals generation、pixel resampling 或者 feature resampling 这些阶段。这样使得 SSD 更容易去优化训练,也更容易地将检测模型融合进系统之中。 在 PASCAL VOC、MS COCO、ILSVRC 数据集上的实验显示,SSD 在保证精度的同时,其速度要比用 region proposals 的方法要快很多。 SSD 相比较于其他单结构模型(YOLO),SSD 取得更高的精度,即是是在输入图像较小的情况下。如输入 300×300 大小的 PASCAL VOC 2007 test 图像,在 Titan X 上,SSD 以 58 帧的速率,同时取得了 72.1% 的 mAP。 如果输入的图像是 500×500,SSD 则取得了 75.1% 的 mAP,比目前最 state-of-art 的 Faster R-CNN 要好很多。 Introduction 现金流行的 state-of-art 的检测系统大致都是如下步骤,先生成一些假设的 bounding boxes,然后在这些 bounding boxes 中提取特征,之后再经过一个分类器,来判断里面是不是物体,是什么物体。 这类 pipeline 自从 IJCV 2013, Selective Search for Object Recognition 开始,到如今在 PASCAL VOC、MS COCO、ILSVRC 数据集上取得领先的基于 Faster R-CNN 的 ResNet 。但这类方法对于嵌入式系统,所需要的计算时间太久了,不足以实时的进行检测。当然也有很多工作是朝着实时检测迈进,但目前为止,都是牺牲检测精度来换取时间。 本文提出的实时检测方法,消除了中间的 bounding boxes、pixel or feature resampling 的过程。虽然本文不是第一篇这样做的文章(YOLO),但是本文做了一些提升性的工作,既保证了速度,也保证了检测精度。 这里面有一句非常关键的话,基本概括了本文的核心思想: Our improvements include using a small convolutional filter to predict object categories and offsets in bounding box locations, using separate predictors (filters) for different aspect ratio detections, and applying these filters to multiple feature maps from the later stages of a network in order to perform detection at multiple scales. 本文的主要贡献总结如下: 提出了新的物体检测方法:SSD,比原先最快的 YOLO: You Only Look Once 方法,还要快,还要精确。保证速度的同时,其结果的 mAP 可与使用 region proposals 技术的方法(如 Faster R-CNN)相媲美。 SSD 方法的核心就是 predict object(物体),以及其 归属类别的 score(得分);同时,在 feature map 上使用小的卷积核,去 predict 一系列 bounding boxes 的 box offsets。 本文中为了得到高精度的检测结果,在不同层次的 feature maps 上去 predict object、box offsets,同时,还得到不同 aspect ratio 的 predictions。 本文的这些改进设计,能够在当输入分辨率较低的图像时,保证检测的精度。同时,这个整体 end-to-end 的设计,训练也变得简单。在检测速度、检测精度之间取得较好的 trade-off。 本文提出的模型(model)在不同的数据集上,如 PASCAL VOC、MS COCO、ILSVRC, 都进行了测试。在检测时间(timing)、检测精度(accuracy)上,均与目前物体检测领域 state-of-art 的检测方法进行了比较。 The Single Shot Detector(SSD) 这部分详细讲解了 SSD 物体检测框架,以及 SSD 的训练方法。 这里,先弄清楚下文所说的 default box 以及 feature map cell 是什么。看下图: feature map cell 就是将 feature map 切分成 8×8 或者 4×4 之后的一个个 格子; 而 default box 就是每一个格子上,一系列固定大小的 box,即图中虚线所形成的一系列 boxes。 Model SSD 是基于一个前向传播 CNN 网络,产生一系列 固定大小(fixed-size) 的 bounding boxes,以及每一个 box 中包含物体实例的可能性,即 score。之后,进行一个 非极大值抑制(Non-maximum suppression) 得到最终的 predictions。 SSD 模型的最开始部分,本文称作 base network,是用于图像分类的标准架构。在 base network 之后,本文添加了额外辅助的网络结构: Multi-scale feature maps for detection 在基础网络结构后,添加了额外的卷积层,这些卷积层的大小是逐层递减的,可以在多尺度下进行 predictions。 Convolutional predictors for detection 每一个添加的特征层(或者在基础网络结构中的特征层),可以使用一系列 convolutional filters,去产生一系列固定大小的 predictions,具体见 Fig.2。对于一个大小为 m×n,具有 p 通道的特征层,使用的 convolutional filters 就是 3×3×p 的 kernels。产生的 predictions,那么就是归属类别的一个得分,要么就是相对于 default box coordinate 的 shape offsets。 在每一个 m×n 的特征图位置上,使用上面的 3×3 的 kernel,会产生一个输出值。bounding box offset 值是输出的 default box 与此时 feature map location 之间的相对距离(YOLO 架构则是用一个全连接层来代替这里的卷积层)。 Default boxes and aspect ratios 每一个 box 相对于与其对应的 feature map cell 的位置是固定的。 在每一个 feature map cell 中,我们要 predict 得到的 box 与 default box 之间的 offsets,以及每一个 box 中包含物体的 score(每一个类别概率都要计算出)。 因此,对于一个位置上的 k 个boxes 中的每一个 box,我们需要计算出 c 个类,每一个类的 score,还有这个 box 相对于 它的默认 box 的 4 个偏移值(offsets)。于是,在 feature map 中的每一个 feature map cell 上,就需要有 (c+4)×k 个 filters。对于一张 m×n 大小的 feature map,即会产生 (c+4)×k×m×n 个输出结果。 这里的 default box 很类似于 Faster R-CNN 中的 Anchor boxes,关于这里的 Anchor boxes,详细的参见原论文。但是又不同于 Faster R-CNN 中的,本文中的 Anchor boxes 用在了不同分辨率的 feature maps 上。 Training 在训练时,本文的 SSD 与那些用 region proposals + pooling 方法的区别是,SSD 训练图像中的 groundtruth 需要赋予到那些固定输出的 boxes 上。在前面也已经提到了,SSD 输出的是事先定义好的,一系列固定大小的 bounding boxes。 如下图中,狗狗的 groundtruth 是红色的 bounding boxes,但进行 label 标注的时候,要将红色的 groundtruth box 赋予 图(c)中一系列固定输出的 boxes 中的一个,即 图(c)中的红色虚线框。 事实上,文章中指出,像这样定义的 groundtruth boxes 不止在本文中用到。在 YOLO 中,在 Faster R-CNN中的 region proposal 阶段,以及在 MultiBox 中,都用到了。 当这种将训练图像中的 groundtruth 与固定输出的 boxes 对应之后,就可以 end-to-end 的进行 loss function 的计算以及 back-propagation 的计算更新了。 训练中会遇到一些问题: 选择一系列 default boxes 选择上文中提到的 scales 的问题 hard negative mining 数据增广的策略 下面会谈本文的解决这些问题的方式,分为以下下面的几个部分。 Matching strategy: 如何将 groundtruth boxes 与 default boxes 进行配对,以组成 label 呢? 在开始的时候,用 MultiBox 中的 best jaccard overlap 来匹配每一个 ground truth box 与 default box,这样就能保证每一个 groundtruth box 与唯一的一个 default box 对应起来。 但是又不同于 MultiBox ,本文之后又将 default box 与任何的 groundtruth box 配对,只要两者之间的jaccard overlap 大于一个阈值,这里本文的阈值为 0.5。 Training objective: SSD 训练的目标函数(training objective)源自于 MultiBox 的目标函数,但是本文将其拓展,使其可以处理多个目标类别。用 xpij=1 表示 第 i 个 default box 与 类别 p 的 第 j 个 ground truth box 相匹配,否则若不匹配的话,则 xpij=0。 根据上面的匹配策略,一定有 ∑ixpij≥1,意味着对于 第 j 个 ground truth box,有可能有多个 default box与其相匹配。 总的目标损失函数(objective loss function)就由 localization loss(loc) 与 confidence loss(conf) 的加权求和: L(x,c,l,g)=1N(Lconf(x,c)+αLloc(x,l,g)) 其中: N 是与 ground truth box 相匹配的 default boxes 个数 localization loss(loc) 是 Fast R-CNN 中 Smooth L1 Loss,用在 predict box(l) 与 ground truth box(g) 参数(即中心坐标位置,width、height)中,回归 bounding boxes 的中心位置,以及 width、height confidence loss(conf) 是 Softmax Loss,输入为每一类的置信度 c 权重项 α,设置为 1 Choosing scales and aspect ratios for default boxes: 大部分 CNN 网络在越深的层,feature map 的尺寸(size)会越来越小。这样做不仅仅是为了减少计算与内存的需求,还有个好处就是,最后提取的 feature map 就会有某种程度上的平移与尺度不变性。 同时为了处理不同尺度的物体,一些文章,如 ICLR 2014, Overfeat: Integrated recognition, localization and detection using convolutional networks,还有 ECCV 2014, Spatial pyramid pooling in deep convolutional networks for visual recognition,他们将图像转换成不同的尺度,将这些图像独立的通过 CNN 网络处理,再将这些不同尺度的图像结果进行综合。 但是其实,如果使用同一个网络中的、不同层上的 feature maps,也可以达到相同的效果,同时在所有物体尺度中共享参数。 之前的工作,如 CVPR 2015, Fully convolutional networks for semantic segmentation,还有 CVPR 2015, Hypercolumns for object segmentation and fine-grained localization 就用了 CNN 前面的 layers,来提高图像分割的效果,因为越底层的 layers,保留的图像细节越多。文章 ICLR 2016, ParseNet: Looking wider to see better 也证明了以上的想法是可行的。 因此,本文同时使用 lower feature maps、upper feature maps 来 predict detections。下图展示了本文中使用的两种不同尺度的 feature map,8×8 的feature map,以及 4×4 的 feature map: 一般来说,一个 CNN 网络中不同的 layers 有着不同尺寸的 感受野(receptive fields)。这里的感受野,指的是输出的 feature map 上的一个节点,其对应输入图像上尺寸的大小。具体的感受野的计算,参见两篇 blog: http://blog.csdn.net/kuaitoukid/article/details/46829355 http://blog.cvmarcher.com/posts/2015/05/17/cnn-trick/ 所幸的是,SSD 结构中,default boxes 不必要与每一层 layer 的 receptive fields 对应。本文的设计中,feature map 中特定的位置,来负责图像中特定的区域,以及物体特定的尺寸。加入我们用 m 个 feature maps 来做 predictions,每一个 feature map 中 default box 的尺寸大小计算如下: sk=smin+smax−sminm−1(k−1), k∈[1,m] 其中,smin 取值 0.2,smax 取值 0.95,意味着最低层的尺度是 0.2,最高层的尺度是 0.95,再用不同 aspect ratio 的 default boxes,用 ar 来表示:ar={1,2,3,12,13},则每一个 default boxes 的 width、height 就可以计算出来: wak=skar−−√hak=sk/ar−−√ 对于 aspect ratio 为 1 时,本文还增加了一个 default box,这个 box 的 scale 是 s′k=sksk+1−−−−−√。所以最终,在每个 feature map location 上,有 6 个 default boxes。 每一个 default box 的中心,设置为:(i+0.5|fk|,j+0.5|fk|),其中,|fk| 是第 k 个 feature map 的大小,同时,i,j∈[0,|fk|)。 在结合 feature maps 上,所有 不同尺度、不同 aspect ratios 的 default boxes,它们预测的 predictions 之后。可以想见,我们有许多个 predictions,包含了物体的不同尺寸、形状。如下图,狗狗的 ground truth box 与 4×4 feature map 中的红色 box 吻合,所以其余的 boxes 都看作负样本。 Hard negative mining 在生成一系列的 predictions 之后,会产生很多个符合 ground truth box 的 predictions boxes,但同时,不符合 ground truth boxes 也很多,而且这个 negative boxes,远多于 positive boxes。这会造成 negative boxes、positive boxes 之间的不均衡。训练时难以收敛。 因此,本文采取,先将每一个物体位置上对应 predictions(default boxes)是 negative 的 boxes 进行排序,按照 default boxes 的 confidence 的大小。 选择最高的几个,保证最后 negatives、positives 的比例在 3:1。 本文通过实验发现,这样的比例可以更快的优化,训练也更稳定。 Data augmentation 本文同时对训练数据做了 data augmentation,数据增广。关于数据增广,推荐一篇文章:Must Know Tips/Tricks in Deep Neural Networks,其中的 section 1 就讲了 data augmentation 技术。 每一张训练图像,随机的进行如下几种选择: 使用原始的图像 采样一个 patch,与物体之间最小的 jaccard overlap 为:0.1,0.3,0.5,0.7 与 0.9 随机的采样一个 patch 采样的 patch 是原始图像大小比例是 [0.1,1],aspect ratio 在 12 与 2 之间。 当 groundtruth box 的 中心(center)在采样的 patch 中时,我们保留重叠部分。 在这些采样步骤之后,每一个采样的 patch 被 resize 到固定的大小,并且以 0.5 的概率随机的 水平翻转(horizontally flipped) Experimental Results Base network and hole filling algorithm 本文的 Base network 是基于 ICLR 2015, VGG16 来做的,在 ILSVRC CLS-LOC 数据集上进行了预训练。 与 ICLR 2015, DeepLab-LargeFOV 的工作类似,本文将 VGG 中的 FC6 layer、FC7 layer 转成为 卷积层,并从模型的 FC6、FC7 上的参数,进行采样得到这两个卷积层的 parameters。 还将 Pool5 layer 的参数,从 2×2−s2 转变成 3×3−s1,外加一个 pad(1),如下图: 但是这样变化后,会改变感受野(receptive field)的大小。因此,采用了 atrous algorithm 的技术,这里所谓的 atrous algorithm,我查阅了资料,就是 hole filling algorithm。 在 DeepLab 的主页上:http://liangchiehchen.com/projects/DeepLab.html,有一张如下的图: 博客 1:http://www.cnblogs.com/jianyingzhou/p/5386222.html 最早用的就是 deeplab 的文章了,Semantic Image Segmentation with Deep Convolutional Nets and Fully Connected CRFS 这篇文章和 fcn 不同的是,在最后产生 score map 时,不是进行upsampling,而是采用了 hole algorithm,就是在 pool4 和 pool 5层,步长由 2 变成 1,必然输出的 score map 变大了,但是 receptive field 也变小了,为了不降低 receptive field,怎么做呢?利用 hole algorithm,将卷积 weights 膨胀扩大,即原来卷积核是 3x3,膨胀后,可能变成 7x7 了,这样 receptive field 变大了,而 score map 也很大,即输出变成 dense 的了。 这么做的好处是,输出的 score map 变大了,即是 dense 的输出了,而且 receptive field 不会变小,而且可以变大。这对做分割、检测等工作非常重要。 博客 2:http://blog.csdn.net/tangwei2014/article/details/50453334 既想利用已经训练好的模型进行 fine-tuning,又想改变网络结构得到更加 dense 的 score map. 这个解决办法就是采用 Hole 算法。如下图 (a) (b) 所示,在以往的卷积或者 pooling 中,一个 filter 中相邻的权重作用在 feature map 上的位置都是物理上连续的。如下图 (c) 所示,为了保证感受野不发生变化,某一层的 stride 由 2 变为 1 以后,后面的层需要采用 hole 算法,具体来讲就是将连续的连接关系是根据 hole size 大小变成 skip 连接的(图 (c) 为了显示方便直接画在本层上了)。不要被 (c) 中的 padding 为 2 吓着了,其实 2 个 padding 不会同时和一个 filter 相连。 pool4 的 stride 由 2 变为 1,则紧接着的 conv5_1, conv5_2 和 conv5_3 中 hole size 为 2。接着 pool5 由 2 变为 1 , 则后面的 fc6 中 hole size 为 4。 本文还将 fully convolutional reduced (atrous) VGGNet 中的所有的 dropout layers、fc8 layer 移除掉了。 本文在 fine-tuning 预训练的 VGG model 时,初始 learning rate 为 10−3,momentum 为 0.9,weight decay 为 0.0005,batch size 为 32,learning rate decay 的策略随数据集的不同而变化。 PASCAL VOC 2007 在这个数据集中,与 Fast R-CNN、Faster R-CNN 进行了比较,几种检测网络都用相同的训练数据集,以及预训练模型(VGG16)。 本文训练图像是 VOC 2007 train + VOC 2007 validation + VOC 2012 train + VOC 2012 validation,共计 16551 张图像; 测试集选取的是 VOC 2007 test,共计 4952 张图像。 下图展示了 SSD300 model 的结构: 我们用 conv4_3,conv7(原先的 FC7),conv8_2,conv9_2,conv10_2,以及 pool11,这些 layer 来predict location、 confidence。 在 VGG16 上新加的 convolutional layers,其参数初始化都用 JMLR 2010, Understanding the difficulty of training deep feedforward neural networks 提出的 xavier 方法。 因为 conv4_3 的尺寸比较大,size 为 38×38 的大小,我们只在上面放置 3 个 default boxes,一个 box 的 scale 为 0.1,另外两个 boxes 的 aspect ratio 分别为 12、2。但对于其他的用来做 predictions 的 layers,本文都放了 6 个 default boxes。 文献 ICLR 2016, ParseNet: Looking wider to see better 指出,conv4_3 相比较于其他的 layers,有着不同的 feature scale,我们使用 ParseNet 中的 L2 normalization 技术将 conv4_3 feature map 中每一个位置的 feature norm scale 到 20,并且在 back-propagation 中学习这个 scale。 在最开始的 40K 次迭代中,本文使用的 learning rate 是 10−3,之后将其减小到 10−4,再接着迭代 20K 次。 下面 Table 1 显示了,我们的 SSD300 model 的精度已经超过了 Fast R-CNN,当我们用 SSD 在更大的图像尺寸上,500×500 训练得到的 model,甚至要比 Faster R-CNN 还要高出 1.9% 的 mAP。 为了更细节的了解本文的两个 SSD model,我们使用了 ECCV 2012, Diagnosing error in object detectors 的检测分析工具。下图 Figure 3 显示了 SSD 可以高质量的检测不同种类的物体。 下图 Figure 4 展示了 SSD 模型对 bounding box 的 size 非常的敏感。也就是说,SSD 对小物体目标较为敏感,在检测小物体目标上表现较差。其实这也算情理之中,因为对于小目标而言,经过多层卷积之后,就没剩多少信息了。虽然提高输入图像的 size 可以提高对小目标的检测效果,但是对于小目标检测问题,还是有很多提升空间的。 同时,积极的看,SSD 对大目标检测效果非常好。同时,因为本文使用了不同 aspect ratios 的 default boxes,SSD 对于不同 aspect ratios 的物体检测效果也很好。 Model analysis 为了更好的理解 SSD,本文还使用控制变量法来验证 SSD 中的每一部分对最终结果性能的影响。测试如下表 Table 2 所示: 从上表可以看出一下几点: 数据增广(Data augmentation)对于结果的提升非常明显 Fast R-CNN 与 Faster R-CNN 使用原始图像,以及 0.5 的概率对原始图像进行水平翻转(horizontal flip),进行训练。如上面写的,本文还使用了额外的 sampling 策略,YOLO 中还使用了 亮度扭曲(photometric distortions),但是本文中没有使用。 做了数据增广,将 mAP 从 65.4% 提升到了 72.1%,提升了 6.7%。 我们还不清楚,本文的 sampling 策略会对 Fast R-CNN、Faster R-CNN 有多少好处。但是估计不会很多,因为 Fast R-CNN、Faster R-CNN 使用了 feature pooling,这比人为的对数据进行增广扩充,还要更 robust。 使用更多的 feature maps 对结果提升更大 类似于 FCN,使用含图像信息更多的低 layer 来提升图像分割效果。我们也使用了 lower layer feature maps 来进行 predict bounding boxes。 我们比较了,当 SSD 不使用 conv4_3 来 predict boxes 的结果。当不使用 conv4_3,mAP 下降到了 68.1%。 可以看见,低层的 feature map 蕴含更多的信息,对于图像分割、物体检测性能提升帮助很大的。 使用更多的 default boxes,结果也越好 如 Table 2 所示,SSD 中我们默认使用 6 个 default boxes(除了 conv4_3 因为大小问题使用了 3 个 default boxes)。如果将 aspect ratios 为 13、3 的 boxes 移除,performance 下降了 0.9%。如果再进一步的,将 12、2 的 default boxes 移除,那么 performance 下降了近 2%。 Atrous 使得 SSD 又好又快 如前面所描述,我们根据 ICLR 2015, DeepLab-LargeFOV,使用结合 atrous algorithm 的 VGG16 版本。 如果我们使用原始的 VGG16 版本,即保留 pool5 的参数为:2×2−s2,且不从 FC6,FC7 上采集 parameters,同时添加 conv5_3 来做 prediction,结果反而会下降 0.7%。同时最关键的,速度慢了 50%。 PASCAL VOC 2012 本文又在 VOC 2012 test 上进行的实验,比较结果如下: MS COCO 为了进一步的验证本文的 SSD 模型,我们将 SSD300、SSD500 在 MS COCO 数据集上进行训练检测。 因为 COCO 数据集中的检测目标更小,我们在所有的 layers 上,使用更小的 default boxes。 这里,还跟 ION 检测方法 进行了比较。 总的结果如下: Inference time 本文的方法一开始会生成大量的 bounding boxes,所以有必要用 Non-maximum suppression(NMS)来去除大量重复的 boxes。 通过设置 confidence 的阈值为 0.01,我们可以过滤掉大多数的 boxes。 之后,我们再用 Thrust CUDA library 进行排序,用 GPU 版本的实现来计算剩下的 boxes 两两之间的 overlap。然后,进行 NMS,每一张图像保留 top 200 detections。这一步 SSD300 在 VOC 20 类的每张图像上,需要耗时 2.2 msec。 下面是在 PASCAL VOC 2007 test 上的速度统计: Related work and result images 这篇文章居然把相关工作总结放在最后面,我还是第一次见到。 具体的看原文吧。 最后放几张结果图:
ElasticSearch已经可以与YARN、Hadoop、Hive、Pig、Spark、Flume等大数据技术框架整合起来使用,尤其是在添加数据的时候,可以使用分布式任务来添加索引数据,尤其是在数据平台上,很多数据存储在Hive中,使用Hive操作ElasticSearch中的数据,将极大的方便开发人员。这里记录一下Hive与ElasticSearch整合,查询和添加数据的配置使用过程。基于Hive0.13.1、Hadoop-cdh5.0、ElasticSearch 2.1.0。 通过Hive读取与统计分析ElasticSearch中的数据 ElasticSearch中已有的数据 _index:lxw1234 _type:tags _id:用户ID(cookieid) 字段:area、media_view_tags、interest Hive建表 由于我用的ElasticSearch版本为2.1.0,因此必须使用elasticsearch-hadoop-2.2.0才能支持,如果ES版本低于2.1.0,可以使用elasticsearch-hadoop-2.1.2. 下载地址:https://www.elastic.co/downloads/hadoop add jar file:///home/liuxiaowen/elasticsearch-hadoop-2.2.0-beta1/dist/elasticsearch-hadoop-hive-2.2.0-beta1.jar; CREATE EXTERNAL TABLE lxw1234_es_tags ( cookieid string, area string, media_view_tags string, interest string ) STORED BY 'org.elasticsearch.hadoop.hive.EsStorageHandler' TBLPROPERTIES( 'es.nodes' = '172.16.212.17:9200,172.16.212.102:9200', 'es.index.auto.create' = 'false', 'es.resource' = 'lxw1234/tags', 'es.read.metadata' = 'true', 'es.mapping.names' = 'cookieid:_metadata._id, area:area, media_view_tags:media_view_tags, interest:interest'); 注意:因为在ES中,lxw1234/tags的_id为cookieid,要想把_id映射到Hive表字段中,必须使用这种方式: ‘es.read.metadata’ = ‘true’, ‘es.mapping.names’ = ‘cookieid:_metadata._id,…’ 在Hive中查询数据 数据已经可以正常查询。 执行SELECT COUNT(1) FROM lxw1234_es_tags;Hive还是通过MapReduce来执行,每个分片使用一个Map任务: 可以通过在Hive外部表中指定search条件,只查询过滤后的数据。比如,下面的建表语句会从ES中搜索_id=98E5D2DE059F1D563D8565的记录: CREATE EXTERNAL TABLE lxw1234_es_tags_2 ( cookieid string, area string, media_view_tags string, interest string ) STORED BY 'org.elasticsearch.hadoop.hive.EsStorageHandler' TBLPROPERTIES( 'es.nodes' = '172.16.212.17:9200,172.16.212.102:9200', 'es.index.auto.create' = 'false', 'es.resource' = 'lxw1234/tags', 'es.read.metadata' = 'true', 'es.mapping.names' = 'cookieid:_metadata._id, area:area, media_view_tags:media_view_tags, interest:interest', 'es.query' = '?q=_id:98E5D2DE059F1D563D8565' ); hive> select * from lxw1234_es_tags_2; OK 98E5D2DE059F1D563D8565 四川|成都 购物|1 购物|1 Time taken: 0.096 seconds, Fetched: 1 row(s) 如果数据量不大,可以使用Hive的Local模式来执行,这样不必提交到Hadoop集群: 在Hive中设置: set hive.exec.mode.local.auto.inputbytes.max=134217728; set hive.exec.mode.local.auto.tasks.max=10; set hive.exec.mode.local.auto=true; set fs.defaultFS=file:///; hive> select area,count(1) as cnt from lxw1234_es_tags group by area order by cnt desc limit 20; Automatically selecting local only mode for query Total jobs = 2 Launching Job 1 out of 2 ….. Execution log at: /tmp/liuxiaowen/liuxiaowen_20151211133030_97b50138-d55d-4a39-bc8e-cbdf09e33ee6.log Job running in-process (local Hadoop) Hadoop job information for null: number of mappers: 0; number of reducers: 0 2015-12-11 13:30:59,648 null map = 100%, reduce = 100% Ended Job = job_local1283765460_0001 Execution completed successfully MapredLocal task succeeded OK 北京|北京 10 四川|成都 4 重庆|重庆 3 山西|太原 3 上海|上海 3 广东|深圳 3 湖北|武汉 2 陕西|西安 2 福建|厦门 2 广东|中山 2 福建|三明 2 山东|济宁 2 甘肃|兰州 2 安徽|合肥 2 湖南|长沙 2 湖南|湘西 2 河南|洛阳 2 江苏|南京 2 黑龙江|哈尔滨 2 广西|南宁 2 Time taken: 13.037 seconds, Fetched: 20 row(s) hive> 很快完成了查询与统计。 通过Hive向ElasticSearch中写数据 Hive建表 add jar file:///home/liuxiaowen/elasticsearch-hadoop-2.2.0-beta1/dist/elasticsearch-hadoop-hive-2.2.0-beta1.jar; CREATE EXTERNAL TABLE lxw1234_es_user_tags ( cookieid string, area string, gendercode STRING, birthday STRING, jobtitle STRING, familystatuscode STRING, haschildrencode STRING, media_view_tags string, order_click_tags STRING, search_egine_tags STRING, interest string ) STORED BY 'org.elasticsearch.hadoop.hive.EsStorageHandler' TBLPROPERTIES( 'es.nodes' = '172.16.212.17:9200,172.16.212.102:9200', 'es.index.auto.create' = 'true', 'es.resource' = 'lxw1234/user_tags', 'es.mapping.id' = 'cookieid', 'es.mapping.names' = 'area:area, gendercode:gendercode, birthday:birthday, jobtitle:jobtitle, familystatuscode:familystatuscode, haschildrencode:haschildrencode, media_view_tags:media_view_tags, order_click_tags:order_click_tags, search_egine_tags:search_egine_tags, interest:interest'); 这里要注意下:如果是往_id中插入数据,需要设置’es.mapping.id’ = ‘cookieid’参数,表示Hive中的cookieid字段对应到ES中的_id,而es.mapping.names中不需要再映射,这点和读取时候的配置不一样。 关闭Hive推测执行,执行INSERT: SET hive.mapred.reduce.tasks.speculative.execution = false; SET mapreduce.map.speculative = false; SET mapreduce.reduce.speculative = false; INSERT overwrite TABLE lxw1234_es_user_tags SELECT cookieid, area, gendercode, birthday, jobtitle, familystatuscode, haschildrencode, media_view_tags, order_click_tags, search_egine_tags, interest FROM source_table; 注意:如果ES集群规模小,而source_table数据量特别大、Map任务数太多的时候,会引发错误: Caused by: org.elasticsearch.hadoop.rest.EsHadoopInvalidRequest: FOUND unrecoverable error [172.16.212.17:9200] returned Too Many Requests(429) - rejected execution of org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryPhase$1@b6fa90f ON EsThreadPoolExecutor[bulk, queue capacity = 50, org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor@22e73289[Running, pool size = 32, active threads = 32, queued tasks = 52, completed tasks = 12505]]; Bailing out.. 原因是Map任务数太多,并发发送至ES的请求数过多。 这个和ES集群规模以及bulk参数设置有关,目前还没弄明白。 减少source_table数据量(即减少Map任务数)之后,没有出现这个错误。 执行完成后,在ES中查询lxw1234/user_tags的数据: curl -XGET http://172.16.212.17:9200/lxw1234/user_tags/_search?pretty -d ' { "query" : { "match" : { "area" : "成都" } } }' 数据已经写入到ElasticSearch中。 总结 使用Hive将数据添加到ElasticSearch中还是非常实用的,因为我们的数据都是在HDFS上,通过Hive可以查询的。 另外,通过Hive可以查询ES数据,并在其上做复杂的统计与分析,但性能一般,比不上使用ES原生API,亦或是还没有掌握使用技巧,后面继续研究。 相关阅读: ElasticSearch集群安装配置 ElasticSearch与Hive整合官方文档
ElasticSearch是一个开源搜索服务框架,它已经成为搜索解决方案领域的重要成员。ElasticSearch还经常被用作文档数据库,这主要得益于它的分布式特性和实时搜索能力,另外,ElasticSearch支持越来越多的聚合功能,而且和Yarn、Hadoop、Hive、Pig、Spark、Flume等大数据处理框架的兼容性越来越好。我主要是想调研一下看是否能将它用于实时的数据搜索统计、以及实时OLAP的业务场景之上。这里先记录一下ElasticSearch集群的安装配置。 ElasticSearch的一些术语 索引(index) 索引(Index)相当于关系型数据库中的表; 文档(document) 文档相当于关系型数据库中的行。但ES中的文档不需要有固定的结构,不同文档可以有不同的字段集合。 文档类型(type) 在一个索引中,可以用不同的文档类型来代表不同的数据集合。 节点和集群 ElastichSearch可以作为一个独立的搜索服务器工作,也可以在多台协同工作的服务器上运行,统称为一个集群,其中有一台被作为Master,其他为Slave; 分片(shard) 一个索引会被分隔成多个分片,分别存放于集群中的不同节点中,类似于HDFS的文件块。 副本 副本是针对每个分片的,可以为一个分片设置多个副本,分布在不同的节点上,即是容错,也可以提高查询任务的性能,原理同HDFS的文件块副本机制。 硬件及软件环境 两台CentOS,172.16.212.17(32G内存)、172.16.212.102(72G内存) JAVA: jdk-8u65-linux-x64.tar.gz ElasticSearch:elasticsearch-2.1.0 下载JAVA和ElasticSearch之后解压到指定目录(/home/liuxiaowen/)。 配置elasticsearch.yml cluster.name: hy_es //集群名称 node.name: es_node_102 //节点名称,两台节点名称不能一样 path.data: /data/es/data //ES存放数据的目录 path.logs: /data/es/logs //ES存放日志的目录 network.host: 172.16.212.102 //这个可以不配置,默认为0.0.0.0 network.publish_host: 172.16.212.102 //这个可以不配置,默认为0.0.0.0 gateway.recover_after_nodes: 2 //设置集群中N个节点启动时进行数据恢复 discovery.zen.ping_timeout: 10s //节点之间通过ping进行应答的超时时间 discovery.zen.ping.unicast.hosts: ["172.16.212.102", "172.16.212.17"] //集群中可以作为master节点的初始列表,通过这些节点来自动发现新加入集群的节点 discovery.zen.minimum_master_nodes: 1 //配置当前集群中最少的主节点数,对于多于两个节点的集群环境,建议配置大于1 配置ElasticSearch使用内存 $ES_HOME/bin/elasticsearch.in.sh if [ "x$ES_MIN_MEM" = "x" ]; then ES_MIN_MEM=10g //最小内存,根据机器内存来定 fi if [ "x$ES_MAX_MEM" = "x" ]; then ES_MAX_MEM=36g //最大内存,根据机器内存来定,最好不要超过机器物理内存的50% fi 配置JAVA环境变量 在用户的.bash_profile中加入: export JAVA_HOME=/hom/liuxiaowen/jdk1.8.0_65 export PATH=$JAVA_HOME/bin:$PATH 或者在$ES_HOME/bin/elasticsearch脚本中配置: export JAVA_HOME=/hom/liuxiaowen/jdk1.8.0_65 或者在启动ElasticSearch之前在命令行export JAVA_HOME=/hom/liuxiaowen/jdk1.8.0_65 都可以。 安装head监控插件 cd $ES_HOME/bin ./plugin install mobz/elasticsearch-head 需要在两台机器上都安装。 安装成功后,如图所示: 启动ElasticSearch cd $ES_HOME/bin ./elasticsearch -d -d参数表示在后台启动。 ElasticSearch运行日志在配置的${path.logs}/${cluster.name}.log中 Master(102) Slave(17) head监控页面(http://172.16.212.102:9200/_plugin/head/) 查看集群状态: curl -XGET 'http://172.16.212.102:9200/_cluster/health?pretty' 响应结果: { "cluster_name" : "hy_es", "status" : "green", "timed_out" : false, "number_of_nodes" : 2, "number_of_data_nodes" : 2, "active_primary_shards" : 5, "active_shards" : 10, "relocating_shards" : 0, "initializing_shards" : 0, "unassigned_shards" : 0, "delayed_unassigned_shards" : 0, "number_of_pending_tasks" : 0, "number_of_in_flight_fetch" : 0, "task_max_waiting_in_queue_millis" : 0, "active_shards_percent_as_number" : 100.0 } 集群配置完毕,刚接触,有些参数可能配置的不合理,后续再慢慢研究。 接下来将介绍ElasticSearch的一些基本操作。
之前介绍过ElasticSearch,它部署简单,搜索聚合功能强大,而且和其他大数据框架整合起来使用,有一点比较不方便,就是查询都需要通过JSON作为请求Body来提交查询,请求响应也是JSON,作为习惯使用SQL的我,迫不及待的试用了一下Crate(crate.io),它是在ElasticSearch之上封装了SQL接口,用户可以通过SQL语句来完成搜索和统计,支持的SQL语法还蛮多的,很想MySQL。 本文记录一下Crate的安装配置(两个节点的Crate集群)和简单使用。 下载和安装Crate 可以从https://cdn.crate.io/downloads/releases/nightly/下载crate的最新版本。 下载后解压到指定目录即可。 配置Crate Crate的配置和ElasticSearch非常类似,以两个节点的Crate集群为例。 cd $CRATE_HOME/conf 编辑crate.yml,修改以下参数: cluster.name: lxw1234_crate node.name: crate_node_17 index.number_of_replicas: 2 path.conf: /home/liuxiaowen/crate-0.54.0/config path.data: /home/liuxiaowen/crate-0.54.0/data path.work: /home/liuxiaowen/crate-0.54.0/tmp path.logs: /home/liuxiaowen/crate-0.54.0/logs path.plugins: /home/liuxiaowen/crate-0.54.0/plugins network.bind_host: 172.16.212.17 network.publish_host: 172.16.212.17 network.host: 172.16.212.17 gateway.recover_after_nodes: 2 discovery.zen.minimum_master_nodes: 2 gateway.expected_nodes: 2 discovery.zen.ping.timeout: 10s discovery.zen.fd.ping_interval: 10s 编辑$CRATE_HOME/bin/crate.in.sh,配置节点使用的内存,根据机器自身内存而定,最大内存一般不要超过物理内存的50%; CRATE_MIN_MEM=8g CRATE_MAX_MEM=16g 配置JAVA_HOME,我这里使用了jdk1.8.0_65 启动Crate 在两个节点上, cd $CRATE_HOME/bin 执行./crate -d 在后台启动Crate,之后可以在配置的path.logs目录下,看到以${ cluster.name }.log命名的日志。 使用Crate命令行 类似于其他数据库,Crate提供了一个命令行来供用户执行SQL查询。 cd $CRATE_HOME/bin 执行./crash进入命令行; 在Crate命令行使用\c 172.16.212.17:4200连接到Crate; 创建表 在Crate命令行使用下面的SQL语句创建表: CREATE TABLE sitelog ( cookieid STRING, siteid STRING, visit_id STRING, pv LONG, is_return_cookie INTEGER, is_bounce_visit INTEGER, visit_stay_times INTEGER, visit_view_page_cnt INTEGER, region STRING, city STRING ); cr> show tables; +------------+ | table_name | +------------+ | sitelog | +------------+ SHOW 1 row in set (0.019 sec) cr> 从外部批量加载数据 crate提供了一个COPY命令,用于从外部文本文件加载数据到表中,但只支持JSON格式的文本,比如: [liuxiaowen@dev sitelog]$ head sitelog_000005_0_9.json {"cookieid" : "DE9C68B401DBE5566A9676","siteid" : "633","visit_id" : "805cdab5-8361-4134-9bbe-7c54771d4dc8","pv" : 1, "is_return_cookie" : 0,"is_bounce_visit" : 1,"visit_stay_times" : 0,"visit_view_page_cnt" : 1,"region" : "江苏","city" : "徐州"} {"cookieid" : "DE9C68B40422A9566A68F2","siteid" : "633","visit_id" : "7f844323-e0c0-48b4-bc1b-69055ac3c308","pv" : 1, "is_return_cookie" : 0,"is_bounce_visit" : 1,"visit_stay_times" : 0,"visit_view_page_cnt" : 1,"region" : "江苏","city" : "徐州"} {"cookieid" : "DE9C68B4066B7F566A6F36","siteid" : "633","visit_id" : "045c3a13-41bf-45c4-93ce-7725a00ada5f","pv" : 1, "is_return_cookie" : 0,"is_bounce_visit" : 1,"visit_stay_times" : 0,"visit_view_page_cnt" : 1,"region" : "江苏","city" : "徐州"} JSON对象中的k需要和表的字段名称相同。 在Crate命令行使用COPY命令加载数据: 加载的速度还是非常快的。 SQL查询 可以从Crate官网上查看支持的SQL语法:https://crate.io/docs/reference/sql/dql.html 值得关注的是,Crate在做COUNT DISTINCT查询的时候,查出来的是真实去重后的数,没有误差,但查询响应时间要慢一些,有待研究。 Crate的监控界面 Crate提供了一个比较炫的监控界面,非常有用,Crate集群启动后,在浏览器输入:http://172.16.212.102:4200/admin/ 进入监控界面: OverView页面:集群整体健康及负载状况。 Tables页面:Crate中所有Table及Schema的情况。 Cluster页面:Crate集群的节点列表及每个节点的健康状况。 Crate的不足 目前只是简单安装试用了一下,发现了几点不足: 不支持子查询; 不支持诸如CASE WHEN、IF ELSE的逻辑判断语法,特别是在聚合函数中; 内置的ElasticSearch版本太低; 没有和其他大数据组件的整合。 但它的查询性能还是很不错的,关键是SQL方便啊。
一直想找一个用于大数据平台实时OLAP(甚至是实时计算)的框架,之前调研的Druid(druid.io)太过复杂,整个Druid由5、6个服务组成,而且加载数据也不太方便,性能一般,亦或是我还不太会用它。后来发现使用ElasticSearch就可以满足海量数据实时OLAP的需求。 ElasticSearch相信大家都很熟悉了,它在搜索领域已经有了举足轻重的地位,而且也支持越来越多的聚合统计功能,还和YARN、Hadoop、Hive、Spark、Pig、Flume等大数据框架兼容的越来越好,比如:可以将ElasticSearch跑在YARN上,还可以在Hive中建立外部表映射到ElasticSearch的Index中,直接在Hive中执行INSERT语句,将数据加载进ElasticSearch。 所谓OLAP,其实就是从事实表中统计任意组合维度的指标,也就是过滤、分组、聚合,其中,聚合除了一般的SUM、COUNT、AVG、MAX、MIN等,还有一个重要的COUNT(DISTINCT),看上去这些操作在SQL中是非常简单的统计,但在海量数据、低延迟的要求下,并不是那么容易做的。 ElasticSearch本来就是做实时搜索的,过滤自然不是问题,现在也支持各种聚合以及Pipeline aggregations(相当于SQL子查询的功能),而且ElasticSearch的安装部署也非常简单,一个节点只有一个服务进程,关于安装配置可参考:http://lxw1234.com/archives/2015/12/582.htm 本文以两个业务场景的例子,看一下ElasticSearch是如何满足我们的需求的。 例子1:网站流量报告 在我们的报表平台有这样一张报表,用于查看每个网站每天的流量指标: 其中,维度有:天、小时、网站,指标有:PV、UV、访问次数、跳出率、平均停留时间、回访率等。另外,还有一张报表是地域报告,维度多了省份和城市,指标一样。目前的做法是将可选的维度组合及对应的指标先在Hive中分析好,再将结果同步至MySQL,供报表展现。 真正意义上的OLAP做法,我是这样做的:在Hive分析好一张最细粒度为visit_id(session_id)的事实表,字段及数据如下: 然后将这张事实表的数据加载到ElasticSearch中的logs2/sitelog1211中。查看数据: curl -XGET 'http://localhost:9200/logs2/sitelog1211/_search?pretty' { "took" : 1015, "timed_out" : false, "_shards" : { "total" : 10, "successful" : 10, "failed" : 0 }, "hits" : { "total" : 3356328, "max_score" : 1.0, "hits" : [ { "_index" : "logs2", "_type" : "sitelog1211", "_id" : "AVGkoWowd8ibEMoyOhve", "_score" : 1.0, "_source":{"cookieid" : "8F97E07300BC7655F6945A","siteid" : "633","visit_id" : "feaa25e6-3208-4801-b7ed-6fa45f11ff42","pv" : 2,"is_return_cookie" : 0, "is_bounce_visit" : 0,"visit_stay_times" : 34,"visit_view_page_cnt" : 2, "region" : "浙江","city" : "绍兴"} }, …… 该天事实表中总记录数为3356328。 接着使用下面的查询,完成了上图中网站ID为1127,日期为2015-12-11的流量报告: curl -XGET 'http://localhost:9200/logs2/sitelog1211/_search?search_type=count&q=siteid:1127&pretty' -d ' { "size": 0, "aggs" : { "pv" : {"sum" : { "field" : "pv" } }, "uv" : {"cardinality" : {"field" : "cookieid" ,"precision_threshold": 40000}}, "return_uv" : { "filter" : {"term" : {"is_return_cookie" : 1}}, "aggs" : { "total_return_uv" : {"cardinality" : {"field" : "cookieid" ,"precision_threshold": 40000}} } }, "visits" : {"cardinality" : {"field" : "visit_id" ,"precision_threshold": 40000}}, "total_stay_times" : {"sum" : { "field" : "visit_stay_times" }}, "bounce_visits" : { "filter" : {"term" : {"is_bounce_visit" : 1}}, "aggs" : { "total_bounce_visits" : {"cardinality" : {"field" : "visit_id" ,"precision_threshold": 40000}} } } } }' 基本上1~2秒就可以返回结果: { "took" : 1887, "timed_out" : false, "_shards" : { "total" : 10, "successful" : 10, "failed" : 0 }, "hits" : { "total" : 5888, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "uv" : { "value" : 5859 }, "visits" : { "value" : 5889 }, "return_uv" : { "doc_count" : 122, "total_return_uv" : { "value" : 119 } }, "bounce_visits" : { "doc_count" : 5177, "total_bounce_visits" : { "value" : 5177 } }, "pv" : { "value" : 10820.0 }, "total_stay_times" : { "value" : 262810.0 } } } 接着是地域报告中维度为省份的指标统计,查询语句为: curl -XGET 'http://localhost:9200/logs2/sitelog1211/_search?search_type=count&q=siteid:1127&pretty' -d ' { "size": 0, "aggs" : { "area_count" : { "terms" : {"field" : "region","order" : { "pv" : "desc" }}, "aggs" : { "pv" : {"sum" : { "field" : "pv" } }, "uv" : {"cardinality" : {"field" : "cookieid" ,"precision_threshold": 40000}}, "return_uv" : { "filter" : {"term" : {"is_return_cookie" : 1}}, "aggs" : { "total_return_uv" : {"cardinality" : {"field" : "cookieid" ,"precision_threshold": 40000}} } }, "visits" : {"cardinality" : {"field" : "visit_id" ,"precision_threshold": 40000}}, "total_stay_times" : {"sum" : { "field" : "visit_stay_times" }}, "bounce_visits" : { "filter" : {"term" : {"is_bounce_visit" : 1}}, "aggs" : { "total_bounce_visits" : {"cardinality" : {"field" : "visit_id" ,"precision_threshold": 40000}} } } } } } }' 因为要根据省份分组,比之前的查询慢一点,但也是秒级返回: { "took" : 4349, "timed_out" : false, "_shards" : { "total" : 10, "successful" : 10, "failed" : 0 }, "hits" : { "total" : 5888, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "area_count" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 2456, "buckets" : [ { "key" : "北京", "doc_count" : 573, "uv" : { "value" : 568 }, "visits" : { "value" : 573 }, "return_uv" : { "doc_count" : 9, "total_return_uv" : { "value" : 8 } }, "bounce_visits" : { "doc_count" : 499, "total_bounce_visits" : { "value" : 499 } }, "pv" : { "value" : 986.0 }, "total_stay_times" : { "value" : 24849.0 } }, { "key" : "山东", "doc_count" : 368, "uv" : { "value" : 366 }, "visits" : { "value" : 368 }, "return_uv" : { "doc_count" : 9, "total_return_uv" : { "value" : 9 } }, "bounce_visits" : { "doc_count" : 288, "total_bounce_visits" : { "value" : 288 } }, "pv" : { "value" : 956.0 }, "total_stay_times" : { "value" : 30266.0 } }, …… 这里需要说明一下,在ElasticSearch中,对于去重计数(COUNT DISTINCT)是基于计数估计(Cardinality),因此如果去重记录数比较大(超过40000),便可能会有误差,误差范围是0~2%。 例子2:用户标签的搜索统计 有一张数据表,存储了每个用户ID对应的标签,同样加载到ElasticSearch中,数据格式如下: curl -XGET 'http://localhost:9200/lxw1234/user_tags/_search?&pretty' { "took" : 220, "timed_out" : false, "_shards" : { "total" : 10, "successful" : 10, "failed" : 0 }, "hits" : { "total" : 820165, "max_score" : 1.0, "hits" : [ { "_index" : "lxw1234", "_type" : "user_tags", "_id" : "222222222222222", "_score" : 1.0, "_source":{"sex" : "女性","age" : "27到30岁","income" : "5000到10000","edu" : "本科", "appcategory" : "娱乐类|1.0","interest" : "","onlinetime" : "9:00~12:00|1.0","os" : "IOS|1.0", "hobby" : "游戏|28.57,房产|8.57,服饰鞋帽箱包|28.57,互联网/电子产品|5.71,家居|8.57,餐饮美食|5.71,体育运动|14.29","region" : "河南省"} } ...... 每个用户都有性别、年龄、收入、教育程度、兴趣、地域等标签,其中使用_id来存储用户ID,也是主键。 查询1:SELECT count(1) FROM user_tags WHERE sex = ‘女性’ AND appcategory LIKE ‘%游戏类%'; curl -XGET 'http://localhost:9200/lxw1234/user_tags/_count?pretty' -d ' { "filter" : { "and" : [ {"term" : {"sex" : "女性"}}, {"match_phrase" : {"appcategory" : "游戏类"}} ] } }' 返回结果: { "count" : 106977, "_shards" : { "total" : 10, "successful" : 10, "failed" : 0 } } 查询2:先筛选,再分组统计: SELECT edu,COUNT(1) AS cnt FROM user_tags WHERE sex = '女性' AND appcategory LIKE '%游戏类%' GROUP BY edu ORDER BY cnt DESC limit 10; 查询语句: curl -XGET 'http://localhost:9200/lxw1234/user_tags/_search?search_type=count&pretty' -d ' { "filter" : { "and" : [ {"term" : {"sex" : "女性"}}, {"match_phrase" : {"appcategory" : "游戏类"}} ] }, "aggs" : { "edu_count" : { "terms" : { "field" : "edu", "size" : 10 } } } }' 返回结果: { "took" : 479, "timed_out" : false, "_shards" : { "total" : 10, "successful" : 10, "failed" : 0 }, "hits" : { "total" : 106977, "max_score" : 0.0, "hits" : [ ] }, "aggregations" : { "edu_count" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "本科", "doc_count" : 802670 }, { "key" : "硕士研究生", "doc_count" : 16032 }, { "key" : "专科", "doc_count" : 1433 }, { "key" : "博士研究生", "doc_count" : 25 }, { "key" : "初中及以下", "doc_count" : 4 }, { "key" : "中专/高中", "doc_count" : 1 } ] } } } 从目前的调研结果来看,ElasticSearch没有让人失望,部署简单,数据加载方便,聚合功能完备,查询速度快,目前完全可以满足我们的实时搜索、统计和OLAP需求,甚至可以作为NOSQL来使用,接下来再做更深入的测试。 另外,还有一个开源的SQL for ElasticSearch的框架Crate(crate.io),是在ElasticSearch之上封装了SQL接口,使得查询统计更加方便,不过SQL支持的功能有限,使用的ElasticSearch版本较低,后面试用一下再看。
1.如何关闭ES,elasticsearch关闭办法 1.使用head插件 找到想关掉的节点进行关停 2.使用命令kill杀掉服务器的ES进程即可 1.查找ES进程 ps -ef | grep elastic 2.杀掉ES进程 kill -9 2382(进程号) 3.重启ES sh elasticsearch -d 2.如何重启ES 没有重启的办法,只有参考上面关闭->开启这样。 以下是详细的操作过程 1.首先是查找服务器是否有ES进程,无果ES没有开启,也就不用关闭了,如果开启,则杀死进程就行 1.查找进程命令 ps -ef | grep elastic [root@bjdhj-125-203 _site]# ps -ef | grep elastic //然后可以看到如下的进程号,2382,2583之类的,之后kill掉就可以啦。 root 2382 1 0 Jan05 ? 00:10:57 /opt/soft/jdk/jdk1.7.0_80/bin/java -Xms2g -Xmx2g -Djava.awt.headless=true -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError -XX:+DisableExplicitGC -Dfile.encoding=UTF-8 -Delasticsearch -Des.path.home=/opt/soft/elasticsearch-1.5.2-2 -cp :/opt/soft/elasticsearch-1.5.2-2/lib/elasticsearch-1.5.2.jar:/opt/soft/elasticsearch-1.5.2-2/lib/*:/opt/soft/elasticsearch-1.5.2-2/lib/sigar/* org.elasticsearch.bootstrap.Elasticsearch root 2583 1 0 Jan05 ? 00:10:24 /opt/soft/jdk/jdk1.7.0_80/bin/java -Xms2g -Xmx2g -Djava.awt.headless=true -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError -XX:+DisableExplicitGC -Dfile.encoding=UTF-8 -Delasticsearch -Des.path.home=/opt/soft/elasticsearch-1.5.2 -cp :/opt/soft/elasticsearch-1.5.2/lib/elasticsearch-1.5.2.jar:/opt/soft/elasticsearch-1.5.2/lib/*:/opt/soft/elasticsearch-1.5.2/lib/sigar/* org.elasticsearch.bootstrap.Elasticsearch root 8682 8564 0 18:04 pts/0 00:00:00 grep elastic 2.杀掉进程 kill -9 2382(进程号) //杀掉杀掉统统杀掉,如果不确定进程号,可以看看上面信息里面的进程路径地址,防止杀错 [root@bjdhj-125-203 _site]# kill -9 2382 [root@bjdhj-125-203 _site]# ps -ef | grep elastic root 2583 1 0 Jan05 ? 00:10:24 /opt/soft/jdk/jdk1.7.0_80/bin/java -Xms2g -Xmx2g -Djava.awt.headless=true -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError -XX:+DisableExplicitGC -Dfile.encoding=UTF-8 -Delasticsearch -Des.path.home=/opt/soft/elasticsearch-1.5.2 -cp :/opt/soft/elasticsearch-1.5.2/lib/elasticsearch-1.5.2.jar:/opt/soft/elasticsearch-1.5.2/lib/*:/opt/soft/elasticsearch-1.5.2/lib/sigar/* org.elasticsearch.bootstrap.Elasticsearch root 8684 8564 0 18:05 pts/0 00:00:00 grep elastic 3.重启命令 sh elasticsearch -d [root@bjdhj-125-203 elasticsearch-1.5.2]# ls bin config data lib LICENSE.txt logs NOTICE.txt plugins README.textile [root@bjdhj-125-203 elasticsearch-1.5.2]# cd bin [root@bjdhj-125-203 bin]# ./elasticsearch -d -bash: ./elasticsearch: Permission denied [root@bjdhj-125-203 bin]# sh elasticsearch -d link /opt/soft/jdk/jdk1.7.0_80 to /opt/soft/java User=root SourceJDKFileBase=10.126.103.198 JDKBasePath=/opt/soft/jdk Timeout=2 DefaultVer=jdk1.6.0_45 CurrentVer=jdk1.7.0_80 Initialize jdk(jdk1.7.0_80) done Current jdk version = 1.7.0_80 [root@bjdhj-125-203 bin]# ps -ef | grep elastic
Visit prometheus.io for the full documentation, examples and guides. Prometheus, a Cloud Native Computing Foundation project, is a systems and service monitoring system. It collects metrics from configured targets at given intervals, evaluates rule expressions, displays the results, and can trigger alerts if some condition is observed to be true. Prometheus' main distinguishing features as compared to other monitoring systems are: a multi-dimensional data model (timeseries defined by metric name and set of key/value dimensions) a flexible query language to leverage this dimensionality no dependency on distributed storage; single server nodes are autonomous timeseries collection happens via a pull model over HTTP pushing timeseries is supported via an intermediary gateway targets are discovered via service discovery or static configuration multiple modes of graphing and dashboarding support support for hierarchical and horizontal federation Architecture overview Install There are various ways of installing Prometheus. Precompiled binaries Precompiled binaries for released versions are available in the download section on prometheus.io. Using the latest production release binary is the recommended way of installing Prometheus. See the Installing chapter in the documentation for all the details. Debian packages are available. Docker images Docker images are available on Quay.io. Building from source To build Prometheus from the source code yourself you need to have a working Go environment with version 1.5 or greater installed. You can directly use the go tool to download and install the prometheus and promtool binaries into your GOPATH. We use Go 1.5's experimental vendoring feature, so you will also need to set the GO15VENDOREXPERIMENT=1 environment variable in this case: $ GO15VENDOREXPERIMENT=1 go get github.com/prometheus/prometheus/cmd/... $ prometheus -config.file=your_config.yml You can also clone the repository yourself and build using make: $ mkdir -p $GOPATH/src/github.com/prometheus $ cd $GOPATH/src/github.com/prometheus $ git clone https://github.com/prometheus/prometheus.git $ cd prometheus $ make build $ ./prometheus -config.file=your_config.yml The Makefile provides several targets: build: build the prometheus and promtool binaries test: run the tests format: format the source code vet: check the source code for common errors assets: rebuild the static assets docker: build a docker container for the current HEAD
访问一个api, 返回如下数据: {"status":"success","data":{"resultType":"matrix","result":[{"metric":{},"values":[[1473820558.361,"28765"],[1473820573.361,"28768"],[1473820588.361,"28772"],[1473820603.361,"28776"],[1473820618.361,"28780"],[1473820633.361,"28783"],[1473820648.361,"28786"],[1473820663.361,"28790"],[1473820678.361,"28793"],[1473820693.361,"28796"],[1473820708.361,"28799"],[1473820723.361,"28802"],[1473820738.361,"28806"],[1473820753.361,"28809"],[1473820768.361,"28817"],[1473820783.361,"28829"],[1473820798.361,"28832"],[1473820813.361,"28858"],[1473820828.361,"28862"],[1473820843.361,"28867"],[1473820858.361,"28873"]]}]}} js, err := simplejson.NewJson(body) if err != nil { panic(err.Error()) } //解析数组 arr, _ := js.Get("data").Get("result").GetIndex(0).Get("values").Array() length := len(arr) for i := 0; i < length; i++ { x:= *js.Get("data").Get("result").GetIndex(0).Get("values").GetIndex(i).GetIndex(0)) //fmt.Println(*js.Get("data").Get("result").GetIndex(0).Get("values").GetIndex(i).GetIndex(1)) } 访问一个api, 返回如下数据: { "data": { "trend": { "fields": [ "min_time", "last_px", "avg_px", "business_amount" ], "600570.SS": [ [ 201501090930, 54.98, 54.98, 28327 ], [ 201501090931, 54.63, 54.829486, 49700 ] ] } } } 需要解析 600570.SS 后的json数据,用了 simplejson包 js, err := simplejson.NewJson([]byte(str)) check(err) arr, _ := js.Get("data").Get("trend").Get("600570.ss").Array() 可是对返回的arr数据,用了18般武艺都解析不了。 arr类型理论是一个interface{}类型,但是里面又包含了四组数据,对于这类json数据,网上文档都没有解析的方法。 反复尝试后,用reflect.type 测试了下,发现系统把arr 认定为[]interface 类型,于是类型断言后,遍历。 这回可以把里面数据分拆开了,系统又把里面的数据判断为 json.Number数据类型。 然后就没有然后了.... 经过这一番摸索,对于空接口、类型断言,json包内部的一些设定有了更深的理解:空接口就是因为它灵活,所以在使用时要经过一系列的判断。 上代码: package main import ( "encoding/json" "fmt" "github.com/bitly/go-simplejson" "io/ioutil" "net/http" //"reflect" "regexp" "strconv" "strings" ) //const blkSize int = 10000 type trend struct { date int64 last_px float32 //最新价 avg_px float32 //平均价 volumn float32 //成交量 } var ( lines []string blksLen []int isGB bool ) func check(err error) { if err != nil { panic(err.Error()) } } func Get(url string) ([]byte, error) { defer func() { if err := recover(); err != nil { fmt.Println(err) } }() resp, err := http.Get(url) check(err) //Println(resp.StatusCode) if resp.StatusCode != 200 { panic("FUCK") } return ioutil.ReadAll(resp.Body) } func strip(src string) string { src = strings.ToLower(src) re, _ := regexp.Compile(`<!doctype.*?>`) src = re.ReplaceAllString(src, "") re, _ = regexp.Compile(`<!--.*?-->`) src = re.ReplaceAllString(src, "") re, _ = regexp.Compile(`<script[\S\s]+?</script>`) src = re.ReplaceAllString(src, "") re, _ = regexp.Compile(`<style[\S\s]+?</style>`) src = re.ReplaceAllString(src, "") re, _ = regexp.Compile(`<.*?>`) src = re.ReplaceAllString(src, "") re, _ = regexp.Compile(`&.{1,5};|&#.{1,5};`) src = re.ReplaceAllString(src, "") src = strings.Replace(src, "\r\n", "\n", -1) src = strings.Replace(src, "\r", "\n", -1) return src } func Do(url string) string { body, err := Get(url) check(err) plainText := strip(string(body)) return plainText } func main() { str := Do("http://xxx:8081/quote/v1/trend?prod_code=600570.SS&fields=last_px,business_amount,avg_px") js, err := simplejson.NewJson([]byte(str)) check(err) arr, _ := js.Get("data").Get("trend").Get("600570.ss").Array() t := len(arr) stockdata := trend{} trends := make([]trend, 0, t) for _, v := range arr { //就在这里i进行类型判断 value, _ := v.([]interface{}) for k, u := range value { x, _ := u.(json.Number) //类型断言 y, _ := strconv.ParseFloat(string(x), 64) //将字符型号转化为float64 //v := reflect.ValueOf(k) //fmt.Println("type:", v.Type()) switch k { case 0: stockdata.date = int64(y) case 1: stockdata.last_px = float32(y) case 2: stockdata.volumn = float32(y) case 3: stockdata.avg_px = float32(y) default: fmt.Println("结构体中不存在此元素") } } trends = append(trends, stockdata) } fmt.Println(trends) }
ACID,指数据库事务正确执行的四个基本要素的缩写。包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。一个支持事务(Transaction)的数据库,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易方的要求。 原子性 编辑 整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。 一致性 编辑 一个事务可以封装状态改变(除非它是一个只读的)。事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。 也就是说:如果事务是并发多个,系统也必须如同串行事务一样操作。其主要特征是保护性和不变性(Preserving an Invariant),以转账案例为例,假设有五个账户,每个账户余额是100元,那么五个账户总额是500元,如果在这个5个账户之间同时发生多个转账,无论并发多少个,比如在A与B账户之间转账5元,在C与D账户之间转账10元,在B与E之间转账15元,五个账户总额也应该还是500元,这就是保护性和不变性 隔离性 编辑 隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。 持久性 编辑 在事务完成以后,该事务对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。 由于一项操作通常会包含许多子操作,而这些子操作可能会因为硬件的损坏或其他因素产生问题,要正确实现ACID并不容易。ACID建议数据库将所有需要更新以及修改的资料一次操作完毕,但实际上并不可行。 目前主要有两种方式实现ACID:第一种是Write ahead logging,也就是日志式的方式(现代数据库均基于这种方式)。第二种是Shadow paging。 相对于WAL(write ahead logging)技术,shadow paging技术实现起来比较简单,消除了写日志记录的开销恢复的速度也快(不需要redo和undo)。shadow paging的缺点就是事务提交时要输出多个块,这使得提交的开销很大,而且以块为单位,很难应用到允许多个事务并发执行的情况——这是它致命的缺点。 WAL 的中心思想是对数据文件 的修改(它们是表和索引的载体)必须是只能发生在这些修改已经 记录了日志之后 -- 也就是说,在日志记录冲刷到永久存储器之后. 如果我们遵循这个过程,那么我们就不需要在每次事务提交的时候 都把数据页冲刷到磁盘,因为我们知道在出现崩溃的情况下, 我们可以用日志来恢复数据库:任何尚未附加到数据页的记录 都将先从日志记录中重做(这叫向前滚动恢复,也叫做 REDO) 然后那些未提交的事务做的修改将被从数据页中删除 (这叫向后滚动恢复 - UNDO)。
经过面试,顺利拿到了百度RD offer,大数据部门。 一面是下午在老校区篮球场打球接到的电话,聊了一点儿,由于有点儿吵,约在了第二天的早上八点,百度RD时间观念超强,一早就是八点整,不差一秒,接到电话后,开始就是问简历上的专业技能几个,重要问了我java、jvm、hadoop等一些问题,着重问了hadoop的底层原理和项目。 发现一般面试都是问你说下你简历上最熟悉自豪的一个项目,那么你就得特备别熟悉,特别是技术原理和细节知识,最好细化到代码层面。因为我面的是大数据方向,我着重说的是hadoop方面的数据处理。细节就不说了,说下问题吧,主要是hadoop的计算框架中的map端和reduce端的理解,还有就是shuffle处理的细节问题,以及搭建好的集群配置下的调优处理,然后就是算法了,二次排序详细过程以及细节问题。不过没有问我基于经典算法的大数据问题,比如大数据问题经典算法(july博客有详解,好像都不问这些了)、bitmap的使用和布隆过滤器的设计等等。 jvm相关问题和java基础等,还有spark的细节问题,下次再写,笔记本快没电了!
几乎每个程序员都知道要“避免重复发明轮子”的道理——尽可能使用那些优秀的第三方框架或库,但当真正进入开发时,我却经常发现他们有时并不知道那些轮子在哪里。最近,我在业余时间带几个年轻的程序员一起做了一个很小的商业项目,而在一起开发的过程中,我几乎在所有需要判断字符串是否为空的地方,看到了下面的代码: if(inputString == null || inputString.length == 0){......} 除了字符串判断是否为空之外,还有很多字符串处理或其他数据类型判断的方法,缺少经验的程序员们往往都会想办法自己来写。这些代码当然都没有错,但我们应该尽可能去利用那些已经非常成熟的第三方库,以更标准的方式去解决这些通用的问题,并且提高开发效率。 下面便是我整理的,在大部分项目中使用到的优秀JAVA第三方库 ,供大家参考: JAVA核心扩展 正如前面说到的字符串判断的例子,JAVA的标准库虽然提供了那些最基本的数据类型操作方法,但仍然对一些常见的需求场景,缺少实用的工具类。而另一些则是JAVA标准库本身不够完善,需要第三方库去加以补充的。 Apache Commons Lang Apache Commons Lang是Apache最著名的JAVA库 (GitHub上的代码库),它是对java.lang的很好扩展,包含了大量非常实用的工具类,其中用的最多的有StringUtils,DateUtils,NumberUtils等。之前提到的代码利用StringUtils可以改写为: if(StringUtils.isBlank(inputString)){...} 在Maven项目中加入Apache Commons Lang这个库 <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency> Google Guava Google Guava在国内项目中很少使用,但我合作过的一些国外JAVA工程师几乎都会推荐这个JAVA库。它包含了Google在自己的JAVA项目中所使用的一些核心JAVA库。包含了对:集合,缓存,并发库,字符串处理, I/O等各个方面的支持。另外Google开发的库总是以性能著称。 添加下面的引用,在Maven项目中加入这个库 <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </dependency> Joda-Time Java SE 8之前的JAVA版本中对日期的支持是比较差的,Joda-Time被经常被使用来替换原有的日期系统,它能够支持更多的日历体系,并提供了很多非常方便的日期处理方法,而且它的性能也是非常出色的。(GitHub上的代码库) 添加下面的引用,在Maven项目中加入这个库 <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9.3</version> </dependency> Web框架 Web框架是一个应用最核心的部分,因此我总是推荐使用那些最标准的,并且有良好社区支持的框架,比如Spring和Struts。 Spring Spring是一个开源的应用框架,它包含很多子项目比如Spring MVC, Spring Security, Spring Data,Sping Boot等等,几乎可以满足你项目上的所有需要。它也是我开发Web项目的首选后端框架。(GitHub上的代码库) 添加下面的引用,在Spring MVC项目中加入这个库(以下仅引入Spring Core的支持) <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.2.5.RELEASE</version> </dependency> Struts 2 Struts 2 是Apache最有名的Web框架,它也是一个免费开源的MVC框架。Struts也能很好地支持REST,SOAP,AJAX等最新技术。(GitHub上的代码库) 添加下面的引用,在Maven项目中加入这个库 <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>2.3.28</version> </dependency> 除了上面提到的两个最长哟你的Web框架之外,还有如Google Web Toolkit, Tapestry, Strips等一些优秀的框架可供选择 。 数据库(持久层) 持久层框架的选择对一个项目的成败同样非常关键,它会直接影响到系统的性能、质量、安全以及稳定性。 MyBatis MyBatis是我最喜欢的数据库(持久层)框架,因为它完全是基于SQL语句的(通过SQL来提取数据并自动映射为所需的数据对象),能够为我带来足够的灵活性。(GitHub上的代码库) 添加下面的引用,在Maven项目中加入这个库(如需配合Spring使用,可选择对应的Maven库) <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.0</version> </dependency> Spring JDBC / Spring Data Spring JDBC并不是独立的Spring子项目,而是一个整合在Spring核心库内,为JDBC操作提供基本封装处理的模块。通过简单的配置后,可以通过对Context中的jdbcTemplate进行调用来获得结果。 String SQL = "select name from Student where id = ?"; String name = jdbcTemplateObject.queryForObject(SQL, new Object[]{10}, String.class); Spring Data是Spring的一个子项目,提供了更加强大的持久层功能封装,和对象映射功能。它能与Spring MVC很好地整合。你可以利用JPA和CrudRepository来极大简化持久层的开发。 public interface EmployeeRepository extends CrudRepository<Employee, Long> { Employee findByFirstName(String firstName); List<Employee> findByLastName(String lastName); } Hibernate 可能是国内用得最广泛的持久层框架了,它非常强大,但用好它并不容易,你需要了解它的内部机制,否则可能会出现一些无法预见的性能问题,特别是在数据量特别大的时候。(GitHub上的代码库) 添加下面的引用,在Maven项目中加入这个库 <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.1.0.Final</version> </dependency> 除了上面一些最常用的持久层库,还有几个优秀的库,比如 JDO,JOOQ,Apache DbUtils等。 日志 JAVA中也包含了日志记录功能,但它在处理日志分级,日志的存储,以及日志的备份、归档方面都不够出色,因此在项目中我们一般都会使用第三方日志库来处理日志。 SLF4J- Simple Logging Facade for Java (SLF4J) SLF4J为我们提供了一个日志服务的抽象层,基于它你可以选择不同的日志实现,比如:java.util.logging,logback,log4j,当你需要改变日志实现组件时,不需要修改任何代码,只需要更改一些相应的配置就可以了。(GitHub上的代码库) 添加下面的引用,在Maven项目中加入这个库 <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.21</version> </dependency> Apache Log4j Log4j是最有名的日志组件,通过简单的配置后就能在程序中方便地记录各个级别的日志,它的日志文件能够根据不同的规则进行命名以及归档。 添加下面的引用,在Maven项目中加入这个库 <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.5</version> </dependency> Logback Logback比Log4j更新,它被视为是log4j的一个替代者。它比log4j 的性能更好(log4j 2的性能可能比logback更好),而且更完整地实现了SLF4J的接口,并且自带了更多的功能,比如自动压缩日志,更多的filter等。 添加下面的引用,在Maven项目中加入这个库 <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.1.7</version> </dependency> JSON JSON已经成为最广泛使用的一种数据传输格式,因此程序中对JSON的处理也正变得越来越多。下面是我推荐的一些JSON处理库: Jackson Jackson是一个多用途的Java库,用于处理JSON数据。使用它可以很方便地在JSON数据和Java对象之间进行转换。(GitHub上的代码库) ObjectMapper mapper = new ObjectMapper(); // can reuse, share globally User user = mapper.readValue(new File("user.json"), User.class); Google Gson Google开发的JSON库,可以实现JSON字符串与JAVA对象之间的转换,使用起来也非常方便。 Gson gson = new Gson(); String[] strings = {"abc", "def", "ghi"}; gson.toJson(strings); // ==> ["abc", "def", "ghi"] 图表 JFreeChart 能够为你生成各种类型的图表,并且支持多种输出格式,包括PNG和JPEG图片格式,以及向PDF,EPS,SVG等矢量图。 JasperReports JasperReports提供了一套完整的报表解决方案,帮助用户使用用Java语言来开发具有报告功能的程序。JasperReports的模版采用XML格式,从数据库中抽取数据,并以PDF、HTML、XLS、CSV及XML等各种格式生成报表。它的一大优势是能够处理大数据量的报表。 测试 JUnit JUnit是目前使用最广泛的JAVA单元测试库通过它,你可以i非常方便地编写自己的单元测试代码,并进行自动化测试。(GitHub上的代码库) @Test public void lookupEmailAddresses() { assertThat(new CartoonCharacterEmailLookupService().getResults("looney"), allOf( not(empty()), containsInAnyOrder( allOf(instanceOf(Map.class), hasEntry("id", "56"), hasEntry("email", "roadrunner@fast.org")), allOf(instanceOf(Map.class), hasEntry("id", "76"), hasEntry("email", "wiley@acme.com")) ) )); } 添加下面的引用,在Maven项目中加入这个库 <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> Office文档处理 Apache POI Apache POI是一个免费的开源库用于处理Microsoft Office文档。用它可以使用Java读取和创建,修改MS Excel文件,MS Word和MSPowerPoint文件。(GitHub上的代码库) 添加下面的引用,在Maven项目中加入这个库 <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.14</version> </dependency> docx4j docx4j是另一套基于JAXB的Office文档(docx,pptx,xlsx)处理库。(GitHub上的代码库) 添加下面的引用,在Maven项目中加入这个库 <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j</artifactId> <version>3.3.0</version> </dependency> XML解析 JDOM JDOM是一个开源项目,它基于树型结构,利用纯JAVA的技术对XML文档实现解析、生成、序列化以及多种操作。在 JDOM 中,XML 元素用 Element 表示,XML 属性用 Attribute 表示,XML 文档本身用 Document 表示。因此这些API都非常直观易用。(GitHub上的代码库) 添加下面的引用,在Maven项目中加入这个库 <dependency> <groupId>org.jdom</groupId> <artifactId>jdom</artifactId> <version>2.0.2</version> </dependency> DOM4J DOM4J是一个处理XML的开源框架,它整合了对于XPath,并且完全支持DOM,SAX,JAXP等技术。 添加下面的引用,在Maven项目中加入这个库 <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency> xerces Xerces是一个开放源代码的XML语法分析器。从JDK1.5以后,Xerces就成了JDK的XML默认实现 添加下面的引用,在Maven项目中加入这个库 <dependency> <groupId>xerces</groupId> <artifactId>xercesImpl</artifactId> <version>2.11.0</version> </dependency> 其他值得关注的代码库 jSOUP jSOUP提供了一套与外部互联网的网页(HTML)进行交互的API,能够让使用者非常方便地 利用CSS选择器来解析HTML页面,从而获取需要的内容。 Document doc = Jsoup.connect("http://en.wikipedia.org/").get(); Elements newsHeadlines = doc.select("#mp-itn b a"); Lomobok Lombok 是一种 Java 实用工具,可用来帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO)。它通过注释实现这一目的。通过在IDE中加入Lombok,开发人员可以节省构建诸如 hashCode()和equals()这样的方法以及以往用来分类各种 accessor 和 mutator 的大量时间。 Netty Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。也就是说,Netty 是一个基于NIO的客户,服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。 如果你发现有其他优秀的第三方库,也请在下面的评论中分享,我会继续完善这份清单,使它更具参考价值^_^。
2.0之后ES的java api用法有了很大变化。在此记录一些。 java应用程序连接ES集群,笔者使用的是TransportClient,获取TransportClient的代码设计为单例模式(见getClient方法)。同时包含了设置自动提交文档的代码。注释比较详细,不再赘述。 下方另有提交文档、提交搜索请求的代码。 1、连接ES集群代码如下: 1 package elasticsearch; 2 3 import com.vividsolutions.jts.geom.GeometryFactory; 4 import com.vividsolutions.jts.geom.MultiPolygon; 5 import com.vividsolutions.jts.geom.Polygon; 6 import com.vividsolutions.jts.io.ParseException; 7 import com.vividsolutions.jts.io.WKTReader; 8 import org.apache.commons.logging.Log; 9 import org.apache.commons.logging.LogFactory; 10 import org.elasticsearch.action.bulk.BulkProcessor; 11 import org.elasticsearch.action.bulk.BulkRequest; 12 import org.elasticsearch.action.bulk.BulkResponse; 13 import org.elasticsearch.client.transport.TransportClient; 14 import org.elasticsearch.common.settings.Settings; 15 import org.elasticsearch.common.transport.InetSocketTransportAddress; 16 import org.elasticsearch.common.unit.ByteSizeUnit; 17 import org.elasticsearch.common.unit.ByteSizeValue; 18 import org.elasticsearch.common.unit.TimeValue; 19 20 import java.net.InetAddress; 21 import java.util.Date; 22 23 /** 24 * Created by ZhangDong on 2015/12/25. 25 */ 26 public class EsClient { 27 static Log log = LogFactory.getLog(EsClient.class); 28 29 // 用于提供单例的TransportClient BulkProcessor 30 static public TransportClient tclient = null; 31 static BulkProcessor staticBulkProcessor = null; 32 33 //【获取TransportClient 的方法】 34 public static TransportClient getClient() { 35 try { 36 if (tclient == null) { 37 String EsHosts = "10.10.2.1:9300,10.10.2.2:9300"; 38 Settings settings = Settings.settingsBuilder() 39 .put("cluster.name", "wshare_es")//设置集群名称 40 .put("tclient.transport.sniff", true).build();//自动嗅探整个集群的状态,把集群中其它机器的ip地址加到客户端中 41 42 tclient = TransportClient.builder().settings(settings).build(); 43 String[] nodes = EsHosts.split(","); 44 for (String node : nodes) { 45 if (node.length() > 0) {//跳过为空的node(当开头、结尾有逗号或多个连续逗号时会出现空node) 46 String[] hostPort = node.split(":"); 47 tclient.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(hostPort[0]), Integer.parseInt(hostPort[1]))); 48 49 } 50 } 51 }//if 52 } catch (Exception e) { 53 e.printStackTrace(); 54 } 55 return tclient; 56 } 57 //【设置自动提交文档】 58 public static BulkProcessor getBulkProcessor() { 59 //自动批量提交方式 60 if (staticBulkProcessor == null) { 61 try { 62 staticBulkProcessor = BulkProcessor.builder(getClient(), 63 new BulkProcessor.Listener() { 64 @Override 65 public void beforeBulk(long executionId, BulkRequest request) { 66 //提交前调用 67 // System.out.println(new Date().toString() + " before"); 68 } 69 70 @Override 71 public void afterBulk(long executionId, BulkRequest request, BulkResponse response) { 72 //提交结束后调用(无论成功或失败) 73 // System.out.println(new Date().toString() + " response.hasFailures=" + response.hasFailures()); 74 log.info( "提交" + response.getItems().length + "个文档,用时" 75 + response.getTookInMillis() + "MS" + (response.hasFailures() ? " 有文档提交失败!" : "")); 76 // response.hasFailures();//是否有提交失败 77 } 78 79 @Override 80 public void afterBulk(long executionId, BulkRequest request, Throwable failure) { 81 //提交结束且失败时调用 82 log.error( " 有文档提交失败!after failure=" + failure); 83 } 84 }) 85 86 .setBulkActions(1000)//文档数量达到1000时提交 87 .setBulkSize(new ByteSizeValue(5, ByteSizeUnit.MB))//总文档体积达到5MB时提交 // 88 .setFlushInterval(TimeValue.timeValueSeconds(5))//每5S提交一次(无论文档数量、体积是否达到阈值) 89 .setConcurrentRequests(1)//加1后为可并行的提交请求数,即设为0代表只可1个请求并行,设为1为2个并行 90 .build(); 91 // staticBulkProcessor.awaitClose(10, TimeUnit.MINUTES);//关闭,如有未提交完成的文档则等待完成,最多等待10分钟 92 } catch (Exception e) {//关闭时抛出异常 93 e.printStackTrace(); 94 } 95 }//if 96 97 98 99 100 101 return staticBulkProcessor; 102 } 103 } 2、插入文档的代码(自动批量提交方式,注释中另有手动批量提交、单个文档提交的方式): 1 package elasticsearch; 2 3 import org.apache.commons.logging.Log; 4 import org.apache.commons.logging.LogFactory; 5 import org.elasticsearch.action.index.IndexRequest; 6 7 8 /** 9 * Created by ZhangDong on 2015/12/25. 10 */ 11 public class EsInsert2 { 12 static Log log = LogFactory.getLog(EsInsert2.class); 13 public static void add(String json) { 14 try { //EsClient.getBulkProcessor()是位于上方EsClient类中的方法 15 EsClient.getBulkProcessor().add(new IndexRequest("设置的index name", "设置的type name","要插入的文档的ID").source(json));//添加文档,以便自动提交 16 } catch (Exception e) { 17 log.error("add文档时出现异常:e=" + e + " json=" + json); 18 } 19 } 20 } 21 //手动 批量更新 22 // BulkRequestBuilder bulkRequest = tclient.prepareBulk(); 23 // for(int i=500;i<1000;i++){ 24 // //业务对象 25 // String json = ""; 26 // IndexRequestBuilder indexRequest = tclient.prepareIndex("twitter", "tweet") 27 // //指定不重复的ID 28 // .setSource(json).setId(String.valueOf(i)); 29 // //添加到builder中 30 // bulkRequest.add(indexRequest); 31 // } 32 // 33 // BulkResponse bulkResponse = bulkRequest.execute().actionGet(); 34 // if (bulkResponse.hasFailures()) { 35 // // process failures by iterating through each bulk response item 36 // System.out.println(bulkResponse.buildFailureMessage()); 37 // } 38 39 //单个文档提交 40 // String json = "{\"relationship\":{},\"tags\":[\"camera\",\"video\"]}"; 41 // IndexResponse response = getClient().prepareIndex("dots", "scan", JSON.parseObject(json).getString("rid")).setSource(json).get(); 42 // return response.toString(); 3、进行搜索的代码,其中有适用于复杂搜索逻辑的BoolQuery用法,以及关键词高亮的配置、在某个字段精确搜索、全文搜索、匹配全部文档、搜索同时返回聚类信息的用法: 1 package service; 2 3 import elasticsearch.EsClient; 4 import org.apache.commons.logging.Log; 5 import org.apache.commons.logging.LogFactory; 6 import org.elasticsearch.action.search.SearchRequestBuilder; 7 import org.elasticsearch.action.search.SearchResponse; 8 import org.elasticsearch.index.query.*; 9 import org.elasticsearch.search.aggregations.AggregationBuilders; 10 import org.springframework.stereotype.Service; 11 12 /** 13 * Created by ZhangDong on 2016/1/5. 14 */ 15 @Service 16 public class SearchService2 { 17 18 Log log = LogFactory.getLog(getClass()); 19 public SearchResponse getSimpleSearchResponse( int page, int pagesize){ 20 21 BoolQueryBuilder mustQuery = QueryBuilders.boolQuery(); 22 mustQuery.must(QueryBuilders.matchAllQuery()); // 添加第1条must的条件 此处为匹配所有文档 23 24 mustQuery.must(QueryBuilders.matchPhraseQuery("title", "时间简史"));//添加第2条must的条件 title字段必须为【时间简史】 25 // ↑ 放入筛选条件(termQuery为精确搜索,大小写敏感且不支持*) 实验发现matchPhraseQuery可对中文精确匹配term 26 27 mustQuery.must(QueryBuilders.matchQuery("auther", "霍金")); // 添加第3条must的条件 28 29 QueryBuilder queryBuilder = QueryBuilders.queryStringQuery("物理")//.escape(true)//escape 转义 设为true,避免搜索[]、结尾为!的关键词时异常 但无法搜索* 30 .defaultOperator(QueryStringQueryBuilder.Operator.AND);//不同关键词之间使用and关系 31 mustQuery.must(queryBuilder);//添加第4条must的条件 关键词全文搜索筛选条件 32 33 SearchRequestBuilder searchRequestBuilder = EsClient.getClient().prepareSearch("index name").setTypes("type name") 34 .setQuery(mustQuery) 35 .addHighlightedField("*")/*星号表示在所有字段都高亮*/.setHighlighterRequireFieldMatch(false)//配置高亮显示搜索结果 36 .setHighlighterPreTags("<高亮前缀标签>").setHighlighterPostTags("<高亮后缀标签>");//配置高亮显示搜索结果 37 38 searchRequestBuilder = searchRequestBuilder.addAggregation(AggregationBuilders.terms("agg1(聚类返回时根据此key获取聚类结果)") 39 .size(1000)/*返回1000条聚类结果*/.field("要在文档中聚类的字段,如果是嵌套的则用点连接父子字段,如【person.company.name】")); 40 41 SearchResponse searchResponse = searchRequestBuilder.setFrom((page - 1) * pagesize)//分页起始位置(跳过开始的n个) 42 .setSize(pagesize)//本次返回的文档数量 43 .execute().actionGet();//执行搜索 44 45 log.info("response="+searchResponse); 46 return searchResponse; 47 } 48 } 4、ES中使用delete-by-query插件,DSL方式按条件删除数据的方法: ES2.1中,默认的文档删除方式只有按ID删除方法: curl -XDELETE 'localhost:9200/customer/external/2?pretty' (参考:Deleting Documents | Elasticsearch Reference [2.1] | Elastic https://www.elastic.co/guide/en/elasticsearch/reference/2.1/_deleting_documents.html) 按条件删除需要安装delete-by-query插件,在线安装方式可使用命令 plugin install delete-by-query 随后会从https://download.elastic.co/elasticsearch/release/org/elasticsearch/plugin/delete-by-query/2.1.0/delete-by-query-2.1.0.zip处下载插件安装包。但是本人使用的某个ES环境是离线的,需要手动下载上述URL对应的ZIP,放置于elasticsearch-2.1.0文件夹下,与bin、config等文件夹同级,同时还要下载 https://download.elastic.co/elasticsearch/release/org/elasticsearch/plugin/delete-by-query/2.1.0/delete-by-query-2.1.0.zip.md5 校验文件放于同一位置(XXX.sha1应该也可以),使用以下命令离线安装: bin/plugin install file:delete-by-query-2.1.0.zip 其中delete-by-query-2.1.0.zip是相对路径,绝对路径应该也可以,随后便安装成功了。 安装成功后查看,发现其实就是解压delete-by-query-2.1.0.zip的内容放置于elasticsearch-2.1.0/plugins/delete-by-query 文件夹下,猜测手动解压也可以使用。 注意:如果是ES集群,需要对每个节点都安装这个插件,而且每个节点安装后要重启ES。 使用DSL方式按条件删除文档的方法: DELETE方式,请求 http://localhost:9200/index_name/type_name/_query http payload内容: { "query":{ "match_all":{} } } 上述query为匹配全部文档。
Ubuntu下Elasticsearch 2.1集群部署过程与遇到的问题及解决方法(开机自启动、root用户启动) SEO:ES 2.0 2.1 Elastic Elasticsearch Linux Ubuntu root start stop 开机启动 开机自启动 安装 部署 使用 脑裂 无法 不能 发现 集群 节点 (本文适合有一定Linux基础的读者阅读。由于几乎是按流水账过程记录,而不是教程,建议操作之前读完一遍) 本人在部署ES2.1集群时,遇到了诸多问题,花了很大功夫,才解决,特在此记录解决过程及方法,希望帮到有需要的人。 据使用者的讨论,ES在2.0版本之后发生了很大变化,因此网上的一些资料已经不再适用,在查阅资料时需要根据版本判断,灵活变通方法。 操作系统:Ubuntu 14.04.1 64位 jdk:1.8.0_20 64位 Elasticsearch 2.1 ES部署步骤:(1)下载目前(2015年12月18日)最新版的ES 2.1的tar.gz包,解压,放至wsn用户目录,修改config\elasticsearch.yml配置文件,添加(或对已有的但被注释掉的语句,取消注释并修改)以下语句: cluster.name: groupname(设定的集群名称) node.name: node_10.10.2.145(设定的当前节点名称) network.host: 10.10.2.145(可以理解为监听/绑定的IP,本人设置为本机在局域网中的IP,尝试过如果设置127.0.0.1,之后在本机可以访问ES但在局域网中不能访问) index.number_of_shards: 9(分片数量) http.max_content_length: 2000mb(使用中发现,如果使用默认值,使用curl post插入300M以上的txt时会报错) http.compression: true 【注意】每行语句前不能有空格,冒号后必须有一个空格,否则可能启动报错 【注意】可使用 bin/elasticsearch 启动,此时不是服务形式。root用户不能以此方式启动,否则会报错其中node.name network.host需要根据每台机器的IP等修改(本人是在一台服务器部署好之后将该服务器复制为3台,复制后需要根据机器修改部分配置) (2)(也可以不进行此步骤,详见下)将elasticsearch添加入系统服务service/etc/init.d/ 下建立软连接,操作步骤为: 使用cd命令进入/etc/init.d/ ,然后 ln -s /home/wsn/wshare/es/elasticsearch-2.1.0/bin/elasticsearch elasticsearch sudo update-rc.d elasticsearch defaults 95 10 //按照官方说法,这是适用于Ubuntu的方法,猜测为加入服务,参考Running as a Service on Linux https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-service.html#using-systemd 加入服务后,经过测试,仍然无法使用start stop等命令,报错(如下图): 需使用service elasticsearch 启动服务,与直接使用bin/elasticsearch效果相同。 因此,本人决定不使用将其加入服务的方法(加入服务除了启动时输命令方便,别的好像没什么卵用),采用手动配置启动脚本与开机自启动方式,因此加入服务部分的操作(2)可不进行。 (3)配置开机自启动 【2016-5-26更新:发现root用户也可以启动ElasticSearch,方法是在启动时添加Des.insecure.allow.root参数,如下: bin/elasticsearch -Des.insecure.allow.root=true(参考 How to run Elasticsearch 2.1.1 as root user in Linux machine - Stack Overflow http://stackoverflow.com/questions/34920801/how-to-run-elasticsearch-2-1-1-as-root-user-in-linux-machine)】 本人使用的Ubuntu中,语句放入/etc/init.d/rc.local文件中,即可在开机时运行。/etc/init.d/rc.local 文件,最后一行后加入: sh /home/wsn/wshare/auto_start.sh 便能够开机启动auto_start.sh 在/home/wsn/wshare/auto_start.sh中写入: sh /home/wsn/wshare/es/elasticsearch-2.1.0/start_es.sh 便能够开机时执行start_es.sh。 start_es.sh是启动Elasticsearch并后台运行的脚本,内容为: su -c “/home/wsn/wshare/es/elasticsearch-2.1.0/bin/elasticsearch &” – wsn # ↑ 临时切换为wsn用户执行-c后的命令,执行后切换为原用户 (wsn用户是Ubuntu系统中建立的另一个用户) 即临时切换为wsn用户并执行”-c”后的命令语句,因此root用户可使用此脚本启动ES(注意第一行后可能需要添加空格,否则有报错)。 通过以上操作,部署ES和开机自启动便完成了。同时,root用户也可使用如上的start_es.sh脚本启动ES,一定程度地避免了root不能直接启动ES带来的不便。 停止ES使用查找进程并kill的方法(ps -ef|grep elastic),尚未找到更好的方法。但使用kill进程方法停止时,ES能感知到并写入ES关闭的日志,因此推测kill进程时ES可以正常退出而非被强制结束。 (4)其它注意事项①期间遇到的问题:开机启动无法成功,并且看不到脚本执行过程的输出,因此将输出重定向到/tmp/debug3.log(tmp目录下不易产生权限问题),即/etc/init.d/rc.local中加入的语句改为 sh /home/wsn/wshare/auto_start.sh < /tmp/debug3.log 之后,如果开机时执行上述命令有报错,便可以在/tmp/debug3.log文件中看到运行过程的输出。 ②另外,jdk配置需要位于/etc/profile文件中,对所有用户都生效。配置方法为,在/etc/profile文件末尾添加 export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_20 export JRE_HOME=${JAVA_HOME}/jre export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib export PATH=${JAVA_HOME}/bin:$PATH (具体的JAVA_HOME路径需要按实际情况填写) 如果配置jdk的方法是将上述语句添加到针对某个用户的~/.bashrc(使用vim ~/.bashrc编辑添加),则ES无法开机启动,错误为启动时没有JAVA环境变量。 ③ES启动时有关于log4j的日志文件权限报错,因此将logs文件夹及其子目录权限设置为777(chmod -R 777 logs 针对所有人都可读可写可执行,是个懒方法),启动时不再有报错。 ④一定程度地防止脑裂(无法发现部分节点)问题,因此ES配置文件中加入: discovery.zen.minimum_master_nodes: 2 discovery.zen.ping_timeout: 10s 即最少需要2个节点才会选举master节点(即产生集群)。在配置文件的注释中看到官方的建议是,数字设置为【节点个数/2+1】,向上取整,本人的是3个节点,因此设置为2; 发现集群的超时时间为10s。 配置启用多播discovery.zen.ping.multicast.enabled: true,但无效,无法发现节点,因此使用单播,添加配置 discovery.zen.ping.unicast.hosts: [“10.10.2.143″, “10.10.2.144”, “10.10.2.145”, “10.10.2.10”] 如此配置之后,该节点便会访问列表中的几个IP,找到这几个IP的机器中的节点,组成集群。 (后查证为ES2.0之后版本已删除了多播功能,如果使用需要安装multicast插件) 本人使用ES也是新手,只是总结了一些自己的经验,文中如有错误、不准确、遗漏之处,希望各位不吝赐教指出,或者解答本人遇到的一些疑虑,以及共同讨论探讨,谢谢! 转载时请注明出处: Ubuntu下Elasticsearch 2.1集群部署过程与遇到的问题及解决方法(开机自启动、root用户启动) - 张冬 - 博客园
返回博客列表 转 关于施用full gc频繁的分析及解决 DEC_LIU 发布时间: 2013/10/13 20:32 阅读: 3431 收藏: 14 点赞: 1 评论: 1 关于应用full gc频繁的分析及解决 很久前的工作日记了,移到ITeye上来。 现象 系统报警full gc次数过多,每2分钟达到了5~6次,这是不正常的现象 在full gc报警时的gc.log如下: 在full gc报警时的jstat如下: sudo -u admin -H /opt/taobao/java/bin/jstat -gcutil `pgrep java` 2000 100 此时的cpu如下(基本都是在做gc): 将应用重启后,问题解决 但是当后台执行低价航线更新时,过大概十几个小时后,又出现上述情况! 分析 当频繁full gc时,jstack打印出堆栈信息如下: sudo -u admin -H /opt/taobao/java/bin/jstack `pgrep java` > #your file path# 可以看到的确是在跑低价信息 另外在应用频繁full gc时和应用正常时,也执行了如下2种命令: sudo -u admin -H /opt/taobao/java/bin/jmap -histo `pgrep` > #your file path# sudo -u admin -H /opt/taobao/java/bin/jmap -histo:live `pgrep` > #your file path#(live会产生full gc) 目的是确认以下2种信息: (1)是否存在某些引用的不正常,造成对象始终可达而无法回收(Java中的内存泄漏) (2)是否真是由于在频繁full gc时同时又有大量请求进入分配内存从而处理不过来, 造成concurrent mode failure? 下图是在应用正常情况下,jmap不加live,产生的histo信息: 下图是在应用正常情况下,jmap加live,产生的histo信息: 下图是在应用频繁full gc情况下,jmap不加live和加live,产生的histo信息: 从上述几个图中可以看到: (1)在应用正常情况下,图中标红的对象是被回收的,因此不是内存泄漏问题 (2)在应用频繁full gc时,标红的对象即使加live也是未被回收的,因上就是在频繁full gc时, 同时又有大量请求进入分配内存从而处理不过来的问题 先从解决问题的角度,看怎样造成频繁的full gc? 从分析CMS GC开始 先给个CMS GC的概况: (1)young gc 可以看到,当eden满时,young gc使用的是ParNew收集器 ParNew: 2230361K->129028K(2403008K), 0.2363650 secs解释: 1)2230361K->129028K,指回收前后eden+s1(或s2)大小 2)2403008K,指可用的young代的大小,即eden+s1(或s2) 3)0.2363650 secs,指消耗时间 2324774K->223451K(3975872K), 0.2366810 sec解释: 1)2335109K->140198K,指整个堆大小的变化 (heap=(young+old)+perm;young=eden+s1+s2;s1=s2=young/(survivor ratio+2)) 2)0.2366810 sec,指消耗时间 [Times: user=0.60 sys=0.02, real=0.24 secs]解释:指用户时间,系统时间,真实时间 (2)cms gc 当使用CMS收集器时,当开始进行收集时,old代的收集过程如下所示: a)首先jvm根据-XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingOccupancyOnly 来决定什么时间开始垃圾收集 b)如果设置了-XX:+UseCMSInitiatingOccupancyOnly,那么只有当old代占用确实达到了 -XX:CMSInitiatingOccupancyFraction参数所设定的比例时才会触发cms gc c)如果没有设置-XX:+UseCMSInitiatingOccupancyOnly,那么系统会根据统计数据自行决定什么时候 触发cms gc;因此有时会遇到设置了80%比例才cms gc,但是50%时就已经触发了,就是因为这个参数 没有设置的原因 d)当cms gc开始时,首先的阶段是CMS-initial-mark,此阶段是初始标记阶段,是stop the world阶段, 因此此阶段标记的对象只是从root集最直接可达的对象 CMS-initial-mark:961330K(1572864K),指标记时,old代的已用空间和总空间 e)下一个阶段是CMS-concurrent-mark,此阶段是和应用线程并发执行的,所谓并发收集器指的就是这个, 主要作用是标记可达的对象 此阶段会打印2条日志:CMS-concurrent-mark-start,CMS-concurrent-mark f)下一个阶段是CMS-concurrent-preclean,此阶段主要是进行一些预清理,因为标记和应用线程是并发执行的, 因此会有些对象的状态在标记后会改变,此阶段正是解决这个问题 因为之后的Rescan阶段也会stop the world,为了使暂停的时间尽可能的小,也需要preclean阶段先做一部分 工作以节省时间 此阶段会打印2条日志:CMS-concurrent-preclean-start,CMS-concurrent-preclean g)下一阶段是CMS-concurrent-abortable-preclean阶段,加入此阶段的目的是使cms gc更加可控一些, 作用也是执行一些预清理,以减少Rescan阶段造成应用暂停的时间 此阶段涉及几个参数: -XX:CMSMaxAbortablePrecleanTime:当abortable-preclean阶段执行达到这个时间时才会结束 -XX:CMSScheduleRemarkEdenSizeThreshold(默认2m):控制abortable-preclean阶段什么时候开始执行, 即当eden使用达到此值时,才会开始abortable-preclean阶段 -XX:CMSScheduleRemarkEdenPenetratio(默认50%):控制abortable-preclean阶段什么时候结束执行 此阶段会打印一些日志如下: CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean, CMS:abort preclean due to time XXX h)再下一个阶段是第二个stop the world阶段了,即Rescan阶段,此阶段暂停应用线程,对对象进行重新扫描并 标记 YG occupancy:964861K(2403008K),指执行时young代的情况 CMS remark:961330K(1572864K),指执行时old代的情况 此外,还打印出了弱引用处理、类卸载等过程的耗时 i)再下一个阶段是CMS-concurrent-sweep,进行并发的垃圾清理 j)最后是CMS-concurrent-reset,为下一次cms gc重置相关数据结构 (3)full gc: 有2种情况会触发full gc,在full gc时,整个应用会暂停 a)concurrent-mode-failure:当cms gc正进行时,此时有新的对象要进行old代,但是old代空间不足造成的 b)promotion-failed:当进行young gc时,有部分young代对象仍然可用,但是S1或S2放不下, 因此需要放到old代,但此时old代空间无法容纳此 频繁full gc的原因 从日志中可以看出有大量的concurrent-mode-failure,因此正是当cms gc进行时,有新的对象要进行old代, 但是old代空间不足造成的full gc 进程的jvm参数如下所示: 影响cms gc时长及触发的参数是以下2个: -XX:CMSMaxAbortablePrecleanTime=5000 -XX:CMSInitiatingOccupancyFraction=80 解决也是针对这两个参数来的 根本的原因是每次请求消耗的内存量过大 解决 (1)针对cms gc的触发阶段,调整-XX:CMSInitiatingOccupancyFraction=50,提早触发cms gc,就可以 缓解当old代达到80%,cms gc处理不完,从而造成concurrent mode failure引发full gc (2)修改-XX:CMSMaxAbortablePrecleanTime=500,缩小CMS-concurrent-abortable-preclean阶段 的时间 (3)考虑到cms gc时不会进行compact,因此加入-XX:+UseCMSCompactAtFullCollection (cms gc后会进行内存的compact)和-XX:CMSFullGCsBeforeCompaction=4 (在full gc4次后会进行compact)参数 但是运行了一段时间后,只不过时间更长了,又会出现频繁full gc 计算了一下heap各个代的大小(可以用jmap -heap查看): total heap=young+old=4096m perm:256m young=s1+s2+eden=2560m young avail=eden+s1=2133.375+213.3125=2346.6875m s1=2560/(10+1+1)=213.3125m s2=s1 eden=2133.375m old=1536m 可以看到eden大于old,在极端情况下(young区的所有对象全都要进入到old时,就会触发full gc), 因此在应用频繁full gc时,很有可能old代是不够用的,因此想到将old代加大,young代减小 改成以下: -Xmn1920m 新的各代大小: total heap=young+old=4096m perm:256m young=s1+s2+eden=1920m young avail=eden+s1=2133.375+213.3125=1760m s1=1760/(10+1+1)=160m s2=s1 eden=1600m old=2176m 此时的eden小于old,可以缓解一些问题
Java垃圾回收概况 Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代 码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对 JVM(Java Virtual Machine)中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,放置出现内存泄露和溢出问题。 关于JVM,需要说明一下的是,目前使用最多的Sun公司的JDK中,自从 1999年的JDK1.2开始直至现在仍在广泛使用的JDK6,其中默认的虚拟机都是HotSpot。2009年,Oracle收购Sun,加上之前收购 的EBA公司,Oracle拥有3大虚拟机中的两个:JRockit和HotSpot,Oracle也表明了想要整合两大虚拟机的意图,但是目前在新发布 的JDK7中,默认的虚拟机仍然是HotSpot,因此本文中默认介绍的虚拟机都是HotSpot,相关机制也主要是指HotSpot的GC机制。 Java GC机制主要完成3件事:确定哪些内存需要回收,确定什么时候需要执行GC,如何执行GC。经过这么长时间的发展(事实上,在Java语言出现之前,就有 GC机制的存在,如Lisp语言),Java GC机制已经日臻完善,几乎可以自动的为我们做绝大多数的事情。然而,如果我们从事较大型的应用软件开发,曾经出现过内存优化的需求,就必定要研究 Java GC机制。 学习Java GC机制,可以帮助我们在日常工作中排查各种内存溢出或泄露问题,解决性能瓶颈,达到更高的并发量,写出更高效的程序。 我们将从4个方面学习Java GC机制,1,内存是如何分配的;2,如何保证内存不被错误回收(即:哪些内存需要回收);3,在什么情况下执行GC以及执行GC的方式;4,如何监控和优化GC机制。 Java内存区域 了解Java GC机制,必须先清楚在JVM中内存区域的划分。在Java运行时的数据区里,由JVM管理的内存区域分为下图几个模块: 其中: 1,程序计数器(Program Counter Register):程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。 每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。 如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写 完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区 域中唯一一个没有定义OutOfMemoryError的区域。 2,虚拟机栈(JVM Stack):一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。 局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占 用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定 好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。 虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多 数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,知道内存不足,此时,会抛出 OutOfMemoryError(内存溢出)。 每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。 3,本地方法栈(Native Method Statck):本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。 本地方法栈也是线程私有的。 4,堆区(Heap):堆区是理解Java GC机制最重要的区域,没有之一。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。 一般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主 流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。 关于堆区的内容还有很多,将在下节“Java内存分配机制”中详细介绍。 5,方法区(Method Area):在Java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实 上,方法区并不是堆(Non-Heap);另外,不少人的博客中,将Java GC的分代收集机制分为3个代:青年代,老年代,永久代,这些作者将方法区定义为“永久代”,这是因为,对于之前的HotSpot Java虚拟机的实现方式中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。不过,除HotSpot之外的多数虚拟机,并不将方法区当做永 久代,HotSpot本身,也计划取消永久代。本文中,由于笔者主要使用Oracle JDK6.0,因此仍将使用永久代一词。 方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。 方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。一般的,方法区上 执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(HotSpot),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对 常量池的内存回收和对已加载类的卸载。 在方法区上进行垃圾收集,条件苛刻而且相当困难,效果也不令人满意,所以一般不做太多考虑,可以留作以后进一步深入研究时使用。 在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。 运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。 6,直接内存(Direct Memory):直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是 JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区 (Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。 由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。 Java对象的访问方式 一般来说,一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。 以最简单的本地变量引用:Object obj = new Object()为例: Object obj表示一个本地引用,存储在JVM栈的本地变量表中,表示一个reference类型数据; new Object()作为实例对象数据存储在堆中; 堆中还记录了Object类的类型信息(接口、方法、field、对象类型等)的地址,这些地址所执行的数据存储在方法区中; 在Java虚拟机规范中,对于通过reference类型引用访问具体对象的方式并未做规定,目前主流的实现方式主要有两种: 1,通过句柄访问(图来自于《深入理解Java虚拟机:JVM高级特效与最佳实现》): 通过句柄访问的实现方式中,JVM堆中会专门有一块区域用来作为句柄池,存储相关句柄所执行的实例数据地址(包括在堆中地址和在方法区中的地址)。这种实现方法由于用句柄表示地址,因此十分稳定。 2,通过直接指针访问:(图来自于《深入理解Java虚拟机:JVM高级特效与最佳实现》) 通过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包含了在方法区中的相应类型数据。这种方法最大的优势是速度快,在HotSpot虚拟机中用的就是这种方式。 Java内存分配机制 这里所说的内存分配,主要指的是在堆上的分配,一般的,对象的内存分配都是在堆上进行,但现代技术也支持将对象拆成标量类型(标量类型即原子类型,表示单个值,可以是基本类型或String等),然后在栈上分配,在栈上分配的很少见,我们这里不考虑。 Java内存分配和回收的机制概括的说,就是:分代分配,分代回收。对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。如下图(来源于《成为JavaGC专家part I》,http://www.importnew.com/1993.html): 年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接 被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消 亡的),这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。 年轻代上的内存分配是这样的,年轻代可以分为3个区域:Eden区(伊甸园,亚当和夏娃偷吃禁果生娃娃的地方,用来表示内存首次分配的区域,再 贴切不过)和两个存活区(Survivor 0 、Survivor 1)。内存分配过程为(来源于《成为JavaGC专家part I》,http://www.importnew.com/1993.html): 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快; 当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的); 此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0; 当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。 从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活 着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方 式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下高效,如果在老年代采用停止复制,则挺悲剧的。 在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread- Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对 象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干 段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内 存。 年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次 Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 Full GC。 可以使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。 如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。 可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。 Java GC机制 GC机制的基本算法是:分代收集,这个不用赘述。下面阐述每个分代的收集方法。 年轻代: 事实上,在上一节,已经介绍了新生代的主要垃圾回收方法,在新生代中,使用“停止-复制”算法进行清理,将新生代内存分为2部分,1部分 Eden区较大,1部分Survivor比较小,并被划分为两个等量的部分。每次进行清理时,将Eden区和一个Survivor中仍然存活的对象拷贝到 另一个Survivor中,然后清理掉Eden和刚才的Survivor。 这里也可以发现,停止复制算法中,用来复制的两部分并不总是相等的(传统的停止复制算法两部分内存相等,但新生代中使用1个大的Eden区和2个小的Survivor区来避免这个问题) 由于绝大部分的对象都是短命的,甚至存活不到Survivor中,所以,Eden区与Survivor的比例较大,HotSpot默认是 8:1,即分别占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下来的内存超过了10%,则需要将一部分对象分配到 老年代。用-XX:SurvivorRatio参数来配置Eden区域Survivor区的容量比值,默认是8,代表Eden:Survivor1:Survivor2=8:1:1. 老年代: 老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-整理算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。 在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC,否则,就查看是否设 置了-XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不 允许,则仍然进行Full GC(这代表着如果设置-XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。 方法区(永久代): 永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点: 类的所有实例都已经被回收 加载类的ClassLoader已经被回收 类对象的Class对象没有被引用(即没有通过反射引用该类的地方) 永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。HotSpot提供-Xnoclassgc进行控制 使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看类加载和卸载信息 -verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用; -XX:+TraceClassUnLoading需要fastdebug版HotSpot支持 垃圾收集器 在GC机制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具体实现,Java虚拟机规范中对于垃圾收集器没有任何规定,所以不同厂商实现的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器如下图(图来源于《深入理解Java虚拟机:JVM高级特效与最佳实现》,图中两个收集器之间有连线,说明它们可以配合使用): 在介绍垃圾收集器之前,需要明确一点,就是在新生代采用的停止复制算法中,“停 止(Stop-the-world)”的意义是在回收内存时,需要暂停其他所 有线程的执行。这个是很低效的,现在的各种新生代收集器越来越优化这一点,但仍然只是将停止的时间变短,并未彻底取消停止。 Serial收集器:新生代收集器,使用停止复制算法,使用一个线程进行GC,其它工作线程暂停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值) ParNew收集器:新生代收集器,使用停止复制算法,Serial收集器的多线程版,用多个线程进行GC,其它工作线程暂停,关注缩短垃圾收集时间。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。 Parallel Scavenge 收集器:新生代收集器,使用停止复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适 合用户交互,提高用户体验)。使用-XX:+UseParallelGC开关控制使用 Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即 1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效) Serial Old收集器:老年代收集器,单线程收集器,使用标记整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存 的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停(注意,在老年代中进行标 记整理算法清理,也需要暂停其它线程),在JDK1.5之前,Serial Old收集器与ParallelScavenge搭配使用。 Parallel Old收集器:老年代收集器,多线程,多线程机制与Parallel Scavenge差不错,使用标记整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清 理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。 CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间,使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS(原因见后面),当用户线程内存不足时,采用备用方案Serial Old收集。 CMS收集的方法是:先3次标记,再1次清除,3次标记中前两次是初始标记和重新标记(此时仍然需要停止(stop the world)), 初始标记(Initial Remark)是标记GC Roots能关联到的对象(即有引用的对象),停顿时间很短;并发标记(Concurrent remark)是执行GC Roots查找引用的过程,不需要用户线程停顿;重新标记(Remark)是在初始标记和并发标记期间,有标记变动的那部分仍需要标记,所以加上这一部分 标记的过程,停顿时间比并发标记小得多,但比初始标记稍长。在完成标记之后,就开始并发清除,不需要用户线程停顿。 所以在CMS清理过程中,只有初始标记和重新标记需要短暂停顿,并发标记和并发清除都不需要暂停用户线程,因此效率很高,很适合高交互的场合。 CMS也有缺点,它需要消耗额外的CPU和内存资源,在CPU和内存资源紧张,CPU较少时,会加重系统负担(CMS默认启动线程数为(CPU数量+3)/4)。 另外,在并发收集过程中,用户线程仍然在运行,仍然产生内存垃圾,所以可能产生“浮动垃圾”,本次无法清理,只能下一次Full GC才清理,因此在GC期间,需要预留足够的内存给用户线程使用。所以使用CMS的收集器并不是老年代满了才触发Full GC,而是在使用了一大半(默认68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction来设置)的时候就要进行Full GC,如果用户线程消耗内存不是特别大,可以适当调高-XX:CMSInitiatingOccupancyFraction以降低GC次数,提高性能,如果预留的用户线程内存不够,则会触发Concurrent Mode Failure,此时,将触发备用方案:使用Serial Old 收集器进行收集,但这样停顿时间就长了,因此-XX:CMSInitiatingOccupancyFraction不宜设的过大。 还有,CMS采用的是标记清除算法,会导致内存碎片的产生,可以使用-XX:+UseCMSCompactAtFullCollection来设置是否在Full GC之后进行碎片整理,用-XX:CMSFullGCsBeforeCompaction来设置在执行多少次不压缩的Full GC之后,来一次带压缩的Full GC。 G1收集器:在JDK1.7中正式发布,与现状的新生代、老年代概念有很大不同,目前使用较少,不做介绍。 注意并发(Concurrent)和并行(Parallel)的区别: 并发是指用户线程与GC线程同时执行(不一定是并行,可能交替,但总体上是在同时执行的),不需要停顿用户线程(其实在CMS中用户线程还是需要停顿的,只是非常短,GC线程在另一个CPU上执行); 并行收集是指多个GC线程并行工作,但此时用户线程是暂停的; 所以,Serial和Parallel收集器都是并行的,而CMS收集器是并发的. 关于JVM参数配置和内存调优实例,见我的下一篇博客(编写中:Java系列笔记(4) - JVM监控与调优),本来想写在同一篇博客里的,无奈内容太多,只好另起一篇。 说明: 本文是Java系列笔记的第3篇,这篇文章写了很久,主要是Java内存和 GC机制相对复杂,难以理解,加上本人这段时间项目和生活中耗费的时间很多,所以进度缓慢。文中大多数笔记内容来源于我在网络上查到的博客和《深入理解 Java虚拟机:JVM高级特效与最佳实现》一书。 本人能力有限,如果有错漏,请留言指正。 参考资料: 《JAVA编程思想》,第5章; 《Java深度历险》,Java垃圾回收机制与引用类型; 《深入理解Java虚拟机:JVM高级特效与最佳实现》,第2-3章; 成为JavaGC专家Part II — 如何监控Java垃圾回收机制, http://www.importnew.com/2057.html JDK5.0垃圾收集优化之--Don't Pause,http://calvin.iteye.com/blog/91905 【原】java内存区域理解-初步了解,http://iamzhongyong.iteye.com/blog/1333100
1.尽量使用final修饰符。 带有final修饰符的类是不可派生的。在JAVA核心API中,有许多应用final的例子,例如java.lang.String。为String类指定final防止了使用者覆盖length()方法。另外,如果一个类是final的,则该类所有方法都是final的。java编译器会寻找机会内联(inline)所有的final方法(这和具体的编译器实现有关)。此举能够使性能平均提高50%。 2.尽量重用对象。 特别是String对象的使用中,出现字符串连接情况时应使用StringBuffer代替,由于系统不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理。因此生成过多的对象将会给程序的性能带来很大的影响。 3.尽量使用局部变量。 调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快。其他变量,如静态变量,实例变量等,都在堆(Heap)中创建,速度较慢。 4.不要重复初始化变量。 默认情况下,调用类的构造函数时,java会把变量初始化成确定的值,所有的对象被设置成null,整数变量设置成0,float和double变量设置成0.0,逻辑值设置成false。当一个类从另一个类派生时,这一点尤其应该注意,因为用new关键字创建一个对象时,构造函数链中的所有构造函数都会被自动调用。 这里有个注意,给成员变量设置初始值但需要调用其他方法的时候,最好放在一个方法比如initXXX()中,因为直接调用某方法赋值可能会因为类尚未初始化而抛空指针异常,public int state = this.getState(); 5.在java+Oracle的应用系统开发中,java中内嵌的SQL语言应尽量使用大写形式,以减少Oracle解析器的解析负担。 6.java编程过程中,进行数据库连接,I/O流操作,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销。 7.过分的创建对象会消耗系统的大量内存,严重时,会导致内存泄漏,因此,保证过期的对象的及时回收具有重要意义。 JVM的GC并非十分智能,因此建议在对象使用完毕后,手动设置成null。 8.在使用同步机制时,应尽量使用方法同步代替代码块同步。 9.尽量减少对变量的重复计算。 比如 for(int i=0;i<list.size();i++) 应修改为 for(int i=0,len=list.size();i<len;i++) 10.采用在需要的时候才开始创建的策略。 例如: String str="abc"; if(i==1){ list.add(str);} 应修改为: if(i==1){String str="abc"; list.add(str);} 11.慎用异常,异常对性能不利。 抛出异常首先要创建一个新的对象。Throwable接口的构造函数调用名为fillInStackTrace()的本地方法,fillInStackTrace()方法检查栈,收集调用跟踪信息。只要有异常被抛出,VM就必须调整调用栈,因为在处理过程中创建了一个新的对象。 异常只能用于错误处理,不应该用来控制程序流程。 12.不要在循环中使用Try/Catch语句,应把Try/Catch放在循环最外层。 Error是获取系统错误的类,或者说是虚拟机错误的类。不是所有的错误Exception都能获取到的,虚拟机报错Exception就获取不到,必须用Error获取。 13.通过StringBuffer的构造函数来设定他的初始化容量,可以明显提升性能。 StringBuffer的默认容量为16,当StringBuffer的容量达到最大容量时,她会将自身容量增加到当前的2倍+2,也就是2*n+2。无论何时,只要StringBuffer到达她的最大容量,她就不得不创建一个新的对象数组,然后复制旧的对象数组,这会浪费很多时间。所以给StringBuffer设置一个合理的初始化容量值,是很有必要的! 14.合理使用java.util.Vector。 Vector与StringBuffer类似,每次扩展容量时,所有现有元素都要赋值到新的存储空间中。Vector的默认存储能力为10个元素,扩容加倍。 vector.add(index,obj) 这个方法可以将元素obj插入到index位置,但index以及之后的元素依次都要向下移动一个位置(将其索引加 1)。 除非必要,否则对性能不利。 同样规则适用于remove(int index)方法,移除此向量中指定位置的元素。将所有后续元素左移(将其索引减 1)。返回此向量中移除的元素。所以删除vector最后一个元素要比删除第1个元素开销低很多。删除所有元素最好用removeAllElements()方法。 如果要删除vector里的一个元素可以使用 vector.remove(obj);而不必自己检索元素位置,再删除,如int index = indexOf(obj);vector.remove(index); 15.当复制大量数据时,使用System.arraycopy(); 16.代码重构,增加代码的可读性。 17.不用new关键字创建对象的实例。 用new关键词创建类的实例时,构造函数链中的所有构造函数都会被自动调用。但如果一个对象实现了Cloneable接口,我们可以调用她的clone()方法。clone()方法不会调用任何类构造函数。 Factory模式 public static Credit getNewCredit() { return new Credit(); } 改进后的代码使用clone()方法, private static Credit BaseCredit = new Credit(); public static Credit getNewCredit() { return (Credit)BaseCredit.clone(); } 18.乘除法如果可以使用位移,应尽量使用位移,但最好加上注释,因为位移操作不直观,难于理解 19.不要将数组声明为:public static final 20.HaspMap的遍历。 1 2 3 4 5 6 Map<String, String[]> paraMap = new HashMap<String, String[]>(); for( Entry<String, String[]> entry : paraMap.entrySet() ) { String appFieldDefId = entry.getKey(); String[] values = entry.getValue(); } 利用散列值取出相应的Entry做比较得到结果,取得entry的值之后直接取key和value。 21.array(数组)和ArrayList的使用。 array 数组效率最高,但容量固定,无法动态改变,ArrayList容量可以动态增长,但牺牲了效率。 22.单线程应尽量使用 HashMap, ArrayList,除非必要,否则不推荐使用HashTable,Vector,她们使用了同步机制,而降低了性能。 23.StringBuffer,StringBuilder的区别在于:java.lang.StringBuffer 线程安全的可变字符序列。一个类似于String的字符串缓冲区,但不能修改。StringBuilder与该类相比,通常应该优先使用StringBuilder类,因为她支持所有相同的操作,但由于她不执行同步,所以速度更快。为了获得更好的性能,在构造StringBuffer或StringBuilder时应尽量指定她的容量。当然如果不超过16个字符时就不用了。 相同情况下,使用StringBuilder比使用StringBuffer仅能获得10%~15%的性能提升,但却要冒多线程不安全的风险。综合考虑还是建议使用StringBuffer。 24.尽量使用基本数据类型代替对象。 25.用简单的数值计算代替复杂的函数计算,比如查表方式解决三角函数问题。 26.使用具体类比使用接口效率高,但结构弹性降低了,但现代IDE都可以解决这个问题。 27.考虑使用静态方法 如果你没有必要去访问对象的外部,那么就使你的方法成为静态方法。她会被更快地调用,因为她不需要一个虚拟函数导向表。这同事也是一个很好的实践,因为她告诉你如何区分方法的性质,调用这个方法不会改变对象的状态。 28.应尽可能避免使用内在的GET,SET方法。 android编程中,虚方法的调用会产生很多代价,比实例属性查询的代价还要多。我们应该在外包调用的时候才使用get,set方法,但在内部调用的时候,应该直接调用。 29.避免枚举,浮点数的使用。 30.二维数组比一维数组占用更多的内存空间,大概是10倍计算。 31.SQLite数据库读取整张表的全部数据很快,但有条件的查询就要耗时30-50MS,大家做这方面的时候要注意,尽量少用,尤其是嵌套查找! 4. 使用懒加载 懒加载 : 当要用的时候才创建该对象。 String prefix = "gebi"; if ("laowang".equals(name)) { list.add(prefix + name); } 替换为: if("laowang".equals(name)) { String prefix = "gebi"; list.add(prefix + name); } 7.循环内尽量避免创建对象的引用。 尤其是循环量大的时候。 while (i<1000) { Object object = new Object(); } 建议修改为: Object object = null; while (i<1000) { object = new Object(); 每次new Object()的时候,Object对象引用指向Object对象。 当循环次数多的时候,如第一种,JVM会创建1000个对象的引用,而第二种内存中只有一份Object对象引用。这样就大大节省了内存空间了。 8.不要随意使用static变量。 当对象被声明为static的变量所引用时,此时,Java垃圾回收器不会清理这个对象所占用的堆内存。静态变量所占用的堆内存直到该变量所在类所在程序结束才被释放。 即静态变量生命周期=类生命周期。 9.不要创建一些不使用的对象,不要导入一些不使用的类。 10.使用带缓冲的I/O流:带缓冲的I/O流可以极大提高I/O效率。BufferedWriter, BufferedReader, BufferedInputStream, BufferedOutputStream。 11.包装类数据转换为字符串使用: toString Integer i = 1; 包装类数据转换为字符串方法速度排名 : i.toString > String.valueOf(i) > "" + i 12.Map遍历效率 : entrySet > keySet //entrySet() for (Entry<String, String> entry : map.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + " : " + value); } //上下对比 //keySet() for (String key : map.keySet()) { String value = map.get(key); System.out.println(key + " : " + value); } 13.关于Iterator与forEach()的集合遍历舍取。 算法导论上说:算法是为了提高空间效率和时间效率。但往往时间和空间不能并存。 时间效率:Iterator > forEach() 代码可读性 : forEach() > Iterator //Iterator Set //forEach() for (Entry<String, String> entry : map.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + " : " + value); } 个人认为:当处理大数据时推荐使用Iterator遍历集合。 但处理小数据的话,为了可读性和后期维护还是使用forEach()。 两者结合使用,都应该掌握。 1、及时清除不再使用的对象,设为null 2、尽可能使用final,static等关键字 3、尽可能使用buffered对象 如何优化代码使JAVA源文件及编译后CLASS文件更小 1 尽量使用继承,继承的方法越多,你要写的代码量也就越少 2 打开JAVA编译器的优化选项: javac -O 这个选项将删除掉CLASS文件中的行号,并能把 一些private, static,final的小段方法申明为inline方法调用 3 把公用的代码提取出来 Util 4 不要初始化很大的数组,尽管初始化一个数组在JAVA代码中只是一行的代码量,但编译后的代码是一行代码插入一个数组的元素,所以如果你有大量的数据需要存在数组中的话,可以先把这些数据放在String中,然后在运行期把字符串解析到数组中 5 日期类型的对象会占用很大的空间,如果你要存储大量的日期对象,可以考虑把它存储为 long型,然后在使用的时候转换为Date类型 6 类名,方法名和变量名尽量使用简短的名字,可以考虑使用Hashjava, Jobe, Obfuscate and Jshrink等工具自动完成这个工作 7 将static final类型的变量定义到Interface中去 首先接口是一种高度抽象的”模版”,,而接口中的属性也就是’模版’的成员,就应当是所有实现”模版”的实现类的共有特性,所以它是public static的 ,是所有实现类共有的 .假如可以是非static的话,因一个类可以继承多个接口,出现重名的变量,如何区分呢? 其次,接口中如果可能定义非final的变量的话,而方法又都是abstract的,这就自相矛盾了,有可变成员变量但对应的方法却无法操作这些变量,虽然可以直接修改这些静态成员变量的值,但所有实现类对应的值都被修改了,这跟抽象类有何区别? 又接口是一种更高层面的抽象,是一种规范、功能定义的声明,所有可变的东西都应该归属到实现类中,这样接口才能起到标准化、规范化的作用。所以接口中的属性必然是final的。 最后,接口只是对事物的属性和行为更高层次的抽象 。对修改关闭,对扩展(不同的实现implements)开放,接口是对开闭原则(Open-Closed Principle )的一种体现。 8 算术运算 能用左移/右移的运算就不要用*和/运算,相同的运算不要运算多次 9 不要两次初始化变量 Java通过调用独特的类构造器默认地初始化变量为一个已知的值。所有的对象被设置成null,integers (byte, short, int, long)被设置成0,float和double设置成0.0,Boolean变量设置成false。这对那些扩展自其它类的类尤其重要,这跟使用一个新的关键词创建一个对象时所有一连串的构造器被自动调用一样。 10 在任何可能的地方让类为Final 标记为final的类不能被扩展。在《核心Java API》中有大量这个技术的例子,诸如java.lang.String。将String类标记为final阻止了开发者创建他们自己实现的长度方法。 更深入点说,如果类是final的,所有类的方法也是final的。Java编译器可能会内联所有的方法(这依赖于编译器的实现)。在我的测试里,我已经看到性能平均增加了50%。 11 异常在需要抛出的地方抛出,try catch能整合就整合 12 try { some.method1(); // Difficult for javac } catch( method1Exception e ) { // and the JVM runtime // Handle exception 1 // to optimize this } // code try { some.method2(); } catch( method2Exception e ) { // Handle exception 2 } try { some.method3(); } catch( method3Exception e ) { // Handle exception 3 } try { some.method1(); // Easier to optimize some.method2(); some.method3(); } catch( method1Exception e ) { // Handle exception 1 } catch( method2Exception e ) { // Handle exception 2 } catch( method3Exception e ) { // Handle exception 3 } 更容易被编译器优化 10. For循环的优化 Replace… for( int i = 0; i < collection.size(); i++ ) { ... } with… for( int i = 0, n = collection.size(); i < n; i++ ) { ... } 本文中,作者(Eva Andreasson)首先介绍了不同种类的编译器,并对客户端编译,服务器端编译器和多层编译的运行性能进行了对比。然后,在文章的最后介绍了几种常见的JVM优化方法,如死代码消除,代码嵌入以及循环体优化。 Java最引以为豪的特性“平台独立性”正是源于Java编译器。软件开发人员尽其所能写出最好的java应用程序,紧接着后台运行的编译器产生高效的基于目标平台的可执行代码。不同的编译器适用于不同的应用需求,因而也就产生不同的优化结果。因此,如果你能更好的理解编译器的工作原理、了解更多种类的编译器,那么你就能更好的优化你的Java程序。 本篇文章突出强调和解释了各种Java虚拟机编译器之间的不同。同时,我也会探讨一些及时编译器(JIT)常用的优化方案。 什么是编译器? 简单来说,编译器就是以某种编程语言程序作为输入,然后以另一种可执行语言程序作为输出。Javac是最常见的一种编译器。它存在于所有的JDK里面。Javac 以java代码作为输出,将其转换成JVM可执行的代码—字节码。这些字节码存储在以.class结尾的文件中,并在java程序启动时装载到java运行时环境。 字节码并不能直接被CPU读取,它还需要被翻译成当前平台所能理解的机器指令语言。JVM中还有另一个编译器负责将字节码翻译成目标平台可执行的指令。一些JVM编译器需要经过几个等级的字节码代码阶段。例如,一个编译器在将字节码翻译成机器指令之前可能还需要经历几种不同形式的中间阶段。 从平台不可知论的角度出发,我们希望我们的代码能够尽可能的与平台无关。 为了达到这个目的,我们在最后一个等级的翻译—从最低的字节码表示到真正的机器代码—才真正将可执行代码与一个特定平台的体系结构绑定。从最高的等级来划分,我们可以将编译器分为静态编译器和动态编译器。 我们可以根据我们的目标执行环境、我们渴望的优化结果、以及我们需要满足的资源限制条件来选择合适的编译器。在上一篇文章中我们简单的讨论了一下静态编译器和动态编译器,在接下来的部分我们将更加深入的解释它们。 静态编译 VS 动态编译 我们前面提到的javac就是一个静态编译的例子。对于静态编译器,输入代码被解释一次,输出即为程序将来被执行的形式。除非你更新源代码并(通过编译器)重新编译,否则程序的执行结果将永远不会改变:这是因为输入是一个静态的输入并且编译器是一个静态的编译器。 通过静态编译,下面的程序: 复制代码 代码如下: staticint add7(int x ){ return x+7;} 将会转换成类似下面的字节码: 复制代码 代码如下: iload0 bipush 7 iadd ireturn 动态编译器动态的将一种语言编译成另外一种语言,所谓动态的是指在程序运行的时候进行编译—边运行边编译!动态编译和优化的好处就是可以处理应用程序加载时的一些变化。Java 运行时常常运行在不可预知甚至变化的环境上,因此动态编译非常适用于Java 运行时。大部分的JVM 使用动态编译器,如JIT编译器。值得注意的是,动态编译和代码优化需要使用一些额外的数据结构、线程以及CPU资源。越高级的优化器或字节码上下文分析器,消耗越多的资源。但是这些花销相对于显著的性能提升来说是微不足道的。 JVM种类以及Java的平台独立性 所有JVM的实现都有一个共同的特点就是将字节码编译成机器指令。一些JVM在加载应用程序时对代码进行解释,并通过性能计数器来找出“热”代码;另一些JVM则通过编译来实现。编译的主要问题是集中需要大量的资源,但是它也能带来更好的性能优化。 如果你是一个java新手,JVM的错综复杂肯定会搞得你晕头转向。但好消息是你并不需要将它搞得特别清楚!JVM将管理代码的编译和优化,你并不需要为机器指令以及采取什么样的方式写代码才能最佳的匹配程序运行平台的体系结构而操心。 从java字节码到可执行 一旦将你的java代码编译成字节码,接下来的一步就是将字节码指令翻译成机器代码。这一步可以通过解释器来实现,也可以通过编译器来实现。 解释 解释是编译字节码最简单的方式。解释器以查表的形式找到每条字节码指令对应的硬件指令,然后将它发送给CPU执行。 你可以将解释器想象成查字典:每一个特定的单词(字节码指令),都有一个具体的翻译(机器代码指令)与之对应。因为解释器每读一条指令就会马上执行该指令,所以该方式无法对一组指令集进行优化。同时每调用一个字节码都要马上对其进行解释,因此解释器运行速度是相当慢得。解释器以一种非常准确的方式来执行代码,但是由于没有对输出的指令集进行优化,因此它对目标平台的处理器来说可能不是最优的结果。 编译 编译器则是将所有将要执行的代码全部装载到运行时。这样当它翻译字节码时,就可以参考全部或部分的运行时上下文。它做出的决定都是基于对代码图分析的结果。如比较不同的执行分支以及参考运行时上下文数据。 在将字节码序列被翻译成机器代码指令集后,就可以基于这个机器代码指令集进行优化。优化过的指令集存储在一个叫代码缓冲区的结构中。当再次执行这些字节码时,就可以直接从这个代码缓冲区中取得优化过的代码并执行。在有些情况下编译器并不使用优化器来进行代码优化,而是使用一种新的优化序列—“性能计数”。 使用代码缓存器的优点是结果集指令可以被立即执行而不再需要重新解释或编译! 这可以大大的降低执行时间,尤其是对一个方法被多次调用的java应用程序。 优化 通过动态编译的引入,我们就有机会来插入性能计数器。例如,编译器插入性能计数器,每次字节码块(对应某个具体的方法)被调用时对应的计数器就加一。编译器通过这些计数器找到“热块”,从而就能确定哪些代码块的优化能对应用程序带来最大的性能提升。运行时性能分析数据能够帮助编译器在联机状态下得到更多的优化决策,从而更进一步提升代码执行效率。因为得到越多越精确的代码性能分析数据,我们就可以找到更多的可优化点从而做出更好的优化决定,例如:怎样更好的序列话指令、是否用更有效率的指令集来替代原有指令集,以及是否消除冗余的操作等。 例如 考虑下面的java代码 复制代码 代码如下: staticint add7(int x ){ return x+7;} Javac 将静态的将它翻译成如下字节码: 复制代码 代码如下: iload0 bipush 7 iadd ireturn 当该方法被调用时,该字节码将被动态的编译成机器指令。当性能计数器(如果存在)达到指定的阀值时,该方法就可能被优化。优化后的结果可能类似下面的机器指令集: 复制代码 代码如下: lea rax,[rdx+7] ret 不同的编译器适用于不同的应用 不同的应用程序拥有不同的需求。企业服务器端应用通常需要长时间运行,所以通常希望对其进行更多的性能优化;而客户端小程序可能希望更快的响应时间和更少的资源消耗。下面让我们一起讨论三种不同的编译器以及他们的优缺点。 客户端编译器(Client-side compilers) C1是一种大家熟知的优化编译器。当启动JVM时,添加-client参数即可启动该编译器。通过它的名字我们即可发现C1是一种客户端编译器。它非常适用于那种系统可用资源很少或要求能快速启动的客户端应用程序。C1通过使用性能计数器来进行代码优化。这是一种方式简单,且对源代码干预较少的优化方式。 服务器端编译器(Server-side compilers) 对于那种长时间运行的应用程序(例如服务器端企业级应用程序),使用客户端编译器可能远远不能够满足需求。这时我们应该选择类似C2这样的服务器端编译器。通过在JVM启动行中加入 –server 即可启动该优化器。因为大部分的服务器端应用程序通常都是长时间运行的,与那些短时间运行、轻量级的客户端应用相比,通过使用C2编译器,你将能够收集到更多的性能优化数据。因此你也将能够应用更高级的优化技术和算法。 提示:预热你的服务端编译器 对于服务器端的部署,编译器可能需要一些时间来优化那些“热点”代码。所以服务器端的部署常常需要一个“加热”阶段。所以当对服务器端的部署进行性能测量时,务必确保你的应用程序已经达到了稳定状态!给予编译器充足的时间进行编译将会给你的应用带来很多好处。 服务器端编译器相比客户端编译器来说能够得到更多的性能调优数据,这样就可以进行更复杂的分支分析,从而找到性能更优的优化路径。拥有越多的性能分析数据就能得到更优的应用程序分析结果。当然,进行大量的性能分析也就需要更多的编译器资源。如JVM若使用C2编译器,那么它将需要使用更多的CPU周期,更大的代码缓存区等等。 多层编译 多层编译混合了客户端编译和服务器端编译。Azul第一个在他的Zing JVM中实现了多层编译。最近,这项技术已经被Oracle Java Hotspot JVM采用(Java SE7 之后)。多层编译综合了客户端和服务器端编译器的优点。客户端编译器在以下两种情况表现得比较活跃:应用启动时;当性能计数器达到较低级别的阈值时进行性能优化。客户端编译器也会插入性能计数器以及准备指令集以备接下来的高级优化—服务器端编译器—使用。多层编译是一种资源利用率很高的性能分析方式。因为它可以在低影响编译器活动时收集数据,而这些数据可以在后面更高级的优化中继续使用。这种方式与使用解释性代码分析计数器相比可以提供更多的信息。 图1所描述的是解释器、客户端编译、服务器端编译、多层编译的性能比较。X轴是执行时间(时间单位),Y轴是性能(单位时间内的操作数) 图1.编译器性能比较 相对于纯解释性代码,使用客户端编译器可以带来5到10倍的性能提升。获得性能提升的多少取决于编译器的效率、可用的优化器种类以及应用程序的设计与目标平台的吻合程度。但对应程序开发人员来讲最后一条往往可以忽略。 相对于客户端编译器,服务器端编译器往往能带来30%到50%的性能提升。在大多数情况下,性能的提升往往是以资源的损耗为代价的。 多层编译综合了两种编译器的优点。客户端编译有更短的启动时间以及可以进行快速优化;服务器端编译则可以在接下来的执行过程中进行更高级的优化操作。 一些常见的编译器优化 到目前为止,我们已经讨论了优化代码的意义以及怎样、何时JVM会进行代码优化。接下来我将以介绍一些编译器实际用到的优化方式来结束本文。JVM优化实际发生在字节码阶段(或者更底层的语言表示阶段),但是这里将使用java语言来说明这些优化方式。我们不可能在本节覆盖所有的JVM优化方式;当然啦,我希望通过这些介绍能激发你去学习数以百计的更高级的优化方式的兴趣并在编译器技术方面有所创新。 死代码消除 死代码消除,顾名思义就是消除那些永远不会被执行到的代码—即“死”代码。 如果编译器在运行过程中发现一些多余指令,它将会将这些指令从执行指令集里面移除。例如,在列表1里面,其中一个变量在对其进行赋值操作后永远不会被用到,所有在执行阶段可以完全地忽略该赋值语句。对应到字节码级别的操作即是,永远不需要将该变量值加载到寄存器中。不用加载意味着消耗更少的cpu时间,因此也就能加快代码执行,最终导致应用程序加快—如果该加载代码每秒被调用好多次,那优化效果将更明显。 列表1 用java 代码列举了一个对永远不会被使用的变量赋值的例子。 列表1. 死代码 复制代码 代码如下: int timeToScaleMyApp(boolean endlessOfResources){ int reArchitect =24; int patchByClustering =15; int useZing =2; if(endlessOfResources) return reArchitect + useZing; else return useZing; } 在字节码阶段,如果一个变量被加载但是永远不会被使用,编译器可以检测到并消除掉这些死代码,如列表2所示。如果永远不执行该加载操作则可以节约cpu时间从而改进程序的执行速度。 列表2. 优化后的代码 复制代码 代码如下: int timeToScaleMyApp(boolean endlessOfResources){ int reArchitect =24; //unnecessary operation removed here… int useZing =2; if(endlessOfResources) return reArchitect + useZing; else return useZing; } 冗余消除是一种类似移除重复指令来改进应用性能的优化方式。 很多优化尝试着消除机器指令级别的跳转指令(如 x86体系结构中得JMP). 跳转指令将改变指令指针寄存器,从而转移程序执行流。这种跳转指令相对其他ASSEMBLY指令来说是一种很耗资源的命令。这就是为什么我们要减少或消除这种指令。代码嵌入就是一种很实用、很有名的消除转移指令的优化方式。因为执行跳转指令代价很高,所以将一些被频繁调用的小方法嵌入到函数体内将会带来很多益处。列表3-5证明了内嵌的好处。 列表3. 调用方法 复制代码 代码如下: int whenToEvaluateZing(int y){ return daysLeft(y)+ daysLeft(0)+ daysLeft(y+1);} 列表4. 被调用方法 复制代码 代码如下: int daysLeft(int x){ if(x ==0) return0; else return x -1;} 列表5. 内嵌方法 复制代码 代码如下: int whenToEvaluateZing(int y){ int temp =0; if(y ==0) temp +=0; else temp += y -1; if(0==0) temp +=0; else temp +=0-1; if(y+1==0) temp +=0; else temp +=(y +1)-1; return temp; } 在列表3-5中我们可以看到,一个小方法在另一个方法体内被调用了三次,而我们想说明的是:将被调用方法直接内嵌到代码中所花费的代价将小于执行三次跳转指令所花费的代价。 内嵌一个不常被调用的方法可能并不会带来太大的不同,但是如果内嵌一个所谓的“热”方法(经常被调用的方法)则可以带来很多的性能提升。内嵌后的代码常常还可以进行更进一步的优化,如列表6所示。 列表6. 代码内嵌后,更进一步的优化实现 复制代码 代码如下: int whenToEvaluateZing(int y){ if(y ==0)return y; elseif(y ==-1)return y -1; elsereturn y + y -1;} 循环优化 循环优化在降低执行循环体所带来的额外消耗方面起着很重要的作用。这里的额外消耗指的是昂贵的跳转、大量的条件检测,非优化管道(即,一系列无实际操作、消耗额外cpu周期的指令集)。这里有很多种循环优化,接下来列举一些比较流行的循环优化: 循环体合并:当两个相邻的循环体执行相同次数的循环时,编译器将试图合并这两个循环体。如果两个循环体相互之间是完全独立的,则它们还可以被同时执行(并行)。 反演循环: 最基本的,你用一个do-while循环来替代一个while循环。这个do-while循环被放置在一个if语句中。这个替换将减少两次跳转操作;但增加了条件判断,因此增加了代码量。这种优化是以适当的增加资源消耗换来更有效的代码的很棒的例子—编译器对花费和收益进行衡量,在运行时动态的做出决定。 重组循环体: 重组循环体,使整个循环体能全部的存储在缓存器中。 展开循环体: 减少循环条件的检测次数和跳转次数。你可以把这想象成将几次迭代“内嵌”执行,而不必进行条件检测。循环体展开也会带来一定的风险,因为它可能因为影响流水线和大量的冗余指令提取而降低性能。再一次,是否展开循环体由编译器在运行时决定,如果能带来更大的性能提升则值得展开。 以上就是对编译器在字节码级别(或更低级别)如何改进应用程序在目标平台执行性能的一个概述。我们所讨论的都是些常见、流行的优化方式。由于篇幅有限我们只举了一些简单的例子。我们的目的是希望通过上面简单的讨论来激起你深入研究优化的兴趣。 结论:反思点和重点 根据不同的目的,选择不同的编译器。 1.解释器是将字节码翻译成机器指令的最简单形式。它的实现基于一个指令查询表。 2.编译器可以基于性能计数器进行优化,但是需要消耗一些额外的资源(代码缓存,优化线程等)。 3.客户端编译器相对于解释器可以带来5到10倍的性能提升。 4.服务器端编译器相对于客户端编译器来说可以带来30%到50%的性能提升,但需要消耗更多的资源。 5.多层编译则综合了两者的优点。使用客户端编译来获取更快的响应速度,接着使用服务器端编译器来优化那些被频繁调用的代码。 这里有很多种可能的代码优化方式。编译器的一个重要工作就是分析所有可能的优化方式,然后对各种优化方式所付出的代价与最终得到的机器指令带来的性能提升进行权衡。 Java应用程序是运行在JVM上的,但是你对JVM技术了解吗?这篇文章(这个系列的第一部分)讲述了经典Java虚拟机是怎么样工作的,例如:Java一次编写的利弊,跨平台引擎,垃圾回收基础知识,经典的GC算法和编译优化。之后的文章会讲JVM性能优化,包括最新的JVM设计——支持当今高并发Java应用的性能和扩展。 如果你是一个开发人员,你肯定遇到过这样的特殊感觉,你突然灵光一现,所有的思路连接起来了,你能以一个新的视角来回想起你以前的想法。我个人很喜欢学习新知识带来的这种感觉。我已经有过很多次这样的经历了,在我使用JVM技术工作时,特别是使用垃圾回收和JVM性能优化时。在这个新的Java世界中,我希望和你分享我的这些启发。希望你能像我写这篇文章一样兴奋的去了解JVM的性能。 这个系列文章,是为所有有兴趣去学习更多JVM底层知识,和JVM实际做了什么的Java开发人员所写的。在更高层次,我将讨论垃圾回收和在不影响应用运行的情况下,对空闲内存安全和速度上的无止境追求。你将学到JVM的关键部分:垃圾回收和GC算法,编译优化,和一些常用的优化。我同样会讨论为什么Java标记这样难,提供建议什么时候应该考虑测试性能。最后,我将讲一些JVM和GC的新的创新,包括Azul’s Zing JVM, IBM JVM, 和Oracle’s Garbage First (G1) 垃圾回收中的重点。 我希望你读完这个系列时对Java可扩展性限制的特点有更深的了解,同样的这样限制是如何强制我们以最优的方式创建一个Java部署。希望你会有一种豁然开朗的感受,并且能激发了一些好的Java灵感:停止接受那些限制,并去改变它!如果你现在还不是一个开源工作者,这个系列或许会鼓励你往这方面发展。 JVM性能和“一次编译,到处运行”的挑战 我有新的消息告诉那些固执的认为Java平台本质上是缓慢的人。当Java刚刚做为企业级应用的时候,JVM被诟病的Java性能问题已经是十几年前的事了,但这个结论,现在已经过时了。这是真的,如果你现在在不同的开发平台上运行简单静态和确定的任务时,你将很可能发现使用机器优化过的代码比使用任何虚拟环境执行的要好,在相同的JVM下。但是,Java的性能在过去10年有了非常大的提升。Java产业的市场需求和增长,导致了少量的垃圾回收算法、新的编译创新、和大量的启发式方法和优化,这些使JVM技术得到了进步。我将在以后的章节中介绍一些。 JVM的技术之美,同样是它最大的挑战:没有什么可以被认为是“一次编译,到处运行”的应用。不是优化一个用例,一个应用,一个特定的用户负载,JVM不断的跟踪Java应用现在在做什么,并进行相应的优化。这种动态的运行导致了一系列动态的问题。当设计创新时(至少不是在我们向生产环境要性能时),致力于JVM的开发者不会依赖静态编译和可预测的分配率。 JVM性能的事业 在我早期的工作中我意识到垃圾回收是非常难“解决”的,我一直着迷于JVMs和中间件技术。我对JVMs的热情开始于我在JRockit团队中时,编码一种新的方法用于自学,自己调试垃圾回收算法(参考 Resources)。这个项目(转变为JRockit一个实验性的特点,并成为Deterministic Garbage Collection算法的基础)开启了我JVM技术的旅程。我已经在BEA系统、Intel、Sun和Oracle(因为Oracle收购BEA系统,所以被Oracle短暂的工作过)工作过。之后我加入了在Azul Systems的团队去管理Zing JVM,现在我为Cloudera工作。 机器优化的代码可能会实现较好的性能(但这是以牺牲灵活性来做代价的),但对于动态装载和功能快速变化的企业应用这并不是一个权衡选择它的理由。大多数的企业为了Java的优点,更愿意去牺牲机器优化代码带来的勉强完美的性能。 1.易于编码和功能开发(意义是更短的时间去回应市场) 2.得到知识渊博的的程序员 3.用Java APIs和标准库更快速的开发 4.可移植性——不用为新的平台去重新写Java应用 从Java代码到字节码 做为一个Java程序员,你可能对编码、编译和执行Java应用很熟悉。例子:我们假设你有一个程序(MyApp.java),现在你想让它运行。去执行这个程序你需要先用javac(JDK内置的静态Java语言到字节码编译器)编译。基于Java代码,javac生成相应的可执行字节码,并保存在相同名字的class文件:MyApp.class中。在把Java代码编译成字节码后,你可以通过java命令(通过命令行或startup脚本,使用不使用startup选项都可以)来启动可执行的class文件,从而运行你的应用。这样你的class被加载到运行时(意味着Java虚拟机的运行),程序开始执行。 这就是表面上每一个应用执行的场景,但是现在我们来探究下当你执行java命令时究竟发生了什么。Java虚拟机是什么?大多数开发人员通过持续调试来与JVM交互——aka selecting 和value-assigning启动选项能让你的Java程序跑的更快,同时避免了臭名昭著的”out of memory”错误。但是,你是否曾经想过,为什么我们起初需要一个JVM来运行Java应用呢? 什么是Java虚拟机? 简单的说,一个JVM是一个软件模块,用于执行Java应用字节码并且把字节码转化到硬件,操作系统特殊指令。通过这样做,JVM允许Java程序在第一次编写后可以在不同的环境中执行,并不需要更改原始的代码。Java的可移植性是通往企业应用语言的关键:开发者并不需要为不同平台重写应用代码,因为JVM负责翻译和平台优化。 一个JVM基本上是一个虚拟的执行环境,作为一个字节码指令机器,而用于分配执行任务和执行内存操作通过与底层的交互。 一个JVM同样为运行的Java应用照看动态资源管理。这就意味着它掌握分配和释放内存,在每个平台上保持一致的线程模型,在应用执行的地方用一种适于CPU架构的方式组织可执行的指令。JVM把开发人员从跟踪对象当中的引用,和它们需要在系统中存在多长时间中解放出来。同样的它不用我们管理何时去释放内存——一个像C语言那样的非动态语言的痛点。 你可以把JVM当做是一个专门为Java运行的操作系统;它的工作是为Java应用管理运行环境。一个JVM基本上是一个虚拟的通过与底层的交互的执行环境,作为一个字节码指令机器,而用于分配执行任务和执行内存操作。 JVM组件概述 有很多写JVM内部和性能优化的文章。作为这个系列的基础,我将会总结概述下JVM组件。这个简短的阅览会为刚接触JVM的开发者有特殊的帮助,会让你更想了解之后更深入的讨论。 从一种语言到另一种——关于Java编译器 编译器是把一种语言输入,然后输出另一种可执行的语句。Java编译器有两个主要任务: 1. 让Java语言更加轻便,不用在第一次写的时候固定在特定的平台; 2. 确保对特定的平台产生有效的可执行的代码。 编译器可以是静态也可以是动态。一个静态编译的例子是javac。它把Java代码当做输入,并转化为字节码(一种在Java虚拟机执行的语言)。静态编译器一次解释输入的代码,输出可执行的形式,这个是在程序执行时将被用到。因为输入是静态的,你将总能看到结果相同。只有当你修改原始代码并重新编译时,你才能看到不同的输出。 动态编译器,例如Just-In-Time (JIT)编译器,把一种语言动态的转化为另一种,这意味着它们做这些时把代码被执行。JIT编译器让你收集或创建运行数据分析(通过插入性能计数的方式),用编译器决定,用手边的环境数据。动态的编译器可以在编译成语言的过程之中,实现更好的指令序列,把一系列的指令替换成更有效的,甚至消除多余的操作。随着时间的增长你将收集更多的代码配制数据,做更多更好的编译决定;整个过程就是我们通常称为的代码优化和重编译。 动态编译给了你可以根据行为去调整动态的变化的优势,或随着应用装载次数的增加催生的新的优化。这就是为什么动态编译器非常适合Java运行。值得注意的是,动态编译器请求外部数据结构,线程资源,CPU周期分析和优化。越深层次的优化,你将需要越多的资源。然而在大多数环境中,顶层对执行性能的提升帮助非常小——比你纯粹的解释要快5到10倍的性能。 分配会导致垃圾回收 分配在每一个线程基于每个“Java进程分配内存地址空间”,或者叫Java堆,或者直接叫堆。在Java世界中单线程分配在客户端应用程序中很常见。然而,单线程分配在企业应用和工作装载服务端变的没有任何益处,因为它并没有使用现在多核环境的并行优势。 并行应用设计同样迫使JVM保证在同一时间,多线程不会分配同一个地址空间。你可以通过在整个分配空间中放把锁来控制。但这种技术(通常叫做堆锁)很消耗性能,持有或排队线程会影响资源利用和应用优化的性能。多核系统好的一面是,它们创造了一个需求,为各种各样的新的方法在资源分配的同时去阻止单线程的瓶颈,和序列化。 一个常用的方法是把堆分成几部分,在对应用来说每个合式分区大小的地方——显然它们需要调优,分配率和对象大小对不同应用来说有显著的变化,同样线程的数量也不同。线程本地分配缓存(Thread Local Allocation Buffer,简写:TLAB),或者有时,线程本地空间(Thread Local Area,简写:TLA),是一个专门的分区,在其中线程不用声明一个全堆锁就可以自由分配。当区域满的时候,堆就满了,表示堆上的空闲空间不够用来放对象的,需要分配空间。当堆满的时候,垃圾回收就会开始。 碎片 使用TLABs捕获异常,是把堆碎片化来降低内存效率。如果一个应用在要分配对象时正巧不能增加或者不能完全分配一个TLAB空间,这将会有空间太小而不能生成新对象的风险。这样的空闲空间被当做“碎片”。如果应用程序一直保持对象的引用,然后再用剩下的空间分配,最后这些空间会在很长一段时间内空闲。 碎片就是当碎片被分散在堆中的时候——通过一小段不用的内存空间来浪费堆空间。为你的应用分配 “错误的”TLAB空间(关于对象的大小、混合对象的大小和引用持有率)是导致堆内碎片增多的原因。在随着应用的运行,碎片的数量会增加在堆中占有的空间。碎片导致性能下降,系统不能给新应用分配足够的线程和对象。垃圾回收器在随后会很难阻止out-of-memory异常。 TLAB浪费在工作中产生。一种方法可以完全或暂时避免碎片,那就是在每次基础操作时优化TLAB空间。这种方法典型的作法是应用只要有分配行为,就需要重新调优。通过复杂的JVM算法可以实现,另一种方法是组织堆分区实现更有效的内存分配。例如,JVM可以实现free-lists,它是连接起一串特定大小的空闲内存块。一个连续的空闲内存块和另一个相同大小的连续内存块相连,这样会创建少量的链表,每个都有自己的边界。在有些情况下free-lists导致更好的合适内存分配。线程可以对象分配在一个差不多大小的块中,这样比你只依靠固定大小的TLAB,潜在的产生少的碎片。 GC琐事 有一些早期的垃圾收集器拥有多个老年代,但是当超过两个老年代的时候会导致开销超过价值。另一种优化分配减少碎片的方法,就是创造所谓的新生代,这是一个专门用于分配新对象的专用堆空间。剩余的堆会成为所谓的老年代。老年代是用来分配长时间存在的对象的,被假定会存在很长时间的对象包括不被垃圾收集的对象或者大对象。为了更好的理解这种分配的方法,我们需要讲一些垃圾收集的知识。 垃圾回收和应用性能 垃圾回收是JVM的垃圾回收器去释放没有引用的被占据的堆内存。当第一次触发垃圾收集时,所有的对象引用还被保存着,被以前的引用占据的空间被释放或重新分配。当所有可回收的内存被收集后,空间等待被抓取和再次分配给新对象。 垃圾回收器永远都不能重声明一个引用对象,这样做会破坏JVM的标准规范。这个规则的异常是一个可以捕获的soft或weak引用 ,如果垃圾收集器将要将近耗尽内存。我强烈推荐你尽量避免weak引用,然而,因为Java规范的模糊导致了错误的解释和使用的错误。更何况,Java是被设计为动态内存管理,因为你不需要考虑什么时候和什么地方释放内存。 垃圾收集器的一个挑战是在分配内存时,需要尽量不影响运行着的应用。如果你不尽量垃圾收集,你的应用将耗近内存;如果你收集的太频繁,你将损失吞吐量和响应时间,这将对运行的应用产生坏的影响。 GC算法 有许多不同的垃圾回收算法。稍后,在这个系列里将深入讨论几点。在最高层,垃圾收集两个最主要的方法是引用计数和跟踪收集器。 引用计数收集器会跟踪一个对象指向多少个引用。当一个对象的引用为0时,内存将被立即回收,这是这种方法的优点之一。引用计数方法的难点在于环形数据结构和保持所有的引用即时更新。 跟踪收集器对仍在引用的对象标记,用已经标记的对象,反复的跟随和标记所有的引用对象。当所有的仍然引用的对象被标记为“live”时,所有的不被标记的空间将被回收。这种方法管理环形数据结构,但是在很多情况下收集器应该等待直到所有标记完成,在重新回收不被引用的内存之前。 有不种的途径来被上面的方法。最著名的算法是 marking 或copying 算法, parallel 或 concurrent算法。我将在稍后的文章中讨论这些。 通常来说垃圾回收的意义是致力于在堆中给新对象和老对象分配地址空间。其中“老对象”是指在许多垃圾回收后幸存的对象。用新生代来给新对象分配,老年代给老对象,这样能通过快速回收占据内存的短时间对象来减少碎片,同样通过把长时间存在的对象聚合在一起,并把它们放到老年代地址空间中。所有这些在长时间对象和保存堆内存不碎片化之间减少了碎片。新生代的一个积极作用是延迟了需要花费更大代价回收老年代对象的时间,你可以为短暂的对象重复利用相同的空间。(老空间的收集会花费更多,是因为长时间存在的对象们,会包含更多的引用,需要更多的遍历。) 最后值的一提的算法是compaction,这是管理内存碎片的方法。Compaction基本来说就是把对象移动到一起,从来释放更大的连续内存空间。如果你熟悉磁盘碎片和处理它的工具,你会发现compaction跟它很像,不同的是这个运行在Java堆内存中。我将在系列中详细讨论compaction。 总结:回顾和重点 JVM允许可移植(一次编程,到处运行)和动态的内存管理,所有Java平台的主要特性,都是它受欢迎和提高生产力的原因。 在第一篇JVM性能优化系统的文章中我解释了一个编译器怎么把字节码转化为目标平台的指令语言的,并帮助动态的优化Java程序的执行。不同的应用需要不同的编译器。 我同样简述了内存分配和垃圾收集,和这些怎么与Java应用性能相关的。基本上,你越快的填满堆和频繁的触发垃圾收集,Java应用的占有率越高。垃圾收集器的一个挑战是在分配内存时,需要尽量不影响运行着的应用,但要在应用耗尽内存之前。在以后的文章中我们会更详细的讨论传统的和新的垃圾回收和JVM性能优化。 Java平台的垃圾收集机制显著提高了开发者的效率,但是一个实现糟糕的垃圾收集器可能过多地消耗应用程序的资源。在Java虚拟机性能优化系列的第三部分,Eva Andreasson向Java初学者介绍了Java平台的内存模型和垃圾收集机制。她解释了为什么碎片化(而不是垃圾收集)是Java应用程序性能的主要问题所在,以及为什么分代垃圾收集和压缩是目前处理Java应用程序碎片化的主要办法(但不是最有新意的)。 垃圾收集(GC)的目的是释放那些不再被任何活动对象引用的Java对象所占用的内存,它是Java虚拟机动态内存管理机制的核心部分。在一个典型的垃圾收集周期里,所有仍然被引用的对象(因此是可达的)都将被保留,而那些不再被引用的对象将被释放、其所占用的空间将被回收用来分配给新的对象。 为了理解垃圾收集机制和各种垃圾收集算法,首先需要知道关于Java平台内存模型的一些知识。 垃圾收集和Java平台内存模型 当用命令行启动一个Java程序并指定启动参数-Xmx时(例如:java -Xmx:2g MyApp),指定大小的内存就分配给了Java进程,这就是所谓的Java堆。这个专用的内存地址空间用于存储Java程序(有时是JVM)所创建的对象。随着应用程序运行并不断为新对象分配内存,Java堆(即专门的内存地址空间)就会慢慢被填满。 最终Java堆会被填满,也就是说内存分配线程找不到一块足够大的连续空间为新对象分配内存,这时JVM决定要通知垃圾收集器并启动垃圾收集。垃圾收集也可以通过在程序中调用System.gc()来触发,但使用System.gc()并不能确保垃圾收集一定被执行。在任何一次垃圾收集之前,垃圾收集机制都会首先判断执行垃圾收集是否安全,当应用程序的所有活动线程都处于安全点时就可以开始执行一次垃圾收集。例如:当正在为对象分配内存时就不能执行垃圾收集,或者是正在优化CPU指令时也不能执行垃圾收集,因为这样很可能会丢失上下文从而搞错最终结果。 垃圾收集器不能回收任何一个有活动引用的对象,那将破坏Java虚拟机规范。也无需立即回收死对象,因为死对象最终还是会被后续的垃圾收集所回收。尽管有很多种垃圾收集的实现方法,但以上两点对所有垃圾收集实现都是相同的。垃圾收集真正的挑战在于如何识别对象是否存活以及如何在尽量不影响应用程序的情况下回收内存,因此垃圾收集器的目标有以下两个: 1.迅速释放没有引用的内存以满足应用程序的内存分配需要从而避免内存溢出。 2.回收内存时对正在运行的应用程序性能(延迟和吞吐量)的影响最小化。 两类垃圾收集 在本系列的第一篇中,我介绍了两种垃圾收集的方法,即引用计数和跟踪收集。接下来我们进一步探讨这两种方法,并介绍一些在生产环境中使用的跟踪收集算法。 引用计数收集器 引用计数收集器记录了指向每个Java对象的引用数,一旦指向某个对象的引用数为0,那么就可以立即回收该对象。这种即时性是引用计数收集器的主要优点,而且维护那些没有引用指向的内存几乎没有开销,不过为每个对象记录最新的引用数却是代价高昂的。 引用计数收集器的主要难点在于如何保证引用计数的准确性,另外一个众所周知的难点是如何处理循环引用的情况。如果两个对象彼此引用,而且没有被其他活动对象所引用,那么这两个对象的内存永远都不会被回收,因为指向这两个对象的引用数都不为0。对循环引用结构的内存回收需要major analysis(译者注:Java堆上的全局分析),这将增加算法的复杂性,从而也给应用程序带来额外的开销。 跟踪收集器 跟踪收集器基于这样的假设:所有的活动对象都可以通过一个已知的初始活动对象集合的迭代引用(引用以及引用的引用)找到。可以通过分析寄存器、全局对象和栈帧来确定初始活动对象集合(也被称为根对象)。确定了初始对象集合后,跟踪收集器顺着这些对象的引用关系依次将引用所指向的对象标注为活动对象,就这样已知的活动对象集合不断扩大。这一过程持续进行直到所有被引用的对象都被标注为活动对象,而那些没有被标注过的对象的内存就被回收。 跟踪收集器不同于引用计数收集器主要在于它可以处理循环引用结构。多数的跟踪收集器都是在标记阶段发现那些循环引用结构中的无引用对象。 跟踪收集器是动态语言中最常用的内存管理方式,也是目前Java中最常见的方式,同时在生产环境中也被验证了很多年。下面我将从实现跟踪收集的一些算法开始介绍跟踪收集器。 跟踪收集算法 复制垃圾收集器和标记-清除垃圾收集器并不是什么新东西,但它们仍然是目前实现跟踪收集的两种最常见算法。 复制垃圾收集器 传统的复制垃圾收集器使用堆中的两个地址空间(即from空间和to空间),当执行垃圾收集时from空间的活动对象被复制到to空间,当from空间的所有活动对象都被移出(译者注:复制到to空间或者老年代)后,就可以回收整个from空间了,当再次开始分配空间时将首先使用to空间(译者注:即上一轮的to空间作为新一轮的from空间)。 在该算法的早期实现中,from空间和to空间不断变换位置,也就是说当to空间满了,触发了垃圾收集,to空间就成为了from空间,如图1所示。 图1 传统的复制垃圾收集顺序 最新的复制算法允许堆内任意地址空间作为to空间和from空间。这样它们不需要彼此交换位置,而只是逻辑上变换了位置。 复制收集器的优点是在to空间被复制的对象紧凑排列,完全没有碎片。而碎片化正是其他垃圾收集器所面临的一个共同问题,也是我之后主要讨论的问题。 复制收集器的缺陷 通常来说复制收集器是stop-the-world的,也就是说只要垃圾收集在进行,应用程序就无法执行。对于这种实现来说,你需要复制的东西越多,对应用程序性能的影响就越大。对于那些响应时间敏感的应用来说这是个缺点。使用复制收集器时,你还要考虑最坏的场景(即from空间中的所有对象都是活动对象),这时你需要为移动这些活动对象准备足够大的空间,因此to空间必须大到可以装下from空间的所有对象。由于这个限制,复制算法的内存利用率稍有不足(译者注:在最坏的情况下to空间需要和from空间大小相同,所以只有50%的利用率)。 标记-清除收集器 部署在企业生产环境上的大多数商业JVM采用的都是标记-清除(或者叫标记)收集器,因为它没有复制垃圾收集器对应用程序性能的影响问题。其中最有名的标记收集器包括CMS、G1、GenPar和DeterministicGC。 标记-清除收集器跟踪对象引用,并且用标志位将每个找到的对象标记为live。这个标志位通常对应堆上的一个地址或是一组地址。例如:活动位可以是对象头的一个位(译者注:bit)或是一个位向量、一个位图。 在标记完成之后就进入了清除阶段。清除阶段通常都会再次遍历堆(不仅是标记为live的对象,而是整个堆),用来定位那些没有标记的连续内存地址空间(没有被标记的内存就是空闲并可回收的),然后收集器将它们整理为空闲列表。垃圾收集器可以有多个空闲列表(通常按照内存块的大小划分),有些JVM(例如:JRockit Real Time)的收集器甚至基于应用程序的性能分析和对象大小的统计结果来动态划分空闲列表。 清除阶段过后,应用程序就可以再次分配内存了。从空闲列表中为新对象分配内存时,新分配的内存块需要符合新对象的大小,或是线程的平均对象大小,或是应用程序的TLAB大小。为新对象找到大小合适的内存块有助于优化内存和减少碎片。 标记-清除收集器的缺陷 标记阶段的执行时间依赖于堆中活动对象的数量,而清除阶段的执行时间依赖于堆的大小。因此对于堆设置较大并且堆中活动对象较多的情况,标记-清除算法会有一定的暂停时间。 对于内存消耗很大的应用程序来说,你可以调整垃圾收集参数以适应各种应用程序的场景和需要。在很多情况下,这种调整至少推迟了标记阶段/清除阶段给应用程序或服务协议SLA(SLA这里指应用程序要达到的响应时间)带来的风险。但是调优仅仅对特定的负载和内存分配率有效,负载变化或是应用程序本身的修改都需要重新调优。 标记-清除收集器的实现 至少有两种已经在商业上验证的方法来实现标记-清除垃圾收集。一种是并行垃圾收集,另一种是并发(或者多数时间是并发)垃圾收集。 并行收集器 并行收集是指资源被垃圾收集线程并行使用。大多数并行收集的商业实现都是stop-the-world收集器,即所有的应用程序线程都暂停直到完成一次垃圾收集,因为垃圾收集器可以高效地使用资源,所以通常会在吞吐量的基准测试中得到高分,如SPECjbb。如果吞吐量对你的应用程序至关重要,那么并行垃圾收集器是一个很好的选择。 并行收集的主要代价(特别是对于生产环境)是应用程序线程在垃圾收集期间无法正常工作,就像复制收集器一样。因此那些对于响应时间敏感的应用程序使用并行收集器会有很大的影响。特别是在堆空间中有很多复杂的活动对象结构时,有很多的对象引用需要跟踪。(还记得吗标记-清除收集器回收内存的时间取决于跟踪活动对象集合的时间加上遍历整个堆的时间)对于并行方法来说,整个垃圾收集时间应用程序都会暂停。 并发收集器 并发垃圾收集器更适合那些对响应时间敏感的应用程序。并发意味着垃圾收集线程和应用程序线程并发执行。垃圾收集线程并不独占所有资源,因此需要决定何时开始一次垃圾收集,需要有足够的时间跟踪活动对象集合并在应用程序内存溢出前回收内存。如果垃圾收集没有及时完成,应用程序就会抛出内存溢出错误,另一方面又不希望垃圾收集执行时间太长因为那样会消耗应用程序的资源进而影响吞吐量。保持这种平衡是需要技巧的,因此在确定开始垃圾收集的时机以及选择垃圾收集优化的时机时都使用了启发式算法。 另一个难点在于确定何时可以安全执行一些操作(需要完整准确的堆快照的操作),例如:需要知道何时标记阶段完成,这样就可以进入清理阶段。对于stop-the-world的并行收集器来说这不成问题,因为世界已经暂停了(译者注:应用程序线程暂停,垃圾收集线程独占资源)。但对于并发收集器而言,从标记阶段立刻切换到清理阶段可能不安全。如果应用程序线程修改了一段内存,而这段内存已经被垃圾收集器跟踪并标注过了,这就可能产生了新的没有标注的引用。在一些并发收集实现中,这会使应用程序陷入长时间重复标注的循环,当应用程序需要这段内存时也无法获得空闲内存。 通过到目前为止的讨论我们知道有很多的垃圾收集器和垃圾收集算法,分别适合特定的应用程序类型和不同的负载。不仅是不同的算法,还有不同的算法实现。所以在指定垃圾收集器钱最好了解应用程序的需求以及自身特点。接下来我们将介绍Java平台内存模型的一些陷阱,这里陷阱的意思是,在动态变化的生产环境中Java程序员容易做出的一些使得应用程序性能变得更差的假设。 为什么调优无法代替垃圾收集 多数的Java程序员都知道如果要优化Java程序可以有很多选择。若干个可选的JVM、垃圾收集器和性能调优参数让开发者花费大量的时间在无休无尽的性能调优方面。这使有些人因此得出结论:垃圾收集是糟糕的,通过调优使垃圾收集较少发生或者持续时间较短是一个很好的变通办法,不过这样做是有风险的。 考虑一下针对具体应用程序的调优,多数的调优参数(例如内存分配率、对象大小、响应时间)都是基于当前测试的数据量对应用程序的内存分配率(译者注:或者其他参数)调整。最终可能造成以下两个结果: 1.在测试中通过的用例在生产环境中失败。 2.数据量的变化或者应用程序的变化要求重新调优。 调优是需要反复的,特别是并发垃圾收集器可能需要很多调优(尤其在生产环境中)。需要启发式方法来满足应用程序的需要。为了要满足最坏的情况,调优的结果可能是一个非常死板的配置,这也导致了大量的资源浪费。这种调优方法是一种堂吉诃德式的探索。事实上,你越是优化垃圾收集器来匹配特定的负载,越是远离了Java运行时的动态特性。毕竟有多少应用程序的负载是稳定的呢,你所预期的负载的可靠性又有多高呢? 那么如果你不将注意力放在调优上,能够做些什么来防止内存溢出错误和提高响应时间呢?首要的事情就是找到影响Java应用程序性能的主要因素。 碎片化 影响Java应用程序性能的因素不是垃圾收集器,而是碎片化以及垃圾收集器如何处理碎片化。所谓碎片化是这样一种状态:堆空间中有空闲可用的空间,但并没有足够大的连续内存空间,以至于无法为新对象分配内存。正如在第一篇中提到的,内存碎片要么是堆中残留的一段空间TLAB,要么是在长期存活对象中间被释放的小对象所占用的空间。 随着时间的推移和应用程序的运行,这些碎片就会遍布在堆中。在某些情况下,使用了静态化调优的参数可能会更糟,因为这些参数无法满足应用程序的动态需要。应用程序无法有效利用这些碎片化的空间。如果不做任何事情,那么将导致接连不断的垃圾收集,垃圾收集器尝试释放内存分配给新对象。在最坏的情况下,即使是接连不断的垃圾收集也无法释放更多的内存(碎片太多),然后JVM不得不抛出内存溢出的错误。你可以通过重启应用程序来解决碎片化,这样Java堆就有连续的内存空间可以分配给新对象。重启程序导致宕机,而且一段时间后Java堆将再次充满碎片,不得不再次重启。 内存溢出错误会挂起进程,日志显示垃圾收集器正在超负荷工作,这些都显示垃圾收集正试图释放内存,也表明堆中碎片很多。一些程序员会试图通过再次优化垃圾收集器来解决碎片化问题。但我认为应该寻找更有新意的办法解决这个问题。接下来的部分将重点讨论解决碎片化的两个办法:分代垃圾收集和压缩。 分代垃圾收集 你可能听过这样的理论:在生产环境中绝大多数对象的存活时间都很短。分代垃圾收集正是由这一理论衍生出的一种垃圾收集策略。在分代垃圾收集中,我们将堆分为不同的空间(或者叫做代),每个空间中保存着不同年龄的对象,所谓对象的年龄就是对象存活的垃圾收集周期数(也就是该对象多少个垃圾收集周期后仍然被引用)。 当新生代没有剩余空间可分配时,新生代的活动对象会被移动到老年代中(通常只有两个代。译者注:只有满足一定年龄的对象才会被移动到老年代),分代垃圾收集常常使用单向的复制收集器,一些更现代的JVM新生代中使用的是并行收集器,当然也可以为新生代和老年代分别实现不同的垃圾收集算法。如果你使用并行收集器或复制收集器,那么你的新生代收集器就是一个stop-the-world的收集器(参见之前的解释)。 老年代分配给那些从新生代移出的对象,这些对象要么是被引用很长一段时间,要么是被一些新生代中对象集合所引用。偶尔也有大对象直接被分配到了老年代,因为移动大对象的成本相对较高。 分代垃圾收集技术 在分代垃圾收集中,老年代运行垃圾收集的频率较低,而在新生代运行垃圾收集的频率较高,而我们也希望在新生代中垃圾收集周期更短。在极少的情况下,新生代的垃圾收集可能会比老年代的垃圾收集更频繁。如果你将新生代设置的太大时并且应用程序中的多数对象都存活较长时间,这种情况就可能会发生。在这种情况下,如果老年代设置的太小以至于无法容纳所有的长时间存活的对象,老年代的垃圾收集也会挣扎于释放空间给那些被移动进来的对象。不过通常来说分代垃圾收集可以使应用程序获得更好的性能。 划分出新生代的另一个好处是某种程度上解决了碎片化问题,或者说将最坏的情况推迟了。那些存活时间短的小对象本来可能产生碎片化问题,但都在新生代的垃圾收集中被清理了。由于存活时间长的对象被移到老年代时被更紧凑的分配空间,老年代也更加紧凑了。随着时间推移(如果你的应用运行时间足够长),老年代也会产生碎片化,这时需要运行一次或是几次完全垃圾收集,同时JVM也有可能抛出内存溢出错误。但是划分出新生代推迟了出现最坏情况的时间,这对于很多应用程序来说已经足够了。对于多数应用程序而言,它的确降低了stop-the-world垃圾收集的频率和内存溢出错误的机会。 优化分代垃圾收集 正如之前提到的,使用分代垃圾收集带来了重复的调优工作,例如调整新生代大小、提升率等。我无法针对具体应用运行时来强调怎样做取舍:选择固定的大小固然可以优化应用程序,但同时也减少了垃圾收集器应对动态变化的能力,而变化是不可避免的。 对于新生代首要原则就是在确保stop-the-world垃圾收集期间延迟时间前提下尽可能的加大,同时也要为那些长期存活的对象在堆中保留足够大的空间。下面是在调整分代垃圾收集器时要考虑的一些额外因素: 1.新生代中多数都是stop-the-world垃圾收集器,新生代设置的越大,相应的暂停时间就越长。因此对于那些受垃圾收集暂停时间影响大的应用程序来说,要仔细考虑将新生代设置为多大合适。 2.可以在不同的代上使用不同的垃圾收集算法。例如在新生代中使用并行垃圾收集,在老年代中使用并发垃圾收集。 3.当发现频繁的提升(译者注:从新生代移动到老年代)失败时说明老年代中碎片太多了,也就是说老年代中没有足够的空间来存放从新生代移出的对象。这时你可以调整一下提升率(即调整提升的年龄),或者确保老年代中的垃圾收集算法会进行压缩(将在下一段讨论)并调整压缩以适应应用程序的负载。也可以增加堆大小和各个代大小,但是这样更会进一步延长老年代上的暂停时间。要知道碎片化是无法避免的。 4.分代垃圾收集最适合这样的应用程序,他们有很多存活时间很短的小对象,很多对象在第一轮垃圾收集周期就被回收了。对于这种应用程序分代垃圾收集可以很好的减少碎片化,并将碎片化产生影响的时机推迟。 压缩 尽管分代垃圾收集延迟了出现碎片化和内存溢出错误的时间,然而压缩才是真正解决碎片化问题的唯一办法。压缩是指通过移动对象来释放连续内存块的垃圾收集策略,这样通过压缩为创建新对象释放了足够大的空间。 移动对象并更新对象引用是stop-the-world操作,会带来一定的消耗(有一种情况例外,将在本系列的下一篇中讨论)。存活的对象越多,压缩造成的暂停时间就越长。在剩余空间很少并且碎片化严重的情况下(这通常是因为程序运行了很长的时间),压缩存活对象较多的区域可能会有几秒种的暂停时间,而当接近内存溢出时,压缩整个堆甚至会花上几十秒的时间。 压缩的暂停时间取决于需要移动的内存大小和需要更新的引用数量。统计分析表明堆越大,需要移动的活动对象和更新的引用数量就越多。每移动1GB到2GB活动对象的暂停时间大约是1秒钟,对于4GB大小的堆很可能有25%的活动对象,因此偶尔会有大约1秒的暂停。 压缩和应用程序内存墙 应用程序内存墙是指在垃圾收集产生的暂停(例如:压缩)前可以设置的堆大小。根据系统和应用的不同,大多数的Java应用程序内存墙都在4GB到20GB之间。这也是多数的企业应用都是部署在多个较小的JVM上,而不是少数较大的JVM上的原因。让我们考虑一下这个问题:有多少现代企业的Java应用程序设计、部署是根据JVM的压缩限制来定义的。在这种情况下,为了绕过整理堆碎片的暂停时间,我们接受了更耗费管理成本的多个实例部署方案。考虑到现在硬件的大容量存储能力和企业级Java应用对增加内存的需求,这就有点奇怪了。为什么为每个实例只设置了几个GB的内存。并发压缩将会打破内存墙,这也是我下一篇文章的主题。 总结 本文是一篇关于垃圾收集的介绍性文章,帮助你了解有关垃圾收集的概念和机制,并希望能够促使你进一步阅读相关文章。这里讨论的很多东西都已经存在了很久,在下一篇中将介绍一些新的概念。例如并发压缩,目前是由Azul‘s Zing JVM实现的。它是一项新兴的垃圾收集技术,甚至尝试重新定义Java内存模型,特别是在今天内存和处理能力都不断提高的情况下。 以下是我总结出的一些关于垃圾收集的要点: 1.不同的垃圾收集算法和实现适应不同的应用程序需要,跟踪垃圾收集器是商业Java虚拟机中使用的最多的垃圾收集器。 2.并行垃圾收集在执行垃圾收集时并行使用所有资源。它通常是一个stop-the-world垃圾收集器,因此有更高的吞吐量,但是应用程序的工作线程必须等待垃圾收集线程完成,这对应用程序的响应时间有一定影响。 3.并发垃圾收集在执行收集时,应用程序工作线程仍然在运行。并发垃圾收集器需要在应用程序需要内存之前完成垃圾收集。 4.分代垃圾收集有助于延迟碎片化,但无法消除碎片化。分代垃圾收集将堆分为两个空间,其中一个空间存放新对象,另一个空间存放老对象。分代垃圾收集适合有很多存活时间很短的小对象的应用程序。 5.压缩是解决碎片化的唯一方法。多数的垃圾收集器都是以stop-the-world的方式执行压缩的,程序运行时间越久,对象引用越是复杂,对象的大小越是分布不均匀都将导致压缩时间延长。堆的大小也会影响压缩时间,因为可能有更多的活动对象和引用需要更新。 6.调优有助于延迟内存溢出错误。但是过度调优的结果是僵化的配置。在通过试错的方式开始调优之前,要确保清楚生产环境的负载、应用程序的对象类型以及对象引用的特性。过于僵化的配置很可能无法应付动态负载,因此在设置非动态值时一定要了解这样做的后果。 深入探讨C4(Concurrent Continuously Compacting Collector)垃圾收集算法
http://blog.csdn.net/oncealong/article/details/52096477
最近看了Tomcat后, 对Tomcat类加载还不是很清楚, 在网上找了这篇文章, 很赞. 原文排版更精美, 推荐阅读原文. 前言 说到本篇的tomcat类加载机制,不得不说翻译学习tomcat的初衷。 之前实习的时候学习javaMelody的源码,但是它是一个Maven的项目,与我们自己的web项目整合后无法直接断点调试。后来同事指导,说是直接把java类复制到src下就可以了。很纳闷….为什么会优先加载src下的java文件(编译出的class),而不是jar包中的class呢? 现在了解tomcat的类加载机制,原来一切是这么的简单。 类加载 在JVM中并不是一次性把所有的文件都加载到,而是一步一步的,按照需要来加载。 比如JVM启动时,会通过不同的类加载器加载不同的类。当用户在自己的代码中,需要某些额外的类时,再通过加载机制加载到JVM中,并且存放一段时间,便于频繁使用。 因此使用哪种类加载器、在什么位置加载类都是JVM中重要的知识。 JVM类加载 JVM类加载采用 父类委托机制,如下图所示: JVM中包括以下几种类加载器: 1 BootStrapClassLoader 引导类加载器 2 ExtClassLoader 扩展类加载器 3 AppClassLoader 应用类加载器 4 CustomClassLoader 用户自定义类加载器 他们的区别上面也都有说明。需要注意的是,不同的类加载器加载的类是不同的(注: JVM中的类的标记是通过类加载器和类的包名一起确定的, 即加载器+包名+类名, 即使同一个类, 由不同的加载器加载, 在JVM看来也是不同的类 ),因此如果用户加载器1加载的某个类,其他用户并不能够使用。 当JVM运行过程中,用户需要加载某些类时,会按照下面的步骤(父类委托机制): 1 用户自己的类加载器,把加载请求传给父加载器,父加载器再传给其父加载器,一直到加载器树的顶层。 2 最顶层的类加载器首先针对其特定的位置加载,如果加载不到就转交给子类。 3 如果一直到底层的类加载都没有加载到,那么就会抛出异常ClassNotFoundException。 因此,按照这个过程可以想到,如果同样在CLASSPATH指定的目录中和自己工作目录中存放相同的class,会优先加载CLASSPATH目录中的文件。 Tomcat类加载 在tomcat中类的加载稍有不同,如下图: 当tomcat启动时,会创建几种类加载器: 1. Bootstrap 引导类加载器 加载JVM启动所需的类,以及标准扩展类(位于jre/lib/ext下) 2. System 系统类加载器 加载tomcat启动的类,比如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下。 3. Common 通用类加载器 加载tomcat使用以及应用通用的一些类,位于CATALINA_HOME/lib下,比如servlet-api.jar 4. webapp 应用类加载器 每个应用在部署后,都会创建一个唯一的类加载器。该类加载器会加载位于 WEB-INF/lib下的jar文件中的class 和 WEB-INF/classes下的class文件。 当应用需要到某个类时,则会按照下面的顺序进行类加载: 1 使用bootstrap引导类加载器加载 2 使用system系统类加载器加载 3 使用应用类加载器在WEB-INF/classes中加载 4 使用应用类加载器在WEB-INF/lib中加载 5 使用common类加载器在CATALINA_HOME/lib中加载 问题扩展 通过对上面tomcat类加载机制的理解,就不难明白 为什么java文件放在Eclipse中的src文件夹下会优先jar包中的class? 这是因为Eclipse中的src文件夹中的文件java以及webContent中的JSP都会在tomcat启动时,被编译成class文件放在 WEB-INF/class 中。 而Eclipse外部引用的jar包,则相当于放在 WEB-INF/lib 中。 因此肯定是 java文件或者JSP文件编译出的class优先加载。 通过这样,我们就可以简单的把java文件放置在src文件夹中,通过对该java文件的修改以及调试,便于学习拥有源码java文件、却没有打包成xxx-source的jar包。 另外呢,开发者也会因为粗心而犯下面的错误。 在 CATALINA_HOME/lib 以及 WEB-INF/lib 中放置了 不同版本的jar包,此时就会导致某些情况下报加载不到类的错误。 还有如果多个应用使用同一jar包文件,当放置了多份,就可能导致 多个应用间 出现类加载不到的错误。 参考 【1】Tomcat Class Loader:http://tomcat.apache.org/tomcat-6.0-doc/class-loader-howto.html 【2】Tomcat 类加载机制:http://blog.csdn.net/dc_726/article/details/11873343 [转自: http://www.cnblogs.com/xing901022/p/4574961.html] Tomcat 是Web应用服务器,是一个Servlet/JSP容器. Tomcat 作为Servlet容器,负责处理客户请求,把请求传送给Servlet,并将Servlet的响应传送回给客户.而Servlet是一种运行在支持Java语言的服务器上的组件. Servlet最常见的用途是扩展Java Web服务器功能,提供非常安全的,可移植的,易于使用的CGI替代品.下面我们描述一下Tomcat与Servlet是如何工作的,首先看下面的时序图. 1、1、Web客户向Servlet容器(Tomcat)发出Http请求 2、Servlet容器分析客户的请求信息 3、Servlet容器创建一个HttpRequest对象,将客户请求的信息封装到这个对象中 4、Servlet容器创建一个HttpResponse对象 5、Servlet容器调用HttpServlet对象的service方法,把HttpRequest对象与HttpResponse对象作为参数 传给 HttpServlet对象 6、HttpServlet调用HttpRequest对象的有关方法,获取Http请求信息 7、HttpServlet调用HttpResponse对象的有关方法,生成响应数据 8、Servlet容器把HttpServlet的响应结果传给Web客户 看到以上这个过程,那么我们会问Servlet容器与HttpServlet又是基于什么样的约定进行交互的? HttpServlet对象的生命周期如何? 首先我们来了解一下Servlet对象的API Servlet的框架是由两个Java包组成的:javax.servlet与javax.servlet.http。在javax.servlet包中定义了所有 的Servlet类都必须实现或者扩展的通用接口和类。在javax.servlet.http包中定义了采用Http协议通信的 HttpServlet类。Servlet的框架的核心是javax.servlet.Servlet接口,所有的Servlet都必须实现这个接口。 在Servlet接口中定义了5个方法, 其中3个方法代表了Servlet的生命周期: 1、init方法:负责初始化Servlet对象。 2、service方法:负责响应客户的请求。 3、destroy方法:当Servlet对象退出生命周期时,负责释放占用的资源。 下面我们来看下面的类图。 在javax.servlet.Servlet接口中有一些do方法,它们对应的是http的请求方式。下面我们就结合类图来 描述一下HttpServlet对象的生命周期 一、创建Servlet对象的时机 1、Servlet容器启动时:读取web.xml配置文件中的信息,构造指定的Servlet对象,创建ServletConfig对象, 同时将ServletConfig对象作为参数来调用Servlet对象的init方法。 2、在Servlet容器启动后:客户首次向Servlet发出请求,Servlet容器会判断内存中是否存在指定的Servlet对 象,如果没有则创建它,然后根据客户的请求创建HttpRequest、HttpResponse对象,从而调用Servlet 对象的service方法。 3、Servlet的类文件被更新后,重新创建Servlet Servlet容器在启动时自动创建Servlet,这是由在web.xml文件中为Servlet设置的<load-on-startup>属性决定 的。从中我们也能看到同一个类型的Servlet对象在Servlet容器中以单例的形式存在。 二、销毁Servlet对象的时机 1、Servlet容器停止或者重新启动:Servlet容器调用Servlet对象的destroy方法来释放资源。 以上所讲的就是Servlet对象的生命周期。那么Servlet容器如何知道创建哪一个Servlet对象? Servlet对象如何配置?实际上这些信息是通过读取web.xml配置文件来实现的。 我们来看一下web.xml文件中的Servlet对象的配置节信息 ------------------------------------------- [html] view plain copy print? <servlet> <servlet-name>action<servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <init-param> <param-name>detail</param-name> <param-value>2</param-value> </init-param> <init-param> <param-name>debug</param-name> <param-value>2</param-value> </init-param> <load-on-startup>2</load-on-startup> </servlet> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> -------------------------------------------- 下面对上面的配置节信息进行解析 servlet-name:Servlet对象的名称 servlet-class:创建Servlet对象所要调用的类 param-name:参数名称 param-value:参数值 load-on-startup:Servlet容器启动时加载Servlet对象的顺序 servlet-mapping/servlet-name:要与servlet中的servlet-name配置节内容对应 url-pattern:客户访问的Servlet的相对URL路径 当Servlet容器启动的时候读取<servlet>配置节信息,根据<servlet-class>配置节信息创建Servlet对象, 同时根据<init-param>配置节信息创建HttpServletConfig对象,然后执行Servlet对象的init方法,并且根据 <load-on-startup>配置节信息来决定创建Servlet对象的顺序,如果此配置节信息为负数或者没有配置,那么 在Servlet容器启动时,将不加载此Servlet对象。 当客户访问Servlet容器时,Servlet容器根据客户访问的URL地址,通过<servlet-mapping>配置节中的<url-pattern> 配置节信息找到指定的Servlet对象,并调用此Servlet对象的service方法。 [转自: http://blog.csdn.net/coolwzjcool/article/details/5269802]
DataSet API和DataFrame两者结合起来,DataSet中许多的API模仿了RDD的API,实现不太一样,但是基于RDD的代码很容易移植过来。 spark未来基本是要在DataSet上扩展了,因为spark基于spark core关注的东西很多,整合内部代码是必然的。 1、加载文件 val rdd = sparkContext.textFile("./data.txt") val ds = sparkSession.read.text("./data.txt") 2、计算总数 rdd.count() ds.count() 3、wordcount实例 val wordsRDD = rdd.flatMap(value => value.split("\\s+")) val wordsPairs = wordsRDD.map(word => (word,1)) val wordCount = wordsPairs.reduceByKey(_+_) import sparkSession.implicits._ val wordsDs = ds.flatMap(value => value.split("\\s+")) val wordsPairDs = wordsDs.groupByKey(value => value) val wordCounts = wordsPairDs.count() 4、缓存 rdd.cache() ds.cache() 5、过滤 val filterRDD = wordsRDD.filter(value => value=="hello") val filterDs = wordsDs.filter(value => value = "hello") 6、map partition val mapPartitionsRDD = rdd.mapPartitions(iterator => List(iterator.count(value=>true)).iterator) val mapPartitionsDs = ds.mapPartitions(iterator => List(iterator.count(value=>true)).iterator) 7 、reduceByKey val reduceCountByRDD = wordsPair.reduceByKey(_+_) val reduceCountByDs = wordsPairDs.mapGroups((key,values) =>(key,values.length)) 8、RDD和 DataSet互换 val dsToRDD = ds.rdd val rddStringToRowRDD = rdd.map(value => Row(value)) val dfschema = StructType(Array(StructField("value",StringType))) val rddToDF = sparkSession.createDataFrame(rddStringToRowRDD,dfschema) val rDDToDataSet = rddToDF.as[String] 9、double val doubleRDD = sparkContext.makeRDD(List(1.0,5.0,8.9,9.0)) val rddSum =doubleRDD.sum() val rddMean = doubleRDD.mean() val rowRDD = doubleRDD.map(value => Row.fromSeq(List(value))) val schema = StructType(Array(StructField("value",DoubleType))) val doubleDS = sparkSession.createDataFrame(rowRDD,schema) import org.apache.spark.sql.functions._ doubleDS.agg(sum("value")) doubleDS.agg(mean("value")) 10、reduce val rddReduce = doubleRDD.reduce((a,b) => a +b) val dsReduce = doubleDS.reduce((row1,row2) =>Row(row1.getDouble(0) + row2.getDouble(0))) code import org.apache.spark.sql.types.{DoubleType, StringType, StructField, StructType} import org.apache.spark.sql.{Row, SparkSession} object RDDToDataSet { def main(args: Array[String]) { val sparkSession = SparkSession.builder.master("local") .appName("example") .getOrCreate() val sparkContext = sparkSession.sparkContext //read data from text file val rdd = sparkContext.textFile("src/main/resources/data.txt") val ds = sparkSession.read.text("src/main/resources/data.txt") // do count println("count ") println(rdd.count()) println(ds.count()) // wordcount println(" wordcount ") val wordsRDD = rdd.flatMap(value => value.split("\\s+")) val wordsPair = wordsRDD.map(word => (word,1)) val wordCount = wordsPair.reduceByKey(_+_) println(wordCount.collect.toList) import sparkSession.implicits._ val wordsDs = ds.flatMap(value => value.split("\\s+")) val wordsPairDs = wordsDs.groupByKey(value => value) val wordCountDs = wordsPairDs.count wordCountDs.show() //cache rdd.cache() ds.cache() //filter val filteredRDD = wordsRDD.filter(value => value =="hello") println(filteredRDD.collect().toList) val filteredDS = wordsDs.filter(value => value =="hello") filteredDS.show() //map partitions val mapPartitionsRDD = rdd.mapPartitions(iterator => List(iterator.count(value => true)).iterator) println(s" the count each partition is ${mapPartitionsRDD.collect().toList}") val mapPartitionsDs = ds.mapPartitions(iterator => List(iterator.count(value => true)).iterator) mapPartitionsDs.show() //converting to each other val dsToRDD = ds.rdd println(dsToRDD.collect()) val rddStringToRowRDD = rdd.map(value => Row(value)) val dfschema = StructType(Array(StructField("value",StringType))) val rddToDF = sparkSession.createDataFrame(rddStringToRowRDD,dfschema) val rDDToDataSet = rddToDF.as[String] rDDToDataSet.show() // double based operation val doubleRDD = sparkContext.makeRDD(List(1.0,5.0,8.9,9.0)) val rddSum =doubleRDD.sum() val rddMean = doubleRDD.mean() println(s"sum is $rddSum") println(s"mean is $rddMean") val rowRDD = doubleRDD.map(value => Row.fromSeq(List(value))) val schema = StructType(Array(StructField("value",DoubleType))) val doubleDS = sparkSession.createDataFrame(rowRDD,schema) import org.apache.spark.sql.functions._ doubleDS.agg(sum("value")).show() doubleDS.agg(mean("value")).show() //reduceByKey API val reduceCountByRDD = wordsPair.reduceByKey(_+_) val reduceCountByDs = wordsPairDs.mapGroups((key,values) =>(key,values.length)) println(reduceCountByRDD.collect().toList) println(reduceCountByDs.collect().toList) //reduce function val rddReduce = doubleRDD.reduce((a,b) => a +b) val dsReduce = doubleDS.reduce((row1,row2) => Row(row1.getDouble(0) + row2.getDouble(0))) println("rdd reduce is " +rddReduce +" dataset reduce "+dsReduce) } }
假如说你想复制一个简单变量。很简单: int apples = 5; int pears = apples; int apples = 5; int pears = apples; 不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适用于该类情况。 但是如果你复制的是一个对象,情况就有些复杂了。 假设说我是一个beginner,我会这样写: class Student { private int number; public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } } public class Test { public static void main(String args[]) { Student stu1 = new Student(); stu1.setNumber(12345); Student stu2 = stu1; System.out.println("学生1:" + stu1.getNumber()); System.out.println("学生2:" + stu2.getNumber()); } } class Student { private int number; public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } } public class Test { public static void main(String args[]) { Student stu1 = new Student(); stu1.setNumber(12345); Student stu2 = stu1; System.out.println("学生1:" + stu1.getNumber()); System.out.println("学生2:" + stu2.getNumber()); } } 打印结果: 学生1:12345 学生2:12345 学生1:12345 学生2:12345 这里我们自定义了一个学生类,该类只有一个number字段。 我们新建了一个学生实例,然后将该值赋值给stu2实例。(Student stu2 = stu1;) 再看看打印结果,作为一个新手,拍了拍胸腹,对象复制不过如此, 难道真的是这样吗? 我们试着改变stu2实例的number字段,再打印结果看看: stu2.setNumber(54321); System.out.println("学生1:" + stu1.getNumber()); System.out.println("学生2:" + stu2.getNumber()); stu2.setNumber(54321); System.out.println("学生1:" + stu1.getNumber()); System.out.println("学生2:" + stu2.getNumber()); 打印结果: 学生1:54321 学生2:54321 学生1:54321 学生2:54321 这就怪了,为什么改变学生2的学号,学生1的学号也发生了变化呢? 原因出在(stu2 = stu1) 这一句。该语句的作用是将stu1的引用赋值给stu2, 这样,stu1和stu2指向内存堆中同一个对象。如图: 那么,怎样才能达到复制一个对象呢? 是否记得万类之王Object。它有11个方法,有两个protected的方法,其中一个为clone方法。 该方法的签名是: protected native Object clone() throws CloneNotSupportedException; 因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,所以都不能在类外进行访问。 要想对一个对象进行复制,就需要对clone方法覆盖。 一般步骤是(浅复制): 被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常) 该接口为标记接口(不含任何方法) 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象,(native为本地方法) 面对上面那个方法进行改造: class Student implements Cloneable{ private int number; public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } @Override public Object clone() { Student stu = null; try{ stu = (Student)super.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return stu; } } public class Test { public static void main(String args[]) { Student stu1 = new Student(); stu1.setNumber(12345); Student stu2 = (Student)stu1.clone(); System.out.println("学生1:" + stu1.getNumber()); System.out.println("学生2:" + stu2.getNumber()); stu2.setNumber(54321); System.out.println("学生1:" + stu1.getNumber()); System.out.println("学生2:" + stu2.getNumber()); } } class Student implements Cloneable{ private int number; public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } @Override public Object clone() { Student stu = null; try{ stu = (Student)super.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return stu; } } public class Test { public static void main(String args[]) { Student stu1 = new Student(); stu1.setNumber(12345); Student stu2 = (Student)stu1.clone(); System.out.println("学生1:" + stu1.getNumber()); System.out.println("学生2:" + stu2.getNumber()); stu2.setNumber(54321); System.out.println("学生1:" + stu1.getNumber()); System.out.println("学生2:" + stu2.getNumber()); } } 打印结果: 学生1:12345 学生2:12345 学生1:12345 学生2:54321 学生1:12345 学生2:12345 学生1:12345 学生2:54321 如果你还不相信这两个对象不是同一个对象,那么你可以看看这一句: System.out.println(stu1 == stu2); // false System.out.println(stu1 == stu2); // false 上面的复制被称为浅复制(Shallow Copy),还有一种稍微复杂的深度复制(deep copy): 我们在学生类里再加一个Address类。 class Address { private String add; public String getAdd() { return add; } public void setAdd(String add) { this.add = add; } } class Student implements Cloneable{ private int number; private Address addr; public Address getAddr() { return addr; } public void setAddr(Address addr) { this.addr = addr; } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } @Override public Object clone() { Student stu = null; try{ stu = (Student)super.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return stu; } } public class Test { public static void main(String args[]) { Address addr = new Address(); addr.setAdd("杭州市"); Student stu1 = new Student(); stu1.setNumber(123); stu1.setAddr(addr); Student stu2 = (Student)stu1.clone(); System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd()); System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd()); } } class Address { private String add; public String getAdd() { return add; } public void setAdd(String add) { this.add = add; } } class Student implements Cloneable{ private int number; private Address addr; public Address getAddr() { return addr; } public void setAddr(Address addr) { this.addr = addr; } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } @Override public Object clone() { Student stu = null; try{ stu = (Student)super.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return stu; } } public class Test { public static void main(String args[]) { Address addr = new Address(); addr.setAdd("杭州市"); Student stu1 = new Student(); stu1.setNumber(123); stu1.setAddr(addr); Student stu2 = (Student)stu1.clone(); System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd()); System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd()); } } 打印结果: 学生1:123,地址:杭州市 学生2:123,地址:杭州市 学生1:123,地址:杭州市 学生2:123,地址:杭州市 乍一看没什么问题,真的是这样吗? 我们在main方法中试着改变addr实例的地址。 addr.setAdd("西湖区"); System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd()); System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd()); addr.setAdd("西湖区"); System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd()); System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd()); 打印结果: 学生1:123,地址:杭州市 学生2:123,地址:杭州市 学生1:123,地址:西湖区 学生2:123,地址:西湖区 学生1:123,地址:杭州市 学生2:123,地址:杭州市 学生1:123,地址:西湖区 学生2:123,地址:西湖区 这就奇怪了,怎么两个学生的地址都改变了? 原因是浅复制只是复制了addr变量的引用,并没有真正的开辟另一块空间,将值复制后再将引用返回给新对象。 所以,为了达到真正的复制对象,而不是纯粹引用复制。我们需要将Address类可复制化,并且修改clone方法,完整代码如下: package abc; class Address implements Cloneable { private String add; public String getAdd() { return add; } public void setAdd(String add) { this.add = add; } @Override public Object clone() { Address addr = null; try{ addr = (Address)super.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return addr; } } class Student implements Cloneable{ private int number; private Address addr; public Address getAddr() { return addr; } public void setAddr(Address addr) { this.addr = addr; } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } @Override public Object clone() { Student stu = null; try{ stu = (Student)super.clone(); //浅复制 }catch(CloneNotSupportedException e) { e.printStackTrace(); } stu.addr = (Address)addr.clone(); //深度复制 return stu; } } public class Test { public static void main(String args[]) { Address addr = new Address(); addr.setAdd("杭州市"); Student stu1 = new Student(); stu1.setNumber(123); stu1.setAddr(addr); Student stu2 = (Student)stu1.clone(); System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd()); System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd()); addr.setAdd("西湖区"); System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd()); System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd()); } } package abc; class Address implements Cloneable { private String add; public String getAdd() { return add; } public void setAdd(String add) { this.add = add; } @Override public Object clone() { Address addr = null; try{ addr = (Address)super.clone(); }catch(CloneNotSupportedException e) { e.printStackTrace(); } return addr; } } class Student implements Cloneable{ private int number; private Address addr; public Address getAddr() { return addr; } public void setAddr(Address addr) { this.addr = addr; } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } @Override public Object clone() { Student stu = null; try{ stu = (Student)super.clone(); //浅复制 }catch(CloneNotSupportedException e) { e.printStackTrace(); } stu.addr = (Address)addr.clone(); //深度复制 return stu; } } public class Test { public static void main(String args[]) { Address addr = new Address(); addr.setAdd("杭州市"); Student stu1 = new Student(); stu1.setNumber(123); stu1.setAddr(addr); Student stu2 = (Student)stu1.clone(); System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd()); System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd()); addr.setAdd("西湖区"); System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd()); System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd()); } } 打印结果: 学生1:123,地址:杭州市 学生2:123,地址:杭州市 学生1:123,地址:西湖区 学生2:123,地址:杭州市 学生1:123,地址:杭州市 学生2:123,地址:杭州市 学生1:123,地址:西湖区 学生2:123,地址:杭州市 这样结果就符合我们的想法了。 总结:浅拷贝是指在拷贝对象时,对于基本数据类型的变量会重新复制一份,而对于引用类型的变量只是对引用进行拷贝, 没有对引用指向的对象进行拷贝。 而深拷贝是指在拷贝对象时,同时会对引用指向的对象进行拷贝。 区别就在于是否对 对象中的引用变量所指向的对象进行拷贝。 最后我们可以看看API里其中一个实现了clone方法的类: java.util.Date: /** * Return a copy of this object. */ public Object clone() { Date d = null; try { d = (Date)super.clone(); if (cdate != null) { d.cdate = (BaseCalendar.Date) cdate.clone(); } } catch (CloneNotSupportedException e) {} // Won't happen return d; } /** * Return a copy of this object. */ public Object clone() { Date d = null; try { d = (Date)super.clone(); if (cdate != null) { d.cdate = (BaseCalendar.Date) cdate.clone(); } } catch (CloneNotSupportedException e) {} // Won't happen return d; } 该类其实也属于深度复制。
“static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。” 这段话虽然只是说明了static方法的特殊之处,但是可以看出static关键字的基本作用,简而言之,一句话来描述就是: 方便在没有创建对象的情况下来进行调用(方法/变量)。 很显然,被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。 static可以用来修饰类的成员方法、类的成员变量,另外可以编写static代码块来优化程序性能。 static方法 static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。 但是要注意的是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的。由于你无法预知在print1方法中是否访问了非静态成员变量,所以也禁止在静态成员方法中访问非静态成员方法。 而对于非静态成员方法,它访问静态成员方法/变量显然是毫无限制的。 因此,如果说想在不创建对象的情况下调用某个方法,就可以将这个方法设置为static。我们最常见的static方法就是main方法,至于为什么main方法必须是static的,现在就很清楚了。因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问。 另外记住,即使没有显示地声明为static,类的构造器实际上也是静态方法。 static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。 static成员变量的初始化顺序按照定义的顺序进行初始化。 static代码块 static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。 为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。下面看个例子: class Person{ private Date birthDate; public Person(Date birthDate) { this.birthDate = birthDate; } boolean isBornBoomer() { Date startDate = Date.valueOf("1946"); Date endDate = Date.valueOf("1964"); return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0; } } isBornBoomer是用来这个人是否是1946-1964年出生的,而每次isBornBoomer被调用的时候,都会生成startDate和birthDate两个对象,造成了空间浪费,如果改成这样效率会更好: class Person{ private Date birthDate; private static Date startDate,endDate; static{ startDate = Date.valueOf("1946"); endDate = Date.valueOf("1964"); } public Person(Date birthDate) { this.birthDate = birthDate; } boolean isBornBoomer() { return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0; } } 因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。 1.static关键字会改变类中成员的访问权限吗? 有些初学的朋友会将java中的static与C/C++中的static关键字的功能混淆了。在这里只需要记住一点:与C/C++中的static不同,Java中的static关键字不会影响到变量或者方法的作用域。在Java中能够影响到访问权限的只有private、public、protected(包括包访问权限)这几个关键字。static关键字并不会改变变量和方法的访问权限。 2、能通过this访问静态成员变量吗? 虽然对于静态方法来说没有this,那么在非静态方法中能够通过this访问静态成员变量吗? 这里面主要考察队this和static的理解。this代表什么?this代表当前对象,那么通过new Main()来调用printValue的话,当前对象就是通过new Main()生成的对象。而static变量是被对象所享有的,因此在printValue中的this.value的值毫无疑问是33。在printValue方法内部的value是局部变量,根本不可能与this关联,所以输出结果是33。在这里永远要记住一点:静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。 3、3.static能作用于局部变量么? 在C/C++中static是可以作用域局部变量的,但是在Java中切记:static是不允许用来修饰局部变量。 public class Test extends Base{ static{ System.out.println("test static"); } public Test(){ System.out.println("test constructor"); } public static void main(String[] args) { new Test(); } } class Base{ static{ System.out.println("base static"); } public Base(){ System.out.println("base constructor"); } } base static test static base constructor test constructor 至于为什么是这个结果,我们先不讨论,先来想一下这段代码具体的执行过程,在执行开始,先要寻找到main方法,因为main方法是程序的入口,但是在执行main方法之前,必须先加载Test类,而在加载Test类的时候发现Test类继承自Base类,因此会转去先加载Base类,在加载Base类的时候,发现有static块,便执行了static块。在Base类加载完成之后,便继续加载Test类,然后发现Test类中也有static块,便执行static块。在加载完所需的类之后,便开始执行main方法。在main方法中执行new Test()的时候会先调用父类的构造器,然后再调用自身的构造器。因此,便出现了上面的输出结果。 public class Test { Person person = new Person("Test"); static{ System.out.println("test static"); } public Test() { System.out.println("test constructor"); } public static void main(String[] args) { new MyClass(); } } class Person{ static{ System.out.println("person static"); } public Person(String str) { System.out.println("person "+str); } } class MyClass extends Test { Person person = new Person("MyClass"); static{ System.out.println("myclass static"); } public MyClass() { System.out.println("myclass constructor"); } } test static myclass static person static person Test test constructor person MyClass myclass constructor 类似地,我们还是来想一下这段代码的具体执行过程。首先加载Test类,因此会执行Test类中的static块。接着执行new MyClass(),而MyClass类还没有被加载,因此需要加载MyClass类。在加载MyClass类的时候,发现MyClass类继承自Test类,但是由于Test类已经被加载了,所以只需要加载MyClass类,那么就会执行MyClass类的中的static块。在加载完之后,就通过构造器来生成对象。而在生成对象的时候,必须先初始化父类的成员变量,因此会执行Test中的Person person = new Person(),而Person类还没有被加载过,因此会先加载Person类并执行Person类中的static块,接着执行父类的构造器,完成了父类的初始化,然后就来初始化自身了,因此会接着执行MyClass中的Person person = new Person(),最后执行MyClass的构造器。 public class Test { static{ System.out.println("test static 1"); } public static void main(String[] args) { } static{ System.out.println("test static 2"); } } test static 1 test static 2 虽然在main方法中没有任何语句,但是还是会输出,原因上面已经讲述过了。另外,static块可以出现类中的任何地方(只要不是方法内部,记住,任何方法内部都不行),并且执行是按照static块的顺序执行的。
类文件是以.java为后缀的代码文件,在每个类文件中最多只允许出现一个public类,当有public类的时候,类文件的名称必须和public类的名称相同,若不存在public,则类文件的名称可以为任意的名称(当然以数字开头的名称是不允许的)。 在类内部,对于成员变量,如果在定义的时候没有进行显示的赋值初始化,则Java会保证类的每个成员变量都得到恰当的初始化: 1)对于 char、short、byte、int、long、float、double等基本数据类型的变量来说会默认初始化为0(boolean变量默认会被初始化为false); 2)对于引用类型的变量,会默认初始化为null。 如果没有显示地定义构造器,则编译器会自动创建一个无参构造器,但是要记住一点,如果显示地定义了构造器,编译器就不会自动添加构造器。注意,所有的构造器默认为static的。 下面我们着重讲解一下 初始化 顺序: 当程序执行时,需要生成某个类的对象,Java执行引擎会先检查是否加载了这个类,如果没有加载,则先执行类的加载再生成对象,如果已经加载,则直接生成对象。 在类的加载过程中,类的static成员变量会被初始化,另外,如果类中有static语句块,则会执行static语句块。static成员变量和static语句块的执行顺序同代码中的顺序一致。记住,在Java中,类是按需加载,只有当需要用到这个类的时候,才会加载这个类,并且只会加载一次。看下面这个例子就明白了: public class Test { public static void main(String[] args) throws ClassNotFoundException { Bread bread1 = new Bread(); Bread bread2 = new Bread(); } } class Bread { static{ System.out.println("Bread is loaded"); } public Bread() { System.out.println("bread"); } } 运行这段代码就会发现”Bread is loaded”只会被打印一次。 在生成对象的过程中,会先初始化对象的成员变量,然后再执行构造器。也就是说类中的变量会在任何方法(包括构造器)调用之前得到初始化,即使变量散步于方法定义之间。 public class Test { public static void main(String[] args) { new Meal(); } } class Meal { public Meal() { System.out.println("meal"); } Bread bread = new Bread(); } class Bread { public Bread() { System.out.println("bread"); } } 输出结果为: bread meal 继承是所有OOP语言不可缺少的部分,在java中使用extends关键字来表示继承关系。当创建一个类时,总是在继承,如果没有明确指出要继承的类,就总是隐式地从根类Object进行继承。比如下面这段代码: class Person { public Person() { } } class Man extends Person { public Man() { } } 类Man继承于Person类,这样一来的话,Person类称为父类(基类),Man类称为子类(导出类)。如果两个类存在继承关系,则子类会自动继承父类的方法和变量,在子类中可以调用父类的方法和变量。在java中,只允许单继承,也就是说 一个类最多只能显示地继承于一个父类。但是一个类却可以被多个类继承,也就是说一个类可以拥有多个子类。 1.子类继承父类的成员变量 当子类继承了某个类之后,便可以使用父类中的成员变量,但是并不是完全继承父类的所有成员变量。具体的原则如下: 1)能够继承父类的public和protected成员变量;不能够继承父类的private成员变量; 2)对于父类的包访问权限成员变量,如果子类和父类在同一个包下,则子类能够继承;否则,子类不能够继承; 3)对于子类可以继承的父类成员变量,如果在子类中出现了同名称的成员变量,则会发生隐藏现象,即子类的成员变量会屏蔽掉父类的同名成员变量。如果要在子类中访问父类中同名成员变量,需要使用super关键字来进行引用。 2.子类继承父类的方法 同样地,子类也并不是完全继承父类的所有方法。 1)能够继承父类的public和protected成员方法;不能够继承父类的private成员方法; 2)对于父类的包访问权限成员方法,如果子类和父类在同一个包下,则子类能够继承;否则,子类不能够继承; 3)对于子类可以继承的父类成员方法,如果在子类中出现了同名称的成员方法,则称为覆盖,即子类的成员方法会覆盖掉父类的同名成员方法。如果要在子类中访问父类中同名成员方法,需要使用super关键字来进行引用。 注意:隐藏和覆盖是不同的。隐藏是针对成员变量和静态方法的,而覆盖是针对普通方法的。 3.构造器 子类是不能够继承父类的构造器,但是要注意的是,如果父类的构造器都是带有参数的,则必须在子类的构造器中显示地通过super关键字调用父类的构造器并配以适当的参数列表。如果父类有无参构造器,则在子类的构造器中用super关键字调用父类构造器不是必须的,如果没有使用super关键字,系统会自动调用父类的无参构造器。看下面这个例子就清楚了: class Shape { protected String name; public Shape(){ name = "shape"; } public Shape(String name) { this.name = name; } } class Circle extends Shape { private double radius; public Circle() { radius = 0; } public Circle(double radius) { this.radius = radius; } public Circle(double radius,String name) { this.radius = radius; this.name = name; } } super主要有两种用法: 1)super.成员变量/super.成员方法; 2)super(parameter1,parameter2….) 第一种用法主要用来在子类中调用父类的同名成员变量或者方法; 第二种主要用在子类的构造器中显示地调用父类的构造器,要注意的是,如果是用在子类构造器中,则必须是子类构造器的第一个语句。 三.常见的面试笔试题 1.下面这段代码的输出结果是什么? public class Test { public static void main(String[] args) { new Circle(); } } class Draw { public Draw(String type) { System.out.println(type+" draw constructor"); } } class Shape { private Draw draw = new Draw("shape"); public Shape(){ System.out.println("shape constructor"); } } class Circle extends Shape { private Draw draw = new Draw("circle"); public Circle() { System.out.println("circle constructor"); } } shape draw constructor shape constructor circle draw constructor circle constructor 这道题目主要考察的是类继承时构造器的调用顺序和初始化顺序。要记住一点:父类的构造器调用以及初始化过程一定在子类的前面。由于Circle类的父类是Shape类,所以Shape类先进行初始化,然后再执行Shape类的构造器。接着才是对子类Circle进行初始化,最后执行Circle的构造器。 2.下面这段代码的输出结果是什么? public class Test { public static void main(String[] args) { Shape shape = new Circle(); System.out.println(shape.name); shape.printType(); shape.printName(); } } class Shape { public String name = "shape"; public Shape(){ System.out.println("shape constructor"); } public void printType() { System.out.println("this is shape"); } public static void printName() { System.out.println("shape"); } } class Circle extends Shape { public String name = "circle"; public Circle() { System.out.println("circle constructor"); } public void printType() { System.out.println("this is circle"); } public static void printName() { System.out.println("circle"); } } shape constructor circle constructor shape this is circle shape 这道题主要考察了隐藏和覆盖的区别(当然也和多态相关,在后续博文中会继续讲到)。 覆盖只针对非静态方法(终态方法不能被继承,所以就存在覆盖一说了),而隐藏是针对成员变量和静态方法的。这2者之间的区别是:覆盖受RTTI(Runtime type identification)约束的,而隐藏却不受该约束。也就是说只有覆盖方法才会进行动态绑定,而隐藏是不会发生动态绑定的。在Java中,除了static方法和final方法,其他所有的方法都是动态绑定。因此,就会出现上面的输出结果。
对于面向对象编程来说,抽象是它的一大特征之一。在Java中,可以通过两种形式来体现OOP的抽象:接口和抽象类。这两者有太多相似的地方,又有太多不同的地方。很多人在初学的时候会以为它们可以随意互换使用,但是实际则不然。 一.抽象类 在了解抽象类之前,先来了解一下抽象方法。抽象方法是一种特殊的方法:它只有声明,而没有具体的实现。抽象方法的声明格式为: abstract void fun(); 抽象方法必须用abstract关键字进行修饰。如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用abstract关键字修饰。因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象。 下面要注意一个问题:在《JAVA编程思想》一书中,将抽象类定义为“包含抽象方法的类”,但是后面发现如果一个类不包含抽象方法,只是用abstract修饰的话也是抽象类。也就是说抽象类不一定必须含有抽象方法。个人觉得这个属于钻牛角尖的问题吧,因为如果一个抽象类不包含任何抽象方法,为何还要设计为抽象类?所以暂且记住这个概念吧,不必去深究为什么。 [public] abstract class ClassName { abstract void fun(); } 从这里可以看出,抽象类就是为了继承而存在的,如果你定义了一个抽象类,却不去继承它,那么等于白白创建了这个抽象类,因为你不能用它来做任何事情。对于一个父类,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为abstract方法,此时这个类也就成为abstract类了。 包含抽象方法的类称为抽象类,但并不意味着抽象类中只能有抽象方法,它和普通类一样,同样可以拥有成员变量和普通的成员方法。注意,抽象类和普通类的主要有三点区别: 1)抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。 2)抽象类不能用来创建对象; 3)如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。 在其他方面,抽象类和普通的类并没有区别。 二.接口 接口,英文称作interface,在软件工程中,接口泛指供别人调用的方法或者函数。从这里,我们可以体会到Java语言设计者的初衷,它是对行为的抽象。在Java中,定一个接口的形式如下: [public] interface InterfaceName { } 接口中可以含有 变量和方法。但是要注意,接口中的变量会被隐式地指定为public static final变量(并且只能是public static final变量,用private修饰会报编译错误),而方法会被隐式地指定为public abstract方法且只能是public abstract方法(用其他关键字,比如private、protected、static、 final等修饰会报编译错误),并且接口中所有的方法不能有具体的实现,也就是说,接口中的方法必须都是抽象方法。从这里可以隐约看出接口和抽象类的区别,接口是一种极度抽象的类型,它比抽象类更加“抽象”,并且一般情况下不在接口中定义变量。 要让一个类遵循某组特地的接口需要使用implements关键字,具体格式如下: class ClassName implements Interface1,Interface2,[....]{ } 可以看出,允许一个类遵循多个特定的接口。如果一个非抽象类遵循了某个接口,就必须实现该接口中的所有方法。对于遵循某个接口的抽象类,可以不实现该接口中的抽象方法。 三.抽象类和接口的区别 1.语法层面上的区别 1)抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法; 2)抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的; 3)接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法; 4)一个类只能继承一个抽象类,而一个类却可以实现多个接口。 2.设计层面上的区别 1)抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类Airplane,将鸟设计为一个类Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口Fly,包含方法fly( ),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。从这里可以看出,继承是一个 “是不是”的关系,而 接口 实现则是 “有没有”的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。 2)设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。什么是模板式设计?最简单例子,大家都用过ppt里面的模板,如果用模板A设计了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它们的公共部分需要改动,则只需要改动模板A就可以了,不需要重新对ppt B和ppt C进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。 下面看一个网上流传最广泛的例子:门和警报的例子:门都有open( )和close( )两个动作,此时我们可以定义通过抽象类和接口来定义这个抽象概念: abstract class Door { public abstract void open(); public abstract void close(); } 或者: interface Door { public abstract void open(); public abstract void close(); } 但是现在如果我们需要门具有报警alarm( )的功能,那么该如何实现?下面提供两种思路: 1)将这三个功能都放在抽象类里面,但是这样一来所有继承于这个抽象类的子类都具备了报警功能,但是有的门并不一定具备报警功能; 2)将这三个功能都放在接口里面,需要用到报警功能的类就需要实现这个接口中的open( )和close( ),也许这个类根本就不具备open( )和close( )这两个功能,比如火灾报警器。 从这里可以看出, Door的open() 、close()和alarm()根本就属于两个不同范畴内的行为,open()和close()属于门本身固有的行为特性,而alarm()属于延伸的附加行为。因此最好的解决办法是单独将报警设计为一个接口,包含alarm()行为,Door设计为单独的一个抽象类,包含open和close两种行为。再设计一个报警门继承Door类和实现Alarm接口。 interface Alram { void alarm(); } abstract class Door { void open(); void close(); } class AlarmDoor extends Door implements Alarm { void oepn() { //.... } void close() { //.... } void alarm() { //.... } } 参考资料: http://blog.csdn.net/chenssy/article/details/12858267 http://dev.yesky.com/436/7581936.shtml http://blog.csdn.net/xw13106209/article/details/6923556 http://android.blog.51cto.com/268543/385282/ http://peiquan.blog.51cto.com/7518552/1271610
谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字。另外,Java中的String类就是一个final类,那么今天我们就来了解final这个关键字的用法。下面是本文的目录大纲: 一.final关键字的基本用法 二.深入理解final关键字 若有不正之处,请多多谅解并欢迎指正。 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/dolphin0520/p/3736238.html 一.final关键字的基本用法 在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。下面就从这三个方面来了解一下final关键字的基本用法。 1.修饰类 当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。 在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。 2.修饰方法 下面这段话摘自《Java编程思想》第四版第143页: “使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。“ 因此,如果只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。 注:类的private方法会隐式地被指定为final方法。 3.修饰变量 修饰变量是final用得最多的地方,也是本文接下来要重点阐述的内容。首先了解一下final变量的基本语法: 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。 举个例子: 上面的一段代码中,对变量i和obj的重新赋值都报错了。 二.深入理解final关键字 在了解了final关键字的基本用法之后,这一节我们来看一下final关键字容易混淆的地方。 1.类的final变量和普通变量有什么区别? 当用final作用于类的成员变量时,成员变量(注意是类的成员变量,局部变量只需要保证在使用之前被初始化赋值即可)必须在定义时或者构造器中进行初始化赋值,而且final变量一旦被初始化赋值之后,就不能再被赋值了。 那么final变量和普通变量到底有何区别呢?下面请看一个例子: 1 2 3 4 5 6 7 8 9 10 11 public class Test { public static void main(String[] args) { String a = "hello2"; final String b = "hello"; String d = "hello"; String c = b + 2; String e = d + 2; System.out.println((a == c)); System.out.println((a == e)); } } View Code 大家可以先想一下这道题的输出结果。为什么第一个比较结果为true,而第二个比较结果为fasle。这里面就是final变量和普通变量的区别了,当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。这种和C语言中的宏替换有点像。因此在上面的一段代码中,由于变量b被final修饰,因此会被当做编译器常量,所以在使用到b的地方会直接将变量b 替换为它的 值。而对于变量d的访问却需要在运行时通过链接来进行。想必其中的区别大家应该明白了,不过要注意,只有在编译期间能确切知道final变量值的情况下,编译器才会进行这样的优化,比如下面的这段代码就不会进行优化: 1 2 3 4 5 6 7 8 9 10 11 12 13 public class Test { public static void main(String[] args) { String a = "hello2"; final String b = getHello(); String c = b + 2; System.out.println((a == c)); } public static String getHello() { return "hello"; } } 这段代码的输出结果为false。 2.被final修饰的引用变量指向的对象内容可变吗? 在上面提到被final修饰的引用变量一旦初始化赋值之后就不能再指向其他的对象,那么该引用变量指向的对象的内容可变吗?看下面这个例子: 1 2 3 4 5 6 7 8 9 10 11 public class Test { public static void main(String[] args) { final MyClass myClass = new MyClass(); System.out.println(++myClass.i); } } class MyClass { public int i = 0; } 这段代码可以顺利编译通过并且有输出结果,输出结果为1。这说明引用变量被final修饰之后,虽然不能再指向其他对象,但是它指向的对象的内容是可变的。 3.final和static 很多时候会容易把static和final关键字混淆,static作用于成员变量用来表示只保存一份副本,而final的作用是用来保证变量不可变。看下面这个例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Test { public static void main(String[] args) { MyClass myClass1 = new MyClass(); MyClass myClass2 = new MyClass(); System.out.println(myClass1.i); System.out.println(myClass1.j); System.out.println(myClass2.i); System.out.println(myClass2.j); } } class MyClass { public final double i = Math.random(); public static double j = Math.random(); } 运行这段代码就会发现,每次打印的两个j值都是一样的,而i的值却是不同的。从这里就可以知道final和static变量的区别了。 4.匿名内部类中使用的外部局部变量为什么只能是final变量? 这个问题请参见上一篇博文中《Java内部类详解》中的解释,在此处不再赘述。 5.关于final参数的问题 关于网上流传的”当你在方法中不需要改变作为参数的对象变量时,明确使用final进行声明,会防止你无意的修改而影响到调用方法外的变量“这句话,我个人理解这样说是不恰当的。 因为无论参数是基本数据类型的变量还是引用类型的变量,使用final声明都不会达到上面所说的效果。 看这个例子就清楚了: 上面这段代码好像让人觉得用final修饰之后,就不能在方法中更改变量i的值了。殊不知,方法changeValue和main方法中的变量i根本就不是一个变量,因为java参数传递采用的是值传递,对于基本类型的变量,相当于直接将变量进行了拷贝。所以即使没有final修饰的情况下,在方法内部改变了变量i的值也不会影响方法外的i。 再看下面这段代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Test { public static void main(String[] args) { MyClass myClass = new MyClass(); StringBuffer buffer = new StringBuffer("hello"); myClass.changeValue(buffer); System.out.println(buffer.toString()); } } class MyClass { void changeValue(final StringBuffer buffer) { buffer.append("world"); } } 运行这段代码就会发现输出结果为 helloworld。很显然,用final进行修饰并没有阻止在changeValue中改变buffer指向的对象的内容。有人说假如把final去掉了,万一在changeValue中让buffer指向了其他对象怎么办。有这种想法的朋友可以自己动手写代码试一下这样的结果是什么,如果把final去掉了,然后在changeValue中让buffer指向了其他对象,也不会影响到main方法中的buffer,原因在于java采用的是值传递,对于引用变量,传递的是引用的值,也就是说让实参和形参同时指向了同一个对象,因此让形参重新指向另一个对象对实参并没有任何影响。 所以关于网上流传的final参数的说法,我个人不是很赞同。 参考资料: 《Java编程思想》
说起内部类这个词,想必很多人都不陌生,但是又会觉得不熟悉。原因是平时编写代码时可能用到的场景不多,用得最多的是在有事件监听的情况下,并且即使用到也很少去总结内部类的用法。今天我们就来一探究竟。下面是本文的目录大纲: 一.内部类基础 二.深入理解内部类 三.内部类的使用场景和好处 四.常见的与内部类相关的笔试面试题 若有不正之处,请多谅解并欢迎批评指正。 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/dolphin0520/p/3811445.html 一.内部类基础 在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。下面就先来了解一下这四种内部类的用法。 1.成员内部类 成员内部类是最普通的内部类,它的定义为位于另一个类的内部,形如下面的形式: 1 2 3 4 5 6 7 8 9 10 11 12 13 class Circle { double radius = 0; public Circle(double radius) { this.radius = radius; } class Draw { //内部类 public void drawSahpe() { System.out.println("drawshape"); } } } 这样看起来,类Draw像是类Circle的一个成员,Circle称为外部类。成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Circle { private double radius = 0; public static int count =1; public Circle(double radius) { this.radius = radius; } class Draw { //内部类 public void drawSahpe() { System.out.println(radius); //外部类的private成员 System.out.println(count); //外部类的静态成员 } } } 不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问: 1 2 外部类.this.成员变量 外部类.this.成员方法 虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Circle { private double radius = 0; public Circle(double radius) { this.radius = radius; getDrawInstance().drawSahpe(); //必须先创建成员内部类的对象,再进行访问 } private Draw getDrawInstance() { return new Draw(); } class Draw { //内部类 public void drawSahpe() { System.out.println(radius); //外部类的private成员 } } } 成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Test { public static void main(String[] args) { //第一种方式: Outter outter = new Outter(); Outter.Inner inner = outter.new Inner(); //必须通过Outter对象来创建 //第二种方式: Outter.Inner inner1 = outter.getInnerInstance(); } } class Outter { private Inner inner = null; public Outter() { } public Inner getInnerInstance() { if(inner == null) inner = new Inner(); return inner; } class Inner { public Inner() { } } } 内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。 2.局部内部类 局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class People{ public People() { } } class Man{ public Man(){ } public People getWoman(){ class Woman extends People{ //局部内部类 int age =0; } return new Woman(); } } 注意,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。 3.匿名内部类 匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。下面这段代码是一段Android事件监听代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 scan_bt.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub } }); history_bt.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub } }); 这段代码为两个按钮设置监听器,这里面就使用了匿名内部类。这段代码中的: 1 2 3 4 5 6 7 8 new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub } } 就是匿名内部类的使用。代码中需要给按钮设置监听器对象,使用匿名内部类能够在实现父类或者接口中的方法情况下同时产生一个相应的对象,但是前提是这个父类或者接口必须先存在才能这样使用。当然像下面这种写法也是可以的,跟上面使用匿名内部类达到效果相同。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void setListener() { scan_bt.setOnClickListener(new Listener1()); history_bt.setOnClickListener(new Listener2()); } class Listener1 implements View.OnClickListener{ @Override public void onClick(View v) { // TODO Auto-generated method stub } } class Listener2 implements View.OnClickListener{ @Override public void onClick(View v) { // TODO Auto-generated method stub } } 这种写法虽然能达到一样的效果,但是既冗长又难以维护,所以一般使用匿名内部类的方法来编写事件监听代码。同样的,匿名内部类也是不能有访问修饰符和static修饰符的。 匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。 4.静态内部类 静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Test { public static void main(String[] args) { Outter.Inner inner = new Outter.Inner(); } } class Outter { public Outter() { } static class Inner { public Inner() { } } } 二.深入理解内部类 1.为什么成员内部类可以无条件访问外部类的成员? 在此之前,我们已经讨论过了成员内部类可以无条件访问外部类的成员,那具体究竟是如何实现的呢?下面通过反编译字节码文件看看究竟。事实上,编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件,下面是Outter.java的代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Outter { private Inner inner = null; public Outter() { } public Inner getInnerInstance() { if(inner == null) inner = new Inner(); return inner; } protected class Inner { public Inner() { } } } 编译之后,出现了两个字节码文件: 反编译Outter$Inner.class文件得到下面信息: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 E:\Workspace\Test\bin\com\cxh\test2>javap -v Outter$Inner Compiled from "Outter.java" public class com.cxh.test2.Outter$Inner extends java.lang.Object SourceFile: "Outter.java" InnerClass: #24= #1 of #22; //Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes t2/Outter minor version: 0 major version: 50 Constant pool: const #1 = class #2; // com/cxh/test2/Outter$Inner const #2 = Asciz com/cxh/test2/Outter$Inner; const #3 = class #4; // java/lang/Object const #4 = Asciz java/lang/Object; const #5 = Asciz this$0; const #6 = Asciz Lcom/cxh/test2/Outter;; const #7 = Asciz <init>; const #8 = Asciz (Lcom/cxh/test2/Outter;)V; const #9 = Asciz Code; const #10 = Field #1.#11; // com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t est2/Outter; const #11 = NameAndType #5:#6;// this$0:Lcom/cxh/test2/Outter; const #12 = Method #3.#13; // java/lang/Object."<init>":()V const #13 = NameAndType #7:#14;// "<init>":()V const #14 = Asciz ()V; const #15 = Asciz LineNumberTable; const #16 = Asciz LocalVariableTable; const #17 = Asciz this; const #18 = Asciz Lcom/cxh/test2/Outter$Inner;; const #19 = Asciz SourceFile; const #20 = Asciz Outter.java; const #21 = Asciz InnerClasses; const #22 = class #23; // com/cxh/test2/Outter const #23 = Asciz com/cxh/test2/Outter; const #24 = Asciz Inner; { final com.cxh.test2.Outter this$0; public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter); Code: Stack=2, Locals=2, Args_size=2 0: aload_0 1: aload_1 2: putfield #10; //Field this$0:Lcom/cxh/test2/Outter; 5: aload_0 6: invokespecial #12; //Method java/lang/Object."<init>":()V 9: return LineNumberTable: line 16: 0 line 18: 9 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/cxh/test2/Outter$Inner; } 第11行到35行是常量池的内容,下面逐一第38行的内容: final com.cxh.test2.Outter this$0; 这行是一个指向外部类对象的指针,看到这里想必大家豁然开朗了。也就是说编译器会默认为成员内部类添加了一个指向外部类对象的引用,那么这个引用是如何赋初值的呢?下面接着看内部类的构造器: public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter); 从这里可以看出,虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。 2.为什么局部内部类和匿名内部类只能访问局部final变量? 想必这个问题也曾经困扰过很多人,在讨论这个问题之前,先看下面这段代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Test { public static void main(String[] args) { } public void test(final int b) { final int a = 10; new Thread(){ public void run() { System.out.println(a); System.out.println(b); }; }.start(); } } 这段代码会被编译成两个class文件:Test.class和Test1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outter1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outterx.class(x为正整数)。 根据上图可知,test方法中的匿名内部类的名字被起为 Test$1。 上段代码中,如果把变量a和b前面的任一个final去掉,这段代码都编译不过。我们先考虑这样一个问题: 当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,怎么办呢?Java采用了 复制 的手段来解决这个问题。将这段代码的字节码反编译可以得到下面的内容: 我们看到在run方法中有一条指令: bipush 10 这条指令表示将操作数10压栈,表示使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。 下面再看一个例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 public class Test { public static void main(String[] args) { } public void test(final int a) { new Thread(){ public void run() { System.out.println(a); }; }.start(); } } 反编译得到: 我们看到匿名内部类Test$1的构造器含有两个参数,一个是指向外部类对象的引用,一个是int型变量,很显然,这里是将变量test方法中的形参a以参数的形式传进来对匿名内部类中的拷贝(变量a的拷贝)进行赋值初始化。 也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。 从上面可以看出,在run方法中访问的变量a根本就不是test方法中的局部变量a。这样一来就解决了前面所说的 生命周期不一致的问题。但是新的问题又来了,既然在run方法中访问的变量a和test方法中的变量a不是同一个变量,当在run方法中改变变量a的值的话,会出现什么情况? 对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。 到这里,想必大家应该清楚为何 方法中的局部变量和形参都必须用final进行限定了。 3.静态内部类有特殊的地方吗? 从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译class文件看一下就知道了,是没有Outter this&0引用的。 三.内部类的使用场景和好处 为什么在Java中需要内部类?总结一下主要有以下四点: 1.每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整, 2.方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。 3.方便编写事件驱动程序 4.方便编写线程代码 个人觉得第一点是最重要的原因之一,内部类的存在使得Java的多继承机制变得更加完善。 四.常见的与内部类相关的笔试面试题 1.根据注释填写(1),(2),(3)处的代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class Test{ public static void main(String[] args){ // 初始化Bean1 (1) bean1.I++; // 初始化Bean2 (2) bean2.J++; //初始化Bean3 (3) bean3.k++; } class Bean1{ public int I = 0; } static class Bean2{ public int J = 0; } } class Bean{ class Bean3{ public int k = 0; } } 从前面可知,对于成员内部类,必须先产生外部类的实例化对象,才能产生内部类的实例化对象。而静态内部类不用产生外部类的实例化对象即可产生内部类的实例化对象。 创建静态内部类对象的一般形式为: 外部类类名.内部类类名 xxx = new 外部类类名.内部类类名() 创建成员内部类对象的一般形式为: 外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名() 因此,(1),(2),(3)处的代码分别为: View Code View Code View Code 2.下面这段代码的输出结果是什么? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Test { public static void main(String[] args) { Outter outter = new Outter(); outter.new Inner().print(); } } class Outter { private int a = 1; class Inner { private int a = 2; public void print() { int a = 3; System.out.println("局部变量:" + a); System.out.println("内部类变量:" + this.a); System.out.println("外部类变量:" + Outter.this.a); } } } View Code 最后补充一点知识:关于成员内部类的继承问题。一般来说,内部类是很少用来作为继承用的。但是当用来继承的话,要注意两点: 1)成员内部类的引用方式必须为 Outter.Inner. 2)构造器中必须有指向外部类对象的引用,并通过这个引用调用super()。这段代码摘自《Java编程思想》 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class WithInner { class Inner{ } } class InheritInner extends WithInner.Inner { // InheritInner() 是不能通过编译的,一定要加上形参 InheritInner(WithInner wi) { wi.super(); //必须有这句调用 } public static void main(String[] args) { WithInner wi = new WithInner(); InheritInner obj = new InheritInner(wi); } } 参考资料: 《java编程思想》 http://www.cnblogs.com/chenssy/p/3388487.html http://blog.csdn.net/zhangjg_blog/article/details/20000769 http://blog.csdn.net/zhangjg_blog/article/details/19996629 http://blog.csdn.net/zhaoqianjava/article/details/6849812 http://www.cnblogs.com/nerxious/archive/2013/01/24/2875649.html
字节流与和字符流的使用非常相似,两者除了操作代码上的不同之外,是否还有其他的不同呢? 实际上字节流在操作时本身不会用到缓冲区(内存),是文件本身直接操作的,而字符流在操作时使用了缓冲区,通过缓冲区再操作文件,如图12-6所示。 下面以两个写文件的操作为主进行比较,但是在操作时字节流和字符流的操作完成之后都不关闭输出流。 范例:使用字节流不关闭执行 package org.lxh.demo12.byteiodemo; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; public class OutputStreamDemo05 { public static void main(String[] args) throws Exception { // 异常抛出, 不处理 // 第1步:使用File类找到一个文件 File f = new File("d:" + File.separator + "test.txt"); // 声明File 对象 // 第2步:通过子类实例化父类对象 OutputStream out = null; // 准备好一个输出的对象 out = new FileOutputStream(f); // 通过对象多态性进行实例化 // 第3步:进行写操作 String str = "Hello World!!!"; // 准备一个字符串 byte b[] = str.getBytes(); // 字符串转byte数组 out.write(b); // 将内容输出 // 第4步:关闭输出流 // out.close(); // 此时没有关闭 } } 程序运行结果: 此时没有关闭字节流操作,但是文件中也依然存在了输出的内容,证明字节流是直接操作文件本身的。而下面继续使用字符流完成,再观察效果。 范例:使用字符流不关闭执行 package org.lxh.demo12.chariodemo; import java.io.File; import java.io.FileWriter; import java.io.Writer; public class WriterDemo03 { public static void main(String[] args) throws Exception { // 异常抛出, 不处理 // 第1步:使用File类找到一个文件 File f = new File("d:" + File.separator + "test.txt");// 声明File 对象 // 第2步:通过子类实例化父类对象 Writer out = null; // 准备好一个输出的对象 out = new FileWriter(f); // 通过对象多态性进行实例化 // 第3步:进行写操作 String str = "Hello World!!!"; // 准备一个字符串 out.write(str); // 将内容输出 // 第4步:关闭输出流 // out.close(); // 此时没有关闭 } } 程序运行结果: 程序运行后会发现文件中没有任何内容,这是因为字符流操作时使用了缓冲区,而 在关闭字符流时会强制性地将缓冲区中的内容进行输出,但是如果程序没有关闭,则缓冲区中的内容是无法输出的,所以得出结论:字符流使用了缓冲区,而字节流没有使用缓冲区。 提问:什么叫缓冲区? 在很多地方都碰到缓冲区这个名词,那么到底什么是缓冲区?又有什么作用呢? 回答:缓冲区可以简单地理解为一段内存区域。 可以简单地把缓冲区理解为一段特殊的内存。 某些情况下,如果一个程序频繁地操作一个资源(如文件或数据库),则性能会很低,此时为了提升性能,就可以将一部分数据暂时读入到内存的一块区域之中,以后直接从此区域中读取数据即可,因为读取内存速度会比较快,这样可以提升程序的性能。 在字符流的操作中,所有的字符都是在内存中形成的,在输出前会将所有的内容暂时保存在内存之中,所以使用了缓冲区暂存数据。 如果想在不关闭时也可以将字符流的内容全部输出,则可以使用Writer类中的flush()方法完成。 范例:强制性清空缓冲区 package org.lxh.demo12.chariodemo; import java.io.File; import java.io.FileWriter; import java.io.Writer; public class WriterDemo04 { public static void main(String[] args) throws Exception { // 异常抛出不处理 // 第1步:使用File类找到一个文件 File f = new File("d:" + File.separator + "test.txt");// 声明File 对象 // 第2步:通过子类实例化父类对象 Writer out = null; // 准备好一个输出的对象 out = new FileWriter(f); // 通过对象多态性进行实例化 // 第3步:进行写操作 String str = "Hello World!!!"; // 准备一个字符串 out.write(str); // 将内容输出 out.flush(); // 强制性清空缓冲区中的内容 // 第4步:关闭输出流 // out.close(); // 此时没有关闭 } } 程序运行结果: 此时,文件中已经存在了内容,更进一步证明内容是保存在缓冲区的。这一点在读者日后的开发中要特别引起注意。 提问:使用字节流好还是字符流好? 学习完字节流和字符流的基本操作后,已经大概地明白了操作流程的各个区别,那么在开发中是使用字节流好还是字符流好呢? 回答:使用字节流更好。 在回答之前,先为读者讲解这样的一个概念,所有的文件在硬盘或在传输时都是以字节的方式进行的,包括图片等都是按字节的方式存储的,而字符是只有在内存中才会形成,所以在开发中,字节流使用较为广泛。 字节流与字符流主要的区别是他们的的处理方式 流分类: 1.Java的字节流 InputStream是所有字节输入流的祖先,而OutputStream是所有字节输出流的祖先。 2.Java的字符流 Reader是所有读取字符串输入流的祖先,而writer是所有输出字符串的祖先。 InputStream,OutputStream,Reader,writer都是抽象类。所以不能直接new 字节流是最基本的,所有的InputStream和OutputStream的子类都是,主要用在处理二进制数据,它是按字节来处理的 但实际中很多的数据是文本,又提出了字符流的概念,它是按虚拟机的encode来处理,也就是要进行字符集的转化 这两个之间通过 InputStreamReader,OutputStreamWriter来关联,实际上是通过byte[]和String来关联 在实际开发中出现的汉字问题实际上都是在字符流和字节流之间转化不统一而造成的 在从字节流转化为字符流时,实际上就是byte[]转化为String时, public String(byte bytes[], String charsetName) 有一个关键的参数字符集编码,通常我们都省略了,那系统就用操作系统的lang 而在字符流转化为字节流时,实际上是String转化为byte[]时, byte[] String.getBytes(String charsetName) 也是一样的道理 至于java.io中还出现了许多其他的流,按主要是为了提高性能和使用方便, 如BufferedInputStream,PipedInputStream等
1.什么是IO Java中I/O操作主要是指使用Java进行输入,输出操作. Java所有的I/O机制都是基于数据流进行输入输出,这些数据流表示了字符或者字节数据的流动序列。Java的I/O流提供了读写数据的标准方法。任何Java中表示数据源的对象都会提供以数据流的方式读写它的数据的方法。 Java.io是大多数面向数据流的输入/输出类的主要软件包。此外,Java也对块传输提供支持,在核心库 java.nio中采用的便是块IO。 流IO的好处是简单易用,缺点是效率较低。块IO效率很高,但编程比较复杂。 Java IO模型 : Java的IO模型设计非常优秀,它使用Decorator模式,按功能划分Stream,您可以动态装配这些Stream,以便获得您需要的功能。例如,您需要一个具有缓冲的文件输入流,则应当组合使用FileInputStream和BufferedInputStream。 2.数据流的基本概念 数据流是一串连续不断的数据的集合,就象水管里的水流,在水管的一端一点一点地供水,而在水管的另一端看到的是一股连续不断的水流。数据写入程序可以是一段、一段地向数据流管道中写入数据,这些数据段会按先后顺序形成一个长的数据流。对数据读取程序来说,看不到数据流在写入时的分段情况,每次可以读取其中的任意长度的数据,但只能先读取前面的数据后,再读取后面的数据。不管写入时是将数据分多次写入,还是作为一个整体一次写入,读取时的效果都是完全一样的。 “流是磁盘或其它外围设备中存储的数据的源点或终点。” 在电脑上的数据有三种存储方式,一种是外存,一种是内存,一种是缓存。比如电脑上的硬盘,磁盘,U盘等都是外存,在电脑上有内存条,缓存是在CPU里面的。外存的存储量最大,其次是内存,最后是缓存,但是外存的数据的读取最慢,其次是内存,缓存最快。这里总结从外存读取数据到内存以及将数据从内存写到外存中。对于内存和外存的理解,我们可以简单的理解为容器,即外存是一个容器,内存又是另外一个容器。那又怎样把放在外存这个容器内的数据读取到内存这个容器以及怎么把内存这个容器里的数据存到外存中呢? 在Java类库中,IO部分的内容是很庞大的,因为它涉及的领域很广泛: 标准输入输出,文件的操作,网络上的数据流,字符串流,对象流,zip文件流等等,java中将输入输出抽象称为流,就好像水管,将两个容器连接起来。将数据冲外存中读取到内存中的称为输入流,将数据从内存写入外存中的称为输出流。 流是一个很形象的概念,当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件,内存,或是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。 总结的基本概念如下: 1) 数据流: 一组有序,有起点和终点的字节的数据序列。包括输入流和输出流。 2) 输入流(Input Stream): 程序从输入流读取数据源。数据源包括外界(键盘、文件、网络…),即是将数据源读入到程序的通信通道 3) 输出流: 程序向输出流写入数据。将程序中的数据输出到外界(显示器、打印机、文件、网络…)的通信通道。 采用数据流的目的就是使得输出输入独立于设备。 Input Stream不关心数据源来自何种设备(键盘,文件,网络) Output Stream不关心数据的目的是何种设备(键盘,文件,网络) 4 数据流分类: 流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种: 1) 字节流:数据流中最小的数据单元是字节 2) 字符流:数据流中最小的数据单元是字符, Java中的字符是Unicode编码,一个字符占用两个字节。 3. 标准I/O Java程序可通过命令行参数与外界进行简短的信息交换,同时,也规定了与标准输入、输出设备,如键盘、显示器进行信息交换的方式。而通过文件可以与外界进行任意数据形式的信息交换。 1. 命令行参数 public class TestArgs { public static void main(String[] args) { for (int i = 0; i < args.length; i++) { System.out.println("args[" + i + "] is <" + args[i] + ">"); } } } public class TestArgs { public static void main(String[] args) { for (int i = 0; i < args.length; i++) { System.out.println("args[" + i + "] is <" + args[i] + ">"); } } } 运行命令:java Java C VB 运行结果: args[0] is <Java> args[1] is <C> args[2] is <VB> 2. 标准输入,输出数据流 java系统自带的标准数据流:java.lang.System: java.lang.System public final class System extends Object{ static PrintStream err;//标准错误流(输出) static InputStream in;//标准输入(键盘输入流) static PrintStream out;//标准输出流(显示器输出流) } java.lang.System public final class System extends Object{ static PrintStream err;//标准错误流(输出) static InputStream in;//标准输入(键盘输入流) static PrintStream out;//标准输出流(显示器输出流) } 注意: (1)System类不能创建对象,只能直接使用它的三个静态成员。 (2)每当main方法被执行时,就自动生成上述三个对象。 1) 标准输出流 System.out System.out向标准输出设备输出数据,其数据类型为PrintStream。方法: Void print(参数) Void println(参数) 2)标准输入流 System.in System.in读取标准输入设备数据(从标准输入获取数据,一般是键盘),其数 据类型为InputStream。方法: int read() //返回ASCII码。若,返回值=-1,说明没有读取到任何字节读取工作结束。 int read(byte[] b)//读入多个字节到缓冲区b中返回值是读入的字节数 例如: import java.io.*; public class StandardInputOutput { public static void main(String args[]) { int b; try { System.out.println("please Input:"); while ((b = System.in.read()) != -1) { System.out.print((char) b); } } catch (IOException e) { System.out.println(e.toString()); } } } import java.io.*; public class StandardInputOutput { public static void main(String args[]) { int b; try { System.out.println("please Input:"); while ((b = System.in.read()) != -1) { System.out.print((char) b); } } catch (IOException e) { System.out.println(e.toString()); } } } 等待键盘输入,键盘输入什么,就打印出什么: 3)标准错误流 System.err输出标准错误,其数据类型为PrintStream。可查阅API获得详细说明。 标准输出通过System.out调用println方法输出参数并换行,而print方法输出参数但不换行。println或print方法都通 过重载实现了输出基本数据类型的多个方法,包括输出参数类型为boolean、char、int、long、float和double。同时,也重载实现 了输出参数类型为char[]、String和Object的方法。其中,print(Object)和println(Object)方法在运行时将调 用参数Object的toString方法。 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class StandardInputOutput { public static void main(String args[]) { String s; // 创建缓冲区阅读器从键盘逐行读入数据 InputStreamReader ir = new InputStreamReader(System.in); BufferedReader in = new BufferedReader(ir); System.out.println("Unix系统: ctrl-d 或 ctrl-c 退出" + "\nWindows系统: ctrl-z 退出"); try { // 读一行数据,并标准输出至显示器 s = in.readLine(); // readLine()方法运行时若发生I/O错误,将抛出IOException异常 while (s != null) { System.out.println("Read: " + s); s = in.readLine(); } // 关闭缓冲阅读器 in.close(); } catch (IOException e) { // Catch any IO exceptions. e.printStackTrace(); } } } import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class StandardInputOutput { public static void main(String args[]) { String s; // 创建缓冲区阅读器从键盘逐行读入数据 InputStreamReader ir = new InputStreamReader(System.in); BufferedReader in = new BufferedReader(ir); System.out.println("Unix系统: ctrl-d 或 ctrl-c 退出" + "\nWindows系统: ctrl-z 退出"); try { // 读一行数据,并标准输出至显示器 s = in.readLine(); // readLine()方法运行时若发生I/O错误,将抛出IOException异常 while (s != null) { System.out.println("Read: " + s); s = in.readLine(); } // 关闭缓冲阅读器 in.close(); } catch (IOException e) { // Catch any IO exceptions. e.printStackTrace(); } } } 4.java.IO层次体系结构 在整个Java.io包中最重要的就是5个类和一个接口。5个类指的是File、OutputStream、InputStream、Writer、Reader;一个接口指的是Serializable.掌握了这些IO的核心操作那么对于Java中的IO体系也就有了一个初步的认识了 Java I/O主要包括如下几个层次,包含三个部分: 1.流式部分――IO的主体部分; 2.非流式部分――主要包含一些辅助流式部分的类,如:File类、RandomAccessFile类和FileDescriptor等类; 3.其他类--文件读取部分的与安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。 主要的类如下: 1. File(文件特征与管理):用于文件或者目录的描述信息,例如生成新目录,修改文件名,删除文件,判断文件所在路径等。 2. InputStream(二进制格式操作):抽象类,基于字节的输入操作,是所有输入流的父类。定义了所有输入流都具有的共同特征。 3. OutputStream(二进制格式操作):抽象类。基于字节的输出操作。是所有输出流的父类。定义了所有输出流都具有的共同特征。 Java中字符是采用Unicode标准,一个字符是16位,即一个字符使用两个字节来表示。为此,JAVA中引入了处理字符的流。 4. Reader(文件格式操作):抽象类,基于字符的输入操作。 5. Writer(文件格式操作):抽象类,基于字符的输出操作。 6. RandomAccessFile(随机文件操作):它的功能丰富,可以从文件的任意位置进行存取(输入输出)操作。 Java中IO流的体系结构如图: 5. 非流式文件类--File类 在Java语言的java.io包中,由File类提供了描述文件和目录的操作与管理方法。但File类不是InputStream、OutputStream或Reader、Writer的子类,因为它不负责数据的输入输出,而专门用来管理磁盘文件与目录。 作用:File类主要用于命名文件、查询文件属性和处理文件目录。 public class File extends Object implements Serializable,Comparable {} public class File extends Object implements Serializable,Comparable {} File类共提供了三个不同的构造函数,以不同的参数形式灵活地接收文件和目录名信息。构造函数: 1)File (String pathname) 例:File f1=new File("FileTest1.txt"); //创建文件对象f1,f1所指的文件是在当前目录下创建的FileTest1.txt 2)File (String parent , String child) 例:File f2=new File(“D:\\dir1","FileTest2.txt") ;// 注意:D:\\dir1目录事先必须存在,否则异常 3)File (File parent , String child) 例:File f4=new File("\\dir3"); File f5=new File(f4,"FileTest5.txt"); //在如果 \\dir3目录不存在使用f4.mkdir()先创建 一个对应于某磁盘文件或目录的File对象一经创建, 就可以通过调用它的方法来获得文件或目录的属性。 1)public boolean exists( )判断文件或目录是否存在 2)public boolean isFile( ) 判断是文件还是目录 3)public boolean isDirectory( ) 判断是文件还是目录 4)public String getName( ) 返回文件名或目录名 5)public String getPath( ) 返回文件或目录的路径。 6)public long length( ) 获取文件的长度 7)public String[ ] list ( ) 将目录中所有文件名保存在字符串数组中返回。 File类中还定义了一些对文件或目录进行管理、操作的方法,常用的方法有: 1) public boolean renameTo( File newFile ); 重命名文件 2) public void delete( ); 删除文件 3) public boolean mkdir( ); 创建目录 例子: import java.io.File; import java.io.IOException; public class TestFile { public static void main(String args[]) throws IOException { File dir = new File("\\root"); File f1 = new File(dir, "fileOne.txt"); File f2 = new File(dir, "fileTwo.java"); // 文件对象创建后,指定的文件或目录不一定物理上存在 if (!dir.exists()) dir.mkdir(); if (!f1.exists()) f1.createNewFile(); if (!f2.exists()) f2.createNewFile(); System.out.println("f1's AbsolutePath= " + f1.getAbsolutePath()); System.out.println("f1 Canread=" + f1.canRead()); System.out.println("f1's len= " + f1.length()); String[] FL; int count = 0; FL = dir.list(); for (int i = 0; i < FL.length; i++) { count++; System.out.println(FL[i] + "is in \\root"); } System.out.println("there are" + count + "file in //root"); } } import java.io.File; import java.io.IOException; public class TestFile { public static void main(String args[]) throws IOException { File dir = new File("\\root"); File f1 = new File(dir, "fileOne.txt"); File f2 = new File(dir, "fileTwo.java"); // 文件对象创建后,指定的文件或目录不一定物理上存在 if (!dir.exists()) dir.mkdir(); if (!f1.exists()) f1.createNewFile(); if (!f2.exists()) f2.createNewFile(); System.out.println("f1's AbsolutePath= " + f1.getAbsolutePath()); System.out.println("f1 Canread=" + f1.canRead()); System.out.println("f1's len= " + f1.length()); String[] FL; int count = 0; FL = dir.list(); for (int i = 0; i < FL.length; i++) { count++; System.out.println(FL[i] + "is in \\root"); } System.out.println("there are" + count + "file in //root"); } } 说明:File类的方法: (1) exists()测试磁盘中指定的文件或目录是否存在 (2) mkdir()创建文件对象指定的目录(单层目录) (3) createNewFile()创建文件对象指定的文件 (4) list()返回目录中所有文件名字符串 6. Java.IO流类库 1. io流的四个基本类 java.io包中包含了流式I/O所需要的所有类。在java.io包中有四个基本类:InputStream、OutputStream及Reader、Writer类,它们分别处理字节流和字符流: 基本数据流的I/O 输入/输出 字节流 字符流 输入流 Inputstream Reader 输出流 OutputStream Writer Java中其他多种多样变化的流均是由它们派生出来的: JDK1.4版本开始引入了新I/O类库,它位于java.nio包中,新I/O类库利用通道和缓冲区等来提高I/O操作的效率。 在java.io包中, java.io.InputStream 表示字节输入流, java.io.OutputStream表示字节输出流,处于java.io包最顶层。这两个类均为抽象类,也就是说它们不能被实例化,必须生成子类之后才能实现一定的功能。 1. io流的具体分类 一、按I/O类型来总体分类: 1. Memory 1)从/向内存数组读写数据: CharArrayReader、 CharArrayWriter、ByteArrayInputStream、ByteArrayOutputStream 2)从/向内存字符串读写数据 StringReader、StringWriter、StringBufferInputStream 2.Pipe管道 实现管道的输入和输出(进程间通信): PipedReader、PipedWriter、PipedInputStream、PipedOutputStream 3.File 文件流。对文件进行读、写操作 :FileReader、FileWriter、FileInputStream、FileOutputStream 4. ObjectSerialization 对象输入、输出 :ObjectInputStream、ObjectOutputStream 5.DataConversion数据流 按基本数据类型读、写(处理的数据是Java的基本类型(如布尔型,字节,整数和浮点数)):DataInputStream、DataOutputStream 6.Printing 包含方便的打印方法 :PrintWriter、PrintStream 7.Buffering缓冲 在读入或写出时,对数据进行缓存,以减少I/O的次数:BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream 8.Filtering 滤流,在数据进行读或写时进行过滤:FilterReader、FilterWriter、FilterInputStream、FilterOutputStream过 9.Concatenation合并输入 把多个输入流连接成一个输入流 :SequenceInputStream 10.Counting计数 在读入数据时对行记数 :LineNumberReader、LineNumberInputStream 11.Peeking Ahead 通过缓存机制,进行预读 :PushbackReader、PushbackInputStream 12.Converting between Bytes and Characters 按照一定的编码/解码标准将字节流转换为字符流,或进行反向转换(Stream到Reader,Writer的转换类):InputStreamReader、OutputStreamWriter 二、按数据来源(去向)分类: 1、File(文件): FileInputStream, FileOutputStream, FileReader, FileWriter 2、byte[]:ByteArrayInputStream, ByteArrayOutputStream 3、Char[]: CharArrayReader, CharArrayWriter 4、String: StringBufferInputStream, StringReader, StringWriter 5、网络数据流:InputStream, OutputStream, Reader, Writer 7. 字节流InputStream/OutputStream 1. InputStream抽象类 InputStream 为字节输入流,它本身为一个抽象类,必须依靠其子类实现各种功能,此抽象类是表示字节输入流的所有类的超类。 继承自InputStream 的流都是向程序中输入数据的,且数据单位为字节(8bit); InputStream是输入字节数据用的类,所以InputStream类提供了3种重载的read方法.Inputstream类中的常用方法: (1) public abstract int read( ):读取一个byte的数据,返回值是高位补0的int类型值。若返回值=-1说明没有读取到任何字节读取工作结束。 (2) public int read(byte b[ ]):读取b.length个字节的数据放到b数组中。返回值是读取的字节数。该方法实际上是调用下一个方法实现的 (3) public int read(byte b[ ], int off, int len):从输入流中最多读取len个字节的数据,存放到偏移量为off的b数组中。 (4) public int available( ):返回输入流中可以读取的字节数。注意:若输入阻塞,当前线程将被挂起,如果InputStream对象调用这个方法的话,它只会返回0,这个方法必须由继承InputStream类的子类对象调用才有用, (5) public long skip(long n):忽略输入流中的n个字节,返回值是实际忽略的字节数, 跳过一些字节来读取 (6) public int close( ) :我们在使用完后,必须对我们打开的流进行关闭. 主要的子类: 1) FileInputStream把一个文件作为InputStream,实现对文件的读取操作 2) ByteArrayInputStream:把内存中的一个缓冲区作为InputStream使用 3) StringBufferInputStream:把一个String对象作为InputStream 4) PipedInputStream:实现了pipe的概念,主要在线程中使用 5) SequenceInputStream:把多个InputStream合并为一个InputStream 2.OutputStream抽象类 OutputStream提供了3个write方法来做数据的输出,这个是和InputStream是相对应的。 1. public void write(byte b[ ]):将参数b中的字节写到输出流。 2. public void write(byte b[ ], int off, int len) :将参数b的从偏移量off开始的len个字节写到输出流。 3. public abstract void write(int b) :先将int转换为byte类型,把低字节写入到输出流中。 4. public void flush( ) : 将数据缓冲区中数据全部输出,并清空缓冲区。 5. public void close( ) : 关闭输出流并释放与流相关的系统资源。 主要的子类: 1) ByteArrayOutputStream:把信息存入内存中的一个缓冲区中 2) FileOutputStream:把信息存入文件中 3) PipedOutputStream:实现了pipe的概念,主要在线程中使用 4) SequenceOutputStream:把多个OutStream合并为一个OutStream 流结束的判断:方法read()的返回值为-1时;readLine()的返回值为null时。 3. 文件输入流: FileInputStream类 FileInputStream可以使用read()方法一次读入一个字节,并以int类型返回,或者是使用read()方法时读入至一个byte数组,byte数组的元素有多少个,就读入多少个字节。在将整个文件读取完成或写入完毕的过程中,这么一个byte数组通常被当作缓冲区,因为这么一个byte数组通常扮演承接数据的中间角色。 作用:以文件作为数据输入源的数据流。或者说是打开文件,从文件读数据到内存的类。 使用方法(1) File fin=new File("d:/abc.txt"); FileInputStream in=new FileInputStream( fin); 使用方法(2) FileInputStream in=new FileInputStream(“d: /abc.txt”); 程序举例: 将InputFromFile.java的程序的内容显示在显示器上 import java.io.IOException; import java.io.FileInputStream; ; public class TestFile { public static void main(String args[]) throws IOException { try{ FileInputStream rf=new FileInputStream("InputFromFile.java"); int n=512; byte buffer[]=new byte[n]; while((rf.read(buffer,0,n)!=-1)&&(n>0)){ System.out.println(new String(buffer) ); } System.out.println(); rf.close(); } catch(IOException IOe){ System.out.println(IOe.toString()); } } } import java.io.IOException; import java.io.FileInputStream; ; public class TestFile { public static void main(String args[]) throws IOException { try{ FileInputStream rf=new FileInputStream("InputFromFile.java"); int n=512; byte buffer[]=new byte[n]; while((rf.read(buffer,0,n)!=-1)&&(n>0)){ System.out.println(new String(buffer) ); } System.out.println(); rf.close(); } catch(IOException IOe){ System.out.println(IOe.toString()); } } } 4.文件输出流:FileOutputStream类 作用:用来处理以文件作为数据输出目的数据流;或者说是从内存区读数据入文件 FileOutputStream类用来处理以文件作为数据输出目的数据流;一个表示文件名的字符串,也可以是File或FileDescriptor对象。 创建一个文件流对象有两种方法: 方式1: File f=new File (“d:/myjava/write.txt "); FileOutputStream out= new FileOutputStream (f); 方式2: FileOutputStream out=new FileOutputStream(“d:/myjava/write.txt "); 方式3:构造函数将 FileDescriptor()对象作为其参数。 FileDescriptor() fd=new FileDescriptor(); FileOutputStream f2=new FileOutputStream(fd); 方式4:构造函数将文件名作为其第一参数,将布尔值作为第二参数。 FileOutputStream f=new FileOutputStream("d:/abc.txt",true); 注意: (1)文件中写数据时,若文件已经存在,则覆盖存在的文件;(2)的读/写操作结束时,应调用close方法关闭流。 程序举例:使用键盘输入一段文章,将文章保存在文件write.txt中 import java.io.IOException; import java.io.FileOutputStream; public class TestFile { public static void main(String args[]) throws IOException { try { System.out.println("please Input from Keyboard"); int count, n = 512; byte buffer[] = new byte[n]; count = System.in.read(buffer); FileOutputStream wf = new FileOutputStream("d:/myjava/write.txt"); wf.write(buffer, 0, count); wf.close(); // 当流写操作结束时,调用close方法关闭流。 System.out.println("Save to the write.txt"); } catch (IOException IOe) { System.out.println("File Write Error!"); } } } import java.io.IOException; import java.io.FileOutputStream; public class TestFile { public static void main(String args[]) throws IOException { try { System.out.println("please Input from Keyboard"); int count, n = 512; byte buffer[] = new byte[n]; count = System.in.read(buffer); FileOutputStream wf = new FileOutputStream("d:/myjava/write.txt"); wf.write(buffer, 0, count); wf.close(); // 当流写操作结束时,调用close方法关闭流。 System.out.println("Save to the write.txt"); } catch (IOException IOe) { System.out.println("File Write Error!"); } } } 5. FileInputStream流和FileOutputStream的应用 利用程序将文件file1.txt 拷贝到file2.txt中。 import java.io.File; import java.io.IOException; import java.io.FileOutputStream; import java.io.FileInputStream; public class TestFile { public static void main(String args[]) throws IOException { try { File inFile = new File("copy.java"); File outFile = new File("copy2.java"); FileInputStream finS = new FileInputStream(inFile); FileOutputStream foutS = new FileOutputStream(outFile); int c; while ((c = finS.read()) != -1) { foutS.write(c); } finS.close(); foutS.close(); } catch (IOException e) { System.err.println("FileStreamsTest: " + e); } } } import java.io.File; import java.io.IOException; import java.io.FileOutputStream; import java.io.FileInputStream; public class TestFile { public static void main(String args[]) throws IOException { try { File inFile = new File("copy.java"); File outFile = new File("copy2.java"); FileInputStream finS = new FileInputStream(inFile); FileOutputStream foutS = new FileOutputStream(outFile); int c; while ((c = finS.read()) != -1) { foutS.write(c); } finS.close(); foutS.close(); } catch (IOException e) { System.err.println("FileStreamsTest: " + e); } } } 6. 缓冲输入输出流 BufferedInputStream/ BufferedOutputStream 计算机访问外部设备非常耗时。访问外存的频率越高,造成CPU闲置的概率就越大。为了减少访问外存的次数,应该在一次对外设的访问中,读写更多的数据。为此,除了程序和流节点间交换数据必需的读写机制外,还应该增加缓冲机制。缓冲流就是每一个数据流分配一个缓冲区,一个缓冲区就是一个临时存储数据的内存。这样可以减少访问硬盘的次数,提高传输效率。 BufferedInputStream:当向缓冲流写入数据时候,数据先写到缓冲区,待缓冲区写满后,系统一次性将数据发送给输出设备。 BufferedOutputStream :当从向缓冲流读取数据时候,系统先从缓冲区读出数据,待缓冲区为空时,系统再从输入设备读取数据到缓冲区。 1)将文件读入内存: 将BufferedInputStream与FileInputStream相接 FileInputStream in=new FileInputStream( “file1.txt ” ); BufferedInputStream bin=new BufferedInputStream( in); 2)将内存写入文件: 将BufferedOutputStream与 FileOutputStream相接 FileOutputStreamout=new FileOutputStream(“file1.txt”); BufferedOutputStream bin=new BufferedInputStream(out); 3)键盘输入流读到内存 将BufferedReader与标准的数据流相接 InputStreamReader sin=new InputStreamReader (System.in) ; BufferedReader bin=new BufferedReader(sin); import java.io.*; public class ReadWriteToFile { public static void main(String args[]) throws IOException { InputStreamReader sin = new InputStreamReader(System.in); BufferedReader bin = new BufferedReader(sin); FileWriter out = new FileWriter("myfile.txt"); BufferedWriter bout = new BufferedWriter(out); String s; while ((s = bin.readLine()).length() > 0) { bout.write(s, 0, s.length()); } } } import java.io.*; public class ReadWriteToFile { public static void main(String args[]) throws IOException { InputStreamReader sin = new InputStreamReader(System.in); BufferedReader bin = new BufferedReader(sin); FileWriter out = new FileWriter("myfile.txt"); BufferedWriter bout = new BufferedWriter(out); String s; while ((s = bin.readLine()).length() > 0) { bout.write(s, 0, s.length()); } } } 程序说明: 从键盘读入字符,并写入到文件中BufferedReader类的方法:String readLine() 作用:读一行字符串,以回车符为结束。 BufferedWriter类的方法:bout.write(String s,offset,len) 作用:从缓冲区将字符串s从offset开始,len长度的字符串写到某处。 8. 字符流Writer/Reader Java中字符是采用Unicode标准,一个字符是16位,即一个字符使用两个字节来表示。为此,JAVA中引入了处理字符的流。 1. Reader抽象类 用于读取字符流的抽象类。子类必须实现的方法只有 read(char[], int, int) 和 close()。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。 1) FileReader :与FileInputStream对应 主要用来读取字符文件,使用缺省的字符编码,有三种构造函数: (1)将文件名作为字符串 :FileReader f=new FileReader(“c:/temp.txt”); (2)构造函数将File对象作为其参数。 File f=new file(“c:/temp.txt”); FileReader f1=new FileReader(f); (3) 构造函数将FileDescriptor对象作为参数 FileDescriptor() fd=new FileDescriptor() FileReader f2=new FileReader(fd); (1) 用指定字符数组作为参数:CharArrayReader(char[]) (2) 将字符数组作为输入流:CharArrayReader(char[], int, int) 读取字符串,构造函数如下: public StringReader(String s); 2) CharArrayReader:与ByteArrayInputStream对应 3) StringReader : 与StringBufferInputStream对应 4) InputStreamReader 从输入流读取字节,在将它们转换成字符:Public inputstreamReader(inputstream is); 5) FilterReader: 允许过滤字符流 protected filterReader(Reader r); 6) BufferReader :接受Reader对象作为参数,并对其添加字符缓冲器,使用readline()方法可以读取一行。 Public BufferReader(Reader r); 主要方法: (1) public int read() throws IOException; //读取一个字符,返回值为读取的字符 (2) public int read(char cbuf[]) throws IOException; /*读取一系列字符到数组cbuf[]中,返回值为实际读取的字符的数量*/ (3) public abstract int read(char cbuf[],int off,int len) throws IOException; /*读取len个字符,从数组cbuf[]的下标off处开始存放,返回值为实际读取的字符数量,该方法必须由子类实现*/ 2. Writer抽象类 写入字符流的抽象类。子类必须实现的方法仅有 write(char[], int, int)、flush() 和 close()。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。 其子类如下: 1) FileWrite: 与FileOutputStream对应 将字符类型数据写入文件,使用缺省字符编码和缓冲器大小。 Public FileWrite(file f); 2) chararrayWrite:与ByteArrayOutputStream对应 ,将字符缓冲器用作输出。 Public CharArrayWrite(); 3) PrintWrite:生成格式化输出 public PrintWriter(outputstream os); 4) filterWriter:用于写入过滤字符流 protected FilterWriter(Writer w); 5) PipedWriter:与PipedOutputStream对应 6) StringWriter:无与之对应的以字节为导向的stream 主要方法: (1) public void write(int c) throws IOException; //将整型值c的低16位写入输出流 (2) public void write(char cbuf[]) throws IOException; //将字符数组cbuf[]写入输出流 (3) public abstract void write(char cbuf[],int off,int len) throws IOException; //将字符数组cbuf[]中的从索引为off的位置处开始的len个字符写入输出流 (4) public void write(String str) throws IOException; //将字符串str中的字符写入输出流 (5) public void write(String str,int off,int len) throws IOException; //将字符串str 中从索引off开始处的len个字符写入输出流 (6) flush( ) //刷空输出流,并输出所有被缓存的字节。 (7)close() 关闭流 public abstract void close() throws IOException 3 .InputStream与Reader差别 OutputStream与Writer差别 InputStream和OutputStream类处理的是字节流,数据流中的最小单位是字节(8个bit) Reader与Writer处理的是字符流,在处理字符流时涉及了字符编码的转换问题 import java.io.*; public class EncodeTest { private static void readBuff(byte [] buff) throws IOException { ByteArrayInputStream in =new ByteArrayInputStream(buff); int data; while((data=in.read())!=-1) System.out.print(data+" "); System.out.println(); in.close(); } public static void main(String args[]) throws IOException { System.out.println("内存中采用unicode字符编码:" ); char c='好'; int lowBit=c&0xFF; int highBit=(c&0xFF00)>>8; System.out.println(""+lowBit+" "+highBit); String s="好"; System.out.println("本地操作系统默认字符编码:"); readBuff(s.getBytes()); System.out.println("采用GBK字符编码:"); readBuff(s.getBytes("GBK")); System.out.println("采用UTF-8字符编码:"); readBuff(s.getBytes("UTF-8")); } } import java.io.*; public class EncodeTest { private static void readBuff(byte [] buff) throws IOException { ByteArrayInputStream in =new ByteArrayInputStream(buff); int data; while((data=in.read())!=-1) System.out.print(data+" "); System.out.println(); in.close(); } public static void main(String args[]) throws IOException { System.out.println("内存中采用unicode字符编码:" ); char c='好'; int lowBit=c&0xFF; int highBit=(c&0xFF00)>>8; System.out.println(""+lowBit+" "+highBit); String s="好"; System.out.println("本地操作系统默认字符编码:"); readBuff(s.getBytes()); System.out.println("采用GBK字符编码:"); readBuff(s.getBytes("GBK")); System.out.println("采用UTF-8字符编码:"); readBuff(s.getBytes("UTF-8")); } } Reader类能够将输入流中采用其他编码类型的字符转换为Unicode字符,然后在内存中为其分配内存 Writer类能够将内存中的Unicode字符转换为其他编码类型的字符,再写到输出流中。 9. IOException异常类的子类 1.public class EOFException : 非正常到达文件尾或输入流尾时,抛出这种类型的异常。 2.public class FileNotFoundException: 当文件找不到时,抛出的异常。 3.public class InterruptedIOException: 当I/O操作被中断时,抛出这种类型的异常。
学过C语言的朋友都知道C编译器在划分内存区域的时候经常将管理的区域划分为数据段和代码段,数据段包括堆、栈以及静态数据区。那么在Java语言当中,内存又是如何划分的呢? 由于Java程序是交由JVM执行的,所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分。在讨论JVM内存区域划分之前,先来看一下Java程序具体执行的过程: 如上图所示,首先Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。 在知道了JVM内存是什么东西之后,下面我们就来讨论一下这段空间具体是如何划分区域的,是不是也像C语言中一样也存在栈和堆呢? 一.运行时数据区包括哪几部分? 根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。 如上图所示,JVM中的运行时数据区应该包括这些部分。在JVM规范中虽然规定了程序在执行期间运行时数据区应该包括这几部分,但是至于具体如何实现并没有做出规定,不同的虚拟机厂商可以有不同的实现方式。 二.运行时数据区的每部分到底存储了哪些数据? 下面我们来了解一下运行时数据区的每部分具体用来存储程序执行过程中的哪些数据。 1.程序计数器 程序计数器(Program Counter Register),也有称作为PC寄存器。想必学过汇编语言的朋友对程序计数器这个概念并不陌生,在汇编语言中,程序计数器是指CPU中的寄存器,它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。 虽然JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是JVM中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示 执行哪条指令的。 由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。 在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。 由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。 2.Java栈 Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈,跟C语言的数据段中的栈类似。事实上,Java栈是Java方法执行的内存模型。为什么这么说呢?下面就来解释一下其中的原因。 Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了以及为什么栈区的空间不用程序员去管理了(当然在Java中,程序员基本不用关系到内存分配和释放的事情,因为Java有自己的垃圾回收机制),这部分空间的分配和释放都是由系统自动实施的。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。下图表示了一个Java栈的模型: 局部变量表,顾名思义,想必不用解释大家应该明白它的作用了吧。就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。 操作数栈,想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。 指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。 方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。 由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈,互不干扰。 3.本地方法栈 本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。 4.堆 在C语言中,堆这部分空间是唯一一个程序员可以管理的内存区域。程序员可以通过malloc函数和free函数在堆上申请和释放空间。那么在Java中是怎么样的呢? Java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。只不过和C语言中的不同,在Java中,程序员基本不用去关心空间释放的问题,Java的垃圾回收机制会自动进行处理。因此这部分空间也是Java垃圾收集器管理的主要区域。另外,堆是被所有线程共享的,在JVM中只有一个堆。 5.方法区 方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。 在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。 在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。 在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。 以上为个人看法和观点,如有不正之处希望谅解并欢迎指正。 参考资料: http://blog.csdn.net/ns_code/article/details/17565503 http://www.cnblogs.com/sunada2005/p/3577799.html 《深入理解Java虚拟机》 《Java虚拟机规范 SE7》 转载请标明地址:http://www.cnblogs.com/dolphin0520/p/3613043.html
原文链接:http://www.cnblogs.com/coshaho/p/5689738.html wsdl解析 首先必然是理解第三方webservice的接口描述,也就是解析wsdl文件。wsdl文件是webservice服务接口描述文档,一个wsdl文件可以包含多个接口,一个接口可以包含多个方法。 public class WsdlInfo { private String wsdlName; private List<InterfaceInfo> interfaces; /** * coshaho * @param path wsdl地址 * @throws Exception */ public WsdlInfo(String path) throws Exception { WProject project = new WProject(); WsdlInterface[] wsdlInterfaces = WsdlImporter.importWsdl( project, path ); this.wsdlName = path; if(null != wsdlInterfaces) { List<InterfaceInfo> interfaces = new ArrayList<InterfaceInfo>(); for(WsdlInterface wsdlInterface : wsdlInterfaces) { InterfaceInfo interfaceInfo = new InterfaceInfo(wsdlInterface); interfaces.add(interfaceInfo); } this.interfaces = interfaces; } } public String getWsdlName() { return wsdlName; } public void setWsdlName(String wsdlName) { this.wsdlName = wsdlName; } public List<InterfaceInfo> getInterfaces() { return interfaces; } public void setInterfaces(List<InterfaceInfo> interfaces) { this.interfaces = interfaces; } } public class InterfaceInfo { private String interfaceName; private List<OperationInfo> operations; private String[] adrress; public InterfaceInfo(WsdlInterface wsdlInterface) { this.interfaceName = wsdlInterface.getName(); this.adrress = wsdlInterface.getEndpoints(); int operationNum = wsdlInterface.getOperationCount(); List<OperationInfo> operations = new ArrayList<OperationInfo>(); for(int i = 0; i < operationNum; i++) { WsdlOperation operation = ( WsdlOperation )wsdlInterface.getOperationAt( i ); OperationInfo operationInfo = new OperationInfo(operation); operations.add(operationInfo); } this.operations = operations; } public String getInterfaceName() { return interfaceName; } public void setInterfaceName(String interfaceName) { this.interfaceName = interfaceName; } public List<OperationInfo> getOperations() { return operations; } public void setOperations(List<OperationInfo> operations) { this.operations = operations; } public String[] getAdrress() { return adrress; } public void setAdrress(String[] adrress) { this.adrress = adrress; } } public class OperationInfo { private String operationName; private String requestXml; private String responseXml; public OperationInfo(WsdlOperation operation) { operationName = operation.getName(); requestXml = operation.createRequest( true ); responseXml = operation.createResponse(true); } public String getOperationName() { return operationName; } public void setOperationName(String operationName) { this.operationName = operationName; } public String getRequestXml() { return requestXml; } public void setRequestXml(String requestXml) { this.requestXml = requestXml; } public String getResponseXml() { return responseXml; } public void setResponseXml(String responseXml) { this.responseXml = responseXml; } } public class WSDLParseTest { public static void main(String[] args) throws Exception { String url = "http://webservice.webxml.com.cn/WebServices/ChinaOpenFundWS.asmx?wsdl"; WsdlInfo wsdlInfo = new WsdlInfo(url); System.out.println("WSDL URL is " + wsdlInfo.getWsdlName()); for(InterfaceInfo interfaceInfo : wsdlInfo.getInterfaces()) { System.out.println("Interface name is " + interfaceInfo.getInterfaceName()); for(String ads : interfaceInfo.getAdrress()) { System.out.println("Interface address is " + ads); } for(OperationInfo operation : interfaceInfo.getOperations()) { System.out.println("operation name is " + operation.getOperationName()); System.out.println("operation request is "); System.out.println("operation request is " + operation.getRequestXml()); System.out.println("operation response is "); System.out.println(operation.getResponseXml()); } } } } WSDL URL is http://webservice.webxml.com.cn/WebServices/ChinaOpenFundWS.asmx?wsdl Interface name is ChinaOpenFundWSSoap12 Interface address is http://webservice.webxml.com.cn/WebServices/ChinaOpenFundWS.asmx operation name is getFundCodeNameDataSet operation request is operation request is <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:web="http://WebXml.com.cn/"> <soap:Header/> <soap:Body> <web:getFundCodeNameDataSet/> </soap:Body> </soap:Envelope> operation response is <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:web="http://WebXml.com.cn/" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <soap:Header/> <soap:Body> <web:getFundCodeNameDataSetResponse> <!--Optional:--> <web:getFundCodeNameDataSetResult> <xs:schema> <!--Ignoring type [{http://www.w3.org/2001/XMLSchema}schema]--> </xs:schema> <!--You may enter ANY elements at this point--> </web:getFundCodeNameDataSetResult> </web:getFundCodeNameDataSetResponse> </soap:Body> </soap:Envelope> operation name is getFundCodeNameString operation request is operation request is <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:web="http://WebXml.com.cn/"> <soap:Header/> <soap:Body> <web:getFundCodeNameString/> </soap:Body> </soap:Envelope> operation response is <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:web="http://WebXml.com.cn/"> <soap:Header/> <soap:Body> <web:getFundCodeNameStringResponse> <!--Optional:--> <web:getFundCodeNameStringResult> <!--Zero or more repetitions:--> <web:string>?</web:string> </web:getFundCodeNameStringResult> </web:getFundCodeNameStringResponse> </soap:Body> </soap:Envelope> operation name is getOpenFundDataSet operation request is operation request is <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:web="http://WebXml.com.cn/"> <soap:Header/> <soap:Body> <web:getOpenFundDataSet> <!--Optional:--> <web:userID>?</web:userID> </web:getOpenFundDataSet> </soap:Body> </soap:Envelope> operation response is <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:web="http://WebXml.com.cn/" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <soap:Header/> <soap:Body> <web:getOpenFundDataSetResponse> <!--Optional:--> <web:getOpenFundDataSetResult> <xs:schema> <!--Ignoring type [{http://www.w3.org/2001/XMLSchema}schema]--> </xs:schema> <!--You may enter ANY elements at this point--> </web:getOpenFundDataSetResult> </web:getOpenFundDataSetResponse> </soap:Body> </soap:Envelope>
HashMap通过hashcode对其内容进行快速查找,而 TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。 HashMap 非线程安全 TreeMap 非线程安全 线程安全 在Java里,线程安全一般体现在两个方面: 1、多个thread对同一个java实例的访问(read和modify)不会相互干扰,它主要体现在关键字synchronized。如ArrayList和Vector,HashMap和Hashtable (后者每个方法前都有synchronized关键字)。如果你在interator一个List对象时,其它线程remove一个element,问题就出现了。 2、每个线程都有自己的字段,而不会在多个线程之间共享。它主要体现在java.lang.ThreadLocal类,而没有Java关键字支持,如像static、transient那样。 1.AbstractMap抽象类和SortedMap接口 AbstractMap抽象类:(HashMap继承AbstractMap)覆盖了equals()和hashCode()方法以确保两个相等映射返回相同的哈希码。如果两个映射大小相等、包含同样的键且每个键在这两个映射中对应的值都相同,则这两个映射相等。映射的哈希码是映射元素哈希码的总和,其中每个元素是Map.Entry接口的一个实现。因此,不论映射内部顺序如何,两个相等映射会报告相同的哈希码。 SortedMap接口:(TreeMap继承自SortedMap)它用来保持键的有序顺序。SortedMap接口为映像的视图(子集),包括两个端点提供了访问方法。除了排序是作用于映射的键以外,处理SortedMap和处理SortedSet一样。添加到SortedMap实现类的元素必须实现Comparable接口,否则您必须给它的构造函数提供一个Comparator接口的实现。TreeMap类是它的唯一一份实现。 HashMap:基于哈希表实现。使用HashMap要求添加的键类明确定义了hashCode()和equals()[可以重写hashCode()和equals()],为了优化HashMap空间的使用,您可以调优初始容量和负载因子。 (1)HashMap(): 构建一个空的哈希映像 (2)HashMap(Map m): 构建一个哈希映像,并且添加映像m的所有映射 (3)HashMap(int initialCapacity): 构建一个拥有特定容量的空的哈希映像 (4)HashMap(int initialCapacity, float loadFactor): 构建一个拥有特定容量和加载因子的空的哈希映像 TreeMap:基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。 (1)TreeMap():构建一个空的映像树 (2)TreeMap(Map m): 构建一个映像树,并且添加映像m中所有元素 (3)TreeMap(Comparator c): 构建一个映像树,并且使用特定的比较器对关键字进行排序 (4)TreeMap(SortedMap s): 构建一个映像树,添加映像树s中所有映射,并且使用与有序映像s相同的比较器排序 HashMap:适用于在Map中插入、删除和定位元素。 Treemap:适用于按自然顺序或自定义顺序遍历键(key)。 import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; public class HashMaps { public static void main(String[] args) { Map<String, String> map = new HashMap<String, String>(); map.put("a", "aaa"); map.put("b", "bbb"); map.put("c", "ccc"); map.put("d", "ddd"); Iterator<String> iterator = map.keySet().iterator(); while (iterator.hasNext()) { Object key = iterator.next(); System.out.println("map.get(key) is :" + map.get(key)); } // 定义HashTable,用来测试 Hashtable<String, String> tab = new Hashtable<String, String>(); tab.put("a", "aaa"); tab.put("b", "bbb"); tab.put("c", "ccc"); tab.put("d", "ddd"); Iterator<String> iterator_1 = tab.keySet().iterator(); while (iterator_1.hasNext()) { Object key = iterator_1.next(); System.out.println("tab.get(key) is :" + tab.get(key)); } TreeMap<String, String> tmp = new TreeMap<String, String>(); tmp.put("a", "aaa"); tmp.put("b", "bbb"); tmp.put("c", "ccc"); tmp.put("d", "cdc"); Iterator<String> iterator_2 = tmp.keySet().iterator(); while (iterator_2.hasNext()) { Object key = iterator_2.next(); System.out.println("tmp.get(key) is :" + tmp.get(key)); } } } HashMap通常比TreeMap快一点(树和哈希表的数据结构使然),建议多使用HashMap,在需要排序的Map时候才用TreeMap。 import java.util.*; public class Exp1 { public static void main(String[] args){ HashMap h1=new HashMap(); Random r1=new Random(); for (int i=0;i<1000;i++){ Integer t=new Integer(r1.nextInt(20)); if (h1.containsKey(t)) ((Ctime)h1.get(t)).count++; else h1.put(t, new Ctime()); } System.out.println(h1); } } class Ctime{ int count=1; public String toString(){ return Integer.toString(count); } } 在HashMap中通过get()来获取value,通过put()来插入value,ContainsKey()则用来检验对象是否已经存在。可以看出,和ArrayList的操作相比,HashMap除了通过key索引其内容之外,别的方面差异并不大。 前面介绍了,HashMap是基于HashCode的,在所有对象的超类Object中有一个HashCode()方法,但是它和equals方法一样,并不能适用于所有的情况,这样我们就需要重写自己的HashCode()方法。 import java.util.*; public class Exp2 { public static void main(String[] args){ HashMap h2=new HashMap(); for (int i=0;i<10;i++) h2.put(new Element(i), new Figureout()); System.out.println("h2:"); System.out.println("Get the result for Element:"); Element test=new Element(5); if (h2.containsKey(test)) System.out.println((Figureout)h2.get(test)); else System.out.println("Not found"); } } class Element{ int number; public Element(int n){ number=n; } } class Figureout{ Random r=new Random(); boolean possible=r.nextDouble()>0.5; public String toString(){ if (possible) return "OK!"; else return "Impossible!"; } } 在这个例子中,Element用来索引对象Figureout,也即Element为key,Figureout为value。在Figureout中随机生成一个浮点数,如果它比0.5大,打印”OK!”,否则打印”Impossible!”。之后查看Element(3)对应的Figureout结果如何。 结果却发现,无论你运行多少次,得到的结果都是”Not found”。也就是说索引Element(3)并不在HashMap中。这怎么可能呢? 原因得慢慢来说:Element的HashCode方法继承自Object,而Object中的HashCode方法返回的HashCode对应于当前的地址,也就是说对于不同的对象,即使它们的内容完全相同,用HashCode()返回的值也会不同。这样实际上违背了我们的意图。因为我们在使用 HashMap时,希望利用相同内容的对象索引得到相同的目标对象,这就需要HashCode()在此时能够返回相同的值。在上面的例子中,我们期望 new Element(i) (i=5)与 Elementtest=newElement(5)是相同的,而实际上这是两个不同的对象,尽管它们的内容相同,但它们在内存中的地址不同。因此很自然的,上面的程序得不到我们设想的结果。 面对Element类更改如下: class Element{ int number; public Element(int n){ number=n; } public int hashCode(){ return number; } public boolean equals(Object o){ return (o instanceof Element) && (number==((Element)o).number); } } 在这里Element覆盖了Object中的hashCode()和equals()方法。覆盖hashCode()使其以number的值作为 hashcode返回,这样对于相同内容的对象来说它们的hashcode也就相同了。而覆盖equals()是为了在HashMap判断两个key是否相等时使结果有意义(有关重写equals()的内容可以参考我的另一篇文章《重新编写Object类中的方法》)。修改后的程序运行结果如下: h2: Get the result for Element: Impossible! 请记住:如果你想有效的使用HashMap,你就必须重写在其的HashCode()。 还有两条重写HashCode()的原则: [list=1] 不必对每个不同的对象都产生一个唯一的hashcode,只要你的HashCode方法使get()能够得到put()放进去的内容就可以了。即”不为一原则”。 生成hashcode的算法尽量使hashcode的值分散一些,不要很多hashcode都集中在一个范围内,这样有利于提高HashMap的性能。即”分散原则”。至于第二条原则的具体原因,有兴趣者可以参考Bruce Eckel的《Thinking in Java》,在那里有对HashMap内部实现原理的介绍,这里就不赘述了。 掌握了这两条原则,你就能够用好HashMap编写自己的程序了。不知道大家注意没有,java.lang.Object中提供的三个方法:clone(),equals()和hashCode()虽然很典型,但在很多情况下都不能够适用,它们只是简单的由对象的地址得出结果。这就需要我们在自己的程序中重写它们,其实java类库中也重写了千千万万个这样的方法。利用面向对象的多态性——覆盖,Java的设计者很优雅的构建了Java的结构,也更加体现了Java是一门纯OOP语言的特性。
Arrays.sort()数组排序 Java Arrays中提供了对所有类型的排序。其中主要分为Primitive(8种基本类型)和Object两大类。 基本类型:采用调优的快速排序; 对象类型:采用改进的归并排序。 也就是说,优化的归并排序既快速(nlog(n))又稳定。 对于对象的排序,稳定性很重要。比如成绩单,一开始可能是按人员的学号顺序排好了的,现在让我们用成绩排,那么你应该保证,本来张三在李四前面,即使他们成绩相同,张三不能跑到李四的后面去。 而快速排序是不稳定的,而且最坏情况下的时间复杂度是O(n^2)。 另外,对象数组中保存的只是对象的引用,这样多次移位并不会造成额外的开销,但是,对象数组对比较次数一般比较敏感,有可能对象的比较比单纯数的比较开销大很多。归并排序在这方面比快速排序做得更好,这也是选择它作为对象排序的一个重要原因之一。 排序优化:实现中快排和归并都采用递归方式,而在递归的底层,也就是待排序的数组长度小于7时,直接使用冒泡排序,而不再递归下去。 分析:长度为6的数组冒泡排序总比较次数最多也就1+2+3+4+5+6=21次,最好情况下只有6次比较。而快排或归并涉及到递归调用等的开销,其时间效率在n较小时劣势就凸显了,因此这里采用了冒泡排序,这也是对快速排序极重要的优化。 源码中的快速排序 1)当待排序的数组中的元素个数较少时,源码中的阀值为7,采用的是插入排序。尽管插入排序的时间复杂度为0(n^2),但是当数组元素较少时,插入排序优于快速排序,因为这时快速排序的递归操作影响性能。 2)较好的选择了划分元(基准元素)。能够将数组分成大致两个相等的部分,避免出现最坏的情况。例如当数组有序的的情况下,选择第一个元素作为划分元,将使得算法的时间复杂度达到O(n^2). 源码中选择划分元的方法: 当数组大小为 size=7 时 ,取数组中间元素作为划分元。int n=m>>1;(此方法值得借鉴) 当数组大小 7 package com.util; public class ArraysPrimitive { private ArraysPrimitive() {} /** * 对指定的 int 型数组按数字升序进行排序。 */ public static void sort(int[] a) { sort1(a, 0, a.length); } /** * 对指定 int 型数组的指定范围按数字升序进行排序。 */ public static void sort(int[] a, int fromIndex, int toIndex) { rangeCheck(a.length, fromIndex, toIndex); sort1(a, fromIndex, toIndex - fromIndex); } private static void sort1(int x[], int off, int len) { /* * 当待排序的数组中的元素个数小于 7 时,采用插入排序 。 * * 尽管插入排序的时间复杂度为O(n^2),但是当数组元素较少时, 插入排序优于快速排序,因为这时快速排序的递归操作影响性能。 */ if (len < 7) { for (int i = off; i < len + off; i++) for (int j = i; j > off && x[j - 1] > x[j]; j--) swap(x, j, j - 1); return; } /* * 当待排序的数组中的元素个数大于 或等于7 时,采用快速排序 。 * * Choose a partition element, v * 选取一个划分元,V * * 较好的选择了划分元(基准元素)。能够将数组分成大致两个相等的部分,避免出现最坏的情况。例如当数组有序的的情况下, * 选择第一个元素作为划分元,将使得算法的时间复杂度达到O(n^2). */ // 当数组大小为size=7时 ,取数组中间元素作为划分元。 int m = off + (len >> 1); // 当数组大小 7<size<=40时,取首、中、末 三个元素中间大小的元素作为划分元。 if (len > 7) { int l = off; int n = off + len - 1; /* * 当数组大小 size>40 时 ,从待排数组中较均匀的选择9个元素, * 选出一个伪中数做为划分元。 */ if (len > 40) { int s = len / 8; l = med3(x, l, l + s, l + 2 * s); m = med3(x, m - s, m, m + s); n = med3(x, n - 2 * s, n - s, n); } // 取出中间大小的元素的位置。 m = med3(x, l, m, n); // Mid-size, med of 3 } //得到划分元V int v = x[m]; // Establish Invariant: v* (<v)* (>v)* v* int a = off, b = a, c = off + len - 1, d = c; while (true) { while (b <= c && x[b] <= v) { if (x[b] == v) swap(x, a++, b); b++; } while (c >= b && x[c] >= v) { if (x[c] == v) swap(x, c, d--); c--; } if (b > c) break; swap(x, b++, c--); } // Swap partition elements back to middle int s, n = off + len; s = Math.min(a - off, b - a); vecswap(x, off, b - s, s); s = Math.min(d - c, n - d - 1); vecswap(x, b, n - s, s); // Recursively sort non-partition-elements if ((s = b - a) > 1) sort1(x, off, s); if ((s = d - c) > 1) sort1(x, n - s, s); } /** * Swaps x[a] with x[b]. */ private static void swap(int x[], int a, int b) { int t = x[a]; x[a] = x[b]; x[b] = t; } /** * Swaps x[a .. (a+n-1)] with x[b .. (b+n-1)]. */ private static void vecswap(int x[], int a, int b, int n) { for (int i=0; i<n; i++, a++, b++) swap(x, a, b); } /** * Returns the index of the median of the three indexed integers. */ private static int med3(int x[], int a, int b, int c) { return (x[a] < x[b] ? (x[b] < x[c] ? b : x[a] < x[c] ? c : a) : (x[b] > x[c] ? b : x[a] > x[c] ? c : a)); } /** * Check that fromIndex and toIndex are in range, and throw an * appropriate exception if they aren't. */ private static void rangeCheck(int arrayLen, int fromIndex, int toIndex) { if (fromIndex > toIndex) throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); if (fromIndex < 0) throw new ArrayIndexOutOfBoundsException(fromIndex); if (toIndex > arrayLen) throw new ArrayIndexOutOfBoundsException(toIndex); } } package com.test; import com.util.ArraysPrimitive; public class ArraysTest { public static void main(String[] args) { int [] a={15,93,15,41,6,15,22,7,15,20}; ArraysPrimitive.sort(a); for(int i=0;i<a.length;i++){ System.out.print(a[i]+","); } //结果:6,7,15,15,15,15,20,22,41,93, } } package com.util; import java.lang.reflect.Array; public class ArraysObject { private static final int INSERTIONSORT_THRESHOLD = 7; private ArraysObject() {} public static void sort(Object[] a) { //java.lang.Object.clone(),理解深表复制和浅表复制 Object[] aux = (Object[]) a.clone(); mergeSort(aux, a, 0, a.length, 0); } public static void sort(Object[] a, int fromIndex, int toIndex) { rangeCheck(a.length, fromIndex, toIndex); Object[] aux = copyOfRange(a, fromIndex, toIndex); mergeSort(aux, a, fromIndex, toIndex, -fromIndex); } /** * Src is the source array that starts at index 0 * Dest is the (possibly larger) array destination with a possible offset * low is the index in dest to start sorting * high is the end index in dest to end sorting * off is the offset to generate corresponding low, high in src */ private static void mergeSort(Object[] src, Object[] dest, int low, int high, int off) { int length = high - low; // Insertion sort on smallest arrays if (length < INSERTIONSORT_THRESHOLD) { for (int i = low; i < high; i++) for (int j = i; j > low && ((Comparable) dest[j - 1]).compareTo(dest[j]) > 0; j--) swap(dest, j, j - 1); return; } // Recursively sort halves of dest into src int destLow = low; int destHigh = high; low += off; high += off; /* * >>>:无符号右移运算符 * expression1 >>> expresion2:expression1的各个位向右移expression2 * 指定的位数。右移后左边空出的位数用0来填充。移出右边的位被丢弃。 * 例如:-14>>>2; 结果为:1073741820 */ int mid = (low + high) >>> 1; mergeSort(dest, src, low, mid, -off); mergeSort(dest, src, mid, high, -off); // If list is already sorted, just copy from src to dest. This is an // optimization that results in faster sorts for nearly ordered lists. if (((Comparable) src[mid - 1]).compareTo(src[mid]) <= 0) { System.arraycopy(src, low, dest, destLow, length); return; } // Merge sorted halves (now in src) into dest for (int i = destLow, p = low, q = mid; i < destHigh; i++) { if (q >= high || p < mid && ((Comparable) src[p]).compareTo(src[q]) <= 0) dest[i] = src[p++]; else dest[i] = src[q++]; } } /** * Check that fromIndex and toIndex are in range, and throw an appropriate * exception if they aren't. */ private static void rangeCheck(int arrayLen, int fromIndex, int toIndex) { if (fromIndex > toIndex) throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); if (fromIndex < 0) throw new ArrayIndexOutOfBoundsException(fromIndex); if (toIndex > arrayLen) throw new ArrayIndexOutOfBoundsException(toIndex); } public static <T> T[] copyOfRange(T[] original, int from, int to) { return copyOfRange(original, from, to, (Class<T[]>) original.getClass()); } public static <T, U> T[] copyOfRange(U[] original, int from, int to, Class<? extends T[]> newType) { int newLength = to - from; if (newLength < 0) throw new IllegalArgumentException(from + " > " + to); T[] copy = ((Object) newType == (Object) Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, from, copy, 0, Math.min(original.length - from, newLength)); return copy; } /** * Swaps x[a] with x[b]. */ private static void swap(Object[] x, int a, int b) { Object t = x[a]; x[a] = x[b]; x[b] = t; } } package com.test; import com.util.ArraysObject; public class ArraysObjectSortTest { public static void main(String[] args) { Student stu1=new Student(1001,100.0F); Student stu2=new Student(1002,90.0F); Student stu3=new Student(1003,90.0F); Student stu4=new Student(1004,95.0F); Student[] stus={stu1,stu2,stu3,stu4}; //Arrays.sort(stus); ArraysObject.sort(stus); for(int i=0;i<stus.length;i++){ System.out.println(stus[i].getId()+" : "+stus[i].getScore()); } /* 1002 : 90.0 * 1003 : 90.0 * 1004 : 95.0 * 1001 : 100.0 */ } } class Student implements Comparable<Student>{ private int id; //学号 private float score; //成绩 public Student(){} public Student(int id,float score){ this.id=id; this.score=score; } @Override public int compareTo(Student s) { return (int)(this.score-s.getScore()); } public int getId() { return id; } public void setId(int id) { this.id = id; } public float getScore() { return score; } public void setScore(float score) { this.score = score; } } package com.lang; public final class System { //System 类不能被实例化。 private System() {} //在 System 类提供的设施中,有标准输入、标准输出和错误输出流;对外部定义的属性 //和环境变量的访问;加载文件和库的方法;还有快速复制数组的一部分的实用方法。 /** * src and dest都必须是同类型或者可以进行转换类型的数组. * @param src the source array. * @param srcPos starting position in the source array. * @param dest the destination array. * @param destPos starting position in the destination data. * @param length the number of array elements to be copied. */ public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); } package com.lang.reflect; public final class Array { private Array() {} //创建一个具有指定的组件类型和维度的新数组。 public static Object newInstance(Class<?> componentType, int length) throws NegativeArraySizeException { return newArray(componentType, length); } private static native Object newArray(Class componentType, int length) throws NegativeArraySizeException; } Arrays.asList 慎用ArrayList的contains方法,使用HashSet的contains方法代替 在启动一个应用的时候,发现其中有一处数据加载要数分钟,刚开始以为是需要load的数据比较多的缘故,查了一下数据库有6条左右,但是单独写了一个数据读取的方法,将这6万多条全部读过来,却只需要不到10秒钟,就觉得这里面肯定有问题,于是仔细看其中的逻辑,其中有一段数据去重的逻辑,就是记录中存在某几个字段相同的,就认为是重复数据,就需要将重复数据给过滤掉。这里就用到了一个List来存放这几个字段所组成的主键,如果发现相同的就不处理,代码无非就是下面这样: List<string> uniqueKeyList = new ArrayList<string>(); //...... if (uniqueKeyList.contains(uniqueKey)) { continue; } 根据键去查找是不是已经存在了,来判断是否重复数据。经过分析,这一块耗费了非常多的时候,于是就去查看ArrayList的contains方法的源码,发现其最终会调用他本身的indexOf方法: 7public int indexOf(Object elem) { if (elem == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (elem.equals(elementData[i])) return i; } return -1; } 原来在这里他做的是遍历整个list进行查找,最多可能对一个键的查找会达到6万多次,也就是会扫描整个List,验怪会这么慢了。 于是将原来的List替换为Set: Set<string> uniqueKeySet = new HashSet<string>(); //...... if (uniqueKeySet.contains(uniqueKey)) { continue; } 速度一下就上去了,在去重这一块最多花费了一秒钟,为什么HashSet的速度一下就上去了,那是因为其内部使用的是Hashtable,这是HashSet的contains的源码: public boolean contains(Object o) { return map.containsKey(o); } 关于UnsupportedOperationException异常 在使用Arrays.asList()后调用add,remove这些method时出现java.lang.UnsupportedOperationException异常。这是由于Arrays.asList() 返回java.util.Arrays$ArrayList, 而不是ArrayList。Arrays$ArrayList和ArrayList都是继承AbstractList,remove,add等method在AbstractList中是默认throw UnsupportedOperationException而且不作任何操作。ArrayList override这些method来对list进行操作,但是Arrays$ArrayList没有override remove(),add()等,所以throw UnsupportedOperationException。
一、源码解析1、 LinkedList类定义2、LinkedList数据结构原理3、私有属性4、构造方法5、元素添加add()及原理6、删除数据remove()7、数据获取get()8、数据复制clone()与toArray()9、遍历数据:Iterator()二、ListItr 一、源码解析 1、 LinkedList类定义。 public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList 实现 List 接口,能对它进行队列操作。LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。LinkedList 是非同步的。 为什么要继承自AbstractSequentialList ? AbstractSequentialList 实现了get(int index)、set(int index, E element)、add(int index, E element) 和 remove(int index)这些骨干性函数。降低了List接口的复杂度。这些接口都是随机访问List的,LinkedList是双向链表;既然它继承于AbstractSequentialList,就相当于已经实现了“get(int index)这些接口”。 此外,我们若需要通过AbstractSequentialList自己实现一个列表,只需要扩展此类,并提供 listIterator() 和 size() 方法的实现即可。若要实现不可修改的列表,则需要实现列表迭代器的 hasNext、next、hasPrevious、previous 和 index 方法即可。 LinkedList的类图关系: 2、LinkedList数据结构原理 LinkedList底层的数据结构是基于双向循环链表的,且头结点中不存放数据,如下: 既然是双向链表,那么必定存在一种数据结构——我们可以称之为节点,节点实例保存业务数据,前一个节点的位置信息和后一个节点位置信息,如下图所示: 3、私有属性 LinkedList中之定义了两个属性: 1 private transient Entry<E> header = new Entry<E>(null, null, null); 2 private transient int size = 0; header是双向链表的头节点,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量: previous, next, element。其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。 size是双向链表中节点实例的个数。 首先来了解节点类Entry类的代码。 1 private static class Entry<E> { 2 E element; 3 Entry<E> next; 4 Entry<E> previous; 5 6 Entry(E element, Entry<E> next, Entry<E> previous) { 7 this.element = element; 8 this.next = next; 9 this.previous = previous; 10 } 11 } 节点类很简单,element存放业务数据,previous与next分别存放前后节点的信息(在数据结构中我们通常称之为前后节点的指针)。 LinkedList的构造方法: 1 public LinkedList() { 2 header.next = header.previous = header; 3 } 4 public LinkedList(Collection<? extends E> c) { 5 this(); 6 addAll(c); 7 } 4、构造方法 LinkedList提供了两个构造方法。 第一个构造方法不接受参数,将header实例的previous和next全部指向header实例(注意,这个是一个双向循环链表,如果不是循环链表,空链表的情况应该是header节点的前一节点和后一节点均为null),这样整个链表其实就只有header一个节点,用于表示一个空的链表。 执行完构造函数后,header实例自身形成一个闭环,如下图所示: 第二个构造方法接收一个Collection参数c,调用第一个构造方法构造一个空的链表,之后通过addAll将c中的元素全部添加到链表中。 5、元素添加 1 public boolean addAll(Collection<? extends E> c) { 2 return addAll(size, c); 3 } 4 // index参数指定collection中插入的第一个元素的位置 5 public boolean addAll(int index, Collection<? extends E> c) { 6 // 插入位置超过了链表的长度或小于0,报IndexOutOfBoundsException异常 7 if (index < 0 || index > size) 8 throw new IndexOutOfBoundsException("Index: "+index+ 9 ", Size: "+size); 10 Object[] a = c.toArray(); 11 int numNew = a.length; 12 // 若需要插入的节点个数为0则返回false,表示没有插入元素 13 if (numNew==0) 14 return false; 15 modCount++;//否则,插入对象,链表修改次数加1 16 // 保存index处的节点。插入位置如果是size,则在头结点前面插入,否则在获取index处的节点插入 17 Entry<E> successor = (index==size ? header : entry(index)); 18 // 获取前一个节点,插入时需要修改这个节点的next引用 19 Entry<E> predecessor = successor.previous; 20 // 按顺序将a数组中的第一个元素插入到index处,将之后的元素插在这个元素后面 21 for (int i=0; i<numNew; i++) { 22 // 结合Entry的构造方法,这条语句是插入操作,相当于C语言中链表中插入节点并修改指针 23 Entry<E> e = new Entry<E>((E)a[i], successor, predecessor); 24 // 插入节点后将前一节点的next指向当前节点,相当于修改前一节点的next指针 25 predecessor.next = e; 26 // 相当于C语言中成功插入元素后将指针向后移动一个位置以实现循环的功能 27 predecessor = e; 28 } 29 // 插入元素前index处的元素链接到插入的Collection的最后一个节点 30 successor.previous = predecessor; 31 // 修改size 32 size += numNew; 33 return true; 34 } 构造方法中的调用了addAll(Collection<? extends E> c)方法,而在addAll(Collection<? extends E> c)方法中仅仅是将size当做index参数调用了addAll(int index,Collection<? extends E> c)方法。 1 private Entry<E> entry(int index) { 2 if (index < 0 || index >= size) 3 throw new IndexOutOfBoundsException("Index: "+index+ 4 ", Size: "+size); 5 Entry<E> e = header; 6 // 根据这个判断决定从哪个方向遍历这个链表 7 if (index < (size >> 1)) { 8 for (int i = 0; i <= index; i++) 9 e = e.next; 10 } else { 11 // 可以通过header节点向前遍历,说明这个一个循环双向链表,header的previous指向链表的最后一个节点,这也验证了构造方法中对于header节点的前后节点均指向自己的解释 12 for (int i = size; i > index; i--) 13 e = e.previous; 14 } 15 return e; 16 } 下面说明双向链表添加元素的原理: 添加数据:add() // 将元素(E)添加到LinkedList中 public boolean add(E e) { // 将节点(节点数据是e)添加到表头(header)之前。 // 即,将节点添加到双向链表的末端。 addBefore(e, header); return true; } public void add(int index, E element) { addBefore(element, (index==size ? header : entry(index))); } private Entry<E> addBefore(E e, Entry<E> entry) { Entry<E> newEntry = new Entry<E>(e, entry, entry.previous); newEntry.previous.next = newEntry; newEntry.next.previous = newEntry; size++; modCount++; return newEntry; } addBefore(E e,Entry<E> entry)方法是个私有方法,所以无法在外部程序中调用(当然,这是一般情况,你可以通过反射上面的还是能调用到的)。 addBefore(E e,Entry<E> entry)先通过Entry的构造方法创建e的节点newEntry(包含了将其下一个节点设置为entry,上一个节点设置为entry.previous的操作,相当于修改newEntry的“指针”),之后修改插入位置后newEntry的前一节点的next引用和后一节点的previous引用,使链表节点间的引用关系保持正确。之后修改和size大小和记录modCount,然后返回新插入的节点。 下面分解“添加第一个数据”的步骤: 第一步:初始化后LinkedList实例的情况: 第二步:初始化一个预添加的Entry实例(newEntry)。 Entry newEntry = newEntry(e, entry, entry.previous); 第三步:调整新加入节点和头结点(header)的前后指针。 newEntry.previous.next = newEntry; newEntry.previous即header,newEntry.previous.next即header的next指向newEntry实例。在上图中应该是“4号线”指向newEntry。 newEntry.next.previous = newEntry; newEntry.next即header,newEntry.next.previous即header的previous指向newEntry实例。在上图中应该是“3号线”指向newEntry。 调整后如下图所示: 图——加入第一个节点后LinkedList示意图 下面分解“添加第二个数据”的步骤: 第一步:新建节点。 图——添加第二个节点 第二步:调整新节点和头结点的前后指针信息。 图——调整前后指针信息 添加后续数据情况和上述一致,LinkedList实例是没有容量限制的。 总结,addBefore(E e,Entry<E> entry)实现在entry之前插入由e构造的新节点。而add(E e)实现在header节点之前插入由e构造的新节点。为了便于理解,下面给出插入节点的示意图。 public void addFirst(E e) { addBefore(e, header.next); } public void addLast(E e) { addBefore(e, header); } 看上面的示意图,结合addBefore(E e,Entry<E> entry)方法,很容易理解addFrist(E e)只需实现在header元素的下一个元素之前插入,即示意图中的一号之前。addLast(E e)只需在实现在header节点前(因为是循环链表,所以header的前一个节点就是链表的最后一个节点)插入节点(插入后在2号节点之后)。 清除数据clear() 1 public void clear() { 2 Entry<E> e = header.next; 3 // e可以理解为一个移动的“指针”,因为是循环链表,所以回到header的时候说明已经没有节点了 4 while (e != header) { 5 // 保留e的下一个节点的引用 6 Entry<E> next = e.next; 7 // 解除节点e对前后节点的引用 8 e.next = e.previous = null; 9 // 将节点e的内容置空 10 e.element = null; 11 // 将e移动到下一个节点 12 e = next; 13 } 14 // 将header构造成一个循环链表,同构造方法构造一个空的LinkedList 15 header.next = header.previous = header; 16 // 修改size 17 size = 0; 18 modCount++; 19 } 数据包含 contains(Object o) public boolean contains(Object o) { return indexOf(o) != -1; } // 从前向后查找,返回“值为对象(o)的节点对应的索引” 不存在就返回-1 public int indexOf(Object o) { int index = 0; if (o==null) { for (Entry e = header.next; e != header; e = e.next) { if (e.element==null) return index; index++; } } else { for (Entry e = header.next; e != header; e = e.next) { if (o.equals(e.element)) return index; index++; } } return -1; } indexOf(Object o)判断o链表中是否存在节点的element和o相等,若相等则返回该节点在链表中的索引位置,若不存在则放回-1。 contains(Object o)方法通过判断indexOf(Object o)方法返回的值是否是-1来判断链表中是否包含对象o。 6、删除数据remove() 几个remove方法最终都是调用了一个私有方法:remove(Entry<E> e),只是其他简单逻辑上的区别。下面分析remove(Entry<E> e)方法。 1 private E remove(Entry<E> e) { 2 if (e == header) 3 throw new NoSuchElementException(); 4 // 保留将被移除的节点e的内容 5 E result = e.element; 6 // 将前一节点的next引用赋值为e的下一节点 7 e.previous.next = e.next; 8 // 将e的下一节点的previous赋值为e的上一节点 9 e.next.previous = e.previous; 10 // 上面两条语句的执行已经导致了无法在链表中访问到e节点,而下面解除了e节点对前后节点的引用 11 e.next = e.previous = null; 12 // 将被移除的节点的内容设为null 13 e.element = null; 14 // 修改size大小 15 size--; 16 modCount++; 17 // 返回移除节点e的内容 18 return result; 19 } 由于删除了某一节点因此调整相应节点的前后指针信息,如下: e.previous.next = e.next;//预删除节点的前一节点的后指针指向预删除节点的后一个节点。 e.next.previous = e.previous;//预删除节点的后一节点的前指针指向预删除节点的前一个节点。 清空预删除节点: e.next = e.previous = null; e.element = null; 交给gc完成资源回收,删除操作结束。 与ArrayList比较而言,LinkedList的删除动作不需要“移动”很多数据,从而效率更高。 7、数据获取get() Get(int)方法的实现在remove(int)中已经涉及过了。首先判断位置信息是否合法(大于等于0,小于当前LinkedList实例的Size),然后遍历到具体位置,获得节点的业务数据(element)并返回。 注意:为了提高效率,需要根据获取的位置判断是从头还是从尾开始遍历。 // 获取双向链表中指定位置的节点 private Entry<E> entry(int index) { if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+size); Entry<E> e = header; // 获取index处的节点。 // 若index < 双向链表长度的1/2,则从前先后查找; // 否则,从后向前查找。 if (index < (size >> 1)) { for (int i = 0; i <= index; i++) e = e.next; } else { for (int i = size; i > index; i--) e = e.previous; } return e; } 注意细节:位运算与直接做除法的区别。先将index与长度size的一半比较,如果index<size/2,就只从位置0往后遍历到位置index处,而如果index>size/2,就只从位置size往前遍历到位置index处。这样可以减少一部分不必要的遍历 8、数据复制clone()与toArray() clone() 1 public Object clone() { 2 LinkedList<E> clone = null; 3 try { 4 clone = (LinkedList<E>) super.clone(); 5 } catch (CloneNotSupportedException e) { 6 throw new InternalError(); 7 } 8 clone.header = new Entry<E>(null, null, null); 9 clone.header.next = clone.header.previous = clone.header; 10 clone.size = 0; 11 clone.modCount = 0; 12 for (Entry<E> e = header.next; e != header; e = e.next) 13 clone.add(e.element); 14 return clone; 15 } 调用父类的clone()方法初始化对象链表clone,将clone构造成一个空的双向循环链表,之后将header的下一个节点开始将逐个节点添加到clone中。最后返回克隆的clone对象。 toArray() 1 public Object[] toArray() { 2 Object[] result = new Object[size]; 3 int i = 0; 4 for (Entry<E> e = header.next; e != header; e = e.next) 5 result[i++] = e.element; 6 return result; 7 } 创建大小和LinkedList相等的数组result,遍历链表,将每个节点的元素element复制到数组中,返回数组。 toArray(T[] a) 1 public <T> T[] toArray(T[] a) { 2 if (a.length < size) 3 a = (T[])java.lang.reflect.Array.newInstance( 4 a.getClass().getComponentType(), size); 5 int i = 0; 6 Object[] result = a; 7 for (Entry<E> e = header.next; e != header; e = e.next) 8 result[i++] = e.element; 9 if (a.length > size) 10 a[size] = null; 11 return a; 12 } 先判断出入的数组a的大小是否足够,若大小不够则拓展。这里用到了发射的方法,重新实例化了一个大小为size的数组。之后将数组a赋值给数组result,遍历链表向result中添加的元素。最后判断数组a的长度是否大于size,若大于则将size位置的内容设置为null。返回a。 从代码中可以看出,数组a的length小于等于size时,a中所有元素被覆盖,被拓展来的空间存储的内容都是null;若数组a的length的length大于size,则0至size-1位置的内容被覆盖,size位置的元素被设置为null,size之后的元素不变。 为什么不直接对数组a进行操作,要将a赋值给result数组之后对result数组进行操作? 9、遍历数据:Iterator() LinkedList的Iterator 除了Entry,LinkedList还有一个内部类:ListItr。 ListItr实现了ListIterator接口,可知它是一个迭代器,通过它可以遍历修改LinkedList。 在LinkedList中提供了获取ListItr对象的方法:listIterator(int index)。 1 public ListIterator<E> listIterator(int index) { 2 return new ListItr(index); 3 } 该方法只是简单的返回了一个ListItr对象。 LinkedList中还有通过集成获得的listIterator()方法,该方法只是调用了listIterator(int index)并且传入0。 二、ListItr 下面详细分析ListItr。 1 private class ListItr implements ListIterator<E> { 2 // 最近一次返回的节点,也是当前持有的节点 3 private Entry<E> lastReturned = header; 4 // 对下一个元素的引用 5 private Entry<E> next; 6 // 下一个节点的index 7 private int nextIndex; 8 private int expectedModCount = modCount; 9 // 构造方法,接收一个index参数,返回一个ListItr对象 10 ListItr(int index) { 11 // 如果index小于0或大于size,抛出IndexOutOfBoundsException异常 12 if (index < 0 || index > size) 13 throw new IndexOutOfBoundsException("Index: "+index+ 14 ", Size: "+size); 15 // 判断遍历方向 16 if (index < (size >> 1)) { 17 // next赋值为第一个节点 18 next = header.next; 19 // 获取指定位置的节点 20 for (nextIndex=0; nextIndex<index; nextIndex++) 21 next = next.next; 22 } else { 23 // else中的处理和if块中的处理一致,只是遍历方向不同 24 next = header; 25 for (nextIndex=size; nextIndex>index; nextIndex--) 26 next = next.previous; 27 } 28 } 29 // 根据nextIndex是否等于size判断时候还有下一个节点(也可以理解为是否遍历完了LinkedList) 30 public boolean hasNext() { 31 return nextIndex != size; 32 } 33 // 获取下一个元素 34 public E next() { 35 checkForComodification(); 36 // 如果nextIndex==size,则已经遍历完链表,即没有下一个节点了(实际上是有的,因为是循环链表,任何一个节点都会有上一个和下一个节点,这里的没有下一个节点只是说所有节点都已经遍历完了) 37 if (nextIndex == size) 38 throw new NoSuchElementException(); 39 // 设置最近一次返回的节点为next节点 40 lastReturned = next; 41 // 将next“向后移动一位” 42 next = next.next; 43 // index计数加1 44 nextIndex++; 45 // 返回lastReturned的元素 46 return lastReturned.element; 47 } 48 49 public boolean hasPrevious() { 50 return nextIndex != 0; 51 } 52 // 返回上一个节点,和next()方法相似 53 public E previous() { 54 if (nextIndex == 0) 55 throw new NoSuchElementException(); 56 57 lastReturned = next = next.previous; 58 nextIndex--; 59 checkForComodification(); 60 return lastReturned.element; 61 } 62 63 public int nextIndex() { 64 return nextIndex; 65 } 66 67 public int previousIndex() { 68 return nextIndex-1; 69 } 70 // 移除当前Iterator持有的节点 71 public void remove() { 72 checkForComodification(); 73 Entry<E> lastNext = lastReturned.next; 74 try { 75 LinkedList.this.remove(lastReturned); 76 } catch (NoSuchElementException e) { 77 throw new IllegalStateException(); 78 } 79 if (next==lastReturned) 80 next = lastNext; 81 else 82 nextIndex--; 83 lastReturned = header; 84 expectedModCount++; 85 } 86 // 修改当前节点的内容 87 public void set(E e) { 88 if (lastReturned == header) 89 throw new IllegalStateException(); 90 checkForComodification(); 91 lastReturned.element = e; 92 } 93 // 在当前持有节点后面插入新节点 94 public void add(E e) { 95 checkForComodification(); 96 // 将最近一次返回节点修改为header 97 lastReturned = header; 98 addBefore(e, next); 99 nextIndex++; 100 expectedModCount++; 101 } 102 // 判断expectedModCount和modCount是否一致,以确保通过ListItr的修改操作正确的反映在LinkedList中 103 final void checkForComodification() { 104 if (modCount != expectedModCount) 105 throw new ConcurrentModificationException(); 106 } 107 } 下面是一个ListItr的使用实例。 1 LinkedList<String> list = new LinkedList<String>(); 2 list.add("First"); 3 list.add("Second"); 4 list.add("Thrid"); 5 System.out.println(list); 6 ListIterator<String> itr = list.listIterator(); 7 while (itr.hasNext()) { 8 System.out.println(itr.next()); 9 } 10 try { 11 System.out.println(itr.next());// throw Exception 12 } catch (Exception e) { 13 // TODO: handle exception 14 } 15 itr = list.listIterator(); 16 System.out.println(list); 17 System.out.println(itr.next()); 18 itr.add("new node1"); 19 System.out.println(list); 20 itr.add("new node2"); 21 System.out.println(list); 22 System.out.println(itr.next()); 23 itr.set("modify node"); 24 System.out.println(list); 25 itr.remove(); 26 System.out.println(list); 1 结果: 2 [First, Second, Thrid] 3 First 4 Second 5 Thrid 6 [First, Second, Thrid] 7 First 8 [First, new node1, Second, Thrid] 9 [First, new node1, new node2, Second, Thrid] 10 Second 11 [First, new node1, new node2, modify node, Thrid] 12 [First, new node1, new node2, Thrid] LinkedList还有一个提供Iterator的方法:descendingIterator()。该方法返回一个DescendingIterator对象。DescendingIterator是LinkedList的一个内部类。 1 public Iterator<E> descendingIterator() { 2 return new DescendingIterator(); 3 } 下面分析详细分析DescendingIterator类。 1 private class DescendingIterator implements Iterator { 2 // 获取ListItr对象 3 final ListItr itr = new ListItr(size()); 4 // hasNext其实是调用了itr的hasPrevious方法 5 public boolean hasNext() { 6 return itr.hasPrevious(); 7 } 8 // next()其实是调用了itr的previous方法 9 public E next() { 10 return itr.previous(); 11 } 12 public void remove() { 13 itr.remove(); 14 } 15 } 从类名和上面的代码可以看出这是一个反向的Iterator,代码很简单,都是调用的ListItr类中的方法。 参考: Java集合类--LinkedList java源码分析之LinkedList
ArrayList是List接口的可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。 注意,此实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。 ArrayList是可以动态增长和缩减的索引序列,它是基于数组实现的List类。ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。 ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。 ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。 如果想ArrayList中添加大量元素,可使用ensureCapacity方法一次性增加capacity,可以减少增加重分配的次数提高性能。 注意,此实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。 ArrayList的用法和Vector向类似,但是Vector是一个较老的集合,具有很多缺点,不建议使用。另外,ArrayList和Vector的区别是:ArrayList是线程不安全的,当多条线程访问同一个ArrayList集合时,程序需要手动保证该集合的同步性,而Vector则是线程安全的。 public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8683452581122892189L; //默认的初始容量为10 private static final int DEFAULT_CAPACITY = 10; private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; transient Object[] elementData; // ArrayList中实际数据的数量 private int size; public ArrayList(int initialCapacity) //带初始容量大小的构造函数 { if (initialCapacity > 0) //初始容量大于0,实例化数组 { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) //初始化等于0,将空数组赋给elementData { this.elementData = EMPTY_ELEMENTDATA; } else //初始容量小于,抛异常 { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } public ArrayList() //无参构造函数,默认容量为10 { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } public ArrayList(Collection<? extends E> c) //创建一个包含collection的ArrayList { elementData = c.toArray(); //返回包含c所有元素的数组 if ((size = elementData.length) != 0) { if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class);//复制指定数组,使elementData具有指定长度 } else { //c中没有元素 this.elementData = EMPTY_ELEMENTDATA; } } //将当前容量值设为当前实际元素大小 public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0)? EMPTY_ELEMENTDATA:Arrays.copyOf(elementData, size); } } //将集合的capacit增加minCapacity public void ensureCapacity(int minCapacity) { int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)?0:DEFAULT_CAPACITY; if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } } private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity); } private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private void grow(int minCapacity) { int oldCapacity = elementData.length; //注意此处扩充capacity的方式是将其向右一位再加上原来的数,实际上是扩充了1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } //返回ArrayList的大小 public int size() { return size; } //判断ArrayList是否为空 public boolean isEmpty() { return size == 0; } //判断ArrayList中是否包含Object(o) public boolean contains(Object o) { return indexOf(o) >= 0; } //正向查找,返回ArrayList中元素Object(o)的索引位置 public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1; } //逆向查找,返回返回ArrayList中元素Object(o)的索引位置 public int lastIndexOf(Object o) { if (o == null) { for (int i = size-1; i >= 0; i--) if (elementData[i]==null) return i; } else { for (int i = size-1; i >= 0; i--) if (o.equals(elementData[i])) return i; } return -1; } //返回此 ArrayList实例的浅拷贝。 public Object clone() { try { ArrayList<?> v = (ArrayList<?>) super.clone(); v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); } } //返回一个包含ArrayList中所有元素的数组 public Object[] toArray() { return Arrays.copyOf(elementData, size); } @SuppressWarnings("unchecked") public <T> T[] toArray(T[] a) { if (a.length < size) return (T[]) Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; } @SuppressWarnings("unchecked") E elementData(int index) { return (E) elementData[index]; } //返回至指定索引的值 public E get(int index) { rangeCheck(index); //检查给定的索引值是否越界 return elementData(index); } //将指定索引上的值替换为新值,并返回旧值 public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; } //将指定的元素添加到此列表的尾部 public boolean add(E e) { ensureCapacityInternal(size + 1); elementData[size++] = e; return true; } // 将element添加到ArrayList的指定位置 public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); //从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。 //arraycopy(被复制的数组, 从第几个元素开始复制, 要复制到的数组, 从第几个元素开始粘贴, 一共需要复制的元素个数) //即在数组elementData从index位置开始,复制到index+1位置,共复制size-index个元素 System.arraycopy(elementData, index, elementData, index + 1,size - index); elementData[index] = element; size++; } //删除ArrayList指定位置的元素 public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index,numMoved); elementData[--size] = null; //将原数组最后一个位置置为null return oldValue; } //移除ArrayList中首次出现的指定元素(如果存在)。 public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } //快速删除指定位置的元素 private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; } //清空ArrayList,将全部的元素设为null public void clear() { modCount++; for (int i = 0; i < size; i++) elementData[i] = null; size = 0; } //按照c的迭代器所返回的元素顺序,将c中的所有元素添加到此列表的尾部 public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); // Increments modCount System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; } //从指定位置index开始,将指定c中的所有元素插入到此列表中 public boolean addAll(int index, Collection<? extends E> c) { rangeCheckForAdd(index); Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); // Increments modCount int numMoved = size - index; if (numMoved > 0) //先将ArrayList中从index开始的numMoved个元素移动到起始位置为index+numNew的后面去 System.arraycopy(elementData, index, elementData, index + numNew, numMoved); //再将c中的numNew个元素复制到起始位置为index的存储空间中去 System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; } //删除fromIndex到toIndex之间的全部元素 protected void removeRange(int fromIndex, int toIndex) { modCount++; //numMoved为删除索引后面的元素个数 int numMoved = size - toIndex; //将删除索引后面的元素复制到以fromIndex为起始位置的存储空间中去 System.arraycopy(elementData, toIndex, elementData, fromIndex,numMoved); int newSize = size - (toIndex-fromIndex); //将ArrayList后面(toIndex-fromIndex)个元素置为null for (int i = newSize; i < size; i++) { elementData[i] = null; } size = newSize; } //检查索引是否越界 private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } private String outOfBoundsMsg(int index) { return "Index: "+index+", Size: "+size; } //删除ArrayList中包含在c中的元素 public boolean removeAll(Collection<?> c) { Objects.requireNonNull(c); return batchRemove(c, false); } //删除ArrayList中除包含在c中的元素,和removeAll相反 public boolean retainAll(Collection<?> c) { Objects.requireNonNull(c); //检查指定对象是否为空 return batchRemove(c, true); } private boolean batchRemove(Collection<?> c, boolean complement) { final Object[] elementData = this.elementData; int r = 0, w = 0; boolean modified = false; try { for (; r < size; r++) if (c.contains(elementData[r]) == complement) //判断c中是否有elementData[r]元素 elementData[w++] = elementData[r]; } finally { if (r != size) { System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if (w != size) { // clear to let GC do its work for (int i = w; i < size; i++) elementData[i] = null; modCount += size - w; size = w; modified = true; } } return modified; } //将ArrayList的“容量,所有的元素值”都写入到输出流中 private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { int expectedModCount = modCount; s.defaultWriteObject(); //写入数组大小 s.writeInt(size); //写入所有数组的元素 for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } //先将ArrayList的“大小”读出,然后将“所有的元素值”读出 private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { elementData = EMPTY_ELEMENTDATA; s.defaultReadObject(); s.readInt(); // ignored if (size > 0) { // be like clone(), allocate array based upon size not capacity ensureCapacityInternal(size); Object[] a = elementData; // Read in all elements in the proper order. for (int i=0; i<size; i++) { a[i] = s.readObject(); } } } ArrayList定义只定义类两个私有属性 elementData存储ArrayList内的元素,size表示它包含的元素的数量。transient。 Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。 /** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. */ private transient Object[] elementData; /** * The size of the ArrayList (the number of elements it contains). * * @serial */ private int size; 被标记为transient的属性在对象被序列化的时候不会被保存。 ArrayList提供了三种方式的构造器,可以构造一个默认初始容量为10的空列表、构造一个指定初始容量的空列表以及构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列的; // ArrayList带容量大小的构造函数。 public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); // 新建一个数组 this.elementData = new Object[initialCapacity]; } // ArrayList无参构造函数。默认容量是10。 public ArrayList() { this(10); } // 创建一个包含collection的ArrayList public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); size = elementData.length; if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } ArrayList提供了set(int index, E element)、add(E e)、add(int index, E element)、addAll(Collection // 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。 public E set(int index, E element) { RangeCheck(index); E oldValue = (E) elementData[index]; elementData[index] = element; return oldValue; } // 将指定的元素添加到此列表的尾部。 public boolean add(E e) { ensureCapacity(size + 1); elementData[size++] = e; return true; } // 将指定的元素插入此列表中的指定位置。 // 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。 public void add(int index, E element) { if (index > size || index < 0) throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size); // 如果数组长度不足,将进行扩容。 ensureCapacity(size+1); // Increments modCount!! // 将 elementData中从Index位置开始、长度为size-index的元素, // 拷贝到从下标为index+1位置开始的新的elementData数组中。 // 即将当前位于该位置的元素以及所有后续元素右移一个位置。 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } // 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。 public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); int numNew = a.length; ensureCapacity(size + numNew); // Increments modCount System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; } // 从指定的位置开始,将指定collection中的所有元素插入到此列表中。 public boolean addAll(int index, Collection<? extends E> c) { if (index > size || index < 0) throw new IndexOutOfBoundsException( "Index: " + index + ", Size: " + size); Object[] a = c.toArray(); int numNew = a.length; ensureCapacity(size + numNew); // Increments modCount int numMoved = size - index; if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; } ArrayList是基于数组实现的,属性中也看到了数组,具体是怎么实现的呢?比如就这个添加元素的方法,如果数组大,则在将某个位置的值设置为指定元素即可,如果数组容量不够了呢? 看到add(E e)中先调用了ensureCapacity(size+1)方法,之后将元素的索引赋给elementData[size],而后size自增。例如初次添加时,size为0,add将elementData[0]赋值为e,然后size设置为1(类似执行以下两条语句elementData[0]=e;size=1)。将元素的索引赋给elementData[size]不是会出现数组越界的情况吗?这里关键就在ensureCapacity(size+1)中了。 // 返回此列表中指定位置上的元素。 public E get(int index) { RangeCheck(index); return (E) elementData[index]; } ArrayList提供了根据下标或者指定对象两种方式的删除功能。如下: // 移除此列表中指定位置上的元素。 public E remove(int index) { RangeCheck(index); modCount++; E oldValue = (E) elementData[index]; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // Let gc do its work return oldValue; } 首先是检查范围,修改modCount,保留将要被移除的元素,将移除位置之后的元素向前挪动一个位置,将list末尾元素置空(null),返回被移除的元素。 // 移除此列表中首次出现的指定元素(如果存在)。这是应为ArrayList中允许存放重复的元素。 public boolean remove(Object o) { // 由于ArrayList中允许存放null,因此下面通过两种情况来分别处理。 if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { // 类似remove(int index),移除列表中指定位置上的元素。 fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } } 首先通过代码可以看到,当移除成功后返回true,否则返回false。remove(Object o)中通过遍历element寻找是否存在传入对象,一旦找到就调用fastRemove移除对象。为什么找到了元素就知道了index,不通过remove(index)来移除元素呢?因为fastRemove跳过了判断边界的处理,因为找到元素就相当于确定了index不会超过边界,而且fastRemove并不返回被移除的元素。下面是fastRemove的代码,基本和remove(index)一致。 private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // Let gc do its work } protected void removeRange(int fromIndex, int toIndex) { modCount++; int numMoved = size - toIndex; System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved); // Let gc do its work int newSize = size - (toIndex-fromIndex); while (size != newSize) elementData[--size] = null; } 执行过程是将elementData从toIndex位置开始的元素向前移动到fromIndex,然后将toIndex位置之后的元素全部置空顺便修改size。 这个方法是protected,及受保护的方法,为什么这个方法被定义为protected呢 public void ensureCapacity(int minCapacity) { modCount++; int oldCapacity = elementData.length; if (minCapacity > oldCapacity) { Object oldData[] = elementData; int newCapacity = (oldCapacity * 3)/2 + 1; //增加50%+1 if (newCapacity < minCapacity) newCapacity = minCapacity; // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } } 数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。 Object oldData[] = elementData;//为什么要用到oldData[] 乍一看来后面并没有用到关于oldData, 这句话显得多此一举!但是这是一个牵涉到内存管理的类, 所以要了解内部的问题。 而且为什么这一句还在if的内部,这跟elementData = Arrays.copyOf(elementData, newCapacity); 这句是有关系的,下面这句Arrays.copyOf的实现时新创建了newCapacity大小的内存,然后把老的elementData放入。好像也没有用到oldData,有什么问题呢。问题就在于旧的内存的引用是elementData, elementData指向了新的内存块,如果有一个局部变量oldData变量引用旧的内存块的话,在copy的过程中就会比较安全,因为这样证明这块老的内存依然有引用,分配内存的时候就不会被侵占掉,然后copy完成后这个局部变量的生命期也过去了,然后释放才是安全的。不然在copy的的时候万一新的内存或其他线程的分配内存侵占了这块老的内存,而copy还没有结束,这将是个严重的事情。 关于ArrayList和Vector区别如下: ArrayList在内存不够时默认是扩展50% + 1个,Vector是默认扩展1倍。 Vector提供indexOf(obj, start)接口,ArrayList没有。 Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为线程安全需要更大的系统开销。 ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。代码如下: public void trimToSize() { modCount++; int oldCapacity = elementData.length; if (size < oldCapacity) { elementData = Arrays.copyOf(elementData, size); } } 由于elementData的长度会被拓展,size标记的是其中包含的元素的个数。所以会出现size很小但elementData.length很大的情况,将出现空间的浪费。trimToSize将返回一个新的数组给elementData,元素内容保持不变,length和size相同,节省空间。 两个转化为静态数组的toArray方法 第一个, 调用Arrays.copyOf将返回一个数组,数组内容是size个elementData的元素,即拷贝elementData从0至size-1位置的元素到新数组并返回。 public Object[] toArray() { return Arrays.copyOf(elementData, size); } 第二个,如果传入数组的长度小于size,返回一个新的数组,大小为size,类型与传入数组相同。所传入数组长度与size相等,则将elementData复制到传入数组中并返回传入的数组。若传入数组长度大于size,除了复制elementData外,还将把返回数组的第size个元素置为空。 public <T> T[] toArray(T[] a) { if (a.length < size) // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; } 关于ArrayList的源码,给出几点比较重要的总结: 1、注意其三个不同的构造方法。无参构造方法构造的ArrayList的容量默认为10,带有Collection参数的构造方法,将Collection转化为数组赋给ArrayList的实现数组elementData。 2、注意扩充容量的方法ensureCapacity。ArrayList在每次增加元素(可能是1个,也可能是一组)时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍加1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后用Arrays.copyof()方法将元素拷贝到新的数组(详见下面的第3点)。从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先能确定元素数量的情况下,才使用ArrayList,否则建议使用LinkedList。 ArrayList的实现中大量地调用了Arrays.copyof()和System.arraycopy()方法。我们有必要对这两个方法的实现做下深入的了解。 Arrays.copyof()方法。它有很多个重载的方法,但实现思路都是一样的,我们来看泛型版本的源码 public static <T> T[] copyOf(T[] original, int newLength) { return (T[]) copyOf(original, newLength, original.getClass()); } public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; } 很明显调用了另一个copyof方法,该方法有三个参数,最后一个参数指明要转换的数据的类型 这里可以很明显地看出,该方法实际上是在其内部又创建了一个长度为newlength的数组,调用System.arraycopy()方法,将原来数组中的元素复制到了新的数组中。 下面来看System.arraycopy()方法。该方法被标记了native,调用了系统的C/C++代码,在JDK中是看不到的,但在openJDK中可以看到其源码。该函数实际上最终调用了C语言的memmove()函数,因此它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很适合用来批量处理数组。Java强烈推荐在复制大量数组元素时用该方法,以取得更高的效率。 ArrayList基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低。 在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理,ArrayList中允许元素为null。
HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用null元素。对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成。 public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { static final long serialVersionUID = -5024744406713321676L; // 底层使用HashMap来保存HashSet中所有元素。 private transient HashMap<E,Object> map; // 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。 private static final Object PRESENT = new Object(); /** * 默认的无参构造器,构造一个空的HashSet。 * * 实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。 */ public HashSet() { map = new HashMap<E,Object>(); } /** * 构造一个包含指定collection中的元素的新set。 * * 实际底层使用默认的加载因子0.75和足以包含指定 * collection中所有元素的初始容量来创建一个HashMap。 * @param c 其中的元素将存放在此set中的collection。 */ public HashSet(Collection<? extends E> c) { map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } /** * 以指定的initialCapacity和loadFactor构造一个空的HashSet。 * * 实际底层以相应的参数构造一个空的HashMap。 * @param initialCapacity 初始容量。 * @param loadFactor 加载因子。 */ public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<E,Object>(initialCapacity, loadFactor); } /** * 以指定的initialCapacity构造一个空的HashSet。 * * 实际底层以相应的参数及加载因子loadFactor为0.75构造一个空的HashMap。 * @param initialCapacity 初始容量。 */ public HashSet(int initialCapacity) { map = new HashMap<E,Object>(initialCapacity); } /** * 以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。 * 此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持。 * * 实际底层会以指定的参数构造一个空LinkedHashMap实例来实现。 * @param initialCapacity 初始容量。 * @param loadFactor 加载因子。 * @param dummy 标记。 */ HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor); } /** * 返回对此set中元素进行迭代的迭代器。返回元素的顺序并不是特定的。 * * 底层实际调用底层HashMap的keySet来返回所有的key。 * 可见HashSet中的元素,只是存放在了底层HashMap的key上, * value使用一个static final的Object对象标识。 * @return 对此set中元素进行迭代的Iterator。 */ public Iterator<E> iterator() { return map.keySet().iterator(); } /** * 返回此set中的元素的数量(set的容量)。 * * 底层实际调用HashMap的size()方法返回Entry的数量,就得到该Set中元素的个数。 * @return 此set中的元素的数量(set的容量)。 */ public int size() { return map.size(); } /** * 如果此set不包含任何元素,则返回true。 * * 底层实际调用HashMap的isEmpty()判断该HashSet是否为空。 * @return 如果此set不包含任何元素,则返回true。 */ public boolean isEmpty() { return map.isEmpty(); } /** * 如果此set包含指定元素,则返回true。 * 更确切地讲,当且仅当此set包含一个满足(o==null ? e==null : o.equals(e)) * 的e元素时,返回true。 * * 底层实际调用HashMap的containsKey判断是否包含指定key。 * @param o 在此set中的存在已得到测试的元素。 * @return 如果此set包含指定元素,则返回true。 */ public boolean contains(Object o) { return map.containsKey(o); } /** * 如果此set中尚未包含指定元素,则添加指定元素。 * 更确切地讲,如果此 set 没有包含满足(e==null ? e2==null : e.equals(e2)) * 的元素e2,则向此set 添加指定的元素e。 * 如果此set已包含该元素,则该调用不更改set并返回false。 * * 底层实际将将该元素作为key放入HashMap。 * 由于HashMap的put()方法添加key-value对时,当新放入HashMap的Entry中key * 与集合中原有Entry的key相同(hashCode()返回值相等,通过equals比较也返回true), * 新添加的Entry的value会将覆盖原来Entry的value,但key不会有任何改变, * 因此如果向HashSet中添加一个已经存在的元素时,新添加的集合元素将不会被放入HashMap中, * 原来的元素也不会有任何改变,这也就满足了Set中元素不重复的特性。 * @param e 将添加到此set中的元素。 * @return 如果此set尚未包含指定元素,则返回true。 */ public boolean add(E e) { return map.put(e, PRESENT)==null; } /** * 如果指定元素存在于此set中,则将其移除。 * 更确切地讲,如果此set包含一个满足(o==null ? e==null : o.equals(e))的元素e, * 则将其移除。如果此set已包含该元素,则返回true * (或者:如果此set因调用而发生更改,则返回true)。(一旦调用返回,则此set不再包含该元素)。 * * 底层实际调用HashMap的remove方法删除指定Entry。 * @param o 如果存在于此set中则需要将其移除的对象。 * @return 如果set包含指定元素,则返回true。 */ public boolean remove(Object o) { return map.remove(o)==PRESENT; } /** * 从此set中移除所有元素。此调用返回后,该set将为空。 * * 底层实际调用HashMap的clear方法清空Entry中所有元素。 */ public void clear() { map.clear(); } /** * 返回此HashSet实例的浅表副本:并没有复制这些元素本身。 * * 底层实际调用HashMap的clone()方法,获取HashMap的浅表副本,并设置到HashSet中。 */ public Object clone() { try { HashSet<E> newSet = (HashSet<E>) super.clone(); newSet.map = (HashMap<E, Object>) map.clone(); return newSet; } catch (CloneNotSupportedException e) { throw new InternalError(); } } } 对于HashSet中保存的对象,请注意正确重写其equals和hashCode方法,以保证放入的对象的唯一性
-------------------------