
目前业界有各种各样的网络输出传输时的序列化和反序列化方案,它们在技术上的实现的初衷和背景有较大的区别,因此在设计的架构也会有很大的区别,最终在落地后的:解析速度、对系统的影响、传输数据的大小、可维护性及可阅读性等方面有着较大的区别,本文分享一些我在一些常见序列化技术的分析和理解: 文章分成3个部分: 1、列举常见的序列化和反序列化方案(ObjectXXStream、XML、JSON) 2、MySQL JDBC结果集的处理方案 3、Google Protocol Buffer处理方案 【一、常见的在API及消息通信调的用中Serialize方案】: 方案1、基于Java原生的ObjectOutputStream.write()和ObjectInputStream.read()来进行对象序列化和反序列化。 方案2、基于JSON进行序列化和反序列化。 方案3、基于XML进行序列化和反序列化。 【方案1浅析,ObjectXXXStream】: 优点: (1)、由Java自带API序列化,简单、方便、无第三方依赖。 (2)、不用担心其中的数据解析会丢失精度、丢失字段、Object的反序列化类型不确定等问题。 缺点: (1)、双方调试麻烦,发送方和接收方最好是同版本的对象描述,否则会有奇怪的问题,调试周期相对长,跨团队合作升级问题很多。 (2)、传递的对象中包含了元数据信息,占用空间较大。 【方案2浅析,JSON序列化】: 优点: (1)、简单、方便,无需关注要序列化的对象格式。 (2)、开源界有较多的组件可以支持,例如FastJSON性能非常好。 (3)、在现在很多RPC的框架中,基本都支持这样的方案。 缺点: (1)、对象属性中如果包含Object类型,在反序列化的时候如果业务也本身也不明确数据类型,处理起来会很麻烦。 (2)、由于文本类型,所以一定会占用较大的数据空间,例如下图。 (3)、比较比较依赖于JSON的解析包的兼容性和性能,在JSON的一些细节处理上(例如一些非标的JSON),各自处理方式可能不一样。 (4)、序列化无论任何数据类型先要转换为String,转成byte[],会增加内存拷贝的次数。 (5)、反序列化的时候,必须将整个JSON反序列化成对象后才能进行读取,大家应该知道,Java对象尤其是层次嵌套较多的对象,占用的内存空间将会远远大于数据本身的空间。 数据放大的极端案例1: 传递数据描述信息为: class PP { long userId = 102333320132133L; int passportNumber = 123456; } 此时传递JSON的格式为: { "userId":102333320132133, "passportNumber":123456 } 我们要传递的数据是1个long、1个int,也就是12个字节的数据,这个JSON的字符串长度将是实际的字节数(不包含回车、空格,这里只是为了可读性,同时注意,这里的long在JSON里面是字符串了),这个字符串有:51个字节,也就是数据放到了4.25倍左右。 数据放大极端案例2: 当你的对象内部有数据是byte[]类型,JSON是文本格式的数据,是无法存储byte[]的,那么要序列化这样的数据,只有一个办法就是把byte转成字符,通常的做法有两种: (1)使用BASE64编码,目前JSON中比较常用的做法。 (2)按照字节进行16进制字符编码,例如字符串:“FF”代表的是0xFF这个字节。 不论上面两种做法的那一种,1个字节都会变成2个字符来传递,也就是byte[]数据会被放大2倍以上。为什么不用ISO-8859-1的字符来编码呢?因为这样编码后,在最终序列化成网络byte[]数据后,对应的byte[]虽然没变大,但是在反序列化成文本的时候,接收方并不知道是ISO-8859-1,还会用例如GBK、UTF-8这样比较常见的字符集解析成String,才能进一步解析JSON对象,这样这个byte[]可能在编码的过程中被改变了,要处理这个问题会非常麻烦。 【方案2浅析,XML序列化】: 优点: (1)、使用简单、方便,无需关注要序列化的对象格式 (2)、可读性较好,XML在业界比较通用,大家也习惯性在配置文件中看到XML的样子 (3)、大量RPC框架都支持,通过XML可以直接形成文档进行传阅 缺点: (1)、在序列化和反序列化的性能上一直不是太好。 (2)、也有与JSON同样的数据类型问题,和数据放大的问题,同时数据放大的问题更为严重,同时内存拷贝次数也和JSON类型,不可避免。 XML数据放大说明: XML的数据放大通常比JSON更为严重,以上面的JSON案例来讲,XML传递这份数据通常会这样传: <Msg> <userId>102333320132133</userId> <passportNumber>123456<passportNumber> <Msg> 这个消息就有80+以上的字节了,如果XML里面再搞一些Property属性,对象再嵌套嵌套,那么这个放大的比例有可能会达到10倍都是有可能的,因此它的放大比JSON更为严重,这也是为什么现在越来越多的API更加喜欢用JSON,而不是XML的原因。 【放大的问题是什么】: (1)、花费更多的时间去拼接字符串和拷贝内存,占用更多的Java内存,产生更多的碎片。 (2)、产生的JSON对象要转为byte[]需要先转成String文本再进行byte[]编码,因为这本身是文本协议,那么自然再多一次内存全量的拷贝。 (3)、传输过程由于数据被放大,占用更大的网络流量。 (4)、由于网络的package变多了,所以TCP的ACK也会变多,自然系统也会更大,同等丢包率的情况下丢包数量会增加,整体传输时间会更长,如果这个数据传送的网络延迟很大且丢包率很高,我们要尽量降低大小;压缩是一条途径,但是压缩会带来巨大的CPU负载提高,在压缩前尽量降低数据的放大是我们所期望的,然后传送数据时根据RT和数据大小再判定是否压缩,有必要的时候,压缩前如果数据过大还可以进行部分采样数据压缩测试压缩率。 (5)、接收方要处理数据也会花费更多的时间来处理。 (6)、由于是文本协议,在处理过程中会增加开销,例如数字转字符串,字符串转数字;byte[]转字符串,字符串转byte[]都会增加额外的内存和计算开销。 不过由于在平时大量的应用程序中,这个开销相对业务逻辑来讲简直微不足道,所以优化方面,这并不是我们关注的重点,但面临一些特定的数据处理较多的场景,即核心业务在数据序列化和反序列化的时候,就要考虑这个问题了,那么下面我继续讨论问题。 此时提出点问题: (1)、网络传递是不是有更好的方案,如果有,为什么现在没有大面积采用? (2)、相对底层的数据通信,例如JDBC是如何做的,如果它像上面3种方案传递结果集,会怎么样? 【二、MySQL JDBC数据传递方案】: 在前文中提到数据在序列化过程被放大数倍的问题,我们是否想看看一些相对底层的通信是否也是如此呢?那么我们以MySQL JDBC为例子来看看它与JDBC之间进行通信是否也是如此。 JDBC驱动程序根据数据库不同有很多实现,每一种数据库实现细节上都有巨大的区别,本文以MySQL JDBC的数据解析为例(MySQL 8.0以前),给大家说明它是如何传递数据的,而传递数据的过程中,相信大家最为关注的就是ResultSet的数据是如何传递的。 抛开结果集中的MetaData等基本信息,单看数据本身: (1)JDBC会读取数据行的时候,首先会从缓冲区读取一个row packege,row package就是从网络package中拿到的,根据协议中传递过来的package的头部判定package大小,然后从网络缓冲中读取对应大小的内容,下图想表达网络传递的package和业务数据中的package之间可能并不是完全对应的。另外,网络中的package如果都到了本地缓冲区,逻辑上讲它们是连续的(图中故意分开是让大家了解到网络中传递是分不同的package传递到本地的),JDBC从本地buffer读取row package这个过程就是内核package到JVM的package拷贝过程,对于我们Java来讲,我们主要关注row package(JDBC中可能存在一些特殊情况读取过来的package并不是行级别的,这种特殊情况请有兴趣的同学自行查阅源码)。 (2)、单行数据除头部外,就是body了,body部分包含各种各样不同的数据类型,此时在body上放数据类型显然是占空间的,所以数据类型是从metadata中提取的,body中数据列的顺序将会和metdata中的列的顺序保持一致。 (3)、MySQL详细解析数据类型: 3.1、如果Metadata对应数据发现是int、longint、tinyint、year、float、double等数据类型会按照定长字节数读取,例如int自然按照4字节读取,long会按照8字节读取。 3.2、如果发现是其它的类型,例如varchar、binary、text等等会按照变长读取。 3.3、变长字符串首先读取1个字节标志位。 3.4、如果这个标志位的值小于等于250,则直接代表后续字节的长度(注意字符串在这里是算转换为字节的长度),这样确保大部分业务中存放的变长字符串,在网络传递过程中只需要1个字节的放大。 3.5、如果这个标志位是:251,代表这个字段为NULL 3.6、如果标志位是:252,代表需要2个字节代表字段的长度,此时加上标志位就是3个字节,在65536以内的长度的数据(64KB),注意,这里会在转成long的时候高位补0,所以2个字节可以填满到65536,只需要放大3个字节来表示这个数据。 3.7、如果标志位是:253,代表需要4个自己大表字段的长度,可以表示4GB(同上高位补0),这样的数据几乎不会出现在数据库里面,即使出现,只会出现5个字节的放大。 3.8、如果标志位是:254,8个字节代表长度,此时没有高位补0,最多可以读取Long.MAX_VALUE的长度,但是这个空间目前不可能有内存放得下,所以无需担心使用问题,此时9个字节的放大,源码如下图: (4)、我们先按照这个理解,MySQL在传递数据的过程中,对数据的放大是很小很小的,是不是真的这样呢?请下面第5点说明。 补充说明: a、在MySQL JDBC中对于ResultSetRow数据的解析(除对MySQL 8.0以上JDBC版本)有2个实现类:BufferRow、ByteArrayRow,这两种方式在读取单行数据在解析这个阶段是一样的逻辑,只不过解析存放数据的方式有所不同,BufferRow一个会解析成数据行的byte[],ByteArrayRow会解析成byte[][]二维数组,第二维就是每1个列的信息,这都是客户端行为,与网络传递数据的格式无关。(两者在不同场景下使用,例如其中一种场景是:ByteArrayRow在游标开启UPDATE模式的时候会启用,但这不是本文的重点,这里提到主要告知大家,无论哪一种方式,读取数据的方式是一致的) b、在MySQL JDBC中的RowData是ResultSet里面数据处理的入口,其实现类有3个:RowStatStatic、RowDataCursor、RowDataDynamic,这虽然有3个实现类,但是同样不会影响数据的格式,它们只是从缓冲区读取数据的方式有所不同:RowStatStatic、RowDataCursor会每次将缓冲区的数据全部读取到JDBC当中形成数组,RowDataCursor在处理上有一个区别在于数据库每次返回的是FetchSize大小的数据内容(实现的细节在上一篇文章中有提到);RowDataDynamic是需要行的时候再从pakcege中去读,package读取完成后就尝试读取下一个package。这些都不会影响数据本身在网络上的传递格式,所以文本提到的解析是目前MySQL JDBC比较通用的解析,与它内部的执行路径无关。 (5)、以BufferRow为例,当你发起getString('xxx')、getDate(int)的时候,首先它需要在内部找到是第几个列(传数字省略该动作),然后其内部会有一个lastRequestedIndex、lastRequestedPos分别记录最后读取的第几个字段和所在字节的位置,如果你传入的index比这个index大,则从当前位置开始,向后扫描,扫描规则和上面的数据库宽度一致,找到对应位置,拷贝出对应的byte[]数组,转换你要的对象类型。 PS:lastRequestedIndex、lastRequestedPos这种其实就是JDBC认为你绝大部分情况是从前向后读取的,因此这样读取对JDBC程序也是最友好的方案,否则指针向前移动,需要从0开始,理由很简单(数据的长度不是在尾部,而是在头部),因此指针来回来回移动的时候,这样会产生很多开销,同时会产生更多的内存拷贝出来的碎片。ByteArrayRow虽然可以解决这个问题,但是其本身会占用相对较大的空间另外,其内部的二维数组返回的byte[]字节是可以被外部所修改的(因为没有拷贝)。 另外,按照这种读取数据的方式,如果单行数据过大(例如有大字段100MB+),读取到Java内存里面来,即使使用CursorFetch和Stream读取,读取几十条数据,就能把JVM内存干挂掉。到目前为止,我还没看到MySQL里面可以“设置限制单行数据长度”的参数,后续估计官方支持这类特殊需求的可能性很小,大多也只能自己改源码来实现。 【回到话题本身:MySQL和JDBC之间的通信似乎放大很小?】 其实不然,MySQL传递数据给JDBC默认是走文本协议的,而不是Binary协议,虽然说它的byte[]数组不会像JSON那样放大,并不算真正意义上的文本协议,但是它很多种数据类型默认情况下,都是文本传输,例如一个上面提到的账号:102333320132133在数据库中是8个自己,但是网络传递的时候如果有文本格式传递将会是:15个字节,如果是DateTime数据在数据库中可以用8个字节存放,但是网络传递如果按照YYYY-MM-DD HH:MI:SS传递,可以达到19个字节,而当他们用String在网络传递的时候,按照我们前面提到的,MySQL会将其当成变长字符,因此会在数据头部加上最少1个自己的标志位。另外,这里增加不仅仅是几个字节,而是你要取到真正的数据,接收方还需要进一步计算处理才能得到,例如102333320132133用文本传送后,接收方是需要将这个字符串转换为long类型才能可以得到long的,大家试想一下你处理500万数据,每一行数据有20个列,有大量的类似的处理不是开销增加了特别多呢? JDBC和MySQL之间可以通过binary协议来进行通信的,也就是按照实际数字占用的空间大小来进行通信,但是比较坑的时,MySQL目前开启Binary协议的方案是:“开启服务端prepareStatemet”,这个一旦开启,会有一大堆的坑出来,尤其是在互联网的编程中,我会在后续的文章中逐步阐述。 抛开“开启binary协议的坑”,我们认为MySQL JDBC在通信的过程中对数据的编码还是很不错的,非常紧凑的编码(当然,如果你开启了SSL访问,那么数据又会被放大,而且加密后的数据基本很难压缩)。 对比传统的数据序列化优劣势汇总: 优势: (1)、数据全部按照byte[]编码后,由于紧凑编码,所以对数据本身的放大很小。 (2)、由于编码和解码都没有解析的过程,都是向ByteBuffer的尾部顺序地写,也就是说不用找位置,读取的时候根据设计也可以减少找位置,即使找位置也是移动偏移量,非常高效。 (3)、如果传递多行数据,反序列化的过程不用像XML或JSON那样一次要将整个传递过来的数据全部解析后再处理,试想一下,如果5000行、20列的结果集,会产生多少Java对象,每一个Java对象对数据本身的放大又是多少,采用字节传递后可以按需转变为Java对象,使用完的Java对象可以释放,这样就不用同时占用那样大的JVM内存,而byte[]数组也只是数据本身的大小,也可以按需释放。 (4)、相对前面提到的3种方式,例如JSON,它不需要在序列化和反序列化的时候要经历一次String的转换,这样会减少一次内存拷贝。 (5)、自己写代码用类似的通信方案,可以在网络优化上做到极致。 劣势: (1)、编码是MySQL和MySQL JDBC之间自定义的,别人没法用(我们可以参考别人的思路) (2)、byte编码和解码过程程序员自己写,对程序员水平和严谨性要求都很高,前期需要大量的测试,后期在网络问题上考虑稍有偏差就可能出现不可预期的Bug。(所以在公司内部需要把这些内容进行封装,大部分程序员无需关注这个内容)。 (3)、从内存拷贝上来讲,从rowBuffer到应用中的数据,这一层内存拷贝是无法避免的,如果你写自定义程序,在必要的条件下,这个地方可以进一步减少内存拷贝,但无法杜绝;同上文中提到,这点开销,对于整个应用程序的业务处理来讲,简直微不足道。 为什么传统通信协议不选择这样做: (1)、参考劣势中的3点。 (2)、传统API通信,我们更讲究快速、通用,也就是会经常和不同团队乃至不同公司调试代码,要设计binary协议,开发成本和调试成本非常高。 (3)、可读性,对于业务代码来讲,byte[]的可读性较差,尤其是对象嵌套的时候,byte[]表达的方式是很复杂的。 MySQL JDBC如果用binary协议后,数据的紧凑性是不是达到极致了呢? 按照一般的理解,就是达到极致了,所有数据都不会进一步放大,int就只用4个字节传递,long就只用8个字节传递,那么还能继续变小,难道压缩来做? 非也、非也,在二进制的世界里,如果你探究细节,还有更多比较神奇的东西,下面我们来探讨一下: 例如: long id = 1L; 此时网络传递的时候会使用8个字节来方,大家可以看下8个字节的排布: 我们先不考虑按照bit有31个bit是0,先按照字节来看有7个0,代表字节没有数据,只有1个字节是有值的,大家可以去看一下自己的数据库中大量的自动增长列,在id小于4194303之前,前面5个字节是浪费掉的,在增长到4个字节(2的32次方-1)之前前面4个自己都是0,浪费掉的。另外,即使8个字节中的第一个字节开始使用,也会有大量的数据,中间字节是为:0的概率极高,就像十进制中进入1亿,那么1亿下面最多会有8个0,越高位的0约难补充上去。 如果真的想去尝试,可以用这个办法:用1个字节来做标志,但会占用一定的计算开销,所以是否为了这个空间去做这个事情,由你决定,本文仅仅是技术性探讨: 方法1:表达目前有几个低位被使用的字节数,由于long只有8个字节,所以用3个bit就够了,另外5个bit是浪费掉的,也无所谓了,反序列化的时候按照高位数量补充0x00即可。 方法2:相对方法1,更彻底,但处理起来更复杂,用1这个字节的8个bit的0、1分别代表long的8个字节是被使用,序列化和反序列化过程根据标志位和数据本身进行字节补0x00操作,补充完整8个字节就是long的值了,最坏情况是9个字节代表long,最佳情况0是1个字节,字节中只占用了2个字节的时候,即使数据变得相当大,也会有大量的数据的字节存在空位的情况,在这些情况下,就通常可以用少于8个字节的情况来表达,要用满7个字节才能够与原数字long的占用空间一样,此时的数据已经是比2的48次方-1更大的数据了。 【三、Google Protocol Buffer技术方案】: 这个对于很多人来讲未必用过,也不知道它是用来干什么的,不过我不得不说,它是目前数据序列化和反序列化的一个神器,这个东西是在谷歌内部为了约定好自己内部的数据通信设计出来的,大家都知道谷歌的全球网络非常牛逼,那么自然在数据传输方面做得那是相当极致,在这里我会讲解下它的原理,就本身其使用请大家查阅其它人的博客,本文篇幅所限没法step by step进行讲解。 看到这个名字,应该知道是协议Buffer,或者是协议编码,其目的和上文中提到的用JSON、XML用来进行RPC调用类似,就是系统之间传递消息或调用API。但是谷歌一方面为了达到类似于XML、JSON的可读性和跨语言的通用性,另一方面又希望达到较高的序列化和反序列化性能,数据放大能够进行控制,所以它又希望有一种比底层编码更容易使用,而又可以使用底层编码的方式,又具备文档的可读性能力。 它首先需要定义一个格式文件,如下: syntax = "proto2"; package com.xxx.proto.buffer.test; message TestData2 { optional int32 id = 2; optional int64 longId = 1; optional bool boolValue = 3; optional string name = 4; optional bytes bytesValue = 5; optional int32 id2 = 6;} 这个文件不是Java文件,也不是C文件,和语言无关,通常把它的后缀命名为proto(文件中1、2、3数字代表序列化的顺序,反序列化也会按照这个顺序来做),然后本地安装了protobuf后(不同OS安装方式不同,在官方有下载和说明),会产生一个protoc运行文件,将其加入环境变量后,运行命令指定一个目标目录: protoc --java_out=~/temp/ TestData2.proto 此时会在指定的目录下,产生package所描述的目录,在其目录内部有1个Java源文件(其它语言的会产生其它语言),这部分代码是谷歌帮你生成的,你自己写的话太费劲,所以谷歌就帮你干了;本地的Java project里面要引入protobuf包,maven引用(版本自行选择): <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.6.1</version> </dependency> 此时生成的代码会调用这个谷歌包里面提供的方法库来做序列化动作,我们的代码只需要调用生成的这个类里面的API就可以做序列化和反序列化操作了,将这些生成的文件放在一个模块里面发布到maven仓库别人就可以引用了,关于测试代码本身,大家可以参考目前有很多博客有提供测试代码,还是很好用的。 谷歌编码比较神奇的是,你可以按照对象的方式定义传输数据的格式,可读性极高,甚至于相对XML和JSON更适合程序员阅读,也可以作为交流文档,不同语言都通用,定义的对象还是可以嵌套的,但是它序列化出来的字节比原始数据只大一点点,这尼玛太厉害了吧。 经过测试不同的数据类型,故意制造数据嵌套的层数,进行二进制数组多层嵌套,发现其数据放大的比例非常非常小,几乎可以等价于二进制传输,于是我把序列化后的数据其二进制进行了输出,发现其编码方式非常接近于上面的JDBC,虽然有一些细节上的区别,但是非常接近,除此之外,它在序列化的时候有几大特征: (1)、如果字段为空,它不会产生任何字节,如果整合对象的属性都为null,产生的字节将是0 (2)、对int32、int64这些数据采用了变长编码,其思路和我们上面描述有一些共通之处,就是一个int64值在比较小的时候用比较少的字节就可以表达了,其内部有一套字节的移位和异或算法来处理这个事情。 (3)、它对字符串、byte[]没有做任何转换,直接放入字节数组,和二进制编码是差不多的道理。 (4)、由于字段为空它都可以不做任何字节,它的做法是有数据的地方会有一个位置编码信息,大家可以尝试通过调整数字顺序看看生成出来的byte是否会发生改变;那么此时它就有了很强兼容性,也就是普通的加字段是没问题的,这个对于普通的二进制编码来讲很难做到。 (5)、序列化过程没有产生metadata信息,也就是它不会把对象的结构写在字节里面,而是反序列化的接收方有同一个对象,就可以反解析出来了。 这与我自己写编码有何区别? (1)、自己写编码有很多不确定性,写不好的话,数据可能放得更大,也容易出错。 (2)、google的工程师把内部规范后,谷歌开源的产品也大量使用这样的通信协议,越来越多的业界中间件产品开始使用该方案,就连MySQL数据库最新版本在数据传输方面也会开始兼容protobuf。 (3)、谷歌相当于开始在定义一个业界的新的数据传输方案,即有性能又降低代码研发的难度,也有跨语言访问的能力,所以才会有越来越多的人喜欢使用这个东西。 那么它有什么缺点呢?还真不多,它基本兼顾了很多序列化和反序列化中你需要考虑的所有的事情,达到了一种非常良好的平衡,但是硬要挑缺陷,我们就得找场景才行: (1)、protobuf需要双方明确数据类型,且定义的文件中每一个对象要明确数据类型,对于Object类型的表达没有方案,你自己必须提前预知这个Object到底是什么类型。 (2)、使用repeated可以表达数组,但是只能表达相同类型的数据,例如上面提到的JDBC一行数据的多个列数据类型不同的时候,要用这个表达,会比较麻烦;另外,默认情况下数组只能表达1维数组,要表达二维数组,需要使用对象嵌套来间接完成。 (3)、它提供的数据类型都是基本数据类型,如果不是普通类型,要自己想办法转换为普通类型进行传输,例如从MongoDB查处一个Docment对象,这个对象序列化是需要自己先通过别的方式转换为byte[]或String放进去的,而相对XML、JSON普通是提供了递归的功能,但是如果protobuf要提供这个功能,必然会面临数据放大的问题,通用和性能永远是矛盾的。 (4)、相对于自定义byte的话,序列化和反序列化是一次性完成,不能逐步完成,这样如果传递数组嵌套,在反序列化的时候会产生大量的Java对象,另外自定义byte的话可以进一步减少内存拷贝,不过谷歌这个相对文本协议来讲内存拷贝已经少很多了。 补充说明: 在第2点中提到repeated表达的数组,每一个元素必须是同类型的,无法直接表达不同类型的元素,因为它没有像Java那样Object[]这样的数组,这样它即使通过本地判定Object的类型传递了,反序列化会很麻烦,因为接收方也不知道数据是什么类型,而protobuf网络传递数据是没有metadata传递的,那么判定唯一的地方就是在客户端自己根据业务需要进行传递。 因此,如果真的有必要的话,可以用List<byte[]>表达一行数据的方案,也就是里面的每1个byte[]元素通过其它地方获得metadata,例如数据库的Metadata得到,然后再自己去转换,但是传递过程全是byte[];就JDBC来讲,我个人更加推荐于一行数据使用一个byte[]来传递,而不是每一项数据用一个byte[],因为在序列化和反序列化过程中,每一个数组元素都会在protobuf会被包装成对象,此时产生的Java对象数量是列的倍数,例如有40个列,会产生40倍的Java对象,很夸张吧。 总之,每一种序列化和反序列化方案目前都有应用场景,它们在设计之初决定了架构,也将决定了最终的性能、稳定性、系统开销、网络传输大小等等。
使用MySQL JDBC读取过较大数据量的人应该清楚(例如超过1GB),在读取的时候内存很可能会Java堆内存溢出,而我们的解决方案是statement.setFetchSize(Integer.MIN_VALUE)并确保游标是只读向前滚动的即可(为游标的默认值),也可以强制类型转换为com.mysql.jdbc.StatementImpl,然后调用其内部方法:enableStreamingResults()这样读取数据内存就不会挂掉了,这两者达到的效果是一致的。当然也可以使用useCursorFetch,但是这种方式测试结果性能要比StreamResult慢很多,为什么?在本文会阐述其大致的原理。 我在前面的部分文章和书籍中都有介绍过其MySQL JDBC在这一块内部处理的代码分成三个不同的类来完成的,不过我一直没有去深究过数据库和JDBC之间到底是如何通信的过程。有一段时间我一直认为这都属于服务端行为或者是客户端与服务端配合的行为,然后并不其然,今天我们来讲一下这个行为是怎么回事。 【先回顾一下简单的通信】: JDBC与数据库之间的通信是通过Socket完成的,因此我们可以把数据库当成一个SocketServer的提供方,因此当SocketServer返回数据的时候(类似于SQL结果集的返回)其流程是:服务端程序数据(数据库) -> 内核Socket Buffer -> 网络 -> 客户端Socket Buffer -> 客户端程序(JDBC所在的JVM内存) 到目前为止,IT行业中大家所看到的JDBC无论是:MySQL JDBC、SQL Server JDBC、PG JDBC、Oracle JDBC。甚至于是NoSQL的Client:Redis Client、MongoDB Client、Memcached,数据的返回基本也是这样一个逻辑。 【方式1:直接使用MySQL JDBC默认参数读取数据,为什么会挂?】 (1)MySQL Server方在发起的SQL结果集会全部通过OutputStream向外输出数据,也就是向本地的Kennel对应的socket buffer中写入数据,这是一次内存拷贝(内存拷贝这个不是本文的重点)。 (2)此时Kennel的Buffer有数据的时候就会把数据通过TCP链路(JDBC主动发起的Socket链路),回传数据,此时数据会回传到JDBC所在机器上,会先进入Kennel区域,同样进入到一个Buffer区。 (3)JDBC在发起SQL操作后,Java代码是在inputStream.read()操作上阻塞,当缓冲区有数据的时候,就会被唤醒,然后将缓冲区的数据读取到Java内存中,这是JDBC端的一次内存拷贝。 (4)接下来MySQL JDBC会不断读取缓冲区数据到Java内存中,MySQL Server会不断发送数据。注意在数据没有完全组装完之前,客户端发起的SQL操作不会响应,也就是给你的感觉MySQL服务端还没响应,其实数据已经到本地,JDBC还没对调用execute方法的地方返回结果集的第一条数据,而是不断从缓冲器读取数据。 (5)关键是这个傻帽就像一把这个数据读取完,根本不管家里放不放的下,就会将整个表的内容读取到Java内存中,先是FULL GC,接下来就是内存溢出。 【方式2:JDBC参数上设置useCursorFetch=true可以解决问题】 这个方案配合FetchSize设置,确实可以解决问题,这个方案其实就是告诉MySQL服务端我要多少数据,每次要多少数据,通信过程有点像这样: 这样做就像我们生活中的那样,我需要什么就去超市买什么,需要多少就去买多少。不过这种交互不像现在网购,坐在家里就可以把东西送到家里来,它一定要走路(网络链路),也就是需要网络的时间开销,假如数据有1亿数据,将FetchSize设置成1000的话,会进行10万次来回通信;如果网络延迟同机房0.02ms,那么10万次通信会增加2秒的时间,不算大。那么如果跨机房2ms的延迟时间会多出来200秒(也就是3分20秒),如果国内跨城市10~40ms延迟,那么时间将会1000~4000秒,如果是跨国200~300ms呢?时间会多出十多个小时出来。 在这里的计算中,我们还没有包含系统调用次数增加了很多,线程等待和唤醒的上下文次数变多,网络包重传的情况对整体性能的影响,因此这种方案看似合理,但是性能确不怎么样。 另外,由于MySQL方不知道客户端什么时候将数据消费完,而自身的对应表可能会有DML写入操作,此时MySQL需要建立一个临时表空间来存放需要拿走的数据。因此对于当你启用useCursorFetch读取大表的时候会看到MySQL上的几个现象: (1)IOPS飙升,因为存在大量的IO读取,如果是普通硬盘,此时可能会引起业务写入的抖动 (2)磁盘空间飙升,这块临时空间可能比原表更大,如果这个表在整个库内部占用相当大的比重有可能会导致数据库磁盘写满,空间会在结果集读取完成后或者客户端发起Result.close()时由MySQL去回收。 (3)CPU和内存会有一定比例的上升,根据CPU的能力决定。 (4)客户端JDBC发起SQL后,长时间等待SQL响应数据,这段时间就是服务端在准备数据,这个等待与原始的JDBC不设置任何参数的方式也表现出等待,在内部原理上是不一样的,前者是一直在读取网络缓冲区的数据,没有响应给业务,现在是MySQL数据库在准备临时数据空间,没有响应给JDBC。 【userCursor原理说明】: (1)在设置JDBC参数useCursorFetch=true后,通过Driver创建Connection的时候会自动将:detectServerPreparedStmts设置为true,这个对应JDBC参数是:useServerPrepStmts=true,也就是当设置useCursorFetch=true时useServerPrepStmt会被自动设置为true,源码片段(ConnectionPropertiesImpl类的postInitialization()中,也就是连接初始化的时候会用的): 内部多提供了另一个方法名,下面会提到: (2)当执行SQL时,会调用到使用prepareStatment方法去执行(即使你自己用Statement内部也会转换成PrepareStatemet,因为它要用服务端预编译),代码如下: 跟下代码,这里的userServerFetch()就是useCursorFetch参数的判定以及游标类型和版本的判定,而游标类型判定的就是为默认值。 (3)步骤1已提到detectServerPreparedStmts被设置为true,在prepareStatement的时候会选择其ServerPreparedStatement作为实现类:具体请参考ConnectionImpl.prepareStatement(String , int , int)的代码,代码太长也不难,就不贴了。 (4)我要说的是ServerPreparedStatement在创建的时候,会在SQL发送前加一个指令在前面,让服务器端预编译,这个指令就是1个int值:22(MysqlDefs.COM_PREPARE),如下: (5)这里仅仅是告知服务端预编译SQL,还没有指定游标也在服务器端,在真正发生execute、executeQuery,会调用到ServerPreparedStatement的serverExecute方法中。 (6)在步骤5描述的方法ServerPreparedStatement.serverExecute()方法中,会再一次判定useCursorFetch的判定,如果useCursorFetch成立,则在发送给服务端的package中,开启游标的指令:1(MysqlDefs.OPEN_CURSOR_FLAG),如下: 当开启游标的时候,服务端返回数据的时候,就会按照fetchSize的大小返回数据了,而客户端接收数据的时候每次都会把换缓冲区数据全部读取干净(可复用不开启游标方式的代码)。 PS:关于PreparedStatement在MySQL JDBC当中是有潜在问题的,无论是否开启服务端Prapare都有一些坑存在,这些我会在后续的一些文章当中逐步讲到。 【方式3:Stream读取数据】 我们知道第1种方式会导致Java挂掉,第2种方式效率低而且对MySQL数据库的影响较大,客户端响应也较慢,仅仅能够解决问题而已,那么现在来看下Stream读取方式。 前面提到当你使用statement.setFetchSize(Integer.MIN_VALUE)或com.mysql.jdbc.StatementImpl.enableStreamingResults()就可以开启Stream读取结果集的方式,在发起execute之前FetchSize不能再手工设置,且确保游标是FORWARD_ONLY的。 这种方式很神奇,似乎内存也不挂了,响应也变快了,对MySQL的影响也变小了,至少IOPS不会那么大了,磁盘占用也没有了。以前仅仅看到JDBC中走了单独的代码,认为这是MySQL和JDBC之间的另一种通信协议,殊不知,它竟然是“客户端行为”,没错,你没看错,它就是客户端行为。 它在发起enableStreamingResults()的时候,几乎不会做任何与服务端的交互工作,也就是服务端会按照方式1回传数据,那么服务端使劲向缓冲区怼数据,客户端是如何扛得住压力的呢? 在JDBC当中,当你开启Stream结果集处理的时候,它并不是一把将所有数据读取到Java内存中的,也就是图1中并不是一次性将数据读取到Java缓冲区的,而是每次读取一个package(这个package可以理解成Java中的一个byte[]数组),一次最多读取这么多,然后会看是否继续向下读取保证数据的完整性。业务代码是按照字节解析成行也业务方使用的。 服务端刚开始使劲向缓冲区怼数据,这些数据也会怼满客户端的内核缓冲区,当两边的缓冲区都被怼满的时候,服务端的1个Buffer尝试通过TCP传递数据给接收方时,此时由于消费方的缓冲区也是满的,因此发送方的线程会阻塞住,等待对方消费,对方消费一部分,就可以推送一部分数据过去。连起来看就是JDBC的Stream数据未来得及消费之前,缓冲区数据如果是满的,那么MySQL发送数据的线程就阻塞住了,这样确保了一个平衡(关于这一点,大家可以使用Java的Socket来尝试下是否是这样的)。 对于JDBC客户端,数据获取的时候每次都在本地的内核缓冲区当中,就在小区的快递包裹箱拿回家一个距离,那么自然比起每次去超市的RT要小得多了,而且这个过程是准备好的数据,所以没有IO阻塞的过程(除非MySQL服务端传递的数据还不如消费端处理数据来得快,那一般也只有消费端不做任何业务,拿到数据直接放弃的测试代码,才会发生这样的事情),这个时候不论:跨机房、跨地区、跨国家,只要服务端开始响应就会源源不断地传递数据过来,而这个动作即使是第1种方式也是必然需要经历的过程。 Stream读取方式是不是就没有问题了呢?肯定是有的,而且还不止一个两个坑,这篇文章我没法一一说清楚,也和每一个人所遇到的情况有所不同,也会遇到一些比较偏的问题和坑,在本文中主要针对对业务的影响程度来看: 【优缺点对比】: 读取方式 优点 缺点 默认参数读取 1、代码简单、JDBC逻辑简单 2、OLTP单行操作速度最佳 3、对MySQL的业务影响小 1、数据量大的时候内存会溢出 2、需要Java程序将所有的数据读取到JVM中才响应程序 3、一旦服务端开始返回数据(不是JDBC响应,是MySQL的服务端准备一条数据开始)无法cancel,且在数据准备好以前,cancel会被阻塞 useCursorFetch 1、相对方式1不会导致内存溢出 2、相对方式3对数据库影响时间更短 1、会占用数据库磁盘空间 2、占用更多的IOPS 3、需要MySQL Server将所有数据准备好,才会响应程序 4、网络RT会根据数据量产生数百倍乃至数千倍的放大。 5、数据准备阶段发起cancel操作会阻塞(可在MySQL服务端数据准备前cancel掉) 6、数据传输阶段发起cancel操作无效 stream读取 1、相对方式1不会内存溢出 2、相对方式3整体速度更快 3、在几种方式中,读取大数据量,响应第一条数据的时间是最短的 4、跨地域传送大量数据,不会放大RT 1、相对方式2,对数据库影响时间会更长一些 2、相对方式1,会多一些系统调用次数。 3、cancel无效,cancel不阻塞 【对业务的影响对比】:在MySQL 5.7下分别测试MyISAM、InnoDB两种存储引擎: MyISAM InnoDB useCursor 数据准备阶段: 单条操作:可读、可DDL、写操作阻塞 交叉操作:发起写操作阻塞,接着读操作会阻塞 交叉操作:DDL后,写操作阻塞,读操作不阻塞,但此时写操作阻塞阶段不同,不会阻塞读操作 PS:DDL需要等待数据准备阶段完成后才能执行下去,但在数据准备阶段DDL已在运行中。 读取数据过程中: 单条操作:可读、可做DDL、可写操作 交叉操作:写入后,可读、可DDL 交叉操作:DDL后,写操作阻塞,读操作不阻塞 数据准备阶段: 单条操作:可读、可写、可DDL 交叉操作:写操作,再读取和DDL不会阻塞 交叉操作:先DDL,读、写均会被阻塞 PS:DDL需要等待数据准备阶段完成后才能执行下去,但在数据准备阶段DDL已在运行中。 读取数据过程中: 单条操作:可读、可写、可DDL 交叉操作:写操作,再读取和DDL不会阻塞 交叉操作:先DDL,读、写均不会被阻塞 stream读取 整个Stream读取过程: 1、单条操作:可读、可做DDL、写操作阻塞 2、交叉操作:发起写操作阻塞,接着读操作会阻塞 3、交叉操作:先做DDL,读操作不阻塞,写操作阻塞,但此时写操作阻塞阶段不同,不会阻塞读操作 4、交叉操作:步骤3阻塞了写操作,此时将DDL Kill掉,写操作会进入步骤2的阻塞状态,阻塞掉所有的读取操作。 整个Stream读取过程: 单条操作:可读、可写、可DDL 交叉操作:写操作后,不阻塞读取和DDL 交叉操作:DDL后,读操作阻塞、写操作阻塞 PS:DDL本身可以在这个过程中运行但在Stream读取完成前它无法结束,要等待数据读取完成才结束(如果DDL本身比Stream要快),但DDL已到最后阶段,也就是说Stream读取的时候,DDL是在运行的,只是在最后阶段需获取meta锁时阻塞住了。 【理论上可以更进一步,只要你愿意】 理论上这种方式是比较好的了,但是就完美主义来讲,我们可以继续探讨一下,对于懒人来讲,我们连到小区楼下快递包裹箱去拿一下的动力也是没有的,我们心里想的就是要是谁给我拿到家里来送到我嘴巴里,连嘴巴都给我掰开多好。 在技术上理论上确实可以做到这样,因为JDBC从内核拷贝内存到Java当中是需要花时间的,要是有另一个人把这个事情做了,我在家里干别的事情的时候它就给我送到家里来了,我要用的时候就直接从家里来,这个时间岂不是省掉了。每错,对于你来讲确实省掉了,不过问题就是谁来送? 在程序中一定需要加一个线程来干这个事情,把内核的数据拷贝到应用内存,甚至于解析成行数据,应用程序直接使用,但这一定完美吗?其实这个中间就有个协调问题了,例如家里要炒菜,缺一包调料,原本可以自己到楼下买,但是非要让别人送家里,这个时候其它的菜都下锅了,就剩一包调料,那么你没别的办法,只能等这包调料送到家里来以后才能进行炒菜的下一道工序。所以,在理想情况下,它可以节约很多次内存拷贝时间,会增加一些协调锁的开销。 那么可以不可以直接从内核缓冲区读取数据呢? 理论上也是可以的,在解释这个问题之前,我们先了解下除了这一次内存拷贝还有那些: JDBC按照二进制将内核缓冲区的数据读取后,也会进一步解析成具体的结构化数据,由于此时要给业务方返回ResultSet的具体行的结构化数据,也就是生成RowData的数据一定会有一次拷贝,而且JDBC返回某些对象类型数据的时候(例如byte []数组),在某些场景的实现,它不希望你通过结果集修改返回结果中的byte []的内容(byte[1] = 0xFF)去修改ResultSet本身内容,可能还会再做1次内存拷贝,业务代码使用过程中还会存在拼字符串,网络输出等,又是一堆的内存拷贝,这些在业务层面是无法避免的,相对这点点拷贝来讲,简直微不足道,所以我们也没去干这事情,以为从整体上看几乎微不足道,除非你的程序瓶颈在这里。 因此从整体上看内存拷贝是无法避免的,多的这一次无非是系统级的调用,开销会更大一点,从技术上来讲,我们是可以做到直接从内核态直接读取数据的;但这个时候就需要按照字节将Buffer从的数据拿走才能让远程更多的数据传递过来,没有第三个位置存放Buffer了,否则又回到了内核到应用的内存拷贝上来了。 相对来讲,服务端倒是可以优化直接将数据通过直接IO的方式传递(不过这种方式数据的协议就和数据的存储格式一致了,显然只是理论上的), 要真正做到自定义的协议,又要通过内核态数据直接发送,需要通过修改OS级别的文件系统协议,来达到转换的目的。
遇到一个MySQL JDBC执行execute方法时指定queryTimeout的坑,比较恶心,算是它的BUG,也可以不算,^_^,为啥这么说?看一下下面的解释: 现象: 用同一个Connection执行大批量SQL的时候,导致了OOM现象。 细节现象描述: 1、SQL是从某个存储设备上拿到的,不会直接占用大量的内存,每次只会取最多1千条数据过去,也会判定容量不超过多少M。 2、每一批SQL执行会单独创建Statement对象,执行一批SQL后,会将这个Statement关闭掉。 3、SQL语句中只有insert,没有其它的语句。 疑问: 这尼玛是什么蛋疼的问题?所有代码也review并debug过,参数是自己理想状态,看了下MySQLJDBC中的StatementImpl.close()的代码会清理掉相应的结果集以及数据,不会留下啥垃圾。 dump内存: dump内存后发现几十万个CancelTask对象,它是StatementImpl的内部类,最终会放到ConnectionImpl中的一个静态Timer类型的对象中。 下面来分析这几个问题:这个对象是干什么的?在什么时候创建的?何时回销毁?坑在那里? 这个对象是干什么的?在什么时候创建的? 这个对象是用于将执行中的SQL取消掉的任务对象,当SQL执行前,通过Statement.setQueryTimeout(int)时(参数单位为秒),这个参数的值只要不是0,它就会在JDBC内部与MySQL通信前会创建一个任务,这个任务会放入到一个Timer的任务队列中(请参看博客中专门介绍Timer与TimerTask的文章)。 它何时回被销毁呢? 1、如果SQL语句在CancelTask还未被Timer调度前响应,则会在JDBC代码中执行调用CancelTask.cancel()方法。 2、如果SQL语句一直未响应,CancelTask在达到设置的设置的timeout值时会一般会被Timer调度,如果已经是cancel状态不执行取消SQL执行操作,直接从队列中移除,如果CancelTask还没有被cancel,则会向MySQL发送相应的取消命令,让其回收资源。Timer在调度这个任务的时候CancelTask内部会创建新的线程来处理,因此Timer很快就会认为任务执行完了,也就是和取消SQL本身的时间无关,Timer也会将这个任务对象从队列中移除,因为这个任务并不是循环执行的。 似乎销毁也是很完善的,那么坑到底在那里呢? 1、根据业务需要,这个Statement.setQueryTimeout(int)这个值设置得非常大。 2、当大批量的SQL同时执行时,每一个SQL都会创建一个CancelTask对象,虽然很快执行完,且会调用CancelTask.cancel()方法,但是CancelTask方法的源代码仅仅是将自己的状态修改为:CANCELLED,而并不会直接从队列中移除这个对象,只有等到超过queryTimeout的值时被Timer调度,才会从队列中移除。 注意:在MySQL JDBC 5.1.13版本有一个purge操作,但是这个操作对execute方法存在BUG,因为它在这个方法的try里面执行了这部分代码: if (timeoutTask != null) { if (timeoutTask.caughtWhileCancelling != null) { throw timeoutTask.caughtWhileCancelling; } timeoutTask.cancel(); timeoutTask = null;} 这里将timeoutTask设置为null了,但没有purge,导致了一个问题就是在finally里面不会进入if语句,从而不会执行purge操作,也会导致问题,这个问题一直延续到现在的最新版本5.1.34。不过executeQuery、executeUpdate方法是在5.1.13版本后修复了这个问题。 3、因此大批量的SQL同时运行时,并很快结束时,JDBC中存放了大量的CancelTask的生命周期如果自己不结束,这个对象是和Timer相关,那么Timer是什么级别的呢? 4、经过源码跟踪,虽然Timer定义在Connection中,但是static修饰的,也就是是全局级别的,换句话说:即使将这个Connection.close(),也不会释放掉这些CancelTask对象所占用的空间。(MySQL JDBC 于5.1.11版本修改为非静态成员变量,但是这个版本还没有做purge,因此还没有真正解决问题,关于5.1.13增加purge请参看上面的说明,而另外需要注意的是修改为非静态成员后,每一个连接都会有一个单独的线程Timer在后台运行,因此在设计上可能需要注意些什么)。 5、通过上面dump内存图看到,每一个CancelTask对象会占用7K左右的空间,29W个对象就会占用将近2G空间。 结论:只要在timeout值没有达到之前,超过一定数量的SQL被执行(不分单线程还是多线程),内存肯定就蹦了。 临时性的解决方法: 对某些大批量的SQL执行execute方法入口不设置timeout,或设置时间非常短的timeout,这个要根据实际场景来讲。 但这样可能会带来更多的问题,所以会陷入一个圈子中。终极方案有点蛋疼,因为这个取舍问题有点麻烦,哥有点想把源代码的这一块改一改,给官网提交了不少BUG,认可了,但没见他们改过。本文只是先让大伙知道有这么一个坑存在。 下面简单贴几小段MySQL JDBC的源码,有兴趣可以看下: 《代码段1:设置QueryTimeout》 public void setQueryTimeout(int seconds) throws SQLException { if (seconds < 0) { throw SQLError.createSQLException(Messages .getString("Statement.21"), //$NON-NLS-1$ SQLError.SQL_STATE_ILLEGAL_ARGUMENT); //$NON-NLS-1$ } this.timeoutInMillis = seconds * 1000; } 《代码段2:如果这个timeout不是0,就会创建一个新的Task》 if (locallyScopedConn.getEnableQueryTimeouts() && this.timeoutInMillis != 0 && locallyScopedConn.versionMeetsMinimum(5, 0, 0)) { timeoutTask = new CancelTask(this); ConnectionImpl.getCancelTimer().schedule(timeoutTask,this.timeoutInMillis); } 《代码段3:SQL执行完会调用Cancel.cancel()方法》if (timeoutTask != null) { timeoutTask.cancel(); } 《代码段4:java.util.Timer的添加任务到队列中的关键部分回顾》 void add(TimerTask task) { // Grow backing store if necessary if (size + 1 == queue.length) queue = Arrays.copyOf(queue, 2*queue.length); queue[++size] = task; fixUp(size); } 《代码段5:TimerTask是CancelTask的父类,其的cancel方法主要就是为了设置状态》 public boolean cancel() { synchronized(lock) { boolean result = (state == SCHEDULED); state = CANCELLED; return result; } } 关于Timer调度部分的源码我就不贴了,以前在其它文章中有描述。 总结下: 1、5.1.11版本后将Timer改为非静态成员,和Conenction绑定,但没有做purge操作,因此没有真正解决问题。另外,每一个连接都会在后台多一个线程出来。 2、5.1.13版本在finally以及相应执行完成部分添加了purge回收资源操作,但是对于execute方法是存在BUG的,这个BUG延续到现在最新版本5.1.34对于executeUpdate、executeQuery方法是可以正常完成了。 3、虽然已经可以顺利完成purge,但要考虑一下这个顺利完成的代价是不断地通过synchonized加锁对队列进行处理,这样也会带来一定得系统开销,所以呢根据实际场景如果能够不使用的情况下可尽量避免使用。
大家好,去年说要写本Java书,近期就快出版了。目前已经开始打印样书了,最快于本月中旬左右就可以在互动网www.china-pub.com上看到消息,其它各个网站何时会发售要看具体进货情况。 去年我预期是半年写好这本书,6个月左右确实将手稿写好,但由于是第一次写书,所以没有意料到许多review的成本也是很高的,另外需要在每次review过后与出版社沟通,一直拖到现在才准备出版(而且还只出了上册),很多小伙伴已经等得花儿都谢了,哈哈!我也有类似的感觉,去年写的有些内容现在已经快过时了,呵呵,不过还好重点不是技术点本身。 经过几次review下来后,书中改掉许多问题,也删掉了一些内容,肯定比手稿看起来要顺畅很多。不过肯定还有一些没有注意到的地方,希望大家能够谅解。 这本书在写的过程中越写越多,去年年底发现写的内容已经远远超过了计划的字数和篇幅。因此与出版社决定分为“上下册”出版,本次出版的就是上册,上册是完全讲解基础的,也是胖哥认为最好的。本次出版的也是上册,据出版社介绍,上册内容部分总共为490页。 这本书从写这一年多以来很久,很多小伙伴一直对这本书的名字很感兴趣,哈哈,本书的名字还是有点霸气的,叫做《Java特种兵》,不过我想说的是名字只是一个嚼头,并不是说看了这本书就成特种兵了,具体细节可以看书里面的前言部分,本书封面基本定下来,可参考下图: 这本书上册有2个篇,一个是基础篇、一个是源码篇。最近也有很多小伙伴问我到底要写什么,这里我就贴下目录: 上册目录如下: 第1篇 Java功底篇 第1章 扎马:看看功底如何 1.1 String的例子,见证下我们的功底 1.1.1 关于“==” 1.1.2 关于“equals()” 1.1.3 编译时优化方案 1.1.4 补充一个例子 1.1.5 跟String较劲上了 1.1.6 intern()/equals() 1.1.7 StringBuilder.append()与String“+”的PK.. 1.2 一些简单算法,你会如何理解 1.2.1 从一堆数据中找max和min 1.2.2 从100万个数字中找最大的10个数字 1.2.3 关于排序,实际场景很重要 1.2.4 数据库是怎么找数据的 1.2.5 Hash算法的形象概念 1.3 简单数字游戏玩一玩 1.3.1 变量A、B交换有几种方式 1.3.2 将无序数据Hash到指定的板块 1.3.3 大量判定“是|否”的操作 1.3.4 简单的数据转换 1.3.5 数字太大,long都存放不下 1.4 功底概述 1.4.1 什么是功底 1.4.2 功底有何用途 1.4.3 如何磨练功底 1.5 功底补充 1.5.1 原生态类型 1.5.2 集合类 1.6 常见的目录与工具包 1.7 面对技术,我们纠结的那些事儿 1.7.1 为什么我这里好用,哪里不好用 1.7.2 你的程序不好用,你会不会用,环境有问题 1.7.3 经验是否能当饭吃 1.8 老A是在逆境中迎难而上者 第2章 Java程序员要知道计算机工作原理 2.1 Java程序员需要知道计算机工作原理吗? 2.2 CPU的那些事儿 2.2.1 从CPU联系到Java 2.2.2 多核 2.2.3 Cache line 2.2.4 缓存一致性协议 2.2.5 上下文切换 2.2.6 并发与征用 2.3 内存 2.4 磁盘 2.5 缓存 2.5.1 缓存的相对性 2.5.2 缓存的用途和场景 2.6 关于网络与数据库 2.6.1 Java基本I/O 2.6.2 Java的网络基本原则 2.6.3 Java与数据库的交互 2.7 总结 第3章 JVM,Java程序员的OS 3.1 学习Java虚拟机对我们有什么好处 3.2 跨平台与字节码基本原理 3.2.1 javap命令工具 3.2.2 Java字节码结构 3.2.3 Class字节码的加载 3.2.4 字节码增强 3.3 从虚拟机的板块开始 3.3.1 Hotspot VM板块划分 3.3.2 “对象存放位置”小总结 3.3.3 关于永久代 3.4 常见的虚拟机回收算法 3.4.1 串行GC 3.4.2 ParallelGC与ParallelOldGC 3.4.3 CMS GC与未来的G1 3.4.4 简单总结 3.4.5 小小补充 3.5 浅析Java对象的内存结构 3.5.1 原始类型与对象的自动拆装箱 3.5.2 对象内存结构 3.5.3 对象嵌套 3.5.4 常见类型 & 集合类的内存结构 3.5.5 程序中内存拷贝和垃圾 3.5.6 如何计算对象大小 3.5.7 轻松玩一玩int[2][100]PK int[100][2] 3.6 常见的OOM现象 3.6.1 HeapSize OOM 3.6.2 PermGen OOM 3.6.3 DirectBuffer OOM 3.6.4 StackOverflowError 3.6.5 其他的一些内存溢出现象 3.7 常见的Java工具 3.7.1 jps. 3.7.2 jstat 3.7.3 jmap. 3.7.4 jstack. 3.7.5 jinfo. 3.7.6 JConsole. 3.7.7 Visual VM... 3.7.8 MAT(MemoryAnalyzer Tool)... 3.7.9 BTrace. 3.7.10 HSDB.. 3.7.11 工具总结... 3.8 总结.... 3.8.1 写代码... 3.8.2 心理上战胜虚拟机带来的恐惧 第4章 Java通信,交互就需要通信 4.1 通信概述.... 4.1.1 Java通信的基本过程... 4.1.2 Java通信的协议包装... 4.1.3 编写自定义通信协议... 4.1.4 Java的I/O流是不是很难学... 4.2 Java I/O与内存的那些事.... 4.2.1 常规I/O操作的运作过程... 4.2.2 DirectBuffer的使用... 4.2.3 关于Buffer 4.2.4 FileChannel的加锁... 4.3 通信调度方式.... 4.3.1 同步与异步... 4.3.2 阻塞与非阻塞... 4.3.3 Linux OS调度IO模型... 4.3.4 Java中的BIO、NIO.. 4.3.5 Java AIO.. 4.4 Tomcat中对I/O的请求处理.... 4.4.1 Tomcat的配置&一个请求的响应... 4.4.2 Request、Response对象生成... 4.4.3 拉与推... 第5章 Java并发,你会遇到吗.............. 5.1 基础介绍.... 5.1.1 线程基础... 5.1.2 多线程... 5.1.3 线程状态... 5.1.4 反面教材suspend()、resume()、stop() 5.1.5 调度优先级... 5.1.6 线程合并(Join)... 5.1.7 线程补充小知识... 5.2 线程安全.... 5.2.1 并发内存模型概述... 5.2.2 一些并发问题描述... 5.2.3 volatile. 5.2.4 final 5.2.5 栈封闭... 5.2.6 ThreadLocal 5.3 原子性与锁.... 5.3.1 synchronized. 5.3.2 什么是乐观锁... 5.3.3 并发与锁... 5.3.4 Atomic. 5.3.5 Lock. 5.3.6 并发编程核心AQS原理... 5.3.7 锁的自身优化方法... 5.4 JDK 1.6并发编程的一些集合类.... 5.5 常见的并发编程工具.... 5.5.1 CountDownLatch. 5.5.2 CyclicBarrier 5.5.3 Semaphor 5.5.4 其他工具简介... 5.6 线程池&调度池.... 5.6.1 阻塞队列模型... 5.6.2 ThreadPoolExecutor 5.6.3 调度器ScheduleThreadPoolExecutor 5.7 总结:编写并发程序要注意些什么.... 5.7.1 锁粒度... 5.7.2 死锁... 5.7.3 “坑”很多... 5.7.4 并发效率一定高吗... 5.8 其他的并发编程知识.... 5.8.1 ShutdownHook(钩子线程) 5.8.2 Future. 332 5.8.3 异步并不等价于多线程... 第6章 好的程序员应当知道数据库基本原理........................................... 6.1 开发人员为什么要知道数据库原理.... 6.2 从开发人员角度看数据库原理.... 6.2.1 实例与存储... 6.2.2 数据库基本原理... 6.2.3 索引基本原理... 6.2.4 数据库主从基本原理... 6.2.5 我们经常相信的那些经验... 6.3 从程序员角度看数据库优化方法.... 6.3.1 不同领域的SQL区别... 6.3.2 执行计划... 6.3.3 SQL逻辑的例子... 6.3.4 模型结构设计的优化... 6.3.5 临时表... 6.3.6 分页知识补充... 6.3.7 计算count值... 6.3.8 分布式事务探讨... 6.3.9 其他... 6.4 学会最基本的性能诊断.... 6.4.1 进入云数据库时代... 6.4.2 从程序员角度关注的数据库诊断信息... 6.5 数风流存储,还看今朝.... 第2篇 源码篇 第7章 源码基础 7.1 为何会出现框架.... 7.2 阅读框架前的技术储备.... 7.2.1 反射基础知识... 7.2.2 AOP基础... 7.2.3 ORM基础... 7.2.4 Annotation与配置文件... 第8章 部分JDBC源码讲解.. 8.1 JDBC通用接口规范.... 8.2 JDBC Driver注册.... 8.3 创建Connection. 8.4 SQL执行及处理.... 8.4.1 创建Statement 8.4.2 Batch设置批处理... 8.4.3 fetchSize与maxRows. 8.4.4 setQueryTimeout()与cancel() 第9章 部分Spring源码讲解 9.1 Spring MVC 9.1.1 Spring加载 9.1.2 Spring MVC处理一个简单请求 9.2 Spring事务管理器 9.2.1 JDBC事务的基本思想 9.2.2 Spring事务管理器的基本架构 9.2.3 Spring如何保存Connection 9.2.4 Spring如何保证程序中多次获取到的连接是同一个 9.3 思考:自己做框架有眉目了吗 第10章 看源码的一些总结. 10.1 高手看API的能力 10.2 通过源码能否量化性能与稳定性 10.3 思考相似方案和技术的优缺点 10.4 明确场景和业务,不做技术控 10.4.1 谈谈技术控的那些事.. 10.4.2 明确业务背景的例子.. 10.5 胖哥对框架的浅析... 10.5.1 框架由来的一个补充. 10.5.2 开源框架与扩展.. 10.5.3 框架与解决问题. 10.6 学海无涯,心境无限. 下册的手稿也已经写好,但是具体的目录和内容还会有所改动,不过不会有非常大的变动了,为了满足大家的好奇心,我写一下下册大概要写的内容(在上册的前言中也有): 1、设计篇,设计篇会提到设计模式和思想,但是胖哥会提出一些探讨性的观点和生活中的思想,然后我们再用一些实例来设计一些东西(2-3章)。 2、实现篇,主要讲解项目中的你我他,胖哥会讲讲自己所经历的一些项目中的稀奇古怪的事情,做事情的方法,人与人之间的合作,一些技术坑。 3、扩展篇,扩展篇比较杂,有走马观花式探讨一些集群、分布式的技术知识,也有探讨思想方法,看问题的角度,做IT人的心态等。 大概就这些了,下次大概就这些了,上册如果大家喜欢的话,下册我也会竭尽所能尽快review好,好了,就这些了! 等书出版的时候我再发个帖子,哈哈!
最近在写一个数据库访问的中间平台时,使用MySQL JDBC处理一些日期数据,遇到点变态的问题,给大家乐一乐! 首先来看看什么样的日期数据这么蛋疼呢? DATE 0000-00-00 DATETIME 0000-00-00 00:00:00 TIMESTAMP 0000-00-00 00:00:00 TIME 25:21:22 对于前3种情况,直接用JDBC读取,肯定会报错,报错信息类似这样: Value '0000-00-00' can not be represented as java.sql.Date 或者 Value '0000-00-00' can not be represented as java.sql.Timestamp 为什么?日期值即使是0,也对应到1970-01-01,这种变态的日期格式,不知道那个奇人想出来的。 对于这样的问题,很多小伙伴想到的第一种办法就是在jdbc url参数上设置一个:zeroDateTimeBehavior=convertToNull 便可以让Java程序不报错了!因为JDBC内部发现是0日期格式,则会转换为null返回。 验证中确实可以解决问题,但是,但是,某个用户的数据库就是写入了这样的一条数据,但是程序确返回了null,用户认为这并不是他想要的数据(例如在做数据迁移时,目标字段不可空,此时就会报错),用户就希望看到的是0000-00-00这样格式的数据。怎么办呢? 首先必须是将参数zeroDateTimeBehavior=convertToNull去掉,否则程序拿到的始终是null,根本不知道数据库的数据是什么。但是这样程序会报错,不论用getString还是getObject都会报错。 经过验证,发现getBytes()不会报错,然后通过得到的bytes[]数组,new String(bytes[])就可以得到一个这样的字符串,并且与数据库内一致。似乎问题解决了? 没有,更蛋疼的问题出现了,当JDBC启用流模式或游标模式时,getBytes()也会报同样的错误,经过验证发现get各种类型都会报错,这尼玛太蛋疼了,没空看源码,据我估计,MySQL JDBC在流模式和游标模式中,对结果集的某些类型转换处理,没有复用普通模式(默认是本能地静态数据)的处理代码导致了这样的问题。 但是对于某些大数据的处理,业务应用中必须启用流模式或游标模式,因此陷入了一个死套--因此我认为这是MySQL JDBC的一个BUG。 但是用户的问题必须要解决,为此,不得不去用一下特殊的处理方式,异常判定,我想你看到这里应该认为这是世界上最土的办法了,呵呵!也就是捕获上面描述的Message信息,若发现则认为是0日期格式来解决,准备提交官方BUG,希望尽快能修复吧。 最后来说说Time类型,MySQL这个蛋疼的Time类型是指时长,而不是指日期上的小时:分钟:秒,因此它的小时数是可以超过24的,但是这样的值让Java来解析就会报错,因此对于MySQL的Time类型处理的时候未了避免问题。通常用getBytes()方式来获取值,然后用new String(byte[])来得到具体值,当然先要判空。
并发编程我自己写过不少文章,不过我由于其相对需要理解更多的东西,我自己写代码也有时长犯2的时候,对于这些犯2的问题,我们只能将它作为自己宝贵的经历和财富,本文是很简单Java并发方面的小文章,为啥?因为是一个犯2的例子,这里给大家做个简单分享。 先简单描述下场景: 在一个app中,我需要为访问者提供某种信息的存储,由于架构上已经确定的方式,所以可以确保每一个app上存储的用户不会太多,于是就放在了内存中,而不是缓存。 这些信息需要定期清理掉,就像会话一样,每个用户都会有一个唯一的key标识符,用一个ConcurrentHashMap存放,长时间不使用就需要删除掉了。 但是它与会话不同的是,在清空的同时会清空掉许多用户级别的网络通信对象,例如Socket或数据库连接对象等。因此它的清理将与传统的清理方法有一些区别,为何? 因为当清理程序发现需要清理该对象的时候,这个对象正好被一个有效请求所使用,在清理对象的时候,需要将内部的Socket等资源关闭,就会导致问题。 因此我不得不在这个用户级别的对象上去做一个状态: 简单来说有一个FREE、USE、DELETE三种状态,FREE是可以修改为任意状态的,USE是使用状态的,DELETE是删除状态的。USE状态的不能被删除,DELETE状态的不能再被使用。 简单逻辑是: 1、如果通过ConcurrentHashMap获取到相应的对象后,需要判定状态不能是DELETE,再尝试在对象上修改状态为USE才能使用,如果修改失败则不能被使用,当然是用后会更新下最新的时间,这个时间将用volatile来保证可见性,以便于最近不会被清理掉,使用完后会讲对象的状态重新修改为FREE。伪代码如下所示: int old = status.get(); if(old != DELETE && status.compareAndSet(old , USED)) { return this.userXXXDO; } return null; 2、在删除操作前也必须先获通过ConcurrentHashMap取到对象,需要判定状态不能是USE,然后尝试将状态修改为DELELE才能真正开始做删除操作。代码与上面类似。 这个逻辑似乎看似完美,我当时晕头转向的也认为CAS就可以简单搞定这个问题,做几个状态嘛,简单事情,呵呵。 结果以外发生了,外部程序偶然情况下获取不到这个对象,但是在获取不到这个对象的断点中,我使用表达式再执行一次又能获取到,这尼玛是什么问题发生了呢? 刚开始我也跑偏了,因为外层有一个ConcurrentHashMap,思维凝固在是不是这有并发可见性问题,不过这样的猜测连我自己都没有相信,因为我对这个组件的内在的源码是比较了解的,如果它有问题,就彻底颠覆可见性的问题了。 在不断加班到半夜的迷糊中,迷迷糊糊地跟踪代码,发现里头还有一层,就看到点希望,看到了刚才的代码。咋一看,代码没有啥问题,因为这个就是状态转换,而且这个是在一个用户下的操作,一个用户并发的概率本来就很低,而且有CAS来保证原子性,能有什么问题呢? 后来一个哥们问我可不可以用synchronzied一下子提醒了我,我的第一反应是不到万不得已不用这个,这个如果放在内部做就是所有的状态转换全部要加上,悲观锁就不好了,放在外面更不靠谱,那就是一个全局的ConcurrentHashMap,那用它来控制个毛的并发啊,我就是要把锁打散。 但是这个提示让我在迷迷糊糊中醒了一下,我发现可能真的有并发问题,或者说假设一个用户的客户端同时发送多个请求上来,此时由于是同一个用户的请求是同一个,所以KEY肯定是一样的,缓存用户对象也应该是一样的,此时如果两个请求都运行到代码: int old = status.get(); 那么两个请求在此时获取到的状态值就是一样的,当发生CAS的时候,只有一个会成功,另一个不成功的就返回null了,代码看了很久,虽然很简单,但是只可能这里有问题。 考虑实际场景,还真的可能有一个客户端的浏览器同时发起多个请求的情况,因为客户端并不是简单的页面跳转(简单页面用户手点击再快也有时差),而是与服务器端很多ajax交互,当一个选项发生变化的时候,确实有可能同时发起多个ajax请求。 不过怎么改呢?用syncrhonezized,显示我不是那么容易放弃自己的人,哈哈,迷迷糊糊中终于才想起来,CAS也需要考虑下尝试,确实是这样,那么就改为循环来做。 但是一旦改为循环大伙第一个担心的问题就是能否退出循环,Java的里面有许多死循环方式,但是这种代码不退出就是一个大问题,但是限制次数的话,多少为好?这不好说,因为乐观锁在这个阶段是不好讲清楚具体的次数的,或许在许多人眼中这算是小问题,但是我认为在这些问题上是关键的关键,如果不注重就会出大问题。 后来考虑来考虑去发现这样写没问题: int old = status.get(); while(old != DELETED) { if(old == USED && status.compareAndSet(old , USED)) { return this.loginDO; } old = use.get(); } return null; 这个while循环的条件是状态没有被删除,状态只要有被删除,这个请求就应该有机会去获取使用机会,只要有机会就应该去尝试,大家会想会不会一直不成功呢?那不会,乐观锁的道理就是我们足够乐观,因为我们发生到这个点上的问题都是偶然,而且是用户级内部发生,所以它尝试的概率非常低,在这样做的方式下,我们采用乐观机制避开了悲观锁带来的巨大开销,同时又能保证原子性。而对于删除就没有必要循环了,删除操作发现状态是USE就不能删除,状态为FREE在做CAS的时候如果CAS征用失败也没有必要再去征用,为何?假如有两个线程在征用DELETE,另一个成功了就OK了,如果有一个USE在与之征用,它本身就没有再征用的必要。 到这里问题基本解决,但是这个程序是不是就没有问题了呢? 未必然也,因为最初我们写代码的时候没有考虑到多个请求同时发起的过程,所以也自然不会考虑到多个请求将状态改为FREE的过程,假如有2个请求,其中1个请求释放掉了将状态修改为FREE,而另一个还在使用中,此时有线程想将它DELETE掉,发现FREE状态,是可以删除的,于是将相应的Socket关闭掉,就出大事了。 如果要完全解决这种问题,还需要一个条件变量来使用和释放的次数,使用时加1,释放时候减掉1,这就有点像Lock机制了,只是可控性上更强,但是对于代码复杂性更大,你自己也需要承担更大的责任。 如果在应用中,出现这种问题的概率极低,那么可以暂时用状态也可以,或者为了简单处理也可以直接换成Lock。为何说概率低呢?因为这种数据的清理理论上不会到秒级别,例如10分钟,一个请求来的时候,会刷新最近的操作时间,后台操作即使一长一短,只要偏差不是10分钟以上,在理论上就不会有问题。 大家可能一想,一般要求系统响应3s,不会有那种情况发生。真的是这样嘛?我认为未必,所谓3s只是常规系统,有的系统就未必了,例如WEB版本的数据库软件,通过UI上输入SQL获取结果,WEB版本的安装系统给上千的服务器安装相应的软件等等,这些操作的响应都是可以很长的,这个值是有可能超过我们的清理时间的,所以一切皆有可能,当你真正遇到的时候,希望这些小思路能帮助到你。
3.2.1javap命令工具 第1章中我们就提到了有些地方需要用javap命令工具来看编译后的指令是什么,第2.2.1节中胖哥使用了一个简单的程序让大家感受了一下javap命令工具是什么,这里再次谈到javap命令工具了。或许这一次我们可以对javap命令工具说得稍微清楚一点。为此,胖哥会单独再写几段小程序给大家说说javap命令工具的结果怎么看。 胖哥为什么要给简单程序呢?为啥不直接来个复杂的程序呢? 答曰:javap命令工具输出的内容是繁杂的,即使是一段小程序输出后,结果也比原始代码要复杂很多。我们要学的其实并不是说看指令就能完全反转为Java代码,把自己当成一个“反编译工具”(除非你真的已经很牛了,自然本书接下来的内容也不适合你),要学会的是通过这种方式可以认知比Java更低一个抽象层次的逻辑,或许有许多问题直接用Java代码不好解释,但是一旦看到虚指令后就一切明了。 在本节,胖哥分别演示String的小代码,和几段数字处理的小程序(延续下第1章的数字游戏)。 String的代码还少吗?第1章就很多了? 没错,胖哥没有必要再来写第1章写过的那些小程序,就用它们来做实验吧。首先来回顾下代码清单1-1的例子(这里仅截图),如下图所示: 图 3-1 代码清单1-1的还原 当时我们提到这个结果是true,并且解释了它是在编译时被优化,现在就用javap指令来论证下这个结论吧: D:\java_A>javac –g:vars,lines chapter01/StringTest.java D:\java_A>javap -verbose chapter01.StringTest public class chapter01.StringTest extends java.lang.Object minor version: 0 major version: 50 Constant pool: const #1 = Method #6.#21; // java/lang/Object."<init>":()V const #2 = String #22; // ab1 const #3 = Field #23.#24; // java/lang/System.out:Ljava/io/PrintStream; const #4 = Method #25.#26; // java/io/PrintStream.println:(Z)V const #5 = class #27; // chapter01/StringTest const #6 = class #28; // java/lang/Object const #7 = Asciz <init>; const #8 = Asciz ()V; const #9 = Asciz Code; const #10 = Asciz LineNumberTable; const #11 = Asciz LocalVariableTable; const #12 = Asciz this; const #13 = Asciz Lchapter01/StringTest;; const #14 = Asciz test1; const #15 = Asciz a; const #16 = Asciz Ljava/lang/String;; const #17 = Asciz b; const #18 = Asciz StackMapTable; const #19 = class #29; // java/lang/String const #20 = class #30; // java/io/PrintStream const #21 = NameAndType #7:#8;// "<init>":()V const #22 = Asciz ab1; const #23 = class #31; // java/lang/System const #24 = NameAndType #32:#33;// out:Ljava/io/PrintStream; const #25 = class #30; // java/io/PrintStream const #26 = NameAndType #34:#35;// println:(Z)V const #27 = Asciz chapter01/StringTest; const #28 = Asciz java/lang/Object; const #29 = Asciz java/lang/String; const #30 = Asciz java/io/PrintStream; const #31 = Asciz java/lang/System; const #32 = Asciz out; const #33 = Asciz Ljava/io/PrintStream;; const #34 = Asciz println; const #35 = Asciz (Z)V; { public chapter01.StringTest(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 4: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lchapter01/StringTest; public static void test1(); Code: Stack=3, Locals=2, Args_size=0 0: ldc #2; //String ab1 2: astore_0 3: ldc #2; //String ab1 5: astore_1 6: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream; 9: aload_0 10: aload_1 11: if_acmpne 18 14: iconst_1 15: goto 19 18: iconst_0 19: invokevirtual #4; //Method java/io/PrintStream.println:(Z)V 22: return LineNumberTable: line 7: 0 line 8: 3 line 9: 6 line 10: 22 LocalVariableTable: Start Length Slot Name Signature 3 20 0 a Ljava/lang/String; 6 17 1 b Ljava/lang/String; StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 18 locals = [ class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class java/lang/String, class java/lang/String ] stack = [ class java/io/PrintStream, int ] } 好长好长的篇幅啊! 没关系,我们慢慢来看哈! 首先我们看比较靠前的一个部分是:“常量池”(Constant pool),每一项都以“const #数字”开头,这个数字是顺序递增的,通常把它叫做常量池的入口位置,当程序中需要使用到常量池的时候,就会在程序的对应位置记录下入口位置的标识符(在字节码文件中,就像一个列表一样,列表中的每一项存放的内容和长度是不一样的而已)。 根据入口位置肯定是要找某些常量内容,常量内容会分为很多种。在每个常量池项最前面的1个字节,来标志常量池的类型(我们看到的Method、String等等都是经过映射转换后得到的,字节码中本身只会有1个字节来存放)。 找到类型后,接下来就是内容,内容可以是直接存放在这个常量池的入口中,也可能由其它的一个或多个常量池域组合而成,听起来蛮抽象,胖哥来给大家讲几个例子: 例子1: const #1 = Method #6.#21; // java/lang/Object."<init>":()V 入口位置#1,简称入口#1,代表一个方法入口,方法入口由:入口#6 和 入口#21两者一起组成,中间用了一个“.”。 const #6 = class #28; // java/lang/Object const #21 = NameAndType #7:#8;// "<init>":()V 入口#6为一个class,class是一种引用,所以它引用了入口#28的常量池。 入口#21 代表一个表示名称和类型(NameAndType),分别由入口#7和入口#8组成。 const #7 = Asciz <init>; const #8 = Asciz ()V; const #28 = Asciz java/lang/Object; 入口#7是一个常量池内容,<init>;代表构造方法的意思。 入口#8 也是一个真正的常量,值为()V,代表没有入口参数,返回值为void,将入口#7和入口#8反推到入口#21,就代表名称为构造方法的名称,入口参数个数为0,返回值为void的意思。 入口#28是一个常量,它的值是“java/lang/Object;”,但这只是一个字符串值,反推到入口#6,要求这个字符串代表的是一个类,那么自然代表的类是java.lang.Object。 综合起来就是:java.lang.Object类的构造方法,入口参数个数为0,返回值为void,其实这在const #1后面的备注中已经标识出来了(这在字节码中本身不存在,只是javap工具帮助合并的)。 例子2: const #2 = String #22; // ab1 它代表将会有一个String类型的引用入口,而引用的是入口#22的内容。 const #22 = Asciz ab1; 这里代表常量池中会存放内容ab1。 综合起来就是:一个String对象的常量,存放的值是ab1。 例子3(稍微复杂一点): const #3 = Field #23.#24; // java/lang/System.out:Ljava/io/PrintStream; const #4 = Method #25.#26; // java/io/PrintStream.println:(Z)V 入口#3代表一个属性,这个属性引用了入口#23的类,入口#24的具体属性。 入口#4代表一个方法,引用了入口#25的类,入口#26的具体方法。 const #23 = class #31; // java/lang/System const #24 = NameAndType #32:#33;// out:Ljava/io/PrintStream; const #25 = class #30; // java/io/PrintStream const #26 = NameAndType #34:#35;// println:(Z)V 入口#23 代表一个类(class),它也是一个引用,它引用了入口#31的常量。 入口#24 代表一个名称和类型(NameAndType),分别对应入口#32:#33。 入口 #25 代表一个class类的引用,具体引用到入口#30。 入口 #26 与入口#24类似,也是一个返回值+引用类型对应入口#34:#35。 const #30 = Asciz java/io/PrintStream; const #31 = Asciz java/lang/System; const #32 = Asciz out; const #33 = Asciz Ljava/io/PrintStream;; const #34 = Asciz println; const #35 = Asciz (Z)V; 入口#30 对应常量池的值为:java/io/PrintStream;反推到入口#25,自然代表类java.lang.PrintStream。 入口#31对应常量池的值为:java/lang/System;反推到入口#23,代表类:java.lang.System。 入口#32 对应常量池的值为:out;反推到入口#24,而入口#24要求名称和类型,这里返回的显然是名称。 入口#33 对应常量池的值为:Ljava/io/PrintStream;; 反推到入口#24这里得到了类型,也就是out的类型是java.io.PrintStream。 入口#34 对应常量池的值为:println;反推到入口#26代表名称为println。 入口#35 对应常量池的值为:(Z)V;反推到入口#26代表入口参数为Z(代表boolean类型),返回值类型是V(代表void) 综合来讲要执行的操作就是: 入口#3是获取到java/lang/System类的属性out,out的类型是Ljava/io/PrintStream; 入口#4是调用java/io/PrintStream类的println方法,方法的返回值类型是void,入口类型是boolean。 小伙伴们应该发现到这个常量池仅仅是操作的陈列,还没有真正的开始执行任务,那么自然就要开始看第2部分的内容,它通过指令将这些内容组合起来。从输出的结果来看,这些的指令是按照方法分开的(其实前面应当还有属性列表),首先看第一个方法: public chapter01.StringTest(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 4: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lchapter01/StringTest; 这是一个构造方法,程序中我们没有写构造方法,但是Java自己会帮我们生成一个,说明这个动作是在编译时完成的。虽然是构造方法,但是它足够简单,所以我们先从它开始来说,请看胖哥的解释: Stack=1, Locals=1, Args_size=1 这一行是所有的方法都会有的,其中Stack代表栈顶的单位大小(每一个大小为一个solt的大小,每个solt是4个字节的宽度),当一个数据需要使用时首先会被放入到栈顶,使用完后会写回到本地变量或主存中。这里的栈的宽度是1,其实是代表有一个this将会被使用。 Locals是本地变量的slot个数,但是并不代表是stack宽度一致,本地变量是在这个方法生命周期内,局部变量最多的时候,需要多大的宽度来存放数据(double、long会占用两个slot)。 Args_size代表的是入参的个数,不再是slot的个数,也就是传入一个long,也只会记录1。 0: aload_0 首先第一个0代表虚指令中的行号(后面会应到,确切说应该是方法的body部分第几个字节),每个方法从0开始顺序递增,但是可以跳跃,跳跃的原因在于一些指令还会接操作的内容,这些操作的内容可能来自常量池,也可以标志是第几个slot的本地变量,因此需要占用一定的空间。 aload_0指令是将“第1个”slot所在的本地变量推到栈顶,并且这个本地变量是引用类型的,相关的指令有:aload_[0-3](范围是:0x2a ~ 0x2d)。如果超过4个,则会使用“aload + 本地变量的slot位置”来完成(此时会多占用1个字节来存放),前者是通过具体的几个指令直接完成。 许多地方会解释为第1个引用类型的本地变量,但胖哥是一个逻辑怪,认为这句话有问题,并不是第1个引用变量,普通变量如果在它之前,它也不是第1个了,此时本身就是第1个本地变量,更确切地说是第一个slot所在位置的本地变量。 1: invokespecial #1; //Method java/lang/Object."<init>":()V 指令中的第2个行号,执行invokespecial指令,这个指令是当发生构造方法调用、父类的构造方法调用、非静态的private方法调用会使用该指令,这里需要从常量池中获取一个方法,这个地方会占用2个字节的宽度,加上指令本身就是3个字节,因此下一个行号是4。 4: return 最后一行是一个return,我们虽然没有自己写return,但是JVM中会自动在编译时加上。 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lchapter01/StringTest; 代表本地变量的列表,这里代表本地变量的作用域起始位置为0,作用域宽度为5(0-4),slot的起始位置也是0,名称为this,类型为chapter01.StringTest。 看了构造方法后,如果你理解了,再来看test1方法或许我们会轻松一点,不过大家可以在这个时候先养一养神,再来看哦。胖哥对于细节就不再一一讲述,就在指令后面写备注即可: public static void test1(); Code: Stack=3, Locals=2, Args_size=0 //Stack=3代表本地栈slot个数为3,两个String需要load,System的out也会占用一个,当发生对比生成boolean的时候,会将两个String的引用从栈顶pop出来,所以栈最多3个slot //Locals为2,因为只有两个String //如果是非静态方法本地变量会自动增加this. //Args_size为0代表这个方法没有任何入口参数 0: ldc #2; //String ab1 //指令body部分从第0个字节为Idc指令,从常量池入口#2中取出内容推到栈顶 //这里的String也是引用,但是它是常量,所以是用Idc指令,不是aload指令 2: astore_0 //将栈顶的引用值,写入第1个slot所在的本地变量中。 //它与aload指令正好相反,对应astore_[0-3](范围是0x4b、0x4e) //更多的本地引用变量写入则使用atore + 引用变量的slot位置。 3: ldc #2; //String ab1 //与第0行一致的操作,引用常量池入口#2来获得 5: astore_1 //类似第2行,将栈顶的值赋值给第2个slot位置的本地引用变量。 6: getstatic #3; //Field java/lang/System.out:Ljava/io/PrintStream; //获取静态域,放入栈顶,引用了常量池入口#3来获得 //此时的静态区域是System类中的out对象 9: aload_0 //将第1个slot所在位置的本地引用变量加载到栈顶 10: aload_1 //将第二个slot所在位置的本地引用变量加载到栈顶 11: if_acmpne 18 14: iconst_1 15: goto 19 18: iconst_0 //判定两个栈顶的引用是否一致(引用值也就是地址),对比处理的结束位置是18行 // if_acmpne操作之前会先将两个操作数从栈顶pop出来,因此栈顶最多3位 //如果一致则将常量值1写入到栈顶,也就是对应到boolean值true,并跳转到19行 //如果不一致则将常量值0写入到栈顶,对应到boolean值false 19: invokevirtual #4; //Method java/io/PrintStream.println:(Z)V //执行out对象的println方法,方法的入口参数是boolean类型,返回值是void。 //从常量池入口#4获得方法的内容实体。 //此时会将栈顶的元素当成入口参数,栈顶的0或1则会转换为boolean值的true、false。 22: return LineNumberTable: line 7: 0 line 8: 3 line 9: 6 line 10: 22 //对应源文件行号,左边的是字节码的位置(也可以叫做行号),右边的是源文件中的实际文本行号 //javac编译默认有这个内容,但是如果-g:none则不会产生,那么调试就会有问题 LocalVariableTable: Start Length Slot Name Signature 3 20 0 a Ljava/lang/String; 6 17 1 b Ljava/lang/String; //本地变量列表,javac中需要使用-g:vars才会生成,使用一些工具会自动生成,若没有,则调试的时候,断点中看到的变量是没有名称的。 //第一个本地变量的作用区域从第3个字节的位置开始,作用区域范围为20个字节,所在slot的位置是第0个位置,名称为a,类型为java.lang.String。 //第二个本地变量也是类似的方式可以得到结果。 在这里,还有一些内容并没有细化,例如StackMapTable的内容,这些请在研究清楚现有的内容后,就可以自己继续去深入和细化了,因为这部分内容会包含的知识是非常多的,关于指令部分,大家可以参考官方文档的介绍来学习。 http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-7.html 我们回过头来看问题,为何会输出true就很简单了,第一个变量a,代码中本身编写的是”a” + “b” + 1的操作,但是在常量池中却找不到这3个值,而且指令中也看不到对它们的操作,指令中只看到了对字符串”ab1”的操作,因此在编译阶段,JVM就将它合并了,这样我们不用去听别人说怎么优化,看看便知道。 这样貌似就是去找一些钻牛角尖的问题? 其实不然,其实是帮我们从根本上去了解一些细节,或者说是相对抽象层次较低的细节,当然可能你平时用不上,当我们真的有一天遇到一些诡异的问题,就可能用得上了。为此,胖哥再来个例子。 神马例子呢?很好奇哦! 第1章我们玩了点数字游戏,也许大家没有玩爽,当时胖哥说这一章会有,这一次我们来看那看一个简单的数字操作的指令细节是什么。在这之前,我们先看看代码如下所示(代码是放在第1章内的,但是说明问题是在本章开始说明): 代码清单 3-1 简单的数字叠加程序 public static void test() { int a = 1 , b = 1 , c = 1 , d = 1; a++; ++b; c = c++; d = ++d; System.out.println(a + "\t" + b + "\t" + c + "\t" + d); } 执行结果你猜到了吗,还是你确认了结果! 我们一起来看看输出结果吧: 2 2 1 2 胖哥此时估计有的小伙伴也惊呆了,为何会有一个1呢? 其余的几个结果为2的答案很好解释,但是这个1这个答案怎么解释呢? 教科书上通常告诉我们:i++是先做操作再自增,而++i是先自增再做操作。好的,我们按照这种思路来理解下c = c++;这条代码,如果是先做操作,那么这里只有赋值操作,就是c赋值给c,再自增显然自增后应该是2,但是输出的结果1,解释不通。难道是先自增在赋值?如果是这样的话,结果也应该是2才对。 小伙伴们迷茫了。这TNND的到底是怎么回事呢?有的小伙伴可能会说这是多么钻牛角尖的问题啊。胖哥也是这么认为的,这样的问题或许结果并不重要,重要的是它可以让我们了解到一个简单的自增操作不止一个步骤来完成的,让我们真正拥有一种去探索知识内在的兴趣。 教科书上的说法仅仅是为了方便大家理解而给出的一种通用说法,每一种语言在实现它的时候,都有自己的实现方式,我们是Java程序员,自然需要知道Java程序是怎么处理它的了(否则我们就真的就不专业了哦)。 难道自己思考是怎么回事吗,其实这种思考就是猜测了哦,猜测下可以锻炼下猜测能力,不过最终还得了解本质,看看这一小节告诉我们的指令就知道啦,就用它来输出指令看看指令里面到底做了什么(篇幅所限,这里不再看常量池,只说指令,而且只说关键部分)。 public static void test(); Code: Stack=3, Locals=4, Args_size=0 0: iconst_1 //将int类型常量值1推送到栈顶 1: istore_0 //将栈顶抛出赋值给第1个slot所在的int类型本的变量中 2: iconst_1 //与第0行一致 3: istore_1 //将栈顶抛出赋值给第2个slot所在的int类型本的变量中 4: iconst_1 //与第0行一致 5: istore_2 //将栈顶抛出赋值给第3个slot所在的int类型本的变量中 6: iconst_1 //与第0行一致 7: istore_3 //将栈顶抛出赋值给第4个slot所在的int类型本的变量中 8: iinc 0, 1 //将第1个slot所在的int类型本的变量自加1 11: iinc 1, 1 //将第2个slot所在的int类型本的变量自加1 14: iload_2 //将第3个slot所在的int类型本的变量放入栈顶 15: iinc 2, 1 //将第3个slot所在的int类型本的变量加1 18: istore_2 //从栈顶抛出数据写入到第3个slot所在的int类型本的变量 19: iinc 3, 1 //将第4个slot位置所在的int类型的本变量自增1 22: iload_3 //将第4个slot位置所在的int类型的本地变量加载到栈顶 23: istore_3 //将栈顶数据抛出,写入到第4个slot所在的int类型的本地变量中 LocalVariableTable: Start Length Slot Name Signature 2 70 0 a I //本地变量a,类型int,作用域第2行开始,作用域范围70行 4 68 1 b I//本地变量b,类型int,作用域第4行开始,作用域范围68行 6 66 2 c I//本地变量c,类型int,作用域第6行开始,作用域范围66行 8 64 3 d I//本地变量d,类型int,作用域第8行开始,作用域范围64行 现在我们来逐步看问题,首先发现的第一个特征是第8行、第11行,它们都做了iinc操作,都是对本地变量做叠加操作,分别是对前面两个本地变量(a、b)做叠加操作,后续没有其它的动作。换句话说,当一个本地变量发生i++或++i的操作的时候,如果这个代码发生在单行上面,即不会用于其它的计算操作,它们最终的指令都是iinc,也就是i++也会被改为++i操作。 进一步来看第3个本地变量c的操作,首先是通过iload_2指令将其拷贝到栈顶,然后发生iinc操作(即自增操作),然后通过istore_2指令将栈顶的数据赋值给这个本地变量,因此,你可以认为它就像做了一个这样的操作: int tmp = c; c++; c= tmp; 这样3个步骤的动作,只是这个tmp并不是真实存在的本地变量,而是栈顶的一份数据拷贝,这一份拷贝的数据其实是为其它的操作,而自己叠加数据并不参与其它的计算,这才是Java中实现i++的真实道理。 对比d的操作,可以看到d是先进行了iinc操作,然后再做iload、istore的两个动作用于赋值的,所以d是会被叠加的,只是最后两个动作是多余的而已。 这样小伙伴们是不是有点晕了! 我们画个图来看看,或许你会清楚一点。 首先来看看,进入方法前,JVM分配的栈大概是什么样子的(这个部分不包含指令及指令中指向的常量池位置): 图 3-2 初始化一个方法后,大概是这样的哦 当iconst_1发生的时候,结构就发生改变了: 当istore_0发生操作的时候,将栈顶抛出,赋值给变量a,此时的结构变成这样: 以此类推,发生到第7行,对4个本地变量都会发生这样的赋值,结果为: 图 3-5 分别通过栈定赋值后的结果,栈顶只用了一个slot iinc指令我们没有必要讲解(实现的细节也可以是利用了一个栈顶来store、叠加1、load),总之a、b两个变量变成了2。当再进一步做c = c++操作的时候首先发生第1个步骤是将数据拷贝到栈顶,然后将本地变量改为2,然后再从栈顶拷贝回来,如下图所示: 图 3-6 c=c++操作的程序运行过程 小伙伴们看懂了,但是又有的小伙伴着急了:后进先出栈明明只用一个slot为什么会有3个呢? 能问出这个问题说明你懂得思考,其实刚开始我们只是输出了一些简单的操作指令,后来还有一条代码System.out.println(a+ "\t" + b + "\t" + c + "\t" + d);相关指令还没有输出呢。别看这就一行代码,指令可多了哦(写代码写得短,并不代表指令短,也就是不能代表跑得快),一起来看看接下来的一些指令: 24: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 27: new #3; //class java/lang/StringBuilder 30: dup 31: invokespecial #4; //Method java/lang/StringBuilder."<init>":()V 34: iload_0 35: invokevirtual #5; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 38: ldc #6; //String \t 40: invokevirtual #7; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 43: iload_1 44: invokevirtual #5; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 47: ldc #6; //String \t 49: invokevirtual #7; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 52: iload_2 53: invokevirtual #5; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 56: ldc #6; //String \t 58: invokevirtual #7;//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 61: iload_3 62: invokevirtual #5; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 65: invokevirtual #8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 68: invokevirtual #9; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 71: return 好长,其实大部分指令都是invokevirtual指令,关键它在操作什么。其实大伙看看后面的注释应该就看懂了(这也是刚开始看不懂虚指令,可以直接看注释的方式)。现在我们想要知道本地栈要使用3个slot怎么来的。首先getstatic指令要将System类的out这个静态属性获取出来放入栈顶(因为没有局部变量存放,只能放在栈顶),接着通过new指令创建一个对象,这个对象通过常量池入口#3获得是一个StringBuilder类型(这是证明第1章中提到的字符串拼接的结论)。此时栈的样子应当是这样的: 图 3-7 执行前两条指令后,栈空间的情况 此时发生的是dup命令,它会拷贝一份栈顶的内容,并写入栈顶,为什么要这样做呢?因为后续的invokespecial操作将会栈顶的信息抛出执行,执行StringBuilder的构造方法(小伙伴们又着急了,不是已经创建了吗,干嘛还有构造方法,其实刚才的new仅仅分配了空间,还没有对内容进行初始化呢,一个简单的创建对象其实需要多条指令来完成的)。因此此时的栈就变成了这样的情况: 图3-8 执行dup指令过后的情况 这里3个栈就用过了,小伙伴们应该清楚了Stack为3的情况了吧!大家可以将这条代码去掉,或自己用一个StringBuilder来拼接看看结果是什么样子的。 接下来就将栈顶抛出,执行StringBuilder的构造方法(两个引用其实引用同一个对象),初始化后就变得和图3-7一样,只是现在的StringBuilder已经执行完构造方法(但是并不代表所有属性都初始化完成,在第5章会提到重排序的问题)。 紧接着,将本地变量、常量“\t”逐个iload或aload到栈顶,然后调用invokevirtual指令调用StringBuilder类的append方法,虽然它也会pop出来做操作,但这个方法会有一个StringBuilder返回值,由于下一个动作是基于这个返回值来操作,所以这个返回值将会再次被赋值到栈顶,因此它执行前无需再拷贝了,如果这个StringBuilder是一个自定义的本地变量,也无需再一次iload操作。 大家可以在这段代码上做几个小改动,进一步分析: ○ 将拼接过程换成一个StringBuilder,看看Stacks的数量有没有变化。 ○ 换成一个StringBuilder在一行代码中append多个变量,与分成多行分别append,指令上是否有区别(这里append的内容有7个,你完全可以拆分2、3个出来看看)。 ○ 添加一个自定义对象,自定义对象中有一个void返回值的方法,也像append那样反复调用,看看它是不是需要每次iload,而StringBuilder不需要。其实为什么我们已经解释过了,接下来就靠大家自己去扩展了哦。 胖哥只是举例说明一些简单的例子,大家可以继续扩展,例如(i++) + (++i) + (i++)等等,或许你看看指令就清楚了内在的执行顺序。关于JVM的指令有200多个,我们要一一看完不容易,可以先看自己想看的一些指令,或者自己写几个简单程序看看指令。等到我们知道了许多的指令后,再系统化的看这些指令,就很轻松了哦。 这些指令还是javap命令告诉我们的,javap命令本身也将字节码翻译成了文字,它比起反编译工具只是更加接近于字节码的结构(大家也大概了解到反编译工具就是基于这种指令反向计算出程序代码的),但是它还不是真正的字节码,如果有兴趣的小伙伴们,可以看看下一节胖哥对于字节码本身的介绍,然后javap命令工具是如何解析这个字节码得到内容的。 这是本书的一个小样章,内容格式贴进来全部乱了,请大家谅解。 哈哈,最后还是插播一个广告: 大家觉得小胖的文章写得还行的话,就投票吧,哈哈! 投票地址:http://vote.blog.csdn.net/blogstaritem/blogstar2013/xieyuooo呵呵,觉得想吐槽就吐吧!
hi,玩Java的小伙伴们,起来吧!很稀里糊涂地成为CSDN 2013年度博客之星评选的候选人,大家觉得小胖的文章写得还行的话,就投票吧,哈哈! 投票地址:http://vote.blog.csdn.net/blogstaritem/blogstar2013/xieyuooo 顺便,小胖的书快出版了哦,一本纯手工打造的Java书,60W字左右,不过是一本比较快乐的Java野书,目前出版社正在紧急的编辑中了,哈哈!有兴趣的小伙伴请关注哦!
这是公司的一个重要项目中的真实案例(目前还未证实其它版本是否存在,不过刚看了最新版5.1 .26版本还是没有修复这个操作方式,不过用的小伙伴们要注意了哦): 【该BUG,官方目前最新版本已经修复,详细请参考文章最后,大家注意使用的版本和原因即可】 什么样的情况呢,当在代码中使用connection.close()方法的时候,神奇般的StackOverflow了!没错,这就是JDBC自己导致的死递归,堆栈输出的内容如下所示: 这个堆栈信息可以这样反推程序: ConnectionImpl.realClose() -> ConnectionImpl.closeAllOpenStatements() ->StatementImpl.realClose() ->ResultSetImpl.close() ->ResultSetImpl.realClose() ->RowDataDynamic.close() ->StatementImpl.executeSimpleNonQuery() ->ConnectionImpl.execSQL() ->ConnectionImpl.cleanup() ->ConnectionImpl.realClose()//到这里回来了,于是乎接下来的事情,就按照这个顺序一发不可收拾,栈日志TNND几十米长,最后StackOverflowError是必然的了。 这是多么神奇的事情啊,MySQL的JDBC发布的时候难道也没有测试下,但是这种情况据同事介绍也不是每次都会发生,是偶然性情况。于是就要跟一下内在代码是怎么回事。 首先在MySQL JDBC现在的代码中,Connection接口是通过它内部的一个com.mysql.jdbc.ConnectionImpl的实现类来实现的,因此要跟踪close方法,就跟踪它就好了,如下图: 这里进入正轨,realClose方法进去了,这个方法很长,里面涉及到一些判定是否已经关闭、回滚、io关闭等等操作,在io关闭操作之前,需要关闭被打开的Statement信息,换句话说,MySQL在调用Connection.close的时候会自动关闭掉Statement信息,而无需业务代码来编写,不过你也写了也没错,其余代码我们忽略掉,因为不是问题的重点,关键是它内部确实调用了一个这样的方法,如下图所示: 在这个方法里面,会做什么动作呢?简单来说,就是循环,并调用对应的statement的close()方法 注意这里传入了两个参数,分别是false、true,第二个参数对这个问题是有用途的,第二个参数代表是否关闭掉Statement下面的ResultSet,在StatementImpl类具体的实现方法中的部分是: 第一个close自然是close掉当前的ResetSet、第二个是要获取generatedKey的结果集,最后一个closeAllOpenResults是会关闭掉内部记录的一个Set列表,这个列表会在获取GeneratedKeys、getMoreResults(java.sql.Statement.KEEP_CURRENT_RESULT)是增加ResultSet进去。 我们这里主要关心第一个,就是ResultSet的close方法,它ResultSetImpl实现类的close方法,如下所示: 这里依然调用了ResultSet的realClose方法,和日志中输出的内容一致,这个方法的finally部分会调用一个叫做rowData.close()方法: 它的类型是:RowData,是MySQL的一个接口:com.mysql.jdbc.RowData,它的实现类有: 具体使用哪一个,会由一些参数来决定,这个说起来又会涉及到许多源码,与本问题关系不大,暂时不扯开,从堆栈输出中可以看到使用的是第二个RowDataDynamic这个实现类,于是乎打开这个close方法的代码来看看,也很长,不过我们关注关键的部分,那就是它还调用了statement,如下图所示: 这里关闭的时候其实要执行一条语句,它创建了一个Statement,没有什么问题,因为这个Statement和当前ResultSet的Statement不是同一个(也就是这个Statement可能不是用的RowDataDynamic),但是它调用了一个executeSimpleNonQuery,这个方法需要传入Connection对象,显然一个Connection下面不论多少个Statement,这个Connection都是同一个。这个方法内部做了什么呢? 这个代码显然调用了connection的一个executeSQL方法(注意了,这里的MySQLConnection其实是一个接口,是MySQL自己继承于java.sql.Connection的接口,ConnectionImpl也是实现这个接口的,对象始终是同一个)。 接下来又回到ConnectionImpl的execSQL方法里面了,这段代码在内部的抛出异常的时候,且highAvailabilityAsBoolean为false的时候,会调用cleanup方法(默认为false,只有设置了autoReconnect才会变成true,这个参数在初始化Connection的时候被赋值): 这里只有抛出异常的时候会到这个里面来,但是异常确实发生了,而且这种发生往往是偶然的,而且一旦偶然发生,将一发不可收拾(例如网络闪了,或服务器端做了什么kill之类的操作),这个cleanup方法内部就会再次调用realClose方法: 显然,这里的Connection还没有关闭完,所以io不会为空,而且isClose也会返回false,自然会调用realClose方法,这个方法就回到前面的第二幅图的代码了,就这样,程序一发不可收的开始递归了。 使用类似代码的童鞋要注意了,换下版本就好,其余的版本还没看过代码,5.1.16代码路径有所不同,但也有类似的问题,打算抽时间看看5.1.26是否已经修复。 版本中5.1.16中在RowDataDynamic的close()方法也同样调用了realClose,里面并没有调用ConnectionImpl来操作,而是直接用本地的一个ResultSet将其关闭掉了: 另外,需要注意的是,其实StackOverflow往往没有平时Demo演示的那么简单,往往经过复杂的嵌套逻辑,以及希望大量的代码复用,在一些偶然的逻辑结构下导致递归起来,而且这种偶然一旦发生可能就形成一种必然了。 最后,小伙伴们不要认为最新的东西就是最好的哦,哈哈!这几天经过验证,可以很容易重现,测试了5.1.6、5.1.16、5.1.26、5.1.27(最新)全部会抛出错误,不过5.1.16以前的StackOverflowError代码路径不同而已。 测试的顺序: 1、在创建的Statement中,使用:((StatementImpl)statement).enableStreamingResults() 2、在发生close动作之前(可以使用断点或其它某种方式),将服务器端对应的session kill,或者将网络断开,或者直接server重启,相信这种操作线上发生是很正常的,不是故意用变态场景来模拟。 3、调用close方法,立即触发,小伙伴们可以自己模拟哈。 【更新于2015-12-18】: 官方邮件回复已经解决该问题,fix bug的版本为5.1.28,经过验证已经OK,并将同样的程序重现在5.1.27上会出现StackOverflowError。 不过值得注意的是,在5.1.28、5.1.29两个版本中,在连接断开和会话被kill的情况下,如果调用close方法会抛出异常: com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after statement closed. 官方后来觉得对于close方法,没有必要抛出这样的异常,因为连接关闭了就关闭了,因为本来这个动作就是做关闭的,确实也应该是这样的,即使官方抛出异常,我们最多打个没用的log,然后忽略掉。所以在5.1.30及以后的版本中,在上述场景调用close方法时是不会抛出异常的。
几个月前,提到了《今年-计划写一本java方面的书籍》,目前初稿已经完成,字数为50W字左右,现在已经正式进入修订阶段,不过出版估计需要一段时间,因为出版社有他们的安排和流程。 章节有所变化,从23章压缩为21章,第一篇的6个章节讲解一些基础技术,是本书的重点,占了半本书的内容,接着会讲解一些源码、设计、实现、和其他的内容。 本书主体依然希望帮助工作时间不长,但渴望成长的人,也许会有所迷茫,也许有所困惑,希望别人给点支撑力,也许这本书里面能找到一些内容。不适合于牛人,不适合于做客户端程序的童鞋,当然更加不适合于对自己绝望和对世界绝望的人。 本书主旨不变,会讲解一些技术,但是从小技术点会参杂大量的对话、探讨,或许有些时候会带点小趣味,重点在于基础功底、思想、方法、心态等等内容。希望本书带给的不是那么多的技术的知识点,而是未来成长的方法、现有的一些技术上的初步引导、同时希望你对自己的发展充满信心。 编写的风格也不变,在很多地方不是那么严谨,有许多武侠、调侃等等,重点在于让你理解大致的思路和方法,希望在许多的技术点上做一些抛钻引玉,而不是深入浅出,希望你能在阅读相关的内容后,能在工作中不断去实践和思考,然后再去看一些牛人秘籍也许会轻松一些。 同样,本书不是牛人秘籍,无法助你从小菜到牛人,更加无法让你从牛人变成大师,任何人都需要经历和过程才能得到真正的成长,而本书希望你的在遇到困难的时候,不是陷入混沌让自己的思维打圈圈,更加不是让自己放弃,而是希望你能建立自己的一些方法。 第一次写书,确实很费劲,尤其是在炎热的夏天,中途几次写得想吐,甚至于想要放弃,不过还是坚持过来了。 干这事确实不是一件容易的事情,不过终于将初稿写完了,接下来的修订工作应该要轻松得多了。由于在一边工作一边写这本书,书中的绝大部分内容都是用键盘敲打出来的,许多图也是逐个画出来的(虽然画得很丑,哈哈),所以时间上比原计划稍微晚一点,而且这里正好接近年底,出版社可能也比较忙,书籍的正式出版估计在修订后还得一段时间。 希望本书带给的不是那么多的技术的知识点,而是未来成长的方法、现有的一些技术上的初步引导、同时希望你对自己的发展充满信心。
关于预编译(PrepareStatement),对于所有的JDBC驱动程序来讲,有一个共同的功能,就是“防止SQL注入”,类似Oracle还有一种“软解析”的概念,它非常适合应用于OLTP类型的系统中。 在JDBC常见的操作框架中,例如ibatis、jdbcTemplate这些框架对JDBC操作时,默认会走预编译(jdbcTemplate如果没有传递参数,则会走createStatement),这貌似没有什么问题。不过在一个应用中发现了大量的预编译对象导致频繁GC,于是进行了源码上的一些跟踪,写下这篇文章,这里分别从提到的几个参数,以及源码中如何应用这几个参数来说明。 看看有那些参数: MySQL JDBC是通过其Driver的connenct方法获取到连接,然后可以将连接参数设置在JDBC URL或者Properties中,它会根据这些参数来创建一个Connection,简单说来就是将这些参数解析为K-V结构,交给Connection的对象来解析,Connection会将它们解析为自己所能识别的许多属性中,这个属性的类型为:ConnectionProperty,当然有许多子类来实现不同的类型,例如:BooleanConnectionProperty、IntegerConnectionProperty是处理不同参数类型的。 这些参数会保存在Connection对象中(在源码中,早期的版本,源码的类名就叫:com.mysql.jdbc.Connection,新版本的叫做:com.mysql.jdbc.ConnectionImpl,抽象了接口与实现类,这里统一称Connection的对象);具体是保存在这个Connection的父类中,这里将几个与本题相关的几个参截取出来,如下所示: private BooleanConnectionProperty cachePreparedStatements = new BooleanConnectionProperty( "cachePrepStmts", //$NON-NLS-1$ false, Messages.getString("ConnectionProperties.cachePrepStmts"), //$NON-NLS-1$ "3.0.10", PERFORMANCE_CATEGORY, Integer.MIN_VALUE); //$NON-NLS-1$ private IntegerConnectionProperty preparedStatementCacheSize = new IntegerConnectionProperty( "prepStmtCacheSize", 25, 0, Integer.MAX_VALUE, //$NON-NLS-1$ Messages.getString("ConnectionProperties.prepStmtCacheSize"), //$NON-NLS-1$ "3.0.10", PERFORMANCE_CATEGORY, 10); //$NON-NLS-1$ private IntegerConnectionProperty preparedStatementCacheSqlLimit = new IntegerConnectionProperty( "prepStmtCacheSqlLimit", //$NON-NLS-1$ 256, 1, Integer.MAX_VALUE, Messages.getString("ConnectionProperties.prepStmtCacheSqlLimit"), //$NON-NLS-1$ "3.0.10", PERFORMANCE_CATEGORY, 11); //$NON-NLS-1$ private BooleanConnectionProperty detectServerPreparedStmts = new BooleanConnectionProperty( "useServerPrepStmts", //$NON-NLS-1$ false, Messages.getString("ConnectionProperties.useServerPrepStmts"), //$NON-NLS-1$ "3.1.0", MISC_CATEGORY, Integer.MIN_VALUE); //$NON-NLS-1$ 找到这个通常要看看获取它的方法名,显然实际执行的时候,一般用方法来获取,而且这里的类型是private,也就是子类不可见,直接访问如果不通过变通手段访问不到;也许我们搞Java的第一眼看到的就是就是属性名的get方法嘛,有些时候MySQL这个该死的就是不按照常规思路走,例如它对属性:“detectServerPreparedStmts”的获取方法是:“getUseServerPreparedStmts()”,如下图: 好吧,不关注它的屌丝做法了,来继续关注正题。 来看看PrepareStatement初始化与编译过程: 要预编译,自然是通过Connection去做的,默认调用的预编译参数是这样一个方法: public java.sql.PreparedStatement prepareStatement(String sql) throws SQLException { return prepareStatement(sql, java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY); } 这个方法貌似还看不出什么东西,但是可以稍微留意下发现默认值是什么,继续往下走,走到一个重载方法中,这个重载方法body部分太长了,看起来费劲,说起来难,经过梳理,我将它简化一下,如下图所示: 这里将逻辑分解为两个大板块:一个为com.mysql.jdbc.ServerPreparedStatement,一个是默认的,反过来讲就是如果是服务器端的Statement,处理类的类名一眼就能看出来。 那么什么时候会走服务器端的PrepareStatement呢?服务器端的PrepareStatement与普通的到底有什么区别呢?先看第一个问题,以下几条代码是进入逻辑的关键: boolean canServerPrepare = true; String nativeSql = getProcessEscapeCodesForPrepStmts() ? nativeSQL(sql): sql; if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) { canServerPrepare = canHandleAsServerPreparedStatement(nativeSql); } if (this.useServerPreparedStmts && canServerPrepare) { ....使用ServerPrepareStatement } 也就是判定逻辑是基于“useServerPreparedStmts”、“canServerPrepare”这两个参数决定的,而“useServerPreparedStmts”我们可以将对应的参数设置为true即可,参数对应到那里呢?在第一个参数列表图中,就对应到:“detectServerPreparedStmts”,而在JDBC URL上需要设置的是:“useServerPrepStmts”,定义如 private BooleanConnectionProperty detectServerPreparedStmts = new BooleanConnectionProperty( "useServerPrepStmts", //$NON-NLS-1$ false, Messages.getString("ConnectionProperties.useServerPrepStmts"), //$NON-NLS-1$ "3.1.0", MISC_CATEGORY, Integer.MIN_VALUE); //$NON-NLS-1$ 而另一个参数canServerPrepare并非默认,它虽然被初始化设置了true,但是getEmulateUnsupportedPstmts()这个方法跟踪进去也会发现默认是true(当然可以通过设置参数将其设置为false),对应到代码中,参数canServerPrepare的值将由方法:canHandleAsServerPreparedStatement(String)来决定,跟踪进去会发现,首先只考虑“SELECT、UPDATE、DELETE、INSERT、REPLACE”几种语法规则,也就是如果不是这几种就直接返回false了。另外会对参数Limit后面7位做一个判定是否有逗号、?这些符号,如果有这些就返回false了,对于这7位一直很纳闷,因为LIMIT后面7位最多包含一个占位符,而分页最少2个。 这里说明这些就只想说明,“并不一定将useServerPrepStmts设置为true,就一定会采用服务器端的PrepareStatement”;这假设已经采用了服务器端的,它做了什么呢? pStmt = ServerPreparedStatement.getInstance(this, nativeSql, this.database, resultSetType, resultSetConcurrency); 这个是代码中的关键,跟踪进去,你会发现它这个动作,会向服务器端发送SQL,很明显的,这里还没有执行SQL,只是预编译,就已经将SQL交给服务器端,那么后面只需要拿到相应的状态标识给服务器端参数即可。 另外,这个里面还有一层是:getCachePreparedStatements(),这个参数就是对应到上图中设置的“cachePrepStmts”,它的定义如下所示: private BooleanConnectionProperty cachePreparedStatements = new BooleanConnectionProperty( "cachePrepStmts", //$NON-NLS-1$ false, Messages.getString("ConnectionProperties.cachePrepStmts"), //$NON-NLS-1$ "3.0.10", PERFORMANCE_CATEGORY, Integer.MIN_VALUE); //$NON-NLS-1$ 它将首先预判定是否将SQL cache到一个内存区域中,然后再内部创建ServerPrepareStatement,如果创建失败则也调用client的,并且在失败的时候put到serverSideStatementCheckCache这个里面(这里可以看到出来是基于SQL的K-V结构,K肯定是SQL了,Value等下来看),成功的值发现做了一个: if (sql.length() < getPreparedStatementCacheSqlLimit()) { ((com.mysql.jdbc.ServerPreparedStatement)pStmt).isCached = true; } 这个判定语句很明显是判定SQL长度的,也就是SQL长度低于某个值就设置这个参数,这个getPreparedStatementCacheSqlLimit()就是来自第一个图中的:preparedStatementCacheSqlLimit参数,JDBC URL参数是:prepStmtCacheSqlLimit,它的默认值是256,如下所示: private IntegerConnectionProperty preparedStatementCacheSqlLimit = new IntegerConnectionProperty( "prepStmtCacheSqlLimit", //$NON-NLS-1$ 256, 1, Integer.MAX_VALUE, Messages.getString("ConnectionProperties.prepStmtCacheSqlLimit"), //$NON-NLS-1$ "3.0.10", PERFORMANCE_CATEGORY, 11); //$NON-NLS-1$ 但是这个isCache仅仅是设置一个boolean值,那里做了cache呢?没有简单做任何cache,仅仅看到是失败的会cache,它到底在哪里有用呢,跟踪到内部会在Statement发生close的时候有用: public synchronized void close() throws SQLException { if (this.isCached && !this.isClosed) { clearParameters(); this.isClosed = true; this.connection.recachePreparedStatement(this); return; } realClose(true, true); } 这个:recachePreparedStatement()方法最终也会调用:serverSideStatementCache来讲编译信息设置进去,也就是这个cache始终在客户端,而服务器端PrepareStatement只是代表了谁来编译这个SQL语句的问题。 也许对clientPrepareStatement感兴趣,就去看看它的代码,同样这个代码很长,我也简单简化了下逻辑如下图所示: 这个逻辑基本与ServerPrepareStatement内部的逻辑差不多,唯一的区别就是这个是显式做了LRU算法,而这个LRU是一是一种最简单的最近最久未使用方式,将最后一个删掉,将现在这个写进去,它同样也有getCachePreparedStatements()、getPreparedStatementCacheSqlLimit()来控制是否做cache操作,也同样用了一个K-V结构来做cache,这个K-V结构,通过Connection的初始化方法:initializeDriverProperties(Properties)间接调用:createPreparedStatementCaches()完成初始化,可以看到他会被初始化为一个HashMap结构,较早的版本会创建多个类似大小的对象出来。 好了,现在来看问题,一个HashMap不足以造成多少问题,因为有LRU队列来控制长度,但是看代码中你会发现它没控制并行处理,HashMap是非线程安全的,那么为啥MySQL JDBC没出问题呢?因为你会发现这个HashMap完全绑定到Connection对象上,成为Connection对象的一个属性,连接池分配的时候没见过会将一个Connection同时分配给两个请求的,因此它将并发的问题交给了连接池来解决,自己认为线程都是安全的,反过来,如果你自己去并行同一个Connection可能会有问题。 继续回到问题上来,每个Connection都可能cache几十个上百个Statement对象,那么一个按照线上数据源的配置,也就配置5~10个是算比较大的了,也就最多上千个对象,JVM配置都是多少G的空间,几千个对象能造成什么问题? 于是我们来看他cache了什么,主要是普通的PrepareStatement,里面的代码发现编译完后返回了一个ParseInfo类型对象,然后将它作为Value写入到HashMap中,它是一个PrepareStatement的内部类,它的定义如下所示: class ParseInfo { char firstStmtChar = 0; boolean foundLimitClause = false; boolean foundLoadData = false; long lastUsed = 0; int statementLength = 0; int statementStartPos = 0; byte[][] staticSql = null; } 我们可以搬着手指头算下,对象头部、属性、padding大致占用的空间(当然是在64bit),发现也不大,而最关键的是这个二维数组,byte[][]staticSql,它占用多大,经过代码跟踪我们发现它与占位符的个数相关,也就是参数中的“?”个数,这个个数将决定第一维的大小,而SQL中的每个字节将填写到数组的第二维。 Java中没有绝对的二维数组,都是通过一维数组虚拟出来的,而第一维本身也是一个引用数组,占用的空间自然很大,参数个数自然和业务表相关,至少会有“增、删、改、查”,查和删其实占位符较少,而相应的业务系统写操作是十分多的,因此参数个数用15~20个来估算不算过分,而SQL长度用200来估算也不过分,通过简单估算,这个空间将会是原来SQL的2~3倍甚至于更多,但是也不至于有问题呀? 再回头看看,一个HashMap里面的Key、Value、next、hash几个会形成一个新的对象,而Key是SQL,自然会占用SQL的空间大小,Vaue是好几倍的SQL空间,其余的再抛开HashMap本身数组的利用率极低外,这里可能SQL的宽度会上K的占用,不过算起来还是不对,因为就算是1000K,也只有1m,再放大几倍也只有几M的空间。 想不通了,后来一个小情况得到了提醒,那就是数据库是分布式的,分布式数据库的连接池配置底层会针对每一个访问过的数据库建立初始化大小的连接数,那么自然的,这个数据应当乘以数据库的个数,该应用存在上百个数据库,那么自然的1M到几M的空间,就上升到一百到几百M,不过也不至于有这么大的问题,因为基本内存都用G来衡量的,再细探,数据库还存在读写分流,也就是部分流量会分配到备库上,而一个数据库会有多个备库,自然的读流量只要访问过也会在备库上建立同样的Connection,即使你用得不多,那么自然的空间还要乘以一套库的个数,例如乘以4,那么这个空间就完全有可能占用得非常大,理论上这些数据就是这样来的了。 回头再来看看ParseInfo到底在什么时候用,普通的prepareStatement(即客户端的),到底是怎么与服务器端通信的,我们用一个常见的executeQuery查询语句来看代码,它内部通过一个叫:Buffer sendPacket = fillSendPacket();这个方法获取到要与MySQL服务器端通信的package的Buffer,它的代码是这样的: protected Buffer fillSendPacket() throws SQLException { return fillSendPacket(this.parameterValues, this.parameterStreams, this.isStream, this.streamLengths); } 发现又调用了一个该死的重载方法,但是知道了传入的是参数列表parameterValues,而重载方法中,这个方法入口参数的名字变成了:batchedParameterStrings,说明重载方法是兼容批处理的,只是单个语句传入的参数可能在里面只循环一次而,跟踪进去,发现一段很重要的循环的地方是这样的: for (int i = 0; i < batchedParameterStrings.length; i++) { if ((batchedParameterStrings[i] == null) && (batchedParameterStreams[i] == null)) { throw SQLError.createSQLException(Messages .getString("PreparedStatement.40") //$NON-NLS-1$ + (i + 1), SQLError.SQL_STATE_WRONG_NO_OF_PARAMETERS); } sendPacket.writeBytesNoNull(this.staticSqlStrings[i]); if (batchedIsStream[i]) { streamToBytes(sendPacket, batchedParameterStreams[i], true, batchedStreamLengths[i], useStreamLengths); } else { sendPacket.writeBytesNoNull(batchedParameterStrings[i]); } } 这个循环看到每次都会将staticSqlStrings拼接一次,然后再拼接一个参数,这个就是一个byte[][]格式,而它的赋值就是来源于ParseInfo,在方法:PrepareStatement中的initializeFromParseInfo()中有相应的说明。 也就是说他用的就是ParseInfo中的内容,而那个内容分析过,与占位符相关,其实就是将SQL从占位符的位置拆分开,然后实际运行时,再通过实际的参数拼接起来,这个就是文本协议,虽然它是预编译,但是它也是拼接SQL出来的。 此时我们很好奇的问题,既然都是拼接SQL,它如何防止SQL注入呢?那么自然是看看setString方法到底干了啥,一下是它的源码: public void setString(int parameterIndex, String x) throws SQLException { // if the passed string is null, then set this column to null if (x == null) { setNull(parameterIndex, Types.CHAR); } else { checkClosed(); int stringLength = x.length(); if (this.connection.isNoBackslashEscapesSet()) { // Scan for any nasty chars boolean needsHexEscape = isEscapeNeededForString(x, stringLength); if (!needsHexEscape) { byte[] parameterAsBytes = null; StringBuffer quotedString = new StringBuffer(x.length() + 2); quotedString.append('\''); quotedString.append(x); quotedString.append('\''); if (!this.isLoadDataQuery) { parameterAsBytes = StringUtils.getBytes(quotedString.toString(), this.charConverter, this.charEncoding, this.connection.getServerCharacterEncoding(), this.connection.parserKnowsUnicode()); } else { // Send with platform character encoding parameterAsBytes = quotedString.toString().getBytes(); } setInternal(parameterIndex, parameterAsBytes); } else { byte[] parameterAsBytes = null; if (!this.isLoadDataQuery) { parameterAsBytes = StringUtils.getBytes(x, this.charConverter, this.charEncoding, this.connection.getServerCharacterEncoding(), this.connection.parserKnowsUnicode()); } else { // Send with platform character encoding parameterAsBytes = x.getBytes(); } setBytes(parameterIndex, parameterAsBytes); } return; } String parameterAsString = x; boolean needsQuoted = true; if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) { needsQuoted = false; // saves an allocation later StringBuffer buf = new StringBuffer((int) (x.length() * 1.1)); buf.append('\''); // // Note: buf.append(char) is _faster_ than // appending in blocks, because the block // append requires a System.arraycopy().... // go figure... // for (int i = 0; i < stringLength; ++i) { char c = x.charAt(i); switch (c) { case 0: /* Must be escaped for 'mysql' */ buf.append('\\'); buf.append('0'); break; case '\n': /* Must be escaped for logs */ buf.append('\\'); buf.append('n'); break; case '\r': buf.append('\\'); buf.append('r'); break; case '\\': buf.append('\\'); buf.append('\\'); break; case '\'': buf.append('\\'); buf.append('\''); break; case '"': /* Better safe than sorry */ if (this.usingAnsiMode) { buf.append('\\'); } buf.append('"'); break; case '\032': /* This gives problems on Win32 */ buf.append('\\'); buf.append('Z'); break; default: buf.append(c); } } buf.append('\''); parameterAsString = buf.toString(); } byte[] parameterAsBytes = null; if (!this.isLoadDataQuery) { if (needsQuoted) { parameterAsBytes = StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charConverter, this.charEncoding, this.connection .getServerCharacterEncoding(), this.connection .parserKnowsUnicode()); } else { parameterAsBytes = StringUtils.getBytes(parameterAsString, this.charConverter, this.charEncoding, this.connection .getServerCharacterEncoding(), this.connection .parserKnowsUnicode()); } } else { // Send with platform character encoding parameterAsBytes = parameterAsString.getBytes(); } setInternal(parameterIndex, parameterAsBytes); this.parameterTypes[parameterIndex - 1 + getParameterIndexOffset()] = Types.VARCHAR; } } 可以发现,它将传入的参数,进行了特殊字符的转义处理,另外就是在字符串的两边加上了单引号,也就是这与MySQL将SQL转义后传送给服务器端的东西,也就是最终传送的不是分解SQL与参数,而是拼接SQL,只是通过转义防止SQL注入。 在MySQL JDBC中,其实还有许多类似的伪转换,例如批处理,它使用循环来完成的,不过它也算满足了JDBC驱动的基本规范。 另外,在MySQL分布式数据库上,分表是非常多的,每个物理分表都会有至少好几个SQL,即使每个库下面也会有许多,那么配置几十个cache,它的命中率到底有多少呢?而即便是一个库下面的多个Connection,他们的cache都是彼此独立的,意味着库越多、同一个库下面的表越多、业务逻辑越复杂,这样一个Connection需要多少cache才能达到想要的效果呢?而cache后的结果是占用更多的JVM空间,而且是许多的JVM空间,即使内存可以放得下,在现在的JVM中,只要做发生FULL GC也会去扫描它们、移动它们。但是反过来,解析这个SQL语句只是解析出占位符,纯CPU密集型,而且次数相对CPU来讲就是小儿科,一个普通SQL可能就是1us的时间,我们没有必要跟JVM过不去,做费力不讨好的事情,因为本身就很土鳖了,再土点不就完蛋了吗。
我来做一回技术控,这部分内容也是简单的API调用例子而已,做一回技术控,发点小骚文,不过你看了,也许知道JConsole是怎么做出来的了,呵呵! 先不用管他干什么,代码运行后,自己改改自然知道做什么的。 例子全部应该都可以运行,使用者,拷贝回去就基本可以用了,无需其他内容的支持,有部分代码对JDK的版本有要求,例如在使用:ThreadMXBean.getThreadAllocatedBytes(id),这个是在JDK 6 update 25中开始支持的,而且在JDK 1.6中获取出来还有问题不少。 我先写了个简单的工具类: import java.io.IOException; import java.lang.management.ManagementFactory; import java.util.Set; import javax.management.AttributeNotFoundException; import javax.management.BadAttributeValueExpException; import javax.management.BadBinaryOpValueExpException; import javax.management.BadStringOperationException; import javax.management.InstanceNotFoundException; import javax.management.IntrospectionException; import javax.management.InvalidApplicationException; import javax.management.MBeanAttributeInfo; import javax.management.MBeanException; import javax.management.MBeanInfo; import javax.management.MBeanServer; import javax.management.MBeanServerConnection; import javax.management.MalformedObjectNameException; import javax.management.ObjectInstance; import javax.management.ObjectName; import javax.management.QueryExp; import javax.management.ReflectionException; import javax.management.RuntimeMBeanException; public class JMXUtils { private final static MBeanServer DEFAULT_MBEAN_SERVER = ManagementFactory .getPlatformMBeanServer(); public static long getYongGC() { return getYoungGC(DEFAULT_MBEAN_SERVER); } public static long getFullGC() { return getFullGC(DEFAULT_MBEAN_SERVER); } public static long findLoadedClass() { return findLoadedClass(DEFAULT_MBEAN_SERVER); } public static long getYoungGC(MBeanServerConnection mbeanServer) { try { ObjectName objectName; if (mbeanServer.isRegistered(new ObjectName("java.lang:type=GarbageCollector,name=ParNew"))) { objectName = new ObjectName("java.lang:type=GarbageCollector,name=ParNew"); } else if (mbeanServer.isRegistered(new ObjectName("java.lang:type=GarbageCollector,name=Copy"))) { objectName = new ObjectName("java.lang:type=GarbageCollector,name=Copy"); } else { objectName = new ObjectName("java.lang:type=GarbageCollector,name=PS Scavenge"); } return (Long) mbeanServer.getAttribute(objectName , "CollectionCount"); } catch (Exception e) { throw new RuntimeException(e); } } public static long getFullGC(MBeanServerConnection mbeanServer) { try { ObjectName objectName; if (mbeanServer.isRegistered(new ObjectName("java.lang:type=GarbageCollector,name=ConcurrentMarkSweep"))) { objectName = new ObjectName("java.lang:type=GarbageCollector,name=ConcurrentMarkSweep"); } else if (mbeanServer.isRegistered(new ObjectName("java.lang:type=GarbageCollector,name=MarkSweepCompact"))) { objectName = new ObjectName("java.lang:type=GarbageCollector,name=MarkSweepCompact"); } else { objectName = new ObjectName("java.lang:type=GarbageCollector,name=PS MarkSweep"); } return (Long) mbeanServer.getAttribute(objectName , "CollectionCount"); } catch (Exception e) { throw new RuntimeException(e); } } public static long findLoadedClass(MBeanServerConnection mBeanServer) { try { return (Long) (mBeanServer.getAttribute(new ObjectName( "java.lang:type=ClassLoading"), "TotalLoadedClassCount")); } catch (Exception e) { throw new RuntimeException(e); } } public static void traceOneDomain(String doMain, MBeanServerConnection mBeanServer) throws MalformedObjectNameException, IntrospectionException, InstanceNotFoundException, AttributeNotFoundException, ReflectionException, MBeanException, IOException { Set<ObjectInstance> set = mBeanServer.queryMBeans(new ObjectName(doMain + ":*"), new QueryExp() { private static final long serialVersionUID = 1L; @Override public boolean apply(ObjectName name) throws BadStringOperationException, BadBinaryOpValueExpException, BadAttributeValueExpException, InvalidApplicationException { return true; } @Override public void setMBeanServer(MBeanServer s) {} }); for (ObjectInstance objectInstance : set) { System.out.println("\t\t\t" + objectInstance.getObjectName() + "\t" + objectInstance.getClassName()); traceMebeanInfo(mBeanServer, objectInstance.getObjectName()); } } public static void traceMebeanInfo(MBeanServerConnection mBeanServer, ObjectName objectName) throws IntrospectionException, InstanceNotFoundException, MalformedObjectNameException, ReflectionException, AttributeNotFoundException, MBeanException, IOException { MBeanInfo mBeanInfo = mBeanServer.getMBeanInfo(objectName); MBeanAttributeInfo[] mBeanAttributes = mBeanInfo.getAttributes(); System.out.println("\t\t\tMBeanInfos : "); for (MBeanAttributeInfo mBeanAttribute : mBeanAttributes) { try { System.out.println("\t\t\t\t\t" + mBeanAttribute.getName() + "\t" + mBeanAttribute.getType() + "\tvalue = >" + mBeanServer.getAttribute(objectName, mBeanAttribute.getName())); } catch (RuntimeMBeanException e) { if (e.getCause() instanceof UnsupportedOperationException) { System.out.println("\t\t\t\t\t" + mBeanAttribute.getName() + "\t" + mBeanAttribute.getType() + "\tvalue = > value not supported"); } } } } public static void traceAll(MBeanServerConnection mBeanServer) throws MalformedObjectNameException, IntrospectionException, InstanceNotFoundException, AttributeNotFoundException, ReflectionException, MBeanException, IOException { System.out.println("MBean count = " + mBeanServer.getMBeanCount()); String[] domains = mBeanServer.getDomains(); for (String domain : domains) { System.out.println("\tbegin trace domain -> " + domain); traceOneDomain(domain, mBeanServer); } } } 这个类写好后,我们就可以写点小测试代码了,呵呵! 那么首先来写个遍历所有的参数列表,层次结构为3层: MBeanServer -> DoMain -> MBean -> MBeanAttributeInfo 但是写好那个Util后调用就简单了: import java.io.IOException; import java.lang.management.ManagementFactory; import javax.management.AttributeNotFoundException; import javax.management.InstanceNotFoundException; import javax.management.IntrospectionException; import javax.management.MBeanException; import javax.management.MalformedObjectNameException; import javax.management.ReflectionException; public class MBeanServerTest { public static void main(String[] args) throws MalformedObjectNameException, IntrospectionException, InstanceNotFoundException, AttributeNotFoundException, ReflectionException, MBeanException, IOException { // System.gc(); // System.out.println(JMXUtils.getYongGC()); // System.out.println(JMXUtils.getFullGC()); JMXUtils.traceAll(ManagementFactory.getPlatformMBeanServer()); } } 结果很多,不同的OS,不同的JVM配置,结果也会有所区别,大家可以自己书出来看看,这里注意里面的ObjectName,通过MBeanServer直接获取里面的参数值,比较好用,尤其是RMI的时候,这个trace不太好用,因为远程可能有些类,本地没有,但是参数值是可以获取到的,所以需要知道名称是比较好的。 远程的输出,我们简单来写一段代码: import java.io.IOException; import java.util.HashMap; import javax.management.AttributeNotFoundException; import javax.management.InstanceNotFoundException; import javax.management.IntrospectionException; import javax.management.MBeanException; import javax.management.MBeanServerConnection; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import javax.management.ReflectionException; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; public class MBeanServerRemoteTest { /** * 远程地址需要开启: * -Dcom.sun.management.jmxremote * -Dcom.sun.management.jmxremote.port=9000 * -Dcom.sun.management.jmxremote.authenticate=true */ final static String RMI_URL = "service:jmx:rmi:///jndi/rmi://xxx.xxx.xxx.xxx:9000/jmxrmi"; public static void main(String []args) throws IOException, MalformedObjectNameException, IntrospectionException, InstanceNotFoundException, AttributeNotFoundException, ReflectionException, MBeanException { JMXServiceURL serviceURL = new JMXServiceURL(RMI_URL); JMXConnector jmxc = JMXConnectorFactory.connect(serviceURL , new HashMap<String , Object>() { private static final long serialVersionUID = 1L; { put(JMXConnector.CREDENTIALS , new String[] {"controlRole" , "FUCK"}); } }); MBeanServerConnection mBeanServer = jmxc.getMBeanServerConnection(); System.out.println("MBean count = " + mBeanServer.getMBeanCount()); String[] domains = mBeanServer.getDomains(); for (String doMain : domains) { System.out.println("============>" + doMain); } System.out.println(JMXUtils.getYoungGC(mBeanServer)); System.out.println(JMXUtils.getFullGC(mBeanServer)); System.out.println(mBeanServer.getAttribute(new ObjectName("JMImplementation:type=MBeanServerDelegate"), "ImplementationVersion")); System.out.println(mBeanServer.getAttribute(new ObjectName("java.lang:type=Runtime"), "BootClassPath")); //其余的可以自己遍历出来 } } 这两个出来了,其实JMX还提供了些简单的,默认的MXBean,可以直接使用,我们简单也写下这些demo,可以拿去自己玩哈。 ===》ClassLoadingMXBean: import java.lang.management.ClassLoadingMXBean; import java.lang.management.ManagementFactory; public class ClassLoadingMXBeanTest { public static void main(String []args) { ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean(); System.out.println(classLoadingMXBean.getLoadedClassCount()); System.out.println(classLoadingMXBean.getTotalLoadedClassCount()); System.out.println(classLoadingMXBean.getUnloadedClassCount()); System.out.println(classLoadingMXBean.isVerbose()); } } ===》CompilationMXBean import java.lang.management.CompilationMXBean; import java.lang.management.ManagementFactory; public class CompilationMXBeanTest { public static void main(String []args) { CompilationMXBean mxBean = ManagementFactory.getCompilationMXBean(); System.out.println(mxBean.getTotalCompilationTime()); System.out.println(mxBean.getName()); System.out.println(mxBean.isCompilationTimeMonitoringSupported()); } } ===》MemoryManagerMXBean import java.lang.management.ManagementFactory; import java.lang.management.MemoryManagerMXBean; import java.util.List; import javax.management.AttributeNotFoundException; import javax.management.InstanceNotFoundException; import javax.management.IntrospectionException; import javax.management.MBeanAttributeInfo; import javax.management.MBeanException; import javax.management.MBeanInfo; import javax.management.MBeanServer; import javax.management.ReflectionException; public class MemoryManagerMXBeanTest { public static void main(String []args) throws IntrospectionException, InstanceNotFoundException, ReflectionException, AttributeNotFoundException, MBeanException { MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); System.gc(); List<MemoryManagerMXBean> list = ManagementFactory.getMemoryManagerMXBeans(); for(MemoryManagerMXBean memoryManagerMXBean : list) { System.out.println(memoryManagerMXBean.getName()); System.out.println(memoryManagerMXBean.getObjectName()); MBeanInfo mBeanInfo = mBeanServer.getMBeanInfo(memoryManagerMXBean.getObjectName()); MBeanAttributeInfo[] mBeanAttributes = mBeanInfo.getAttributes(); for(MBeanAttributeInfo mBeanAttribute : mBeanAttributes) { System.out.println("=============>" + mBeanAttribute.getName() + "\t" + mBeanAttribute.getType()); System.out.println("=============value = >" + mBeanServer.getAttribute(memoryManagerMXBean.getObjectName(), mBeanAttribute.getName())); } /*String []poolNames = memoryManagerMXBean.getMemoryPoolNames(); for(String poolName : poolNames) { System.out.println("\t" + poolName); }*/ } } } ===》MemoryMXBeanimport java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; public class MemoryMXBeanTest { public static void main(String []args) { MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); //memoryMXBean.gc(); System.out.println(memoryMXBean.getHeapMemoryUsage()); System.out.println(memoryMXBean.getObjectPendingFinalizationCount()); System.out.println(memoryMXBean.getNonHeapMemoryUsage()); } } ===》MemoryPoolMXBean import java.lang.management.ManagementFactory; import java.lang.management.MemoryPoolMXBean; import java.util.List; public class MemoryPoolMXBeanTest { public static void main(String[] args) { List<MemoryPoolMXBean> list = ManagementFactory.getMemoryPoolMXBeans(); for (MemoryPoolMXBean memoryPoolMXBean : list) { System.out.println(memoryPoolMXBean.getName() + memoryPoolMXBean.getCollectionUsage() + "\n\nPeakUsage:\t\t" + memoryPoolMXBean.getPeakUsage() + "\n\nUsage:\t\t" + memoryPoolMXBean.getUsage()); /* * + memoryPoolMXBean.getUsageThreshold() + "\t" + * memoryPoolMXBean.getUsageThresholdCount() + * "\t\n\nCollectionUsage:\t\t" * * + memoryPoolMXBean.getType() + "\t" */ // memoryPoolMXBean.getCollectionUsageThreshold() + "\t" // memoryPoolMXBean.getCollectionUsageThresholdCount() + "\t" ); // String []memoryManagerNames = // memoryPoolMXBean.getMemoryManagerNames(); /* * for(String memoryManagerName : memoryManagerNames) { * System.out.println("\t\t\t\t" + memoryManagerName); } */ } } } ===》OperatingSystemMXBean import java.lang.management.ManagementFactory; import java.lang.management.OperatingSystemMXBean; public class OperatingSystemMXBeanTest { public static void main(String []args) { OperatingSystemMXBean mxBean = ManagementFactory.getOperatingSystemMXBean(); System.out.println(mxBean.getArch()); System.out.println(mxBean.getAvailableProcessors()); System.out.println(mxBean.getName()); System.out.println(mxBean.getSystemLoadAverage()); System.out.println(mxBean.getVersion()); } } ===》RuntimeMXBean import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; public class RuntimeMXBeanTest { public static void main(String []args) { RuntimeMXBean runTimeMXBean = ManagementFactory.getRuntimeMXBean(); System.out.println(runTimeMXBean.getBootClassPath()); System.out.println(runTimeMXBean.getClassPath()); System.out.println(runTimeMXBean.getLibraryPath()); System.out.println(runTimeMXBean.getManagementSpecVersion()); System.out.println(runTimeMXBean.getName()); System.out.println(runTimeMXBean.getSpecName()); System.out.println(runTimeMXBean.getSpecVendor()); System.out.println(runTimeMXBean.getStartTime()); System.out.println(runTimeMXBean.getUptime()); System.out.println(runTimeMXBean.getVmName()); System.out.println(runTimeMXBean.getVmVendor()); System.out.println(runTimeMXBean.getVmVersion()); System.out.println(runTimeMXBean.getInputArguments()); System.out.println(runTimeMXBean.getSystemProperties()); } } ===》ThreadMXBean import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import com.sun.management.ThreadMXBean; //import sun.management.VMOptionCompositeData; //import com.sun.management.VMOption; //import java.lang.management.ManagementFactory; public class ThreadMXBeanTest { public static void main(String []args) { ThreadMXBean thredMXBean = (ThreadMXBean) ManagementFactory.getThreadMXBean(); long []ids = thredMXBean.getAllThreadIds(); for(long id : ids) { System.out.println(id + "\t" + thredMXBean.getThreadAllocatedBytes(id) + "\t" + thredMXBean.getThreadInfo(id)); } System.out.println(thredMXBean.getCurrentThreadCpuTime()); System.out.println(thredMXBean.getCurrentThreadUserTime()); System.out.println(thredMXBean.getDaemonThreadCount()); System.out.println(thredMXBean.getPeakThreadCount()); System.out.println(thredMXBean.getThreadCount()); System.out.println(thredMXBean.getTotalStartedThreadCount()); System.out.println("==========================>"); displayThreadInfos(thredMXBean , ids); } private static void displayThreadInfos(ThreadMXBean thredMXBean , long []ids) { ThreadInfo []threadInfos = thredMXBean.getThreadInfo(ids); for(ThreadInfo thread : threadInfos) { System.out.println(thread.getThreadName() + "\t" + thread.getLockOwnerId() + "\t" + thread.getThreadState() + "\t" + thread.getBlockedCount() + "\t" + thread.getBlockedTime() ); } } }
最近因为写书的事情,一段时间没有写博客了,有朋友最近问到了spring加载类的过程,尤其是基于annotation注解的加载过程,有些时候如果由于某些系统部署的问题,加载不到,很是不解!就针对这个问题,我这篇博客说说spring启动过程,用源码来说明,这部分内容也会在书中出现,只是表达方式会稍微有些区别,我将使用spring 3.0的版本来说明(虽然版本有所区别,但是变化并不是特别大),另外,这里会从WEB中使用spring开始,中途会穿插自己通过new ClassPathXmlApplicationContext的区别和联系。 要看这部分源码,其实在spring 3.0以上大家都一般会配置一个Servelet,如下所示: <servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet>当然servlet的名字决定了,你自己获取SpringContext的方式,在前面文章:《spring里头各种获取ApplicationContext的方法》有详细的说明,这里就不细说了,我们就通过DispatcherServlet来说明和跟踪(注意我们这里不说请求转发,就说bean的加载过程),我们知道servlet的规范中,如果load-on-startup被设定了,那么就会被初始化的时候装载,而servlet装载时会调用其init()方法,那么自然是调用DispatcherServlet的init方法,通过源码一看,竟然没有,但是并不带表真的没有,你会发现在父类的父类中:org.springframework.web.servlet.HttpServletBean有这个方法,如下图所示: public final void init() throws ServletException { if (logger.isDebugEnabled()) { logger.debug("Initializing servlet '" + getServletName() + "'"); } // Set bean properties from init parameters. try { PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader)); initBeanWrapper(bw); bw.setPropertyValues(pvs, true); } catch (BeansException ex) { logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex); throw ex; } // Let subclasses do whatever initialization they like. initServletBean(); if (logger.isDebugEnabled()) { logger.debug("Servlet '" + getServletName() + "' configured successfully"); } }注意代码:initServletBean(); 其余的都和加载bean关系并不是特别大,跟踪进去会发I发现这个方法是在类:org.springframework.web.servlet.FrameworkServlet类中(是DispatcherServlet的父类、HttpServletBean的子类),内部通过调用initWebApplicationContext()来初始化一个WebApplicationContext,源码片段(篇幅所限,不拷贝所有源码,仅仅截取片段) 接下来需要知道的是如何初始化这个context的(按照使用习惯,其实只要得到了ApplicationContext,就得到了bean的信息,所以在初始化ApplicationCotext的时候,就已经初始化好了bean的信息,至少至少,它初始化好了bean的路径,以及描述信息),所以我们一旦知道ApplicationCotext是怎么初始化的,就基本知道bean是如何加载的了。 这里的parent基本不用管,因为Root的ApplicationContext的信息还根本没创建,所以主要是看createWebApplicationContext这个方法,进去后,该方法前面部分,都是在设置一些相关的参数,例如我们需要将WEB容器、以及容器的配置信息设置进去,然后会调用一个refresh()方法,这个方法表面上是用来刷新的,其实也是用来做初始化bean用的,也就是配置修改后,如果你能调用它的这个方法,就可以重新装载spring的信息,我们看看源码中的片段如下(同样,不相关的部分,我们就不贴太多了): 其实这个方法,不论是通过ClassPathXmlApplicationContext还是WEB装载都会调用这里,我们看下ClassPathXmlApplicationContext中调用的部分: 他们的区别在于,web容器中,用servlet装载了,servlet中包装了一个XmlWebApplicationContext而已,而ClassPathXmlApplicationContext是直接调用的,他们共同点是,不论是XmlWebApplicationContext、还是ClassPathXmlApplicationContext都继承了类(间接继承): AbstractApplicationContext,这个类中的refresh()方法是共用的,也就是他们都调用的这个方法来加载bean的,在这个方法中,通过obtainFreshBeanFactory方法来构造beanFactory的,如下图所示: 是不是看到一层调用一层很烦人,其实回过头来想一想,它没一层都有自己的处理动作,毕竟spring不是简单的做一个bean加载,即使是这样,我们最少也需要做xml解析、类装载和实例化的过程,每个步骤可能都有很多需求,因此分离设计,使得代码更加具有扩展性,我们继续来看obtainFreshBeanFactory方法的描述: 这里很多人可能会不太注意refreshBeanFactory()这个方法,尤其是第一遍看这个代码的,如果你忽略掉,你可能会找不到bean在哪里加载的,前面提到了refresh其实可以用以初始化,这里也是这样,refreshBeanFactory如果没有初始化beanFactory就是初始化它了,后面你看到的都是getBeanFactory的代码,也就是已经初始化好了,这个refreshBeanFactory方法类AbstractRefreshableApplicationContext中的方法,它是AbstractApplicationContext的子类,同样不论是XmlWebApplicationContext、还是ClassPathXmlApplicationContext都继承了它,因此都能调用到这个一样的初始化方法,来看看body部分的代码: 注意第一个红圈圈住的地方,是创建了一个beanFactory,然后下面的方法可以通过名称就能看出是“加载bean的定义”,将beanFactory传入,自然要加载到beanFactory中了,createBeanFactory就是实例化一个beanFactory没别的,我们要看的是bean在哪里加载的,现在貌似还没看到重点,继续跟踪 loadBeanDefinitions(DefaultListableBeanFactory)方法 它由AbstractXmlApplicationContext类中的方法实现,web项目中将会由类:XmlWebApplicationContext来实现,其实差不多,主要是看启动文件是在那里而已,如果在非web类项目中没有自定义的XmlApplicationContext,那么其实功能可以参考XmlWebApplicationContext,可以认为是一样的功能。那么看看loadBeanDefinitions方法如下: 这里有一个XmlBeanDefineitionReader,是读取XML中spring的相关信息(也就是解析SpringContext.xml的),这里通过getConfigLocations()获取到的就是这个或多个文件的路径,会循环,通过XmlBeanDefineitionReader来解析,跟踪到loadBeanDefinitions方法里面,会发现方法实现体在XmlBeanDefineitionReader的父类:AbstractBeanDefinitionReader中,代码如下: 这里大家会疑惑,为啥里面还有一个loadBeanDefinitions,大家要知道,我们目前只解析到我们的springContext.xml在哪里,但是还没解析到springContext.xml的内容是什么,可能有多个spring的配置文件,这里会出现多个Resource,所以是一个数组(这里如何通过location找到文件部分,在我们找class的时候自然明了,大家先不纠结这个问题)。 接下来有很多层调用,会以此调用: AbstractBeanDefinitionReader.loadBeanDefinitions(Resources []) 循环Resource数组,调用方法: XmlBeanDefinitionReader.loadBeanDefinitions(Resource ) 和上面这个类是父子关系,接下来会做:doLoadBeanDefinitions、registerBeanDefinitions的操作,在注册beanDefinitions的时候,其实就是要真正开始解析XML了 它调用了DefaultBeanDefinitionDocumentReader类的registerBeanDefinitions方法,如下图所示: 中间有解析XML的过程,但是貌似我们不是很关心,我们就关系类是怎么加载的,虽然已经到XML解析部分了,所以主要看parseBeanDefinitions这个方法,里面会调用到BeanDefinitionParserDelegate类的parseCustomElement方法,用来解析bean的信息: z 这里解析了XML的信息,跟踪进去,会发现用了NamespaceHandlerSupport的parse方法,它会根据节点的类型,找到一种合适的解析BeanDefinitionParser(接口),他们预先被spring注册好了,放在一个HashMap中,例如我们在spring 的annotation扫描中,通常会配置: <context:component-scan base-package="com.xxx" /> 此时根据名称“component-scan”就会找到对应的解析器来解析,而与之对应的就是ComponentScanBeanDefinitionParser的parse方法,这地方已经很明显有扫描bean的概念在里面了,这里的parse获取到后,中间有一个非常非常关键的步骤那就是定义了ClassPathBeanDefinitionScanner来扫描类的信息,它扫描的是什么?是加载的类还是class文件呢?答案是后者,为何,因为有些类在初始化化时根本还没被加载,ClassLoader根本还没加载,只是ClassLoader可以找到这些class的路径而已: 注意这里的scanner创建后,最关键的是doScan的功能,解析XML我想来看这个的不是问题,如果还不熟悉可以先看看,那么我们得到了类似:com.xxx这样的信息,就要开始扫描类的列表,那么再哪里扫描呢?这里的doScan返回了一个Set<BeanDefinitionHolder>我们感到希望就在不远处,进去看看doScan方法。 我们看到这么大一坨代码,其实我们目前不关心的代码,暂时可以不管,我们就看怎么扫描出来的,可以看出最关键的扫描代码是:findCandidateComponents(String basePackage)方法,也就是通过每个basePackage去找到有那些类是匹配的,我们这里假如配置了com.abc,或配置了 * 两种情况说明。 主要看红线部分,下面非红线部分,是已经拿到了类的定义,红线部分,会组装信息,如果我们配置了 com.abc会组装为:classpath*:com/abc/**/*.class ,如果配置是 * ,那么将会被组装为classpath*:*/**/*.class ,但是这个好像和我们用的东西不太一样,java中也没见这种URL可以获取到,spring到底是怎么搞的呢?就要看第二个红线部分的代码: Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath); 它竟然神奇般的通过这个路径获取到了URL,你一旦跟踪你会发现,获取出来的全是.class的路径,包括jar包中的相关class路径,这里有些细节,我们先不说,先看下这个resourcePatternResolover是什么类型的,看到定义部分是: private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); 为此胖哥还将其做了一个测试,用一个简单main方法写了一段: ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); Resource[] resources = resourcePatternResolver.getResources("classpath*:com/abc/**/*.class"); 获取出来的果然是那样,胖哥开始猜测,这个和ClassLoader的getResource方法有关系了,因为太类似了,我们跟踪进去看下: 这个CLASSPATH_ALL_URL_PREFIX就是字符串 classpath*: , 我们传递参数进来的时候,自然会走第一个红圈圈住部分的代码,但是第二个红圈圈住部分的代码是干嘛的呢,胖哥告诉你先知道有这个,然后回头会有用,继续找findPathMatchingResources方法,好了,越来越接近真相了。 这里有一个rootDirPath,这个地方有个容易出错的,是如果你配置的是 com.abc,那么rootDirPath部分应该是:classpath*:com/abc/ 而如果配置是 * 那么classpath*: 只有这个结果,而不是classpath*:*(这里我就不说截取字符串的源码了),回到上一段代码,这里再次调用了getResources(String)方法,又回到前面一个方法,这一次,依然是以classpath*:开头,所以第一层 if 语句会进去,而第二层不会,为什么?在里面的isPattern() 的实现中是这样写的: public boolean isPattern(String path) { return (path.indexOf('*') != -1 || path.indexOf('?') != -1); } 在匹配前,做了一个substring的操作,会将“classpath*:”这个字符串去掉,如果是配置的是com.abc就变成了"com/abc/",而如果配置为*,那么得到的就是“” ,也就是长度为0的字符串,因此在我们的这条路上,这个方法返回的是false,就会走到代码段findAllClassPathResources中,这就是为什么上面提到会有用途的原因,好了,最最最最关键的地方来了哦。例如我们知道了一个com/abc/为前缀,此时要知道相关的classpath下面有哪些class是匹配的,如何做?自然用ClassLoader,我们看看Spring是不是这样做的: 果然不出所料,它也是用ClassLoader,只是它自己提供的getClassLoader()方法,也就是和spring的类使用同一个加载器范围内的,以保证可以识别到一样的classpath,自己模拟的时候,可以用一个类 类名.class.getClassLoader().getResources("") 如果放为空,那么就是获取classpath的相关的根路径(classpath可能有很多,但是根路径,可以被合并),也就是如果你配置的*,获取到的将是这个,也许你在web项目中,你会获取到项目的根路径(classes下面,以及tomcat的lib目录)。 如果写入一个:com/abc/ 那么得到的将是扫描相关classpath下面所有的class和jar包中与之匹配的类名(前缀部分)的路径信息,但是需要注意的是,如果有两层jar包,而你想要扫描的类或者说想要通过spring加载的类在第二层jar包中,这个方法是获取不到的,这不是spring没有去做这个事情,而是,java提供的getResources方法就是这样的,有朋友问我的时候,正好遇到了类似的事情,另外需要注意的是,getResources这个方法是包含当前路径的一个递归文件查找(一般环境变量中都会配置 . ),所以如果是一个jar包,你要运行的话,切记放在某个根目录来跑,因为当前目录,就是根目录也会被递归下去,你的程序会被莫名奇怪地慢。 回到上面的代码中,在findPathMatchingResources中我们这里刚刚获取到base的路径列表,也就是所有包含类似com/abc/为前缀的路径,或classpath合并后的目录根路径;此时我们需要下面所有的class,那么就需要的是递归,这里我就不再跟踪了,大家可以自己去跟踪里面的几个方法调用:doFindPathMatchingJarResources、doFindPathMatchingFileResources 。 几乎不会用到:VfsResourceMatchingDelegate.findMatchingResources,所以主要是上面两个,分别是jar包中的和工程里面的class,跟踪进去会发现,代码会不断递归循环调用目录路径下的class文件的路径信息,最终会拿到相关的class列表信息,但是这些class还并没有做检测是否有annotation,那是下一步做的事情,但是下一个步骤已经很简单了,因为要检测一个类的annotation,在前面的文章中:《 java之annotation与框架的那些秘密》中已经提到了。 这里大家还可以通过以下简单的方式来测试调用路径的问题: ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); Set<BeanDefinition> beanDefinitions = provider.findCandidateComponents("com/abc"); for(BeanDefinition beanDefinition : beanDefinitions) { System.out.println(beanDefinition.getBeanClassName() + "\t" + beanDefinition.getResourceDescription() + "\t" + beanDefinition.getClass()); } 这是直接引用spring的源码部分的内容,如果这里可以获取到,且路径是正确的,一般情况下,都是可以加载到类的。 看了这么多,是不是有点晕,没关系,谁第一回看都这样,当你下一次看的时候,有个思路就好了,我这里并没有像UML一样理出他们的层次关系,和调用关系,仅仅针对代码调用逐层来说明,大家如果初步看就是,由Servlet初始化来创建ApplicationContext,在设置了Servelt相关参数后,获取servlet的配置文件路径或自己指定的配置文件路径(applicationContext.xml或其他的名字,可以一个或多个),然后通过系列的XML解析,以及针对每种不同的节点类型使用不同的加载方式,其中component-scan用于指定扫描类的对应有一个Scanner,它会通过ClassLoader的getResources方法来获取到class的路径信息,那么class的路径都能获取到,类的什么还拿不到呢?呵呵! 好,本文基本内容就说到这里,接下来我会提到spring MVC的中的简单跳转的解析,其中有部分源码是这里看过的,只是还不是这里的重点而已。 而我想说的也是这点,其实本文虽然在说启动,其实有很多代码也没说,因为那样的话我就是一个复制咱贴机了; 其实看源码,要带着目的,大家要知道主体情况或实现的功能,不要就看源码而看源码,一个是根本记不下来,另一个这样看代码没有太大的意义。 当你有了疑问,遇到了难题不知道原因,或发现了新大陆,很有兴趣,那么去看看,也许看之前你会思考下如果我来实现会怎么做?再看看别人是怎么做的,有何区别,不断吸取这些开源框架中优秀的品质,包括代码的设计层次,了解它用到了什么,为何要这样设计,那么你的代码相信会越来越漂亮,你对开源界的代码也会越来越熟悉,熟悉得像自己亲人一样,呵呵。 【对于5楼的回复(CSDN发神经,我回复的内容提示我链接过多,其实一个链接都没有,神奇,所以回复在正文)】: 你能看到isCandidateComponent(MetadataReader metadataReader)方法,其实呢,你再跟下应该就有结果了!要细写可以写一篇文章,简单写下如下:这个方法里先循环excludeFilters,再循环includeFilters,excludeFilters默认情况下没有啥内容,includeFilters默认情况下最少会有一个new AnnotationTypeFilter(Component.class);也就是默认情况下excludeFilters排除内容不会循环,includeFilters包含内容最少会匹配到AnnotationTypeFilter,调用AnnotationTypeFilter.match方法是其父类AbstractTypeHierarchyTraversingFilter.math()方法,其内部调用matchSelf()调回子类的AnnotationTypeFilter.matchSelf()方法。该方法中用||连接两个判定分别是hasAnnotation、hasMetaAnnotation,前者判定注解名称本身是否匹配因此Component肯定能匹配上,后者会判定注解的meta注解是否包含,Service、Controller、Repository注解都注解了Component因此它们会在后者匹配上。这样match就肯定成立了。此时类还没有被装载,Resource中仅仅是类的目录信息,Spring也没有通过ClassLoader将类加载后通过反射读取类的Annotation信息(这条路也是通的),而是通过自己的asm对类的class字节码的解析来完成的,这部分是字节码相关的知识,在我的书中和其它博客有所介绍。spring我想这样做的目的是方便自己做AOP相关的字节码增强一带搞定,也不会多加载不需要的类,因为本文提到的访问如果写了目录,可以访问到jar包中的内容,可能类信息会比较多。
在前面介绍了java的多线程的基本原理信息:《Java线程池架构原理和源码解析(ThreadPoolExecutor)》,本文对这个java本身的线程池的调度器做一个简单扩展,如果还没读过上一篇文章,建议读一下,因为这是调度器的核心组件部分。 我们如果要用java默认的线程池来做调度器,一种选择就是Timer和TimerTask的结合,在以前的文章:《Timer与TimerTask的真正原理&使用介绍》中有明确的说明:一个Timer为一个单独的线程,虽然一个Timer可以调度多个TimerTask,但是对于一个Timer来讲是串行的,至于细节请参看对应的那篇文章的内容,本文介绍的多线程调度器,也就是定时任务,基于多线程调度完成,当然你可以为了完成多线程使用多个Timer,只是这些Timer的管理需要你来完成,不是一个框架体系,而ScheduleThreadPoolExecutor提供了这个功能,所以我们第一要搞清楚是如何使用调度器的,其次是需要知道它的内部原理是什么,也就是知其然,再知其所以然! 首先如果我们要创建一个基于java本身的调度池通常的方法是: Executors.newScheduledThreadPool(int); 当有重载方法,我们最常用的是这个就从这个,看下定义: public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } 其实内部是new了一个实例化对象出来,并传入大小,此时就跟踪到ScheduledThreadPoolExecutor的构造方法中: public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0,TimeUnit.NANOSECONDS, new DelayedWorkQueue()); } 你会发现调用了super,而super你跟踪进去会发现,是ThreadPoolExecutor中,那么ScheduledThreadPoolExecutor和ThreadPoolExecutor有何区别,就是本文要说得重点了,首先我们留下个引子,你发现在定义队列的时候,不再是上文中提到的LinkedBlockingQueue,而是DelayedWorkQueue,那么细节上我们接下来就是要讲解的重点,既然他们又继承关系,其实搞懂了不同点,就搞懂了共同点,而且有这样的关系大多数应当是共同点,不同点的猜测:这个是要实现任务调度,任务调度不是立即的,需要延迟和定期做等情况,那么是如何实现的呢? 这就是我们需要思考的了,通过源码考察,我们发现,他们都有execute方法,只是ScheduledThreadPoolExecutor将源码进行了重写,并且还有以下四个调度器的方法: public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit); public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit); public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); 那么这四个方法有什么区别呢?其实第一个和第二个区别不大,一个是Runnable、一个是Callable,内部包装后是一样的效果;所以把头两个方法几乎当成一种调度,那么三种情况分别是: 1、 进行一次延迟调度:延迟delay这么长时间,单位为:TimeUnit传入的的一个基本单位,例如:TimeUnit.SECONDS属于提供好的枚举信息;(适合于方法1和方法2)。 2、 多次调度,每次依照上一次预计调度时间进行调度,例如:延迟2s开始,5s一次,那么就是2、7、12、17,如果中间由于某种原因导致线程不够用,没有得到调度机会,那么接下来计算的时间会优先计算进去,因为他的排序会被排在前面,有点类似Timer中的:scheduleAtFixedRate方法,只是这里是多线程的,它的方法名也叫:scheduleAtFixedRate,所以这个是比较好记忆的(适合方法3) 3、 多次调度,每次按照上一次实际执行的时间进行计算下一次时间,同上,如果在第7秒没有被得到调度,而是第9s才得到调度,那么计算下一次调度时间就不是12秒,而是9+5=14s,如果再次延迟,就会延迟一个周期以上,也就会出现少调用的情况(适合于方法3); 4、 最后补充execute方法是一次调度,期望被立即调度,时间为空: public void execute(Runnable command) { if (command == null) throw new NullPointerException(); schedule(command, 0, TimeUnit.NANOSECONDS); } 我们简单看看scheduleAtFixedRate、scheduleWithFixedDelay对下面的分析会更加有用途: public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); if (period <= 0) throw new IllegalArgumentException(); RunnableScheduledFuture<?> t = decorateTask(command, new ScheduledFutureTask<Object>(command, null, triggerTime(initialDelay, unit), unit.toNanos(period))); delayedExecute(t); return t; } public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); if (delay <= 0) throw new IllegalArgumentException(); RunnableScheduledFuture<?> t = decorateTask(command, new ScheduledFutureTask<Boolean>(command, null, triggerTime(initialDelay, unit), unit.toNanos(-delay))); delayedExecute(t); return t; } 你是否发现,两段源码唯一的区别就是在unit.toNanos(int)这唯一一个地方,scheduleAtFixedRate里面是直接传入值,而scheduleWithFixedDelay里面是取了相反数,也就是假如我们都传入正数,scheduleWithFixedDelay其实就取反了,没有任何区别,你是否联想到前面文章介绍Timer中类似的处理手段通过正负数区分时间间隔方法,为0代表仅仅调度一次,其实在这里同样是这样的,他们也同样有一个问题就是,如果你传递负数,方法的功能正好是相反的。 而你会发现,不论是那个schedule方法里头,都会创建一个ScheduledFutureTask类的实例,此类究竟是何方神圣呢,我们来看看。 ScheduledFutureTask的类(ScheduleThreadPoolExecutor的私有的内部类)来进行调度,那么可以看看内部做了什么操作,如下: ScheduledFutureTask(Runnable r, V result, long ns) { super(r, result); this.time = ns; this.period = 0; this.sequenceNumber = sequencer.getAndIncrement(); } /** * Creates a periodic action with given nano time and period. */ ScheduledFutureTask(Runnable r, V result, long ns, long period) { super(r, result); this.time = ns; this.period = period; this.sequenceNumber = sequencer.getAndIncrement(); } /** * Creates a one-shot action with given nanoTime-based trigger. */ ScheduledFutureTask(Callable<V> callable, long ns) { super(callable); this.time = ns; this.period = 0; this.sequenceNumber = sequencer.getAndIncrement(); } 最核心的几个参数正好对应了调度的延迟的构造方法,这些参数如何用起来的?那么它还提供了什么方法呢? public long getDelay(TimeUnit unit) { return unit.convert(time - now(), TimeUnit.NANOSECONDS); } public int compareTo(Delayed other) { if (other == this) // compare zero ONLY if same object return 0; if (other instanceof ScheduledFutureTask) { ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other; long diff = time - x.time; if (diff < 0) return -1; else if (diff > 0) return 1; else if (sequenceNumber < x.sequenceNumber) return -1; else return 1; } long d = (getDelay(TimeUnit.NANOSECONDS) - other.getDelay(TimeUnit.NANOSECONDS)); return (d == 0)? 0 : ((d < 0)? -1 : 1); } /** * 返回是否为片段,也就是多次调度 * */ public boolean isPeriodic() { return period != 0; } 这里发现了,他们可以运行,且判定时间的方法是getDelay方法我们知道了。 对比时间的方法是:compareTo,传入了参数类型为:Delayed类型,不难猜测出,ScheduledFutureTask和Delayed有某种继承关系,没错,ScheduledFutureTask实现了Delayed的接口,只是它是间接实现的;并且Delayed接口继承了Comparable接口,这个接口可用来干什么?看过我前面写的一篇文章关于中文和对象排序的应该知道,这个是用来自定义对比和排序的,我们的调度任务是一个对象,所以需要排序才行,接下来我们回溯到开始定义的代码中,找一个实际调用的代码来看看它是如何启动到run方法的?如何排序的?如何调用延迟的?就是我们下文中会提到的,而这里我们先提出问题,后文我们再来说明这些问题。 我们先来看下run方法的一些定义。 /** * 时间片类型任务执行 */ private void runPeriodic() { //运行对应的程序,这个是具体的程序 boolean ok = ScheduledFutureTask.super.runAndReset(); boolean down = isShutdown(); // Reschedule if not cancelled and not shutdown or policy allows if (ok && (!down || (getContinueExistingPeriodicTasksAfterShutdownPolicy() && !isStopped()))) { long p = period; if (p > 0)//规定时间间隔算出下一次时间 time += p; else//用当前时间算出下一次时间,负负得正 time = triggerTime(-p); //计算下一次时间,并资深再次放入等待队列中 ScheduledThreadPoolExecutor.super.getQueue().add(this); } else if (down) interruptIdleWorkers(); } /** * 是否为逐片段执行,如果不是,则调用父亲类的run方法 */ public void run() { if (isPeriodic())//周期任务 runPeriodic(); else//只执行一次的任务 ScheduledFutureTask.super.run(); } 可以看到run方法首先通过isPeriod()判定是否为时间片,判定的依据就是我们说的时间片是否“不为零”,如果不是周期任务,就直接运行一次,如果是周期任务,则除了运行还会计算下一次执行的时间,并将其再次放入等待队列,这里对应到scheduleAtFixedRate、scheduleWithFixedDelay这两个方法一正一负,在这里得到判定,并且将为负数的取反回来,负负得正,java就是这么干的,呵呵,所以不要认为什么是不可能的,只要好用什么都是可以的,然后计算的时间一个是基于标准的time加上一个时间片,一个是根据当前时间计算一个时间片,在上文中我们已经明确说明了两者的区别。 以:schedule方法为例: public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { if (callable == null || unit == null) throw new NullPointerException(); RunnableScheduledFuture<V> t = decorateTask(callable, new ScheduledFutureTask<V>(callable, triggerTime(delay, unit))); delayedExecute(t); return t; } 其实这个方法内部创建的就是一个我们刚才提到的:ScheduledFutureTask,外面又包装了下叫做RunnableScheduledFuture,也就是适配了下而已,呵呵,代码里面就是一个return操作,java这样做的目的是方便子类去扩展。 关键是delayedExecute(t)方法中做了什么?看名称是延迟执行的意思,难道java的线程可以延迟执行,那所有的任务线程都在运行状态? 它的源码是这样的: private void delayedExecute(Runnable command) { if (isShutdown()) { reject(command); return; } if (getPoolSize() < getCorePoolSize()) prestartCoreThread(); super.getQueue().add(command); } 我们主要关心prestartCoreThread()和super.getQueue().add(command),因为如果系统关闭,这些讨论都没有意义的,我们分别叫他们第二小段代码和第三小段代码。 第二个部分如果线程数小于核心线程数设置,那么就调用一个prestartCoreThread(),看方法名应该是:预先启动一个核心线程的意思,先看完第三个部分,再跟踪进去看源码。 第三个部分很明了,就是调用super.getQueue().add(command);也就是说直接将任务放入一个队列中,其实super是什么?super就是我们上一篇文章所提到的ThreadPoolExecutor,那么这个Queue就是上一篇文章中提到的等待队列,也就是任何schedule任务首先放入等待队列,然后等待被调度的。 public boolean prestartCoreThread() { return addIfUnderCorePoolSize(null); } private boolean addIfUnderCorePoolSize(Runnable firstTask) { Thread t = null; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { if (poolSize < corePoolSize && runState == RUNNING) t = addThread(firstTask); } finally { mainLock.unlock(); } if (t == null) return false; t.start(); return true; } 这个代码是否似曾相似,没错,这个你在上一篇文章介绍ThreadPoolExecutor的时候就见到过,说明不论是ThreadPoolExecutor还是ScheduleThreadPoolExecutor他们的Thread都是由一个Worker来处理的(上一篇文章有介绍),而这个Worker处理的基本机制就是将当前任务执行后,不断从线程等待队列中获取数据,然后用以执行,直到队列为空为止。 那么他们的区别在哪里呢?延迟是如何实现的呢?和我们上面介绍的ScheduledFutureTask又有何关系呢? 那么我们回过头来看看ScheduleThreadPool的定义是如何的。 public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0,TimeUnit.NANOSECONDS, new DelayedWorkQueue()); } 发现它和ThreadPoolExecutor有个定义上很大的区别就是,ThreadPoolExecutor用的是LinkedBlockingQueue(当然可以修改),它用的是DelayedWeorkQueue,而这个DelayedWorkQueue里面你会发现它仅仅是对java.util.concurrent.DelayedQueue类一个简单访问包装,这个队列就是等待队列,可以看到任务是被直接放到等待队列中的,所以取数据必然从这里获取,而这个延迟的队列有何神奇之处呢,它又是如何实现的呢,我们从什么地方下手去看这个DelayWorkQueue? 我们还是回头看看Worker里面的run方法(上一篇文章中已经讲过): public void run() { try { Runnable task = firstTask; firstTask = null; while (task != null || (task = getTask()) != null) { runTask(task); task = null; } } finally { workerDone(this); } } 这里面要调用等待队列就是getTask()方法: Runnable getTask() { for (;;) { try { int state = runState; if (state > SHUTDOWN) return null; Runnable r; if (state == SHUTDOWN) // Help drain queue r = workQueue.poll(); else if (poolSize > corePoolSize || allowCoreThreadTimeOut) r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS); else r = workQueue.take(); if (r != null) return r; if (workerCanExit()) { if (runState >= SHUTDOWN) // Wake up others interruptIdleWorkers(); return null; } } catch (InterruptedException ie) { } } } 你会发现,如果没有设置超时,默认只会通过workQueue.take()方法获取数据,那么我们就看take方法,而增加到队列里面的方法自然看offer相关的方法。接下来我们来看下DelayQueue这个队列的take方法: public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { for (;;) { E first = q.peek(); if (first == null) { available.await();//等待信号,线程一直挂在哪里 } else { long delay = first.getDelay(TimeUnit.NANOSECONDS); if (delay > 0) { long tl = available.awaitNanos(delay);//最左等delay的时间段 } else { E x = q.poll();//可以运行,取出一个 assert x != null; if (q.size() != 0) available.signalAll(); return x; } } } } finally { lock.unlock(); } } 这里的for就是要找到数据为止,否则就等着,而这个“q”和“available”是什么呢? private transient final Condition available = lock.newCondition(); private final PriorityQueue<E> q = new PriorityQueue<E>(); 怎么里面还有一层队列,不用怕,从这里你貌似看出点名称意味了,就是它是优先级队列,而对于任务调度来讲,优先级的方式就是时间,我们用这中猜测来继续深入源码。 上面首先获取这个队列的第一个元素,若为空,就等待一个“available”发出的信号,我们可以猜测到这个offer的时候会发出的信号,一会来验证即可;若不为空,则通过getDelay方法来获取时间信息,这个getDelay方法就用上了我们开始说的ScheduledFutureTask了,如果是时间大于0,则也进入等待,因为还没开始执行,等待也是“available”发出信号,但是有一个最长时间,为什么还要等这个信号,是因为有可能进来一个新的任务,比这个等待的任务还要先执行,所以要等这个信号;而最多等这么长时间,就是因为如果这段时间没任务进来肯定就是它执行了。然后就返回的这个值,被Worker(上面有提到)拿到后调用其run()方法进行运行。 那么写入队列在那里?他们是如何排序的?我们看看队列的写入方法是这样的:public boolean offer(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { E first = q.peek(); q.offer(e); if (first == null || e.compareTo(first) < 0) available.signalAll(); return true; } finally { lock.unlock(); } } 队列也是首先取出第一个(后面会用来和当前任务做比较),而这里“q”是上面提到的“PriorityQueue”,看来offer的关键还在它的里面,我们看看调用过程: public boolean offer(E e) { if (e == null) throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) grow(i + 1); size = i + 1; if (i == 0) queue[0] = e; else siftUp(i, e);//主要是这条代码很关键 return true; } private void siftUp(int k, E x) { if (comparator != null) siftUpUsingComparator(k, x); else //我们默认走这里,因为DelayQueue定义它的时候默认没有给定义comparator siftUpComparable(k, x); } /* 可以发现这个方法是将任务按照compareTo对比后,放在队列的合适位置,但是它肯定不是绝对顺序的,这一点和Timer的内部排序机制类似。 */ private void siftUpComparable(int k, E x) { Comparable<? super E> key = (Comparable<? super E>) x; while (k > 0) { int parent = (k - 1) >>> 1; Object e = queue[parent]; if (key.compareTo((E) e) >= 0) break; queue[k] = e; k = parent; } queue[k] = key; } 你是否发现,compareTo也用上了,就是我们前面描述一大堆的:ScheduledFutureTask类中的一个方法,那么run方法也用上了,这个过程貌似完整了。 我们再来理一下思路: 1、调用的Thread的包装,由在ThreadPoolExecutor中的Worker调用你传入的Runnable的run方法,变成了Worker调用Runnable的run方法,由它来处理时间片的信息调用你传入的线程。 2、ScheduledFutureTask类在整个过程中提供了基础参考的方法,其中最为关键的就是实现了接口Comparable,实现内部的compareTo方法,也实现了Delayed接口中的getDelay方法用以判定时间(当然Delayed接口本身也是继承于Comparable,我们不要纠结于细节概念就好)。 3、等待队列由在ThreadPoolExecutor中默认使用的LinkedBlockingQueue换成了DelayQueue(它是被DelayWorkQueue包装了一下子,没多大区别),而DelayQueue主要提供了一个信号量“available”来作为写入和读取的信号控制开关,通过另一个优先级队列“PriorityQueue”来控制实际的队列顺序,他们的顺序就是基于上面提到的ScheduledFutureTask类中的compareTo方法,而是否运行也是基于getDelay方法来实现的。 4、ScheduledFutureTask类的run方法会判定是否为时间片信息,如果为时间片,在执行完对应的方法后,开始计算下一次执行时间(注意判定时间片大于0,小于0,分别代表的是以当前执行完的时间为准计算下一次时间还是以当前时间为准),这个在前面有提到。 5、它是支持多线程的,和Timer的机制最大的区别就在于多个线程会最征用这个队列,队里的排序方式和Timer有很多相似之处,并非完全有序,而是通过位移动来尽量找到合适的位置,有点类似贪心的算法,呵呵。
在前面介绍JUC的文章中,提到了关于线程池Execotors的创建介绍,在文章:《java之JUC系列-外部Tools》中第一部分有详细的说明,请参阅; 文章中其实说明了外部的使用方式,但是没有说内部是如何实现的,为了加深对实现的理解,在使用中可以放心,我们这里将做源码解析以及反馈到原理上,Executors工具可以创建普通的线程池以及schedule调度任务的调度池,其实两者实现上还是有一些区别,但是理解了ThreadPoolExecutor,在看ScheduledThreadPoolExecutor就非常轻松了,后面的文章中也会专门介绍这块,但是需要先看这篇文章。 使用Executors最常用的莫过于是使用:Executors.newFixedThreadPool(int)这个方法,因为它既可以限制数量,而且线程用完后不会一直被cache住;那么就通过它来看看源码,回过头来再看其他构造方法的区别: 在《java之JUC系列-外部Tools》文章中提到了构造方法,为了和本文对接,再贴下代码。 public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } 其实你可以自己new一个ThreadPoolExecutor,来达到自己的参数可控的程度,例如,可以将LinkedBlockingQueue换成其它的(如:SynchronousQueue),只是可读性会降低,这里只是使用了一种设计模式。 我们现在来看看ThreadPoolExecutor的源码是怎么样的,也许你刚开始看他的源码会很痛苦,因为你不知道作者为什么是这样设计的,所以本文就我看到的思想会给你做一个介绍,此时也许你通过知道了一些作者的思想,你也许就知道应该该如何去操作了。 这里来看下构造方法中对那些属性做了赋值: 源码段1: public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; } 这里你可以看到最终赋值的过程,可以先大概知道下参数的意思: corePoolSize:核心运行的poolSize,也就是当超过这个范围的时候,就需要将新的Thread放入到等待队列中了; maximumPoolSize:一般你用不到,当大于了这个值就会将Thread由一个丢弃处理机制来处理,但是当你发生:newFixedThreadPool的时候,corePoolSize和maximumPoolSize是一样的,而corePoolSize是先执行的,所以他会先被放入等待队列,而不会执行到下面的丢弃处理中,看了后面的代码你就知道了。 workQueue:等待队列,当达到corePoolSize的时候,就向该等待队列放入线程信息(默认为一个LinkedBlockingQueue),运行中的队列属性为:workers,为一个HashSet;内部被包装了一层,后面会看到这部分代码。 keepAliveTime:默认都是0,当线程没有任务处理后,保持多长时间,cachedPoolSize是默认60s,不推荐使用。 threadFactory:是构造Thread的方法,你可以自己去包装和传递,主要实现newThread方法即可; handler:也就是参数maximumPoolSize达到后丢弃处理的方法,java提供了5种丢弃处理的方法,当然你也可以自己弄,主要是要实现接口:RejectedExecutionHandler中的方法: public void rejectedExecution(Runnabler, ThreadPoolExecutor e) java默认的是使用:AbortPolicy,他的作用是当出现这中情况的时候会抛出一个异常;其余的还包含: 1、CallerRunsPolicy:如果发现线程池还在运行,就直接运行这个线程 2、DiscardOldestPolicy:在线程池的等待队列中,将头取出一个抛弃,然后将当前线程放进去。 3、DiscardPolicy:什么也不做 4、AbortPolicy:java默认,抛出一个异常:RejectedExecutionException。 通常你得到线程池后,会调用其中的:submit方法或execute方法去操作;其实你会发现,submit方法最终会调用execute方法来进行操作,只是他提供了一个Future来托管返回值的处理而已,当你调用需要有返回值的信息时,你用它来处理是比较好的;这个Future会包装对Callable信息,并定义一个Sync对象(),当你发生读取返回值的操作的时候,会通过Sync对象进入锁,直到有返回值的数据通知,具体细节先不要看太多,继续向下: 来看看execute最为核心的方法吧: 源码段2: public void execute(Runnable command) { if (command == null) throw new NullPointerException(); if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) { if (runState == RUNNING && workQueue.offer(command)) { if (runState != RUNNING || poolSize == 0) ensureQueuedTaskHandled(command); } else if (!addIfUnderMaximumPoolSize(command)) reject(command); // is shutdown or saturated } } 这段代码看似简单,其实有点难懂,很多人也是这里没看懂,没事,我一个if一个if说: 首先第一个判定空操作就不用说了,下面判定的poolSize >= corePoolSize成立时候会进入if的区域,当然它不成立也有可能会进入,他会判定addIfUnderCorePoolSize是否返回false,如果返回false就会进去; 我们先来看下addIfUnderCorePoolSize方法的源码是什么: 源码段3: private boolean addIfUnderCorePoolSize(Runnable firstTask) { Thread t = null; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { if (poolSize < corePoolSize && runState == RUNNING) t = addThread(firstTask); } finally { mainLock.unlock(); } if (t == null) return false; t.start(); return true; } 可以发现,这段源码是如果发现小雨corePoolSize就会创建一个新的线程,并且调用线程的start()方法将线程运行起来:这个addThread()方法,我们先不考虑细节,因为我们还要先看到前面是怎么进去的,这里可以发信啊,只有没有创建成功Thread才会返回false,也就是当当前的poolSize > corePoolSize的时候,或线程池已经不是在running状态的时候才会出现; 注意:这里在外部判定一次poolSize和corePoolSize只是初步判定,内部是加锁后判定的,以得到更为准确的结果,而外部初步判定如果是大于了,就没有必要进入这段有锁的代码了。 此时我们知道了,当前线程数量大于corePoolSize的时候,就会进入【代码段2】的第一个if语句中,回到【源码段2】,继续看if语句中的内容: 这里标记为 源码段4: if (runState == RUNNING && workQueue.offer(command)) { if (runState != RUNNING || poolSize == 0) ensureQueuedTaskHandled(command); } else if (!addIfUnderMaximumPoolSize(command)) reject(command); // is shutdown or saturated 第一个if,也就是当当前状态为running的时候,就会去执行workQueue.offer(command),这个workQueue其实就是一个BlockingQueue,offer()操作就是在队列的尾部写入一个对象,此时写入的对象为线程的对象而已;所以你可以认为只有线程池在RUNNING状态,才会在队列尾部插入数据,否则就执行else if,其实else if可以看出是要做一个是否大于MaximumPoolSize的判定,如果大于这个值,就会做reject的操作,关于reject的说明,我们在【源码段1】的解释中已经非常明确的说明,这里可以简单看下源码,以应征结果: 源码段5: private boolean addIfUnderMaximumPoolSize(Runnable firstTask) { Thread t = null; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { if (poolSize < maximumPoolSize && runState == RUNNING) //在corePoolSize = maximumPoolSize下,该代码几乎不可能运行 t = addThread(firstTask); } finally { mainLock.unlock(); } if (t == null) return false; t.start(); return true; } void reject(Runnable command) { handler.rejectedExecution(command, this); } 也就是如果线程池满了,而且线程池调用了shutdown后,还在调用execute方法时,就会抛出上面说明的异常:RejectedExecutionException 再回头来看下【代码段4】中进入到等待队列后的操作: if (runState != RUNNING || poolSize == 0) ensureQueuedTaskHandled(command); 这段代码是要在线程池运行状态不是RUNNING或poolSize == 0才会调用,他是干啥呢? 他为什么会不等于RUNNING呢?外面那一层不是判定了他== RUNNING了么,其实有时间差就是了,如果是poolSize == 0也会执行这段代码,但是里面的判定条件是如果不是RUNNING,就做reject操作,在第一个线程进去的时候,会将第一个线程直接启动起来;很多人也是看这段代码很绕,因为不断的循环判定类似的判定条件,你主要记住他们之间有时间差,要取最新的就好了。 此时貌似代码看完了?咦,此时有问题了: 1、 等待中的线程在后来是如何跑起来的呢?线程池是不是有类似Timer一样的守护进程不断扫描线程队列和等待队列?还是利用某种锁机制,实现类似wait和notify实现的? 2、 线程池的运行队列和等待队列是如何管理的呢?这里还没看出影子呢! NO,NO,NO! Java在实现这部分的时候,使用了怪异的手段,神马手段呢,还要再看一部分代码才晓得。 在前面【源码段3】中,我们看到了一个方法叫:addThread(),也许很少有人会想到关键在这里,其实关键就是在这里: 我们看看addThread()方法到底做了什么。 源码段6: private Thread addThread(Runnable firstTask) { Worker w = new Worker(firstTask); Thread t = threadFactory.newThread(w); if (t != null) { w.thread = t; workers.add(w); int nt = ++poolSize; if (nt > largestPoolSize) largestPoolSize = nt; } return t; } 这里创建了一个Work,其余的操作,就是讲poolSize叠加,然后将将其放入workers的运行队列等操作; 我们主要关心Worker是干什么的,因为这个threadFactory对我们用途不大,只是做了Thread的命名处理;而Worker你会发现它的定义也是一个Runnable,外部开始在代码段中发现了调用哪个这个Worker的start()方法,也就是线程的启动方法,其实也就是调用了Worker的run()方法,那么我们重点要关心run方法是如何处理的 源码段7: public void run() { try { Runnable task = firstTask; firstTask = null; while (task != null || (task = getTask()) != null) { runTask(task); task = null; } } finally { workerDone(this); } } FirstTask其实就是开始在创建work的时候,由外部传入的Runnable对象,也就是你自己的Thread,你会发现它如果发现task为空,就会调用getTask()方法再判定,直到两者为空,并且是一个while循环体。 那么看看getTask()方法的实现为: 源码段8: Runnable getTask() { for (;;) { try { int state = runState; if (state > SHUTDOWN) return null; Runnable r; if (state == SHUTDOWN) // Help drain queue r = workQueue.poll(); else if (poolSize > corePoolSize || allowCoreThreadTimeOut) r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS); else r = workQueue.take(); if (r != null) return r; if (workerCanExit()) { if (runState >= SHUTDOWN) // Wake up others interruptIdleWorkers(); return null; } // Else retry } catch (InterruptedException ie) { // On interruption, re-check runState } } } 你会发现它是从workQueue队列中,也就是等待队列中获取一个元素出来并返回! 回过头来根据代码段6理解下: 当前线程运行完后,在到workQueue中去获取一个task出来,继续运行,这样就保证了线程池中有一定的线程一直在运行;此时若跳出了while循环,只有workQueue队列为空才会出现或出现了类似于shutdown的操作,自然运行队列会减少1,当再有新的线程进来的时候,就又开始向worker里面放数据了,这样以此类推,实现了线程池的功能。 这里可以看下run方法的finally中调用的workerDone方法为: 源码段9: void workerDone(Worker w) { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { completedTaskCount += w.completedTasks; workers.remove(w); if (--poolSize == 0) tryTerminate(); } finally { mainLock.unlock(); } } 注意这里将workers.remove(w)掉,并且调用了—poolSize来做操作。 至于tryTerminate是做了更多关于回收方面的操作。 最后我们还要看一段代码就是在【源码段6】中出现的代码调用为:runTask(task);这个方法也是运行的关键。 源码段10: private void runTask(Runnable task) { final ReentrantLock runLock = this.runLock; runLock.lock(); try { if (runState < STOP && Thread.interrupted() && runState >= STOP) thread.interrupt(); boolean ran = false; beforeExecute(thread, task); try { task.run(); ran = true; afterExecute(task, null); ++completedTasks; } catch (RuntimeException ex) { if (!ran) afterExecute(task, ex); throw ex; } } finally { runLock.unlock(); } } 你可以看到,这里面的task为传入的task信息,调用的不是start方法,而是run方法,因为run方法直接调用不会启动新的线程,也是因为这样,导致了你无法获取到你自己的线程的状态,因为线程池是直接调用的run方法,而不是start方法来运行。 这里有个beforeExecute和afterExecute方法,分别代表在执行前和执行后,你可以做一段操作,在这个类中,这两个方法都是【空body】的,因为普通线程池无需做更多的操作。 如果你要实现类似暂停等待通知的或其他的操作,可以自己extends后进行重写构造; 本文没有介绍关于ScheduledThreadPoolExecutor调用的细节,下一篇文章会详细说明,因为大部分代码和本文一致,区别在于一些细节,在介绍:ScheduledThreadPoolExecutor的时候,会明确的介绍它与Timer和TimerTask的巨大区别,区别不在于使用,而是在于本身内在的处理细节。
引文: 在J U C里面,要谈到并发,就必然就存在可见性问题,其实对于程序来讲,要说到锁,首先要确保可见性,也就是要在这个基础上才能做到,而CAS也是基于这种原理来完成,我们在文章:Java JUC之Atomic系列12大类实例讲解和原理分解 中关于Atomic的介绍中有提到通过unsafe调用底层的compareAndSwapXXX的三个方法,都是基于可见性变量才会有效。 谈到可见性,首先要明白一下内存和CPU以及多个CPU之间数据修改的基本原理,我们不要谈及CPU上太深的东西,我只需要明白,要将数据进行运算,就需要将数据拷贝到CPU上面去计算,那么就会有内存和CPU之间的通信、CPU的计算、写回到内存一些动作,此时基于线程的私有栈中会保存这些数据;而可见性会体现在:当另一线程对共享数据进行修改的时候,另一个线程未必能看到或者未必能马上看到这个数据。那什么叫看到这个数据呢?说起来蛮抽象的,并且这些情况通常不好模拟,在不同的CPU下也会模拟出来不同的效果或者根本模拟不出来(所以本文只会给出很多理论,因为给你的代码你可能会认为他们是无法将场景实现的),我们下面用简短的一段例子描述下大概: 当一个线程创建多个子线程去做很多任务的时候,在每个子线程内部的都有一个状态区域设置(例如:初始化、运行中、执行完成、执行失败等),主线程会不断去读取子线程的状态,从而做进一步的操作;上面所提到的可见性就是体现在当主线程去读取子线程的数据的时候,有可能会导致数据的还是“老”的值或“失效”的值的情况,但是并不是任何时候都出现,只是一些偶然的情况会发生,由于某些CPU的优化或当JVM被调节为-server模式下运行时,允许很多信息被优化后才会发生;所以你经常在本地调试一些并发程序发生没有什么问题,当你发布到server下后,经常会出现一些稀奇古怪的问题,这是为什么呢,程序的优化和CPU的优化,它认为这里应该是安全的,可以被优化或转换,如果你不想让他变化,你就需要告诉他们,你的数据是存在多线程安全隐患的。 文章中会介绍很多关于线程安全的知识理论分享,也许你第一遍看下来头晕脑胀,但是通过理解后再看看,也许你就会有很多自己的理解,从而在多线程编程时对于线程的安全有新的认识。 什么是线程安全? 从上面的信息可以发现,问题通常出现在多个线程之间的共享数据的访问,也就是没有“共享”就不会出现征用;当多个线程并发得读或写一些共享的数据的时候,我们就可能会产生各种各样的问题,例如上面提到的可见性问题,但是可见性并不代表原子性,因为原子性要求读、修改、写入三个动作要一致,所以原子性要求更高,而原子性代表不了锁,锁要求这个片段的执行或相关片段的执行都是相互隔离的,也就是他不仅仅是单个步骤或某个变量操作需要原子的,而是整个这些步骤操作都是相互隔离的。 栈隔离: 要让线程安全,最简单的方法就是栈隔离,有些翻译为栈封闭,也就是每个线程之间的信息都是局部变量,相互之间是不存在读写的,有本地的局部属性,也有可能是ThreadLocal的延伸,他们都是线程隔离的,通常WEB应用的系统业务代码都是栈隔离的,并不是代表WEB应用是栈隔离的,因为WEB容器帮我们把复杂的线程分派等工作处理掉了,业务代码大部分情况下无需关心多线程处理而已。 可见性: 为了说明可见性,我们来写一个例子程序,代码如下,复制到你的机器上就可以运行: public class NoVisiability { private static class ReadThread extends Thread { private boolean ready; private int number; public void run() { while(!ready) { number++; } System.out.println(ready); } public void readyOn() { this.ready = true; } } public static void main(String []args) throws InterruptedException { ReadThread readThread = new ReadThread(); readThread.start(); Thread.sleep(2000); readThread.readyOn(); System.out.println(readThread.ready); } }这个代码很简单吧,就是一个线程对另一个线程的数据进行了修改,然后看下结果;可能你觉得很无聊,这个结果很明显,然后拿到IDE工具下一运行结果是延迟两秒后就输出来是两个true;但是不然,你要运行这段代码,你需要将运行设置为-server状态,要么在命令行下运行,要么要设置IDE工具运行这个java程序时需要携带的命令,eclipse就是可以在Run Configurations->Arguments->VM arguments里面增加-server即可; 运行结果可能有多重,看机器、看OS、和VM版本; 如果你用的hotspot的VM,可能出现的结果有: 1、正常输出两个true,说明正好被赶上了或OS和机器未做一些处理; 2、主线程输出了一个false,子线程正常退出,看到了true; 3、主线程输出了true,子线程未看到,始终在死循环; 真的假的,你试一试就知道了,呵呵;我的机器上出现的是第三种情况,上述代码中如果将while循环内部写为yield就不会出现死循环的情况,他空闲出对CPU的使用,在获取变量时会重新进行一次拷贝。 其实我们在刚开始引文中已经大概说到了可见性的问题,我们具体来说说什么情况会出现,例如,在一个类中有多个属性,其中一个属性来标示状态(status),其他的属性来标示这个属性的值(name、number等),某一个线程正在等待这个类的值被填充,填充的标志位status,可能线程的代码为: name= “aaa”; number= 12; status= true; 也就是将name和number写入完后开始写入status,这表面上看上去没有什么问题,是的,但是随着编译器发现这三个赋值完全是没有任何顺序关系的,所以在运行一段时间后,随着JIT和CPU的优化,会导致他们执行顺序的乱序,也就是他们三条代码的执行顺序未必是一致的,当status的值被先被赋予true,而name和number可能还未被赋值,所以另一个线程可能会得到的name是null或以前赋值过的信息; 而还有什么可能呢,在某些特殊的情况下,status可能被赋值了true,而另一个线程一直看不到,那么等待这个对象被赋值的线程会出现死等的情况。 再深入一下,对于jvm来讲,很多时候他并不认为这个线程赋值不是安全的,因为它并不知道你有多个线程要操作这个对象,所以他通常在对long、double类型的赋值或读取的时候,会按照32个bit(4个字节)一个基本操作为基本单位,这样可能会造成的是,当读取了前面4个字节后,这个内存单元被修改,此时后面4个字节发生了变化,那么读取出来的数据可想而知。 那么如何保证可见性呢?volatile,这就是volatile真正的意义,要保证原子性,首先要保证可见性,因为你看到的都不是真的,就没法保证数据是原子的;volatile有三大特征: 1、 要求编译器对指令是顺序的,优化器对相关变量赋值的顺序是不改变的;CPU不做相关的指令顺序; 2、 每次访问volatile会向纵向发起一个简单的lock,用于做add(0)的操作,一个轻量级的锁,并从内存中获取最新的数据; 3、 对于long、double类型的数据,读取他们的时候,会是原子的,也就是两个步骤会产生一个简单的锁。 volatile由于在读写时发生一个短暂的锁,所以他的性能会比普通的变量稍微低一点,所以你在后面提到的很多情况下,无需将所有的内容都设置为volatile,因为这样会降低系统的性能。 volatile变量仅仅能保证可见性,也就是你在读取的一瞬间这个数据是不会被修改的,但是要达到原子或锁的目的是不行的,接下来,我们再看一个线程安全,但是可能很多人不想看的final,但是他在线程安全中的确有一些重要的作用: final使用: 在很多应用中,经常发现定义的变量出现了final,但是自己不知道怎么用,除了他不可改变以外,其实他另一种重要用途就是线程是绝对安全的,当一个引用或一个变量被定为final,他在多线程中自然只有读的操作,而没有写的操作;但是这并不意味着这个对象本身内部的所有属性的访问是线程安全的,如果某些属性是被多个线程所访问的,如果可以被认为他们是不会改变的,那么属性也应当是final的; 在很多系统的代码中经常会出现init()或initialize()这些方法,他们如果没有被类似构造方法或某些特殊的基于锁的方法调用的话,就会出现一些问题;由于他们的调用可能会是被并发调用的,如果你没有加锁的情况下,内部的某些属性,你又想让他被初始化一次,这就是不可能的了;当然你在构造方法中可以去调用,那么就涉及到外部的一个线程安全,此时对于很多场景来讲,是推荐使用final,因为它在初始化的时候强制要求被赋值或必须在构造方法中被赋值,不是final类型的,即使你没有对它做任何操作,它在构造父亲类Object的时候会给所有的属性做一次初始化操作,使得这些变量的值是“老”值;当某个线程获取到对象的引用后,调用相关的初始化方法来初始化,而第二个线程进来的时候,发现还未被赋值,继续初始化,等等会产生各种问题。 而还有一类比较重要的问题,就是当一个对象被定义为final,也就是不可以改变的对象,这个对象内有很多属性也不可以改变,此时虽然定义成了final,但是如果提供了对该对象的get方法,外部线程获取到后同样可以修改内部的属性,所以要将内部属性不可改变,同样需要将其定义为final。 某些变量是内部使用的值,子类可能也会被使用,那么可能会被定义为protected类型的,这些类行的方法和属性通常是不会被访问到的,但是通过继承或内部实例就,可以在内部使用一个匿名块或方法,然后使用this访问到这些属性或方法,从而进一步得到数据,所以protected的一些属性在java并发编程中也是需要被慎重使用的。 ThreadLocal: ThreadLocal已经在专门的文章中讲到,请参看文章: ThreadLocal实现方式&使用介绍---无锁化线程封闭 拷贝实现不可变和线程安全: 上面提及到了某些共享的数据是不可变的,可能是一个对象、数组或某个集合类等,虽然我们在管理这些数据的时候使用了final,但是他们本身内部的属性并非final,例如数组获取到后,可以对数组内部的某个下标做修改,而集合类对象也是如此; 在这种情况还有一种方式就是拷贝,将数据拷贝一份给使用者,使用者的修改并不会影响原有数据的信息,也许使用者的确会根据这些模板来做一些个性化的调整(Prototype),此时的方法就是利用克隆,而数组也可以使用Arrays.copyOf方法来操作,集合类就使用Collections里面的相关方法;但是要注意的是,这些拷贝方法就是拷贝当前这一层,不论是克隆还是下面的拷贝,如果还有深入引用,需要自己进一步去拷贝才可以达到效果,否则更深一层的内容的修改同样会影响这些数据;例如,一个数组中每个引用都引用了一个Person对象,那么拷贝的结果并没有创建很多新的Person对象,而是只生成一个新的数组,将原有数组上所有指向Person的地址内容拷贝过来而已。 事实不可变: 什么叫做事实不可变,就是说这个变量虽然我没有定义为final,而且多线程会访问,但是他在运行时是不会改变的,也就是语法上允许改变,但是业务代码不会有对他的写操作;那么访问这些对象或变量是无需加锁的,他们被任意组装到数组、集合类或对象中,只要数组和集合类或对象本身是线程安全的,访问他们都是线程安全的。 也就是你知道这个对象的内容是不会变化的,你就无需对他进行锁操作,以提高程序的整体性能,避免不必要的锁开销。 原子性: 这里提到的原子性,就是指对某个内存单元进行读写操作是一致的,类似一次count++的操作,会经历:获取count的值、在CPU上计算结果、将count的结果写回到内存单元; 而volatile只能保证一个点上的一致性的,不能保证一个过程,所以要保证过程的一致性,就需要有锁的概念引入,synchronized、Lock系列我们会在后续的文章中介绍,而对于单个内存单元来讲,我们实用Atomic系列的功能就足以解决,它采用CAS的方式完成,基于unsafe提供的compareAndSwapXXX三个核心方法,这是CPU上的条件指令,也就是每次修改完后会做一次对比,若一致就认为成功,否则失败返回falase,那么对于可见性的volatile加上他们的组合,就可以完成CAS的功能。 关于Atomic系列的文章,在: Java JUC之Atomic系列12大类实例讲解和原理分解 包含老Atomic类对基本变量、引用、数组等内容的一致性修改操作;Atomic系列基于volatile来实现,锁机制比volatile更加强,对于内存单元的访问,它的速度比volatile要更低一些,但是内存单元的修改来讲,它在并发编程中是最简单的,除了Lock和synchronized外的一个选择,大部分情况下他在对单个内存单元上的修改的性能要比Lock和Synchronized要好。 在java并发编程中,本文是一个引导性的作用,认识到了多线程访问的重要性,接下来就是针对问题如何去解决,当然本文也给出了一些基本的变量处理方式,但是JUC中还有很多的内容,需要逐步去挖掘,例如我们即将要介绍的锁机制和并发集合类的相关操作。 本文先介绍到这里,相信对于以前没接触过并发编程的人来讲,有点晕,没事,多理解下就不晕了。
在C、C++中有很多排序算法,但是通常排序算法不得不让程序员在写代码的过程中陷入对底层很多指针和位置的理解,java不希望这样,所以排序大多可以由java帮你做掉,例如,你要对一个数组排序,就通过:Collections.sort(list)那么这个list就被排序了,排序最终调用的是Arrays.sort方法来完成的,所以数组自然是用Arrays.sort了,而SortedSet里面内部也有排序功能也是类似的方式的来实现的,只是内部调用了相关的方法来完成而已;SortedSet只是一个接口,实现类有很多,本文以TreeSet实现类作为例子。 而排序必然就存在对比大小,那么传递的信息,java是通过什么来对比大小的呢?compareTo这个来对比的,而内部对比过程中,需要将数据转换为Comparable来对比,所以你的对象就需要implementsComparable,并实现内部的方法compareTo,只要你的compareTo实现是你所想要的,那么排序必然是正确的,那么是否还有其他的方法,有的,排序的时候,允许你传入一个对比类,因为这样也可以减少一些空指针出现的可能性,传入的类需要实现:Comparator接口,实现其方法:compare类,虽然接口中还定义了equals方法基本不用管它,因为Object就已经实现了,并且内部排序中并没有用到equals方法来做排序。 下面开始使用实例分别来做中文排序、对象排序,并分别使用对象实现Comparable接口,以及单独定义排序对象实现Comparator接口来完成排序: 实例1(通过实现Comparator接口完成中文排序): import java.text.Collator; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; public class ChineseSortCompare { @SuppressWarnings("rawtypes") private final static Comparator CHINA_COMPARE = Collator.getInstance(java.util.Locale.CHINA); public static void main(String []args) { sortArray(); sortList(); System.out.println("李四".compareTo("张三"));//前者大于后者,则为正数,否则为负数,相等为0 } @SuppressWarnings("unchecked") private static void sortList() { List<String>list = Arrays.asList("张三", "李四", "王五"); Collections.sort(list , CHINA_COMPARE); for(String str : list) { System.out.println(str); } } @SuppressWarnings("unchecked") private static void sortArray() { String[] arr = {"张三", "李四", "王五"}; Arrays.sort(arr, CHINA_COMPARE); for(String str : arr) { System.out.println(str); } } } 可以看到输出的结果都是一样的,当然String本身有compare方法,而且其本身也是实现了Comparable接口的,所以你如果不放入CHINA_COMPARE来进行处理的话,将会默认按照String自己的compareTo来做排序,排序的结果自然不是你想要的,当然英文应该是你想要的。 实例2(通过外部定义Comparator来完成对象排序): 这里首先要构造一个对象的类,为了简单,我们就用两属性,定义一个UserDO这样一个类,描述如下: public class UserDO { protected String name; protected String email; public UserDO() {} public UserDO(String name , String email) { this.name = name; this.email = email; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } } 定义了两个属性为name和email,此时我们想要按照name了排序,那么我们定义排序的类如下: import java.text.Collator; import java.util.Comparator; public class UserDOComparator implements Comparator<UserDO> { Collator cmp = Collator.getInstance(java.util.Locale.CHINA); @Override public int compare(UserDO userDO1, UserDO userDO2) { return cmp.compare(userDO1.getName(), userDO2.getName()); } } 此时可以看出我们实现了compare方法,是使用拼音排序的,然后我们来模拟一些数据验证结果: import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; public class SortUserListTest { private final static UserDOComparator USER_COMPARATOR = new UserDOComparator(); public static void main(String []args) { sortUserDOArray(); sortUserDOList(); sortUserBySortedSet(); } private static void sortUserBySortedSet() { SortedSet<UserDO>userSet = new TreeSet<UserDO>(USER_COMPARATOR); userSet.add(new UserDO("张三" , "aaazhangsan@ddd.com")); userSet.add(new UserDO("李四" , "ddlisi@dsfds.com")); userSet.add(new UserDO("王五" , "ddwangwu@fsadfads.com")); for(UserDO userDO : userSet) { System.out.println(userDO.getName()); } } private static void sortUserDOList() { List<UserDO>list = Arrays.asList( new UserDO("张三" , "aaazhangsan@ddd.com"), new UserDO("李四" , "ddlisi@dsfds.com"), new UserDO("王五" , "ddwangwu@fsadfads.com") ); Collections.sort(list , USER_COMPARATOR); for(UserDO userDO : list) { System.out.println(userDO.getName()); } } private static void sortUserDOArray() { UserDO []userDOArray = new UserDO[] { new UserDO("张三" , "aaazhangsan@ddd.com"), new UserDO("李四" , "ddlisi@dsfds.com"), new UserDO("王五" , "ddwangwu@fsadfads.com") }; Arrays.sort(userDOArray , USER_COMPARATOR); for(UserDO userDO : userDOArray) { System.out.println(userDO.getName()); } } } 根据这些输入,你可以看到它的输出和实际想要的按照名称的拼音排序是一致的,那么有人会问,如果我按照两个字段排序,先按照一个字段排序,再按照另一个字段排序该怎么办,其次如果是倒叙应该是如何操作,其实倒叙来讲只需要在compare方法中将原有的输出改成相反数就可以了,compare得到的结果为正数、负数、或0,若为正数,代表第一个数据比第二个大,而负数相反,为0的时候代表相等;而多字段排序也是如此,通过第一层排序后得到结果,看是否是0,如果是0,那么就再按照第二个字段排序即可,否则就直接返回第一层返回的结果,两者混合应用以及多层排序自然就实现了。 实例3(将上面的UserDO使用一个叫UserComparableDO在类的基础上进行排序) 首先将UserDO重新编写为UserComparableDO: import java.text.Collator; import java.util.Comparator; public class UserComparableDO extends UserDO implements Comparable<UserDO> { public UserComparableDO() {} public UserComparableDO(String name , String email) { this.name = name; this.email = email; } @SuppressWarnings("rawtypes") private final static Comparator CHINA_COMPARE = Collator.getInstance(java.util.Locale.CHINA); @SuppressWarnings("unchecked") @Override public int compareTo(UserDO userDO) { return CHINA_COMPARE.compare(this.getName(), userDO.getName()); } } 当然这段代码里面直接在里面定义一个Comparator是不正确的,一般这个东西是被抽象到系统某些公共的Commons组件里面的,其次,如果原本没有UserDO类,相应的属性写一次即可,我这里原本有UserDO所有直接集成,减少很多代码。 此时就不需要自己再去写一个Comparator了,就可以直接排序了,下面是我们的测试程序: import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; public class SortUserListByComparable { public static void main(String []args) { sortUserBySortedSet(); sortUserDOList(); sortUserDOArray(); } private static void sortUserBySortedSet() { SortedSet<UserComparableDO>userSet = new TreeSet<UserComparableDO>(); userSet.add(new UserComparableDO("张三" , "aaazhangsan@ddd.com")); userSet.add(new UserComparableDO("李四" , "ddlisi@dsfds.com")); userSet.add(new UserComparableDO("王五" , "ddwangwu@fsadfads.com")); for(UserComparableDO userDO : userSet) { System.out.println(userDO.getName()); } } private static void sortUserDOList() { List<UserComparableDO>list = Arrays.asList( new UserComparableDO("张三" , "aaazhangsan@ddd.com"), new UserComparableDO("李四" , "ddlisi@dsfds.com"), new UserComparableDO("王五" , "ddwangwu@fsadfads.com") ); Collections.sort(list); for(UserComparableDO userDO : list) { System.out.println(userDO.getName()); } } private static void sortUserDOArray() { UserComparableDO []userDOArray = new UserComparableDO[] { new UserComparableDO("张三" , "aaazhangsan@ddd.com"), new UserComparableDO("李四" , "ddlisi@dsfds.com"), new UserComparableDO("王五" , "ddwangwu@fsadfads.com") }; Arrays.sort(userDOArray); for(UserComparableDO userDO : userDOArray) { System.out.println(userDO.getName()); } } } 可以看到本次排序中没有再使用自定义的Comparator作为参数,另外TreeSet的入口参数也没有再传入这些参数。 结果知道了,我们简单看看相关的源码来证实这个说法,我们首先来看Collections.sort方法: 源码片段1:Collections.sort(List<T> list) public static <T extends Comparable<? super T>> void sort(List<T> list) { Object[] a = list.toArray(); Arrays.sort(a); ListIterator<T> i = list.listIterator(); for (int j=0; j<a.length; j++) { i.next(); i.set((T)a[j]); } } 此时直接调用了Arrays.sort(a)来排序后,将数组的数据写回到list,另外根据方法的定义,泛型T要求传入的类必须是Comparable类的子类或实现类,所以要调用Collections.sort(list)这个方法,传入的list中包含的每行数据必须是implements Comparable这个接口的,否则编译时就会报错。 再看重载方法,传入自定义的Comparator 源码片段2:Collections.sort(List<T> list, Comparator<? super T> c) public static <T> void sort(List<T> list, Comparator<? super T> c) { Object[] a = list.toArray(); Arrays.sort(a, (Comparator)c); ListIterator i = list.listIterator(); for (int j=0; j<a.length; j++) { i.next(); i.set(a[j]); } } 也是和第一个方法类似,就是调用了Arrays.sort相应的重载方法,看来都是在Arrays里面是实现的,那么就进一步向下看: 源码片段3:Arrays.sort(T[]t): public static void sort(Object[] a) { Object[] aux = (Object[])a.clone(); mergeSort(aux, a, 0, a.length, 0); } 看来代码片段交给了mergeSort来处理,而对数组做了一次克隆,作为排序的基础数据,而原来的数组作为排序的目标,mergeSort的代码片段应该是核心部分,我们先放在这里,先看下sort的另一个重载方法,另外需要注意,这里并没有像Collections.sort(List<T>list)那样在编译时检查类型,也就是在使用这个方法的时候,数组里面的每行并没有implements Comparable也会不会出错,只是在运行时会报错而已,在下面的源码中会有说明。 源码片段4 : Arrays.sort(T[]t, Comparator<? super T> c) public static <T> void sort(T[] a, Comparator<? super T> c) { T[] aux = (T[])a.clone(); if (c==null) mergeSort(aux, a, 0, a.length, 0); else mergeSort(aux, a, 0, a.length, 0, c); } 看来mergeSort也进行了重载,也就是当传入了自定义的Comparator和不传入自定义的Comparator是调用不同的方法来实现的,然后我们来看下两个方法的实现。 源码片段5:mergeSort(Object[]src , Object[]dst , int low , int high , int off) private static void mergeSort(Object[] src, Object[] dest, int low, int high, int off) { int length = high - low; 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; } int destLow = low; int destHigh = high; low += off; high += off; int mid = (low + high) >>> 1; mergeSort(dest, src, low, mid, -off); mergeSort(dest, src, mid, high, -off); if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) { System.arraycopy(src, low, dest, destLow, length); return; } 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++]; } } /** * 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; } 仔细阅读代码可以发现排序是分段递归回调的方式来排序(注意中间的low和high两个参数的变化),每次如果分段的大小大于INSERTIONSORT_THRESHOLD(定义为7)的时候,则再分段,前一段和后一段,然后分开的两段再调用递推,递推后再回归排序,若发现中间分隔的位置两个数据是有序,则认为两段是完全有序的,若不是,那么再将两段做一次排序,此时排序就很好排序了,因为两个块是排序排好的,所以不需要两次循环,只需要循环扫描下去,两个数组按照顺序向下走,分别对比出最小值写入数组,较大者暂时不写入数组与另一个数组的下一个值进行对比,最后一截数据(源码中是通过越界来判定的)写入到尾巴当中: 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++]; } 这段对两个有序数组的排序是很经典的写法,主要是if语句的浓缩,不然代码会写得很长。 注意:这里的代码排序中使用了强制类型转换为Comparable来调用内部的comareTo方法,所以如果你的类没有implements Comparable那么在Collections.sort(List<T>list)时编译时会报错上面已经说到,在调用Arrays.sort(Object []t)时,编译时并不会报错,但是运行时会报错为:java.lang.ClassCastExceptionXXXDO cannot be cast to java.lang.Comparable 排序部分我们再看看其重载的mergeSort方法,就是传入了自定义的Comparator的方法 源码片段6: mergeSort(Object[]src,Object[]dst,int low,int high,intoff,Comparator c) private static void mergeSort(Object[] src, Object[] dest, int low, int high, int off, Comparator c) { int length = high - low; if (length < INSERTIONSORT_THRESHOLD) { for (int i=low; i<high; i++) for (int j=i; j>low && c.compare(dest[j-1], dest[j])>0; j--) swap(dest, j, j-1); return; } int destLow = low; int destHigh = high; low += off; high += off; int mid = (low + high) >>> 1; mergeSort(dest, src, low, mid, -off, c); mergeSort(dest, src, mid, high, -off, c); if (c.compare(src[mid-1], src[mid]) <= 0) { System.arraycopy(src, low, dest, destLow, length); return; } for(int i = destLow, p = low, q = mid; i < destHigh; i++) { if (q >= high || p < mid && c.compare(src[p], src[q]) <= 0) dest[i] = src[p++]; else dest[i] = src[q++]; } } 可以发现算法和上一个方法完全一样,唯一的区别就是排序时使用的compare变成了传入的comparator了,其余的没有任何区别。 大概清楚了,此时发现java提供的排序还是比较高效的,大多数情况下你不需要自己去写排序算法,最后我们再看看TreeSet中的在add的时候如何实现排序的,也是分别传入了comparator和没有传入,我们跟着源码里面,可以看到传入了comparator将这个属性设置给了TreeSet里面定义的一个TreeeMap,而TreeMap中的一个属性设置了这个Comparator: 源码片段7:TreeSet以及TreeMap设置Comparator的构造方法 public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<E,Object>(comparator)); } TreeSet(NavigableMap<E,Object> m) { this.m = m; } public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; } 当然没有传入这个Comparator的时候自然没有设置到TreeMap中了,那么我们来看看TreeMap的add方法: 源码片段8:TreeSet#add(E e) public boolean add(E e) { return m.put(e,PRESENT)==null; } 这个m是什么呢?其实通过源码片段7就可以看出,m是开始实例化的一个TreeMap,那么我们就需要看TreeMap的put方法 代码片段9:TreeMap#put(K key , V value) public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { root = new Entry<K,V>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // split comparator and comparable paths Comparator<? super K> cpr = comparator; if (cpr != null) { do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } else { if (key == null) throw new NullPointerException(); Comparable<? super K> k = (Comparable<? super K>) key; do { parent = t; cmp = k.compareTo(t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } Entry<K,V> e = new Entry<K,V>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; fixAfterInsertion(e); size++; modCount++; return null; } 这里判定了是否存在Comparator进行不同方式来写入不同的位置,并没有重载方法,所以实现上也不一定有什么绝对非要如何做,只需要保证代码可读性很好就好,一切为它服务,否则那些过多的设计是属于过度设计,当然并不是说代码设计不重要,但是这些需要适可而止;另外TreeSet里面对于其他的方法也会做排序处理,我们这里仅仅是用add方法来做一个例子而已。 相信你对java的排序有了一些了解,也许本文说了一堆废话,因为本文不是在说排序算法,我们只是告诉你java是如何排序的,你在大部分情况下无需自己写排序算法来完成排序导致一些不必要的bug,而且效率未必有java本身提供的排序算法高效。
其实就Timer来讲就是一个调度器,而TimerTask呢只是一个实现了run方法的一个类,而具体的TimerTask需要由你自己来实现,例如这样: Timer timer = new Timer(); timer.schedule(new TimerTask() { public void run() { System.out.println("abc"); } }, 200000 , 1000); 这里直接实现一个TimerTask(当然,你可以实现多个TimerTask,多个TimerTask可以被一个Timer会被分配到多个Timer中被调度,后面会说到Timer的实现机制就是说内部的调度机制),然后编写run方法,20s后开始执行,每秒执行一次,当然你通过一个timer对象来操作多个timerTask,其实timerTask本身没什么意义,只是和timer集合操作的一个对象,实现它就必然有对应的run方法,以被调用,他甚至于根本不需要实现Runnable,因为这样往往混淆视听了,为什么呢?也是本文要说的重点。 在说到timer的原理时,我们先看看Timer里面的一些常见方法: public void schedule(TimerTask task, long delay) 这个方法是调度一个task,经过delay(ms)后开始进行调度,仅仅调度一次。 public void schedule(TimerTask task, Date time) 在指定的时间点time上调度一次。 public void schedule(TimerTask task, long delay, long period) 这个方法是调度一个task,在delay(ms)后开始调度,每次调度完后,最少等待period(ms)后才开始调度。 public void schedule(TimerTask task, Date firstTime, long period) 和上一个方法类似,唯一的区别就是传入的第二个参数为第一次调度的时间。 public void scheduleAtFixedRate(TimerTask task, long delay, long period) 调度一个task,在delay(ms)后开始调度,然后每经过period(ms)再次调度,貌似和方法:schedule是一样的,其实不然,后面你会根据源码看到,schedule在计算下一次执行的时间的时候,是通过当前时间(在任务执行前得到) + 时间片,而scheduleAtFixedRate方法是通过当前需要执行的时间(也就是计算出现在应该执行的时间)+ 时间片,前者是运行的实际时间,而后者是理论时间点,例如:schedule时间片是5s,那么理论上会在5、10、15、20这些时间片被调度,但是如果由于某些CPU征用导致未被调度,假如等到第8s才被第一次调度,那么schedule方法计算出来的下一次时间应该是第13s而不是第10s,这样有可能下次就越到20s后而被少调度一次或多次,而scheduleAtFixedRate方法就是每次理论计算出下一次需要调度的时间用以排序,若第8s被调度,那么计算出应该是第10s,所以它距离当前时间是2s,那么再调度队列排序中,会被优先调度,那么就尽量减少漏掉调度的情况。 public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period) 方法同上,唯一的区别就是第一次调度时间设置为一个Date时间,而不是当前时间的一个时间片,我们在源码中会详细说明这些内容。 接下来看源码 首先看Timer的构造方法有几种: 构造方法1:无参构造方法,简单通过Tiemer为前缀构造一个线程名称: public Timer() { this("Timer-" + serialNumber()); } 传入是否为后台线程,如果设置为后台线程,则主线程结束后,timer自动结束,而无需使用cancel来完成对timer的结束 构造方法2:传入了是否为后台线程,后台线程当且仅当进程结束时,自动注销掉。 public Timer(boolean isDaemon) { this("Timer-" + serialNumber(), isDaemon); } 另外两个构造方法负责传入名称和将timer启动: public Timer(String name, boolean isDaemon) { thread.setName(name); thread.setDaemon(isDaemon); thread.start(); } 这里有一个thread,这个thread很明显是一个线程,被包装在了Timer类中,我们看下这个thread的定义是: private TimerThread thread = new TimerThread(queue); 而定义TimerThread部分的是: class TimerThread extends Thread { 看到这里知道了,Timer内部包装了一个线程,用来做独立于外部线程的调度,而TimerThread是一个default类型的,默认情况下是引用不到的,是被Timer自己所使用的。 接下来看下有那些属性 除了上面提到的thread,还有一个很重要的属性是: private TaskQueue queue = new TaskQueue(); 看名字就知道是一个队列,队列里面可以先猜猜看是什么,那么大概应该是我要调度的任务吧,先记录下了,接下来继续向下看: 里面还有一个属性是:threadReaper,它是Object类型,只是重写了finalize方法而已,是为了垃圾回收的时候,将相应的信息回收掉,做GC的回补,也就是当timer线程由于某种原因死掉了,而未被cancel,里面的队列中的信息需要清空掉,不过我们通常是不会考虑这个方法的,所以知道java写这个方法是干什么的就行了。 接下来看调度方法的实现: 对于上面6个调度方法,我们不做一一列举,为什么等下你就知道了: 来看下方法: public void schedule(TimerTask task, long delay) 的源码如下: public void schedule(TimerTask task, long delay) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); sched(task, System.currentTimeMillis()+delay, 0); } 这里调用了另一个方法,将task传入,第一个参数传入System.currentTimeMillis()+delay可见为第一次需要执行的时间的时间点了(如果传入Date,就是对象.getTime()即可,所以传入Date的几个方法就不用多说了),而第三个参数传入了0,这里可以猜下要么是时间片,要么是次数啥的,不过等会就知道是什么了;另外关于方法:sched的内容我们不着急去看他,先看下重载的方法中是如何做的 在看看方法: public void schedule(TimerTask task, long delay,long period) 源码为: public void schedule(TimerTask task, long delay, long period) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, System.currentTimeMillis()+delay, -period); } 看来也调用了方法sched来完成调度,和上面的方法唯一的调度时候的区别是增加了传入的period,而第一个传入的是0,所以确定这个参数为时间片,而不是次数,注意这个里的period加了一个负数,也就是取反,也就是我们开始传入1000,在调用sched的时候会变成-1000,其实最终阅读完源码后你会发现这个算是老外对于一种数字的理解,而并非有什么特殊的意义,所以阅读源码的时候也有这些困难所在。 最后再看个方法是: public void scheduleAtFixedRate(TimerTasktask,long delay,long period) 源码为: public void scheduleAtFixedRate(TimerTask task, long delay, long period) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, System.currentTimeMillis()+delay, period); } 唯一的区别就是在period没有取反,其实你最终阅读完源码,上面的取反没有什么特殊的意义,老外不想增加一个参数来表示scheduleAtFixedRate,而scheduleAtFixedRate和schedule的大部分逻辑代码一致,因此用了参数的范围来作为区分方法,也就是当你传入的参数不是正数的时候,你调用schedule方法正好是得到scheduleAtFixedRate的功能,而调用scheduleAtFixedRate方法的时候得到的正好是schedule方法的功能,呵呵,这些讨论没什么意义,讨论实质和重点: 来看sched方法的实现体: private void sched(TimerTask task, long time, long period) { if (time < 0) throw new IllegalArgumentException("Illegal execution time."); synchronized(queue) { if (!thread.newTasksMayBeScheduled) throw new IllegalStateException("Timer already cancelled."); synchronized(task.lock) { if (task.state != TimerTask.VIRGIN) throw new IllegalStateException( "Task already scheduled or cancelled"); task.nextExecutionTime = time; task.period = period; task.state = TimerTask.SCHEDULED; } queue.add(task); if (queue.getMin() == task) queue.notify(); } } queue为一个队列,我们先不看他数据结构,看到他在做这个操作的时候,发生了同步,所以在timer级别,这个是线程安全的,最后将task相关的参数赋值,主要包含nextExecutionTime(下一次执行时间),period(时间片),state(状态),然后将它放入queue队列中,做一次notify操作,为什么要做notify操作呢?看了后面的代码你就知道了。 简言之,这里就是讲task放入队列queue的过程,此时,你可能对queue的结构有些兴趣,那么我们先来看看queue属性的结构TaskQueue: class TaskQueue { private TimerTask[] queue = new TimerTask[128]; private int size = 0; 可见,TaskQueue的结构很简单,为一个数组,加一个size,有点像ArrayList,是不是长度就128呢,当然不是,ArrayList可以扩容,它可以,只是会造成内存拷贝而已,所以一个Timer来讲,只要内部的task个数不超过128是不会造成扩容的;内部提供了add(TimerTask)、size()、getMin()、get(int)、removeMin()、quickRemove(int)、rescheduleMin(long newTime)、isEmpty()、clear()、fixUp()、fixDown()、heapify(); 这里面的方法大概意思是: add(TimerTaskt)为增加一个任务 size()任务队列的长度 getMin()获取当前排序后最近需要执行的一个任务,下标为1,队列头部0是不做任何操作的。 get(inti)获取指定下标的数据,当然包括下标0. removeMin()为删除当前最近执行的任务,也就是第一个元素,通常只调度一次的任务,在执行完后,调用此方法,就可以将TimerTask从队列中移除。 quickRmove(inti)删除指定的元素,一般来说是不会调用这个方法的,这个方法只有在Timer发生purge的时候,并且当对应的TimerTask调用了cancel方法的时候,才会被调用这个方法,也就是取消某个TimerTask,然后就会从队列中移除(注意如果任务在执行中是,还是仍然在执行中的,虽然在队列中被移除了),还有就是这个cancel方法并不是Timer的cancel方法而是TimerTask,一个是调度器的,一个是单个任务的,最后注意,这个quickRmove完成后,是将队列最后一个元素补充到这个位置,所以此时会造成顺序不一致的问题,后面会有方法进行回补。 rescheduleMin(long newTime)是重新设置当前执行的任务的下一次执行时间,并在队列中将其从新排序到合适的位置,而调用的是后面说的fixDown方法。 对于fixUp和fixDown方法来讲,前者是当新增一个task的时候,首先将元素放在队列的尾部,然后向前找是否有比自己还要晚执行的任务,如果有,就将两个任务的顺序进行交换一下。而fixDown正好相反,执行完第一个任务后,需要加上一个时间片得到下一次执行时间,从而需要将其顺序与后面的任务进行对比下。 其次可以看下fixDown的细节为: private void fixDown(int k) { int j; while ((j = k << 1) <= size && j > 0) { if (j < size && queue[j].nextExecutionTime > queue[j+1].nextExecutionTime) j++; // j indexes smallest kid if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime) break; TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp; k = j; } } 这种方式并非排序,而是找到一个合适的位置来交换,因为并不是通过队列逐个找的,而是每次移动一个二进制为,例如传入1的时候,接下来就是2、4、8、16这些位置,找到合适的位置放下即可,顺序未必是完全有序的,它只需要看到距离调度部分的越近的是有序性越强的时候就可以了,这样即可以保证一定的顺序性,达到较好的性能。 最后一个方法是heapify,其实就是将队列的后半截,全部做一次fixeDown的操作,这个操作主要是为了回补quickRemove方法,当大量的quickRmove后,顺序被打乱后,此时将一半的区域做一次非常简单的排序即可。 这些方法我们不在说源码了,只需要知道它提供了类似于ArrayList的东西来管理,内部有很多排序之类的处理,我们继续回到Timer,里面还有两个方法是:cancel()和方法purge()方法,其实就cancel方法来讲,一个取消操作,在测试中你会发现,如果一旦执行了这个方法timer就会结束掉,看下源码是什么呢: public void cancel() { synchronized(queue) { thread.newTasksMayBeScheduled = false; queue.clear(); queue.notify(); // In case queue was already empty. } } 貌似仅仅将队列清空掉,然后设置了newTasksMayBeScheduled状态为false,最后让队列也调用了下notify操作,但是没有任何地方让线程结束掉,那么就要回到我们开始说的Timer中包含的thread为:TimerThread类了,在看这个类之前,再看下Timer中最后一个purge()类,当你对很多Task做了cancel操作后,此时通过调用purge方法实现对这些cancel掉的类空间的回收,上面已经提到,此时会造成顺序混乱,所以需要调用队里的heapify方法来完成顺序的重排,源码如下: public int purge() { int result = 0; synchronized(queue) { for (int i = queue.size(); i > 0; i--) { if (queue.get(i).state == TimerTask.CANCELLED) { queue.quickRemove(i); result++; } } if (result != 0) queue.heapify(); } return result; } 那么调度呢,是如何调度的呢,那些notify,和清空队列是如何做到的呢?我们就要看看TimerThread类了,内部有一个属性是:newTasksMayBeScheduled,也就是我们开始所提及的那个参数在cancel的时候会被设置为false。 另一个属性定义了 private TaskQueue queue; 也就是我们所调用的queue了,这下联通了吧,不过这里是queue是通过构造方法传入的,传入后赋值用以操作,很明显是Timer传递给这个线程的,我们知道它是一个线程,所以执行的中心自然是run方法了,所以看下run方法的body部分是: public void run() { try { mainLoop(); } finally { synchronized(queue) { newTasksMayBeScheduled = false; queue.clear(); // Eliminate obsolete references } } } try很简单,就一个mainLoop,看名字知道是主循环程序,finally中也就是必然执行的程序为将参数为为false,并将队列清空掉。 那么最核心的就是mainLoop了,是的,看懂了mainLoop一切都懂了: private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired; synchronized(queue) { // Wait for queue to become non-empty while (queue.isEmpty() && newTasksMayBeScheduled) queue.wait(); if (queue.isEmpty()) break; // Queue is empty and will forever remain; die // Queue nonempty; look at first evt and do the right thing long currentTime, executionTime; task = queue.getMin(); synchronized(task.lock) { if (task.state == TimerTask.CANCELLED) { queue.removeMin(); continue; // No action required, poll queue again } currentTime = System.currentTimeMillis(); executionTime = task.nextExecutionTime; if (taskFired = (executionTime<=currentTime)) { if (task.period == 0) { // Non-repeating, remove queue.removeMin(); task.state = TimerTask.EXECUTED; } else { // Repeating task, reschedule queue.rescheduleMin( task.period<0 ? currentTime - task.period : executionTime + task.period); } } } if (!taskFired) // Task hasn't yet fired; wait queue.wait(executionTime - currentTime); } if (taskFired) // Task fired; run it, holding no locks task.run(); } catch(InterruptedException e) { } } } 可以发现这个timer是一个死循环程序,除非遇到不能捕获的异常或break才会跳出,首先注意这段代码: while (queue.isEmpty() &&newTasksMayBeScheduled) queue.wait(); 循环体为循环过程中,条件为queue为空且newTasksMayBeScheduled状态为true,可以看到这个状态其关键作用,也就是跳出循环的条件就是要么队列不为空,要么是newTasksMayBeScheduled状态设置为false才会跳出,而wait就是在等待其他地方对queue发生notify操作,从上面的代码中可以发现,当发生add、cancel以及在threadReaper调用finalize方法的时候会被调用,第三个我们基本可以不考虑其实发生add的时候也就是当队列还是空的时候,发生add使得队列不为空就跳出循环,而cancel是设置了状态,否则不会进入这个循环,那么看下面的代码: if (queue.isEmpty()) break; 当跳出上面的循环后,如果是设置了newTasksMayBeScheduled状态为false跳出,也就是调用了cancel,那么queue就是空的,此时就直接跳出外部的死循环,所以cancel就是这样实现的,如果下面的任务还在跑还没运行到这里来,cancel是不起作用的。 接下来是获取一个当前系统时间和上次预计的执行时间,如果预计执行的时间小于当前系统时间,那么就需要执行,此时判定时间片是否为0,如果为0,则调用removeMin方法将其移除,否则将task通过rescheduleMin设置最新时间并排序: currentTime = System.currentTimeMillis(); executionTime = task.nextExecutionTime; if (taskFired = (executionTime<=currentTime)) { if (task.period == 0) { // Non-repeating, remove queue.removeMin(); task.state = TimerTask.EXECUTED; } else { // Repeating task, reschedule queue.rescheduleMin( task.period<0 ? currentTime - task.period : executionTime + task.period); } } 这里可以看到,period为负数的时候,就会被认为是按照按照当前系统时间+一个时间片来计算下一次时间,就是前面说的schedule和scheduleAtFixedRate的区别了,其实内部是通过正负数来判定的,也许java是不想增加参数,而又想增加程序的可读性,才这样做,其实通过正负判定是有些诡异的,也就是你如果在schedule方法传入负数达到的功能和scheduleAtFixedRate的功能是一样的,相反在scheduleAtFixedRate方法中传入负数功能和schedule方法是一样的。 同时你可以看到period为0,就是只执行一次,所以时间片正负0都用上了,呵呵,然后再看看mainLoop接下来的部分: if (!taskFired)// Taskhasn't yet fired; wait queue.wait(executionTime- currentTime); 这里是如果任务执行时间还未到,就等待一段时间,当然这个等待很可能会被其他的线程操作add和cancel的时候被唤醒,因为内部有notify方法,所以这个时间并不是完全准确,在这里大多数情况下是考虑Timer内部的task信息是稳定的,cancel方法唤醒的话是另一回事。 最后: if (taskFired) // Task fired; run it, holding no locks task.run(); 如果线程需要执行,那么调用它的run方法,而并非启动一个新的线程或从线程池中获取一个线程来执行,所以TimerTask的run方法并不是多线程的run方法,虽然实现了Runnable,但是仅仅是为了表示它是可执行的,并不代表它必须通过线程的方式来执行的。 回过头来再看看: Timer和TimerTask的简单组合是多线程的嘛?不是,一个Timer内部包装了“一个Thread”和“一个Task”队列,这个队列按照一定的方式将任务排队处理,包含的线程在Timer的构造方法调用时被启动,这个Thread的run方法无限循环这个Task队列,若队列为空且没发生cancel操作,此时会一直等待,如果等待完成后,队列还是为空,则认为发生了cancel从而跳出死循环,结束任务;循环中如果发现任务需要执行的时间小于系统时间,则需要执行,那么根据任务的时间片从新计算下次执行时间,若时间片为0代表只执行一次,则直接移除队列即可。 但是是否能实现多线程呢?可以,任何东西是否是多线程完全看个人意愿,多个Timer自然就是多线程的,每个Timer都有自己的线程处理逻辑,当然Timer从这里来看并不是很适合很多任务在短时间内的快速调度,至少不是很适合同一个timer上挂很多任务,在多线程的领域中我们更多是使用多线程中的: Executors.newScheduledThreadPool 来完成对调度队列中的线程池的处理,内部通过new ScheduledThreadPoolExecutor来创建线程池的Executor的创建,当然也可以调用: Executors.unconfigurableScheduledExecutorService 方法来创建一个DelegatedScheduledExecutorService其实这个类就是包装了下下scheduleExecutor,也就是这只是一个壳,英文理解就是被委派的意思,被托管的意思。
虽然现在可以说很多程序员会用ThreadLocal,但是我相信大多数程序员还不知道ThreadLocal,而使用ThreadLocal的程序员大多只是知道其然而不知其所以然,因此,使用ThreadLocal的程序员很多时候会被它导入到陷进中去,其实java很多高级机制系列的很多东西都是一把双刃剑,也就是有利必有其弊,那么我们的方法是找到利和弊的中间平衡点,最佳的方式去解决问题。 本文首先说明ThreadLocal能做什么,然后根据功能为什么要用它,如何使用它,最后通过内部说明讲解他的坑在哪里,使用的人应该如何避免坑。 ThreadLocal的定义和用途的概述(我的理解): 它是一个线程级别变量,在并发模式下是绝对安全的变量,也是线程封闭的一种标准用法(除了局部变量外),即使你将它定义为static,它也是线程安全的。 ThreadLocal能做什么呢? 这个一句话不好说,我们不如来看看实际项目中遇到的一些困解:当你在项目中根据一些参数调用进入一些方法,然后方法再调用方法,进而跨对象调用方法,很多层次,这些方法可能都会用到一些相似的参数,例如,A中需要参数a、b、c,A调用B后,B中需要b、c参数,而B调用C方法需要a、b参数,此时不得不将所有的参数全部传递给B,以此类推,若有很多方法的调用,此时的参数就会越来越繁杂,另外,当程序需要增加参数的时候,此时需要对相关的方法逐个增加参数,是的,很麻烦,相信你也遇到过,这也是在C语言面向对象过来的一些常见处理手段,不过我们简单的处理方法是将它包装成对象传递进去,通过增加对象的属性就可以解决这个问题,不过对象通常是有意义的,所以有些时候简单的对象包装增加一些扩展不相关的属性会使得我们class的定义变得十分的奇怪,所以在这些情况下我们在架构这类复杂的程序的时候,我们通过使用一些类似于Scope的作用域的类来处理,名称和使用起来都会比较通用,类似web应用中会有context、session、request、page等级别的scope,而ThreadLocal也可以解决这类问题,只是他并不是很适合解决这类问题,它面对这些问题通常是初期并没有按照scope以及对象的方式传递,认为不会增加参数,当增加参数时,发现要改很多地方的地方,为了不破坏代码的结构,也有可能参数已经太多,已经使得方法的代码可读性降低,增加ThreadLocal来处理,例如,一个方法调用另一个方法时传入了8个参数,通过逐层调用到第N个方法,传入了其中一个参数,此时最后一个方法需要增加一个参数,第一个方法变成9个参数是自然的,但是这个时候,相关的方法都会受到牵连,使得代码变得臃肿不堪。 上面提及到了ThreadLocal一种亡羊补牢的用途,不过也不是特别推荐使用的方式,它还有一些类似的方式用来使用,就是在框架级别有很多动态调用,调用过程中需要满足一些协议,虽然协议我们会尽量的通用,而很多扩展的参数在定义协议时是不容易考虑完全的以及版本也是随时在升级的,但是在框架扩展时也需要满足接口的通用性和向下兼容,而一些扩展的内容我们就需要ThreadLocal来做方便简单的支持。 简单来说,ThreadLocal是将一些复杂的系统扩展变成了简单定义,使得相关参数牵连的部分变得非常容易,以下是我们例子说明: Spring的事务管理器中,对数据源获取的Connection放入了ThreadLocal中,程序执行完后由ThreadLocal中获取connection然后做commit和rollback,使用中,要保证程序通过DataSource获取的connection就是从spring中获取的,为什么要做这样的操作呢,因为业务代码完全由应用程序来决定,而框架不能要求业务代码如何去编写,否则就失去了框架不让业务代码去管理connection的好处了,此时业务代码被切入后,spring不会向业务代码区传入一个connection,它必须保存在一个地方,当底层通过ibatis、spring jdbc等框架获取同一个datasource的connection的时候,就会调用按照spring约定的规则去获取,由于执行过程都是在同一个线程中处理,从而获取到相同的connection,以保证commit、rollback以及业务操作过程中,使用的connection是同一个,因为只有同一个conneciton才能保证事务,否则数据库本身也是不支持的。 其实在很多并发编程的应用中,ThreadLocal起着很重要的重要,它不加锁,非常轻松的将线程封闭做得天衣无缝,又不会像局部变量那样每次需要从新分配空间,很多空间由于是线程安全,所以,可以反复利用线程私有的缓冲区。 如何使用ThreadLocal? 在系统中任意一个适合的位置定义个ThreadLocal变量,可以定义为public static类型(直接new出来一个ThreadLocal对象),要向里面放入数据就使用set(Object),要获取数据就用get()操作,删除元素就用remove(),其余的方法是非public的方法,不推荐使用。 下面是一个简单例子(代码片段1): public class ThreadLocalTest2 { public final static ThreadLocal <String>TEST_THREAD_NAME_LOCAL = new ThreadLocal<String>(); public final static ThreadLocal <String>TEST_THREAD_VALUE_LOCAL = new ThreadLocal<String>(); public static void main(String[]args) { for(int i = 0 ; i < 100 ; i++) { final String name = "线程-【" + i + "】"; final String value = String.valueOf(i); new Thread() { public void run() { try { TEST_THREAD_NAME_LOCAL.set(name); TEST_THREAD_VALUE_LOCAL.set(value); callA(); }finally { TEST_THREAD_NAME_LOCAL.remove(); TEST_THREAD_VALUE_LOCAL.remove(); } } }.start(); } } public static void callA() { callB(); } public static void callB() { new ThreadLocalTest2().callC(); } public void callC() { callD(); } public void callD() { System.out.println(TEST_THREAD_NAME_LOCAL.get() + "\t=\t" + TEST_THREAD_VALUE_LOCAL.get()); } } 这里模拟了100个线程去访问分别设置name和value,中间故意将name和value的值设置成一样,看是否会存在并发的问题,通过输出可以看出,线程输出并不是按照顺序输出,说明是并行执行的,而线程name和value是可以对应起来的,中间通过多个方法的调用,以模实际的调用中参数不传递,如何获取到对应的变量的过程,不过实际的系统中往往会跨类,这里仅仅在一个类中模拟,其实跨类也是一样的结果,大家可以自己去模拟就可以。 相信看到这里,很多程序员都对ThreadLocal的原理深有兴趣,看看它是如何做到的,尽然参数不传递,又可以像局部变量一样使用它,的确是蛮神奇的,其实看看就知道是一种设置方式,看到名称应该是是和Thread相关,那么废话少说,来看看它的源码吧,既然我们用得最多的是set、get和remove,那么就从set下手: set(T obj)方法为(代码片段2): public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } 首先获取了当前的线程,和猜测一样,然后有个getMap方法,传入了当前线程,我们先可以理解这个map是和线程相关的map,接下来如果 不为空,就做set操作,你跟踪进去会发现,这个和HashMap的put操作类似,也就是向map中写入了一条数据,如果为空,则调用createMap方法,进去后,看看(代码片段3): void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } 返现创建了一个ThreadLocalMap,并且将传入的参数和当前ThreadLocal作为K-V结构写入进去(代码片段4): ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } 这里就不说明ThreadLocalMap的结构细节,只需要知道它的实现和HashMap类似,只是很多方法没有,也没有implements Map,因为它并不想让你通过某些方式(例如反射)获取到一个Map对他进一步操作,它是一个ThreadLocal里面的一个static内部类,default类型,仅仅在java.lang下面的类可以引用到它,所以你可以想到Thread可以引用到它。 我们再回过头来看看getMap方法,因为上面我仅仅知道获取的Map是和线程相关的,而通过代码片段3,有一个t.threadLocalMap = new ThreadLocalMap(this, firstValue)的时候,相信你应该大概有点明白,这个变量应该来自Thread里面,我们根据getMap方法进去看看: ThreadLocalMap getMap(Thread t) { return t.threadLocals; } 是的,是来自于Thread,而这个Thread正好又是当前线程,那么进去看看定义就是: ThreadLocal.ThreadLocalMap threadLocals = null; 这个属性就是在Thread类中,也就是每个Thread默认都有一个ThreadLocalMap,用于存放线程级别的局部变量,通常你无法为他赋值,因为这样的赋值通常是不安全的。 好像是不是有点乱,不着急,我们回头先摸索下思路: 1、Thread里面有个属性是一个类似于HashMap一样的东西,只是它的名字叫ThreadLocalMap,这个属性是default类型的,因此同一个package下面所有的类都可以引用到,因为是Thread的局部变量,所以每个线程都有一个自己单独的Map,相互之间是不冲突的,所以即使将ThreadLocal定义为static线程之间也不会冲突。 2、ThreadLocal和Thread是在同一个package下面,可以引用到这个类,可以对他做操作,此时ThreadLocal每定义一个,用this作为Key,你传入的值作为value,而this就是你定义的ThreadLocal,所以不同的ThreadLocal变量,都使用set,相互之间的数据不会冲突,因为他们的Key是不同的,当然同一个ThreadLocal做两次set操作后,会以最后一次为准。 3、综上所述,在线程之间并行,ThreadLocal可以像局部变量一样使用,且线程安全,且不同的ThreadLocal变量之间的数据毫无冲突。 我们继续看看get方法和remove方法,其实就简单了: public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } 通过根据当前线程调用getMap方法,也就是调用了t.threadLocalMap,然后在map中查找,注意Map中找到的是Entry,也就是K-V基本结构,因为你set写入的仅仅有值,所以,它会设置一个e.value来返回你写入的值,因为Key就是ThreadLocal本身。你可以看到map.getEntry也是通过this来获取的。 同样remove方法为: public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } 同样根据当前线程获取map,如果不为空,则remove,通过this来remove。 补充下(2013-6-29),搞忘写有什么坑了,这个ThreadLocal有啥坑呢,大家从前面应该可以看出来,这个ThreadLocal相关的对象是被绑定到一个Map中的,而这个Map是Thread线程的中的一个属性,那么就有一个问题是,如果你不自己remove的话或者说如果你自己的程序中不知道什么时候去remove的话,那么线程不注销,这些被set进去的数据也不会被注销。 反过来说,写代码中除非你清晰的认识到这个对象应该在哪里set,哪里remove,如果是模糊的,很可能你的代码中不会走remove的位置去,或导致一些逻辑问题,另外,如果不remove的话,就要等线程注销,我们在很多应用服务器中,线程是被复用的,因为在内核分配线程还是有开销的,因此在这些应用中线程很难会被注销掉,那么向ThreadLocal写入的数据自然很不容易被注销掉,这些可能在我们使用某些开源框架的时候无意中被隐藏用到,都有可能会导致问题,最后发现OOM得时候数据竟然来自ThreadLocalMap中,还不知道这些数据是从哪里设置进去的,所以你应当注意这个坑,可能不止一个人掉进这个坑里去过。
在java6以后我们不但接触到了Lock相关的锁,也接触到了很多更加乐观的原子修改操作,也就是在修改时我们只需要保证它的那个瞬间是安全的即可,经过相应的包装后可以再处理对象的并发修改,以及并发中的ABA问题,本文讲述Atomic系列的类的实现以及使用方法,其中包含: 基本类:AtomicInteger、AtomicLong、AtomicBoolean; 引用类型:AtomicReference、AtomicReference的ABA实例、AtomicStampedRerence、AtomicMarkableReference; 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray 属性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater 看到这么多类,你是否觉得很困惑,其实没什么,因为你只需要看懂一个,其余的方法和使用都是大同小异的,相关的类会介绍他们之间的区别在哪里,在使用中需要注意的地方即可。 在使用Atomic系列前,我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题,不过它的具体使用并不是本文的重点,本文重点是Atomic系列的内容大多会基于unsafe类中的以下几个本地方法来操作: 对象的引用进行对比后交换,交换成功返回true,交换失败返回false,这个交换过程完全是原子的,在CPU上计算完结果后,都会对比内存的结果是否还是原先的值,若不是,则认为不能替换,因为变量是volatile类型所以最终写入的数据会被其他线程看到,所以一个线程修改成功后,其他线程就发现自己修改失败了。 参数1:对象所在的类本身的对象(一般这里是对一个对象的属性做修改,才会出现并发,所以该对象所存在的类也是有一个对象的) 参数2:这个属性在这个对象里面的相对便宜量位置,其实对比时是对比内存单元,所以需要属性的起始位置,而引用就是修改引用地址(根据OS、VM位数和参数配置决定宽度一般是4-8个字节),int就是修改相关的4个字节,而long就是修改相关的8个字节。 获取偏移量也是通过unsafe的一个方法:objectFieldOffset(Fieldfield)来获取属性在对象中的偏移量;静态变量需要通过:staticFieldOffset(Field field)获取,调用的总方法是:fieldOffset(Fieldfield) 参数3:修改的引用的原始值,用于对比原来的引用和要修改的目标是否一致。 参数4:修改的目标值,要将数据修改成什么。 public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3); public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2); #对long的操作,要看VM是否支持对Long的CAS,因为有可能VM本身不支持,若不支持,此时运算会变成Lock方式,不过现在VM都基本是支持的而已。 public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3); 我们不推荐直接使用unsafe来操作原子变量,而是通过java封装好的一些类来操作原子变量。 实例代码1:AtomicIntegerTest.java import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerTest { /** * 常见的方法列表 * @see AtomicInteger#get() 直接返回值 * @see AtomicInteger#getAndAdd(int) 增加指定的数据,返回变化前的数据 * @see AtomicInteger#getAndDecrement() 减少1,返回减少前的数据 * @see AtomicInteger#getAndIncrement() 增加1,返回增加前的数据 * @see AtomicInteger#getAndSet(int) 设置指定的数据,返回设置前的数据 * * @see AtomicInteger#addAndGet(int) 增加指定的数据后返回增加后的数据 * @see AtomicInteger#decrementAndGet() 减少1,返回减少后的值 * @see AtomicInteger#incrementAndGet() 增加1,返回增加后的值 * @see AtomicInteger#lazySet(int) 仅仅当get时才会set * * @see AtomicInteger#compareAndSet(int, int) 尝试新增后对比,若增加成功则返回true否则返回false */ public final static AtomicInteger TEST_INTEGER = new AtomicInteger(1); public static void main(String []args) throws InterruptedException { final Thread []threads = new Thread[10]; for(int i = 0 ; i < 10 ; i++) { final int num = i; threads[i] = new Thread() { public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } int now = TEST_INTEGER.incrementAndGet(); System.out.println("我是线程:" + num + ",我得到值了,增加后的值为:" + now); } }; threads[i].start(); } for(Thread t : threads) { t.join(); } System.out.println("最终运行结果:" + TEST_INTEGER.get()); } } 代码例子中模拟多个线程并发对AtomicInteger进行增加1的操作,如果这个数据是普通类型,那么增加过程中出现的问题就是两个线程可能同时看到的数据都是同一个数据,增加完成后写回的时候,也是同一个数据,但是两个加法应当串行增加1,也就是加2的操作,甚至于更加特殊的情况是一个线程加到3后,写入,另一个线程写入了2,还越变越少,也就是不能得到正确的结果,在并发下,我们模拟计数器,要得到精确的计数器值,就需要使用它,我们希望得到的结果是11,可以拷贝代码进去运行后看到结果的确是11,顺然输出的顺序可能不一样,也同时可以证明线程的确是并发运行的(只是在输出的时候,征用System.out这个对象也不一定是谁先抢到),但是最终结果的确是11。 相信你对AtomicInteger的使用有一些了解了吧,要知道更多的方法使用,请参看这段代码中定义变量位置的注释,有关于AtomicInteger的相关方法的详细注释,可以直接跟踪进去看源码,注释中使用了简单的描述说明了方法的用途。 而对于AtomicLong呢,其实和AtomicInteger差不多,唯一的区别就是它处理的数据是long类型的就是了; 对于AtomicBoolean呢,方法要少一些,常见的方法就两个: AtomicBoolean#compareAndSet(boolean, boolean) 第一个参数为原始值,第二个参数为要修改的新值,若修改成功则返回true,否则返回false AtomicBoolean#getAndSet(boolean) 尝试设置新的boolean值,直到成功为止,返回设置前的数据 因为boolean值就两个值,所以就是来回改,相对的很多增加减少的方法自然就没有了,对于使用来讲,我们列举一个boolean的并发修改,仅有一个线程可以修改成功的例子: 实例代码2:AtomicBooleanTest.java import java.util.concurrent.atomic.AtomicBoolean; public class AtomicBooleanTest { /** * 主要方法: * @see AtomicBoolean#compareAndSet(boolean, boolean) 第一个参数为原始值,第二个参数为要修改的新值,若修改成功则返回true,否则返回false * @see AtomicBoolean#getAndSet(boolean) 尝试设置新的boolean值,直到成功为止,返回设置前的数据 */ public final static AtomicBoolean TEST_BOOLEAN = new AtomicBoolean(); public static void main(String []args) { for(int i = 0 ; i < 10 ; i++) { new Thread() { public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } if(TEST_BOOLEAN.compareAndSet(false, true)) { System.out.println("我成功了!"); } } }.start(); } } } 这里有10个线程,我们让他们几乎同时去征用boolean值的修改,修改成功者输出:我成功了!此时你运行完你会发现只会输出一个“我成功了!”,说明征用过程中达到了锁的效果。 那么几种基本类型就说完了,我们来看看里面的实现是不是如我们开始说的Unsafe那样,看几段源码即可,我们看下AtomicInteger的一些源码,例如开始用的:incrementAndGet方法,这个,它的源码是: public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } } 可以看到内部有一个死循环,只有不断去做compareAndSet操作,直到成功为止,也就是修改的根本在compareAndSet方法里面,可以去看下相关的修改方法均是这样实现,那么看下compareAndSet方法的body部分是: public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } 可以看到这里使用了unsafe的compareAndSwapInt的方法,很明显this就是指AtomicInteger当前的这个对象(这个对象不用像上面说的它不能是static和final,它无所谓的),而valueOffset的定义是这样的: private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } 可以看出是通过我们前面所述的objectFieldOffset方法来获取的属性偏移量,所以你自己如果定义类似的操作的时候,就要注意,这个属性不能是静态的,否则不能用这个方法来获取。 后面两个参数自然是对比值和需要修改的目标对象的地址。 其实Atomic系列你看到这里,java层面你就知道差不多了,其余的就是特殊用法和包装而已,刚才我们说了unsafe的3个方法无非是地址和值的区别在内存层面是没有本质区别的,因为地址本身也是数字值。 为了说明这个问题,我们就先说Reference的使用: 我们测试一个reference,和boolean测试方式一样,也是测试多个线程只有一个线程能修改它。 实例代码1:AtomicReferenceTest.java import java.util.concurrent.atomic.AtomicReference; public class AtomicReferenceTest { /** * 相关方法列表 * @see AtomicReference#compareAndSet(Object, Object) 对比设置值,参数1:原始值,参数2:修改目标引用 * @see AtomicReference#getAndSet(Object) 将引用的目标修改为设置的参数,直到修改成功为止,返回修改前的引用 */ public final static AtomicReference <String>ATOMIC_REFERENCE = new AtomicReference<String>("abc"); public static void main(String []args) { for(int i = 0 ; i < 100 ; i++) { final int num = i; new Thread() { public void run() { try { Thread.sleep(Math.abs((int)(Math.random() * 100))); } catch (InterruptedException e) { e.printStackTrace(); } if(ATOMIC_REFERENCE.compareAndSet("abc", new String("abc"))) { System.out.println("我是线程:" + num + ",我获得了锁进行了对象修改!"); } } }.start(); } } } 测试结果如我们所料,的确只有一个线程,执行,跟着代码:compareAndSet进去,发现源码中的调用是: public final boolean compareAndSet(V expect, V update) { return unsafe.compareAndSwapObject(this, valueOffset, expect, update); } OK,的确和我们上面所讲一致,那么此时我们又遇到了引用修改的新问题,什么问题呢?ABA问题,什么是ABA问题呢,当某些流程在处理过程中是顺向的,也就是不允许重复处理的情况下,在某些情况下导致一个数据由A变成B,再中间可能经过0-N个环节后变成了A,此时A不允许再变成B了,因为此时的状态已经发生了改变,例如:银行资金里面做一批账目操作,要求资金在80-100元的人,增加20元钱,时间持续一天,也就是后台程序会不断扫描这些用户的资金是否是在这个范围,但是要求增加过的人就不能再增加了,如果增加20后,被人取出10元继续在这个范围,那么就可以无限套现出来,就是ABA问题了,类似的还有抢红包或中奖,比如每天每个人限量3个红包,中那个等级的奖的个数等等。 此时我们需要使用的方式就不是简单的compareAndSet操作,因为它仅仅是考虑到物理上的并发,而不是在业务逻辑上去控制顺序,此时我们需要借鉴数据库的事务序列号的一些思想来解决,假如每个对象修改的次数可以记住,修改前先对比下次数是否一致再修改,那么这个问题就简单了,AtomicStampedReference类正是提供这一功能的,其实它仅仅是在AtomicReference类的再一次包装,里面增加了一层引用和计数器,其实是否为计数器完全由自己控制,大多数我们是让他自增的,你也可以按照自己的方式来标示版本号,下面一个例子是ABA问题的简单演示: 实例代码3(ABA问题模拟代码演示):import java.util.concurrent.atomic.AtomicReference; /** * ABA问题模拟,线程并发中,导致ABA问题,解决方案是使用|AtomicMarkableReference * 请参看相应的例子:AtomicStampedReferenceTest、AtomicMarkableReferenceTest * */ public class AtomicReferenceABATest { public final static AtomicReference <String>ATOMIC_REFERENCE = new AtomicReference<String>("abc"); public static void main(String []args) { for(int i = 0 ; i < 100 ; i++) { final int num = i; new Thread() { public void run() { try { Thread.sleep(Math.abs((int)(Math.random() * 100))); } catch (InterruptedException e) { e.printStackTrace(); } if(ATOMIC_REFERENCE.compareAndSet("abc" , "abc2")) { System.out.println("我是线程:" + num + ",我获得了锁进行了对象修改!"); } } }.start(); } new Thread() { public void run() { while(!ATOMIC_REFERENCE.compareAndSet("abc2", "abc")); System.out.println("已经改为原始值!"); } }.start(); } } 代码中和原来的例子,唯一的区别就是最后增加了一个线程让他将数据修改为原来的值,并一直尝试修改,直到修改成功为止,为什么没有直接用:方法呢getAndSet方法呢,因为我们的目的是要让某个线程先将他修改为abc2后再让他修改回abc,所以需要这样做; 此时我们得到的结果是: 我是线程:41,我获得了锁进行了对象修改! 已经改为原始值! 我是线程:85,我获得了锁进行了对象修改! 当然你的线程编号多半和我不一样,只要征用到就对,可以发现,有两个线程修改了这个字符串,我们是想那一堆将abc改成abc2的线程仅有一个成功,即使其他线程在他们征用时将其修改为abc,也不能再修改。 此时我们通过类来AtomicStampedReference解决这个问题:实例代码4(AtomicStampedReference解决ABA问题): import java.util.concurrent.atomic.AtomicStampedReference; public class AtomicStampedReferenceTest { public final static AtomicStampedReference <String>ATOMIC_REFERENCE = new AtomicStampedReference<String>("abc" , 0); public static void main(String []args) { for(int i = 0 ; i < 100 ; i++) { final int num = i; final int stamp = ATOMIC_REFERENCE.getStamp(); new Thread() { public void run() { try { Thread.sleep(Math.abs((int)(Math.random() * 100))); } catch (InterruptedException e) { e.printStackTrace(); } if(ATOMIC_REFERENCE.compareAndSet("abc" , "abc2" , stamp , stamp + 1)) { System.out.println("我是线程:" + num + ",我获得了锁进行了对象修改!"); } } }.start(); } new Thread() { public void run() { int stamp = ATOMIC_REFERENCE.getStamp(); while(!ATOMIC_REFERENCE.compareAndSet("abc2", "abc" , stamp , stamp + 1)); System.out.println("已经改回为原始值!"); } }.start(); } } 此时再运行程序看到的结果就是我们想要的了,发现将abc修改为abc2的线程仅有一个被访问,虽然被修改回了原始值,但是其他线程也不会再将abc改为abc2。 而类:AtomicMarkableReference和AtomicStampedReference功能差不多,有点区别的是:它描述更加简单的是与否的关系,通常ABA问题只有两种状态,而AtomicStampedReference是多种状态,那么为什么还要有AtomicMarkableReference呢,因为它在处理是与否上面更加具有可读性,而AtomicStampedReference过于随意定义状态,并不便于阅读大量的是和否的关系,它可以被认为是一个计数器或状态列表等信息,java提倡通过类名知道其意义,所以这个类的存在也是必要的,它的定义就是将数据变换为true|false如下: public final static AtomicMarkableReference <String>ATOMIC_MARKABLE_REFERENCE = new AtomicMarkableReference<String>("abc" , false); 操作时使用: ATOMIC_MARKABLE_REFERENCE.compareAndSet("abc", "abc2", false, true); 好了,reference的三个类的种类都介绍了,我们下面要开始说Atomic的数组用法,因为我们开始说到的都是一些简单变量和基本数据,操作数组呢?如果你来设计会怎么设计,Atomic的数组要求不允许修改长度等,不像集合类那么丰富的操作,不过它可以让你的数组上每个元素的操作绝对安全的,也就是它细化的力度还是到数组上的元素,为你做了二次包装,所以如果你来设计,就是在原有的操作上增加一个下标访问即可,我们来模拟一个Integer类型的数组,即:AtomicIntegerArray 实例代码5(AtomicIntegerArrayTest.java) import java.util.concurrent.atomic.AtomicIntegerArray; public class AtomicIntegerArrayTest { /** * 常见的方法列表 * @see AtomicIntegerArray#addAndGet(int, int) 执行加法,第一个参数为数组的下标,第二个参数为增加的数量,返回增加后的结果 * @see AtomicIntegerArray#compareAndSet(int, int, int) 对比修改,参数1:数组下标,参数2:原始值,参数3,修改目标值,修改成功返回true否则false * @see AtomicIntegerArray#decrementAndGet(int) 参数为数组下标,将数组对应数字减少1,返回减少后的数据 * @see AtomicIntegerArray#incrementAndGet(int) 参数为数组下标,将数组对应数字增加1,返回增加后的数据 * * @see AtomicIntegerArray#getAndAdd(int, int) 和addAndGet类似,区别是返回值是变化前的数据 * @see AtomicIntegerArray#getAndDecrement(int) 和decrementAndGet类似,区别是返回变化前的数据 * @see AtomicIntegerArray#getAndIncrement(int) 和incrementAndGet类似,区别是返回变化前的数据 * @see AtomicIntegerArray#getAndSet(int, int) 将对应下标的数字设置为指定值,第二个参数为设置的值,返回是变化前的数据 */ private final static AtomicIntegerArray ATOMIC_INTEGER_ARRAY = new AtomicIntegerArray(new int[]{1,2,3,4,5,6,7,8,9,10}); public static void main(String []args) throws InterruptedException { Thread []threads = new Thread[100]; for(int i = 0 ; i < 100 ; i++) { final int index = i % 10; final int threadNum = i; threads[i] = new Thread() { public void run() { int result = ATOMIC_INTEGER_ARRAY.addAndGet(index, index + 1); System.out.println("线程编号为:" + threadNum + " , 对应的原始值为:" + (index + 1) + ",增加后的结果为:" + result); } }; threads[i].start(); } for(Thread thread : threads) { thread.join(); } System.out.println("=========================>\n执行已经完成,结果列表:"); for(int i = 0 ; i < ATOMIC_INTEGER_ARRAY.length() ; i++) { System.out.println(ATOMIC_INTEGER_ARRAY.get(i)); } } } 计算结果说明:100个线程并发,每10个线程会被并发修改数组中的一个元素,也就是数组中的每个元素会被10个线程并发修改访问,每次增加原始值的大小,此时运算完的结果看最后输出的敲好为原始值的11倍数,和我们预期的一致,如果不是线程安全那么这个值什么都有可能。 而相应的类:AtomicLongArray其实和AtomicIntegerArray操作方法类似,最大区别就是它操作的数据类型是long;而AtomicRerenceArray也是这样,只是它方法只有两个: AtomicReferenceArray#compareAndSet(int, Object, Object) 参数1:数组下标; 参数2:修改原始值对比; 参数3:修改目标值 修改成功返回true,否则返回false AtomicReferenceArray#getAndSet(int, Object) 参数1:数组下标 参数2:修改的目标 修改成功为止,返回修改前的数据 到这里你是否对数组内部的操作应该有所了解了,和当初预期一样,参数就是多了一个下标,为了完全验证这点,跟踪到源码中可以看到: public final int addAndGet(int i, int delta) { while (true) { int current = get(i); int next = current + delta; if (compareAndSet(i, current, next)) return next; } } 可以看到根据get(i)获取到对应的数据,然后做和普通AtomicInteger差不多的操作,get操作里面有个细节是: public final int get(int i) { return unsafe.getIntVolatile(array, rawIndex(i)); } 这里通过了unsafe获取基于volatile方式获取(可见性)获取一个int类型的数据,而获取的位置是由rawIndex来确定,它的源码是: private long rawIndex(int i) { if (i < 0 || i >= array.length) throw new IndexOutOfBoundsException("index " + i); return base + (long) i * scale; } 可以发现这个结果是一个地址位置,为base加上一耳光偏移量,那么看看base和scale的定义为: private static final int base = unsafe.arrayBaseOffset(int[].class); private static final int scale = unsafe.arrayIndexScale(int[].class); 可以发现unsafe里面提供了对数组base的位置的获取,因为对象是有头部的,而数组还有一个长度位置,第二个很明显是一个数组元素所占用的宽度,也就是基本精度;这里应该可以体会到unsafe所带来的强大了吧。 本文最后要介绍的部分为Updater也就是修改器,它算是Atomic的系列的一个扩展,Atomic系列是为你定义好的一些对象,你可以使用,但是如果是别人已经在使用的对象会原先的代码需要修改为Atomic系列,此时若全部修改类型到对应的对象相信很麻烦,因为牵涉的代码会很多,此时java提供一个外部的Updater可以对对象的属性本身的修改提供类似Atomic的操作,也就是它对这些普通的属性的操作是并发下安全的,分别由:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceUpdater,这样操作后,系统会更加灵活,也就是可能那些类的属性只是在某些情况下需要控制并发,很多时候不需要,但是他们的使用通常有以下几个限制: 限制1:操作的目标不能是static类型,前面说到unsafe的已经可以猜测到它提取的是非static类型的属性偏移量,如果是static类型在获取时如果没有使用对应的方法是会报错的,而这个Updater并没有使用对应的方法。 限制2:操作的目标不能是final类型的,因为final根本没法修改。 限制3:必须是volatile类型的数据,也就是数据本身是读一致的。 限制4:属性必须对当前的Updater所在的区域是可见的,也就是private如果不是当前类肯定是不可见的,protected如果不存在父子关系也是不可见的,default如果不是在同一个package下也是不可见的。 实现方式:通过反射找到属性,对属性进行操作,但是并不是设置accessable,所以必须是可见的属性才能操作。 说了这么多,来个实例看看吧。 实例代码6:(AtomicIntegerFieldUpdaterTest.java) import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; public class AtomicIntegerFieldUpdaterTest { static class A { volatile int intValue = 100; } /** * 可以直接访问对应的变量,进行修改和处理 * 条件:要在可访问的区域内,如果是private或挎包访问default类型以及非父亲类的protected均无法访问到 * 其次访问对象不能是static类型的变量(因为在计算属性的偏移量的时候无法计算),也不能是final类型的变量(因为根本无法修改),必须是普通的成员变量 * * 方法(说明上和AtomicInteger几乎一致,唯一的区别是第一个参数需要传入对象的引用) * @see AtomicIntegerFieldUpdater#addAndGet(Object, int) * @see AtomicIntegerFieldUpdater#compareAndSet(Object, int, int) * @see AtomicIntegerFieldUpdater#decrementAndGet(Object) * @see AtomicIntegerFieldUpdater#incrementAndGet(Object) * * @see AtomicIntegerFieldUpdater#getAndAdd(Object, int) * @see AtomicIntegerFieldUpdater#getAndDecrement(Object) * @see AtomicIntegerFieldUpdater#getAndIncrement(Object) * @see AtomicIntegerFieldUpdater#getAndSet(Object, int) */ public final static AtomicIntegerFieldUpdater <A>ATOMIC_INTEGER_UPDATER = AtomicIntegerFieldUpdater.newUpdater(A.class, "intValue"); public static void main(String []args) { final A a = new A(); for(int i = 0 ; i < 100 ; i++) { final int num = i; new Thread() { public void run() { if(ATOMIC_INTEGER_UPDATER.compareAndSet(a, 100, 120)) { System.out.println("我是线程:" + num + " 我对对应的值做了修改!"); } } }.start(); } } } 此时你会发现只有一个线程可以对这个数据进行修改,其他的方法如上面描述一样,实现的功能和AtomicInteger类似。 而AtomicLongFieldUpdater其实也是这样,区别在于它所操作的数据是long类型。 AtomicReferenceFieldUpdater方法较少,主要是compareAndSet以及getAndSet两个方法的使用,它的定义比数字类型的多一个参数如下: static class A { volatile String stringValue = "abc"; } AtomicReferenceFieldUpdater <A ,String>ATOMIC_REFERENCE_FIELD_UPDATER = AtomicReferenceFieldUpdater.newUpdater(A.class, String.class, "stringValue"); 可以看到,这里传递的参数增加了一个属性的类型,因为引用的是一个对象,对象本身也有一个类型。
前面写了两篇JDBC源码的文章,自己都觉得有点枯燥,先插一段JUC系列的文章来换换胃口,前面有文章大概介绍过J U C包含的东西,JUC体系包含的内容也是非常的多,不是一两句可以说清楚的,我这首先列出将会列举的JUC相关的内容,然后介绍本文的版本:Tools部分 J.U.C体系的主要大板块包含内容,如下图所示: 注意这个里面每个部分都包含很多的类和处理器,而且是相互包含,相互引用的,相互实现的。 说到J UC其实就是说java的多线程等和锁,前面说过一些状态转换,中断等,我们今天来用它的tools来实现一些有些小意思的东西,讲到其他内容的时候,再来想想这写tools是怎么实现的。 tools是本文说要讲到的重点,而tools主要包含哪些东西呢: Tools也包含了5个部分的知识:Executors、Semaphor、Exchanger、CyclicBarrier、CountDownLatch,其实也就是五个工具类,这5个工具类有神马用途呢,就是我们接下来要将的内容了。 Executors: 其实它主要用来创建线程池,代理了线程池的创建,使得你的创建入口参数变得简单,通过方法名便知道了你要创建的线程池是什么样一个线程池,功能大概是什么样的,其实线程池内部都是统一的方法来实现,通过构造方法重载,使得实现不同的功能,但是往往这种方式很多时候不知道具体入口参数的改变有什么意思,除非读了源码才知道,此时builder模式的方式来完成,builder什么样的东西它告诉你就可以。 常见的方法有(都是静态方法): 1、创建一个指定大小的线程池,如果超过大小,放入blocken队列中,默认是LinkedBlockingQueue,默认的ThreadFactory为:Executors.defaultThreadFactory(),是一个Executors的一个内部类。 Executors.newFixedThreadPool(int) 内部实现是: public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } 2、创建一个指定大小的线程池,如果超过大小,放入blocken队列中,默认是LinkedBlockingQueue,自己指定ThreadFactory,自己写的ThreadFactory,必须implements ThreadFactory,实现方法:newThread(Runnable)。 Executors.newFixedThreadPool(int,ThreadFactory) 内部实现是: public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory); } 3、创建线程池长度为1的,也就是只有一个长度的线程池,多余的必须等待,它和调用Executors.newFixedThreadPool(1)得到的结果一样: Executors.newSingleThreadExecutor() 内部实现是: public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }是不是蛮简单的,就是在变参数,你自己也可以new的。 4、和方法3类似,可以自定义ThreadFactory,这里就不多说了! 5、创建可以进行缓存的线程池,默认缓存60s,数据会放在一个SynchronousQueue上,而不会进入blocken队列中,也就是只要有线程进来就直接进入调度,这个不推荐使用,因为容易出问题,除非用来模拟一些并发的测试: Executors.newCachedThreadPool(); 内部实现为: public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }6、和方法5类似,增加自定义ThreadFactory 7、添加一个Schedule的调度器的线程池,默认只有一个调度: Executors.newSingleThreadScheduledExecutor(); 内部实现为(这里可以看到不是用ThreadPoolExector了,schedule换了一个类,内部实现通过ScheduledThreadPoolExecutor类里面的内部类ScheduledFutureTask来实现的,这个内部类是private,默认是引用不到的哦): public static ScheduledExecutorService newSingleThreadScheduledExecutor() { return new DelegatedScheduledExecutorService (new ScheduledThreadPoolExecutor(1)); }8、和7一样,增加自己定义的ThreadFactory 9、添加一个schedule的线程池调度器,和newFixedThreadPool有点类似: Executors.newScheduledThreadPool(); 内部代码为: public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } 其实内部Exectors里面还有一些其他的方法,我们就不多说明了,另外通过这里,大家先可以了解一个大概,知道Exectors其实是一个工具类,提供一系列的静态方法,来完成对对应线程池的形象化创建,所以不用觉得很神奇,神奇的是内部是如何实现的,本文我们不阐述文章中各种线程池的实现,只是大概上有个认识,等到我们专门将Exector系列的时候,我们会详细描述这些细节。 OK,我们继续下一个话题: Semaphor,这个鸟东西是敢毛吃的呢? 答:通过名字就看出来了,是信号量。 信号量可以干什么呢? 答:根据一些阀值做访问控制。 OK,我们这里模拟一个当多个线程并发一段代码的时候,如何控制其访问速度: import java.util.Random; import java.util.concurrent.Semaphore; public class SemaphoreTest { private final static Semaphore MAX_SEMA_PHORE = new Semaphore(10); public static void main(String []args) { for(int i = 0 ; i < 100 ; i++) { final int num = i; final Random radom = new Random(); new Thread() { public void run() { boolean acquired = false; try { MAX_SEMA_PHORE.acquire(); acquired = true; System.out.println("我是线程:" + num + " 我获得了使用权!" + DateTimeUtil.getDateTime()); long time = 1000 * Math.max(1, Math.abs(radom.nextInt() % 10)); Thread.sleep(time); System.out.println("我是线程:" + num + " 我执行完了!" + DateTimeUtil.getDateTime()); }catch(Exception e) { e.printStackTrace(); }finally { if(acquired) { MAX_SEMA_PHORE.release(); } } } }.start(); } } } 这里是简单模拟并发100个线程去访问一段程序,此时要控制最多同时运行的是10个,用到了这个信号量,运行程序用了一个线程睡眠一个随机的时间来代替,你可以看到后面有线程说自己释放了,就有线程获得了,没释放是获取不到的,内部实现方面,我们暂时不管,暂时知道这样用就OK。 接下来: Exchanger十个神马鬼东西呢? 答:线程之间交互数据,且在并发时候使用,两两交换,交换中不会因为线程多而混乱,发送出去没接收到会一直等,由交互器完成交互过程。 啥时候用,没想到案例? 答:的确很少用,而且案例很少,不过的确有这种案例,Exchanger import java.util.concurrent.Exchanger; public class ExchangerTest { public static void main(String []args) { final Exchanger <Integer>exchanger = new Exchanger<Integer>(); for(int i = 0 ; i < 10 ; i++) { final Integer num = i; new Thread() { public void run() { System.out.println("我是线程:Thread_" + this.getName() + "我的数据是:" + num); try { Integer exchangeNum = exchanger.exchange(num); Thread.sleep(1000); System.out.println("我是线程:Thread_" + this.getName() + "我原先的数据为:" + num + " , 交换后的数据为:" + exchangeNum); } catch (InterruptedException e) { e.printStackTrace(); } } }.start(); } } } 这里运行你可以看到,如果某个线程和另一个线程传送了数据,它接受到的数据必然是另一个线程传递给他的,中间步骤由Exchanger去控制,其实你可以说,我自己随机取选择,不过中间的算法逻辑就要复杂一些了。 接下来: CyclicBarrier,关卡模式,搞啥玩意的呢? 答:当你在很多环节需要卡住,要多个线程同时在这里都达到后,再向下走,很有用途。 能否举个例子,有点抽象? 答:团队出去旅行,大家一起先达到酒店住宿,然后一起达到游乐的地方游玩,然后一起坐车回家,每次需要点名后确认相关人员均达到,然后LZ一声令下,触发,大伙就疯子般的出发了。 下面的例子也是以旅游的方式来呈现给大家: import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class BarrierTest { private static final int THREAD_COUNT = 10; private final static CyclicBarrier CYCLIC_BARRIER = new CyclicBarrier(THREAD_COUNT , new Runnable() { public void run() { System.out.println("======>我是导游,本次点名结束,准备走下一个环节!"); } } ); public static void main(String []args) throws InterruptedException, BrokenBarrierException { for(int i = 0 ; i < 10 ; i++) { new Thread(String.valueOf(i)) { public void run() { try { System.out.println("我是线程:" + this.getName() + " 我们达到旅游地点!"); CYCLIC_BARRIER.await(); System.out.println("我是线程:" + this.getName() + " 我开始骑车!"); CYCLIC_BARRIER.await(); System.out.println("我是线程:" + this.getName() + " 我们开始爬山!"); CYCLIC_BARRIER.await(); System.out.println("我是线程:" + this.getName() + " 我们回宾馆休息!"); CYCLIC_BARRIER.await(); System.out.println("我是线程:" + this.getName() + " 我们开始乘车回家!"); CYCLIC_BARRIER.await(); System.out.println("我是线程:" + this.getName() + " 我们到家了!"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }.start(); } } } 测试结果中可以发现,大家一起走到某个步骤后,导游说:“我是导游,本次点名结束,准备走下一个环节!”,然后才会进入下一个步骤,OK,这个有点意思吧,其实赛马也是这个道理,只是赛马通常只有一个步骤,所以我们还有一个方式是: CountDownLatch的方式来完成赛马操作,CountDownLatch是用计数器来做的,所以它不可以被复用,如果要多次使用,就要从新new一个出来才可以。我们下面的代码中,用两组赛马,每组5个参与者来,做一个简单测试: import java.util.concurrent.CountDownLatch; public class CountDownLatchTest { private final static int GROUP_SIZE = 5; public static void main(String []args) { processOneGroup("分组1"); processOneGroup("分组2"); } private static void processOneGroup(final String groupName) { final CountDownLatch start_count_down = new CountDownLatch(1); final CountDownLatch end_count_down = new CountDownLatch(GROUP_SIZE); System.out.println("==========================>\n分组:" + groupName + "比赛开始:"); for(int i = 0 ; i < GROUP_SIZE ; i++) { new Thread(String.valueOf(i)) { public void run() { System.out.println("我是线程组:【" + groupName + "】,第:" + this.getName() + " 号线程,我已经准备就绪!"); try { start_count_down.await();//等待开始指令发出即:start_count_down.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我是线程组:【" + groupName + "】,第:" + this.getName() + " 号线程,我已执行完成!"); end_count_down.countDown(); } }.start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("各就各位,预备!"); start_count_down.countDown();//开始赛跑 try { end_count_down.await();//等待多个赛跑者逐个结束 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("分组:" + groupName + "比赛结束!"); } } 有点意思哈,如果你自己用多线程实现是不是有点麻烦,不过你可以用Thread的join方法来实现,也就是线程的发生join的时候,当前线程(一般是主线程)要等到对应线程执行完run方法后才会进入下一步,为了模拟下,我们也来玩玩: public class ThreadJoinTest { private final static int GROUP_SIZE = 5; public static void main(String []args) throws InterruptedException { Thread []threadGroup1 = new Thread[5]; Thread []threadGroup2 = new Thread[5]; for(int i = 0 ; i < GROUP_SIZE ; i++) { final int num = i; threadGroup1[i] = new Thread() { public void run() { int j = 0; while(j++ < 10) { System.out.println("我是1号组线程:" + num + " 这个是我第:" + j + " 次运行!"); } } }; threadGroup2[i] = new Thread() { public void run() { int j = 0; while(j++ < 10) { System.out.println("我是2号组线程:" + num + " 这个是我第:" + j + " 次运行!"); } } }; threadGroup1[i].start(); } for(int i = 0 ; i < GROUP_SIZE ; i++) { threadGroup1[i].join(); } System.out.println("-==================>线程组1执行完了,该轮到俺了!"); for(int i = 0 ; i < GROUP_SIZE ; i++) { threadGroup2[i].start(); } for(int i = 0 ; i < GROUP_SIZE ; i++) { threadGroup2[i].join(); } System.out.println("全部结束啦!哈哈,回家喝稀饭!"); } } 代码是不是繁杂了不少,呵呵,我们再看看上面的信号量,如果不用工具,自己写会咋写,我们模拟CAS锁,使用Atomic配合完成咋来做呢。也来玩玩,呵呵: import java.util.concurrent.atomic.AtomicInteger; public class ThreadWaitNotify { private final static int THREAD_COUNT = 100; private final static int QUERY_MAX_LENGTH = 2; private final static AtomicInteger NOW_CALL_COUNT = new AtomicInteger(0); public static void main(String []args) throws InterruptedException { Thread []threads = new Thread[THREAD_COUNT]; for(int i = 0 ; i < THREAD_COUNT ; i++) { threads[i] = new Thread(String.valueOf(i)) { synchronized public void run() { int nowValue = NOW_CALL_COUNT.get(); while(true) { if(nowValue < QUERY_MAX_LENGTH && NOW_CALL_COUNT.compareAndSet(nowValue, nowValue + 1)) { break;//获取到了 } try { this.wait(1000); } catch (InterruptedException e) { e.printStackTrace(); } nowValue = NOW_CALL_COUNT.get();//获取一个数据,用于对比 } System.out.println(this.getName() + "======我开始做操作了!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.getName() + "======操作结束了!"); NOW_CALL_COUNT.getAndDecrement(); this.notify(); } }; } for(int i = 0 ; i < THREAD_COUNT ; i++) { threads[i].start(); } } } 还是有点意思哈,这样写就是大部分人对while循环那部分会写晕掉,主要是要不断去判定和尝试,wait()默认是长期等待,但是我们不想让他长期等待,就等1s然后再尝试,其实例子还可以改成wait一个随机的时间范围,这样模拟的效果会更加好一些;另外实际的代码中,如果获取到锁后,notify方法应当放在finally中,才能保证他肯定会执行notify这个方法。 OK,本文就是用,玩,希望玩得有点爽,我们后面会逐步介绍它的实现机制以及一写线程里头很好用,但是大家又不是经常用的东西。
前一篇文章说了一些基本的注册:http://blog.csdn.net/xieyuooo/article/details/8502585 ,本文注重讲究一些核心类的一些方法,后面有时间再写一个jdbc级别错误的问题,注意事项: 本文介绍Connection的一些创建,篇幅所限,不能一一将所有代码贴出,可以跟着这种思路去阅读更为细节的源码为好; 上一篇文章,说到了Driver注册的过程,我们接着Connection的创建,这个要深入到各个Driver,我们以Oracle的Driver为核心来说明,进入驱动类: oracle.jdbc.driver.OracleDriver 看下connect方法(关于url解析过程在上一篇文章中已经说明,这里主要看,调用了一个getConnection方法来获取connection,并设置了相关的参数): public Connection connect(String s, Properties properties) throws SQLException { if(s.regionMatches(0, "jdbc:default:connection", 0, 23)) { String s1 = "jdbc:oracle:kprb"; int j = s.length(); if(j > 23) s = s1.concat(s.substring(23, s.length())); else s = s1.concat(":"); s1 = null; } int i = oracleAcceptsURL(s); if(i == 1) return null; if(i == 2) { DBError.throwSqlException(67); return null; } Hashtable hashtable = parseUrl(s); if(hashtable == null) return null; String s2 = properties.getProperty("user"); String s3 = properties.getProperty("password"); String s4 = properties.getProperty("database"); if(s4 == null) s4 = properties.getProperty("server"); if(s2 == null) s2 = (String)hashtable.get("user"); s2 = parseLoginOption(s2, properties); if(s3 == null) s3 = (String)hashtable.get("password"); if(s4 == null) s4 = (String)hashtable.get("database"); String s5 = (String)hashtable.get("protocol"); properties.put("protocol", s5); if(s5 == null) { DBError.throwSqlException(40, "Protocol is not specified in URL"); return null; } String s6 = properties.getProperty("dll"); if(s6 == null) properties.put("dll", "ocijdbc9"); String s7 = properties.getProperty("prefetch"); if(s7 == null) s7 = properties.getProperty("rowPrefetch"); if(s7 == null) s7 = properties.getProperty("defaultRowPrefetch"); if(s7 != null && Integer.parseInt(s7) <= 0) s7 = null; String s8 = properties.getProperty("batch"); if(s8 == null) s8 = properties.getProperty("executeBatch"); if(s8 == null) s8 = properties.getProperty("defaultExecuteBatch"); if(s8 != null && Integer.parseInt(s8) <= 0) s8 = null; String s9 = properties.getProperty("remarks"); if(s9 == null) s9 = properties.getProperty("remarksReporting"); String s10 = properties.getProperty("synonyms"); if(s10 == null) s10 = properties.getProperty("includeSynonyms"); String s11 = properties.getProperty("restrictGetTables"); String s12 = properties.getProperty("fixedString"); String s13 = properties.getProperty("dataSizeUnits"); String s14 = properties.getProperty("AccumulateBatchResult"); if(s14 == null) s14 = "true"; Enumeration enumeration; for(enumeration = DriverManager.getDrivers(); enumeration.hasMoreElements();) { Driver driver = (Driver)enumeration.nextElement(); if(driver instanceof OracleDriver) break; } while(enumeration.hasMoreElements()) { Driver driver1 = (Driver)enumeration.nextElement(); if(driver1 instanceof OracleDriver) DriverManager.deregisterDriver(driver1); } /** * s5 为协议如thin * s 整个jdbc url串 * s2 为user用户名 * s3 为密码 * s4 为数据库描述信息 * properties 为其他的参数说明 */ Connection connection = getConnectionInstance(s5, s, s2, s3, s4, properties); if(s7 != null) ((oracle.jdbc.driver.OracleConnection)connection).setDefaultRowPrefetch(Integer.parseInt(s7)); if(s8 != null) ((oracle.jdbc.driver.OracleConnection)connection).setDefaultExecuteBatch(Integer.parseInt(s8)); if(s9 != null) ((oracle.jdbc.driver.OracleConnection)connection).setRemarksReporting(s9.equalsIgnoreCase("true")); if(s10 != null) ((oracle.jdbc.driver.OracleConnection)connection).setIncludeSynonyms(s10.equalsIgnoreCase("true")); if(s11 != null) ((oracle.jdbc.driver.OracleConnection)connection).setRestrictGetTables(s11.equalsIgnoreCase("true")); if(s12 != null) ((oracle.jdbc.driver.OracleConnection)connection).setDefaultFixedString(s12.equalsIgnoreCase("true")); if(s13 != null) ((oracle.jdbc.driver.OracleConnection)connection).setDataSizeUnits(s13); ((oracle.jdbc.driver.OracleConnection)connection).setAccumulateBatchResult(s14.equalsIgnoreCase("true")); hashtable = null; return connection; } 进入方法: 参数列表,请参看上一个方法,这里就标示出s代表的是协议,我们通常就是thin private Connection getConnectionInstance(String s, String s1, String s2, String s3, String s4, Properties properties) throws SQLException { Object obj = null; if(s.compareTo("ultra") == 0) { try { Class aclass[] = null; Object aobj[] = new Object[6]; aobj[0] = s; aobj[1] = s1; aobj[2] = s2; aobj[3] = s3; aobj[4] = s4; aobj[5] = properties; Class class1 = Class.forName("oracle.jdbc.ultra.client.Driver"); Method amethod[] = class1.getMethods(); for(int i = 0; i < amethod.length; i++) { if(!amethod[i].getName().equals("getConnection")) continue; aclass = amethod[i].getParameterTypes(); break; } Method method = class1.getMethod("getConnection", aclass); obj = (Connection)method.invoke(class1.newInstance(), aobj); } catch(Exception exception) { exception.printStackTrace(); DBError.throwSqlException(1); } } else { String s5 = null; if(s.equals("thin") && System.getProperty("oracle.jserver.version") != null) s5 = "thin-server"; else if((s.equals("oci8") || s.equals("oci")) && System.getProperty("oracle.jserver.version") != null) s5 = "oci-server"; else s5 = s; String s6 = (String)m_driverAccess.get(s5); if(s6 == null) DBError.throwSqlException(67, "Invalid protocol " + s); DBAccess dbaccess = null; try { dbaccess = (DBAccess)Class.forName(s6).newInstance(); } catch(Exception _ex) { return null; } if(properties.getProperty("is_connection_pooling") == "true") { properties.put("database", s4 != null ? ((Object) (s4)) : ""); obj = new OracleOCIConnection(dbaccess, s1, s2, s3, s4, properties); } else { obj = new oracle.jdbc.driver.OracleConnection(dbaccess, s1, s2, s3, s4, properties); } } return ((Connection) (obj)); } 如果通常是thin的情况下,代码片段,可以看到s5就是"thin",此时m_driverAccess.get(s5)后,得到s6后,通过Class.forName(s6).newInstance()得到dbAccess的实例,这个dbAccess是非常重要的,虽然它还不是我们想要找的OracleConnection,但是可以看到下面去new OracleConnection的时候,是带上这个实例的,m_driverAccess是什么呢? private static Properties m_driverAccess; static { m_driverAccess = new Properties(); m_driverAccess.put("thin-server", "oracle.jdbc.thinserver.ServerTTC7Protocol"); m_driverAccess.put("oci-server", "oracle.jdbc.ociserver.ServerOCIDBAccess"); m_driverAccess.put("thin", "oracle.jdbc.ttc7.TTC7Protocol"); m_driverAccess.put("oci8", "oracle.jdbc.oci8.OCIDBAccess"); m_driverAccess.put("oci", "oracle.jdbc.oci8.OCIDBAccess"); m_driverAccess.put("kprb", "oracle.jdbc.kprb.KprbDBAccess"); 在上面的代码片段中可以看到他是一个Properties,也就是一个Map,可以看出,这里是要找到真正的协议处理类,thin的模式下,我们需要处理协议,有专门的类来处理对应的协议,这里就是要实例化对应的类; 最后通过new oracle.jdbc.driver.OracleConnection就获取到了相关的Connection对象了 也许你和我一样,想看看OracleConnection到底是什么,此时应该和数据库端发起了通信请求,是的,我们继续看看里头是啥,记住我们现在已经看到的是OracleConnection、TTC7Protocol、thin、以及连接串的信息,不然看到里面是晕的; 下面的代码我一般只贴出一些片段,因为方法区太长: 首先来看看被调用的构造方法: public OracleConnection(DBAccess dbaccess, String s, String s1, String s2, String s3, Properties properties) throws SQLException { //.....各种参数赋值,这里省掉了 if(properties != null) { s4 = (String)properties.get("protocol"); String s6 = properties.getProperty("processEscapes"); if(s6 != null && s6.equalsIgnoreCase("false")) m_process_escapes = false; connectionProperties = (Properties)properties.clone(); connectionProperties.remove("password");//将password在链接参数中去掉,安全措施 } initialize(s, s1, s4, dbaccess, null, null, null, s3); logicalHandle = false; try { needLine(); conversion = db_access.logon(s1, s2, s3, properties);//用户名、密码、database描述、扩展参数 m_warning = DBError.addSqlWarning(m_warning, db_access.getWarnings()); if(properties == null || properties.getProperty("connection_pool") != "connection_pool") { default_row_prefetch = db_access.getDefaultPrefetch(); if(properties != null) { String s5 = properties.getProperty("autoCommit"); if(s5 != null && s5.equalsIgnoreCase("false")) flag = false; } setAutoCommit(flag); db_access.initNls(this); } } catch(IOException ioexception) { DBError.throwSqlException(ioexception); } catch(SQLException sqlexception) { try { db_access.logoff(); } catch(IOException _ex) { } catch(SQLException _ex) { } throw sqlexception; } m_txn_mode = 0; } 在看核心方法之前,我们先看下initialize方法里面做的事情: private void initialize(String s, String s1, String s2, DBAccess dbaccess, Hashtable hashtable, Map map1, Map map2, String s3) throws SQLException { initClientDataSupport(); statementCache = null; m_stmtClearMetaData = false; database = s3; url = s; if(s1 != null) user = s1.toUpperCase(); else user = s1; db_access = dbaccess; protocol = s2; physicalStatus = true; default_row_prefetch = DEFAULT_ROW_PREFETCH; default_batch = 1; statement_table = new Hashtable(10); if(hashtable != null) descriptorCache = hashtable; else descriptorCache = new Hashtable(10); map = map1; if(map2 != null) m_javaObjectMap = map2; else m_javaObjectMap = new Hashtable(10); closed = false; trans_level = 2; XA_wants_error = false; UsingXA = false; fdo = null; big_endian = null; m_occ = null; m_privData = null; m_clientIdSet = false; m_clientId = null; } 这里有个很重要的参数设置是:default_row_prefetch的设置,也就是我们要说的每次从数据库端读取数据的行数,默认值为一个DEFAULT_ROW_PREFETCH,这个值为一个全局常量: static int DEFAULT_ROW_PREFETCH = 10; 所以oracle默认就是每次从服务器端获取10行数据出来,cache在应用端; 解析来我们要看logon方法了,里面会比较复杂或者说有点乱,可以喝口水,再看; 开始我们知道dbAccess的实体类是:TTC7Protocol了,所以logon方法自然就是在这个类或这各类的父类里面;看看源码是: /** * s 为用户名 * s1为密码 * s2为数据库描述信息 * */ public synchronized DBConversion logon(String s, String s1, String s2, Properties properties) throws SQLException, IOException { try { if(state > 0) DBError.check_error(428); if(s == null || s1 == null) DBError.check_error(433); if(s.length() == 0 || s1.length() == 0) DBError.check_error(443); if(s2 == null) s2 = "localhost:1521:orcl"; //如果你没有设置连接串,Oracle会自己默认一个,就是一个本地叫orcl的sid,也许oracle认为这个是demo吧 connect(s2, properties);//这个是核心链接类 all7 = new Oall7(MEngine); commoncall = new Ocommoncall(MEngine); opencall = new Oopen(MEngine); close = new Oclose(MEngine); TTCTypeRep _tmp = MEngine.types; describe = (Odscrarr)MEngine.types.newTTIFunObject((byte)1, MEngine); bfileMsg = new v8TTIBfile(MEngine); blobMsg = new v8TTIBlob(MEngine);//建立BLOB通信对象 clobMsg = new v8TTIClob(MEngine);//建立CLOB通信对象 TTCTypeRep _tmp1 = MEngine.types; dty = (TTIdty)MEngine.types.newTTCMsgObject((byte)2, MEngine); dty.marshal();// dty.receive(); //....这里省掉很多代码,是链接创建后续的一些处理,可以继续向下看 //也有挺多东西,但是第一遍不要因为这些代码卡着看整体流程 return MEngine.conv; } catch(SQLException sqlexception) { try { net.disconnect(); } catch(Exception exception) { } state = 0; throw sqlexception; } } 我们进入这个类核心的connect方法: /** * * @param s 数据库地址描述信息 * @param properties * @throws IOException * @throws SQLException */ private void connect(String s, Properties properties) throws IOException, SQLException { if(s == null || properties == null) DBError.check_error(433); net = new NSProtocol(); try { net.connect(s, properties); } catch(NetException netexception) { throw new IOException(netexception.getMessage()); } MEngine = new MAREngine(net); pro = new v8TTIpro(MEngine);//发送一个字节1过去 pro.marshal();//发送字节,获取版本号和字符集 pro.receive();//开启接受 short word0 = pro.getOracleVersion();//获取oracle的版本号码 short word1 = pro.getCharacterSet();//获取oracle字符集 short word2 = TTCConversion.findAccessCharSet(word1, word0); TTCConversion ttcconversion = new TTCConversion(word1, word2, word0, pro.getncharCHARSET()); MEngine.types.setServerConversion(word2 != word1); MEngine.types.setVersion(word0); if(DBConversion.isCharSetMultibyte(word2)) { if(DBConversion.isCharSetMultibyte(pro.getCharacterSet())) MEngine.types.setFlags((byte)1); else MEngine.types.setFlags((byte)2); } else { MEngine.types.setFlags(pro.getFlags()); } MEngine.conv = ttcconversion; } 这里又创建一个NSProtocol类,然后由他的connect方法来创建链接,是有点晕哈,主要是oracle认为不同的协议,有些东西是公用的,所以将这些部分有一个类来处理,当然会设置一些冗余参数而已,也会导致前面判定过的地方再次判定: /** * s为数据库地址描述信息例如:10.233.133.11:1521:orcl */ public void connect(String s, Properties properties) throws IOException, NetException { if(sAtts.connected) throw new NetException(201); if(s == null) throw new NetException(208); addrRes = new AddrResolution(s, properties);//地址描述信息配置 if(addrRes.connection_revised) {//一般我们不用TNS,在thin模式下 s = addrRes.getTNSAddress(); properties = addrRes.getUp(); } if(addrRes.jndi)//一般用的不是JNDI sAtts.profile = new ClientProfile(properties, addrRes.getJndi()); else sAtts.profile = new ClientProfile(properties);//常规一般走这里,设置一些client属性,大多数我们都是默认 establishConnection(s); Object obj4 = null; 还有调用,,很烦人,不过还是再继续向下看:establishConnection吧,哎,要看就要看到底: private SessionAtts establishConnection(String s) throws NetException, IOException { sAtts.cOption = addrRes.resolveAndExecute(s);//执行后就能到一个inputStream和outputStream了 sAtts.ntInputStream = sAtts.cOption.nt.getInputStream(); sAtts.ntOutputStream = sAtts.cOption.nt.getOutputStream(); sAtts.setTDU(sAtts.cOption.tdu); sAtts.setSDU(sAtts.cOption.sdu); sAtts.nsOutputStream = new NetOutputStream(sAtts, 255);//255字节大小的package buffer sAtts.nsInputStream = new NetInputStream(sAtts); return sAtts; } 可以看到,到这里我们可以拿到和数据库之间交互的输入流和输出流了;最关键的就是resolveAndExecute这个方法了 public ConnOption resolveAndExecute(String s) throws NetException, IOException { cs = new ConnStrategy(); if(s.indexOf("//") != -1) resolveUrl(s); else if(s.indexOf(':') != -1 && s.indexOf(')') == -1) resolveSimple(s);//注意这里进去,默认简单的计算,此时判定,有冒号、但是没有括号,其他的方法,是解析不同种类的DB描述符,所以,JDBC的描述符并不是只有一种写法,而是很多 else if(newSyntax) resolveAddrTree(s); else resolveAddr(s); if(!cs.optAvailable()) return cs.execute();//第一次取到时候需要调用这个方法 else return cs.getOption();//后面就直接回去 } 我们看看:resolveSimple这个方法的实现吧(主要是看他的URL怎么解析的): private void resolveSimple(String s) throws NetException { ConnOption connoption = new ConnOption(); int i = 0; int j = 0; int k = 0; if((i = s.indexOf(':')) == -1 || (j = s.indexOf(':', i + 1)) == -1) throw new NetException(115); if((k = s.indexOf(':', j + 1)) != -1) throw new NetException(115); try { connoption.host = s.substring(0, i); connoption.port = Integer.parseInt(s.substring(i + 1, j)); connoption.addr = "(ADDRESS=(PROTOCOL=tcp)(HOST=" + connoption.host + ")(PORT=" + connoption.port + "))"; connoption.sid = s.substring(j + 1, s.length()); String s1 = "(DESCRIPTION=(CONNECT_DATA=(SID=" + connoption.sid + ")(CID=(PROGRAM=)(HOST=__jdbc__)(USER=)))" + "(ADDRESS=" + "(PROTOCOL=tcp)(HOST=" + connoption.host + ")(PORT=" + connoption.port + ")))"; connoption.protocol = "TCP"; connoption.conn_data = new StringBuffer(s1); cs.addOption(connoption); } catch(NumberFormatException _ex) { throw new NetException(116); } } 可以看到,最创建链接前将会将协议解析为TNS的连接串模式,也就是说,你自己也可以讲这个连接串写到JDBC URL的后面;其次协议也被解析成真正的TCP协议,而不是thin什么的,因为这个时候就涉及到交互了; 好,协议解析好了,还得回到上一个方法中,得到了ConnOption类型的对象后(我们目前只知道这个类型里面存放着一些物理协议的属性描述,还没见到真正的链接),回到上一个方法中就看到: if(!cs.optAvailable()) return cs.execute(); else return cs.getOption(); 如果还没有生效,此时要调用执行命令,cs是什么就是开始包装ConnOption的另一个类,或者说,它里面可以包含多个ConnOption;看看他的execute方法是什么: public ConnOption execute() throws NetException { for(int i = 0; i <= cOpts.size() - 1;) try { copt = (ConnOption)cOpts.elementAt(i); copt.connect(); optFound = true; return copt; } catch(IOException _ex) { i++; } throw new NetException(20); } 这里就调用了开始的记录下链接串信息的,一个connect方法,是不是很绕,不过的确很复杂,设计自然层次很多,继续向下看吧: public void connect() throws IOException { nt = getNT(); nt.connect(); } private NTAdapter getNT() throws NetException { try { nt = new TcpNTAdapter(addr); } catch(NLException _ex) { throw new NetException(501); } return nt; } 这里做了两个操作,一个是getNT(),一个调用这个返回值的.connect方法,nt是什么呢,看到下面的getNT方法就是一个处理TcpAdapter的类,前面的所谓协议解析,只是将某种协议,解析为对应的TCP转换,真正处理在这里: public class TcpNTAdapter implements NTAdapter { //构造方法这里就开始解析协议了,以及相关的参数信息,这里可能只关心,协议、HOST、PORT这几个信息 //注意,这里的JDBC URL串是被改完后的,也就是类似TNS中的describe连接串 public TcpNTAdapter(String s) throws NLException { NVNavigator nvnavigator = new NVNavigator(); NVPair nvpair = (new NVFactory()).createNVPair(s); if(nvpair == null) throw new NLException((short)100); NVPair nvpair1 = nvnavigator.findNVPair(nvpair, "PROTOCOL"); NVPair nvpair2 = nvnavigator.findNVPair(nvpair, "HOST"); NVPair nvpair3 = nvnavigator.findNVPair(nvpair, "PORT"); if(nvpair1 == null || nvpair2 == null || nvpair3 == null) throw new NLException((short)100); prot = nvpair1.getAtom(); host = nvpair2.getAtom(); port = Integer.parseInt(nvpair3.getAtom()); if(!prot.equals("TCP") && !prot.equals("tcp")) throw new NLException((short)100); else return; } public void connect() throws IOException { //看到这里的socket是不是很喜悦,因为这个是最基本的socket调用,所以通信 socket = new Socket(host, port); } 关于交互过程,等到下一篇文章我们说prepareStatement相关再说,因为每一样信息的提取都会比较麻烦: 接下来说下MySQL的jdbc Driver获取Connection过程,mysql其实也是类似,因为有oracle为前提,所以我们就简单说下mysql就好: mysql的Driver为:com.mysql.jdbc.Driver 其父类为:com.mysql.jdbc.NonRegisteringDriver,同上,首先来看他的connect方法: public java.sql.Connection connect(String url, Properties info) throws SQLException { if (url != null) { if (StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX)) { return connectLoadBalanced(url, info); } else if (StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) { return connectReplicationConnection(url, info); } } Properties props = null; if ((props = parseURL(url, info)) == null) { return null; } try { Connection newConn = new com.mysql.jdbc.Connection(host(props), port(props), props, database(props), url); return newConn; } catch (SQLException sqlEx) { // Don't wrap SQLExceptions, throw // them un-changed. throw sqlEx; } catch (Exception ex) { throw SQLError.createSQLException(Messages .getString("NonRegisteringDriver.17") //$NON-NLS-1$ + ex.toString() + Messages.getString("NonRegisteringDriver.18"), //$NON-NLS-1$ SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE); } } 关于解析URL部分,已经在上一篇文章中,说明,可以看到这里我们主要关注的是:加粗部分的代码:newcom.mysql.jdbc.Connection这个部分,进去看看: Connection(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException { this.charsetToNumBytesMap = new HashMap(); this.connectionCreationTimeMillis = System.currentTimeMillis(); this.pointOfOrigin = new Throwable(); // Stash away for later, used to clone this connection for Statement.cancel // and Statement.setQueryTimeout(). // this.origHostToConnectTo = hostToConnectTo; this.origPortToConnectTo = portToConnectTo; this.origDatabaseToConnectTo = databaseToConnectTo; try { Blob.class.getMethod("truncate", new Class[] {Long.TYPE}); this.isRunningOnJDK13 = false; } catch (NoSuchMethodException nsme) { this.isRunningOnJDK13 = true; } this.sessionCalendar = new GregorianCalendar(); this.utcCalendar = new GregorianCalendar(); this.utcCalendar.setTimeZone(TimeZone.getTimeZone("GMT")); // // Normally, this code would be in initializeDriverProperties, // but we need to do this as early as possible, so we can start // logging to the 'correct' place as early as possible...this.log // points to 'NullLogger' for every connection at startup to avoid // NPEs and the overhead of checking for NULL at every logging call. // // We will reset this to the configured logger during properties // initialization. // this.log = LogFactory.getLogger(getLogger(), LOGGER_INSTANCE_NAME); // We store this per-connection, due to static synchronization // issues in Java's built-in TimeZone class... this.defaultTimeZone = Util.getDefaultTimeZone(); if ("GMT".equalsIgnoreCase(this.defaultTimeZone.getID())) { this.isClientTzUTC = true; } else { this.isClientTzUTC = false; } this.openStatements = new HashMap(); this.serverVariables = new HashMap(); this.hostList = new ArrayList(); //设置主机 if (hostToConnectTo == null) {//默认主机 this.host = "localhost"; this.hostList.add(this.host); } else if (hostToConnectTo.indexOf(",") != -1) {//多个主机 // multiple hosts separated by commas (failover) StringTokenizer hostTokenizer = new StringTokenizer( hostToConnectTo, ",", false); while (hostTokenizer.hasMoreTokens()) { this.hostList.add(hostTokenizer.nextToken().trim()); } } else {//一个主机,我们通常认为就一个主机 this.host = hostToConnectTo; this.hostList.add(this.host); } this.hostListSize = this.hostList.size(); this.port = portToConnectTo; if (databaseToConnectTo == null) { databaseToConnectTo = ""; } this.database = databaseToConnectTo; this.myURL = url; this.user = info.getProperty(NonRegisteringDriver.USER_PROPERTY_KEY); this.password = info .getProperty(NonRegisteringDriver.PASSWORD_PROPERTY_KEY); if ((this.user == null) || this.user.equals("")) { this.user = ""; } if (this.password == null) { this.password = ""; } this.props = info; initializeDriverProperties(info); try { createNewIO(false); this.dbmd = new DatabaseMetaData(this, this.database); } catch (SQLException ex) { cleanup(ex); // don't clobber SQL exceptions throw ex; } catch (Exception ex) { cleanup(ex); StringBuffer mesg = new StringBuffer(); if (getParanoid()) { mesg.append("Cannot connect to MySQL server on "); mesg.append(this.host); mesg.append(":"); mesg.append(this.port); mesg.append(".\n\n"); mesg.append("Make sure that there is a MySQL server "); mesg.append("running on the machine/port you are trying "); mesg .append("to connect to and that the machine this software is " + "running on "); mesg.append("is able to connect to this host/port " + "(i.e. not firewalled). "); mesg .append("Also make sure that the server has not been started " + "with the --skip-networking "); mesg.append("flag.\n\n"); } else { mesg.append("Unable to connect to database."); } mesg.append("Underlying exception: \n\n"); mesg.append(ex.getClass().getName()); if (!getParanoid()) { mesg.append(Util.stackTraceToString(ex)); } throw SQLError.createSQLException(mesg.toString(), SQLError.SQL_STATE_COMMUNICATION_LINK_FAILURE); } } 可以看到,我们最终要的是createNewIO方法来与数据库通信,其余的都是辅助建立通信的,这个方法很长,注意了,要看加粗部分的代码: protected com.mysql.jdbc.MysqlIO createNewIO(boolean isForReconnect) throws SQLException { MysqlIO newIo = null; Properties mergedProps = new Properties(); mergedProps = exposeAsProperties(this.props); long queriesIssuedFailedOverCopy = this.queriesIssuedFailedOver; this.queriesIssuedFailedOver = 0; try { if (!getHighAvailability() && !this.failedOver) {//如果不是高可用,且不是failover(这里指通过连接池自己做,这样会有多个host) boolean connectionGood = false; Exception connectionNotEstablishedBecause = null; int hostIndex = 0; // // TODO: Eventually, when there's enough metadata // on the server to support it, we should come up // with a smarter way to pick what server to connect // to...perhaps even making it 'pluggable' // if (getRoundRobinLoadBalance()) { hostIndex = getNextRoundRobinHostIndex(getURL(), this.hostList); } for (; hostIndex < this.hostListSize; hostIndex++) { if (hostIndex == 0) { this.hasTriedMasterFlag = true; } try { String newHostPortPair = (String) this.hostList .get(hostIndex); int newPort = 3306; String[] hostPortPair = NonRegisteringDriver .parseHostPortPair(newHostPortPair); String newHost = hostPortPair[NonRegisteringDriver.HOST_NAME_INDEX]; if (newHost == null || newHost.trim().length() == 0) { newHost = "localhost"; } if (hostPortPair[NonRegisteringDriver.PORT_NUMBER_INDEX] != null) { try { newPort = Integer .parseInt(hostPortPair[NonRegisteringDriver.PORT_NUMBER_INDEX]); } catch (NumberFormatException nfe) { throw SQLError.createSQLException( "Illegal connection port value '" + hostPortPair[NonRegisteringDriver.PORT_NUMBER_INDEX] + "'", SQLError.SQL_STATE_INVALID_CONNECTION_ATTRIBUTE); } } this.io = new MysqlIO(newHost, newPort, mergedProps, getSocketFactoryClassName(), this, getSocketTimeout());//获取IO链接 this.io.doHandshake(this.user, this.password, this.database);//登陆 this.connectionId = this.io.getThreadId();//mysql端的线程ID this.isClosed = false; // save state from old connection boolean oldAutoCommit = getAutoCommit(); int oldIsolationLevel = this.isolationLevel; boolean oldReadOnly = isReadOnly(); String oldCatalog = getCatalog(); // Server properties might be different // from previous connection, so initialize // again... initializePropsFromServer(); if (isForReconnect) { // Restore state from old connection setAutoCommit(oldAutoCommit); if (this.hasIsolationLevels) { setTransactionIsolation(oldIsolationLevel); } setCatalog(oldCatalog); } if (hostIndex != 0) { setFailedOverState(); queriesIssuedFailedOverCopy = 0; } else { this.failedOver = false; queriesIssuedFailedOverCopy = 0; if (this.hostListSize > 1) { setReadOnlyInternal(false); } else { setReadOnlyInternal(oldReadOnly); } } connectionGood = true; break; // low-level connection succeeded } catch (Exception EEE) { if (this.io != null) { this.io.forceClose(); } connectionNotEstablishedBecause = EEE; connectionGood = false; if (EEE instanceof SQLException) { SQLException sqlEx = (SQLException)EEE; String sqlState = sqlEx.getSQLState(); // If this isn't a communications failure, it will probably never succeed, so // give up right here and now .... if ((sqlState == null) || !sqlState .equals(SQLError.SQL_STATE_COMMUNICATION_LINK_FAILURE)) { throw sqlEx; } } // Check next host, it might be up... if (getRoundRobinLoadBalance()) { hostIndex = getNextRoundRobinHostIndex(getURL(), this.hostList) - 1 /* incremented by for loop next time around */; } else if ((this.hostListSize - 1) == hostIndex) { throw new CommunicationsException(this, (this.io != null) ? this.io .getLastPacketSentTimeMs() : 0, EEE); } } } if (!connectionGood) { // We've really failed! throw SQLError.createSQLException( "Could not create connection to database server due to underlying exception: '" + connectionNotEstablishedBecause + "'." + (getParanoid() ? "" : Util .stackTraceToString(connectionNotEstablishedBecause)), SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE); } } else { double timeout = getInitialTimeout();//启动延迟,主要是为了保证通信 boolean connectionGood = false; Exception connectionException = null; int hostIndex = 0; if (getRoundRobinLoadBalance()) { hostIndex = getNextRoundRobinHostIndex(getURL(), this.hostList); } for (; (hostIndex < this.hostListSize) && !connectionGood; hostIndex++) { if (hostIndex == 0) { this.hasTriedMasterFlag = true; } if (this.preferSlaveDuringFailover && hostIndex == 0) { hostIndex++; } for (int attemptCount = 0; (attemptCount < getMaxReconnects()) && !connectionGood; attemptCount++) { try { if (this.io != null) { this.io.forceClose(); } String newHostPortPair = (String) this.hostList .get(hostIndex); int newPort = 3306; String[] hostPortPair = NonRegisteringDriver .parseHostPortPair(newHostPortPair); String newHost = hostPortPair[NonRegisteringDriver.HOST_NAME_INDEX]; if (newHost == null || newHost.trim().length() == 0) { newHost = "localhost"; } if (hostPortPair[NonRegisteringDriver.PORT_NUMBER_INDEX] != null) { try { newPort = Integer .parseInt(hostPortPair[NonRegisteringDriver.PORT_NUMBER_INDEX]); } catch (NumberFormatException nfe) { throw SQLError.createSQLException( "Illegal connection port value '" + hostPortPair[NonRegisteringDriver.PORT_NUMBER_INDEX] + "'", SQLError.SQL_STATE_INVALID_CONNECTION_ATTRIBUTE); } } this.io = new MysqlIO(newHost, newPort, mergedProps, getSocketFactoryClassName(), this, getSocketTimeout()); this.io.doHandshake(this.user, this.password, this.database); pingInternal(false); this.connectionId = this.io.getThreadId(); this.isClosed = false; // save state from old connection boolean oldAutoCommit = getAutoCommit(); int oldIsolationLevel = this.isolationLevel; boolean oldReadOnly = isReadOnly(); String oldCatalog = getCatalog(); // Server properties might be different // from previous connection, so initialize // again... initializePropsFromServer(); if (isForReconnect) {//重新链接,设置老的connection参数 // Restore state from old connection setAutoCommit(oldAutoCommit); if (this.hasIsolationLevels) { setTransactionIsolation(oldIsolationLevel); } setCatalog(oldCatalog); } connectionGood = true; if (hostIndex != 0) { setFailedOverState(); queriesIssuedFailedOverCopy = 0; } else { this.failedOver = false; queriesIssuedFailedOverCopy = 0; if (this.hostListSize > 1) { setReadOnlyInternal(false); } else { setReadOnlyInternal(oldReadOnly); } } break; } catch (Exception EEE) { connectionException = EEE; connectionGood = false; // Check next host, it might be up... if (getRoundRobinLoadBalance()) { hostIndex = getNextRoundRobinHostIndex(getURL(), this.hostList) - 1 /* incremented by for loop next time around */; } } if (connectionGood) { break; } if (attemptCount > 0) { try { Thread.sleep((long) timeout * 1000); } catch (InterruptedException IE) { ; } } } // end attempts for a single host } // end iterator for list of hosts if (!connectionGood) { // We've really failed! throw SQLError.createSQLException( "Server connection failure during transaction. Due to underlying exception: '" + connectionException + "'." + (getParanoid() ? "" : Util .stackTraceToString(connectionException)) + "\nAttempted reconnect " + getMaxReconnects() + " times. Giving up.", SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE); } } if (getParanoid() && !getHighAvailability() && (this.hostListSize <= 1)) { this.password = null; this.user = null; } if (isForReconnect) {//是否为重新链接,如果是,将拷贝原有的statements到这个链接上 // // Retrieve any 'lost' prepared statements if re-connecting // Iterator statementIter = this.openStatements.values() .iterator(); // // We build a list of these outside the map of open statements, // because // in the process of re-preparing, we might end up having to // close // a prepared statement, thus removing it from the map, and // generating // a ConcurrentModificationException // Stack serverPreparedStatements = null; while (statementIter.hasNext()) { Object statementObj = statementIter.next(); if (statementObj instanceof ServerPreparedStatement) { if (serverPreparedStatements == null) { serverPreparedStatements = new Stack(); } serverPreparedStatements.add(statementObj); } } if (serverPreparedStatements != null) { while (!serverPreparedStatements.isEmpty()) { ((ServerPreparedStatement) serverPreparedStatements .pop()).rePrepare(); } } } return newIo; } finally { this.queriesIssuedFailedOver = queriesIssuedFailedOverCopy; } } 对于我们来讲,其中最重要的,也是最想看到的两条代码就是: this.io = new MysqlIO(newHost, newPort, mergedProps, getSocketFactoryClassName(), this, getSocketTimeout()); this.io.doHandshake(this.user, this.password, this.database); 那么首先来看下第一条:new MysqlIO,这个类是:com.mysql.jdbc,MySqlIO,对应的构造方法如下: public MysqlIO(String host, int port, Properties props, String socketFactoryClassName, com.mysql.jdbc.Connection conn, int socketTimeout) throws IOException, SQLException { this.connection = conn; if (this.connection.getEnablePacketDebug()) { this.packetDebugRingBuffer = new LinkedList(); } this.logSlowQueries = this.connection.getLogSlowQueries(); this.reusablePacket = new Buffer(INITIAL_PACKET_SIZE); this.sendPacket = new Buffer(INITIAL_PACKET_SIZE); this.port = port; this.host = host; this.socketFactoryClassName = socketFactoryClassName; this.socketFactory = createSocketFactory(); this.mysqlConnection = this.socketFactory.connect(this.host, this.port, props); if (socketTimeout != 0) { try {//设置socket超时 this.mysqlConnection.setSoTimeout(socketTimeout); } catch (Exception ex) { /* Ignore if the platform does not support it */ ; } } this.mysqlConnection = this.socketFactory.beforeHandshake(); if (this.connection.getUseReadAheadInput()) { this.mysqlInput = new ReadAheadInputStream(this.mysqlConnection .getInputStream(), 16384, this.connection .getTraceProtocol(), this.connection.getLog()); } else if (this.connection.useUnbufferedInput()) { this.mysqlInput = this.mysqlConnection.getInputStream(); } else { this.mysqlInput = new BufferedInputStream(this.mysqlConnection .getInputStream(), 16384); } this.mysqlOutput = new BufferedOutputStream(this.mysqlConnection .getOutputStream(), 16384); this.isInteractiveClient = this.connection.getInteractiveClient(); this.profileSql = this.connection.getProfileSql(); this.sessionCalendar = Calendar.getInstance(); this.autoGenerateTestcaseScript = this.connection .getAutoGenerateTestcaseScript(); this.needToGrabQueryFromPacket = (this.profileSql || this.logSlowQueries || this.autoGenerateTestcaseScript); if (this.connection.getUseNanosForElapsedTime() && Util.nanoTimeAvailable()) { this.useNanosForElapsedTime = true; this.queryTimingUnits = Messages.getString("Nanoseconds"); } else { this.queryTimingUnits = Messages.getString("Milliseconds"); } if (this.connection.getLogSlowQueries()) { calculateSlowQueryThreshold(); } } 上面标示出来的socketFactory就是用来创建socket的,创建出来的mysqlConnection就是Socket类型,是不是又很熟悉了: protected Socket mysqlConnection = null; private SocketFactory socketFactory = null; 而SocketFactory是一个接口,实例化是通过createSocketFactory()方法创建的,limit里面有个socketFactoryClassName,也就是要创建的实例的类名,可以再上面的代码中看到这个className是在com.mysql.jdbc.Connection类里面从newMySqlIO时候传入的,MySqlIO中getSocketFactoryClassName方法来获取类名的,可以看看这个类中的对应方法,发现在com.mysql.jdbc.Connection的父类:com.mysql.jdbc.ConnectionProperties中如下定义: public String getSocketFactoryClassName() { return this.socketFactoryClassName.getValueAsString(); } 发现是一个属性: private StringConnectionProperty socketFactoryClassName = new StringConnectionProperty( "socketFactory", StandardSocketFactory.class.getName(), "The name of the class that the driver should use for creating socket connections to the server. This class must implement the interface 'com.mysql.jdbc.SocketFactory' and have public no-args constructor.", "3.0.3", CONNECTION_AND_AUTH_CATEGORY, 4); 继续跟踪你可以发现,这个函数通过getValueAsString可以得到的是StandardSocketFactory.class.getName()这个返回值,所以是通过类:com.mysql.jdbc.StandardSocketFactory来实现的, 找到他的connect方法: public Socket connect(String hostname, int portNumber, Properties props) throws SocketException, IOException { if (props != null) { this.host = hostname; this.port = portNumber; Method connectWithTimeoutMethod = null; Method socketBindMethod = null; Class socketAddressClass = null; String localSocketHostname = props .getProperty("localSocketAddress"); String connectTimeoutStr = props.getProperty("connectTimeout");//超时设置 int connectTimeout = 0; boolean wantsTimeout = (connectTimeoutStr != null && connectTimeoutStr.length() > 0 && !connectTimeoutStr .equals("0")); boolean wantsLocalBind = (localSocketHostname != null && localSocketHostname .length() > 0); boolean needsConfigurationBeforeConnect = socketNeedsConfigurationBeforeConnect(props); if (wantsTimeout || wantsLocalBind || needsConfigurationBeforeConnect) { if (connectTimeoutStr != null) { try { connectTimeout = Integer.parseInt(connectTimeoutStr); } catch (NumberFormatException nfe) { throw new SocketException("Illegal value '" + connectTimeoutStr + "' for connectTimeout"); } } try { // Have to do this with reflection, otherwise older JVMs // croak socketAddressClass = Class .forName("java.net.SocketAddress"); connectWithTimeoutMethod = Socket.class.getMethod( "connect", new Class[] { socketAddressClass, Integer.TYPE }); socketBindMethod = Socket.class.getMethod("bind", new Class[] { socketAddressClass }); } catch (NoClassDefFoundError noClassDefFound) { // ignore, we give a better error below if needed } catch (NoSuchMethodException noSuchMethodEx) { // ignore, we give a better error below if needed } catch (Throwable catchAll) { // ignore, we give a better error below if needed } if (wantsLocalBind && socketBindMethod == null) { throw new SocketException( "Can't specify \"localSocketAddress\" on JVMs older than 1.4"); } if (wantsTimeout && connectWithTimeoutMethod == null) { throw new SocketException( "Can't specify \"connectTimeout\" on JVMs older than 1.4"); } } if (this.host != null) { if (!(wantsLocalBind || wantsTimeout || needsConfigurationBeforeConnect)) { InetAddress[] possibleAddresses = InetAddress .getAllByName(this.host); Throwable caughtWhileConnecting = null; // Need to loop through all possible addresses, in case // someone has IPV6 configured (SuSE, for example...) for (int i = 0; i < possibleAddresses.length; i++) { try { this.rawSocket = new Socket(possibleAddresses[i], port); configureSocket(this.rawSocket, props); break; } catch (Exception ex) { caughtWhileConnecting = ex; } } if (rawSocket == null) { unwrapExceptionToProperClassAndThrowIt(caughtWhileConnecting); } } else { // must explicitly state this due to classloader issues // when running on older JVMs :( try { InetAddress[] possibleAddresses = InetAddress .getAllByName(this.host); Throwable caughtWhileConnecting = null; Object localSockAddr = null; Class inetSocketAddressClass = null; Constructor addrConstructor = null; try { inetSocketAddressClass = Class .forName("java.net.InetSocketAddress"); addrConstructor = inetSocketAddressClass .getConstructor(new Class[] { InetAddress.class, Integer.TYPE }); if (wantsLocalBind) { localSockAddr = addrConstructor .newInstance(new Object[] { InetAddress .getByName(localSocketHostname), new Integer(0 /* * use ephemeral * port */) }); } } catch (Throwable ex) { unwrapExceptionToProperClassAndThrowIt(ex); } // Need to loop through all possible addresses, in case // someone has IPV6 configured (SuSE, for example...) for (int i = 0; i < possibleAddresses.length; i++) { try { this.rawSocket = new Socket();//创建链接 configureSocket(this.rawSocket, props);//做一些扩展配置 Object sockAddr = addrConstructor .newInstance(new Object[] { possibleAddresses[i], new Integer(port) }); // bind to the local port, null is 'ok', it // means // use the ephemeral port socketBindMethod.invoke(rawSocket, new Object[] { localSockAddr }); connectWithTimeoutMethod.invoke(rawSocket, new Object[] { sockAddr, new Integer(connectTimeout) }); break; } catch (Exception ex) { this.rawSocket = null; caughtWhileConnecting = ex; } } if (this.rawSocket == null) { unwrapExceptionToProperClassAndThrowIt(caughtWhileConnecting); } } catch (Throwable t) { unwrapExceptionToProperClassAndThrowIt(t); } } return this.rawSocket; } } throw new SocketException("Unable to create socket"); } 这里Socket就创建了,socket里面进一步的connectionTimeout以及configureSocket里面设置了tcpNoDelay、keepAlive、sendBufferSize、ReceiveBufferSize等信息; 最后再来看下在Conection类中方法createNewIO:当中获取到MySqlIO后,要进行用户名密码校验了: this.io.doHandshake(this.user, this.password , this.database); void doHandshake(String user, String password, String database) throws SQLException { // Read the first packet this.checkPacketSequence = false; this.readPacketSequence = 0; Buffer buf = readPacket(); // Get the protocol version this.protocolVersion = buf.readByte(); if (this.protocolVersion == -1) {//版本检测如果为-1 try { this.mysqlConnection.close(); } catch (Exception e) { ; // ignore } int errno = 2000; errno = buf.readInt(); String serverErrorMessage = buf.readString(); StringBuffer errorBuf = new StringBuffer(Messages.getString( "MysqlIO.10")); //$NON-NLS-1$ errorBuf.append(serverErrorMessage); errorBuf.append("\""); //$NON-NLS-1$ String xOpen = SQLError.mysqlToSqlState(errno, this.connection.getUseSqlStateCodes()); throw SQLError.createSQLException(SQLError.get(xOpen) + ", " //$NON-NLS-1$ +errorBuf.toString(), xOpen, errno); } this.serverVersion = buf.readString(); // Parse the server version into major/minor/subminor int point = this.serverVersion.indexOf("."); //$NON-NLS-1$ if (point != -1) { try { int n = Integer.parseInt(this.serverVersion.substring(0, point)); this.serverMajorVersion = n; } catch (NumberFormatException NFE1) { ; } String remaining = this.serverVersion.substring(point + 1, this.serverVersion.length()); point = remaining.indexOf("."); //$NON-NLS-1$ if (point != -1) { try { int n = Integer.parseInt(remaining.substring(0, point)); this.serverMinorVersion = n; } catch (NumberFormatException nfe) { ; } remaining = remaining.substring(point + 1, remaining.length()); int pos = 0; while (pos < remaining.length()) { if ((remaining.charAt(pos) < '0') || (remaining.charAt(pos) > '9')) { break; } pos++; } try { int n = Integer.parseInt(remaining.substring(0, pos)); this.serverSubMinorVersion = n; } catch (NumberFormatException nfe) { ; } } } if (versionMeetsMinimum(4, 0, 8)) { this.maxThreeBytes = (256 * 256 * 256) - 1; this.useNewLargePackets = true; } else { this.maxThreeBytes = 255 * 255 * 255; this.useNewLargePackets = false; } this.colDecimalNeedsBump = versionMeetsMinimum(3, 23, 0); this.colDecimalNeedsBump = !versionMeetsMinimum(3, 23, 15); // guess? Not noted in changelog this.useNewUpdateCounts = versionMeetsMinimum(3, 22, 5); threadId = buf.readLong(); //线程ID this.seed = buf.readString(); this.serverCapabilities = 0; if (buf.getPosition() < buf.getBufLength()) { this.serverCapabilities = buf.readInt(); } if (versionMeetsMinimum(4, 1, 1)) { int position = buf.getPosition(); /* New protocol with 16 bytes to describe server characteristics */ this.serverCharsetIndex = buf.readByte() & 0xff; this.serverStatus = buf.readInt(); buf.setPosition(position + 16); String seedPart2 = buf.readString(); StringBuffer newSeed = new StringBuffer(20); newSeed.append(this.seed); newSeed.append(seedPart2); this.seed = newSeed.toString(); } if (((this.serverCapabilities & CLIENT_COMPRESS) != 0) && this.connection.getUseCompression()) { this.clientParam |= CLIENT_COMPRESS; } this.useConnectWithDb = (database != null) && (database.length() > 0) && !this.connection.getCreateDatabaseIfNotExist(); if (this.useConnectWithDb) { this.clientParam |= CLIENT_CONNECT_WITH_DB; } if (((this.serverCapabilities & CLIENT_SSL) == 0) && this.connection.getUseSSL()) { if (this.connection.getRequireSSL()) { this.connection.close(); forceClose(); throw SQLError.createSQLException(Messages.getString("MysqlIO.15"), //$NON-NLS-1$ SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE); } this.connection.setUseSSL(false);//不采用SSL } if ((this.serverCapabilities & CLIENT_LONG_FLAG) != 0) { // We understand other column flags, as well this.clientParam |= CLIENT_LONG_FLAG; this.hasLongColumnInfo = true; } // return FOUND rows this.clientParam |= CLIENT_FOUND_ROWS; if (this.connection.getAllowLoadLocalInfile()) { this.clientParam |= CLIENT_LOCAL_FILES; } if (this.isInteractiveClient) { this.clientParam |= CLIENT_INTERACTIVE; } // Authenticate if (this.protocolVersion > 9) { this.clientParam |= CLIENT_LONG_PASSWORD; // for long passwords } else { this.clientParam &= ~CLIENT_LONG_PASSWORD; } // // 4.1 has some differences in the protocol // if (versionMeetsMinimum(4, 1, 0)) { if (versionMeetsMinimum(4, 1, 1)) { this.clientParam |= CLIENT_PROTOCOL_41; this.has41NewNewProt = true; // Need this to get server status values this.clientParam |= CLIENT_TRANSACTIONS; // We always allow multiple result sets this.clientParam |= CLIENT_MULTI_RESULTS; // We allow the user to configure whether // or not they want to support multiple queries // (by default, this is disabled). if (this.connection.getAllowMultiQueries()) { this.clientParam |= CLIENT_MULTI_QUERIES; } } else { this.clientParam |= CLIENT_RESERVED; this.has41NewNewProt = false; } this.use41Extensions = true; } int passwordLength = 16; int userLength = (user != null) ? user.length() : 0; int databaseLength = (database != null) ? database.length() : 0; int packLength = ((userLength + passwordLength + databaseLength) * 2) + 7 + HEADER_LENGTH + AUTH_411_OVERHEAD; Buffer packet = null; if (!this.connection.getUseSSL()) { if ((this.serverCapabilities & CLIENT_SECURE_CONNECTION) != 0) { this.clientParam |= CLIENT_SECURE_CONNECTION; //没有使用SSL if (versionMeetsMinimum(4, 1, 1)) { secureAuth411(null, packLength, user, password, database, true); } else { secureAuth(null, packLength, user, password, database, true); } } else { // Passwords can be 16 chars long 这相当于是一个buffer packet = new Buffer(packLength); if ((this.clientParam & CLIENT_RESERVED) != 0) { if (versionMeetsMinimum(4, 1, 1)) { packet.writeLong(this.clientParam);//发送一个0过去,代表要发起一个请求 packet.writeLong(this.maxThreeBytes);//最大字节数 // charset, JDBC will connect as 'latin1', // and use 'SET NAMES' to change to the desired // charset after the connection is established. packet.writeByte((byte) 8); // Set of bytes reserved for future use. packet.writeBytesNoNull(new byte[23]); } else { packet.writeLong(this.clientParam); packet.writeLong(this.maxThreeBytes); } } else { packet.writeInt((int) this.clientParam); packet.writeLongInt(this.maxThreeBytes); } // User/Password data packet.writeString(user, "Cp1252", this.connection); //写入密码 if (this.protocolVersion > 9) { packet.writeString(Util.newCrypt(password, this.seed), "Cp1252", this.connection); } else { packet.writeString(Util.oldCrypt(password, this.seed), "Cp1252", this.connection); } //写入数据库信息 if (this.useConnectWithDb) { packet.writeString(database, "Cp1252", this.connection); } //将packet里面的信息,写入socket发送出去 send(packet, packet.getPosition()); } } else { negotiateSSLConnection(user, password, database, packLength); } // Check for errors, not for 4.1.1 or newer, // as the new auth protocol doesn't work that way // (see secureAuth411() for more details...) if (!versionMeetsMinimum(4, 1, 1)) { checkErrorPacket(); } // // Can't enable compression until after handshake // if (((this.serverCapabilities & CLIENT_COMPRESS) != 0) && this.connection.getUseCompression()) { // The following matches with ZLIB's // compress() this.deflater = new Deflater(); this.useCompression = true; this.mysqlInput = new CompressedInputStream(this.connection, this.mysqlInput); } if (!this.useConnectWithDb) { changeDatabaseTo(database); } } 最后看到的send方法,就不说代码了,你跟踪进去看看,就知道,他是使用了开始创建好的MySqlIO中的 protected BufferedOutputStream mysqlOutput = null; 这个属性,将数据out.write出去,然后做了一次flush,然后里面通过checkErrorPacket方法来读取MySQL返回的数据,如果返回的第一个字符是0xff,则认为是错误的信息,此时判定错误的内容。 最后我们说下,MySql的另一个Driver,是:com.mysql.jdbc.ReplicationDriver,用于集群下,用的时候没在Connection上,setReadOnlytrue|false就进行主备份切换了,他创建的Connection是:com.mysql.jdbc.ReplicationConnection,我们简单看一些代码: public class ReplicationConnection implements java.sql.Connection, PingTarget { private Connection currentConnection;//当前链接 private Connection masterConnection;//主库链接 private Connection slavesConnection;//备库连接 从这就可以看出,他是封装了两个connection,来回切换,currentConnection为当前使用的那个connection,再随便抽调一些方法出来看看: public synchronized void close() throws SQLException { this.masterConnection.close(); this.slavesConnection.close(); } public synchronized void commit() throws SQLException { this.currentConnection.commit(); } public Statement createStatement() throws SQLException { Statement stmt = this.currentConnection.createStatement(); ((com.mysql.jdbc.Statement) stmt).setPingTarget(this); return stmt; } public synchronized boolean isReadOnly() throws SQLException { return this.currentConnection == this.slavesConnection; } public synchronized void setReadOnly(boolean readOnly) throws SQLException { if (readOnly) { if (currentConnection != slavesConnection) { switchToSlavesConnection(); } } else { if (currentConnection != masterConnection) { switchToMasterConnection(); } } } public synchronized void doPing() throws SQLException { if (this.masterConnection != null) { this.masterConnection.ping();//发送一个14命令号过去,类似于ping命令 } if (this.slavesConnection != null) { this.slavesConnection.ping(); } } 或许你看到这个会有点头晕,不过没事,多看几次就明白了,我这里出来也是个大概,理清楚思路,在处理问题的时候,可以快速定位到源码,然后找到基本依据。
简单说下,本文是说源码的,但是不会一篇文章就说得很深入,本文是【jdbc源码入口篇】,分别会说明一些源码和使用细节,所提及的源码可能相对于jdbc的源码还是初级看源码,看个大概,细节上还有很多东西,后续有时间会跟进; 文章会以oracle、mysql jdbc的实现的源码作为说明的依据来参考; 首先,我们要创建一个链接(连接池是在内部做的),会操作: Class.forName("xxx.xxx.xxxx.xxx");//类名通常为jdbc的Dirver; oracle的一般是: oracle.jdbc.driver.OracleDriver 而mysql通常是: com.mysql.jdbc.Driver sql server通常是(本人很少使用SQL Server,所以文章中不会出现SQL server的Driver细节): com.microsoft.sqlserver.jdbc.SQLServerDriver 然后我们才能用: DriverManager.getConnection() 方法来获取链接;我们知道这个是获取一个Cnnection,那么我们首先来看看DriverManager的getConnection到底做了什么?为什么必须要Class.forName才行; 跟踪DriverManager类进去发现有四个getConnection方法: 1、所有参数全部通过URL传递过去 getConnection(String url); 2、用户名和密码单独传递: getConnection(String url , String user , String password); 3、传递多个参数的K-V结构 getConnection(String url , Properties properties); 4、传递多个参数后,还加上指定的ClassLoader,很少用,当跨ClassLoader访问的时候需要使用到,默认使用当前线程的ClassLoader; getConnection(String url , Properties properties , ClassLoader callerCL); 其实无论如何,会调用到最后一个方法,最后一个方法主要代码为: private static Connection getConnection( String url, java.util.Properties info, ClassLoader callerCL) throws SQLException { java.util.Vector drivers = null; synchronized(DriverManager.class) { if(callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader();//如果没传递CLassLoader用当前线程的 } } if(url == null) { throw new SQLException("The url cannot be null", "08001");//没有传递URL,则抛出异常 } //注意这个日志,默认是打印不出来的,程序内部会去判定logWriter是否为空,若为空则不会输出,默认也没有值 println("DriverManager.getConnection(\"" + url + "\")"); if (!initialized) {//初始化,不过几乎可以忽略 initialize(); } synchronized (DriverManager.class){ drivers = readDrivers; // readDrivers为一个类变量,下面的部分我们看看是如何被注册的 } SQLException reason = null; for (int i = 0; i < drivers.size(); i++) {//循环扫描所有的Drivers DriverInfo di = (DriverInfo)drivers.elementAt(i); if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {//如果类和类的ClassLoader不匹配,跳过 println(" skipping: " + di); continue; } try { println(" trying " + di); Connection result = di.driver.connect(url, info);//调用对应Driver的connect方法,得到Connection对象 if (result != null) {//如果得到了,则返回connection // Success! println("getConnection returning " + di); return (result); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } // if we got here nobody could connect. if (reason != null) { println("getConnection failed: " + reason); throw reason; } println("getConnection: no suitable driver found for "+ url); throw new SQLException("No suitable driver found for "+ url, "08001");//没有获取到链接会抛出一个SQL Exception } 先不用看其他的代码,我们首先可以看出一点DriverManager中注册不止一个Driver驱动,如果一些系统中有多个驱动的时候,然后它循环扫描所有的驱动程序,然后通过connect方法获取是否来调用,这个connect方法每次去循环扫描,是不是会造成一些不必要的开销;为此,我们想看看里面到底有那些Driver,以及得到对应的Driver,输出Driver信息: 我们可以通过以下方式输出循环了那些内容: 方法1: Enumeration<Driver> enumeration = DriverManager.getDrivers(); 然后遍历这个迭代器,就可以得到这些Driver相关信息,可以自己根据实际情况作其他的操作也可以,可以看出Driver就是一个普通java类的实例,只是他有一些基于标准规范,类似于驱动链接的功能而已; 方法2: 设置将Log输出,将数据输出到控制台,我们这里简单输出到控制台: DriverManager.setLogWriter(new PrintWriter(System.out)); 运行这个getConnection方法、getDriver等方法的时候,就会打印出相应的日志信息在控制台上面; 例如运行getConnection方法: DriverManager.getConnection("jdbc:oracle:thin:@10.20.149.82:1521:fuck") trying driver[className=sun.jdbc.odbc.JdbcOdbcDriver,sun.jdbc.odbc.JdbcOdbcDriver@14b9a74] *Driver.connect (jdbc:oracle:thin:@10.20.149.82:1521:fuck) trying driver[className=com.mysql.jdbc.Driver,com.mysql.jdbc.Driver@1779e93] trying driver[className=oracle.jdbc.OracleDriver,oracle.jdbc.OracleDriver@1871e77] getConnection returning driver[className=oracle.jdbc.OracleDriver,oracle.jdbc.OracleDriver@1871e77] 其实DirverManager里面还有一个方法是: getDriver(String url)方法,其实更加确切的意思应该叫findDriverByUrl,入口参数为url,也就是jdbc url,它是负责匹配url是不是这个dirver可以接受的,每个driver都需要实现一个方法叫:acceptsURL来返回,当前这个Dirver是否可以接受这个URL;而acceptsURL方法是各个厂商提供的驱动程序自己编写的,也就是自己编写这个方法说明我是否支持这个URL,类似Oracle、Mysql、sql server等等数据库都会有不同的jdbc驱动包,所以他们自然就区分开了;而对应到类里面,就是前面在Class.forName("xxx")所对应的类。 在getConection方法中,会不断尝试的connect方法中,传入的URL也将会被先判定,然后再执行,jdbc通常认为connect方法本身需要判定一次,就不需要再调用acceptsURL判定一次再调用connection方法了,判定成功就直接返回connection,否则就返回null;只是有个问题是,如果有很多Driver,这里需要逐个遍历,所以文章后面我们建议是将自己的Driver保存起来; 接下来看看每个Driver的connect方法的细节,因为他是负责返回connection的,不过我们可以先看看其中调用解析URL是在哪里调用的,如下: 对于oracle的connect方法相关部分的源码为(OracleDirver类中): mysq相关的源码为(为com.mysql.jdbc.Dirver(你注册的MySQL驱动)类的父类的:NonRegisteringDriver(同一包)中): 注意parseURL方法就是acceptURL方法调用的下一个目标,内部被隐藏了,例如Mysql jdbc的源码中: public boolean acceptsURL(String url) throws SQLException { return (parseURL(url, null) != null);} 我们这里分别来说下parseURL的一些细节: 在OracleDriver中: private Hashtable parseUrl(String s) throws SQLException { Hashtable hashtable = new Hashtable(5); int i = s.indexOf(':', s.indexOf(':') + 1) + 1;//第二个冒号(注意里面还有一个indexOf,所以是第二个) int j = s.length(); if(i == j) return hashtable; int k = s.indexOf(':', i);//第三个冒号 if(k == -1) return hashtable; hashtable.put("protocol", s.substring(i, k));//第二个冒号和第三个冒号之间的认为是协议,比如thin或oci等等 int l = k + 1; int i1 = s.indexOf('/', l);//解析反斜杠 int j1 = s.indexOf('@', l);//解析@符号的位置 if(j1 > l && l > i && i1 == -1)//如果既没有反斜杠也没有@则返回null,解析失败 return null; if(j1 == -1) j1 = j; if(i1 == -1) i1 = j1; if(i1 < j1) {//如果有反斜杠(在@前面),则认为在jdbc URL上传递了用户名和密码,当然可以不传递 hashtable.put("user", s.substring(l, i1)); hashtable.put("password", s.substring(i1 + 1, j1)); } if(j1 < j)//如果传递了数据库信息,则将数据库连接串放进去,可以是IP:PORT:SID、describe、TNS等多种格式 hashtable.put("database", s.substring(j1 + 1)); return hashtable; } 它将jdbc url解析为一个Hash table,并且将协议、db信息解析出来,并且可以放用户名和密码,这样估计很少有人在oracle jdbc url上方用户名和密码,但是的确是可行的,例如你可以这样写你的oracle jdbc: jdbc:oracle:thin:<user>/<passowrd>@ip:port:sid 然后在DriverManager.getConnection的参数时候就【无需传入用户名和密码】了; 通常的写法更多是: jdbc:oracle:thin:@ip:port:sid 然后在getConnection的时候,带上用户名和密码 其实MYSQL也是这样,URL上可以传递,只是Oracle在URL上最多就加这些了,不能再加其他的了,而MySQL很麻烦的就在解析URL上面; MySQL的解析部分: mysql的解析部分比较复杂,以为内mysql大部分参数都可以通过URL来设置,所以解析很复杂,这里就简单列举部分代码: public Properties parseURL(String url, Properties defaults) throws java.sql.SQLException { Properties urlProps = (defaults != null) ? new Properties(defaults) : new Properties(); if (url == null) {//主要看看参数是不是空的 return null; } //这几个常量就是mysql jdbc 的前缀特征,如果一个都不符合,则返回null,标示解析失败 //REPLICATION_URL_PREFIX "jdbc:mysql:replication://" //URL_PREFIX "jdbc:mysql://" //MXJ_URL_PREFIX "jdbc:mysql:mxj://"; //LOADBALANCE_URL_PREFIX "jdbc:mysql:loadbalance://"; 集群下使用 if (!StringUtils.startsWithIgnoreCase(url ,URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url ,MXJ_URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url , LOADBALANCE_URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url , REPLICATION_URL_PREFIX)) { //$NON-NLS-1$ return null; } int beginningOfSlashes = url.indexOf("//");//分析content部分,后面将会是IP、PORT、库名以及扩展串 if (StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX)) { urlProps.setProperty("socketFactory" , "com.mysql.management.driverlaunched.ServerLauncherSocketFactory"); } int index = url.indexOf("?"); //参数分隔符号 if (index != -1) {//如果带有非默认参数,则循环 String paramString = url.substring(index + 1, url.length()); url = url.substring(0, index); StringTokenizer queryParams = new StringTokenizer(paramString, "&");//拆分每个参数,放在迭代器上 while (queryParams.hasMoreTokens()) {//迭代每个参数 String parameterValuePair = queryParams.nextToken(); int indexOfEquals = StringUtils.indexOfIgnoreCase(0 , parameterValuePair, "=");//拆分K-V,即参数和值 String parameter = null; String value = null; if (indexOfEquals != -1) { parameter = parameterValuePair.substring(0, indexOfEquals); if (indexOfEquals + 1 < parameterValuePair.length()) { value = parameterValuePair.substring(indexOfEquals + 1); } } //将数据放在urlProps中,为一个当前Driver全局参数配置Properties类型 if ((value != null && value.length() > 0) && (parameter != null && parameter.length() > 0)) { try { urlProps.put(parameter, URLDecoder.decode(value , "UTF-8")); } catch (UnsupportedEncodingException badEncoding) { // punt urlProps.put(parameter, URLDecoder.decode(value)); } catch (NoSuchMethodError nsme) { // punt again urlProps.put(parameter, URLDecoder.decode(value)); } } } } ...... 好了,还有些代码没贴,太多了,上面的URL解析知道是那个Driver去解析后,然后getConection就是newXXXConnection的问题,当然如果自己设置Properties这些参数会做相关的设置,例如Oracle的Properties参数中除了正常的参数外,还可以设置(一般情况下保持默认就可以了,后续的文章中我会提到一些参数,并根据源码说明它的默认值): database、server、protocol、dll、prefetch、rowPrefetch、defaultRowPrefetch、batch、executeBatch、defaultExecuteBatch、remarks、remarksReporting、synonyms、includeSynonyms、restrictGetTables、fixedString、dataSizeUnits、AccumulateBatchResult 等等参数; 如源码所示(还是在connect方法中): 同时oracle还可能是:oracle.jdbc.ultra.client.Driver(一般用不到,知道有就可以,在源码中有体现)。也有可能是OCI接口(OracleOCIConnection);如果是在我们大多数用thin的模式下,最终会调用: neworacle.jdbc.driver.OracleConnection(dbaccess, s1, s2, s3, s4, properties); 这些参数细节,下一篇文章说,这里要说进去很多。 而mysql的参数都在parserURL里面被设置到Driver的全局变量中(Properties urlProps),在解析后,只需要解析后直接new一个com.mysql.jdbc.Connection就可以了; 我们现在知道connection通过【对应driver】的【connect】方法如何new出来了,也可以看出每次请求DirverManger.getConnection都会new 一个Connection出来(所以要注意连接池的用途,需要对connection提供调度等策略); 调用getConection的时候,还会遍历所有【被注册的Dirver】,所以我们也想办法让它尽量【不要被反复遍历】,顺便提及下,大家别被Dirver吓怕了,其实我们看看源码,其实就是一个对象嘛,只是它的功能有点像驱动而已,他提供一些注册和链接的方法,被我们成为Driver; 回过头来,Driver是在什么时候被注册到DriverManager里面的呢?奇怪,如果未被注册,肯定是获取不到的,看看自己的代码: Class.forName("dddd"); 为什么要写这一句,这一句话简单来讲仅仅是用当前的CLassLoader将一个类进行装载的代码,若已经装载,什么也不会做; 但是DriverManager里面怎么知道的呢?难道所有的类都在里面,那要遍历起来太吓人了,而且我们开始输出过所有的Driver这是错误的,而且Dirver都要求有相应的方法,难道是都implements Driver?有这种可能性,在方法实现上可以做到; 到底是怎么样的呢? 其实在Oracle JDBC或MySQL JDBC里面都一个【static匿名块】,static 匿名块是当你【第一次访问这个Class(类)的时候】,会被调用,以后不会被重新初始化,分别如下: Orale的oracle.jdbc.driver.OracleDriver类里面: 可见,他自己调用了DriverManager.registerDirever方法,注册进去,然后new了一个自己;使得自己是全局单例的; 再看看MySQL的: 在类:com.mysql.jdbc.Driver类中: 同样的方式,进行注册了,原来是这样,忽悠了很久,不是什么人名字,而是提供了registerDriver(Dirver driver)方法来注册; 我们再来看看注册到底做了什么: DriverManager.registerDriver()我们看看: 说明什么,就是做了一个类似于List的东西(内部可以看出它是用Vector实现的,java用vector其实是很早的写法,不过由于这部分代码实际应用中一般不会被经常调用,所以先关的代码也没有做多少修改),然后将数据写进去,原来注册这么简单,当你有一天要来注册某种链接的时候,你要基于一个统一标准来写的时候,就可以用这种方式注册了,例如,你自己写一个某种数据库库的链接对象,或对某些NoSQL做二次包装,这样上面的程序就可以变得比较通用了; 通过上面的可以看出,放在static匿名块中的代码是在第一次访问这个类的时候被调用,所以不一定非要用Class.forName()因为这样就是访问了这个类;你可以调用这个类里面的一个静态方法,或new一个出来都会访问到这个类,当然driver为了避免重复创建,所以我们一般不用new而已,因为他自己会内部创建一个注册到Driver中,除非调用DriverManager.deregisterDriver(Driver driver)来取消注册,将Driver中被注册的那个对象去掉; 原理上我们说得差不多了,有些地方其实我们可以节约一些开销,当然不是特别关键的; 其一: 我们发现每次通过DriverManager.getConnection都需要去遍历获取Driver然后去调用connect方法,其实我们认为Driver获取一次就可以了,对于相同类型的driver,一般我们的程序就可以认出来是mysql还是oracle; 所以我们可以通过DriverManager.getDriver(url)只获取一次,你可以放入一个模型就可以,分别表示: oracleDriver、mySqlDriver,这样就直接调用对应的driver了; 例如: private static OracleDriver oracleDriver = null; static { try { //Class.forName("oracle.jdbc.driver.OracleDriver"); OracleDriver.getCompileTime();//随便访问一个静态方法即可,其实只是想访问下这个类,Class.forName是最简单的访问。 oracleDriver = (OracleDriver) DriverManager.getDriver("jdbc:oracle:thin:@ip:port:sid"); } catch (Exception e) { e.printStackTrace(); } } 这样这个OracleDriver就被保存起来,以后就可以直接使用oracleDriver.connect来获取链接了。 其二: 你也可以new一个driver出来,但是注意这里请保持单利,这样会比较好,一些全局设置可以再全局生效,上面也说了,不推荐,只是说他是一种方法而已; 例如: private final static Driver oracleDriver = new OracleDriver(); 最后DriverManager里面还有些其他的方法,比如一些去掉注册的方法之类的,如果对于注册较多的情况下,需要将一些不需要的去掉,可以再这进行参考; 下一篇文章,会提及到Connection获取的一些细节,就是具体某个Driver中Connection类的实现体,以及PrepareStatement实现,setTimeout实现机制、fetchSize内部运行 后面还会继续介绍,和数据库交互过程中,反解析数据的过程,中断链接的原理等具体的实现。 不过肯定不会将所有的源码全部说到,因为有很多不常用,例如存储过程的调用之类的,暂时不是提到。
文章简单,相信在很多网站都能搜索到java enum枚举的使用方式;可能有些东西我当时在刚开始用的时候没找到,所以我写了这篇文章,例如: 大多数地方写的枚举都是给一个枚举然后例子就开始switch,可是我想说,我代码里头来源的数据不太可能就是枚举,通常是字符串或数字,比如一个SQL我解析后首先判定SQL类型,通过截取SQL的token,截取出来可能是SELECT、DELETE、UPDATE、INSERT、ALTER等等,但是都是字符串,此时我想用枚举就不行了,我要将字符串转换成枚举怎么转呢,类似的情况还有从数据库取出数据根据一些类型做判定,从页面传入数据,根据不同的类型做不同的操作,但是都是字符串,不是枚举,悲剧的是我很少看到有人写到这个东西;所以我把它写下来,希望有人能用到。 首先为什么要用枚举?我们在什么时候用枚举比较好,用枚举有啥优势? 我认为哈,当你在一些一个范畴类,并可列举,不变化的类型,用以指导程序向不同的地方路由,用枚举是较好的选择; 听起来有点绕,不过有个例子也许可以明白,例如: 我们可以列举下日常工作日所做的事情: 上班、开会、吃饭、睡觉等 我们可以列举医院五官科需要检查人的部位: 眼睛、鼻子、耳朵、嘴巴等 这些都是可以被列举的,且每种事情我们要用不同的方式去做; 当然你可以说: 1、可以用动态方法分派,通过配置文件或annotation; 2、可以使用常量来达到类似的效果; 3、直接通过字符串的equals来表达,用if else来表达 如果用配置加方法分派来做,是灵活,便于修改;但是如果在很多不经常修改的参数上,我们用这中方式往往增加配置的负担,并且当你需要看系统逻辑的时候,需要需要一遍看配置一遍看代码;不过,如果参数是可动态变换的信息,用配置是正确的选择; 而常量的使用,通常在switch case的时候都是数字,字符串在java中是不能做switch case的,使用常量的目的比case 1、case 2 ...这种增加了可读性;但是字符串数据也麻烦,除非再映射一次,那没那个必要,其实枚举也差不多是帮你映射了一次,只是它将代码封装了而已吧了,既然他弄好了,而且语法上支持,干嘛不用呢!其次,常量虽然增加了可读性,不过他没有范畴和管理类型的概念,即一个枚举的定义会定义个范畴,可以很好的将这个范围所需要的东西列举出来,而常量通常是些自己定义的一些池,放在一些公共类中或随机定义,都是比较零散的,并且枚举在switch的时候就明确定义好了就在锁列举的范围内case,既可以控制好系统,增加可读性,并且可以随时查看这个范畴的枚举信息到底有那些,达到类似看配置文件的作用;不过还是回到那句话,如果参数是可变的,那么就不适合做枚举,枚举是一定是可列举的,或者说当前系统考虑范围是可以被枚举的,例如上面的医院五官科,可能还有很多没有列举到,但是当前医院只处理几个部位,不处理其他的,就是这个道理;什么是可变的呢,例如URL参数来分派到对应方法,不可能大家加一段逻辑就去加一个枚举,加一个case,此时用【配置+动态方法分派】更好,当然配置可以用文件或annotation而已。 还有最土的就是,通过字符串equals,用if else来实现,呵呵,这个并没有什么不好,只是这个写比较零散,其次,字符串匹配的equals每次匹配都需要对比每个字符,如果你的代码中大量循环,性能并不是很好,其余的看看上面的描述就更加清楚了; 其次,枚举提供一种类型管理的组件,让面向对象的体系更加完善,使得一些类型的管理既可配置化,并可以管理,在使用枚举的地方都可以沿着枚举的定义找到那些有处理过,那些没处理过,而上述几种很难做到;例如,数据库的操作类型定义了10种,那么再判定的过程中就可以讲枚举像配置文件一样看待,而又非常简单的来管理。 最后,枚举绝对是单例的,对比的性能和数字性能相当,既可以得到可读性,也可以得到性能。 我们先定义个简单枚举(这里只是个例子,就简单定义3个变量了): public enum SqlTypeEnum { INSERT , UPDATE , DELETE , SELECT } 此时解析SQL后,获取出来一个token,我们要获取这个token的枚举怎么获取呢? 这样获取: String token = "select"; SqlTypeEnum sqlTypeEnum = SqlTypeEnum.valueOf(token.toUpperCase()); 如果没获取到,java会抛出一个异常哦:IllegalArgumentException No enum const class SqlTypeEnum.XXX 我做大写处理的原因是因为枚举也是大写的(当然如果你的枚举是小写的,那你就小写,不过混写比较麻烦哈),其实valueOf就是调用了枚举的底层映射: 调用的时候会调用这个方法: 所以内部也是一个HashMap,呵呵! 拿到这个信息后,就可以做想要的操作了: switch(sqlTypeEnum) { case INSERT:处理insert逻辑;break; case DELETE:处理delete逻辑;break; .... } OK,有些时候可能我们不想直接用INSERT、UPDATE这样的字符串在交互中使用,因为很多时候命名规范的要求; 例如定义一些用户操作类型: 1、保存用户信息 2、通过ID获取用户基本信息 3、获取用户列表 4、通过ID删除用户信息 等等 我们可能定义枚举会定义为: public enum UserOptionEnum { SAVE_USER, GET_USER_BY_ID, GET_USER_LIST, DELETE_USER_BY_ID } 但是系统的方法和一些关键字的配置,通常会写成: saveUser、getUserById、getUserById、deleteUserById 当然各自有各自的规则,不过中间这层映射,你不想做,就一方面妥协,要么枚举名称全部换掉,貌似挺奇怪的,要么方法名称全部换掉,更加奇怪,要么自己做映射,可以,稍微麻烦点,其实也不麻烦? 我们首先写个将枚举下划线风格的数据转换为驼峰的方法,放在一个StringUtils里面: public static String convertDbStyleToJavaStyle(String dbStyleString , boolean firstUpper) { dbStyleString = dbStyleString.toLowerCase(); String []tokens = dbStyleString.split("_"); StringBuilder stringBuilder = new StringBuilder(128); int length = 0; for(String token : tokens) { if(StringUtils.isNotBlank(token)) { if(length == 0 && !firstUpper) { stringBuilder.append(token); }else { char c = token.charAt(0); if(c >= 'a' || c <= 'z') c = (char)(c - 32); stringBuilder.append(c); stringBuilder.append(token.substring(1)); } } ++length; } return stringBuilder.toString(); } 重载一个方法: public static String convertDbStyleToJavaLocalStyle(String dbStyleString) { return convertDbStyleToJavaStyle(dbStyleString , false); } 然后定义枚举: public enum UserOptionEnum { SAVE_USER, GET_USER_BY_ID, GET_USER_LIST, DELETE_USER_BY_ID; private final static Map<String , UserOptionEnum> ENUM_MAP = new HashMap<String, UserOptionEnum>(64); static { for(UserOptionEnum v : values()) { ENUM_MAP.put(v.toString() , v); } } public staticUserOptionEnum fromString(String v) { UserOptionEnum userOptionEnum = ENUM_MAP.get(v); return userOptionEnum == null ? DEFAULT :userOptionEnum; } public String toString() { String stringValue = super.toString(); return StringUtil.convertDbStyleToJavaLocalStyle(stringValue); } } OK,这样传递一个event参数让如果是:saveUser,此时就用: String event = "saveUser";//假如这里得到参数 UserOptionEnum enum = UserOptionEnum.fromString(event); 其实就是自己做了一个hashMap,我这加了一个fromString,因为枚举有一些限制,有些方法不让你覆盖,比如valueOf方法就是这样。 其实没啥好讲的了,非要说,再说说枚举加一些自定义变量吧,其实枚举除了是单例的外,其余的和普通类也相似,它也可以有构造方法,只是默认情况下不是而已,也可以提供自定义的变量,然后获取set、get方法,但是如果有set的话,线程不是安全的哦,要注意这点;所以一般是构造方法就写好了: public enum SqlTypeEnum { INSERT("insert into"), DELETE("delete from") ......省略; private String name;//定义自定义的变量 private SqlTypeEnum(String name) { this.name = name; } public String getName() { return name; } public String toString() { return name + " 我靠";//重写toString方法 } //一般不推荐 public void setName(String name) { this.name = name; } } 调用下: SqlTypeEnum sqlTypeEnum = SqlTypeEnum.valueOf("INSERT"); System.out.println(sqlTypeEnum); System.out.println(sqlTypeEnum.getName()); 不推荐也调用下: sqlTypeEnum.setName("我靠"); 在另一个线程: SqlTypeEnum sqlTypeEnum = SqlTypeEnum.valueOf("INSERT"); System.out.println(sqlTypeEnum); System.out.println(sqlTypeEnum.getName()); 发现结果被改了,呵呵!
为啥写这个文章呢?spring各个版本不同,以及和系统框架套在一起不同,导致获取的方式不同,网络上各种版本,太乱了,写获取方式的人都不写这个获取方式是在本地还是在WEB,在那种应用服务器下,在spring那个版本下,太过分了! 我这写一些,常见的,可能经常要用的版本; 首先了解,为什么要获取这个东西:当你想通过spring获取一个你指定的类的实例的时候,而又没有通过spring加载到当前调用的类里面,例如你在filter里面,可能要对人员角色做判定,此时还没到业务层代码,但是又要访问数据库或其他的服务类。 然后再确保一点:这个context是一个全局变量,spring加载的时候,根handle信息就被装载,无论是本地应用程序还是web应用都是这样,下面分别说下如果是本地程序和其他情况的获取方式。 如果是main方法,你要启动spring,有很多方法,有基于annotation的注解来讲配置文件装载起来,当然,你想获取applicationCntext可在main方法中这样获取: XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));//这样来加载配置文件 还有没有其他的方式呢?有的 ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"a.xml", "b.xml"}); 还有没有其他的?有 XmlWebApplicationContext context = new XmlWebApplicationContext(); context.setConfigLocations(new String[] {"aaa.xml" , "bb.xml"}); MockServletContext msc = new MockServletContext(); context.setServletContext(msc); context.refresh(); msc.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, context); 其实方法差不多,他们有着继承关系,所以方法很多,你每次new的时候,相当于重新创建一个applicationContext,他会重新装载,所以不适合反复调用,如果自己new,你就应当把它放到一个全局变量中,用main启动的,当然你通过直接或间接的static应用到这个application即可。 而在WEB上呢,有一种是通过spring来加载spring本身的方式是: 通过实现接口: org.springframework.context.ApplicationContextAware 然后spring反射,来源文章:http://blog.163.com/xuyang1974@126/blog/static/2684016320101028101923914/ 这种方式适在spring 2、3当中均有效: 编写类: import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Service; @Service public class SpringContextHolder implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextHolder.applicationContext = applicationContext; } public static ApplicationContext getApplicationContext() { return applicationContext; } public static Object getBean(String beanName) { return applicationContext.getBean(beanName); } public static <T>T getBean(String beanName , Class<T>clazz) { return applicationContext.getBean(beanName , clazz); } } 我这里是通过annotation注解的,如果不是annotation,那么可以通过配置文件: <bean class="xxx.xxx.xxx.SpringContextHolder"></bean> 来进行注入操作,结果一样,如果的spring配置中,没有设置byName的话,bean的配置里面记得要加参数来设置applicationContext来反射进去。 而你要加载spring,很多时候,并不是进入业务层的,因为反射是反射到业务层的,你还没有进入业务层,怎么来获取这个反射的东西呢?除非你反射的时候,用static变量来获取,那么就没有问题了;所以上面的例子中他也用的是static; 当你不想用static来反射,而经常想要用到它的时候,就有很多种获取方式了。 在spring 3以前的版本,我们在WEB应用中通常是这样获取的: WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(context); 而contexnt是什么呢?如果是servlet中,是可以直接通过getServletContext()获取, 而通过request要这样获取: 对于所有的tomcat通用的写法是: ServletContext context = req.getSession().getServletContext(); 对于tomcat 7以上的写法是(也就是tomcat 7可以直接从request中获取servletContext,tomcat6不行,必须通过session才可以): ServletContext context = req.getServletContext(); 其实从spring 3过后,获取的方法就有所改变,变得很诡异,因为竟然不兼容以前的获取方法,spring 3当中将其进行了进一步的包装,你在其他地方可能看到各种各样的版本。 spring 2中之所以可以那样获取,是因为spring 2当中通常会配置一个listener,由他来加载spring,他在filter之前;spring 3当中,通过org.springframework.web.servlet.DispatcherServlet来装载spring的信息,初始化在其父亲类:org.springframework.web.servlet.FrameworkServlet中方法:initWebApplicationContext(); 跟踪方法明显看到内部获取增加了一个参数: WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(getServletContext(),attrName); 这个参数是什么呢? 经过跟踪可以发现是: FrameworkServlet.SERVLET_CONTEXT_PREFIX + getServletName() 而SERVLET_CONTEXT_PREFIX的定义是: public static final String SERVLET_CONTEXT_PREFIX = FrameworkServlet.class.getName() + ".CONTEXT."; 也就是: “org.springframework.web.servlet.FrameworkServlet.CONTEXT.” 而getServletName()呢?他是当前请求的servlet,可以获取到的一个web.xml里面配置的名称,例如, 如果你的web.xml中配置的是: <servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> 说明getServletName()的结果就是spring,否则就是其他,那么如果是spring,就是: org.springframework.web.servlet.FrameworkServlet.CONTEXT.spring ok,如果按照上面的配置,获取方式就是: request.getSession().getServletContext().getAttribute("org.springframework.web.servlet.FrameworkServlet.CONTEXT.spring"); tomcat 7以上可以写成: request.getServletContext().getAttribute("org.springframework.web.servlet.FrameworkServlet.CONTEXT.spring"); 更为好的写法是: request.getSession().getServletContext().getAttribute(FrameworkServlet.SERVLET_CONTEXT_PREFIX +"spring"); 以下为spring为了方便,做的一些扩展: spring为了业务代码中获取这个参数方便,在进入业务代码前做了一个操作,在DispatcherServlet的方法:doService中doDispatch调用之前: request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext()); 也就是,当你进入Controller以后,获取就不用那么麻烦了,你只需要这样就能获取到: request.getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE); 当然,你可以将值写进去,看定义是: public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = DispatcherServlet.class.getName() + ".CONTEXT"; 那么值就应该是: org.springframework.web.servlet.DispatcherServlet.CONTEXT 所以在Controller中你还可以这样来获取: request.getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT") 经过spring包装后,你也可以通过: RequestContextUtils.getWebApplicationContext(request , context) 来获取,源码如下: 其实它获取的方式和上面给的方法是一样的,RequestContextUtils.getWebApplicationContext在spring 3当中,如果没有启动ContextLoaderListener(当然你可以配置监听),是不会成功的。 ContextLoaderListener的简单配置为(web.xml中): <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> spring 3以后基本不这样配置了。
前面有一篇文章提及到乱码的产生:http://blog.csdn.net/xieyuooo/article/details/6919007 那么知道主要原因是编码和解码方式不一样,那么有些时候如果我们知道编码方式,那么解码自然很好搞,例如输出的contentType会告诉浏览器我输出的内容是什么编码格式的,否则浏览器会才用一个当前默认的字符集编码来处理;本文要将一些java如何处理没有带正常协议头部的字符集应当如何来处理。 这里就说的是文件字符集,在了解字符集之前,回到上一篇文章说到默认字符集,自定义字符集,系统字符集,那么当前环境到底用的什么字符集呢? System.out.println(Charset.defaultCharset()); 当前java应用可以支持的所有字符集编码列表: Set<String> charsetNames = Charset.availableCharsets().keySet(); for(String charsetName : charsetNames) { System.out.println(charsetName); } 因为java的流当中并没有默认说明如何得知文件的字符集,很神奇的是,一些编辑器,类似window的记事本、editplus、UltraEdit他们可以识别各种各样的字符集的字符串,是如何做到的呢,如果面对上传的文件,需要对文件内容进行解析,此时需要如何来处理呢? 首先,文本文件也有两种,一种是带BOM的,一种是不带BOM的,GBK这系列的字符集是不带BOM的,UTF-8、UTF-16LE、16UTF-16BE、UTF-32等等不一定;所谓带BOM就是指文件【头部有几个字节】,是用来标示这个文件的字符集是什么的,例如: UTF-8 头部有三个字节,分别是:0xEF、0xBB、0xBF UTF-16BE 头部有两个字节,分别是:0xFE、0xFF UTF-16LE 头部有两个字节,分别是:0xFF、0xFE UTF-32BE 头部有4个字节,分别是:0x00、0x00、0xFE、0xFF 貌似常用的字符集我们都可以再这得到解答,因为常用的对我们的程序来讲大多是UTF-8或GBK,其余的字符集相对比较兼容(例如GB2312,而GB18030是特别特殊的字符才会用到)。 我们先来考虑文件有头部的情况,因为这样子,我们不用将整个文件读取出来,就可以得到文件的字符集方便,我们继续写代码: 通过上面的描述,我们不难写出一个类来处理,通过inputStream来处理,自己写一个类: import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; public class UnicodeInputStream extends InputStream { PushbackInputStream internalIn; boolean isInited = false; String defaultEnc; String encoding; private byte[]inputStreamBomBytes; private static final int BOM_SIZE = 4; public UnicodeInputStream(InputStream in) { internalIn = new PushbackInputStream(in, BOM_SIZE); this.defaultEnc = "GBK";//这里假如默认字符集是GBK try { init(); } catch (IOException ex) { IllegalStateException ise = new IllegalStateException( "Init method failed."); ise.initCause(ise); throw ise; } } public UnicodeInputStream(InputStream in, String defaultEnc) { internalIn = new PushbackInputStream(in, BOM_SIZE); this.defaultEnc = defaultEnc; } public String getDefaultEncoding() { return defaultEnc; } public String getEncoding() { return encoding; } /** * Read-ahead four bytes and check for BOM marks. Extra bytes are unread * back to the stream, only BOM bytes are skipped. */ protected void init() throws IOException { if (isInited) return; byte bom[] = new byte[BOM_SIZE]; int n, unread; n = internalIn.read(bom, 0, bom.length); inputStreamBomBytes = bom; if ((bom[0] == (byte) 0x00) && (bom[1] == (byte) 0x00) && (bom[2] == (byte) 0xFE) && (bom[3] == (byte) 0xFF)) { encoding = "UTF-32BE"; unread = n - 4; } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE) && (bom[2] == (byte) 0x00) && (bom[3] == (byte) 0x00)) { encoding = "UTF-32LE"; unread = n - 4; } else if ((bom[0] == (byte) 0xEF) && (bom[1] == (byte) 0xBB) && (bom[2] == (byte) 0xBF)) { encoding = "UTF-8"; unread = n - 3; } else if ((bom[0] == (byte) 0xFE) && (bom[1] == (byte) 0xFF)) { encoding = "UTF-16BE"; unread = n - 2; } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) { encoding = "UTF-16LE"; unread = n - 2; } else {//没有捕获到的字符集 //encoding = defaultEnc; //这里暂时不用默认字符集 unread = n; //inputStreamBomBytes = new byte[0]; } // System.out.println("read=" + n + ", unread=" + unread); if (unread > 0) internalIn.unread(bom, (n - unread), unread); isInited = true; } public byte[] getInputStreamBomBytes() { return inputStreamBomBytes; } public void close() throws IOException { isInited = true; internalIn.close(); } public int read() throws IOException { isInited = true; return internalIn.read(); } } 好了,下面来看看是否OK,我们测试一个文件,用【记事本】打开一个文件,编写一些中文,将文件分别另存为几种字符集,如下图所示: 通过这种方式保存的文件是有头部的,windows里面也保存了这个标准,但是并不代表,所有的编辑器都必须要写这个头部,因为文件上并没有定义如果不写头部,就不能保存文件,其实所谓的字符集,是我们逻辑上抽象出来的,和文件本身无关,包括这些后缀的.txt|.sql等等,都是人为定义的; 好,不带头部的,我们后面来讲,若带有头部,我们用下面的代码来看看是否正确(用windows自带的记事本、UE工具另存为是OK的,用EditPlus是不带头部的,这里为了测试,可以用前两种工具来保存): 我们这里写个组件类,方便其他地方都来调用,假如我们自己定义个叫FileUtils的组件类,里面定义一个方法:getFileStringByInputStream,传入输入流,和是否关闭输入流两个参数(因为有些时候就是希望暂时不关闭,由外部的框架来关闭),再定义一个重载方法,第二个参数不传递,调用第一个方法是,传入的是true(也就是默认情况下我们认为是需要关闭的)。 代码如下(其中closeStream是一个自己编写的关闭Closeable实现类方法,这里就不多说了): public static String getFileStringByInputStream2(InputStream inputStream , boolean isCloseInputStream) throws IOException { if(inputStream.available() < 2) return ""; try { UnicodeInputStream in = newUnicodeInputStream(inputStream); String encoding = in.getEncoding(); int available = inputStream.available(); byte []bomBytes = in.getInputStreamBomBytes(); int bomLength = bomBytes.length; byte []last = new byte[available + bomLength]; System.arraycopy(bomBytes , 0 , last , 0 , bomLength);//将头部拷贝进去 inputStream.read(last , bomBytes.length , available);//抛开头部位置开始读取 String result = new String(last , encoding); if(encoding != null && encoding.startsWith("GB")) { return result; }else { return result.substring(1); } }finally { if(isCloseInputStream) closeStream(inputStream); } } 此时找了几个文件果然OK,不论改成什么字符集都是OK的,此时欣喜了一把,另一个人给了我一个Editplus的文件悲剧了,然后发现没有头部,用java默认的OuputStream输出文件也不会有头部,除非自己写进去才会有,或者说,如果你将头部乱写成另一种字符集的头部,通过上述方面就直接悲剧了; 但是如果是不带BOM的,这个方法是不行的,因为没有头部,就没法判定,可以这样说,目前没有任何一种编辑器可以再任何情况下保证没有乱码(一会我们来证明下),类似Editplus保存没有头部的文件,为什么记事本、UE、Editplus都可以认识出来呢(注意,这里指绝大部分情况,并非所有情况); 首先来说下,如果没有头部,只有咋判定字符集,没办法哈,只有一个办法,那就是读取文件字符流,根据字符流和各类字符集的编码进行匹配,来完成字符集的匹配,貌似是OK的,不过字符集之间是存在一个冲突的,若出现冲突,那么这就完蛋了。 做个实验: 写一个记事本或EditPlus,打开文件,在文件开始部分,输入两个字“联通”,然后另存为GBK格式,注意,windows下ASNI就是GBK格式的,或者一些默认,就是,此时,你用任何一种编辑器打开都是乱码,如下所示: 重新打开这个文件,用记事本: 用Editplus打开: 用UE打开: 很悲剧吧,这里仅仅是个例子,不仅仅这个字符,有些其他的字符也有可能,只是正好导致了,如果多写一些汉字(不是从新打开后写),此时会被认出来,因为多一些汉字绝大部分汉字还是没有多少冲突的,例如:联通公司现在表示OK,这是没问题的。 回到我们的问题,java如何处理,既然没有任何一种东西可以完全将字符集解析清楚,那么,java能处理多少,我们能否像记事本一样,可以解析编码,可以的,有一个框架是基于:mozilla的一个叫:chardet的东西,下载这个包可以到http://sourceforge.net/projects/jchardet/files/ 里面去下载,下载后面有相应的jar包和源码,内部有大量的字符集的处理。 那么如何使用呢,他需要扫描整个文件(注意,我们这里没考虑超过2G以上的文件)。 简单例子,在他的包中有个文件叫:HtmlCharsetDetector.java的测试类,有main方法可以运行,这个我大概测试过,大部分文本文件的字符集解析都是OK的,在使用上稍微做了调整而已;它的代码我这就不贴了,这里说下基于这个类和原先基于头部判定的两种方法结合起来的样子; 首先再写一个基于第三包的处理方法: /** * 通过CharDet来解析文本内容 * @param inputStream 输入流 * @param bomBytes 头部字节,因为取出来后,需要将数据补充回去 因为先判定了头部,所以头部4个字节是传递进来,也需要判定,而inputStream的指针已经指在第四个位置了 * @param bomLength 头部长度,即使定义为4位,可能由于程序运行,不一定是4位长度 这里没有使用bomBytes.length直接获取,而是直接从外部传入,主要为了外部通用 * @param last 后面补充的数据 * @return 返回解析后的字符串 * @throws IOException 当输入输出发生异常时,抛出,例如文件未找到等 */ private static String processEncodingByCharDet(InputStream inputStream, byte[] bomBytes, int bomLength, byte[] last) throws IOException { byte []buf = new byte[1024]; nsDetector det = new nsDetector(nsPSMDetector.ALL); final String []findCharset = new String[1];//这里耍了点小聪明,让找到字符集的时候,写到外部变量里面来下,继承下也可以 det.Init(new nsICharsetDetectionObserver() { public void Notify(String charset) { if(CHARSET_CONVERT.containsKey(charset)) { findCharset[0] = CHARSET_CONVERT.get(charset); } } }); int len , allLength = bomLength; System.arraycopy(bomBytes, 0, last, 0, bomLength); boolean isAscii = det.isAscii(bomBytes , bomLength); boolean done = det.DoIt(bomBytes , bomLength , false); BufferedInputStream buff = new BufferedInputStream(inputStream); while((len = buff.read(buf , 0 , buf.length)) > 0) { System.arraycopy(buf , 0 , last , allLength , len); allLength += len; if (isAscii) { isAscii = det.isAscii(buf , len); } if (!isAscii && !done) { done = det.DoIt(buf , len , false); } } det.Done(); if (isAscii) {//这里采用默认字符集 return new String(last , Charset.defaultCharset()); } if(findCharset[0] != null) { return new String(last , findCharset[0]); } String encoding = null; for(String charset : det.getProbableCharsets()) {//遍历下可能的字符集列表,取到可用的,跳出 encoding = CHARSET_CONVERT.get(charset); if(encoding != null) { break; } } if(encoding == null) encoding = Charset.defaultCharset();//设置为默认值 return new String(last , encoding); } CHARSET_CONVERT的定义如下,也就是返回的字符集仅仅是可以被解析的字符集,其余的字符集不考虑,因为有些时候,chardet也不好用: private final static Map<String , String> CHARSET_CONVERT = new HashMap<String , String>() { { put("GB2312" , "GBK"); put("GBK" , "GBK"); put("GB18030" , "GB18030"); put("UTF-16LE" , "UTF-16LE"); put("UTF-16BE" , "UTF-16BE"); put("UTF-8" , "UTF-8"); put("UTF-32BE" , "UTF-32BE"); put("UTF-32LE" , "UTF-32LE"); } }; 这个方法写好了,我们将原来的那个方法和这个方法进行合并: /** * 获取文件的内容,包括字符集的过滤 * @param inputStream 输入流 * @param isCloseInputStream 是否关闭输入流 * @throws IOException IO异常 * @return String 文件中的字符串,获取完的结果 */ public static String getFileStringByInputStream(InputStream inputStream , boolean isCloseInputStream) throws IOException { if(inputStream.available() < 2) return ""; UnicodeInputStream in = new UnicodeInputStream(inputStream); try { String encoding = in.getEncoding();//先获取字符集 int available = inputStream.available();//看下inputStream一次性还能读取多少(不超过2G文件,就可以认为是剩余多少) byte []bomBytes = in.getInputStreamBomBytes();//取出已经读取头部的字节码 int bomLength = bomBytes.length;//提取头部的长度 byte []last = new byte[available + bomLength];//定义下总长度 if(encoding == null) {//如果没有取到字符集,则调用chardet来处理 return processEncodingByCharDet(inputStream, bomBytes, bomLength, last); }else {//如果获取到字符集,则按照常规处理 System.arraycopy(bomBytes , 0 , last , 0 , bomLength);//将头部拷贝进去 inputStream.read(last , bomBytes.length , available);//抛开头部位置开始读取 String result = new String(last , encoding); if(encoding.startsWith("GB")) { return result; }else { return result.substring(1); } } }finally { if(isCloseInputStream) closeStream(in); } } 外部再重载下方法,可以传入是否关闭输入流; 这样,通过测试,绝大部分文件都是可以被解析的; 注意,上面有个substring(1)的操作,是因为如果带BOM头部的文件,第一个字符(可能包含2-4个字节),但是转换为字符后就1个,此时需要将他去掉,GBK没有头部。
以前说了大多的原理,今天来说下spring的事务管理器的实现过程,顺带源码干货带上。 其实这个文章唯一的就是带着看看代码,但是前提你要懂得动态代理以及字节码增强方面的知识(http://blog.csdn.net/xieyuooo/article/details/7624146),关于annotation在文章:http://blog.csdn.net/xieyuooo/article/details/8002321 也有说明,所以本文也就带着看看代码而已。 关于annotation这里就不说了,我们看看平时一般会怎么样来配置spring的配置,通过配置文件反射源码如何看看。 一般来讲首先会配置一个datasource,至于你配置什么连接池还是用JNDI这里就不提到细节,总之我们认为配置的spring的全局名称为dataSource就可以了。 接下来会将datasource交给各种连接池的操作类,如:ibatis、jdbcTemplate等等,这些不是我们关心的重点,我们需要关心的是dataSource是谁来管理了,在spring中配置了给一个DataSourceTransactionManager的对象: <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource"> <ref bean="dataSource" /> </property> </bean> ok,先记录下来,至于下面的NameMatchTransactionAttributeSource描述了那些情况要进行事务管理,我们将它理解为一种属性配置,在运行时需要解析即可,所以他也并不是我们特别需要的重点。 接下里看看:TransactionInterceptor,它看起来有点像拦截器了,他将transactionManager包装进去了; <bean id="txInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"> <property name="transactionManager"> <ref bean="transactionManager"/> </property> <property name="transactionAttributeSource"> <ref bean="txAttributes"/> </property> </bean> 这里是一个点,再继续看,BeanNameAutoProxyCreator,这个看名字就知道是自动代理的类了,并且包装了TransactionInterceptor的对象进去,也就是这个地方就是代理,然后会将事务的处理交给TransactionInterceptor拦截器来完成,可能这个不是我们的重点,不过简单看看也无妨,这个类细节代码就不贴了,进去你会看到就是讲拦截器包装后,然后通过beanName设置哪些类需要被拦截,根据你的配置来完成,spring 后来基于annotation实现的就不是这样,他会扫描类中的annotation来实现类似的功能。 一起来简单看看TransactionInterceptor吧: 细节代码太多,看关键代码,红色部分表示出来了,其实在AOP调用中,我们比较关注invoke、intercept这类代码关联字的方法,因为proxy调用的就是他们,由他们自己去调用其他的方法,这里invoke首先发现在事务下,首先调用了createTranscationIfNecessary这个方法。 跟踪进去看看: 这里看到开始获取TransactionManager了,get方法没啥好看的,配置文件注入进去了,我们看看tm.getTranscation里头做了啥。 也就是跟踪到你set的那个TranscationManager里头去了,PlatformTransactionManager有多个实现类,注意选择自己实现的那部分:本程序中叫:DataSourceTransactionManager,跟踪进去看看getTransaction方法 这个代理类,需要注意几个加红色的地方: 1、目前看来应该是获事务的方法 2、部分如果发现事务对象获取到就直接返回 3、做一个doBegin的操作,这i类关键字一般是指切入时的预先操作,那么闲看看这个doBegin干啥了 我们想要的东西来了,相信看到第二个红色区域部分,大家都会很熟悉自己做事务是怎么做的,发现spring也是这样做的。 将connection做了一个setAutoCommit(false);非自动提交模式,接下来就要看如何和框架结合起来了,如何让调用的时候使用到这个connection,调用方如何知道使用这个connection; 看另外两个红色的部分: 第一个红色部分可以看出是获取事务对象若为空(不是事务或已经是事务),则从datasource对象中获取一个connection,包装成一个handle,放入事务对象中(事务对象内部的包装请自己去看下)。 而第三个额部分是,如果是一个新的ConnectionHandler(其实判定的是一些状态,使用中,spring会修改handler的状态,这也是为什么spring要包装一个handler了,因为需要自定义的很多状态信息);他执行了一个 TransactionSynchronizationManager.bindResource(getDatasource() , txObject.getConnectionHolder()); 这样一个操作,可见:TransactionSynchronizationManager提供了一个静态方法getDatasource(),看名称是绑定的意思,那么绑定什么呢?我们跟踪进去看看: 咦,resouces貌似里面有一个map,如果为空,就put一个进去,那么resouce是个什么东西呢?他会不会有线程冲突的问题? 看看resouces是什么: ThreadLocal,对了,就是它了,有关ThreadLocal的原理和细节,我这不想多提,也不是这里的重点,这里明确的就是,虽然它是全局的一个静态属性,不过他是线程安全的,不论是get还是set还是remove。 我们知道这个connection被绑定到当前运行的线程中了,接下来只需要在使用时从这个里面获取出来即可。 我们再回到上面看到的doTransaction方法还没看细节,这里来看看。 可以看到,它果然是从这里来获取保存到当前线程的connection。 貌似看得差不多了,好像少了一半,那一半,datasouces是各个厂家的,他们的各自的datasouce方法是自己的,getConnection内部有自己的算法,如何做到他们在getConnection的时候,执行相应的,这个时候,我们来看看一个拦截connection的方式:TransactionAwareDataSourceProxy, 他内部有啥道理所在: 这里可以看到使用了动态代理,获取相应的datasouces,那么就找到对应的代理类里面去看看他的invoke方法是什么: TransactionAwareInvocationHandler里面,可以跟踪这个是一个内部类了,进去看看他的invoke方法: 可以看到切入的位置,向上看到两个红色部分是要去获取connection,我们跟踪进去看看: 接下来的就不用多说了吧,回到刚才的代码,不论是doGetConnection还是doReleaseConnection内部都会调用 TransactionSynchronizationManager.getResource(datasouce) 来获取当前线程的connection。 当然各种连接操作对象也会有自己的transaction操作,他们也会去做setAutoCommit等相应的操作。不过最外层设置后,getConnection方法保证后,内部的操作几乎就可以跳过了。
在大家使用spring MVC或Hibernate 3.0以上的版本时,可能会注意到annotation带来的方便性,不过这往往让人觉得annotation真的很强大,而这算是一种接近错误的理解吧,annotation其实本身是属于一种文档注解的方式,帮助我们在编译时、运行时、文档生成时使用,部分annotation其实基本和注释差不多,这里其实是要说下annotation的原理,以及各种功能在它上面如何实现的,以及在继承的时候,他会发生什么?为什么会这样? 首先,就我个人使用的理解,annotation是一种在类、类型、属性、参数、局部变量、方法、构造方法、包、annotation本身等上面的一个附属品(ElementType这个枚举中有阐述),他依赖于这些元素而存在,他本身并没有任何作用,annotation的作用是根据其附属在这些对象上,根据外部程序解析引起了他的作用,例如编译时的,其实编译阶段就在运行:java Compiler,他就会检查这些元素,例如:@SuppressWarnings、@Override、@Deprecated等等; 生成文档运行javadoc也是单独的一个进程去解析的,其实他是识别这些内容的,而spring MVC和Hibernate的注解,框架程序在运行时去解析这些annotation,至于运行的初始化还是什么时候要和具体的框架结合起来看,那么今天我们就要说下所谓的annotation是如何实现功能的(再次强调:它本身没有功能,功能又程序决定,他只是上面描述的几大元素的附属品而已,如果认为他本身有功能,就永远不知道annotation是什么); 我们首先自己来写个annotation,写annotation就像写类一样,创建一个java文件,和annotation的名称保持一致,他也会生成class文件,说明他也是java,只是以前要么是interface、abstract class、class开头,现在多了一个@interface,可见它是属于jvm可以识别的一种新的对象,就像序列化接口一样的标记,那么我们简单写一个: 下面的代码可能你看了觉得没啥意思,接着向下可能你会找到有意思的地方: import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.CONSTRUCTOR , ElementType.FIELD , ElementType.TYPE}) public @interface NewAnnotation { String value() default ""; } 那么上面的annotation代表: 在Runtime的时候将会使用(RetentionPolicy里面有阐述其他的SOURCE、CLASS级别),可以注解到方法、构造方法、属性、类型和类上面;名称为:NewAnnotation、里面有一个属性为value,为String类型,默认值为空字符串,也就是可以不传递参数。 程序中使用例如: public class A { @NewAnnotation private String b; @NewAnnotation(value = "abc") public void setB() {...} } 那么很多人看到这里都会问,这样写了有什么用途呢?貌似是没啥用途,我第一次看到这里也没太看懂,而且看到spring MVC做得如此多功能,这到底是怎么回事? 再一些项目的框架制作中,我逐步发现一些功能,如果有一种代码的附属品,将会将框架制作得更加漂亮和简洁,于是又联想到了spring的东西,spring的AOP是基于字节码增强技术完成,拦截器的实现不再是神话,那么反过来如如果annotation是可以被解析的,基于annotation的注入就是十分简单明了的事情了,Hibernate也是如此,当然我这不讨论一些解析的缓存问题,因为不会让每个对象都这样去解析一次,都会尽量记忆下来使得性能更高,这里只说他的原理而已。 这里拿一个简单的request对象转换为javaBean对象的,假如我们用DO为后缀,而部分请求的参数名和实际的属性并不一样(一般规范要求是一样的),其次在网络传输中某个项目前台的日期提交到后台都是以毫秒之方式提交到后台,但是需要转换为对应的字符串格式来处理,提交中包含:String、int、Integer、Long、long、String[]这几种数据类型,日期的我们大家可以扩展,带着这些小需求我们来简单写一个: import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) public @interface ReuqestAnnotation { String name() default "";//传入的参数名 boolean dateString() default false;//是否为dateString类型 } 这里的annotation一个是name一个是dateString,两个都有默认值,name我们认为是request中参数名,否则直接以属性名为主,dateString属性是否是日期字符串(前面描述传递的日期都会变成毫秒值,所以需要自动转换下)。 那么我们写一个DO: import xxx.xxx.ReuqestAnnotation;//import部分请自己根据项目引用 public class RequestTemplateDO { private String name; @ReuqestAnnotation(name = "myemail") private String email; private String desc; @ReuqestAnnotation(dateString = true) private String inputDate; private Integer int1; private int int2; public int getInt1() { return int1; } public int getInt2() { return int2; } public String getInputDate() { return inputDate; } public String getName() { return name; } public String getEmail() { return email; } public String getDesc() { return desc; } } 注意这里没有写set方法,我们为了说明问题,而不是怎么去调用set方法,所以我们直接用属性去设置值,属性是private一样可以设置,下面就是一个转换方法: 假如有一个HttpUtils,我们写一个静态方法: /** * 通过request获取对应的对象 * @param <T> * @param request * @param clazz * @return * @throws IllegalAccessException * @throws InstantiationException */ @SuppressWarnings("unchecked") public static <T extends Object> T convertRequestToDO(HttpServletRequest request , Class<T> clazz) throws InstantiationException, IllegalAccessException { Object object = clazz.newInstance(); Field []fields = clazz.getDeclaredFields(); for(Field field : fields) { field.setAccessible(true); String requestName = field.getName(); ReuqestAnnotation requestAnnotation = field.getAnnotation(ReuqestAnnotation.class); boolean isDateString = false; if(requestAnnotation != null) { if(StringUtils.isNotEmpty(requestAnnotation.name())) requestName = requestAnnotation.name(); isDateString = requestAnnotation.dateString(); } Class <?>clazzf = field.getType(); if(clazzf == String.class) { if(isDateString) { String dateStr = request.getParameter(requestName); if(dateStr != null) { field.set(object, DateTimeUtil.getDateTime(new Date(Long.valueOf(dateStr)) , DateTimeUtil.DEFAULT_DATE_FORMAT)); } }else { field.set(object, request.getParameter(requestName)); } } else if(clazzf == Integer.class) field.set(object, getInteger(request.getParameter(requestName))); else if(clazzf == int.class) field.set(object, getInt(request.getParameter(requestName))); else if(clazzf == Long.class) field.set(object, getLongWapper(request.getParameter(requestName))); else if(clazzf == long.class) field.setLong(object, getLong(request.getParameter(requestName))); else if(clazzf == String[].class) field.set(object, request.getParameterValues(requestName)); } return (T)object; } 这里面就会负责将request相应的值填充到数据中,返回对应的DO,而代码中使用的是: RequestTemplateDO requestTemplateDO = HttpUtils.convertRequestToDO(request , RequestTemplateDO.class); 注意:这部分spring帮我们写了,只是我在说大概原理,而且spring本身实现和这部分也有区别,也更加完整,这里仅仅是为了说明局部问题。spring在拦截器中拦截后就可以组装好这个DO,所以在spring MVC中可以将其直接作为扩展参数传递进入我们的业务方法中,首先知道业务方法的annotation,根据URL决定方法后,获取参数列表,根据参数类型,如果是业务DO,那么填充业务DO即可,Hibernate也可以同样的方式去推理。 OK,貌似很简单,如果你真的觉得简单了,那么这块你就真的懂了,那么我们说点特殊的,就是继承,貌似annotation很少去继承,但是在我遇到一些朋友的项目中,由于部分设计需要或本身设计缺陷但是又不想修改的时候,就会遇到,多个DO大部分属性是一样的,如果不抽象父亲类出来,如果修改属性要同时修改非常多的DO,而且操作的时候绝大部分情况是操作这些共享的属性,所以还想用上溯造型来完成代码的通用性并保持多态,当时一问我还真蒙了,因为是基于类似annotation的一些框架,例如hibernate,后来带着问题做了很多测试并且和资料对应上,是如果annotation在class级别、构造方法级别,是不会被子类所拥有的,也就是当子类通过XXX.class.getAnnotation(XXXAnnotation.class)的时候是获取不到,不过public类型的方法、public的属性是可以的,其次,如果子类重写了父类的某个属性或某个方法,不管子类是否写过annotation,这个子类中父类的属性或方法的所有的annotation全部失效,也就是如父亲类有一个属性A,有两个annotation,若A是public的,子类可以继承这个属性和annotation,若子类也有一个A属性,不管A是否有annotation,父类中这些annotation在子类中都将失效掉。 这就是为什么我说annotation是属性、方法、包。。。的附属品,他是被绑定在这些元素上的,而并非拥有实际功能,当重写的时候,将会被覆盖,属性、方法当被覆盖的时候,其annotation也随之被覆盖,而不会按照annotation再单独有一个覆盖;所以当时要解决那个问题,我就告诉他,要用的方法只有public,否则没办法,即使用反射也不行,因为hibernate根本找不到这个field,还没有机会提供一个setAccessible的能力,因为这个时候根本看不到这些field,但是通过父类本身可以看到这些field;就技术层面是这样的,否则只有修改设计是最佳的方法。
J.U.C是java系列一块看似简单,水很深的区域,但是不论是深入java还是分布式的一些东西,这都算是基础,虽然以前乱七八糟写过一些多线程的文章,不过都比较乱了一点,最近有打算逐步深入来写多篇文章来说说我对这些东西的小理解。 1、首先线程分为内核线程、用户线程;在Linux下java的线程其实是在java私有栈上有一个用户线程,和OS级别有一个轻量级的进程来实现。 2、在操作java多线程的时候必然会遇到锁的问题,在锁的问题中,线程会首先进入一个所谓的Entry Set的集合中,然后尝试去征用锁,征用到锁的就是他的owner,没有征用到,调用wait(),那么就进入Wait Set的区域中,使用完后,锁被释放,调用notfiy或notifyAll欢迎Wait Set区域的内容重新尝试,和Entry Set里面的信息一起征用;调用sleep是没有锁的;synchronized是系统级别的锁;notify是唤醒一个Wait Set区域的等待线程,而notifyAll是唤醒所有相关的线程,join是当前线程会等待对应的几个线程执行完再向下走;Daemon设置为true设置为后台线程,指:主线程执行完后,后台线程自动完成,否则不会;yield是当前让出一个CPU的时间,给其他线程来做,但是让多久,谁也不知道,要知道让多久,sleep知道。 3、blocked状态的线程一般是挂住的线程,由于锁机制的原因导致,或一些网络原因导致,并且此时interrupt是无法断开的,锁机制的问题在后续的文章中会介绍很多机制来避免,但是网络机制的在java中请用socket的超时soTimeout来设置超时,否则无法断开(包括连接数据库等)soTimeout意义在于发生一次read操作的时候,连接超时时间,也就是等结果的时候一直没等到,而不是总的连接超时; 4、interrupt只会对wait和time_wait状态的线程有效,如果程序是处于循环运行状态,外部发起一个interrupt命令要求停止这个线程,此时程序默认是不会被停止的,所以你在看某些框架的时候,里面会在循环体内部有一个操作是: Thread.currentThread().isInterrupted() 就是判定当前线程时候被中断,从这里你可以看出中断只是在线程上打了一个tag(true|false),此时你发现被中断就应该break跳出循环;注意还有一个方法是:interrupted();这个方法不仅仅会设置方法是否被中断,而且还会设置线程是未中断的状态,也就是将interrupt的状态重新会设置为false,不过当前会返回false,可以在任意一个main方法中写一下代码测试: Thread.currentThread().interrupt(); System.out.println(Thread.currentThread().interrupted()); System.out.println(Thread.currentThread().interrupted()); //Thread.currentThread().interrupt(); System.out.println(Thread.currentThread().isInterrupted()); System.out.println(Thread.currentThread().isInterrupted()); 然后将注释掉的代码打开再看看结果;源码部分很简单: public boolean isInterrupted() { return isInterrupted(false); } public static boolean interrupted() { return currentThread().isInterrupted(true); } 上一篇文章说过一些关于CPU Cache Line的一些问题,其实可以看出,如果你想要自己在java的内存中设置缓存的话,建议这些缓存是基本不被修改的,最好是final的;如果是变化的保证可见性的情况下需要选择volatile、保证原子性优先选择Atomic*,不到万不得已,不自己玩锁,尤其是在静态方法或Class上去做锁;关于可见性的描述,后面的文章中还会更加深入的阐述下。 5、java 现在提供了很多java.util.concurrent包来替代原来的很多集合类,但并不代表可以解决所有问题,也不代表原来的集合类没用,更加更加不代表不需要了解各种集合类的特征和使用场景;在后面的文章中我们会介绍这些内容的细节,总之如果你发现你所要访问的内容存在多线程的访问,并且他们是可能被修改的,那么就存在并发,并发就存在诸多的问题需要处理; 6、有人认为,并发时我不确保数据的正确性的情况下或者报个错也可以接受,不使用非并发包(HashMap这类)也觉得可以,NO,在并发的情况下有些时候不仅仅是报错的问题,有些时候可能会引起并发时Key重复死循环情况。 7、ABA问题是我们比较痛苦的问题,即使使用很多并发的机制来解决也未必是真实的,所以对于ABA问题,java为我们提供了版本控制的方法,后续文章也逐步说明。 8、ThreadLocal这把双刃剑,能爽死你,也能玩死你;ThreadLoad在传递参数的时候,是非常有效的,加参数也很方便,他是与当前线程绑定的,不过查问题痛苦了,尤其是这些东西被封装到三方包里面的时候,因为WEB模型的线程是不会被释放的,所以ThreadLocal内部的参数也不会释放,在什么地方被修改一个不知道,其次容易引起内存泄露。 9、JVM也为你提供了各种各样的线程池,他们能为你解决什么,和普通线程的区别,线程池的处理模型和策略;通过线程池如何完成一个调度器的功能;通过线程池完成很多的现实模型而不需要你自己去写算法去解决一些复杂恶心的线程交互问题;在这里还有一个Future、FutureTask是咋回事;这个我们在后面说 10、锁机制的到底是啥玩意,现在的java如何玩锁,怎么玩清楚锁;synchronized、Lock(ReentrantLock、ReadWriteLock)如何选择,等待队列分组;啥时候死锁,除了交叉死锁还有什么死锁?tryLock咋玩的?如何提升一些性能? 11、Queue、List、Map并发容器的介绍和使用;CountDownLatch、Semaphone、CyclicBarrier、Exchanger使用(AbstractQueuedSynchronizer)工具篇;后续专门介绍。 12、Atomic系列之Atomic<基本的变量>、Atomic<基本变量>FiledUpdater、Atomic<基本变量>Array,以及AtomicReference(引用)、AtomicStampedReference(带版本号的引用)、AtomicMarkableReference(可以进行计数);后续专门说明; 本文除了前面的简单介绍外,后续部分就是一个大概介绍,由于篇幅所限,只能逐步完善后面的内容。
其实写java的人貌似和CPU没啥关系,最多最多和我们在前面提及到的如何将CPU跑满、如何设置线程数有点关系,但是那个算法只是一个参考,很多场景不同需要采取实际的手段来解决才可以;而且将CPU跑满后我们还会考虑如何让CPU不是那么满,呵呵,人类,就是这么XX,呵呵,好了,本文要说的是其他的一些东西,也许你在java的写代码时几乎不用关注CPU,因为满足业务才是第一重要的事情,如果你要做到框架级别,为框架提供很多共享数据缓存之类的东西,中间必然存在很多数据的征用问题,当然java提供了很多concurrent包的类,你可以用它,但是它内部如何做的,你要明白细节才能用得比较好,否则还不如不用,本文可能不是阐述这些内容作为重点,因为如标题党:我们要说CPU,呵呵。 还是那句话,貌似java和CPU没有多少关系,我们现在来聊聊有啥关系; 1、当遇到共享元素,我们通常第一想法是通过volatile来保证一致性读的操作,也就是绝对的可见性,所谓可见性,就是每次要使用该数据的时候,CPU不会使用任何cache的内容都会从内存中去抓取一次数据,并且这个过程对多CPU仍然有效,也就是相当CPU和内存之间此时是同步的,CPU会像总线发出一个Lock addl 0类似的的汇编指令,+0但相对于什么都不会做;不过一旦该指令完成,后续操作将不再影响这个元素其他线程的访问,也就是他能实现的绝对可见性,但是不能实现一致性操作,也就是说,volatile不能实现的是i++这类操作的一致性(在多线程下并发),因为i++操作是被分解为: int tmp = i; tmp = tmp + 1; i = tmp; 这三个步骤来完成,从这点你也能看出i++为什么能实现先做其他的事情再自我加1,因为它讲值赋予给了另一个变量。 2、我们要用到多线程并发一致性,就需要用到锁的机制,目前类似Atomic*的东西基本可以满足这些要求,内部提供了很多unsafe类的方法,通过不断对比绝对可见性的数据来保证获取的数据是最新的;接下来我们继续来说一些CPU其他的事情。 3、以前我们为了将CPU跑满,但是无论如何跑不满,因为我们开始说了忽略掉内存与CPU的延迟,今天既然提及到这里,我们就简单说下延迟,一般来讲现在的CPU有三级cache,年代不同延迟不同,所以具体数字只能说个大概而已,现在的CPU一般一级cache的延迟在1-2ns,二级cache一般是几个ns到十来ns左右,三级cache一般是30ns到50ns不等,内存访问普遍会上到70ns甚至更多(计算机发展速度很快,这个值也仅仅在某些CPU上的数据,做一个范围参考而已);别看这个延迟很小,都是纳秒级别,你会发现你的程序被拆分为指令运算的时候,会有很多CPU交互,每次交互的延迟如果有这么大的偏差,此时系统性能是会有变化的; 4、回到刚才说的volatile,它每次从内存中获取数据,就是放弃cache,自然如果在某些单线程的操作中,会变得更加慢,有些时候我们也不得不这样做,甚至于读写操作都要求一致性,甚至于整个数据块都被同步,我们只能在一定程度上降低锁的粒度,但是不能完全没有锁,即使是CPU本身级别也会有指令级别的限制,如下: 5、在CPU本身级别的原子操作一般叫屏障,有读屏障、写屏障等,一般是基于一个点的触发,当程序多条指令发送到CPU的时候,有些指令未必是按照程序的顺序来执行,有些必须按照程序的顺序来执行,只要能最终保证一致即可;在排序上,JIT在运行时会做改变,CPU指令级别也会做改变,原因主要是为了优化运行时指令让程序跑得更快。 6、CPU级别会对内存做cache line的操作,所谓cache line会连续读一块内存,一般和CPU型号和架构有关系,现在很多CPU每次读取连续内存一般是64byte,早期的有32byte的,所以在某些数组遍历的时候会比较快(基于列遍历很慢),但这个并不完全对,下面会对照一些相反的情况来说。 7、CPU对数据如果发生了修改,此时就不得不说CPU对数据修改的状态,数据如果都被读取,在多CPU下可以被多线程并行读取并,当对数据块发生写操作的时候,就不一样了,数据块会有独占、修改、失效等状态,数据修改后自然就会失效,当在多CPU下,多个线程都在对同一个数据块进行修改时,就会发生CPU之间的总线数据拷贝(QPI),当然如果修改到同一个数据上的时候我们是没有办法的,但是回到第6点的cache line里面,问题就比较麻烦了,如果数据是在同一个数组上,而数组中的元素会被同时cache line到一个CPU上的时候,多线程的QPI就会非常频繁,有些时候即使是数组上组装的是对象也会出现这个问题,如: class InputInteger { private int value; public InputInteger(int i) { this.value = i; } } InputInteger[] integers = new InputInteger[SIZE]; for(int i=0 ; i < SIZE ; i++) { integers[i] = new InputInteger(i); } 此时你看出来integers里面放的全部是对象,数组上只有对象的引用,但是对象的排布理论上说各自对象是独立的,不会连续存放,不过java在分配对象内存的时候,很多时候,在Eden区域是连续分配的,当在for循环的时候,如果没有其他线程的接入,这些对象就会被存放在一起,即使被GC到OLD区域也很有可能会放在一起,所以靠简单对象来解决cache line后还对整个数组修改的方式貌似不靠谱,因为int 是4字节,如果在64模式下,这个大小是24字节(有4byte补齐),指针压缩开启是16byte;也就是每次cpu可以看齐3-4个对象,如何让CPUcache了,但是又不影响系统的QPI,别想通过分隔对象来完成,因为GC过程内存拷贝过程很可能会拷贝到一起,最好的办法是补齐,虽然有点浪费内存,但是这是最靠谱的方法,就是将对象补齐到64字节,上述若未开启指针压缩有24byte,此时还有40个字节,只需要在对象内部增加5个long即可。 class InputInteger { public int value; private long a1,a2,a3,a4,a5; } 呵呵,这个办法很土,不过很管用,有些时候,Jvm编译的时候发现这几个参数啥都没做,就直接给你干掉了,优化无效,土办法加土办法就是在一个方法体里面简单对这5个参数做一个操作(都用上),但是这个方法永远不调用它即可。 8、在CPU这个级别有些时候就未必能先做尽量先做的道理为王者了,类似获取锁这种操作,在AtomicIntegerFieldUpdater的操作,如果调用getAndSet(true)在单线程下你会发现跑得还蛮快,在多核CPU下就开始变慢,为什么上面说得很清楚了,因为getAndSet里面是修改后对比,先改了再说,QPI会很高,所以这个时候,先做get操作,再修改才是比较好的做法;还有就是获取一次,如果获取不到,就让步一下,让其他的线程去做其他的事情; 9、CPU有些时候为了解决某些CPU忙和不繁忙的问题,会有很多算法来解决,如NUMA是其中一种方案,不过不论哪种架构都在一定场景下比较有用,对有所有场景未必有效;有队列锁机制来完成对CPU状态管理,不过这又存在了cache line的问题,因为状态都是经常改变的,各类应用程序的内核为了配合CPU也会出一些算法来做,使得CPU可以更加有效的利用起来,如CLH队列等。 有关这方面的细节会很多如用普通变量循环叠加和用volatile类型的做以及Atomic*系列的来做,完全是不一样的;多维度数组循环,按照不同纬度向后次序来循环也是不一样的,细节上点很多,明白为什么就在实际优化过程中有灵感了;锁的细节说太细很晕,在系统底层的级别,始终有一些轻量级的原子操作,不论谁说他的代码是不需要加锁的,最细的可以细到CPU在每个瞬间只能执行一条指令那么简单,多核心CPU在总线级别也会有共享区来控制一些内容,有读级别、写级别、内存级别等,在不同的场景下使得锁的粒度尽量降低,那么系统的性能不言而喻,很正常的结果。 本文就说到这里,闲扯了下,仅供参考!
众所周知,java在处理数据量比较大的时候,加载到内存必然会导致内存溢出,而在一些数据处理中我们不得不去处理海量数据,在做数据处理中,我们常见的手段是分解,压缩,并行,临时文件等方法; 例如,我们要将数据库(不论是什么数据库)的数据导出到一个文件,一般是Excel或文本格式的CSV;对于Excel来讲,对于POI和JXL的接口,你很多时候没有办法去控制内存什么时候向磁盘写入,很恶心,而且这些API在内存构造的对象大小将比数据原有的大小要大很多倍数,所以你不得不去拆分Excel,还好,POI开始意识到这个问题,在3.8.4的版本后,开始提供cache的行数,提供了SXSSFWorkbook的接口,可以设置在内存中的行数,不过可惜的是,他当你超过这个行数,每添加一行,它就将相对行数前面的一行写入磁盘(如你设置2000行的话,当你写第20001行的时候,他会将第一行写入磁盘),其实这个时候他些的临时文件,以至于不消耗内存,不过这样你会发现,刷磁盘的频率会非常高,我们的确不想这样,因为我们想让他达到一个范围一次性将数据刷如磁盘,比如一次刷1M之类的做法,可惜现在还没有这种API,很痛苦,我自己做过测试,通过写小的Excel比使用目前提供刷磁盘的API来写大文件,效率要高一些,而且这样如果访问的人稍微多一些磁盘IO可能会扛不住,因为IO资源是非常有限的,所以还是拆文件才是上策;而当我们写CSV,也就是文本类型的文件,我们很多时候是可以自己控制的,不过你不要用CSV自己提供的API,也是不太可控的,CSV本身就是文本文件,你按照文本格式写入即可被CSV识别出来;如何写入呢?下面来说说。。。 在处理数据层面,如从数据库中读取数据,生成本地文件,写代码为了方便,我们未必要1M怎么来处理,这个交给底层的驱动程序去拆分,对于我们的程序来讲我们认为它是连续写即可;我们比如想将一个1000W数据的数据库表,导出到文件;此时,你要么进行分页,oracle当然用三层包装即可,mysql用limit,不过分页每次都会新的查询,而且随着翻页,会越来越慢,其实我们想拿到一个句柄,然后向下游动,编译一部分数据(如10000行)将写文件一次(写文件细节不多说了,这个是最基本的),需要注意的时候每次buffer的数据,在用outputstream写入的时候,最好flush一下,将缓冲区清空下;接下来,执行一个没有where条件的SQL,会不会将内存撑爆?是的,这个问题我们值得去思考下,通过API发现可以对SQL进行一些操作,例如,通过:PreparedStatement statement = connection.prepareStatement(sql),这是默认得到的预编译,还可以通过设置:PreparedStatement statement = connection.prepareStatement(sql , ResultSet.TYPE_FORWARD_ONLY , ResultSet.CONCUR_READ_ONLY); 来设置游标的方式,以至于游标不是将数据直接cache到本地内存,然后通过设置statement.setFetchSize(200);设置游标每次遍历的大小;OK,这个其实我用过,oracle用了和没用没区别,因为oracle的jdbc API默认就是不会将数据cache到java的内存中的,而mysql里头设置根本无效,我上面说了一堆废话,呵呵,我只是想说,java提供的标准API也未必有效,很多时候要看厂商的实现机制,还有这个设置是很多网上说有效的,但是这纯属抄袭;对于oracle上面说了不用关心,他本身就不是cache到内存,所以java内存不会导致什么问题,如果是mysql,首先必须使用5以上的版本,然后在连接参数上加上useCursorFetch=true这个参数,至于游标大小可以通过连接参数上加上:defaultFetchSize=1000来设置,例如: jdbc:mysql://xxx.xxx.xxx.xxx:3306/abc?zeroDateTimeBehavior=convertToNull&useCursorFetch=true&defaultFetchSize=1000 上次被这个问题纠结了很久(mysql的数据老导致程序内存膨胀,并行2个直接系统就宕了),还去看了很多源码才发现奇迹竟然在这里,最后经过mysql文档的确认,然后进行测试,并行多个,而且数据量都是500W以上的,都不会导致内存膨胀,GC一切正常,这个问题终于完结了。 我们再聊聊其他的,数据拆分和合并,当数据文件多的时候我们想合并,当文件太大想要拆分,合并和拆分的过程也会遇到类似的问题,还好,这个在我们可控制的范围内,如果文件中的数据最终是可以组织的,那么在拆分和合并的时候,此时就不要按照数据逻辑行数来做了,因为行数最终你需要解释数据本身来判定,但是只是做拆分是没有必要的,你需要的是做二进制处理,在这个二进制处理过程,你要注意了,和平时read文件不要使用一样的方式,平时大多对一个文件读取只是用一次read操作,如果对于大文件内存肯定直接挂掉了,不用多说,你此时因该每次读取一个可控范围的数据,read方法提供了重载的offset和length的范围,这个在循环过程中自己可以计算出来,写入大文件和上面一样,不要读取到一定程序就要通过写入流flush到磁盘;其实对于小数据量的处理在现代的NIO技术的中也有用到,例如多个终端同时请求一个大文件下载,例如视频下载吧,在常规的情况下,如果用java的容器来处理,一般会发生两种情况: 其一为内存溢出,因为每个请求都要加载一个文件大小的内存甚至于更多,因为java包装的时候会产生很多其他的内存开销,如果使用二进制会产生得少一些,而且在经过输入输出流的过程中还会经历几次内存拷贝,当然如果有你类似nginx之类的中间件,那么你可以通过send_file模式发送出去,但是如果你要用程序来处理的时候,内存除非你足够大,但是java内存再大也会有GC的时候,如果你内存真的很大,GC的时候死定了,当然这个地方也可以考虑自己通过直接内存的调用和释放来实现,不过要求剩余的物理内存也足够大才行,那么足够大是多大呢?这个不好说,要看文件本身的大小和访问的频率; 其二为假如内存足够大,无限制大,那么此时的限制就是线程,传统的IO模型是线程是一个请求一个线程,这个线程从主线程从线程池中分配后,就开始工作,经过你的Context包装、Filter、拦截器、业务代码各个层次和业务逻辑、访问数据库、访问文件、渲染结果等等,其实整个过程线程都是被挂住的,所以这部分资源非常有限,而且如果是大文件操作是属于IO密集型的操作,大量的CPU时间是空余的,方法最直接当然是增加线程数来控制,当然内存足够大也有足够的空间来申请线程池,不过一般来讲一个进程的线程池一般会受到限制也不建议太多的,而在有限的系统资源下,要提高性能,我们开始有了new IO技术,也就是NIO技术,新版的里面又有了AIO技术,NIO只能算是异步IO,但是在中间读写过程仍然是阻塞的(也就是在真正的读写过程,但是不会去关心中途的响应),还未做到真正的异步IO,在监听connect的时候他是不需要很多线程参与的,有单独的线程去处理,连接也又传统的socket变成了selector,对于不需要进行数据处理的是无需分配线程处理的;而AIO通过了一种所谓的回调注册来完成,当然还需要OS的支持,当会掉的时候会去分配线程,目前还不是很成熟,性能最多和NIO吃平,不过随着技术发展,AIO必然会超越NIO,目前谷歌V8虚拟机引擎所驱动的node.js就是类似的模式,有关这种技术不是本文的说明重点; 将上面两者结合起来就是要解决大文件,还要并行度,最土的方法是将文件每次请求的大小降低到一定程度,如8K(这个大小是经过测试后网络传输较为适宜的大小,本地读取文件并不需要这么小),如果再做深入一些,可以做一定程度的cache,将多个请求的一样的文件,cache在内存或分布式缓存中,你不用将整个文件cache在内存中,将近期使用的cache几秒左右即可,或你可以采用一些热点的算法来配合;类似迅雷下载的断点传送中(不过迅雷的网络协议不太一样),它在处理下载数据的时候未必是连续的,只要最终能合并即可,在服务器端可以反过来,谁正好需要这块的数据,就给它就可以;才用NIO后,可以支持很大的连接和并发,本地通过NIO做socket连接测试,100个终端同时请求一个线程的服务器,正常的WEB应用是第一个文件没有发送完成,第二个请求要么等待,要么超时,要么直接拒绝得不到连接,改成NIO后此时100个请求都能连接上服务器端,服务端只需要1个线程来处理数据就可以,将很多数据传递给这些连接请求资源,每次读取一部分数据传递出去,不过可以计算的是,在总体长连接传输过程中总体效率并不会提升,只是相对相应和所开销的内存得到量化控制,这就是技术的魅力,也许不要太多的算法,不过你得懂他。 类似的数据处理还有很多,有些时候还会将就效率问题,比如在HBase的文件拆分和合并过程中,要不影响线上业务是比较难的事情,很多问题值得我们去研究场景,因为不同的场景有不同的方法去解决,但是大同小异,明白思想和方法,明白内存和体系架构,明白你所面临的是沈阳的场景,只是细节上改变可以带来惊人的效果。
本文核心主要参数动态代理和cglib; 在以前的文章中,有提及到动态代理,它要解决的就是,当我们的某些代码前面或后面都需要一些处理的时候,如写日志、事务控制、做agent、自动化代码跟踪等,此时会给你带来无限的方便,这是JVM级别的提供的一种代理机制,不过在这种机制下调用方法在JVM7出来前还没有invokeDynamic的时候,调用的效率是很低的,此时方法调用都是通过method的invoke去实现。 其基本原理是基于实现JVM提供的一个: InvocationHandler的接口,实现一个方法叫:public Object invoke(Object proxyed, Method method, Object[] args); 创建类的时候,通过实例化这个类(这个类就是实现InvocationHandler的类),再将实际要实现的类的class放进去,通过Proxy来实例化。 以下为一段简单动态代理的实现代码(以下代码放入一个文件:DynamicProxy.java): import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; //定义了一个接口 interface Hello { public String getInfos1(); public String getInfos2(); public void setInfo(String infos1, String infos2); public void display(); } //定义它的实现类 class HelloImplements implements Hello { private volatile String infos1; private volatile String infos2; public String getInfos1() { return infos1; } public String getInfos2() { return infos2; } public void setInfo(String infos1, String infos2) { this.infos1 = infos1; this.infos2 = infos2; } public void display() { System.out.println("\t\t" + infos1 + "\t" + infos2); } } 定义AOP的Agent class AOPFactory implements InvocationHandler { private Object proxyed; public AOPFactory(Object proxyed) { this.proxyed = proxyed; } public void printInfo(String info, Object... args) { System.out.println(info); if (args == null) { System.out.println("\t空值。"); }else { for(Object obj : args) { System.out.println(obj); } } } public Object invoke(Object proxyed, Method method, Object[] args) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { System.out.println("\n\n====>调用方法名:" + method.getName()); Class<?>[] variables = method.getParameterTypes(); for(Class<?>typevariables: variables) { System.out.println("=============>" + typevariables.getName()); } printInfo("传入的参数为:", args); Object result = method.invoke(this.proxyed, args); printInfo("返回的参数为:", result); printInfo("返回值类型为:", method.getReturnType()); return result; } } //测试调用类 public class DynamicProxy { public static Object getBean(String className) throws InstantiationException, IllegalAccessException, ClassNotFoundException { Object obj = Class.forName(className).newInstance(); InvocationHandler handler = new AOPFactory(obj); return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj .getClass().getInterfaces(), handler); } @SuppressWarnings("unchecked") public static void main(String[] args) { try { Hello hello = (Hello) getBean("dynamic.HelloImplements"); hello.setInfo("xieyu1", "xieyu2"); hello.getInfos1(); hello.getInfos2(); hello.display(); } catch (Exception e) { e.printStackTrace(); } } } OK,可以看看输出结果,此时的输出结果不仅仅有自己的Hello实现类的中打印结果,还有proxy代理中的结果,它可以捕获方法信息和入参数,也可以捕获返回结果,也可以操作方法,所以这种非侵入式编程本身就是侵入式的,呵呵! 好了,你会发现都有接口,有些时候写太多接口很烦,而且上面的调用性能的确不怎么样,除了JVM提供的动态代理,还有什么办法吗?有的,org的asm包可以动态修改字节码信息,也就是可以动态在内存中创建class类和修改class类信息;但是听起来貌似很复杂,cglib为我们包装了对asm的操作,整个ASM包的操作非常小,但是代码很精炼,很容易看懂。那么cglib实现的时候,就是通过创建一个类的子类,然后在调用时,子类方法肯定覆盖父类方法,然后子类在完成相关动作后,进行super的回调; 我们来看个例子(首先下载asm包,和cglib包,各个版本不同而不同,我使用的是asm-all-3.1.jar和cglib-2.2.jar): 下面的程序只需创建文件:CglibIntereceptor.java import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; //创建一个类,用来做测试 class TestClass { public void doSome() { System.out.println("====>咿呀咿呀喂"); } } public class CglibIntereceptor { static class MethodInterceptorImpl implements MethodInterceptor { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println(method); proxy.invokeSuper(obj, args); return null; } } public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(TestClass.class); enhancer.setCallback( new MethodInterceptorImpl() ); TestClass my = (TestClass)enhancer.create(); my.doSome(); } } 看看打印结果: public void dynamic.TestClass.doSome()====>咿呀咿呀喂 //注意看黑色粗体标识出来的代码,首先要实现MethodInterceptor,然后实现方法intercept,内部使用invokeSuper来调用父类;下面的实例都是通过Enhancer 来完成的;细节的后续我们继续探讨,现在就知道这样可以使用,而spring的真正实现也是类似于此,只是spring对于cglib的使用做了其他的包装而已;大家可以去看看spring对事务管理器的源码即可了解真相。 下面问题来了,我们有些时候对某些方法不想去AOP,因为我认为只有需要包装的才去包装,就像事务管理器中切入的时候,我们一般会配置一个模式匹配,哪些类和那些方法才需要做AOP;那么cglib怎么实现的,它提供了一个CallbackFilter来实现这个机制。OK,我们来看一个CallbackFilter的实例: 以下代码创建文件:CglibCallBackFilter.java import java.lang.reflect.Method; import net.sf.cglib.proxy.Callback; import net.sf.cglib.proxy.CallbackFilter; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import net.sf.cglib.proxy.NoOp; class CallBackFilterTest { public void doOne() { System.out.println("====>1"); } public void doTwo() { System.out.println("====>2"); } } public class CglibCallBackFilter { static class MethodInterceptorImpl implements MethodInterceptor { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println(method); return proxy.invokeSuper(obj, args); } } static class CallbackFilterImpl implements CallbackFilter { public int accept(Method method) {//返回1代表不会进行intercept的调用 return ("doTwo".equals(method.getName())) ? 1 : 0; } } public static void main(String[] args) { Callback[] callbacks = new Callback[] { new MethodInterceptorImpl(), NoOp.INSTANCE }; Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(CallBackFilterTest.class); enhancer.setCallbacks( callbacks ); enhancer.setCallbackFilter( new CallbackFilterImpl()); CallBackFilterTest callBackFilterTest = (CallBackFilterTest)enhancer.create(); callBackFilterTest.doOne(); callBackFilterTest.doTwo(); } } 看下打印结果: public void dynamic.CallBackFilterTest.doOne()====>1 ====>2可以看到只有方法1打印出了方法名,方法2没有,也就是方法2调用时没有调用:intercept来做AOP操作,而是直接调用的;可以看出,他上层有一个默认值,而callbacks里面设置了NoOp.INSTANCE,就代表了不做任何操作的一个实例,你应该懂了吧,就是当不做AOP的时候调用那种实例来运行,当需要AOP的时候调用那个实例来运行;怎么对应上的,它自己不知道,accept返回的是一个数组的下标,callbacks是一个数组,那么你猜猜是不是数组的下标呢,你自己换下位置就知道了,呵呵,是的,没错就是数组下标,不相信可以翻翻他的源码就知道了。 其实你可以看出cglib就是在修改字节码,貌似很方面,spring、Hibernate等也大量使用它,但是并不代表你可以大量使用它,尤其是在写业务代码的时候,只有写框架才可以适当考虑使用这些东西,spring的反射等相关一般都是初始化决定的,一般都是单例的,前面谈及到JVM时,很多JVM优化原则都是基于VM的内存结构不会发生变化,如果发生了变化,那么优化就会存在很多的问题了,其次无限制使用这个东西可能会使得VM的Perm Gen内存溢出。 最后我们看个实际应用中没啥用途,但是cglib实现的一些东西,java在基于接口、抽象类的情况下,实现了很多特殊的机制,而cglib可以将两个根本不想管的接口和类合并到一起来操作,这也是字节码的一个功劳,呵呵,它的原理就是在接口下实现了子类,并把其他两个作为回调的方法,即可实现,但是实际应用中这种用法很诡异,cglib中是使用:Mixin来创建,而并非Enhancer了。例子如下: 创建文件:CglibMixin.java import net.sf.cglib.proxy.Mixin; interface Interface1 { public void doInterface1(); } interface Interface2 { public void doInterface2(); } class ImpletmentClass1 implements Interface1 { public void doInterface1() { System.out.println("===========>方法1"); } } class ImpletmentClass2 implements Interface2 { public void doInterface2() { System.out.println("===========>方法2"); } } public class CglibMixin { public static void main(String []args) { Class<?>[] interfaces = new Class[] { Interface1.class, Interface2.class }; Object[] implementObjs = new Object[] { new ImpletmentClass1(), new ImpletmentClass2()}; Object obj = Mixin.create(interfaces,implementObjs); Interface1 interface1 = (Interface1)obj; Interface2 interface2 = (Interface2)obj; interface1.doInterface1(); interface2.doInterface2(); } } 结果就不用打印了,上面有描述,主要是两个接口、两个实例,最终用一个对象完成了,传递过程中只有一个,比起传统意义上的多态更加具有多态的效果,呵呵,不过还是建议少用。 本文只是简单阐述框架级别动态代理和cglib的实现,后续会深入探讨一些cglib的实现细节和功能,以及如何在框架中抽象出模型出来。
byte buffer一般在网络交互过程中java使用得比较多,尤其是以NIO的框架中; 看名字就知道是以字节码作为缓冲的,先buffer一段,然后flush到终端。 而本文要说的一个重点就是HeapByteBuffer与DirectByteBuffer,以及如何合理使用DirectByteBuffer。 1、HeapByteBuffer与DirectByteBuffer,在原理上,前者可以看出分配的buffer是在heap区域的,其实真正flush到远程的时候会先拷贝得到直接内存,再做下一步操作(考虑细节还会到OS级别的内核区直接内存),其实发送静态文件最快速的方法是通过OS级别的send_file,只会经过OS一个内核拷贝,而不会来回拷贝;在NIO的框架下,很多框架会采用DirectByteBuffer来操作,这样分配的内存不再是在java heap上,而是在C heap上,经过性能测试,可以得到非常快速的网络交互,在大量的网络交互下,一般速度会比HeapByteBuffer要快速好几倍。 最基本的情况下 分配HeapByteBuffer的方法是: ByteBuffer.allocate(int capacity);参数大小为字节的数量 分配DirectByteBuffer的方法是: ByteBuffer.allocateDirect(int capacity);//可以看到分配内存是通过unsafe.allocateMemory()来实现的,这个unsafe默认情况下java代码是没有能力可以调用到的,不过你可以通过反射的手段得到实例进而做操作,当然你需要保证的是程序的稳定性,既然叫unsafe的,就是告诉你这不是安全的,其实并不是不安全,而是交给程序员来操作,它可能会因为程序员的能力而导致不安全,而并非它本身不安全。 由于HeapByteBuffer和DirectByteBuffer类都是default类型的,所以你无法字节访问到,你只能通过ByteBuffer间接访问到它,因为JVM不想让你访问到它,对了,JVM不想让你访问到它肯定就有它不可告人的秘密;后面我们来跟踪下他的秘密吧。 2、前面说到了,这块区域不是在java heap上,那么这块内存的大小是多少呢?默认是一般是64M,可以通过参数:-XX:MaxDirectMemorySize来控制,你够牛的话,还可以用代码控制,呵呵,这里就不多说了。 3、直接内存好,我们为啥不都用直接内存?请注意,这个直接内存的释放并不是由你控制的,而是由full gc来控制的,直接内存会自己检测情况而调用system.gc(),但是如果参数中使用了DisableExplicitGC 那么这是个坑了,所以啊,这玩意,设置不设置都是一个坑坑,所以java的优化有没有绝对的,只有针对实际情况的,针对实际情况需要对系统做一些拆分做不同的优化。 4、那么full gc不触发,我想自己释放这部分内存有方法吗?可以的,在这里没有什么是不可以的,呵呵!私有属性我们都任意玩他,还有什么不可以玩的;我们看看它的源码中DirectByteBuffer发现有一个:Cleaner,貌似是用来搞资源回收的,经过查证,的确是,而且又看到这个对象是sun.misc开头的了,此时既惊喜又郁闷,呵呵,只要我能拿到它,我就能有希望消灭掉了;下面第五步我们来做个试验。 5、因为我们的代码全是私有的,所以我要访问它不能直接访问,我需要通过反射来实现,OK,我知道要调用cleaner()方法来获取它Cleaner对象,进而通过该对象,执行clean方法;(付:以下代码大部分也取自网络上的一篇copy无数次的代码,但是那个代码是有问题的,有问题的部分,我将用红色标识出来,如果没有哪条代码是无法运行的) import java.nio.ByteBuffer; import sun.nio.ch.DirectBuffer; public class DirectByteBufferCleaner { public static void clean(final ByteBuffer byteBuffer) { if (byteBuffer.isDirect()) { ((DirectBuffer)byteBuffer).cleaner().clean(); } } } 上述类你可以在任何位置建立都可以,这里多谢一楼的回复,以前我的写法是见到DirectByteBuffer类是Default类型的,因此这个类无法直接引用到,是通过反射去找到cleaner的实例,进而调用内部的clean方法,那样做麻烦了,其实并不需要那么麻烦,因为DirectByteBuffer implements了DirectBuffer,而DirectBuffer本身是public的,所以通过接口去调用内部的Clear对象来做clean方法。 我们下面来做测试来证明这个程序是有效地回收的: 在任意一个地方写一段main方法来调用,我这里就直接写在这个类里面了: public static void sleep(long i) { try { Thread.sleep(i); }catch(Exception e) { /*skip*/ } } public static void main(String []args) throws Exception { ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 100); System.out.println("start"); sleep(10000); clean(buffer); System.out.println("end"); sleep(10000); } 这里分配了100M内存,为了将结果看清楚,在执行前,执行后分别看看延迟10s,当然你可以根据你的要求自己改改。请提前将OS的资源管理器打开,看看当前使用的内存是多少,如果你是linux当然是看看free或者用top等命令来看;本地程序我是用windows完成,在运行前机器的内存如下图所示: 开始运行在输入start后,但是未输出end前,内存直接上升将近100m。 在输入end后发现内存立即降低到2.47m,说明回收是有效的。 此时可以观察JVM堆的内存,不会有太多的变化,注意:JVM本身启动后也有一些内存开销,所以不要将那个开销和这个绑定在一起;这里之所以一次性申请100m也是为了看清楚过程,其余的可以做实验玩玩了。
java其实更多用来写业务代码,代码写得好不好,关键看抽象能力如何,不过如果你要用java写很核心的插件和高并发的片段,那么可能还是需要注意一些写法,那种写法可能会更好,才能使得并发量提高,而且更少的使用CPU和内存;我最近在一段采集系统访问的java代码,通过过滤器切入到应用中,遇到的一些小细节的调整,感觉还有点意思,以下为收集信息中碰到的两个需要判定的地方(对java优化没有任何要求的,本文纯属扯淡,呵呵): 原始代码片段1(用于判定是否为静态资源): if(servletPath == null || servletPath.endsWith(".gif") || servletPath.endsWith(".png") || servletPath.endsWith(".jpg") || servletPath.endsWith(".bmp") || servletPath.endsWith(".js") || servletPath.endsWith(".css") || servletPath.endsWith(".ico")) return true; 原始代码片段2(用于判定浏览器类型): if(StringUtils.isEmpty(userAgent)) return null; if(userAgent.contains("MetaSr")) return "metasr"; if(userAgent.contains("Chrome")) return "chrome"; if(userAgent.contains("Firefox")) return "firefox"; if(userAgent.contains("Maxthon")) return "maxthon"; if(userAgent.contains("360SE")) return "360se"; if(userAgent.contains("Safari")) return "safari"; if(userAgent.contains("opera")) return "opera"; if(userAgent.contains("MSIE")) return "ie"; 貌似一眨眼的样子,这个代码没啥优化的余地,代码片段1无非是将出现概率高的放在前面,就尽快判定成功,片段2也是这样,但是片段1里头其实有很多请求都不是静态资源,不是静态资源的话就会将7个endwith全部执行一遍下来了,也就是有很多很多的请求都将遍历所有的内容。 endWith做什么?看看源码,就是将字符串最后那部分截取下来(截取和对比串一样长的,然后和对比串对比),那么7个就会发生7次substring,每个substring操作将会生成一个新的java对象(这个大家应该清楚),每个字符串进行对比是按照字符对比的,所以按照4个四个字符进行计算,也就是会发生20多次对比操作(最坏情况),当然说最好的情况就是进行一次substring,4次对比操作(因为对比成功是每个字符对比成功才算成功)。 那么怎么优化呢,这个貌似除了调试顺序,没有太多优化的空间,根据数据你会发现一个规律,对比的结束字符有5种可能性,那么通过结束字符,就可以定位到一到两个字符串上面,所以我们第一个考虑就是取出要对比的那个字符串的结束符,看下结束符是那个字符串的结束符,然后就在一两个字符串上使用endWith,那么概率就很低了,于是乎,我们对代码片段1,做第一步优化就是: if(servletPath == null) return false; int length = servletPath.length(); char last = servletPath.charAt(length - 1); if((last == 'f' && servletPath.endsWith(".gif")) || (last == 'g' && (servletPath.endsWith(".png") || servletPath.endsWith(".jpg"))) || (last == 'p' && servletPath.endsWith(".bmp")) || (last == 's' && (servletPath.endsWith(".js")|| servletPath.endsWith(".css"))) || (last == 'o' && servletPath.endsWith(".ico"))) return true; 此时判定完首字母后,就使用最多2个字符串进行endWith操作,经过测试,在上面最坏的情况下,效率要高出5-8倍,最初的写法在最好的情况下,和现在速度基本保持一致,但是我们上面说了,很多请求都不是静态资源,所以有很多请求都是走的最坏情况。 好了,如果你的代码不需要极高级别的优化,走到这一步已经够用了,或者说这个已经非常非常够用了,虽然优化的思路非常简单,再写就写成C++了;呵呵,不过我喜欢钻牛角尖,我也有点点洁癖,就是要玩什么,就喜欢玩到我认为的至高境界,所以我又再进行分析,画个图看看: 想办法是否可以去掉subtring的操作,也就是不做字符串截取的操作,我看到就那么几个字母,那么就匹配几个字母而已嘛! 于是乎代码有点像C写的了,做了进一步的改动: if(servletPath == null) return false; int length = servletPath.length(); if(length < 3) return false; char last = servletPath.charAt(length - 1); char second = servletPath.charAt(length - 2); char third = servletPath.charAt(length - 3); char forth; if(length > 3 && third != '.') forth = servletPath.charAt(length - 4); else forth = 0; if((last == 'f' && second == 'i' && third == 'g' && forth == '.') || (last == 'g' && ((second == 'n' && third == 'p' && forth == '.') || (second == 'p' && third == 'j' && forth == '.'))) || (last == 'p' && (second == 'm' && third == 'b' && forth == '.')) || (last == 's' && ((second == 'j' && third == '.')|| (second == 's' && third == 'c' && forth == '.'))) || (last == 'o' && (second == 'c' && third == 'i'))) return true; 改动后的代码,更加像低级语言在写代码,呵呵,不过效率的确提高了一些,这个时候提高就不是太明显了,只是减少substring创创建中间对象生成时间,能到这一步,我想很少有人再去想了,但是但是代码洁癖超级高的话,还会去想,五个字符,最坏情况要匹配5次才能找到自己想要的入口,有没有办法一次性找到,还有,找到后通过路径直接匹配到内容,回到那个图上,看到很像图形结构 首先考虑入口应该如何处理?看到入口全是字母,这些字母的ascii都是数据0-127的,所以我们想直接用字母的ascii作为下标,我们姑且浪费点空间,创建一个长度为128的数组,使用结束字母ascii作为入口位置; 再考虑,进入后该如何算?我们就想通过图上的路由,最终可以找到最后一个字符的就是成立的,此时设计到子节点的查找,理论上这样是最快的,但是,实现起来每一层需要循环到自己要的那个路由,循环体本身对CPU的开销也是有的,而且要构造对象和指针来实现,访问数据需要通过间接访问,理论上的最优化,并不是我们想要的最优,但是我们的优化到此结束了吗?不是,算法行不通,我更喜欢钻牛角尖了,呵呵, 怎么解决第二步无法完成的情况呢?我考虑到虽然不能按照图的结构完成,那么我发现开始字符都是 "."将它抛开算,如果入参不是这样直接错误,另外,剩余的字符,就只有1-2个字符,而且这个字符的ascii都是数据0-127的,我惊喜了,0-127就是在byte的范围内,我可以组织成任何我想要的内容,我此时将两个字符,按照byte拼接,就可以拼接为2个byte位的数字,也就是short int 类型,因为java在JVM内核实现中其实在局部变量中都是一样大的,所以我们就直接用int了,如果int和int匹配就是一个compare,而不是按照每个字符去compare了; 再发现,上面的图结构中,按照路由向下走,每个入口下面最多2个可匹配的内容,所以我们就定死一个结构就是一个入口下两个int,默认为-1,如下(我这里定义的是内部类,写成static是为了静态方法调用): static class TempNode { int c1 = -1; int c2 = -1; } OK,开始尝试,首先我们初始化我们需要进行对比的信息,就像编译时完成一些东西一样: 我们首先初始化一个128长度的数组: private static TempNode[] tempNode1 = new TempNode[128]; private static void addNode(char []chars) { int b = (int)chars[0]; TempNode node = tempNode1[b]; if(node == null) { node = new TempNode(); tempNode1[b] = node; } int size = chars.length; int tmp = 0; if(size == 2) { tmp = (int)(chars[1]); }else if(size == 3) { tmp = (int)(chars[1] << 8 | chars[2]); } if(node.c1 == -1) node.c1 = tmp; else node.c2 = tmp; } static { addNode(new char[] {'f' , 'i' , 'g'}); addNode(new char[] {'g' , 'n' , 'p'}); addNode(new char[] {'g' , 'p' , 'j'}); addNode(new char[] {'p' , 'm' , 'b'}); addNode(new char[] {'s' , 'j'}); addNode(new char[] {'s' , 's' , 'c'}); addNode(new char[] {'o' , 'c' , 'i'}); } 这部分访问这个类的时候,就会被初始化掉,初始化的目的就是为了可以被反复使用,而没有将这部分运算时间抛开掉了;此时怎么运算呢?代码描述如下: 1、取出访问字符最后一个字母,根据ascii到tempNode1取出对象,如果对象为空,则就没有任何匹配的,直接返回错误。 2、倒转访问字符,如果长度超过3个,且第三个字符不是“.”,则看第四个字符是不是“.”。 3、上述成立后,根据字符长度拼接处对应的int数据,与取出的TempNode对象中的两个int值进行匹配,得到boolean类型的值返回。 实际代码如下: private static boolean testServletPath(String servletPath) { int length = servletPath.length(); char last = servletPath.charAt(length - 1); TempNode node = tempNode1[(int)last]; if(node == null) return false; char second = servletPath.charAt(length - 2); char third = servletPath.charAt(length - 3); char forth; int tmp = -1; if(length > 3 && third != '.') { forth = servletPath.charAt(length - 4); if(forth != '.') return false; tmp = (int)(second << 8 | third); }else if(third != '.') { return false; }else { forth = 0; tmp = second; } return tmp == node.c1 || tmp == node.c2; } 我想这已经快到极点了,可以看到上面有进行 ‘.’ 的compare,也就是多进行操作了,那么是否可以直接将这个字符也放入到int中呢,int本身还有2个byte的空位,是的,可以这样做,但是会增加一次位偏移操作,所以和多一次判定,所以总体的效率会看不出多大的区别,但是也是可以那样做的,因为你会发现compare操作会做2次。 也就是只要能挖,在这种代码中能挖出很多宝贝,在高并发的应用系统中,如果在极高并发的代码段,尤其是无论任何程序都会经过的代码段,而且经过多次的那种,那么,这种优化就会产生总体上的提升。 最后,我们看看代码片段2,其实和片段1类似,只不过第一步是考虑endWith,第二个是contains,contarins其实有可能会更加慢,因为会在内部在到相应的字符做一次匹配;就像对比MSIE这个字符串,如果文本中出现多次M开头就又要开始匹配,这里将同一个文本和8种情况对比是很痛苦的事情,其实我们完全可以将上面8个字符串的第一个字母取出来,后面字符串作为byte位,最后组成一个long数据,代表一个数字,放在一个小数组中(此时就是用起始字符作为数组下标了);然后,在使用传入字符串时,每位进行位偏移,将数字和对应下标下的数组看看是否一致,一致就直接返回那个下标记录下的常量,若不是就继续向后了,也就是一遍扫描下来8个字符串可以对比到,而且每次内部对比的时候,就是一个简单的数字对比而已,这就是一种优化。 不论不论怎么说,优化还是归结到能不做的就不做,能少做的就少做,能只做一次就只做一次!
本来有点不太想写这篇文章,原因是写了这个,就感觉WEB应用怎么都可以自己写代码访问内部的资源信息!不过出于技术本身的我还是考虑些点点东西,而且即使我不写,这玩意也有,呵呵,前面一篇文章我提及到双方要约定token来进行认证交互等等,如果你想访问某个网站内部的资源,而且是需要登录的,但是又想通过本地程序直接蹦进去,怎么蹦呢? 办法不是没有,其实httpclient就是模拟一个浏览器的功能,而登录的动作其实就是获取到你的cookie,而httpclient本身有记录cookie的功能,所以这并不难。 也就是说,你要用httpclient来模拟一个网站的登录,然后后续的操作;那么你只需要到那个网站的登录页面中找到用户名和密码的标签的name值,以及其action的目标地址,将其在本地开始进行模拟,标签,并类似于上一篇文章中编写用户名和密码并向action的目标地址发起的post操作(注意这里的POST应该是绝对路径,而不是页面看到的相对路径),此时你就能获取到你的cookie了; 那么登录OK了,cookie获取到了,如何保存cookie呢?其实你根本不用保存,因为httpclient已经帮你保存了,接下来所有的动作,只需要你使用同一个httpclient对象,它们的cookie就是一致的,可以通过同一个httpclient对象发起多次POST或GET请求,这个时候内部想要请求什么就请求什么了。 貌似本文就结束了,要这么简单,我也不会专门抽出来写了,其实上述方法只能适合于一般的普通网站,如果网站的认证方法是https的,也就是你看到它的登录界面是https开头的,那就不行了因为协议不一样了,不过也是有办法的,这也是本文需要阐述的重点,要做这个工作,httpcleint请换成4以上的版本,对应到的包有:commons-httpclient-4.1.3.jar以及httpclient-4.1.2.jar两个包不是一样的内容,在这里这两个包都需要(注意一下代码为非U盾认证方式,U盾认证需要其他的代码来支持)。 加上了这两个包后,在程序开始部分import的内容应该包含: import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import org.apache.http.protocol.HTTP; import org.apache.http.util.EntityUtils; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; 如果你有什么包引入不进来,说明你找的包还不对。 public class HttpsTest1 { /** * SSL部分的处理 * @param httpClient * @throws NoSuchAlgorithmException * @throws KeyManagementException */ private static void securityProcess(DefaultHttpClient httpClient) throws NoSuchAlgorithmException, KeyManagementException { TrustManager easyTrustManager = new X509TrustManager() { public void checkClientTrusted(java.security.cert.X509Certificate[] x509Certificates, String s) throws java.security.cert.CertificateException {} public void checkServerTrusted(java.security.cert.X509Certificate[] x509Certificates, String s) throws java.security.cert.CertificateException {} public java.security.cert.X509Certificate[] getAcceptedIssuers() { return new java.security.cert.X509Certificate[0]; } }; SSLContext sslcontext = SSLContext.getInstance("TLS"); sslcontext.init(null, new TrustManager[]{easyTrustManager}, null); SSLSocketFactory sf = new SSLSocketFactory(sslcontext); Scheme sch = new Scheme("https", 443, sf); httpClient.getConnectionManager().getSchemeRegistry().register(sch); } /** * 处理登录 * @param httpClient * @throws UnsupportedEncodingException * @throws IOException * @throws ClientProtocolException */ private static void login(DefaultHttpClient httpClient) throws UnsupportedEncodingException, IOException,ClientProtocolException { HttpPost httppost = new HttpPost("https://www.xxx.xxx.com/login/");//这是用户名和密码提交的目标路径 List<NameValuePair> params=new ArrayList<NameValuePair>(); params.add(new BasicNameValuePair("id",MyUserInfo.USER_NAME));//写入用户名 params.add(new BasicNameValuePair("pass_word",MyUserInfo.PASSWORD));//写入密码 httppost.setEntity(new UrlEncodedFormEntity(params,HTTP.UTF_8)); HttpResponse response = httpClient.execute(httppost); HttpEntity entity = response.getEntity(); String content = EntityUtils.toString(entity); System.out.println(content); } public static void main(String[] args) throws Exception { DefaultHttpClient httpClient = new DefaultHttpClient(); try { securityProcess(httpClient); login(httpClient); //以下是你要请求其他的URL HttpGet get = new HttpGet("http://www.xxx.xxx.com/xxx/xxx"); HttpResponse response2 = httpClient.execute(get); System.out.println(EntityUtils.toString(response2.getEntity())); }finally { httpClient.getConnectionManager().shutdown(); } } } OK。代码细节上我不想多说,不过这段代码去请求一个https的登录是绝对没有问题的,可以将相应的URL换成自己系统的,再试一试!虽然是这个功能,不过,我个人还是不建议这样做,这样做太张扬了,这里仅仅为简单探讨下,可以用它写点程序实现远程系统的本地自动化处理步骤。 另外验证码需要一些解析程序,相对较为复杂,而且可能解析会有问题,对这类问题我也不想研究太多,呵呵,本文说提及的这个程序也和验证码无关。
本文偏重使用,简单讲述httpclient,其实在网络编程中,基于java的实现几乎都是包装了socket的通信,然后来模拟各种各样的协议;httpclient其实就是模拟浏览器发起想服务器端的请求,而这种更加类似于JS的请求或页面的POST、GET,不过这种数据的返回一般需要得到有意义的数据,才方便做其他的交互,否则得到一个页面结果,全是标签了,毕竟不是浏览器,所以我们用httpclient更多使得系统的交互更加的简单,本文从如何使用httpclient开始说明到性能的优化方法切入: 1、httpclient客户端调用例子,以及服务器端需要做什么。 2、安全性怎么样去控制。 3、httpclient在并发量较高的调用下问题如何去解决。 1、httpclient客户端调用例子,以及服务器端需要做什么。 首先说明服务器端需要做什么,httpclient模拟的是一个浏览器,要服务器端进行数据交互,那么就是和浏览器一样发起请求,接受请求的操作,但是它和浏览器与服务器交互最大的区别是它没有登录动作,当然也可以通过模拟登录来完成cookie的获取,但是这个代码写起来就费劲了,而且这个账号必须在你的代码中写明登录账号才能获取到cookie,如果程序是单独自己用还是可以的,如果很多人用就有点乱了,因为每个人的密码你也得用某种方法传递到服务器端,但是没有cookie很明显会被服务器端拦截到某个直接的登录界面上去,从而得不到自己想要的数据,所以,我们先抛开安全性问题,那么就是将部分URL开放出来,也就是不经过过滤器的URL,或将某些目录单独开放出来访问,安全性的问题,第二章来讨论,OK,如果服务器端开放了一个URL路径后,客户端访问就像浏览器访问一个URL一样简单,用httpclient如何去访问呢? 在使用之前,需要先了解,httpclient是apache提供的,所以需要先引入相关的包,要使用它基本需要几个包: commons-logging、commons-httpclient、commons-codec具体的版本以及引入方式请自己根据项目和工程打包方法决定,目前来讲maven引入是比较方便的方法,然后在代码前面引入: import org.apache.commons.httpclient.*; <顺便说下 .* 这个说法,有人说用这个 * 是很慢的,对于现在的JVM来说只能说它是在乱说,一个一个引入唯一的好处是可以很快知道这个类是那个包下面来的,但是绝对不是提高性能,jvm在编译时早就决定了哪些是需要的,哪些是不需要的,如果引入同一个包太多,即使每个单独写,jvm也会给改成*,JVM的内存结构也不会因为某个类多引入几个*,就会修改他们之间的链接结构,初始化是由父子集成关系以及包装关系决定的,而运行是优化器决定的> 首先来看一个Get请求的非常简单的例子: try { HttpClient client = new HttpClient();//定义client对象 client.getHttpConnectionManager().getParams().setConnectionTimeout(2000);//设置连接超时时间为2秒(连接初始化时间) GetMethod method = new GetMethod("http://www.google.com.hk/");//访问下谷歌的首页 int statusCode = client.executeMethod(method);//状态,一般200为OK状态,其他情况会抛出如404,500,403等错误 if (statusCode != HttpStatus.SC_OK) { System.out.println("远程访问失败。"); } System.out.println(method.getResponseBodyAsString());//输出反馈结果 client.getHttpConnectionManager().closeIdleConnections(1); }catch(....) {.....} 注意,上述反馈结果可能和你用一个socket去模拟一些系统没有什么区别,因为返回的内容没有任何价值,都是页面标签,当你和另一个系统交互时,它做response数据时,可以返回指定的json、xml等格式,用处就非常好用了,下面还会提及到它的好处;注意,采用GET方法,参数放在URL上面,要将非英文字符传递过去,需要对数据进行编码,如: String url = "http://www.xxx.xxx.com/xxx?name=" + URLEncoder.encode("谢宇" , "GBK") + "&otherName=" + URLEncoder.encode("谢宇" , "GBK") ; 其中URLEncoder使用apache或jdk自带的均可。 顺便再写个POST的例子: try { HttpClient client = new HttpClient();//定义client对象 client.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET, "GBK");//指定传送字符集为GBK格式 client.getHttpConnectionManager().getParams().setConnectionTimeout(2000);//设置连接超时时间为2秒(连接初始化时间) PostMethod method = new PostMethod("http://www.xxx.xxx.com/aaa/bbb.do"); method.setRequestBody(new NameValuePair[] { new NameValuePair("name" , "谢宇"), new NameValuePair("otherName" , "谢宇") }); int statusCode = client.executeMethod(method); if (statusCode != HttpStatus.SC_OK) { System.out.println("远程访问失败。"); } System.out.println(method.getResponseBodyAsString());//输出反馈结果 client.getHttpConnectionManager().closeIdleConnections(1); }catch(....) {.....} 可以看出,Post的例子和Get差不多,唯一的区别是传入参数的方法post采用了单独逐个写参数的方法,而get是在URL上面。 如果上面两个例子,在你的机器上跑通了,那么好,我们开始来讨论它的安全性问题: 2、安全性怎么样去控制。 其实接口既然是人定义的,也就是协议是自己控制的,我们基于了一种轻量级的编程模式,让服务器端和客户端编码都变得十分简单,简单了,安全问题来了,谁都可以调用,如果你的接口是十分open的,那么这不是问题,只是控制好并发就可以,但是如果存在安全隐患的话,那么就有问题了,这个需要双方定义好一个加密方法,加密的粒度可以根据实际情况来决定,这种传送最好不要使用简单加密,一般有两种方法: 其一:使用不可逆加密算法(如:MD5,注意md5在对中文数据加密时,采用不同字符集转码加密出来结果也不一样),不可逆加密算法,就必须要双方都约定一个协议,在传递的参数上增加一个加密后的token值,其余的参数照样传递,加密过程为使用某种key与数据本身进行组合,并且存在一些动态变化性,将加密数据作为一个参数传递到接收方,接收方使用相同的方法得到一个密文,两个密文进行对比,若对比一致,则认证成功,若对比不一致,则认证失败;这样做算是比较简单的方法,有些还用了可逆和不可逆同时来用,先将数据按照可逆算法加密,然后再计算token,不过比较复杂些了。 其二:可逆加密算法,但是这种可逆,需要有一个密匙,最好的是非对称密匙,而且最好是双方的密匙可以随着某个值而变化,而不是固定的密匙。非对称密匙比较复杂,如果要用的话,这个可能会比较麻烦,安全级别极高的可以考虑,如果你的密匙本身可以随着某种方式得到一个变化,使用对称也基本够用,如:Blowfish就还算是不错的,但是没有密匙的就别用了,类似Base64就太简单了;当然你也可以像上面说的,可逆和不可逆混用来提高安全级别。 3、httpclient在并发量较高的调用下问题如何去解决 前面有提及到httpclient模拟系统之间的交互,如果系统之间的交互不高,是非常轻松的动作,不过httpclient是作为WEB容器的web请求存在,在http协议下,都是无状态的协议,也就是连接-请求-反馈-断开几个基本动作,好在现在WEB容器有了keep-alive的功能,包括很多负载均衡设备:如:LB、LVS、nginx、apache、jboss、tomcat等等都是支持的,虽然支持,但是看看上面的代码,就发现,每次请求都会重新建立连接,如何让他们不要重复创建连接呢?或者说在服务器端没有断开前不要重复创建连接,一个连接可以被使用多次请求,不至于一次请求就被断开一次;建立一次连接需要三次握手过程,以及更多的网络开销,所以你懂的。 道理很简单,其实和链接数据库差不多,将上面的请求的client对象以及method对象作为共享变量时,发起多次请求,平均效率会提升2倍左右,注意,这里是循环测试,而不是多线程。 但是对于并发较高的,我们不可能将method只用一个,因为它本身不能并发,于是我们就要用多个,在多个共享的对象中,如果控制好征用,有涉及到连接池的问题,不过这个连接池相对数据库的连接池要简单很多,因为,重试等动作,apache已经为你包装好了,你只需要顺序找和分配就可以了,如何降低竞争就是算法和策略的问题了。 但是,让客户端来编写这么一段代码是不是有点过分,当然你愿意写也是可以的,其实apache又为我们提供了一个后面就是异步httpclient(其实这里所知的异步并非真正的异步IO模式),也就是将这部分包装了,对于访问者来说还是同步的,只是在IO层面是非阻塞的了,这个就配合了服务器端的keep-alive,就像服务器端同时向一个站点请求多个资源时,我们希望是一个连接,而不是多个链接,其实在很多浏览器(如chrome、FF)都可以监控到它同时请求的服务器端资源,那么要用httpclient实现异步IO应该如何来做呢?其实也蛮简单的,下面是一个简单例子: 首先你要增加一个关于异步IO需要的包: 1、async-http-client包,可以在这里下载:https://oss.sonatype.org/content/repositories/releases/com/ning/async-http-client/1.6.2/ 2、log4j的包,这个不用我说了,都知道在哪里 3、slf4j-spi 的包,目前用1.5以上的版本比较多。 4、slf4j-log4j 的包,可以看出,slf4j是在log4j基础上包装的。 OK,就这几个了,弄好后再看看下面这段代码,通过使用它,性能可以得到明显改善: AsyncHttpClient client = new AsyncHttpClient(); try { Future<Response> f = client.prepareGet("http://www.google.com.hk/").execute(); System.out.println(f.get().getResponseBody("Big5"));//谷歌的输出编码集为Big5,反向解析结果的时候使用 }catch(...) {....} 这段代码是不是超级简单,可以通过上面描述的三种方式: 1、直接调用 2、将GetMethod或PostMethod对象作为共享对象反复使用。 3、使用AsyncHttpClient 这三种方法,非别使用一次调用、循环多次调用、并发调用来测试性能,后面两者的性能比第一种方法的性能要高很多。
关于java对象的大小测量,网上有很多例子,大多数是申请一个对象后开始做GC,后对比前后的大小,不过这样,虽然说这样测量对象的大小是可行的,不过未必是完全准确的,因为过程中包含对象本身的开销,也许你运气好,正好能碰上,差不多,不过这种测试往往显得十分的笨重,因为要写一堆代码才能测试一点点东西,而且只能在本地测试玩玩,要真正测试实际的系统的对象大小这样可就不行了,本文说说java一些比较偏底层的知识,如何测量对象大小,java其实也是有提供方法的。注意:本文的内容仅仅针对于Hotspot VM,如果你以前不知道jvm的对象大小怎么测量,而又很想知道,跟我一步一步做一遍你就明白了。 首先,我们先写一段大家可能不怎么写或者认为不可能的代码:一个类中,几个类型都是private类型,没有public方法,如何对这些属性进行读写操作,看似不可能哦,为什么,这违背了面向对象的封装,其实在必要的时候,留一道后面可以使得语言的生产力更加强大,对象的序列化不会因为没有public方法就无法保存成功吧,OK,我们简单写段代码开个头,逐步引入到怎么样去测试对象的大小,一下代码非常简单,相信不用我解释什么: import java.lang.reflect.Field; class NodeTest1 { private int a = 13; private int b = 21; } public class Test001 { public static void main(String []args) { NodeTest1 node = new NodeTest1(); Field []fields = NodeTest1.class.getDeclaredFields(); for(Field field : fields) { field.setAccessible(true); try { int i = field.getInt(node); field.setInt(node, i * 2); System.out.println(field.getInt(node)); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } 代码最基本的意思就是:实例化一个NodeTest1这个类的实例,然后取出两个属性,分别乘以2,然后再输出,相信大家会认为这怎么可能,NodeTest1根本没有public方法,代码就在这里,将代码拷贝回去运行下就OK了,OK,现在不说这些了,运行结果为: 26 42 为什么可以取到,是每个属性都留了一道门,主要是为了自己或者外部接入的方便,相信看代码自己仔细的朋友,应该知道门就在:field.setAccessible(true);,代表这个域的访问被打开,好比是一道后门打开了,呵呵,上面的方法如果不设置这个,就直接报错。 看似和对象大小没啥关系,不过这只是抛砖引玉,因为我们首先要拿到对象的属性,才能知道对象的大小,对象如果没有提供public方法我们也要知道它有哪些属性,所以我们后面多半会用到这段类似的代码哦! 对象测量大小的方法关键为java提供的(1.5过后才有):java.lang.instrument.Instrumentation,它提供了丰富的对结构的等各方面的跟踪和对象大小的测量的API(本文只阐述对象大小的测量方法),于是乎我心喜了,不过比较恶心的是它是实例化类:sun.instrument.IntrumentationImpl是sun开头的,这个鬼东西有点不好搞,翻开源码构造方法是private类型,没有任何getInstance的方法,写这个类干嘛?看来这个只能被JVM自己给初始化了,那么怎么将它自己初始化的东西取出来用呢,唯一能想到的就是agent代理,那么我们先抛开代理,首先来写一个简单的对象测量方法: //步骤1(先创建一个用于测试对象大小的处理类): import java.lang.instrument.Instrumentation; public class MySizeOf { private static Instrumentation inst; /** *这个方法必须写,在agent调用时会被启用 */ public static void premain(String agentArgs, Instrumentation instP) { inst = instP; } //用来测量java对象的大小(这里先理解这个大小是正确的,后面再深化) public static long sizeOf(Object o) { if(inst == null) { throw new IllegalStateException("Can not access instrumentation environment.\n" + "Please check if jar file containing SizeOfAgent class is \n" + "specified in the java's \"-javaagent\" command line argument."); } return inst.getObjectSize(o); } } //步骤2:上面我们写好了agent的代码,此时我们要将上面这个类编译后打包为一个jar文件,并且在其包内部的META-INF/MANIFEST.MF文件中增加一行:Premain-Class: MySizeOf代表执行代理的全名,这里的类名称是没有package的,如果你有package,那么就写全名,我们这里假设打包完的jar包名称为agent.jar(打包过程这里简单阐述,就不细说了),OK,继续向下走: //步骤3:编写测试类,测试类中写: public class TestSize { public static void main(String []args) { System.out.println(MySizeOf.sizeOf(new Integer(1))); System.out.println(MySizeOf.sizeOf(new String("a"))); System.out.println(MySizeOf.sizeOf(new char[1])); } } 下一步准备运行,运行前我们准备初步估算下结果是什么,目前我是在32bit模式下运行jvm(注意,不同位数的JVM参数设置不一样,对象大小也不一样大)。 1、首先看Integer对象,在32bit模式下,_class区域占用4byte,_mark区域占用最少4byte,所以最少8byte头部,Integer内部有一个int类型的数据,占4个byte,所以此时为8+4=12,java默认要求按照8byte对象对其,所以对其到16byte,所以我们理论结果第一个应该是16; 2、再看String,长度为1,String对象内部本身有4个非静态属性(静态属性我们不计算空间,因为所有对象都是共享一块空间的),4个非静态属性中,有offset、count、hash为int类型,分别占用4个byte,char value[]为一个指针,指针的大小在bit模式下或64bit开启指针压缩下默认为4byte,所以属性占用了16byte,String本身有8直接头部,所以占用了24byte;其次,一个String包含了子对象char数组,数组对象和普通对象的区别是需要用一个字段来保存数组的长度,所以头部变成12字节,java中一个char采用UTF-16编码,占用2个byte,所以是14byte,对其到16byte,24+16=40byte; 3、第三个在第二个基础上已经分析,就是16byte大小 也就是理论结果是:16、40、16; //步骤3:现在开始运行代码: 运行代码前需要保证classpath把刚才的agent.jar包含进去: D:\>javac TestSize.java D:\>java -javaagent:agent.jar TestSize16 24 16 第一个和第三个结果一致了,不过奇怪了,第二个怎么是24,不是40,怎么和理论结果偏差这么大,再回到理论结果中,有一个24曾经出现过,24是指String而不包含char数组的空间大小,那么这么算还真是对的,可见,java默认提供的方法只能测量对象当前的大小,如果要测量这个对象实际的大小(也就是包含了子对象,那么就需要自己写算法来计算了,最简单的方法就是递归,不过递归一项是我不喜欢用的,无意中在一个地方看到有人用栈写了一个代码写得还不错,自己稍微改了下,就是下面这种了)。 import java.lang.instrument.Instrumentation; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.IdentityHashMap; import java.util.Map; import java.util.Stack; public class MySizeOf { static Instrumentation inst; public static void premain(String agentArgs, Instrumentation instP) { inst = instP; } public static long sizeOf(Object o) { if(inst == null) { throw new IllegalStateException("Can not access instrumentation environment.\n" + "Please check if jar file containing SizeOfAgent class is \n" + "specified in the java's \"-javaagent\" command line argument."); } return inst.getObjectSize(o); } public static long fullSizeOf(Object obj) {//深入检索对象,并计算大小 Map<Object, Object> visited = new IdentityHashMap<Object, Object>(); Stack<Object> stack = new Stack<Object>(); long result = internalSizeOf(obj, stack, visited); while (!stack.isEmpty()) {//通过栈进行遍历 result += internalSizeOf(stack.pop(), stack, visited); } visited.clear(); return result; } //判定哪些是需要跳过的 private static boolean skipObject(Object obj, Map<Object, Object> visited) { if (obj instanceof String) { if (obj == ((String) obj).intern()) { return true; } } return (obj == null) || visited.containsKey(obj); } private static long internalSizeOf(Object obj, Stack<Object> stack, Map<Object, Object> visited) { if (skipObject(obj, visited)) {//跳过常量池对象、跳过已经访问过的对象 return 0; } visited.put(obj, null);//将当前对象放入栈中 long result = 0; result += sizeOf(obj); Class <?>clazz = obj.getClass(); if (clazz.isArray()) {//如果数组 if(clazz.getName().length() != 2) {// skip primitive type array int length = Array.getLength(obj); for (int i = 0; i < length; i++) { stack.add(Array.get(obj, i)); } } return result; } return getNodeSize(clazz , result , obj , stack); } //这个方法获取非数组对象自身的大小,并且可以向父类进行向上搜索 private static long getNodeSize(Class <?>clazz , long result , Object obj , Stack<Object> stack) { while (clazz != null) { Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (!Modifier.isStatic(field.getModifiers())) {//这里抛开静态属性 if (field.getType().isPrimitive()) {//这里抛开基本关键字(因为基本关键字在调用java默认提供的方法就已经计算过了) continue; }else { field.setAccessible(true); try { Object objectToAdd = field.get(obj); if (objectToAdd != null) { stack.add(objectToAdd);//将对象放入栈中,一遍弹出后继续检索 } } catch (IllegalAccessException ex) { assert false; } } } } clazz = clazz.getSuperclass();//找父类class,直到没有父类 } return result; } } OK,通过上面已经可以看出,保持了原有方法,因为深度递归毕竟比较慢,我们有些时候可以选择到底用那一种: 回到步骤重新做一次: 1、编译agent 2、打包class,并修改META-INF/MANIFEST.MF文件中增加一行:Premain-Class: MySizeOf 3、修改测试类: public class TestSize { public static void main(String []args) { System.out.println(MySizeOf.sizeOf(new Integer(1))); System.out.println(MySizeOf.sizeOf(new String("a"))); System.out.println(MySizeOf.fullSizeOf(new String("a"))); System.out.println(MySizeOf.sizeOf(new char[1])); } } 4、设置环境变量开始运行(如果已经设置好了就无需重复设置): D:\>javac TestSize.java D:\>java -javaagent:agent.jar TestSize 16 24 40 16 这个结果是我们想要的了,看来这个测试是靠谱的,面对理论和测试结果,以及上面所谓的对齐方法,大家可以自己编写一些类的对象来测试大小看时候和实际的保持一致; 最后,文章补充一些: 1、对象采用8字节对齐的方式是不论32bit还是64bit都是一样的 2、java在64bit模式下开启指针压缩,比32bit模式下,头部会大4byte(_mark区域变成8byte,_class区域被压缩),如果没有开启指针压缩,头部会大8byte(_mark和_class都会变成8byte),jdk1.6推出参数-XX:+UseCompressedOops,在32G内存一下默认会自动打开这个参数,如下: [xieyu@oracle001 ~]$ java -Xmx31g -XX:+PrintFlagsFinal |grep Compress bool SpecialStringCompress = true {product} bool UseCompressedOops := true {lp64_product} bool UseCompressedStrings = false {product} [xieyu@oracle001 ~]$ java -Xmx32g -XX:+PrintFlagsFinal |grep Compress bool SpecialStringCompress = true {product} bool UseCompressedOops = false {lp64_product} bool UseCompressedStrings = false {product} 简单计算一个,在指针压缩的情况下,一个new String("a");这个对象的空间大小为:12字节头部+4*4 = 28字节对齐到32字节,然后c所指向的char数组头部比普通对象多4个byte来存放长度,12+4+2byte的字符=16,也就是48个byte,其实即使你new String()也会占这么大的空间,因为有对齐,如果字符的长度是8个,那么就是12+4+16=32,也就是有64byte; 如果不开启指针压缩再算算:头部变成16byte + 4*3个int数据 + 8(1个指针) = 36对齐到40byte,对应的char数组的头部变成16+4 + 2 = 22对齐到24byte,40+24=64,也就是只有一个字符或者0个字符都会对齐到64byte,所以,你懂的,参数该怎么调,代码该怎么写,如果长度为8个字符的那么后面部分就会变成16+4+16=36对齐到40byte,40+40=80byte,也就是说,抛开其他的引用空间(比如通过数组或集合类引用),如果你有10来个String,每个大小就装8个字符,就会有1K的大小,你的代码里头有多少?呵呵! 这些不是我说的,这些是一种计算方法,而且这个计算结果只会少不会多,因为代码运行过程中,一些对象的头部会伸展,_mark区域装不下会用外部的空间来存放,所以官方给出的说明也是,最少会占用多少字节,绝对不会说只占用多少字节。 OK,说得挺吓人的,不过写代码还是不要怕,不过就这些而言,只是说明java是如何浪费空间的,不要一味使用一些高级的东西,在必要的时候,考虑性能还是有很大的空间,类似集合类以及多维数组,前面的引用其实和数据一点关系都没有,但是占用的空间比数据本身都要大很多。 本文只是通过一种方式让大家知道如何去测量对象大小,同时知道一个java对象如何开销内存,开销而且很大,所以回过头来说,即使java并不看重性能和空间,不过如果你的代码写得好同样会跑得更加快。
在文章《系统架构-性能篇章2(系统拆分1)》有提及到过关于系统在什么情况下会拆分,拆分的目之类的问题,本文会阐述一些关于拆分过程中遇到的各种各样的常见问题进行分析,和上一个文章中提及到的一样,讲解的目录如下: 1、负载均衡设备的问题。 2、不同系统之间的通信问题。 3、数据写入和查找的问题。 4、跨数据库事务问题。 5、跨数据库序列问题。 6、不同应用的本地缓存问题。 7、系统之间的直接依赖和间接依赖问题。 8、独立模块面临的单点问题。 9、各类批量分组、切换、扩展的问题。 10、统一监控和恢复问题。 进入正题: 一、负载均衡设备的问题: 负载均衡设备就是当系统被拆分为多个节点进行发布后,前端应用系统访问的过程中,还是应当有一个被一个统一认识的整体,也就是有一个统一的入口,而不能让客户端来记住每一个节点的地址来输入;如输入:www.google.com,谷歌的后台就有非常多的服务器来为我们服务,但是我们访问的是一个目录地址。 付:负载均衡设备目前是狭义的一个公司内部的入口或者一套系统的入口,不过很多广义的负载均衡还包含链路的负载均衡(网络层的解析)、同一台机器上进程之间的负载均衡,不过都可以理解为节点之间的负载均衡。 我们在经过网络层解析后,访问到目标地址后,然后再开始找对应的应用系统平台,然后找到可执行对应应用的主机信息,开始进行负载均衡的操作动作;什么是应用自己的平台呢?那就是URL上面的子目录,或主目录本身也有可能。 负载均衡也有硬件负载和软件负载,硬件负载均衡器一般比较贵,受限制于硬件本身,在一定程度上,它较软件负载均衡更加有效,但是在要求更好的负载下并且要求低成本的情况,软件负载又具有了更好的优势,也就是高集成度的东西始终处于中间的那个位置,但是两者都是可以并存的。 目前我们讨论的主要是软件负载均衡,在Weblogic中有一个自带的集群配置策略,通过Proxy进行代理,Admin节点管理多个Managed节点,不过这个负载均衡比较挫,数量稍微多一点就会死掉,这个节点也做得不好,在系统的负载一般的情况下,选择这种方式会比较简单一些,配置的方法也是weblogic提供的傻瓜式下一步就可以了;不过注意的一点,不要在proxy节点和admin节点去发布什么应用啥的,这种节点要是再配置些应用就更费了,qps就更低了。 其次就是我们注明的apache,其广泛应用于很多领域,也是目前全球使用范围最广的负载均衡,而且提供了很多web模块直接编译一些语言(如php模块),一般在这种负载均衡器下,qps可以达到3000以上甚至于更多,不过有些时候要看反馈路径是否经过apache本身以及每次请求数据的大小,在大部分的应用下,使用它已经可以注意支撑起来;很多时候这种代理也称反向代理服务器,apache里头也拥有非常丰富的第三方模块,这方面它甚至于超越了nginx,在安全性方面也较好,技术资料较为健全。 世界级大型反向代理服务器nginx(Engine-X),可支持Http反向代理、负载均衡、FastCGI支持、Rewrite、缓存等等,邮件相关的各种协议,其QPS可以支持3万以上,经过改进后的nginx更加强大;这是一位俄罗斯的著名程序员(Igor Sysoev,自称为一个高级的系统管理员)编写的,目前全球排位第四位,不过它的性能是最强大的,也是高并发的互联网公司借鉴的标准,并且它轻量、开源、稳定、高性能、低CPU开销、低内存开销、缓存压力甚至于抵挡常见的攻击;nginx发展为2001年开始,04年第一个public release版本出来,至今已经有10年历史,其接受第三方模块较少,虽然在第三方的支持上较少,但是其代码非常的干净利落,非常高的代码质量,并且更新很快;另外nginx在热部署上远远超越于apache,不过要在上面做扩展比较困难,技术资料相对较少一点(关于nginx的一些细节,后面有文章单独说明,因为他的确太强大了)。 其次全球还有很多的负载均衡设备,如:IIS(微软支持的)、google等,各自有自己的应用场景需求。 负载均衡主要需要做几件事情: 1、需要根据信息找到目标主机进行负载。 2、多个目标节点,需要均衡的负载。 3、记住统一个session前一次访问的主机,下次还会访问这个机器(看模式,有些是无状态session) 4、某个节点失败后,需要进行相应的切换操作。 5、在指定的配置下,可能会负责反馈结果,不过在高并发应用下,这里可能会涉及到多层负载均衡的情况,最顶层的负载均衡设备一般不干这事。 6、必要时需要在切换时发生session的复制(在有状态session下,需要将session的内容复制到另外的机器上,要求session中保存的内容是可以被序列化的) 7、在增加节点时,高性能的要求是在这种情况下不能出现负载偏向,这种依赖于负载均衡设备的算法,一般我们采用一致性hash可以达到这个目的,不过最初的一致性hash算法还存在很多问题(Hash的KEY通常是一些URL或者参数的内容),并不能真正解决这些问题,后来出现了很多变种的一致性hash算法的确可以很大程度上降低负载偏向的问题,其次:一致性hash并不适合于解决数据层的问题,也就是数据层具有持久性的,用一致性hash并不能解决其动态扩展的问题,虽然一致性hash为了解决这个问题做出了很多算法的变动,不过仍然存在很多版本问题。 二、不同系统之间的通信问题 上一篇文章中提及到了,系统能做到一起,我们做到一起最简单,因为模块之间调用就直接用对象直接就可以引用到,拆分成多个系统就不一样了,系统之间的调用就成了进程和进程之间的通信了; 有关通信就说到早期的socket,这个socket虽然是最古老的技术,不过它也算是目前所有网络技术衍生的基石,网络的交互的不断优化过程就是socket特征不断变化的过程。 说到socket就是编写程序比较麻烦,双方调用和接受都需要编写单独的程序去通信,要求传输的内容都可以被序列化,写的一方有点类似于写文件,读的一方有点类似读文件,不过它也是使用IO本身的一种方法去通信;很多程序员初手在调用完了也会搞忘把socket关掉。 面对java想要把底层封装的,而且尽量减少程序员的错误,所以RMI诞生了,远程方法调用诞生了,RMI是jvm自己封装的一种远程方法调用的协议,中间通过对象序列化来完成,调用方需要有远程的接口,由于RMI本身调用过程中配置比较麻烦,但是又有了这种技术,于是EJB诞生了,EJB在RMI的基础上衍生出标准化的分布式编程模型,不过它都是基于RMI来编写的,主要目的是将业务和VIEW分解开,不过它是物理上将其分开了,在部署和调试程序上相对比较困难,最头痛的就是RMI里头在调用完后会自己做一个System.gc()方法,将会导致Full GC,于此同时衍生出不同厂商不同系统的通信方法也是沿用EJB,是的各个工程里头都有和自己不想管的系统的代码和jar包,Perm区域增加的开销暂时忽略掉,不过系统的移植性和产品化就出现了很多困难(如果在另一个地方要发布同样的系统,但是这个系统中有很多外部调用需要,要么最后工程鱼龙混杂,要么要把这些东西分解出来是很困难的事情,甚至于会报一些诡异的错误)。 后来基于RPC的webservice出来了,它跨语言,因为它传递过程中你可以认为它不是传递对象(后来EJB也把它融进去了),不过我们很多时候还是喜欢用spring来集成它,高版本的webservice可以通过注解完成大部分的工作,不过tomcat发布这个玩意一直不是很好用;另外Spring里头也提供了对于RMI本身的封装的支持,以及spring hessian也是非常不错的交互框架,而且spring是轻量级的,越来越多的人开始选择spring了,因为EJB很多功能它都有,没有的大部分东西也不想要,用了EJB还有各种各样的问题。 最最简单的交互方法,HttpClient,这种是apache组织提供的一种非常轻量级的交互方法,其实也是基于socket写的,因为浏览器本身和服务器交互也是一种socket,只是建立了协议包头和一些短连接机制;所以HttpClient就是使用socket模拟的一个浏览器客户端发起的一次提交操作,可以发起Get和POST的请求,也可以控制参数的传递的字符集等等,传递和结果信息由双方决定;服务方只需要通过正常的response输出数据即可,只是这个时候不是输出一个页面,而是输出一些客户端可以被解释的数据,如json结构或xml结构的字符串信息。 总之,系统一旦拆分,通信是避免不了,从这里也可以看出,并不是系统想怎么拆分就怎么拆分的,要尽量减少相互之间的通信,就需要了解系统,做到系统的低耦合、高内聚,减少外部依赖,不然系统大部分时间就在通信了,没有做其他的事情,不过也不排除有这种情况,那就是有些系统是专门用来做通信的,这种系统可以例外,它处理的核心就是通信处理,做中间转换。其余的业务系统尽量做到减少通信的模块数量。 三、数据写入和查找的问题 关于数据级别被拆分后,尤其是再数据库级别被拆分后,就会面临数据写入和读取的问题,那么在写入的数据就需要能够读取出来,那么这里就需要相同的规则进行读写操作,此时数据库拆分后,我们更多的是将数据库作为一种类似NoSQL的目标机器,或者说是一种存储引擎的分布式部署方法,要实现读写的一致性就要保证一样的规则(当然你说你可以用不一样的规则,除非你中间用了十分复杂的数学算法来做,的确有这种可能,不过考虑到业务数据的准确性,我也一直不敢尝试这些经验,所以本文约定,写入的规则和读出的规则是一致的) 上一篇关于拆分的文章中提及到了数据的拆分方法,这里就不更多的提及了,总之数据的拆分方法就那些,写入和读取就按照这种规则。 在这种设计下,读数据,如果要做表关联成为一种困难的事情,所以它只是解决了一些问题,在并发的系统中,我们在这种情况下更多想将数据库目标作为存储引擎来做,对这类分布式的表读操作基本都是单表,降低数据库的压力,根据读的数据,再去检索其他的信息,相关性的静态数据可以适当用缓存来处理以提高性能。 其次,如果是对于非常大的表,如果还有扩展表,如有一个人类表,每个人类都有很多不同种类的属性,扩展属性都是动态的。所以扩展属性也数不胜数,由于人类这个表本身就很大,扩展属性就更加可怕,要是两个表做关联,结果可想而知,但是我们发现人类很多属性是具有共性的,也就是类型是相同的(如:肤色、学历、婚姻,至少属性名是相同的,是否可以只存储一份?),再考虑,这些属性都是可以被枚举的,或者说可以被数清楚的,即使要增加也不会像业务数据那么多;那么OK,我们就将这种属性和属性值单独存放起来,这里可以将这些数据放在缓存中;而这些扩展属性可以作为原表中的一个大字段来存放,如json结构,为了节约空间可以适当对K和V做一些压缩,这样就在OLTP下要查找数据,适应了扩展性的问题。 而在OLAP中,这样肯定是不行的,因为他们需要其他的维度的数据,所以OLAP更多的是清理和整理计算数据,一般这类海量数据我们是通过一些分布式平台去计算,增量信息也可以,也就是一些关联的结果需要被分析才能被统计以及被搜索;如需要统计具有硕士学历二婚以上的人(每个城市取前面几名),正常的数据库,一个GROUP BY就搞定了,但如果在海量分布式下就没那么容易了,需要通过计算成较为好计算的数据才方便使用;再例如:在搜索中输入一个黑色,那么就应当将相关黑色的内容搜索出来,而不仅仅在主表中存储了黑色的代码(如0代表了黑色);在这类计算中,OLAP就需要计算过滤处理数据了。 OK,这部分暂时就说到这里,关于拆分的方法,上一篇文章中有说明。 四、跨数据库事务问题 从这一章开始,后面都是些细节问题,相对字眼要少点,呵呵。 跨了数据库,最大的困难就是数据库的事务一致性,类似多个库,如果要做大绝对一致性几乎不太可能,除非网络各方面因素非常好,JTA本身提供了多数据源一致性操作,存储过程也可以远程操作其他的数据库(不过跨数据库类型应该还有问题),不过他们或多或少的都存在一些小问题,如果你要用最简单的方法来实现一个一致性事务,可以开辟多个数据源,然后分别用不同的数据源去操作,操作完成后,几个数据源一起做commit,或一起做rollback,虽然说这个未必能完全保证一致性,但是很大程度上是可以保证的,因为数据执行完了,commit和rollback失败的概率很低,除非网络级别和数据库级别发生不可预知的异常。 很多时候我们更加愿意用最终一致性来考虑这种问题,或者最终一致性与绝对一致性结合的方法,如上面的问题,可能我们会在和核心库中写入数据,然后将要写入其他某个库中的数据放入到一个中间件中,这个可能是一个服务器,可能是一个文件,也可能是一张表,总之在写入时,写入到本库结束,就认为成功或失败了,远程的服务,异步同步数据,如果出现什么问题,报警出来由人为处理(这种只要程序没有问题,出现问题的概率非常低),错误后自己需要有一些机制进行重试,总之不会因为某一个远程没有写入或者网络而卡住,导致一些看不懂的错误。 与之结合的方法无非是先去尝试绝对一致性,如果成功当然最好,失败的话,就采用最终一致性来完成,最大程度上保证绝对一致性,各别数据由一个小的后台线程完成异步同步,这样这个小的后台异步程序压力也会降低很多,关于一致性的细节概念还有很多很多,这里就不再屡了,不然越屡越乱。 五、跨数据库序列问题 跨了数据库,序列就有问题,不论是MySQL的索引还是Oracle的序列都会有问题,当然对于MySQL自动增长列,你可以在里头设置增长值来控制多个库之间的相同表不会被交叉,这也算是一种方法;不过如果是Oracle就不是很好办了,因为都是序列,此时这种序列应当建立统一的公共服务,类似于序列,也就是是全局唯一的,那么这个服务体系就是需要一个公共来源,我们先假设是一个数据库的表来存储这些内容。 那么这个公共服务的压力将会非常大,对程序来说也是不可信任的,而由于你为了保证锁,也就是每个时候只有一个请求能拿到序列号,那么不得不做的事情就是做一些update操作或内存直接锁掉,那么这就出现了严重的系统瓶颈,也就是系统访问上升时,这个地方自然就上去了。 OK,那么我们如何来解决这种问题,首先,通过UPDATE table SET NUM=?+100 WHERE NUM = ? AND ID=?;这条SQL语句,在一个瞬间只有一个线程可以被执行成功,也就是多个主机之间只会有一个得到最新的序号,其余的SQL执行返回的影响行数都是0(因为前面哪前一个执行完以后,NUM已经被修改,所以其余的都不会得到修改),只有这个是1,所以它此时得到它的线程会得到当前的NUM和NUM+100的值,那么它自己在这个范围内去循环分配编号,在Java中为了保证一致性可以使用AtomicInteger相关的变量来执行incrementAndGet操作活得最新的数据,保证一致性,注意这里虽然可能会造成序列的浪费,也就是数据库中的序列跳跃,但是数据库本身也是存在这些现象的,对于海量数据,我们不用纠结这些细节。 从上面的理论提出,你应该看到如果请求过大,这里虽然做了100的增长,在并发量极高的时候,也是会出现问题的,为了降低压力,我们想用两个序列,但是又想保证一致性,没办法吗?不是,办法稍微变通下,就是每个序列每次增长200,然后两个序列交叉100偏移量,如:A序列、B序列,应用按照某种规则分别负载到A或者B,A从0开始,B从100开始,A第一次取的时候,A被变成200,但是获取到A的应用只能使用A0-100之间的数据,也就是A当前值 ~ A当前值 + 偏移量;同理B也是这样,两者不重复,但是也可以负载压力,去做操作的时候基本都是行级锁,所以A和B的压力自然被分开了。 序列的问题就先说到这里,有问题再说。 六、不同应用的本地缓存问题 很多系统,为了让自己的系统跑得快一点,就初始化的时候加载一些烂七八糟的数据,首先,这个东西不要乱用,尤其是再java语言中,不要用其他所有语言的缓存思想来理解java的所有,虽然你可能测试效果是要好一些,但是可能你的测试没有出现过什么大GC或者大缓存,如果现场的数据并不适合这样做,要有办法能扯开,否则java的缓存就会成为一个累赘。 同时,这种缓存当跨越到分布式,多个系统之间,当对缓存进行修改时,其他的节点都得不到相应,此时需要定时同步、客户端指定目标去刷新等等方法去做,但是都是很挫的,而且这样做本身就不合适,除非你只是缓存一点点小数据,应用本身也不存在很大的并发量。 我们考虑的是在分布式的环境下应当采用分布式的缓存机制来完成这些工作,分布式缓存,我们非常注明就是MemCached,其实它本身是独立部署的,也就是将缓存部分和应用部分独立开了,由于它操作的独立性,所以在网络层面可以认为是分布式的,不过谈及到真正的分布式缓存还是有很多台机器组成的缓存集群组,这里的分解规则就很像数据库了,不过最大的区别就是数据库全部都是基于持久化的,缓存默认情况下是使用内存,当然也有可以支持持久化的。 此时通信缓存可能会本地基于路由规则去获取,或者直接通过某个中间件统一获取。 七、系统之间的直接依赖和间接依赖问题 系统做多了,依赖关系越来越复杂,相互之间调用越来越看不懂,越来越趋向于网络,不过网络还可以通过单纯的访问来统计,但是系统就很困难了,一个系统动了可能一串系统出现问题,尤其是数据库修改一个表可能导致一片系统不能运行,在这种情况下依赖关系分析非常重要,我们要做修改前发现这是有影响的,或者影响谁的,要统计之间的依赖,没有太好的办法,要么看源码(这个是土办法,适合代码量少,但是系统多了不可能代码量少),系统和系统之间的依赖,可以通过类似agent去跟踪代码之间的调用,包含了上述的各种RMI、RPC、HTTP等调用方法;其次对数据库的调用需要分析SQL或一些日志信息。 OK,上面这个步骤其实看了已经比较有难度了,主要是实施起来有难度,但是在必要情况下也得做,那么更有难度的是间接依赖,最终你可能会递归死在里头,因为一旦间接依赖引发了,这种相互之间的依赖将会无穷无尽,这种信息的分析和搜集必须是准确的或者说是非常精确的,否则不如不做。 这算是在一个数百个系统的相互调用中,必然存在的问题,因为代码每天在变化,会面临各种问题的挑战,变更是必然的,而且是快速的,所以依赖很重要。 八、独立模块面临的单点问题 上面有提及到缓存、统一序列的问题,这些服务虽然是一些小动作,但是当外部请求越来越多的时候,那么就不是一个服务可以搞定的了,那么此时他们也会面临单点问题,一旦他们出现问题,如果调用方没有任何容错处理,那么就会导致大面积的宕机,这是我们不想看到的,在解决模块单点问题上我们有一些常见的手段: 方法1:通过集群负载均衡来解决,一般我不愿意看到这样,因为这样投入的成本会更多,这种服务模式更多是比较集中式的大型服务中心。 方法2:一主一备,服务中的信息,日志之间是通信的,当前主宕机后,备机马上启动服务,备份机器平时可以用来做其他的工作,只需要简单监控目标是否还活着,当负载较高的时候开始。 方法3:管理者模式,也就是类似于企业,有老总、有各种副总、还有部门老大,还有员工;观察者负责去协调资源,进行分配,应用方先从观察者哪里拿到可以到那些机器上去拿数据,那些机器上可以拿什么数据,他们有没有主备关系以及状态,并缓存在本地,应用方去尝试拿他们的数据,当失败后,可以采取两种策略,一种是观察者察觉到一个切换,将配置信息改掉,应用重新获取配置信息或由观察者统一推送给应用方;另一种模式就是由应用方自己去完成主备之间的尝试,除非全部失败,否则下次就用当前尝试成功的哪一个。两者不能说谁好谁坏,各自有自己的场景。 九、各类批量分组、切换、扩展的问题 在应用、数据库、主机很多的时候,我们操作任何动作都会操作类似的动作,所以我们开始想要有批量这些动作,其实和应用系统中批量操作最大的区别就是应用系统基本是做一个update操作,可以通过数据库本身的一致性来完成,但是如果类似这样批量切换和分组落实到实际中,有可能会发生中途失败,就没有失败回滚的那么简单了。 这种分组呢,我们还比较好说,可以通过统一的绑定,将某些目标节点绑定为一个业务层次中,方便做扩展; 切换就是在同一个分组下做批量的切换动作,能不能完全成功不好说,因为策略不同,但是要保证切换的动作和最终的反馈要保持一致,批量只是提供一个平台,不过能不能完全自动化未必能保证,但是需要的是自动化的重试,提醒和容错等处理。 扩展的问题就是当业务发展过程中,主机性能或磁盘容量等已经无法满足需求了,还有就是现在中国式的热点式访问(因为中国喜欢瞎起哄看热闹,所以有啥新鲜事、新鲜人、活动什么的自然就成为),付:还有一类是瞬间高峰,这类解决方案比较特殊,需要很多预热过程,否则什么系统也扛不住,因为各类cache和系统内部各种资源都是临时分配的,而违背了系统本身的局部化原理,要让他局部化需要提前对这个系统提前预热和做相应的特殊处理才可以; 在这些情况下,我们需要扩展了,应用扩展比较方便,更多的压力排到了负载均衡设备上,数据库的扩展就不一样了,涉及到历史数据的迁移,因为扩展后就需要新的数据规则,我们一般在这些层面上,可能是因为热点数据,可能是因为容量,可能是因为压力,他们需要去扩展,如果使用时间点来记录两套规则的切换点,这个倒是一种貌似简单的方法,但是这个实施起来倒是存在很多的问题,而且对于容量不够的情况,始终还是需要迁移数据,所以这个时候需要后台做全量和增量的结合迁移,业务还在运行并写入,全量迁移中,记录增量log,最后通过log开始做增量,如果是完全迁移,最后就将原来数据truncate,如果不是,就看迁移规则是否是分区信息,是分区也可以truncate掉分区,如果不是,就只有用最慢的delete了,很恐怖,系统此时压力可能会上来,做delete的同时由于两遍都已经有了数据,所以就可以将规则进行切换,中间可能有那么很少量的数据会出现问题,不过这些数据几乎瞬间就可以搞定。 关于热点偏向,一般局部化处理它,或者说要么不要因为他影响一大堆的内容,或者不要因为其他的内容影响它,所以我们可以独立他们,偏向处理除了本身的规则外,还需要更多的信息来处理他们,因为独立后,索引可能对他们就没有任何效果了,那个表可能绝大部分内容都是他,要查询更多的业务信息可以通过自定义的业务规范来定义。 10、统一监控和恢复问题 最后,一个大型,好的分布式系统,需要有完善的监控体系,包含对应用系统各种性能指标、数据库的各种性能指标、网络、稳定性,健康状况,等做出实时监控,并且对某些关键性数据进行实时统计运算。 这类监控系统目前还没有什么太好的开源软件,因为这类大型分布式没有多少公司有,有的公司也是通过多年积淀下来的,不会一次性透露出来,因为这里的监控会涉及到多个方面,一般不是一个软件就可以搞定的问题,在大的公司内部,这些软件由于管理的内容非常庞大,而且也要求实时,所以可能会导致本身也会出现性能问题,也可能需要集群来运算,不过这不是关键,关键的是需要用它来做什么。 最后,系统出现问题,怎么恢复,除了应用上的负载均衡,数据库级别的主备切换,还涉及到更多的灾难性恢复,也就是数据丢失或者错误如何恢复的问题,并且是在线恢复,对于灾难性恢复,更多的大家会选择一种跨地区备份的策略,而在线恢复就要更多考验一些经验信息了。 更多的监控粒度是可以根据键控制预知未来的某些信息,根据趋势图预测未来的内容,按照一些数学模型建立起自动化的余量统计和预算工作,其次,就是对问题的发生具有预先判定的能力,也就是根据性能趋势图,提前预知机器可能会发生问题; 对于恢复更高的境界也是如此,当监控自动化的完成预知的时候,那么恢复也希望绝大部分是可以被自动化的,因为主机成千万上万的时候人为很难管理,再说人需要睡觉,所以运维的朋友经常睡不着,但是很多恢复的工作,我们可以抽象出百分之八十以上的经验出来是具有很多共性的,要相信,生在IT行业,要做得更好,就首先要为自己铺路,共性的东西我们基本都是可以通过软件来实现的,因为软件本身就是为了解决重复和简单,这些内容一旦被自动化(注意,这和人工智能还是有很大区别的,人工智能讲究各种模型来模拟人理方法,这里是自动通过计算机来解决我们的简单和重复,让我们做更加有意义的事情),资源的动态调配工作很多由计算机去完成,那么这就逐步迈向我们所谓的云了。 OK,本文先介绍到这里!
这篇文章说难不难,说简单不简单,其实更多的在乎与经验,不过就本文来说,我更多的想阐述为什么会产生乱码,什么情况下会产生乱码,然后如何去解决乱码,对于有哪些乱码情况非常多,并不一定是那一种情况导致的,清楚了过程和原理,那么乱码都不在乎是什么大问题: 本文纲要: 1、乱码的来源与本质。 2、什么时候会产生乱码? 3、如何分析乱码和解决乱码? 4、我所遇到过的乱码情况。 第一部分:乱码的来源与本质: 其实,乱码的来源要追溯到语言文字在计算机中的表达方法了,也就是在计算机中存储和显示过程中,计算机本身并不识别文字,而只是识别数字本身,所以对文字的存储和显示以及转换我们都需要一个编码的过程,在常见的阿拉伯数字和英文字母,以及常见的英文符号,计算机在早期使用了256个字符就足以表达,那个时候也不存在什么乱码的问题;随着计算机的普及,需要适应越来越多的文字,所以,256个字符以已经不能满足大家的需求,所以为了适应更多的文字,出现了各种各样的编码,有些是为了本身的语言,有些是为了国际标准,类似可以表达中文的字符集就有:Unicode、GB2312、GBK、GB18030、UTF-8等等,以GB打头的都是支持基本的字符和中文的,GB2312只能支持六千多个字符,GBK可以支持两万多,GB18030可以支持两万七千多,他们向下兼容,采用2个字节表达中文,而UTF-8是采用3字节表达一个中文。 那么乱码产生的原因就可以简单归结为:在进行数据的编码、解码的过程中,编码和解码的所使用的方法不一样;这个说起来貌似简单但是抽象,我们先定位到这里,然后用下面的内容来充实它(我们通常将数据编码和解码的过程,如果是在程序中是面向对象的,就称之为序列化和反序列化的过程;就像webService也是基于RPC协议,在对象进行序列化和反序列化的时候也可能会出现乱码)。 编码本身也首先由对应的操作系统所能支持的字符集来决定,其实就目前来说,这些字符集几乎所有的操作系统都会支持,所以这个问题不用考虑太多(查看操作系统所能支持的字符集locale -e); 那么操作系统默认会使用一种字符集,所谓默认使用也就是启动进程的时候,或者说某个运行于这个操作系统的进程发生数据交换的过程中,如果没有指定字符集,就会使用默认的字符集。 操作系统本身的字符集可以通过环境变量来控制,对于一个类似于通过终端登录上去的用户,也可以默认指定字符集,就是在用户级别下面设置对应的环境变量即可。 同一个用户下面可以启动不同的进程可以在进程启动时通过export设置对应的默认字符集,这也是可以的; 如果某个进程要去进行某种IO操作,而这个IO操作的来源编码和进程默认字符集不一样,那么就要在读取的时候指定转换的字符集或手工将二进制数据转换为字符格式的数据。 我们再反过来说,也就是当一个程序发生某种交互的过程中,如果当前程序中有指定的字符集,就用指定的,如果没有就会逐层向上去找默认的:从当前进程、父进程、用户、OS逐层向上找;所以它的情况就会变得比较复杂了。 OK,可能看到这里你会更加的晕,那么一般情况下编码和解码同时发生一般只会发生在两个进程之间或者两个时间点上,因为同一个进程内部通信并且发生在同步调用上是没有必要编码的,就像你在同一段程序中,要传递一个String到子方法中是不需要发生编码的一样。 那么我们将乱码产生的原因再细化一层就是:在数据进行了两个进程之间的通信或发生在两个时间点的编码和解码工作,就有可能会发生乱码。 那么两个进程通信是什么意思?发生在两个时间点是什么意思?为什么需要编码和解码? 所谓两个进程通信就是指程序和程序之间的交互,例如:程序之间通过RPC、HTTP、RMI等传递数据,这些通信可能是网络交互,可能就是本机的交互,但是他们始终是进程与进程之间的通信,最简单的例子就是服务器向客户端浏览器通信,客户端的浏览器本身就是一个进程,每种浏览器可能会采用不同的线程、缓存处理方法来与服务器端通信,不过总体上会基于一个国际标准的协议规范。 而发生在两个时间点是指,将数据放在某个中间位置,这个中间位置是需要被编码的,另一个时间点有一个进程去读取这个中间数据(这个进程可能是同一个进程,可能不是);例如:程序将一个带有中文内容的信息保存到一个文件中,然后这个文件可以被用户所下载,下载的时候就需要读取这个文件的内容,读取这个文件的内容可以是另一个进程来完成,也可以是同一个,只要两者的编码和解码方法不一样,就会产生乱码。 第二部分:什么时候会产生乱码? 通过第一部分的阅读,相信大家在对乱码的认识上有一个原理上了解,我们没有必要纠结于原理上的非常细节的细节,最终你只需要知道他们在什么情况下产生乱码了;这样说好像很抽象,我们举一些常见的web应用系统的例子,可能对大家的理解会更加好一点,在一个web请求中,可能会发生以下动作: 1、请求普通的JSP或VM的渲染页面; 首先客户端发起请求,客户端的请求内容将会在提交前被浏览器所编码,这个编码和字符集以及协议有一些编码,而且会形成请求的http头部信息,如果没有手工去做编码的过程,那么浏览器为你的编码一般是浏览器在请求这个资源文件时默认的编码;然后服务器端接受的是一个二进制数据(注意,网络中传输的就是二进制数据,这个二进制数据就是被编码后的数据); 如果你的URL上面有中文的话,要注意了,你可能会遇到时而好用,时而不好用的情况,因为浏览器会自动将这个中文进行编码,至于它怎么编码,就又和浏览器本身和访问有些关系了;如果你需要指定一种编码集的话,需要提前将其编码掉,可以使用java的URLEncoding或者js中提供的一些编码方法。 然后服务器端需要进行反解析,这部分可能会被服务器进程所控制,也可能被应用本身所控制,也有可能会被应用中的某个框架所控制,也可能会被程序本身所控制,但是控制的基本原理都是实用request请求上设置的字符集所决定,也就是request设置这个字符集是告诉request对来源数据应当用什么样的字符集去解析,这部分可能会被框架本身所做,如果没做回到上面一章就是逐步向上找; 服务器处理完请求后,就开始在程序内部进行运行,这个时候可能会去读写取数据库,其实,应用程序和数据库交互是通过jdbc去交互的,jdbc本身是和数据库之间建立一种TCP协议,也是打开一个socket流,传输过程仍然是进程和进程之间的交互,所以在交付前,jdbc需要进行对应的编码工作,而返回时jdbc会进行相应的解码工作,同样数据库端也需要相应的编码工作才能返回数据;数据库内部也有进程和进程之间交互,包括和磁盘之间的交互;在Oracle内部一般是统一字符集,而MySQL会存在很多级别的字符集,所以如果MySQL表级别的字符集和数据库级别的字符集不一样的时候,需要在URL上设置字符集才能好用,jdbc本身的实现过程是由厂商来决定的,它中间可以自己指定字符集或和数据库之间通信拿到字符集都可以完成。 服务器端得到信息后,然后开始渲染数据到页面,这个过程也存在字符集的问题,这个也可能会被框架本身所决定,如果是jsp渲染一般有pageEncoding来决定渲染的字符集(但是这里并不代表浏览器就要用这种字符集来解析),velocity一般是由配置文件告诉框架,当然也可以自己response中将对应的中文字符串通过.getBytes("GBK")来进行编码,当response输出后,剩下就是浏览器的解析了。 浏览器会如果是首次请求这个站点或已经失效,那么会使用:<meta http-equiv="Content-Type" content="text/html; charset=GBK"/>中指定的字符集,否则会去采用默认的某种字符集进行尝试,一般第一次请求这个站点也不会出现什么问题;如果你发现请求一个点偶尔出现乱码,清空缓存就没有了,那么就是浏览器记住了某些东西,这种问题一般是静态资源的编码导致了一些站点记忆并且服务器端没有告诉浏览器应该用什么方法来解析,此时即使在head部分加上上面那段信息也不好用,这个头部只是页面的头部,而并不是真正http交互的头部;所以对于这类情况,需要在服务器端输出的时候指定Content-Type,在JSP中就是通过:<%@ page contentType="text/html;charset=GBK" %>,在java中是通过:response.setContentType("text/html;charset=GBK");这句话是告诉浏览器,这是一个文本类型的html格式数据,请你通过GBK进行方解析。 2、浏览器请求一个静态资源(JS这一类) 其实CSS不太可能有乱码,因为这个里面几乎不会有非英文字符,而图片是二进制数据;JS有可能是用户自己编写,那么也有可能有乱码,请求的过程和上面差不多,差别有两点;其一是没有数据库操作,其二是JS如果没有用户自己处理的话,web容器在处理在调度时发现资源没有处理的servlet,那么就会向容器上层进行抛,每一种容器都会有自己的一个默认的servlet来处理静态资源,一般我们叫他:DefaultServlet,mapping的时候,就是使用 / 关键字来mapping,就是找不到的就走这里(换言之,如果你不想用服务器端默认的DefaultServlet,你可以自己写一个,最简单的写法就是,将文件直接用二进制读出来输出去(用二进制读出来不会进行编码,直接就是文件本身的编码,所以只需要浏览器能知道该怎么解码就可以了,这样性能也会更好),当然如果这个Servlet不是使用二进制处理的,那么它应该就会有一个参数让你设置它的defaltEncoding,这个要看具体的应用服务器的实现了)。 这个servlet一般会用二进制来处理这些静态资源,而且会判定资源的lastModified以判定文件是否需要重新加载,以及如果文件没有被修改过,那么就想客户端输出304状态(浏览器会认为这个文件没有被修改过),如果被修改则直接被装在输出,状态为200,但是静态文件在服务器端一般是不会被修改的。 如果是304状态,浏览器此时也有可能会采用站点以前的某种字符集,这个一般不会出现什么问题,除非你的JS文件本身的字符集发生了变化,其次就可能是浏览器的bug了;如果JS真的字符集有可能会发生变化,而且不想因为浏览器的问题导致乱码,那么js文件上可以增加一个charset告诉浏览器是什么字符集解析<script type="text/javascript" src="abc.js" charset="GBK"></script>注意,这里的字符集和文件本身的字符集关系好就可以;如果你觉得这样还不够帅,那么告诉你一个狠招,那么就是将DefaultServlet重写掉,冲写的时候,在输出文件时就需要设置:response.setContentType("text/javascript; charset=GBK");代表是一个文本类型javascript,头部告诉浏览器使用GBK进行解码。 3、服务器进程之间通信: 其实这种问题上面有说过,就是程序和数据库之间就是类似的例子,不同的程序通信也是这个道理,在java方面,还存在一类特殊的操作就是对象序列化,其实所谓的对象序列化就是在数据结构方面做了一个特殊的标志而是,本身对象是没有序列化的能力的;它最终还是需要传递数据,只是结构和数据按照某种特定的格式传输,也就是说理解对象的序列化同样可以使用上面的说明来理解; 同样和文件的交互就是类似的IO操作,存储在磁盘上肯定是二进制的信息,所以需要在存储前将其编码,在java中如果使用默认的FiltReader和FileWriter,而没有进行编码那么就会像第一章所述采用一些默认的信息;这也是为什么有些人说自己的程序在自己调试程序的时候是好用的,为什么放到服务器上就不好用了,因为服务器上某些默认的环境信息和你的本地不一样,这种情况不仅仅针对于读写文件,在进程的通信各方面都有这种说明;要对文件进行字节转换字符,或字符转换为字节进行磁盘读写,有两种方法保证字符集的一致性,一种方法就是文件的内容提前转换为byte数据,通过InputStream和OutputStream数据来读写;另一种方法就是通过FileInputStream和FileOutputStream,转换为对应的Reader和Writer的时候,需要在参数上设置字符集,如:new InputStreamReader(new FileInputStream(file) , "GBK");至于是否进行Buffer是另外的问题了,这里就是告诉如何进行编码和解码的设置过程,同理在对象序列化的时候也是通过这类似的方法来进行包装。 第三部部分:如何分析乱码和解决乱码? 通过上面两章的说明,应该知道乱码的原因和常见的乱码情况,那么如何分析乱码呢,我想你无论是那一种乱码也逃不掉我们在第二部分所讨论的乱码大的种类,但是小的种类应当如何去定位呢,这个一个是逐步细化,一个是经验积累,有些乱码问题及时知道是怎么回事也未必知道如何去解决,尤其是面对一些客户端浏览器本身的问题,所以解决方法也是很重要的。 分析乱码你看到了,中间只要有发生任何两个进程通信或时间点差异的都有可能会发生乱码,最基本的你要学会断点跟踪,一般这类问题是首先要跟踪,也就是在那两个进程之间交互的时候发生了乱码,也许是一个小小的细节,也许是连锁反应,也许是蝴蝶效应,看具体情况而定,按照上面每种情况再加上应用场景中框架本身的情况,那么乱码的种类我们估计可以数出上千种、上万种,经验固然重要,不过如果头两次遇到乱码解决了总结经验是一种成长,遇到数十次还是没有理解为什么是乱码,那么永永远远都会出现乱码,而且可能在偶然的时候出现自己可能理解不了的乱码。 所以分析乱码刚开始只能靠自己碰到或者自己去模拟,强制将一些字符集设置得不一样;而有一些乱码遇到的经验,就可以解决一些,这种就是凭借经验了,但是遇到新的情况就又要慢慢去“猜”了;如果要成为乱码解决的高手,就要理解原理,而不是猜,乱码的情况多种多样,科学的理解乱码,原理是为了理解乱码出现的原因(其实就两点:不同的进程、不同的时间点),跟踪是为了定位乱码出现在哪里,认识本质一般就能定位到对应应用场景为什么会出现乱码,根据应用场景去做对应的调整就可以解决本质性的问题(所谓对应的场景,就是这种乱码出现的抽象粒度,在对应的抽象粒度去修改而不影响其他的代码;其次,如果是框架、服务器本身所提供,那么就从框架服务器本身所需要的配置信息上去解决即可,第二点也可以归结为抽象粒度,只是较高级别的抽象粒度)。 OK,这这一段貌似看起来像是废话,因为什么也没说,也没说怎么定位乱码,没说怎么解决乱码;但是我并不这么认为,因为乱码本身的定位就是场景所决定,这里宗旨是首先学会去理解原理,然后跟踪定位,通过本质认识原因和抽象级别,进而和对应场景结合来解决,就场景而言千变万化,没有说谁就是正确的,关键的是你需要有整个对应场景处理过程的理解,以及清晰冷静的跟踪到问题的点上来,进而通过原理去分析这个过程;乱码的分析和解决,切忌之处为妄下结论,过于依赖于经验本身,甚至于认为是java本身的问题,这样很难解决问题甚至自己不可能解决问题;下面第四部分给出一些常见的场景应用场景: 第四部分:我所遇到过的乱码情况。 我所遇到的乱码情况也不能一一列举,只能说在两年前自己通过一些较为底层的接触,了解了乱码原因后,后来解决乱码问题已经不是什么太困难的事情,直到前端时间我遇到了和浏览器本身的缓存所导致的显示乱码,才将我折腾了一翻,不过这也算是一种场景下的表现,在对应的服务器、对应的框架、对应的代码、对应的浏览器下偶然的发生了,让人难以捉摸,不过还好,最终将偶然变成必然进行了模拟,更加了解到交互的细节,解决了这个问题,这个问题也是两年多来遇到最难搞定的乱码问题了。 1、在请求一个资源时,URL上带上中文,没有编码,导致了乱码:这种不推荐,但是非要用,就用JS对URL进行Encode操作,同理,两个程序之间通过Http进行交互,如果需要携带中文也通过这种方法编码,否则浏览器为你编码,编出来是什么就不好说了,不同的浏览器会采用不同一些默认值和缓存手段来处理。 2、请求服务器端一个资源,数据中包含有中文,中文在客户端使用了utf-8编码规则,服务器端接受的是乱码,通过上面第二章的分析,我们可以看到一个请求到服务器上是请求一个进程,这个进程会交给对应的应用下的程序去处理,应用下有可能有框架去处理,框架最终交给业务代码去处理,也就是中间任何一个部分都可以进行解码操作,如果发生的转码行为不一样或者发生了两次不一样的转码就会出现乱码(发生两次不同的编码就有可能会产生不好解决的问题),如在很多tomcat中默认的请求是按照IOS8859-1来处理的,struts你可以通过设置struts.i18n.encoding来控制它的编码,但是不论在哪里设置,最终是在讲byte转换为char的过程中进行不同的处理,如果服务器和框架本身没有对它进行处理,那么你可以使用一个:request.setCharacterEncoding()来处理,但是这个一定要和提交请求的编码一致,这个代码如果过滤器后面没有其他的处理,就可以放在过滤器中保证这个应用下面都是用这种字符集来接受请求的参数; 如果转码的和客户端发送的编码一致,但是程序处理需要另一种编码,那么就你就需要先按照对应编码转换为byte[]数组,然后再然后自己需要的编码进行解析出来,这个过程就是new String(a.getByte("IOS-8859-1"),"UTF-8");这样就OK了。 在weblogic服务器上可能会需要设置(在web.xml中增加,这个为全局参数,这个参数会被weblogic服务器在启动对应的应用时读取并使用): <context-param> <param-name>weblogic.httpd.inputCharset./*</param-name> <param-value>GBK</param-value> </context-param>补充一点:这类请求也同时可以解决你在通过了一个servlet后forward到下一个jsp页面使用request.getParameter的出现乱码的问题,因为request对象都是一个。 3、页面在输出时本身为乱码,如果是velocity的配置就要看看在配置velocity中的参数:output.encoding是否正确;如果是JSP那么就要看看页面头部指定的:pageEncoding="UTF-8",或者contentType="text/html;charset=UTF-8",默认会使用后者,前者主要是在输出时使用,但是没有的话后者可以起作用,而前者不会去设置输出的头部,也就是浏览器获取到头部信息,而contentType正好是浏览器的头部信息,如: 指定了contentType这里就会变成对应的头部,或者设置Header时,将Key设置为:Content-Type即可;不设置这个,并不代表浏览器解析出来就是乱码,要看情况,但是设置了这个基本不太可能是乱码,因为即使本地有缓存,头部信息浏览器都会去请求服务器端的,注意这个头部并不是客户端浏览器在<head></head>内部设置的,这个设置在客户端某些浏览器有一些站点缓存的时候,不会被生效,所以最终极的方法还是在头部去设置。 当然页面输出乱码有可能本身某个步骤读取出来的数据就是乱码,而并不一定是渲染的时候,那么这个部分就需要跟踪代码了,比如数据库中本身存的就是乱码、或者读取数据库的时候变成了乱码、获取读取某个文件的时候出现的问题、或者和其他进程中交互中出现的乱码,其原理一致 ,框架能控制的在框架控制,框架不方便控制的就用代码控制。 如在weblogic中,你可以在weblogic.xml中配置一段: <jsp-descriptor> <jsp-param> <param-name>compilerSupportsEncoding</param-name> <param-value>true</param-value> </jsp-param> <jsp-param> <param-name>encoding</param-name> <param-value>GBK</param-value> </jsp-param> </jsp-descriptor> 来说明你的就jsp要使用什么样的一个编码集来进行编译(jsp渲染的时候是首先要将jsp内部本身转换为servlet,然后在输出的)。 4、客户端浏览器本身的头部问题,上面已经说明,通过contentType的设置即可解决问题,这类问题一般是偶尔出现,而且是因为站点引用资源多种编码集导致的缓存在浏览器中的一些内容,而服务器端没有返回contentType的时候就会发生。 5、客户端JS或CSS的静态资源乱码,其实上面也是说过了,就是DefaultServlet如果提供了字符集的选择就将其设置对应的字符集,如果没有,那么它一般是采用二进制处理的话传输过程中不会出现问题,客户端接收的时候,在js上设置一个charset,即:<script type="text/javascript" src="abc.js" charset="GBK"></script>,如果这样设置显得过于麻烦,或者在某些部分很恶心的浏览器上不见效,那么就将DefaultServelt进行重写,在输出内容时候,设置response.setContentType("text/javascript; charset=GBK");至于静态资源如何去做缓存,或是否做缓存,是业务上的问题,默认静态资源会做一定程度的缓存,这个具体要看WEB服务器的默认指标和服务器设置。 6、读写文件时乱码,如果读写文件不需要识别内部的数据,进而作一些处理,那么就不需要转换为字符,直接使用字节流进行处理即可,这样最快速,减少两个步骤的转码过程;如果是需要处理内容,那么在Reader和Writer部分,设置字符集,如:new InputStreamReader(new FileInputStream(file) , "GBK");如果你不设置,而直接用FileReader和FileWriter将会采用某种父进程的默认字符集。 7、和远程的程序进行简单socket交互时,同上不需要任何处理数据转换,那么用二进制处理最稳定、最快速; 8、和远程的程序进行类似RPC协议,也就是传递的都是具有数据类型的信息,序列化的过程需要交给框架,此时需要在框架内部指定序列化和反序列化的字符集,或者在程序中指定,否则使用默认。 9、URL上本身有中文,那么就需要提前编码,可以使用java中的URLEncoding提前编码好渲染到前端,也可以使用JS提供Encoding方法来完成。 其实还有很多很多乱码的场景,上面说的应该不是8种乱码,而是8大类,其实也算是最初提出3大类的细化版本,还可以继续细化,总之如果你真想出现了乱码,可以让他千变万化,要让它不出现乱码关键是编码和解码的方法要一致,现今的乱码不能说用一种公共的方法来解决,因为标准和个性化是并存的,到目前为止不能谁将谁干掉;但是可以用一种较为科学的流程和方法来处理;每种应用场景都有可能遇到它所存在的乱码的可能性,关键是要了解其实质上在哪里发生的,哪里发生了一个错误的转换。
对于字符串部分,小胖在《Java特种兵》一书穿插了不少讲解,会讲得更加透彻一些,本文是小胖几年前写的,当初还在初窥门径阶段,很多结论的总结仅用于简单参考: 本文非常简单,不过有很多朋友经常问,网上很多例子也写个大概,很多人也只是知道和大概,就本文而来读起来非常的轻松,不过算是一些小技巧;但是我们的程序中相信用得最多的就是char数组和byte[]数组,而String就是由char[]数组组成的,一般情况下我们就可以认为String用得是最多的对象之一。 有关Sring的空间利用率方面,这里不想多说,只能说很低很低,尤其是你定义的String长度很短的时候,简直利用率不好说;在前序的一篇文章中说明了关于java的对象空间申请方法以及对象在JVM内部如何做对其的过程,就能较为明确的知道一个String是多么的浪费空间;本文就不在多提及这方面的问题了。 再谈及到String与StringBuffer和StringBuilder的区别时,前面一篇文章中将他们循环做了一系列的性能对比,发现StringBuilder性能最高,大家都知道用StringBuilder来用了,但是要明白细节才是最好的;简单来讲String是不可变的字符串,而StringBuffer和StringBuilder是可变的字符串对象,而StringBuffer是在进行内容修改时(即char数组修改)会进行线程同步操作,在同步过程中存在征用加锁和访问对象的过程,开销较大,在方法内定义的局部变量中没有必要同步,因为就是当前线程使用,所以StringBuilder为一个非同步的可变字符串对象。 OK,我们介绍了基本的概念,可以回到正题了;那么String到底是一个神马东西,通过前面的对象结构来看,首先根据String内部的定义,应该有以下内容:一个char数组指针指向一个数组对象(数组对象也是一个对象,和普通对象最大的区别需要一个位置来记录数组的长度)、offset、count、hash、serialVersionUID(这个不用计算在对象的大小中,因为在JVM启动时就会被装入到方法区中)。其次,还有对象对其的过程,而String的内容为char数组引用,指向的数组对象的内部的内容,也就是一个String相当于就包含了两个对象,两个对象都有头部,以及对其方式,数组头部会多一个保存数组长度的区域,头部还会存储对象加锁状态、唯一标识、方法区指针、GC中的Mark标志等等相应的内容,如果头部存储空间不够就会在外部开辟一个空间来存储,内部用一个指针指向那块空间;另外对象会按照8byte对其方法进行对其,即对象大小不是8byte的倍数,将会填充,方便寻址。 String经常说是不可变的字符串,但是我个人并不习惯将他说成是常量,而很多人也对String字符串不可变以及StringBuilder可变有着很多疑惑之处,String可以做+,为什么说它不可变呢?String的+到底做了什么?有人说String还有一些内容可能会放在常量池,这是什么东西?常量池和常量池的字符串拼接结果是什么(我曾在网上看到有人写常量池中字符串和常量池中字符串拼接结果还在常量池,其实未必,后面我们用事实来说话)? 当你对上述问题了如指掌,String你基本了解得有点通透了;OK,在解释这个问题之前,我们先说明一个在Hotspot自从分代JVM产生后到目前为止(G1还没有正式出来之前)不变的道理就是,当你在程序中只要使用了new关键字或者通过任何反射机制实例化的任何对象都将首先放在堆当中,当然一般情况下首先是放在Eden空间中(在一些细节的版本中会有一些区别,如启动了TABL、或对象超过指定大小直接进入Old或对象连Eden也放不下也会直接进入Old);这是不用说的事实,总之目前我们只要知道它肯定是在堆当中的就可以了。 我们先来看一段非常非常简单的代码如下所示: public class StringTest { public static void main(String[] args) { String a = "abc"; String b = "def"; String c = a + b; String d = "abc" + "def"; String e = new String("abc"); System.out.println(a == e); System.out.println(a.equals(e)); System.out.println(a == "abc"); System.out.println(a == e.intern()); System.out.println(c == "abcdef"); System.out.println(d == "abcdef"); } } 请在没有在java上运行前猜猜结果是多少,然后再看结果。 结果如下: falsetruetruetruefalsetrue 如果你的结果不是猜得,而是直接自己通过理解得到的,后面的文章你就不用看了,对你来说应该没有多大意义,如果你某一个结果说得不对,或者是自己瞎猜出来的,OK,后文可能会对你的理解造成一些影响。 我们首先解释前面4个结果,再解释最后2个结果;前4个其实在前面的文章中已经说过他们的区别,不过为了方便文本继续向下说明,这里再说明一次,首先String a = "abc"这样的申请,会将对象放入常量池中,也就是放在Perm Geration中的,而String e = new String("abc")这个对象是放在Eden空间的,所以当使用a == e发生地址对比,两者肯定结果是不一样的;而当发生a == "abc"两个地址是一样的,都是指向常量池的对应对象的首地址;而equals是对比值不用多说,肯定是一样的;a == e.intern()为什么也是true呢,就是当intern()这个方法发生时,它会在常量池中寻找和e这个字符串等值的字符串(匹配的方法为equals),如果没有发现则在常量池申请一个一样的字符串对象,并将对象首地址范围,如果发现了则直接范围首地址;而a是常量池中的对象,所以e在常量池中就能找到的地址就是a的首地址;关于这个问题就不多阐述了,也有相关的很多说明,下面说下后面两个结果;算是较为神奇的结果,也是另很多人纳闷的结果,不过不用着急,说完后就很简单了。 后面两个结果一个是a指向常量池的“abc”,b指向常量池中的“def”,c是通过a和b相加,两个都是常量池对象;而d是直接等价于“abc”+“def”按照道理说,两个也是常量池对象,为什么两个对象和常量池的“abcdef”比较的结果不一样呢?(关于他们为什么是在常量池就不多说了,上面那一段已经有结果了);我们不管怎么样,首先秒杀掉一句话就是:常量池的String+常量池String结果还在常量池,这句话是不正确的,或者你的测试用例正好是后者,那么你中招了,很多事情只是通过测试也未必能得出非常有效的结果,但是较为全面的测试会让我们得出更多的结论,看看我们两种几乎一摸一样的测试,但是结果竟然是不一样的;简单说结果是前者的对象结果不是在常量池中(记住,常量池中同一个字符串肯定是唯一的),后者的结果肯定在常量池;为什么,不是我说的,是Hotspot VM告诉我的,我们做一个简单的小实验,就知道是为什么了,首先将代码修改成这样: public class StringTest { public static void main(String[] args) { String a = "abc"; String b = "def"; String c = a + b; } } 我们看看编译完成后它是个什么样子: C:\>javac StringTest.javaC:\>javap -verbose StringTest Compiled from "StringTest.java" public class StringTest extends java.lang.Object SourceFile: "StringTest.java" minor version: 0 major version: 50 Constant pool: const #1 = Method #9.#18; // java/lang/Object."<init>":()V const #2 = String #19; // abc const #3 = String #20; // def const #4 = class #21; // java/lang/StringBuilder const #5 = Method #4.#18; // java/lang/StringBuilder."<init>":()V const #6 = Method #4.#22; // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; const #7 = Method #4.#23; // java/lang/StringBuilder.toString:()Ljava/lang/String; const #8 = class #24; // StringTest const #9 = class #25; // java/lang/Object const #10 = Asciz <init>; const #11 = Asciz ()V; const #12 = Asciz Code; const #13 = Asciz LineNumberTable; const #14 = Asciz main; const #15 = Asciz ([Ljava/lang/String;)V; const #16 = Asciz SourceFile; const #17 = Asciz StringTest.java; const #18 = NameAndType #10:#11;// "<init>":()V const #19 = Asciz abc; const #20 = Asciz def; const #21 = Asciz java/lang/StringBuilder; const #22 = NameAndType #26:#27;// append:(Ljava/lang/String;)Ljava/lang/StringBuilder; const #23 = NameAndType #28:#29;// toString:()Ljava/lang/String; const #24 = Asciz StringTest; const #25 = Asciz java/lang/Object; const #26 = Asciz append; const #27 = Asciz (Ljava/lang/String;)Ljava/lang/StringBuilder;; const #28 = Asciz toString; const #29 = Asciz ()Ljava/lang/String;; { public StringTest(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 2: 0 public static void main(java.lang.String[]); Code: Stack=2, Locals=4, Args_size=1 0: ldc #2; //String abc 2: astore_1 3: ldc #3; //String def 5: astore_2 6: new #4; //class java/lang/StringBuilder 9: dup 10: invokespecial #5; //Method java/lang/StringBuilder."<init>":()V 13: aload_1 14: invokevirtual #6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: aload_2 18: invokevirtual #6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #7; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: astore_3 25: return LineNumberTable: line 7: 0 line 8: 3 line 10: 6 line 13: 25 } 说明(这里不解释关于栈的计算指令,只说明大概意思):首先看到使用了一个指针指向一个常量池中的对象内容为“abc”,而另一个指针指向“def”,此时通过new申请了一个StringBuilder(jdk 1.5以前是StringBuffer),然后调用这个StringBuilder的初始化方法;然后分别做了两次append操作,然后最后做一个toString()操作;可见String的+在编译后会被编译为StringBuilder来运行(关于为什么性能还是比StringBuilder慢那么多,文章后面来说明),我们知道这里做了一个new StringBuilder的操作,并且做了一个toString的操作,前面我们已经明确说明,凡是new出来的对象绝对不会放在常量池中;toString会发生一次内容拷贝,但是也不会在常量池中,所以在这里常量池String+常量池String放在了堆中;而下面这个后面那种情况呢,我们也用同样的方式来看看结果是什么,代码更简单了: public class StringTest { public static void main(String[] args) { String d = "abc" + "def"; } } 看下结果: C:\>javac StringTest.javaC:\>javap -verbose StringTestCompiled from "StringTest.java"public class StringTest extends java.lang.Object SourceFile: "StringTest.java" minor version: 0 major version: 50 Constant pool:const #1 = Method #4.#13; // java/lang/Object."<init>":()Vconst #2 = String #14; // abcdefconst #3 = class #15; // StringTestconst #4 = class #16; // java/lang/Objectconst #5 = Asciz <init>;const #6 = Asciz ()V;const #7 = Asciz Code;const #8 = Asciz LineNumberTable;const #9 = Asciz main;const #10 = Asciz ([Ljava/lang/String;)V;const #11 = Asciz SourceFile;const #12 = Asciz StringTest.java;const #13 = NameAndType #5:#6;// "<init>":()Vconst #14 = Asciz abcdef;const #15 = Asciz StringTest;const #16 = Asciz java/lang/Object;{public StringTest(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 2: 0public static void main(java.lang.String[]); Code: Stack=1, Locals=2, Args_size=1 0: ldc #2; //String abcdef 2: astore_1 3: return LineNumberTable: line 11: 0 line 13: 3} 这下看下可能有人一下通透了,可能有人觉得更加模糊了,怎么编译完后比前面那个少那么多,是的,就是少那么多,因为当发生“abc” + “def”在同一行发生时,JVM在编译时就认为这个加号是没有用处的,编译的时候就直接变成成 String d = "abcdef"; 同理如果出现:String a = "a" + 1,编译时候就会变成:String a = "a1"; 再例如: final String a = "a"; final String b = "ab"; String c = a + b; 在编译时候,c部分会被编译为:String c = "aab";但是如果a或b有任意一个不是final的,都会new一个新的对象出来;其次再补充下,如果a和b,是某个方法返回回来的,不论方法中是final类型的还是常量什么的,都不会被在编译时将数据编译到常量池,因为编译器并不会跟踪到方法体里面去看你做了什么,其次只要是变量就是可变的,即使你认为你看到的代码是不可变的,但是运行时是可以被切入的。 就是这么简单,运行时自然直接就在常量池中是一个对象了,而不需要每次访问到这里做一个加法操作,有引用的时候,JVM不确定你要拿引用去做什么,所以它并不会直接将你的字符串进行编译时的合并(其实在某些情况下JVM可以适当考虑合并,但是JVM可能是考虑到编译时优化的算法复杂性,所以这些优化可能会放在运行时的JIT来完成,但JIT优化这部分java代码是有一些前提条件的) 所以并不是常量池String+常量池String结果还在常量池,而是编译时JVM就认为他们没有必要做,直接合并了,就像JVM做if(true)和if(false)的优化一样的道理,而前者如果是引用给出来的常量池对象,JVM在拼接过程中是通过申请StringBuilder来完成的,也就是它的结果就像普通对象一样放在堆当中的。 好了,反过来一切都很明了了,String为什么不可变,因为+操作是新申请了对象;+到底做了什么,是申请了一个StringBuilder来做append操作,然后再toString成一个新的对象;如果不是new出来的字符串或者是通过.intern()得到的字符串,则是常量池中的对象;常量池中的字符串和常量池中的字符串拼接,他们的结果不一定还在常量池,如果还在常量池只有一种可能性就是编译时就合并了,因为运行时new出来的StringBuilder是不可能放在常量池中的,我们绝大部分字符串拼接都是有引用的,而不是直接两个常量串来做的。 下面回顾最后一个问题就是,既然String拼接是通过StringBuilder来完成的,那么为什么String的+和StringBuilder会有那么大的差距呢?这是一个值得考虑的问题,如果String的+操作和StringBuilder是一样的操作,那么我们的StringBuilder就没有多大存在的必要了,因为apend太多字符串是一件非常恶心的事情。 首先你会发现,如果在同一条代码中(不一定是同一行代码,因为java代码可以相互包装嵌套,指对于成来讲基本的一条代码), 如String a = a + b + c;这条代码算是同一行,而System.out.println(a + b + c + String.format(d , "[%s]"));对于d就会单独处理后,再和a + b+ c处理,然后再调用System中的静态成员out对象中的println方法; 回到正题,对于同一条代码中,如果发生这种加法操作(不是编译时合并的),那么你在通过javap命令分析时会发现,他们的结果回将其申请一个StringBuilder然后进行append,不论多少个字符串都会append,然后最后toString()操作,这就纳闷了,为什么性能差距会那么大(在循环次数越多的时候差距会越来越大),最终没办法,我们用多行和循环测试,又看了下两者之间的区别,在使用String做+操作时,如果是多条代码或者在循环中做的话,每条代码都会做一个新的new StringBuilder,然后最后会toString一下,也就是当两个字符串相加时,会“最少”多申请一个StringBuilder然后再转换为一个String(虽然是将StringBuilder中内容拷贝到一个新的String中,但是空间是两块),所以浪费空间比较快,而且如果字符串越长,循环的过程中就会逐步进入old,而且old中的东西也会越来越多,导致了疯狂的GC,最后会疯狂的Full GC,再多的内存也会很快达到Full GC,只要你做循环;其实在常规应用中,一般你只需要做几行的字符串叠加也无所谓,如果能写成一行就写成一行,如果非要写成多行还想要性能的话,就用StringBuilder吧;其实快并不是在多少申请了对象,因为java申请对象的速度非常快速,不存在说因为多申请了两个对象就会导致什么大的问题,大的问题是因为这些临时空间所产生的垃圾,最终导致了疯狂的GC,上述两种情况在做多次循环的过程中本地使用代码:-XX:+PrintGCDetails来运行,你会发现,使用String做加法,刚开始会疯狂的YGC,过一段后会疯狂的FullGC,最后内存溢出,而使用StringBuilder几乎不会做GC,要做应该是做YGC,如果发生FGC一般说明这个字符串已经快把OLD区域撑满了,也就说马上要内存溢出了,而前者临时对象也应该去掉的,但是它会比StringBuilder叠加次数更少的时候,发生内存溢出,那是因为对象比较大的时候,临时对象已经在old区域,而前一个临时对象正好是要作为后一个对象的拷贝,所以在后面那个对象还没有拷贝成功前,前面那个对象的空间还不能被释放,那么很明显,old区域的利用率一般到一半的时候就溢出了。 最后补充一个话题,其实StringBuilder也有一些问题,就是在动态扩容的过程中,每次增加2倍的空间,并不是在原有空间上做类似的C语言的realloc操作,而是新申请一个2倍大小的空间,将这些内容再拷贝过去;StringBuilder之所以可以动态增加是因为一个预先分配的char长度,如果没有满可以继续在后面添加内容,如果满了就申请一个2倍的空间,然后将前面的拷贝过去;不难说出两个问题,所谓的动态扩容只是逻辑上的实现,而并非真正的动态扩容,这也有它的内存安全性考虑,而String是多长,数组的长度就多长(注意:这个长度和前面说的对象大小关系并不大,对象大小前面有一定的介绍);另一个可以看出的问题就是动态扩容的过程中同样会产生各种各样的垃圾对象,其实在循环的过程中,看得往往还没有那么明显,在多线程访问多个随机方法,每个随机方法内部都会去做一些apend,而且都大于10的时候,临时对象就多了;不过还好,它的临时对象只是char数组,而不是String对象,前面说了,String对象相当于两个对象,前面那个对象的大小也是很大的;但是如果你需要考虑这样的细节,那么请在编写StringBuilder的时候,预先写好你认为它可能的最大长度,尤其是被反复调用的代码,如StringBuilder builder = new StringBuilder(2048);一般的小对象没有必要这样做,而且一次申请对象如果过大可能很容易进入old区域,甚至于直接进入old区域,这是我们不想看到的;但是这种方法就要求每一位程序员都要有非常高的素质和修养,但是大多数的程序员你可能叫他写StringBuilder就够意思了,呵呵,更加不要说叫他去些意思了,那么这个办法并不能让所有的程序员所接受,目前的Hotspot还未解决这个问题,但是JRockit已经有一种解决方案了,它的解决方案很好的一种方法,就是在编译时它就能决定在这个局部方法内部你会发生多少次的append操作,那么它的StringBuilder内部做的就不是char数组,而是一个String[],预先分配数组的长度就是和append次数一样大小的数组,每做一次append就像数组下标增加1,并且放在对应的数组位置,并记录下总体的长度,待这个对象发生toString操作时,此时再申请一个这个长度一样大小的char[]空间,将数据拷贝进去,就解决了所有的临时对象的问题,对于在增加了一次间接访问和toString时候发生的逐个拷贝这些开销都是可以接受的(只要append的次数不是特别的多,一般append的次数也不可能特别多,所以利用循环测试出来的性能区别这个时候也是不靠谱的); 最后,所谓的String拼接和StringBuilder下的使用,只要不是太大的字符串或者太多次数的拼接或者高并发访问的代码段做了2行代码以上的拼接,String做加法几乎和StringBuilder区别不大;太大的字符串产生的太大的临时空间,太多的拼接次数是产生太多的临时空间,同一条代码中作String的拼接(不论拼接次数)和使用StringBuilder做append效果一致,只是每次append结果在这行发生完成后会发生toString操作,而默认申请的StringBuilder大小默认为10,如果超过限制则翻倍,这也算是一个限制。 其余的就没什么了,此文闲扯,做做实验便知道,使用命令分析更加深入,关于动态扩展,在集合类里面也有类似的情况,需要注意。
对于JVM的内存写过的文章已经有点多了,而且有点烂了,不过说那么多大多数在解决OOM的情况,于此,本文就只阐述这个内容,携带一些分析和理解和部分扩展内容,也就是JVM宕机中的一些问题,OK,下面说下OOM的常见情况(本文基于jdk 1.6系列版本来编写,其余的版本未必完全适用): 第一类内存溢出,也是大家认为最多,第一反应认为是的内存溢出,就是堆栈溢出: 那什么样的情况就是堆栈溢出呢?当你看到下面的关键字的时候它就是堆栈溢出了: java.lang.OutOfMemoryError: ......java heap space..... 也就是当你看到heap相关的时候就肯定是堆栈溢出了,此时如果代码没有问题的情况下,适当调整-Xmx和-Xms是可以避免的,不过一定是代码没有问题的前提,为什么会溢出呢,要么代码有问题,要么访问量太多并且每个访问的时间太长或者数据太多,导致数据释放不掉,因为垃圾回收器是要找到那些是垃圾才能回收,这里它不会认为这些东西是垃圾,自然不会去回收了;主意这个溢出之前,可能系统会提前先报错关键字为: java.lang.OutOfMemoryError:GC over head limit exceeded 这种情况是当系统处于高频的GC状态,而且回收的效果依然不佳的情况,就会开始报这个错误,这种情况一般是产生了很多不可以被释放的对象,有可能是引用使用不当导致,或申请大对象导致,但是java heap space的内存溢出有可能提前不会报这个错误,也就是可能内存就直接不够导致,而不是高频GC. 第二类内存溢出,PermGen的溢出,或者PermGen 满了的提示,你会看到这样的关键字: 关键信息为: java.lang.OutOfMemoryError: PermGen space 原因:系统的代码非常多或引用的第三方包非常多、或代码中使用了大量的常量、或通过intern注入常量、或者通过动态代码加载等方法,导致常量池的膨胀,虽然JDK 1.5以后可以通过设置对永久带进行回收,但是我们希望的是这个地方是不做GC的,它够用就行,所以一般情况下今年少做类似的操作,所以在面对这种情况常用的手段是:增加-XX:PermSize和-XX:MaxPermSize的大小。 第三类内存溢出:在使用ByteBuffer中的allocateDirect()的时候会用到,很多javaNIO的框架中被封装为其他的方法 溢出关键字: java.lang.OutOfMemoryError: Direct buffer memory如果你在直接或间接使用了ByteBuffer中的allocateDirect方法的时候,而不做clear的时候就会出现类似的问题,常规的引用程序IO输出存在一个内核态与用户态的转换过程,也就是对应直接内存与非直接内存,如果常规的应用程序你要将一个文件的内容输出到客户端需要通过OS的直接内存转换拷贝到程序的非直接内存(也就是heap中),然后再输出到直接内存由操作系统发送出去,而直接内存就是由OS和应用程序共同管理的,而非直接内存可以直接由应用程序自己控制的内存,jvm垃圾回收不会回收掉直接内存这部分的内存,所以要注意了哦。 如果经常有类似的操作,可以考虑设置参数:-XX:MaxDirectMemorySize 第四类内存溢出错误: 溢出关键字: java.lang.StackOverflowError 这个参数直接说明一个内容,就是-Xss太小了,我们申请很多局部调用的栈针等内容是存放在用户当前所持有的线程中的,线程在jdk 1.4以前默认是256K,1.5以后是1M,如果报这个错,只能说明-Xss设置得太小,当然有些厂商的JVM不是这个参数,本文仅仅针对Hotspot VM而已;不过在有必要的情况下可以对系统做一些优化,使得-Xss的值是可用的。 第五类内存溢出错误: 溢出关键字: java.lang.OutOfMemoryError: unable to create new native thread 上面第四种溢出错误,已经说明了线程的内存空间,其实线程基本只占用heap以外的内存区域,也就是这个错误说明除了heap以外的区域,无法为线程分配一块内存区域了,这个要么是内存本身就不够,要么heap的空间设置得太大了,导致了剩余的内存已经不多了,而由于线程本身要占用内存,所以就不够用了,说明了原因,如何去修改,不用我多说,你懂的。 第六类内存溢出: 溢出关键字 java.lang.OutOfMemoryError: request {} byte for {}out of swap 这类错误一般是由于地址空间不够而导致。 六大类常见溢出已经说明JVM中99%的溢出情况,要逃出这些溢出情况非常困难,除非一些很怪异的故障问题会发生,比如由于物理内存的硬件问题,导致了code cache的错误(在由byte code转换为native code的过程中出现,但是概率极低),这种情况内存 会被直接crash掉,类似还有swap的频繁交互在部分系统中会导致系统直接被crash掉,OS地址空间不够的话,系统根本无法启动,呵呵;JNI的滥用也会导致一些本地内存无法释放的问题,所以尽量避开JNI;socket连接数据打开过多的socket也会报类似:IOException: Too many open files等错误信息。 JNI就不用多说了,尽量少用,除非你的代码太牛B了,我无话可说,呵呵,这种内存如果没有在被调用的语言内部将内存释放掉(如C语言),那么在进程结束前这些内存永远释放不掉,解决办法只有一个就是将进程kill掉。 另外GC本身是需要内存空间的,因为在运算和中间数据转换过程中都需要有内存,所以你要保证GC的时候有足够的内存哦,如果没有的话GC的过程将会非常的缓慢。 顺便这里就提及一些新的CMS GC的内容和策略(有点乱,每次写都很乱,但是能看多少看多少吧): 首先我再写一次一前博客中的已经写过的内容,就是很多参数没啥建议值,建议值是自己在现场根据实际情况科学计算和测试得到的综合效果,建议值没有绝对好的,而且默认值很多也是有问题的,因为不同的版本和厂商都有很大的区别,默认值没有永久都是一样的,就像-Xss参数的变化一样,要看到你当前的java程序heap的大致情况可以这样看看(以下参数是随便设置的,并不是什么默认值): $sudo jmap -heap `pgrep java` Attaching to process ID 4280, please wait... Debugger attached successfully. Server compiler detected. JVM version is 19.1-b02 using thread-local object allocation. Parallel GC with 8 thread(s) Heap Configuration: MinHeapFreeRatio = 40 MaxHeapFreeRatio = 70 MaxHeapSize = 1073741824 (1024.0MB) NewSize = 134217728 (128.0MB) MaxNewSize = 134217728 (128.0MB) OldSize = 5439488 (5.1875MB) NewRatio = 2 SurvivorRatio = 8 PermSize = 134217728 (128.0MB) MaxPermSize = 268435456 (256.0MB) Heap Usage: PS Young Generation Eden Space: capacity = 85721088 (81.75MB) used = 22481312 (21.439849853515625MB) free = 63239776 (60.310150146484375MB) 26.22611602876529% used From Space: capacity = 24051712 (22.9375MB) used = 478488 (0.45632171630859375MB) free = 23573224 (22.481178283691406MB) 1.9894134770946867% used To Space: capacity = 24248320 (23.125MB) used = 0 (0.0MB) free = 24248320 (23.125MB) 0.0% used PS Old Generation capacity = 939524096 (896.0MB) used = 16343864 (15.586723327636719MB) free = 923180232 (880.4132766723633MB) 1.7395896571023124% used PS Perm Generation capacity = 134217728 (128.0MB) used = 48021344 (45.796722412109375MB) free = 86196384 (82.20327758789062MB) 35.77868938446045% used 付:sudo是需要拿到管理员权限,如果你的系统权限很大那么就不需要了,最后的grep java那个内容如果不对,可以直接通过jps或者ps命令将和java相关的进程号直接写进去,如:java -map 4280,这个参数其实完全可以通过jstat工具来替代,而且看到的效果更加好,这个参数在线上应用中,尽量少用(尤其是高并发的应用中),可能会触发JVM的bug,导致应用挂起;在jvm 1.6u14后可以编写任意一段程序,然后在运行程序的时候,增加参数为:-XX:+PrintFlagsFinal来输出当前JVM中运行时的参数值,或者通过jinfo来查看,jinfo是非常强大的工具,可以对部分参数进行动态修改,当然内存相关的东西是不能修改的,只能增加一些不是很相关的参数,有关JVM的工具使用,后续文章中如果有机会我们再来探讨,不是本文的重点;补充:关于参数的默认值对不同的JVM版本、不同的厂商、运行于不同的环境(一般和位数有关系)默认值会有区别。 OK,再说下反复的一句,没有必要的话就不要乱设置参数,参数不是拿来玩的,默认的参数对于这门JDK都是有好处的,关键是否适合你的应用场景,一般来讲你常规的只需要设置以下几个参数就可以了: -server 表示为服务器端,会提供很多服务器端默认的配置,如并行回收,而服务器上一般这个参数都是默认的,所以都是可以省掉,与之对应的还有一个-client参数,一般在64位机器上,JVM是默认启动-server参数,也就是默认启动并行GC的,但是是ParallelGC而不是ParallelOldGC,两者算法不同(后面会简单说明下),而比较特殊的是windows 32位上默认是-client,这两个的区别不仅仅是默认的参数不一样,在jdk包下的jre包下一般会包含client和server包,下面分别对应启动的动态链接库,而真正看到的java、javac等相关命令指示一个启动导向,它只是根据命令找到对应的JVM并传入jvm中进行启动,也就是看到的java.exe这些文件并不是jvm;说了这么多,最终总结一下就是,-server和-client就是完全不同的两套VM,一个用于桌面应用,一个用于服务器的。 -Xmx 为Heap区域的最大值 -Xms 为Heap区域的初始值,线上环境需要与-Xmx设置为一致,否则capacity的值会来回飘动,飘得你心旷神怡,你懂的。 -Xss(或-ss) 这个其实也是可以默认的,如果你真的觉得有设置的必要,你就改下吧,1.5以后是1M的默认大小(指一个线程的native空间),如果代码不多,可以设置小点来让系统可以接受更大的内存。注意,还有一个参数是-XX:ThreadStackSize,这两个参数在设置的过程中如果都设置是有冲突的,一般按照JVM常理来说,谁设置在后面,就以谁为主,但是最后发现如果是在1.6以上的版本,-Xss设置在后面的确都是以-Xss为主,但是要是-XX:ThreadStackSize设置在后面,主线程还是为-Xss为主,而其它线程以-XX:ThreadStackSize为主,主线程做了一个特殊判定处理;单独设置都是以本身为主,-Xss不设置也不会采用其默认值,除非两个都不设置会采用-Xss的默认值。另外这个参数针对于hotspot的vm,在IBM的jvm中,还有一个参数为-Xoss,主要原因是IBM在对栈的处理上有操作数栈和方法栈等各种不同的栈种类,而hotspot不管是什么栈都放在一个私有的线程内部的,不区分是什么栈,所以只需要设置一个参数,而IBM的J9不是这样的;有关栈上的细节,后续我们有机会专门写文章来说明。 -XX:PermSize与-XX:MaxPermSize两个包含了class的装载的位置,或者说是方法区(但不是本地方法区),在Hotspot默认情况下为64M,主意全世界的JVM只有hostpot的VM才有Perm的区域,或者说只有hotspot才有对用户可以设置的这块区域,其他的JVM都没有,其实并不是没有这块区域,而是这块区域没有让用户来设置,其实这块区域本身也不应该让用户来设置,我们也没有一个明确的说法这块空间必须要设置多大,都是拍脑袋设置一个数字,如果发布到线上看下如果用得比较多,就再多点,如果用的少,就减少点,而这块区域和性能关键没有多大关系,只要能装下就OK,并且时不时会因为Perm不够而导致Full GC,所以交给开发者来调节这个参数不知道是怎么想的;所以Oracle将在新一代JVM中将这个区域彻底删掉,也就是对用户透明,G1的如果真正稳定起来,以后JVM的启动参数将会非常简单,而且理论上管理再大的内存也是没有问题的,其实G1(garbage first,一种基于region的垃圾收集回收器)已经在hotspot中开始有所试用,不过目前效果不好,还不如CMS呢,所以只是试用,G1已经作为ORACLE对JVM研发的最高重点,CMS自现在最高版本后也不再有新功能(可以修改bug),该项目已经进行5年,尚未发布正式版,CMS是四五年前发布的正式版,但是是最近一两年才开始稳定,而G1的复杂性将会远远超越CMS,所以要真正使用上G1还有待考察,全世界目前只有IBM J9真正实现了G1论文中提到的思想(论文于05年左右发表),IBM已经将J9应用于websphere中,但是并不代表这是全世界最好的jvm,全世界最好的jvm是Azul(无停顿垃圾回收算法和一个零开销的诊断/监控工具),几乎可以说这个jvm是没有暂停的,在全世界很多顶尖级的公司使用,不过价格非常贵,不能直接使用,目前这个jvm的主导者在研究JRockit,而目前hotspot和JRockit都是Oracle的,所以他们可能会合并,所以我们应该对JVM的性能充满信心。 也就是说你常用的情况下只需要设置4个参数就OK了,除非你的应用有些特殊,否则不要乱改,那么来看看一些其他情况的参数吧: 先来看个不大常用的,就是大家都知道JVM新的对象应该说几乎百分百的在Eden里面,除非Eden真的装不下,我们不考虑这种变态的问题,因为线上环境Eden区域都是不小的,来降低GC的次数以及全局 GC的概率;而JVM习惯将内存按照较为连续的位置进行分配,这样使得有足够的内存可以被分配,减少碎片,那么对于内存最后一个位置必然就有大量的征用问题,JVM在高一点的版本里面提出了为每个线程分配一些私有的区域来做来解决这个问题,而1.5后的版本还可以动态管理这些区域,那么如何自己设置和查看这些区域呢,看下英文全称为:Thread Local Allocation Buffer,简称就是:TLAB,即内存本地的持有的buffer,设置参数有: -XX:+UseTLAB 启用这种机制的意思-XX:TLABSize=<size in kb> 设置大小,也就是本地线程中的私有区域大小(只有这个区域放不下才会到Eden中去申请)。-XX:+ResizeTLAB 是否启动动态修改 这几个参数在多CPU下非常有用。 -XX:+PrintTLAB 可以输出TLAB的内容。 下面再闲扯些其它的参数: 如果你需要对Yong区域进行并行回收应该如何修改呢?在jdk1.5以后可以使用参数: -XX:+UseParNewGC 注意: 与它冲突的参数是:-XX:+UseParallelOldGC和-XX:+UseSerialGC,如果需要用这个参数,又想让整个区域是并行回收的,那么就使用-XX:+UseConcMarkSweepGC参数来配合,其实这个参数在使用了CMS后,默认就会启动该参数,也就是这个参数在CMS GC下是无需设置的,后面会提及到这些参数。 默认服务器上的对Full并行GC策略为(这个时候Yong空间回收的时候启动PSYong算法,也是并行回收的): -XX:+UseParallelGC 另外,在jdk1.5后出现一个新的参数如下,这个对Yong的回收算法和上面一样,对Old区域会有所区别,上面对Old回收的过程中会做一个全局的Compact,也就是全局的压缩操作,而下面的算法是局部压缩,为什么要局部压缩呢?是因为JVM发现每次压缩后再逻辑上数据都在Old区域的左边位置,申请的时候从左向右申请,那么生命力越长的对象就一般是靠左的,所以它认为左边的对象就是生命力很强,而且较为密集的,所以它针对这种情况进行部分密集,但是这两种算法mark阶段都是会暂停的,而且存活的对象越多活着的越多;而ParallelOldGC会进行部分压缩算法(主意一点,最原始的copy算法是不需要经过mark阶段,因为只需要找到一个或活着的就只需要做拷贝就可以,而Yong区域借用了Copy算法,只是唯一的区别就是传统的copy算法是采用两个相同大小的内存来拷贝,浪费空间为50%,所以分代的目标就是想要实现很多优势所在,认为新生代85%以上的对象都应该是死掉的,所以S0和S1一般并不是很大),该算法为jdk 1.5以后对于绝大部分应用的最佳选择。 -XX:+UseParallelOldGC -XX:ParallelGCThread=12:并行回收的线程数,最好根据实际情况而定,因为线程多往往存在征用调度和上下文切换的开销;而且也并非CPU越多线程数也可以设置越大,一般设置为12就再增加用处也不大,主要是算法本身内部的征用会导致其线程的极限就是这样。 设置Yong区域大小: -Xmn Yong区域的初始值和最大值一样大 -XX:NewSize和-XX:MaxNewSize如果设置以为一样大就是和-Xmn,在JRockit中会动态变化这些参数,根据实际情况有可能会变化出两个Yong区域,或者没有Yong区域,有些时候会生出来一个半长命对象区域;这里除了这几个参数外,还有一个参数是NewRatio是设置Old/Yong的倍数的,这几个参数都是有冲突的,服务器端建议是设置-Xmn就可以了,如果几个参数全部都有设置,-Xmn和-XX:NewSize与-XX:MaxNewSize将是谁设置在后面,以谁的为准,而-XX:NewSize -XX:MaxNewSize与-XX:NewRatio时,那么参数设置的结果可能会以下这样的(jdk 1.4.1后): min(MaxNewSize,max(NewSize, heap/(NewRatio+1))) -XX:NewRatio为Old区域为Yong的多少倍,间接设置Yong的大小,1.6中如果使用此参数,则默认会在适当时候被动态调整,具体请看下面参数UseAdaptiveSizepollcy 的说明。 三个参数不要同时设置,因为都是设置Yong的大小的。 -XX:SurvivorRatio:该参数为Eden与两个求助空间之一的比例,注意Yong的大小等价于Eden + S0 + S1,S0和S1的大小是等价的,这个参数为Eden与其中一个S区域的大小比例,如参数为8,那么Eden就占用Yong的80%,而S0和S1分别占用10%。 以前的老版本有一个参数为:-XX:InitialSurivivorRatio,如果不做任何设置,就会以这个参数为准,这个参数的默认值就是8,不过这个参数并不是Eden/Survivor的大小,而是Yong/Survivor,所以所以默认值8,代表每一个S区域的空间大小为Yong区域的12.5%而不是10%。另外顺便提及一下,每次大家看到GC日志的时候,GC日志中的每个区域的最大值,其中Yong的空间最大值,始终比设置的Yong空间的大小要小一点,大概是小12.5%左右,那是因为每次可用空间为Eden加上一个Survivor区域的大小,而不是整个Yong的大小,因为可用空间每次最多是这样大,两个Survivor区域始终有一块是空的,所以不会加上两个来计算。 -XX:MaxTenuringThreshold=15:在正常情况下,新申请的对象在Yong区域发生多少次GC后就会被移动到Old(非正常就是S0或S1放不下或者不太可能出现的Eden都放不下的对象),这个参数一般不会超过16(因为计数器从0开始计数,所以设置为15的时候相当于生命周期为16)。 要查看现在的这个值的具体情况,可以使用参数:-XX:+PrintTenuringDistribution 通过上面的jmap应该可以看出我的机器上的MinHeapFreeRatio和MaxHeapFreeRatio分别为40个70,也就是大家经常说的在GC后剩余空间小于40%时capacity开始增大,而大于70%时减小,由于我们不希望让它移动,所以这两个参数几乎没有意义,如果你需要设置就设置参数为: -XX:MinHeapFreeRatio=40-XX:MaxHeapFreeRatio=70 JDK 1.6后有一个动态调节板块的,当然如果你的每一个板块都是设置固定值,这个参数也没有用,不过如果是非固定的,建议还是不要动态调整,默认是开启的,建议将其关掉,参数为: -XX:+UseAdaptiveSizepollcy 建议使用-XX:-UseAdaptiveSizepollcy关掉,为什么当你的参数设置了NewRatio、Survivor、MaxTenuringThreshold这几个参数如果在启动了动态更新情况下,是无效的,当然如果你设置-Xmn是有效的,但是如果设置的比例的话,初始化可能会按照你的参数去运行,不过运行过程中会通过一定的算法动态修改,监控中你可能会发现这些参数会发生改变,甚至于S0和S1的大小不一样。 如果启动了这个参数,又想要跟踪变化,那么就使用参数:-XX:+PrintAdaptiveSizePolicy 上面已经提到,javaNIO中通过Direct内存来提高性能,这个区域的大小默认是64M,在适当的场景可以设置大一些。 -XX:MaxDirectMemorySize 一个不太常用的参数: -XX:+ScavengeBeforeFullGC 默认是开启状态,在full GC前先进行minor GC。 对于java堆中如果要设置大页内存,可以通过设置参数: 付:此参数必须在操作系统的内核支持的基础上,需要在OS级别做操作为: echo 1024 > /proc/sys/vm/nr_hugepages echo 2147483647 > /proc/sys/kernel/shmmax -XX:+UseLargePages -XX:LargePageSizeInBytes 此时整个JVM都将在这块内存中,否则全部不在这块内存中。 javaIO的临时目录设置 -Djava.io.tmpdir jstack会去寻找/tmp/hsperfdata_admin下去寻找与进程号相同的文件,32位机器上是没有问题的,64为机器的是有BUG的,在jdk 1.6u23版本中已经修复了这个bug,如果你遇到这个问题,就需要升级JDK了。 还记得上次说的平均晋升大小吗,在并行GC时,如果平均晋升大小大于old剩余空间,则发生full GC,那么当小于剩余空间时,也就是平均晋升小于剩余空间,但是剩余空间小于eden + 一个survivor的空间时,此时就依赖于参数: -XX:-HandlePromotionFailure 启动该参数时,上述情况成立就发生minor gc(YGC),大于则发生full gc(major gc)。 一般默认直接分配的对象如果大于Eden的一半就会直接晋升到old区域,但是也可以通过参数来指定: -XX:PretenureSizeThreshold=2m 我个人不建议使用这个参数 也就是当申请对象大于这个值就会晋升到old区域。 传说中GC时间的限制,一个是通过比例限制,一个是通过最大暂停时间限制,但是GC时间能限制么,呵呵,在增量中貌似可以限制,不过不能限制住GC总体的时间,所以这个参数也不是那么关键。 -XX:GCTimeRatio= -XX:MaxGCPauseMillis -XX:GCTimeLimit 要看到真正暂停的时间就一个是看GCDetail的日志,另一个是设置参数看: -XX:+PrintGCApplicationStoppedTime 有些人,有些人就是喜欢在代码里面里头写System.gc(),耍酷,这个不是测试程序是线上业务,这样将会导致N多的问题,不多说了,你应该懂的,不懂的话看下书吧,而RMI是很不听话的一个鸟玩意,EJB的框架也是基于RMI写的,RMI为什么不听话呢,就是它自己在里面非要搞个System.gc(),哎,为了放置频繁的做,频繁的做,你就将这个命令的执行禁用掉吧,当然程序不用改,不然那些EJB都跑步起来了,呵呵: -XX:+DisableExplicitGC 默认是没有禁用掉,写成+就是禁用掉的了,但是有些时候在使用allocateDirect的时候,很多时候还真需要System.gc来强制回收这块资源。 内存溢出时导出溢出的错误信息:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/xieyu/logs/ 这个参数指定导出时的路径,不然导出的路径就是虚拟机的目标位置,不好找了,默认的文件名是:java_pid<进程号>.hprof,这个文件可以类似使用jmap -dump:file=....,format=b <pid>来dump类似的内容,文件后缀都是hprof,然后下载mat工具进行分析即可(不过内存有多大dump文件就多大,而本地分析的时候内存也需要那么大,所以很多时候下载到本地都无法启动是很正常的),后续文章有机会我们来说明这些工具,另外jmap -dump参数也不要经常用,会导致应用挂起哦;另外此参数只会在第一次输出OOM的时候才会进行堆的dump操作(java heap的溢出是可以继续运行再运行的程序的,至于web应用是否服务要看应用服务器自身如何处理,而c heap区域的溢出就根本没有dump的机会,因为直接就宕机了,目前系统无法看到c heap的大小以及内部变化,要看大小只能间接通过看JVM进程的内存大小(top或类似参数),这个大小一般会大于heap+perm的大小,多余的部分基本就可以认为是c heap的大小了,而看内部变化呢只有google perftools可以达到这个目的),如果内存过大这个dump操作将会非常长,所以hotspot如果以后想管理大内存,这块必须有新的办法出来。 最后,用dump出来的文件,通过mat分析出来的结果往往有些时候难以直接确定到底哪里有问题,可以看到的维度大概有:那个类使用的内存最多,以及每一个线程使用的内存,以及线程内部每一个调用的类和方法所使用的内存,但是很多时候无法判定到底是程序什么地方调用了这个类或者方法,因为这里只能看到最终消耗内存的类,但是不知道谁使用了它,一个办法是扫描代码,但是太笨重,而且如果是jar包中调用了就不好弄了,另一种方法是写agent,那么就需要相应的配合了,但是有一个非常好的工具就是btrace工具(jdk 1.7貌似还不支持),可以跟踪到某个类的某个方法被那些类中的方法调用过,那这个问题就好说了,只要知道开销内存的是哪一个类,就能知道谁调用过它,OK,关于btrace的不是本文重点,网上都有,后续文章有机会再探讨,原理:No performance impact during runtime(无性能影响) Dumping a –Xmx512m heap Create a 512MB .hprof file(512M内存就dump出512M的空间大小) JVM is “dead” during dumping(死掉时dump) Restarting JVM during this dump will cause unusable .hprof file(重启导致文件不可用) 注明的NUMA架构,在JVM中开始支持,当然也需要CPU和OS的支持才可以,需要设置参数为: -XX:+UseNUMA 必须在并行GC的基础上才有的 老年代无法分配区域的最大等待时间为(默认值为0,但是也不要去动它): -XX:GCExpandToAllocateDelayMillis 让JVM中所有的set和get方法转换为本地代码: -XX:+UseFastAccessorMethods 以时间戳输出Heap的利用率 -XX:+PrintHeapUsageOverTime 在64bit的OS上面(其实一般达不到57位左右),由于指针会放大为8个byte,所以会导致空间使用增加,当然,如果内存够大,就没有问题,但是如果升级到64bit系统后,只是想让内存达到4G或者8G,那么就完全可以通过很多指针压缩为4byte就OK了,所以在提供以下参数(本参数于jdk 1.6u23后使用,并自动开启,所以也不需要你设置,知道就OK): -XX:+UseCompressedOops 请注意:这个参数默认在64bit的环境下默认启动,但是如果JVM的内存达到32G后,这个参数就会默认为不启动,因为32G内存后,压缩就没有多大必要了,要管理那么大的内存指针也需要很大的宽度了。 后台JIT编译优化启动 -XX:+BackgroundCompilation 如果你要输出GC的日志以及时间戳,相关的参数有: -XX:+PrintGCDetails 输出GC的日志详情,包含了时间戳 -XX:+PrintGCTimeStamps 输出GC的时间戳信息,按照启动JVM后相对时间的每次GC的相对秒值(毫秒在小数点后面),也就是每次GC相对启动JVM启动了多少秒后发生了这次GC -XX:+PrintGCDateStamps输出GC的时间信息,会按照系统格式的日期输出每次GC的时间 -XX:+PrintGCTaskTimeStamps输出任务的时间戳信息,这个细节上比较复杂,后续有文章来探讨。 -XX:-TraceClassLoading 跟踪类的装载 -XX:-TraceClassUnloading 跟踪类的卸载 -XX:+PrintHeapAtGC 输出GC后各个堆板块的大小。 将常量信息GC信息输出到日志文件: -Xloggc:/home/xieyu/logs/gc.log 现在面对大内存比较流行是是CMS GC(最少1.5才支持),首先明白CMS的全称是什么,不是传统意义上的内容管理系统(Content Management System)哈,第一次我也没看懂,它的全称是:Concurrent Mark Sweep,三个单词分别代表并发、标记、清扫(主意这里没有compact操作,其实CMS GC的确没有compact操作),也就是在程序运行的同时进行标记和清扫工作,至于它的原理前面有提及过,只是有不同的厂商在上面做了一些特殊的优化,比如一些厂商在标记根节点的过程中,标记完当前的根,那么这个根下面的内容就不会被暂停恢复运行了,而移动过程中,通过读屏障来看这个内存是不是发生移动,如果在移动稍微停一下,移动过去后再使用,hotspot还没这么厉害,暂停时间还是挺长的,只是相对其他的GC策略在面对大内存来讲是不错的选择。 下面看一些CMS的策略(并发GC总时间会比常规的并行GC长,因为它是在运行时去做GC,很多资源征用都会影响其GC的效率,而总体的暂停时间会短暂很多很多,其并行线程数默认为:(上面设置的并行线程数 + 3)/ 4 付:CMS是目前Hotspot管理大内存最好的JVM,如果是常规的JVM,最佳选择为ParallelOldGC,如果必须要以响应时间为准,则选择CMS,不过CMS有两个隐藏的隐患: 1、CMS GC虽然是并发且并行运行的GC,但是初始化的时候如果采用默认值92%(JVM 1.5的白皮书上描述为68%其实是错误的,1.6是正确的),就很容易出现问题,因为CMS GC仅仅针对Old区域,Yong区域使用ParNew算法,也就是Old的CMS回收和Yong的回收可以同时进行,也就是回收过程中Yong有可能会晋升对象Old,并且业务也可以同时运行,所以92%基本开始启动CMS GC很有可能old的内存就不够用了,当内存不够用的时候,就启动Full GC,并且这个Full GC是串行的,所以如果弄的不好,CMS会比并行GC更加慢,为什么要启用串行是因为CMS GC、并行GC、串行GC的继承关系决定的,简单说就是它没办法去调用并行GC的代码,细节说后续有文章来细节说明),建议这个值设置为70%左右吧,不过具体时间还是自己决定。 2、CMS GC另一个大的隐患,其实不看也差不多应该清楚,看名字就知道,就是不会做Compact操作,它最恶心的地方也在这里,所以上面才说一般的应用都不使用它,它只有内存垃圾非常多,多得无法分配晋升的空间的时候才会出现一次compact,但是这个是Full GC,也就是上面的串行,很恐怖的,所以内存不是很大的,不要考虑使用它,而且它的算法十分复杂。 还有一些小的隐患是:和应用一起征用CPU(不过这个不是大问题,增加CPU即可)、整个运行过程中时间比并行GC长(这个也不是大问题,因为我们更加关心暂停时间而不是运行时间,因为暂停会影响非常多的业务)。 启动CMS为全局GC方法(注意这个参数也不能上面的并行GC进行混淆,Yong默认是并行的,上面已经说过 -XX:+UseConcMarkSweepGC 在并发GC下启动增量模式,只能在CMS GC下这个参数才有效。 -XX:+CMSIncrementalMode 启动自动调节duty cycle,即在CMS GC中发生的时间比率设置,也就是说这段时间内最大允许发生多长时间的GC工作是可以调整的。 -XX:+CMSIncrementalPacing 在上面这个参数设定后可以分别设置以下两个参数(参数设置的比率,范围为0-100): -XX:CMSIncrementalDutyCycleMin=0-XX:CMSIncrementalDutyCycle=10 增量GC上还有一个保护因子(CMSIncrementalSafetyFactor),不太常用;CMSIncrementalOffset提供增量GC连续时间比率的设置;CMSExpAvgFactor为增量并发的GC增加权重计算。 -XX:CMSIncrementalSafetyFactor=-XX:CMSIncrementalOffset= -XX:CMSExpAvgFactor= 是否启动并行CMS GC(默认也是开启的) -XX:+CMSParallelRemarkEnabled 要单独对CMS GC设置并行线程数就设置(默认也不需要设置): -XX:ParallelCMSThreads 对PernGen进行垃圾回收: JDK 1.5在CMS GC基础上需要设置参数(也就是前提是CMS GC才有): -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled 1.6以后的版本无需设置:-XX:+CMSPermGenSweepingEnabled,注意,其实一直以来Full GC都会触发对Perm的回收过程,CMS GC需要有一些特殊照顾,虽然VM会对这块区域回收,但是Perm回收的条件几乎不太可能实现,首先需要这个类的classloader必须死掉,才可以将该classloader下所有的class干掉,也就是要么全部死掉,要么全部活着;另外,这个classloader下的class没有任何object在使用,这个也太苛刻了吧,因为常规的对象申请都是通过系统默认的,应用服务器也有自己默认的classloader,要让它死掉可能性不大,如果这都死掉了,系统也应该快挂了。 CMS GC因为是在程序运行时进行GC,不会暂停,所以不能等到不够用的时候才去开启GC,官方说法是他们的默认值是68%,但是可惜的是文档写错了,经过很多测试和源码验证这个参数应该是在92%的时候被启动,虽然还有8%的空间,但是还是很可怜了,当CMS发现内存实在不够的时候又回到常规的并行GC,所以很多人在没有设置这个参数的时候发现CMS GC并没有神马优势嘛,和并行GC一个鸟样子甚至于更加慢,所以这个时候需要设置参数(这个参数在上面已经说过,启动CMS一定要设置这个参数): -XX:CMSInitiatingOccupancyFraction=70 这样保证Old的内存在使用到70%的时候,就开始启动CMS了;如果你真的想看看默认值,那么就使用参数:-XX:+PrintCMSInitiationStatistics 这个变量只有JDK 1.6可以使用 1.5不可以,查看实际值-XX:+PrintCMSStatistics;另外,还可以设置参数-XX:CMSInitiatingPermOccupancyFraction来设置Perm空间达到多少时启动CMS GC,不过意义不大。 JDK 1.6以后有些时候启动CMS GC是根据计算代价进行启动,也就是不一定按照你指定的参数来设置的,如果你不想让它按照所谓的成本来计算GC的话,那么你就使用一个参数:-XX:+UseCMSInitiatingOccupancyOnly,默认是false,它就只会按照你设置的比率来启动CMS GC了。如果你的程序中有System.gc以及设置了ExplicitGCInvokesConcurrent在jdk 1.6中,这种情况使用NIO是有可能产生问题的。 启动CMS GC的compation操作,也就是发生多少次后做一次全局的compaction: -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction:发生多少次CMS Full GC,这个参数最好不要设置,因为要做compaction的话,也就是真正的Full GC是串行的,非常慢,让它自己去决定什么时候需要做compaction。 -XX:CMSMaxAbortablePrecleanTime=5000 设置preclean步骤的超时时间,单位为毫秒,preclean为cms gc其中一个步骤,关于cms gc步骤比较多,本文就不细节探讨了。 并行GC在mark阶段,可能会同时发生minor GC,old区域也可能发生改变,于是并发GC会对发生了改变的内容进行remark操作,这个触发的条件是: -XX:CMSScheduleRemarkEdenSizeThreshold -XX:CMSScheduleRemarkEdenPenetration 即Eden区域多大的时候开始触发,和eden使用量超过百分比多少的时候触发,前者默认是2M,后者默认是50%。 但是如果长期不做remark导致old做不了,可以设置超时,这个超时默认是5秒,可以通过参数: -XX:CMSMaxAbortablePrecleanTime -XX:+ExplicitGCInvokesConcurrent 在显示发生GC的时候,允许进行并行GC。 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 几乎和上面一样,只不过多一个对Perm区域的回收而已。 补充: 其实JVM还有很多的版本,很多的厂商,与其优化的原则,随便举两个例子hotspot在GC中做的一些优化(这里不说代码的编译时优化或运行时优化): Eden申请的空间对象由Old区域的某个对象的一个属性指向(也就是Old区域的这个空间不回收,Eden这块就没有必要考虑回收),所以Hotspot在CPU写上面,做了一个屏障,当发生赋值语句的时候(对内存来讲赋值就是一种写操作),如果发现是一个新的对象由Old指向Eden,那么就会将这个对象记录在一个卡片机里面,这个卡片机是有很多512字节的卡片组成,当在YGC过程中,就基本不会去移动或者管理这块对象(付:这种卡片机会在CMS GC的算法中使用,不过和这个卡片不是放在同一个地方的,也是CMS GC的关键,对于CMS GC的算法细节描述,后续文章我们单独说明)。 Old区域对于一些比较大的对象,JVM就不会去管理个对象,也就是compact过程中不会去移动这块对象的区域等等吧。 以上大部分参数为hotspot的自带关于性能的参数,参考版本为JDK 1.5和1.6的版本,很多为个人经验说明,不足以说明所有问题,如果有问题,欢迎探讨;另外,JDK的参数是不是就只有这些呢,肯定并不是,我知道的也不止这些,但是有些觉得没必要说出来的参数和一些数学运算的参数我就不想给出来了,比如像禁用掉GC的参数有神马意义,我们的服务器要是把这个禁用掉干个屁啊,呵呵,做测试还可以用这玩玩,让它不做GC直接溢出;还有一些什么计算因子啥的,还有很多复杂的数学运算规则,要是把这个配置明白了,就太那个了,而且一般情况下也没那个必要,JDK到现在的配置参数多达上500个以上,要知道完的话慢慢看吧,不过意义不大,而且要知道默认值最靠谱的是看源码而不是看文档,官方文档也只能保证绝大部是正确的,不能保证所有的是正确的。 本文最后追加在jdk 1.6u 24后通过上面说明的-XX:+PrintFlagsFinal输出的参数以及默认值(还是那句话,在不同的平台上是不一样的),输出的参数如下,可以看看JVM的参数是相当的多,参数如此之多,你只需要掌握关键即可,参数还有很多有冲突的,不要纠结于每一个参数的细节: $java -XX:+PrintFlagsFinal uintx AdaptivePermSizeWeight = 20 {product} uintx AdaptiveSizeDecrementScaleFactor = 4 {product} uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} uintx AdaptiveSizePausePolicy = 0 {product} uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product} uintx AdaptiveSizePolicyInitializingSteps = 20 {product} uintx AdaptiveSizePolicyOutputInterval = 0 {product} uintx AdaptiveSizePolicyWeight = 10 {product} uintx AdaptiveSizeThroughPutPolicy = 0 {product} uintx AdaptiveTimeWeight = 25 {product} bool AdjustConcurrency = false {product} bool AggressiveOpts = false {product} intx AliasLevel = 3 {product} intx AllocatePrefetchDistance = -1 {product} intx AllocatePrefetchInstr = 0 {product} intx AllocatePrefetchLines = 1 {product} intx AllocatePrefetchStepSize = 16 {product} intx AllocatePrefetchStyle = 1 {product} bool AllowJNIEnvProxy = false {product} bool AllowParallelDefineClass = false {product} bool AllowUserSignalHandlers = false {product} bool AlwaysActAsServerClassMachine = false {product} bool AlwaysCompileLoopMethods = false {product} intx AlwaysInflate = 0 {product} bool AlwaysLockClassLoader = false {product} bool AlwaysPreTouch = false {product} bool AlwaysRestoreFPU = false {product} bool AlwaysTenure = false {product} bool AnonymousClasses = false {product} bool AssertOnSuspendWaitFailure = false {product} intx Atomics = 0 {product} uintx AutoGCSelectPauseMillis = 5000 {product} intx BCEATraceLevel = 0 {product} intx BackEdgeThreshold = 100000 {pd product} bool BackgroundCompilation = true {pd product} uintx BaseFootPrintEstimate = 268435456 {product} intx BiasedLockingBulkRebiasThreshold = 20 {product} intx BiasedLockingBulkRevokeThreshold = 40 {product} intx BiasedLockingDecayTime = 25000 {product} intx BiasedLockingStartupDelay = 4000 {product} bool BindGCTaskThreadsToCPUs = false {product} bool BlockOffsetArrayUseUnallocatedBlock = false {product} bool BytecodeVerificationLocal = false {product} bool BytecodeVerificationRemote = true {product} intx CICompilerCount = 1 {product} bool CICompilerCountPerCPU = false {product} bool CITime = false {product} bool CMSAbortSemantics = false {product} uintx CMSAbortablePrecleanMinWorkPerIteration = 100 {product} intx CMSAbortablePrecleanWaitMillis = 100 {product} uintx CMSBitMapYieldQuantum = 10485760 {product} uintx CMSBootstrapOccupancy = 50 {product} bool CMSClassUnloadingEnabled = false {product} uintx CMSClassUnloadingMaxInterval = 0 {product} bool CMSCleanOnEnter = true {product} bool CMSCompactWhenClearAllSoftRefs = true {product} uintx CMSConcMarkMultiple = 32 {product} bool CMSConcurrentMTEnabled = true {product} uintx CMSCoordinatorYieldSleepCount = 10 {product} bool CMSDumpAtPromotionFailure = false {product} uintx CMSExpAvgFactor = 50 {product} bool CMSExtrapolateSweep = false {product} uintx CMSFullGCsBeforeCompaction = 0 {product} uintx CMSIncrementalDutyCycle = 10 {product} uintx CMSIncrementalDutyCycleMin = 0 {product} bool CMSIncrementalMode = false {product} uintx CMSIncrementalOffset = 0 {product} bool CMSIncrementalPacing = true {product} uintx CMSIncrementalSafetyFactor = 10 {product} uintx CMSIndexedFreeListReplenish = 4 {product} intx CMSInitiatingOccupancyFraction = -1 {product} intx CMSInitiatingPermOccupancyFraction = -1 {product} intx CMSIsTooFullPercentage = 98 {product} double CMSLargeCoalSurplusPercent = {product} double CMSLargeSplitSurplusPercent = {product} bool CMSLoopWarn = false {product} uintx CMSMaxAbortablePrecleanLoops = 0 {product} intx CMSMaxAbortablePrecleanTime = 5000 {product} uintx CMSOldPLABMax = 1024 {product} uintx CMSOldPLABMin = 16 {product} uintx CMSOldPLABNumRefills = 4 {product} uintx CMSOldPLABReactivityCeiling = 10 {product} uintx CMSOldPLABReactivityFactor = 2 {product} bool CMSOldPLABResizeQuicker = false {product} uintx CMSOldPLABToleranceFactor = 4 {product} bool CMSPLABRecordAlways = true {product} uintx CMSParPromoteBlocksToClaim = 16 {product} bool CMSParallelRemarkEnabled = true {product} bool CMSParallelSurvivorRemarkEnabled = true {product} bool CMSPermGenPrecleaningEnabled = true {product} uintx CMSPrecleanDenominator = 3 {product} uintx CMSPrecleanIter = 3 {product} uintx CMSPrecleanNumerator = 2 {product} bool CMSPrecleanRefLists1 = true {product} bool CMSPrecleanRefLists2 = false {product} bool CMSPrecleanSurvivors1 = false {product} bool CMSPrecleanSurvivors2 = true {product} uintx CMSPrecleanThreshold = 1000 {product} bool CMSPrecleaningEnabled = true {product} bool CMSPrintChunksInDump = false {product} bool CMSPrintObjectsInDump = false {product} uintx CMSRemarkVerifyVariant = 1 {product} bool CMSReplenishIntermediate = true {product} uintx CMSRescanMultiple = 32 {product} uintx CMSRevisitStackSize = 1048576 {product} uintx CMSSamplingGrain = 16384 {product} bool CMSScavengeBeforeRemark = false {product} uintx CMSScheduleRemarkEdenPenetration = 50 {product} uintx CMSScheduleRemarkEdenSizeThreshold = 2097152 {product} uintx CMSScheduleRemarkSamplingRatio = 5 {product} double CMSSmallCoalSurplusPercent = {product} double CMSSmallSplitSurplusPercent = {product} bool CMSSplitIndexedFreeListBlocks = true {product} intx CMSTriggerPermRatio = 80 {product} intx CMSTriggerRatio = 80 {product} bool CMSUseOldDefaults = false {product} intx CMSWaitDuration = 2000 {product} uintx CMSWorkQueueDrainThreshold = 10 {product} bool CMSYield = true {product} uintx CMSYieldSleepCount = 0 {product} intx CMSYoungGenPerWorker = 16777216 {product} uintx CMS_FLSPadding = 1 {product} uintx CMS_FLSWeight = 75 {product} uintx CMS_SweepPadding = 1 {product} uintx CMS_SweepTimerThresholdMillis = 10 {product} uintx CMS_SweepWeight = 75 {product} bool CheckJNICalls = false {product} bool ClassUnloading = true {product} intx ClearFPUAtPark = 0 {product} bool ClipInlining = true {product} uintx CodeCacheExpansionSize = 32768 {pd product} uintx CodeCacheFlushingMinimumFreeSpace = 1536000 {product} uintx CodeCacheMinimumFreeSpace = 512000 {product} bool CollectGen0First = false {product} bool CompactFields = true {product} intx CompilationPolicyChoice = 0 {product} intx CompilationRepeat = 0 {C1 product} ccstrlist CompileCommand = {product} ccstr CompileCommandFile = {product} ccstrlist CompileOnly = {product} intx CompileThreshold = 1500 {pd product} bool CompilerThreadHintNoPreempt = true {product} intx CompilerThreadPriority = -1 {product} intx CompilerThreadStackSize = 0 {pd product} uintx ConcGCThreads = 0 {product} bool ConvertSleepToYield = true {pd product} bool ConvertYieldToSleep = false {product} bool DTraceAllocProbes = false {product} bool DTraceMethodProbes = false {product} bool DTraceMonitorProbes = false {product} uintx DefaultMaxRAMFraction = 4 {product} intx DefaultThreadPriority = -1 {product} intx DeferPollingPageLoopCount = -1 {product} intx DeferThrSuspendLoopCount = 4000 {product} bool DeoptimizeRandom = false {product} bool DisableAttachMechanism = false {product} bool DisableExplicitGC = false {product} bool DisplayVMOutputToStderr = false {product} bool DisplayVMOutputToStdout = false {product} bool DontCompileHugeMethods = true {product} bool DontYieldALot = false {pd product} bool DumpSharedSpaces = false {product} bool EagerXrunInit = false {product} intx EmitSync = 0 {product} uintx ErgoHeapSizeLimit = 0 {product} ccstr ErrorFile = {product} bool EstimateArgEscape = true {product} intx EventLogLength = 2000 {product} bool ExplicitGCInvokesConcurrent = false {product} bool ExplicitGCInvokesConcurrentAndUnloadsClasses = false {produ bool ExtendedDTraceProbes = false {product} bool FLSAlwaysCoalesceLarge = false {product} uintx FLSCoalescePolicy = 2 {product} double FLSLargestBlockCoalesceProximity = {product} bool FailOverToOldVerifier = true {product} bool FastTLABRefill = true {product} intx FenceInstruction = 0 {product} intx FieldsAllocationStyle = 1 {product} bool FilterSpuriousWakeups = true {product} bool ForceFullGCJVMTIEpilogues = false {product} bool ForceNUMA = false {product} bool ForceSharedSpaces = false {product} bool ForceTimeHighResolution = false {product} intx FreqInlineSize = 325 {pd product} intx G1ConcRefinementGreenZone = 0 {product} intx G1ConcRefinementRedZone = 0 {product} intx G1ConcRefinementServiceIntervalMillis = 300 {product} uintx G1ConcRefinementThreads = 0 {product} intx G1ConcRefinementThresholdStep = 0 {product} intx G1ConcRefinementYellowZone = 0 {product} intx G1ConfidencePercent = 50 {product} uintx G1HeapRegionSize = 0 {product} intx G1MarkRegionStackSize = 1048576 {product} intx G1RSetRegionEntries = 0 {product} uintx G1RSetScanBlockSize = 64 {product} intx G1RSetSparseRegionEntries = 0 {product} intx G1RSetUpdatingPauseTimePercent = 10 {product} intx G1ReservePercent = 10 {product} intx G1SATBBufferSize = 1024 {product} intx G1UpdateBufferSize = 256 {product} bool G1UseAdaptiveConcRefinement = true {product} bool G1UseFixedWindowMMUTracker = false {product} uintx GCDrainStackTargetSize = 64 {product} uintx GCHeapFreeLimit = 2 {product} bool GCLockerInvokesConcurrent = false {product} bool GCOverheadReporting = false {product} intx GCOverheadReportingPeriodMS = 100 {product} intx GCPauseIntervalMillis = 500 {product} uintx GCTaskTimeStampEntries = 200 {product} uintx GCTimeLimit = 98 {product} uintx GCTimeRatio = 99 {product} ccstr HPILibPath = {product} bool HandlePromotionFailure = true {product} uintx HeapBaseMinAddress = 2147483648 {pd product} bool HeapDumpAfterFullGC = false {manageable} bool HeapDumpBeforeFullGC = false {manageable} bool HeapDumpOnOutOfMemoryError = false {manageable} ccstr HeapDumpPath = {manageable} uintx HeapFirstMaximumCompactionCount = 3 {product} uintx HeapMaximumCompactionInterval = 20 {product} bool IgnoreUnrecognizedVMOptions = false {product} uintx InitialCodeCacheSize = 163840 {pd product} uintx InitialHeapSize := 16777216 {product} uintx InitialRAMFraction = 64 {product} uintx InitialSurvivorRatio = 8 {product} intx InitialTenuringThreshold = 7 {product} uintx InitiatingHeapOccupancyPercent = 45 {product} bool Inline = true {product} intx InlineSmallCode = 1000 {pd product} intx InterpreterProfilePercentage = 33 {product} bool JNIDetachReleasesMonitors = true {product} bool JavaMonitorsInStackTrace = true {product} intx JavaPriority10_To_OSPriority = -1 {product} intx JavaPriority1_To_OSPriority = -1 {product} intx JavaPriority2_To_OSPriority = -1 {product} intx JavaPriority3_To_OSPriority = -1 {product} intx JavaPriority4_To_OSPriority = -1 {product} intx JavaPriority5_To_OSPriority = -1 {product} intx JavaPriority6_To_OSPriority = -1 {product} intx JavaPriority7_To_OSPriority = -1 {product} intx JavaPriority8_To_OSPriority = -1 {product} intx JavaPriority9_To_OSPriority = -1 {product} bool LIRFillDelaySlots = false {C1 pd product} uintx LargePageHeapSizeThreshold = 134217728 {product} uintx LargePageSizeInBytes = 0 {product} bool LazyBootClassLoader = true {product} bool ManagementServer = false {product} uintx MarkStackSize = 32768 {product} uintx MarkStackSizeMax = 4194304 {product} intx MarkSweepAlwaysCompactCount = 4 {product} uintx MarkSweepDeadRatio = 5 {product} intx MaxBCEAEstimateLevel = 5 {product} intx MaxBCEAEstimateSize = 150 {product} intx MaxDirectMemorySize = -1 {product} bool MaxFDLimit = true {product} uintx MaxGCMinorPauseMillis = 4294967295 {product} uintx MaxGCPauseMillis = 4294967295 {product} uintx MaxHeapFreeRatio = 70 {product} uintx MaxHeapSize := 268435456 {product} intx MaxInlineLevel = 9 {product} intx MaxInlineSize = 35 {product} intx MaxJavaStackTraceDepth = 1024 {product} uintx MaxLiveObjectEvacuationRatio = 100 {product} uintx MaxNewSize = 4294967295 {product} uintx MaxPermHeapExpansion = 4194304 {product} uintx MaxPermSize = 67108864 {pd product} uint64_t MaxRAM = 1073741824 {pd product} uintx MaxRAMFraction = 4 {product} intx MaxRecursiveInlineLevel = 1 {product} intx MaxTenuringThreshold = 15 {product} intx MaxTrivialSize = 6 {product} bool MethodFlushing = true {product} intx MinCodeCacheFlushingInterval = 30 {product} uintx MinHeapDeltaBytes = 131072 {product} uintx MinHeapFreeRatio = 40 {product} intx MinInliningThreshold = 250 {product} uintx MinPermHeapExpansion = 262144 {product} uintx MinRAMFraction = 2 {product} uintx MinSurvivorRatio = 3 {product} uintx MinTLABSize = 2048 {product} intx MonitorBound = 0 {product} bool MonitorInUseLists = false {product} bool MustCallLoadClassInternal = false {product} intx NUMAChunkResizeWeight = 20 {product} intx NUMAPageScanRate = 256 {product} intx NUMASpaceResizeRate = 1073741824 {product} bool NUMAStats = false {product} intx NativeMonitorFlags = 0 {product} intx NativeMonitorSpinLimit = 20 {product} intx NativeMonitorTimeout = -1 {product} bool NeedsDeoptSuspend = false {pd product} bool NeverActAsServerClassMachine = true {pd product} bool NeverTenure = false {product} intx NewRatio = 2 {product} uintx NewSize = 1048576 {product} uintx NewSizeThreadIncrease = 4096 {pd product} intx NmethodSweepFraction = 4 {product} uintx OldPLABSize = 1024 {product} uintx OldPLABWeight = 50 {product} uintx OldSize = 4194304 {product} bool OmitStackTraceInFastThrow = true {product} ccstrlist OnError = {product} ccstrlist OnOutOfMemoryError = {product} intx OnStackReplacePercentage = 933 {pd product} uintx PLABWeight = 75 {product} bool PSChunkLargeArrays = true {product} intx ParGCArrayScanChunk = 50 {product} uintx ParGCDesiredObjsFromOverflowList = 20 {product} bool ParGCTrimOverflow = true {product} bool ParGCUseLocalOverflow = false {product} intx ParallelGCBufferWastePct = 10 {product} bool ParallelGCRetainPLAB = true {product} uintx ParallelGCThreads = 0 {product} bool ParallelGCVerbose = false {product} uintx ParallelOldDeadWoodLimiterMean = 50 {product} uintx ParallelOldDeadWoodLimiterStdDev = 80 {product} bool ParallelRefProcBalancingEnabled = true {product} bool ParallelRefProcEnabled = false {product} uintx PausePadding = 1 {product} intx PerBytecodeRecompilationCutoff = 200 {product} intx PerBytecodeTrapLimit = 4 {product} intx PerMethodRecompilationCutoff = 400 {product} intx PerMethodTrapLimit = 100 {product} bool PerfAllowAtExitRegistration = false {product} bool PerfBypassFileSystemCheck = false {product} intx PerfDataMemorySize = 32768 {product} intx PerfDataSamplingInterval = 50 {product} ccstr PerfDataSaveFile = {product} bool PerfDataSaveToFile = false {product} bool PerfDisableSharedMem = false {product} intx PerfMaxStringConstLength = 1024 {product} uintx PermGenPadding = 3 {product} uintx PermMarkSweepDeadRatio = 20 {product} uintx PermSize = 12582912 {pd product} bool PostSpinYield = true {product} intx PreBlockSpin = 10 {product} intx PreInflateSpin = 10 {pd product} bool PreSpinYield = false {product} bool PreferInterpreterNativeStubs = false {pd product} intx PrefetchCopyIntervalInBytes = -1 {product} intx PrefetchFieldsAhead = -1 {product} intx PrefetchScanIntervalInBytes = -1 {product} bool PreserveAllAnnotations = false {product} uintx PreserveMarkStackSize = 1024 {product} uintx PretenureSizeThreshold = 0 {product} bool PrintAdaptiveSizePolicy = false {product} bool PrintCMSInitiationStatistics = false {product} intx PrintCMSStatistics = 0 {product} bool PrintClassHistogram = false {manageable} bool PrintClassHistogramAfterFullGC = false {manageable} bool PrintClassHistogramBeforeFullGC = false {manageable} bool PrintCommandLineFlags = false {product} bool PrintCompilation = false {product} bool PrintConcurrentLocks = false {manageable} intx PrintFLSCensus = 0 {product} intx PrintFLSStatistics = 0 {product} bool PrintFlagsFinal := true {product} bool PrintFlagsInitial = false {product} bool PrintGC = false {manageable} bool PrintGCApplicationConcurrentTime = false {product} bool PrintGCApplicationStoppedTime = false {product} bool PrintGCDateStamps = false {manageable} bool PrintGCDetails = false {manageable} bool PrintGCTaskTimeStamps = false {product} bool PrintGCTimeStamps = false {manageable} bool PrintHeapAtGC = false {product rw} bool PrintHeapAtGCExtended = false {product rw} bool PrintHeapAtSIGBREAK = true {product} bool PrintJNIGCStalls = false {product} bool PrintJNIResolving = false {product} bool PrintOldPLAB = false {product} bool PrintPLAB = false {product} bool PrintParallelOldGCPhaseTimes = false {product} bool PrintPromotionFailure = false {product} bool PrintReferenceGC = false {product} bool PrintRevisitStats = false {product} bool PrintSafepointStatistics = false {product} intx PrintSafepointStatisticsCount = 300 {product} intx PrintSafepointStatisticsTimeout = -1 {product} bool PrintSharedSpaces = false {product} bool PrintTLAB = false {product} bool PrintTenuringDistribution = false {product} bool PrintVMOptions = false {product} bool PrintVMQWaitTime = false {product} uintx ProcessDistributionStride = 4 {product} bool ProfileInterpreter = false {pd product} bool ProfileIntervals = false {product} intx ProfileIntervalsTicks = 100 {product} intx ProfileMaturityPercentage = 20 {product} bool ProfileVM = false {product} bool ProfilerPrintByteCodeStatistics = false {product} bool ProfilerRecordPC = false {product} uintx PromotedPadding = 3 {product} intx QueuedAllocationWarningCount = 0 {product} bool RangeCheckElimination = true {product} intx ReadPrefetchInstr = 0 {product} intx ReadSpinIterations = 100 {product} bool ReduceSignalUsage = false {product} intx RefDiscoveryPolicy = 0 {product} bool ReflectionWrapResolutionErrors = true {product} bool RegisterFinalizersAtInit = true {product} bool RelaxAccessControlCheck = false {product} bool RequireSharedSpaces = false {product} uintx ReservedCodeCacheSize = 33554432 {pd product} bool ResizeOldPLAB = true {product} bool ResizePLAB = true {product} bool ResizeTLAB = true {pd product} bool RestoreMXCSROnJNICalls = false {product} bool RewriteBytecodes = false {pd product} bool RewriteFrequentPairs = false {pd product} intx SafepointPollOffset = 256 {C1 pd product} intx SafepointSpinBeforeYield = 2000 {product} bool SafepointTimeout = false {product} intx SafepointTimeoutDelay = 10000 {product} bool ScavengeBeforeFullGC = true {product} intx SelfDestructTimer = 0 {product} uintx SharedDummyBlockSize = 536870912 {product} uintx SharedMiscCodeSize = 4194304 {product} uintx SharedMiscDataSize = 4194304 {product} uintx SharedReadOnlySize = 10485760 {product} uintx SharedReadWriteSize = 12582912 {product} bool ShowMessageBoxOnError = false {product} intx SoftRefLRUPolicyMSPerMB = 1000 {product} bool SplitIfBlocks = true {product} intx StackRedPages = 1 {pd product} intx StackShadowPages = 3 {pd product} bool StackTraceInThrowable = true {product} intx StackYellowPages = 2 {pd product} bool StartAttachListener = false {product} intx StarvationMonitorInterval = 200 {product} bool StressLdcRewrite = false {product} bool StressTieredRuntime = false {product} bool SuppressFatalErrorMessage = false {product} uintx SurvivorPadding = 3 {product} intx SurvivorRatio = 8 {product} intx SuspendRetryCount = 50 {product} intx SuspendRetryDelay = 5 {product} intx SyncFlags = 0 {product} ccstr SyncKnobs = {product} intx SyncVerbose = 0 {product} uintx TLABAllocationWeight = 35 {product} uintx TLABRefillWasteFraction = 64 {product} uintx TLABSize = 0 {product} bool TLABStats = true {product} uintx TLABWasteIncrement = 4 {product} uintx TLABWasteTargetPercent = 1 {product} bool TaggedStackInterpreter = false {product} intx TargetPLABWastePct = 10 {product} intx TargetSurvivorRatio = 50 {product} uintx TenuredGenerationSizeIncrement = 20 {product} uintx TenuredGenerationSizeSupplement = 80 {product} uintx TenuredGenerationSizeSupplementDecay = 2 {product} intx ThreadPriorityPolicy = 0 {product} bool ThreadPriorityVerbose = false {product} uintx ThreadSafetyMargin = 52428800 {product} intx ThreadStackSize = 0 {pd product} uintx ThresholdTolerance = 10 {product} intx Tier1BytecodeLimit = 10 {product} bool Tier1OptimizeVirtualCallProfiling = true {C1 product} bool Tier1ProfileBranches = true {C1 product} bool Tier1ProfileCalls = true {C1 product} bool Tier1ProfileCheckcasts = true {C1 product} bool Tier1ProfileInlinedCalls = true {C1 product} bool Tier1ProfileVirtualCalls = true {C1 product} bool Tier1UpdateMethodData = false {product} intx Tier2BackEdgeThreshold = 100000 {pd product} intx Tier2CompileThreshold = 1500 {pd product} intx Tier3BackEdgeThreshold = 100000 {pd product} intx Tier3CompileThreshold = 2500 {pd product} intx Tier4BackEdgeThreshold = 100000 {pd product} intx Tier4CompileThreshold = 4500 {pd product} bool TieredCompilation = false {pd product} bool TimeLinearScan = false {C1 product} bool TraceBiasedLocking = false {product} bool TraceClassLoading = false {product rw} bool TraceClassLoadingPreorder = false {product} bool TraceClassResolution = false {product} bool TraceClassUnloading = false {product rw} bool TraceGen0Time = false {product} bool TraceGen1Time = false {product} ccstr TraceJVMTI = {product} bool TraceLoaderConstraints = false {product rw} bool TraceMonitorInflation = false {product} bool TraceParallelOldGCTasks = false {product} intx TraceRedefineClasses = 0 {product} bool TraceSafepointCleanupTime = false {product} bool TraceSuspendWaitFailures = false {product} intx TypeProfileMajorReceiverPercent = 90 {product} intx TypeProfileWidth = 2 {product} intx UnguardOnExecutionViolation = 0 {product} bool Use486InstrsOnly = false {product} bool UseAdaptiveGCBoundary = false {product} bool UseAdaptiveGenerationSizePolicyAtMajorCollection = true {p bool UseAdaptiveGenerationSizePolicyAtMinorCollection = true {p bool UseAdaptiveNUMAChunkSizing = true {product} bool UseAdaptiveSizeDecayMajorGCCost = true {product} bool UseAdaptiveSizePolicy = true {product} bool UseAdaptiveSizePolicyFootprintGoal = true {product} bool UseAdaptiveSizePolicyWithSystemGC = false {product} bool UseAddressNop = false {product} bool UseAltSigs = false {product} bool UseAutoGCSelectPolicy = false {product} bool UseBiasedLocking = true {product} bool UseBoundThreads = true {product} bool UseCMSBestFit = true {product} bool UseCMSCollectionPassing = true {product} bool UseCMSCompactAtFullCollection = true {product} bool UseCMSInitiatingOccupancyOnly = false {product} bool UseCodeCacheFlushing = false {product} bool UseCompiler = true {product} bool UseCompilerSafepoints = true {product} bool UseConcMarkSweepGC = false {product} bool UseCountLeadingZerosInstruction = false {product} bool UseCounterDecay = true {product} bool UseDepthFirstScavengeOrder = true {product} bool UseFastAccessorMethods = true {product} bool UseFastEmptyMethods = true {product} bool UseFastJNIAccessors = true {product} bool UseG1GC = false {product} bool UseGCOverheadLimit = true {product} bool UseGCTaskAffinity = false {product} bool UseHeavyMonitors = false {product} bool UseInlineCaches = true {product} bool UseInterpreter = true {product} bool UseLWPSynchronization = true {product} bool UseLargePages = false {pd product} bool UseLargePagesIndividualAllocation := false {pd product} bool UseLoopCounter = true {product} bool UseMaximumCompactionOnSystemGC = true {product} bool UseMembar = false {product} bool UseNUMA = false {product} bool UseNewFeature1 = false {C1 product} bool UseNewFeature2 = false {C1 product} bool UseNewFeature3 = false {C1 product} bool UseNewFeature4 = false {C1 product} bool UseNewLongLShift = false {product} bool UseNiagaraInstrs = false {product} bool UseOSErrorReporting = false {pd product} bool UseOnStackReplacement = true {pd product} bool UsePSAdaptiveSurvivorSizePolicy = true {product} bool UseParNewGC = false {product} bool UseParallelDensePrefixUpdate = true {product} bool UseParallelGC = false {product} bool UseParallelOldGC = false {product} bool UseParallelOldGCCompacting = true {product} bool UseParallelOldGCDensePrefix = true {product} bool UsePerfData = true {product} bool UsePopCountInstruction = false {product} intx UseSSE = 99 {product} bool UseSSE42Intrinsics = false {product} bool UseSerialGC = false {product} bool UseSharedSpaces = true {product} bool UseSignalChaining = true {product} bool UseSpinning = false {product} bool UseSplitVerifier = true {product} bool UseStoreImmI16 = true {product} bool UseStringCache = false {product} bool UseTLAB = true {pd product} bool UseThreadPriorities = true {pd product} bool UseTypeProfile = true {product} bool UseUTCFileTimestamp = true {product} bool UseUnalignedLoadStores = false {product} bool UseVMInterruptibleIO = true {product} bool UseVectoredExceptions = false {pd product} bool UseXMMForArrayCopy = false {product} bool UseXmmI2D = false {product} bool UseXmmI2F = false {product} bool UseXmmLoadAndClearUpper = true {product} bool UseXmmRegToRegMoveAll = false {product} bool VMThreadHintNoPreempt = false {product} intx VMThreadPriority = -1 {product} intx VMThreadStackSize = 0 {pd product} intx ValueMapInitialSize = 11 {C1 product} intx ValueMapMaxLoopSize = 8 {C1 product} bool VerifyMergedCPBytecodes = true {product} intx WorkAroundNPTLTimedWaitHang = 1 {product} uintx YoungGenerationSizeIncrement = 20 {product} uintx YoungGenerationSizeSupplement = 80 {product} uintx YoungGenerationSizeSupplementDecay = 8 {product} uintx YoungPLABSize = 4096 {product} bool ZeroTLAB = false {product} intx hashCode = 0 {product}
在前面的文章中,说了很多JVM和数据库方面的东西,我所描述的内容大多偏重于技术本身,和实际的业务系统结合的比较少,本文开始进入实际的系统设计中应当注意的方方面面(文章偏重于访问量高,但是每次访问量并不是很大的系统),而偏重点在于性能和效率本身,由于这个知识涉及的基础和面很广,所以建议是先看下以前写的内容或自己有一定的基础来才开始接触比较好,另外本文也不能诠释性能的关键,从一个应用系统前端到后端涉及的部分非常多,本文也只会说明其中一部分,后续的部分我们再继续说;下面我们想一下一个web应用绝大部分请求的整个过程:client发出请求->server开始响应并创建请求对象及反馈对象->如果没有用户对象就创建session信息->调用业务代码->业务代码分层组织数据->调用数据(从某个远程或数据库或文件等)->开始组织输出数据->反馈数据开始通过模板引擎进行渲染->渲染完成未静态文件向客户端进行输出->待客户端接收完成结束掉请求对象(这种请求针对短连接,长连接有所区别)。 就从前端说起吧,说下一下几个内容: 1、线程数量 2、内容输出 3、线程上下文切换 4、内存 1.首先说下线程数量,线程数量很多人认为在配置服务器的线程数量时认为越多越好,各大网站上很多人也给出了自己的测试数据,也有人说了每个CPU配置多少线程为合适(比如有人说过每个CPU给25个线程较为合适),但是没有一个明确的为什么,其实这个要和CPU本身的运行效率和上来说明,并非一概而论的,也需要考虑每个请求所持有的CPU开销大小以及其处于非Running状态的时间来说明,线程配置得过多,其实往往会形成CPU的征用调度问题,要比较恰当将CPU用满才是性能的最佳状态(说到线程就不得不说下CPU,因为线程就是消耗CPU的,其本身持有的内存片段非常小,前面文章已经说明了它的内存使用情况,所以我们主要是讨论它与CPU之间的关系)。 首先内存到CPU的延迟在几十纳秒,虽然CPU内部的三级缓存比这个更加小,但是几乎对于我们所能识别的时间来讲可以被忽略;另外内存与CPU之间的带宽也是以最少几百M每秒的速度通信,所以对于内存与CPU交互数据的时间开销对于常规的高并发小请求的应用客户忽略掉,我们只计算本身的计算延迟开销以及非计算的等待开销,这些都一般会用毫秒来计算,相互之间是用10e6的级别来衡量,所以前者可以忽略,我们可以认为处于running的时间就是CPU实际执行的时间,因为这种短暂的时间也很难监控出来到底用了多久。 那么首先可以将线程的运行状态划分为两大类,就是:运行与等待,我们不考虑被释放的情况,因为线程池一般不会释放线程,至于等待有很多种,我们都认为它是等待就可以了;为什么是这两种呢,这两种正好对应了CPU是否在被使用,running状态的线程就在持有CPU的占用,等待的就处于没有使用CPU。 再明确一个概念,一个常规的web请求,后台对应一个线程对它的请求进行处理,同一个线程在同一个时间片上只能请求一个CPU为他进行处理,也就是说我们可以认为它不论请求过多少次CPU、不论请求了多少个CPU,只要这些CPU的型号是一样的,我们就可以认为它是请求的一个CPU(注意这里的CPU不包含多个core的情况,因为多个core的CPU只能说明这个CPU的处理速度可以接近于多个CPU的速度,而真正对线程的请求来讲,它认为这是一个CPU,在主板上也是一个插槽,所以计算CPU的时候不考虑多核心)。 最后明确线程在什么情况下会发生等待,比如读取数据库时,数据库尚未反馈内容之前,该线程是不会占用CPU的,只会处理等待;类似的是向客户端输出、线程为了去持有锁的等待一系列的情况。 此时一个线程过来如果一个线程毫无等待(这种情况不存在,只是一种假设),不论它处理多久,处理时间长度多长,此时如果只有一个CPU,那么这个应用服务器只需要一个1个线程就足以支撑,因为线程没有等待,那么CPU就没有停止运行,1个线程处理完这个请求后,接着就处理下一个请求,CPU一直是满的,也几乎没有太大的征用,此时1个线程就是最佳的,如果是多个同型号的CPU,那么就是CPU数量的线程是最佳的;不过这个例子比较极端,在很多类似的情况下,大家喜欢用CPU+1或CPU-1来完成对类似情况的线程设置,为了保证一些特殊情况的发生。 那么考虑下实际的情况,如果有等待,这个等待不是锁等待的(因为锁等待有瓶颈,瓶颈在于CPU的个数对于他们无效),应该如何考虑呢?我们此时来考虑下这个等待的时间长度应该如何去考虑,假如等待的时间长度为100ms,而运行的时间长度为10ms,那么在等待的这100ms中,就可以有另外10个线程进来,对CPU进行占用,也就是说对于单个CPU来说,11个线程就可以占满整个CPU的使用,如果是多个CPU当然在理论上可以乘以CPU的个数,这里再次强调,这里的CPU个数是物理的,而不算多核,多核在这里的意义比如以前一个CPU处理一个线程需要30ms,现在采用4个core,只需要处理10ms了,在这里体现了速度,所以计算是不要用它来计算。 那么对于锁等待呢?这个有点麻烦了,因为这个和模块有关系,这里也只能说明某个有锁等待的模块要达到最佳状态的访问效率可以配置的线程数,首先要明确锁等待已经没有CPU个数的概念,不论多少个CPU,只要运行到这段代码,他们就是一个CPU,不然锁就没有存在的意义了;另外,假如访问是非常密集的,那么当某个线程持有锁并访问的时候,其他没有得到的运行到这个位置都会处于等待,我们将一个模块的所有有锁等待的时间集中在一起,只有当前一个线程将具有锁的这段代码运行完成后,下一个线程才可以继续运行,所以它其他地方都没有瓶颈,或者说其他地方理论配置的线程数都会很高,唯独遇到这个地方就会很慢,假如一个线程从运行代码时长为20ms,等待事件为100ms,锁等待为20ms,此时假如该线程没有受到任何等待就是140ms即可运行完成,而当多个线程同时并发到这里的时候,后续每个线程将会等待20*N的时间长度,当有7个线程的时候,恰好排满运行的队列,也就是当又7个线程访问这个模块的时候,理论上刚好达到每个线程顺序执行而且成流水线状态,但是这里不能乘以CPU的个数了,为什么,你懂的。 2.内容输出,其实内容输出有很多种方法,在java方面,你可以自己编写OutputStream或者PrintWriter去输出,也可以用渲染模板去渲染输出,渲染的模板也有很多,最常见的就是JSP模板来渲染,也有velocity等各种各样的渲染模板,当然对于页面来讲只能用渲染模板去做,不过异步请求你可以选择,在选择时要对应选择才能将效果做得比较好。 说到这里不得不说下velocity这个东西,也就是经常看到的vm的文件,这种文件和JSP一样都是渲染模板的方法,只是语法格式有所区别,velocity是新出来的东西,很多人认为新的东西肯定很好,其实velocity是渲染效率很低的,在内容较小的输出上对性能进行压力测试,其单位时间内所能承受的访问量,比JSP渲染模板要低好几倍,不过对较大的数据输出和JSP差不多,也就是页面输出使用velocity无所谓的,而且效果比JSP要好,但是类似ajax交互中的小数据输出建议不要使用vm模板引擎,使用JSP模板引擎甚至于直接输出是最佳的方式。 说到这里JSP模板引擎在输出时是会被预先编译为java的class文件,VM是解释执行的,所以小文件两者性能差距很大,当遇到大数据输出时,其实大部分时间在输出文件的过程中,解释时间几乎就可以被忽略掉了。 那么JSP输出小文件是不是最快的呢?未必,JSP的输出其实是将JSP页面的内容组成字符串,最终使用PrintWriter流取完成,中间跳转交互其实还是蛮多的,而且有部分容器在组装字符串的时候竟然用+,这个让我很是郁闷啊,所以很多时候小数据的输出,我还是喜欢自己写,经过测试得到的结果是使用OutputStream的性能将会比PrintWriter高一些,(至于高多少,大家可以自己用工具或写代码测试下就知道了,这里可能单个处理速度几乎看不出区别,要并发访问看下平均每秒能处理的请求数就会有区别了),字符集方面,在获取要输出内容的时候,指定byte的字符集,如:String.getByte(“字符集”),一般这类输出也不会有表头,只需要和接收方或者叫浏览器一致就可以了(有些接收方可能是请求方);其实OutputStream比PrintWriter快速的原因很简单,在底层运行和传输的过程中,始终采用二进制流来完成,即使是字符也需要转换成byte格式,在转换前,它需要去寻找很多的字符集关系,最终定位到应该如何去转换,内部代码看过一下就明白,内部的方法调用非常多,一层套一层,相应占用的CPU开销也会升高。 总结起来说,如果你有vm模板引擎,在页面请求时建议使用vm模板引擎来做,因为代码要规范一些,而且也很好用;另外如果在简单的ajax请求,返回数据较小的情况下,建议使用OutputStream直接输出,这个输出可以放在你的BaseAction的中,对实现类中是透明的,实现类只需要将处理的反馈结果数据放在一个地方,由父类完成统一的输出即可,此处将Ajax类的调用可以独立一个父亲类出来,这样继承后就不用关心细节了。 输出中文件和大数据将是一个问题,对于文件来说,尤其是大文件,在前面文章已经说明,输出时压缩只能节省服务器输出时和客户端的流量,从而提高下载速度,但是绝对不会提高服务器端的性能,因为服务器端是通过消耗CPU去做动作,而且压缩的这个过程是需要时间的,这种只会降低速度,而绝对不会提高;那么大文件的方法就是一种是将大文件提前压缩好存放,如果实在太大,需要考虑采用断点传送,并将文件分解。 对大数据来讲,和文件类似,不过数据可能对我们要好处理一点,需要控制访问频率甚至于直接在超过访问频率下拒绝访问请求,每次请求的量也需要控制,如果对特殊大的数据量,建议采用异步方式输出到文件并压缩后,再由客户端下载,这样不论是客户端还是服务器端都是有好处的。 3、线程上下文切换,对于线程的上下文切换,在一般的系统中基本遇不到,不过一些特殊应用会遇到,比如刚才的异步导出的功能,请求的线程只是将事情提交上去,但是不是由它去下载,而是由其他线程再去处理这个问题,处理完成后再回写某个状态即可;在javaNIO中是非常的多,NIO是一种高性能服务器的解决方案,在有限的线程资源情况下,对极高并发的小请求,并存在很多推拉数据的情况下是很有效的,最大的要求就是服务器要有较好的连接支撑能力,NIO细节不用多说,理解上就是异步IO,把事情交给异步的一个线程去做,但是它也未必马上做,它做完再反馈,这段时间交给你的这个线程不是等待而是去做其他的事情,充分利用线程的资源,处理完反馈结果的线程也未必是开始请求的线程,几个来来回回是有很多的开销的,总体其实效率上未必有单个请求好,但是对服务器的性能发挥是非常有效的。 线程之间的开销大小也要看具体应用情况以及配置情况决定,此时将任务和线程没有做一个一对一的绑定,而是放一堆事情在队列中,处理线程也有很多,谁有时间处理谁就处理它,每个线程都做自己这一类的事情,甚至于将一些内容交给远程去做,交互后就不管了,结果反馈的时候,这边再由一个线程去处理结果请求即可。 在整个过程中会涉及到一次或多次的线程切换,这个过程中的开销在某些时候也是不小的,关键还是要看应用场景,不能一概而论。 4、内存,最后还是内存,其实这里我就不想多说了,因为前面几篇文章说得太多了,不论是理论上还是实现上,以及经验上都说了非常多,不过可以说明的一点就是内存的问题绝大部分来源于代码,而代码有很大一部分可能性来源于工程的程序员编写或者框架,第三方包的内存问题相对较少,一般被开源出来的包内存溢出的可能性不大,但是不排除有写得比较烂的代码;二方包呢,一般指代公司内部人员封装的包,如果在经过很多项目的验证可以比较放心使用,要绝对放心的话还是需要看看源码才行,至于JVM本身的BUG一般不要找到这个上面来,虽然也有这种可能性,不过这种问题除了升级JVM外也没有太多的办法,修改它的源码的可能性不大,除非你真的太厉害了(这里在内存上一般是指C或C++语言的源码,java部分的基础类包这些代码如果真的有问题,还是比较容易修改的,但还是不建议自己刻意去修改,除非你能肯定有你更好的解决方案而且是稳定有效的);在编写代码的时候将那些可以提前做的事情做了(比如这个事情以后会反复做,重复做,而且都是一样的,那么可以提前做一次,以后就不用做了),那些逻辑是可以省掉的,最后是如果你的应用很特殊是不是更好的解决方案和算法来完成。 总结下,从今天提到的系统设计的角度来说,影响QPS的最关键的东西就是模板渲染,它会占据请求的很大一部分时间,而且这个东西可以做非常大的改进,比如:压缩空白字符、重复对象的简化和模板化、大数据和重复信息的CSS化、尽量将输出转化为网络可以直接接受的内容;而其次就是如何配置线程,配置得太少,CPU的开销一直处于一种比较闲的状态,而配置得太多,CPU的征用情况比较严重,没有建议值,只要最适合应用场景的值,不过你的代码如果没有太多的同步,线程最少应该设置为CPU的格式+1或-1个;上下文切换对常规应用一般不要使用,对特殊的应用要注意中间的切换开销应该如何降低;文件输出上讲提前做的压缩提前做掉,注意控制访问频率和单次输出量;最后内存上多多注意代码,配置上只需要控制好常规的几个参数,其余的在没有特殊情况不要修改默认的配置。 扩展,那么关于一个系统的架构中是不是就这么一点就完了呢,当然不是,这应该说说出了一个常见的OLTP系统的一些常见的性能指标,但是还有很内容,比如:缓存、宕机类异常处理、session切换、IO、数据库、分布式、集群等都是这方面的关键内容,尤其是IO也是当今系统中性能瓶颈的最主要原因之一;在后续的文章中会逐步说明一些相关的解决方案。
本来写完前面两篇JVM,已经不再想写这类似的东西,因为很多知识点很难吃透,即使写出来也很难让人理解,即使理解还不如看官方资料,不过还是鼓起勇气写下这篇文章,本文主要是demo去理解一些JVM的内存知识,版本为hotspot的1.6.24版本,不过本文不讲指令,只是模拟一些东西,类似于出题目,和大家一起来做下;本文几个简单实验不能说明所有问题,仅仅是分享一下理解JVM的内在和一些不可告人的秘密,以及告诉分享一些方法,以后可以自己做实验去扩展。 1、首先来模拟一个简单的溢出,代码很简单,我们也把GC的过程拿出来看看: import java.util.*; public class Hello { private final static int BYTE_SIZE = 4 * 1024 * 1024; public static void main(String []args) { List <Object> List = new ArrayList<Object>(); for(int i = 0 ; i < 10 ; i ++) { List.add(new byte[BYTE_SIZE]); System.out.println(i); } } } 我们采用下面的命令运行一下: C:\>javac Hello.java C:\>java -Xmn4m -Xms20m -Xmx20m -XX:+PrintGCDetails Hello 0 1 2 [GC [DefNew: 266K->145K(3712K), 0.0012704 secs][Tenured: 12288K->12433K(16384K), 0.0078754 secs] 12554K->12433K(20096K), [Perm : 367K->367K(12288K)], 0.0097094 .01 secs] [Full GC [Tenured: 12433K->12420K(16384K), 0.0081298 secs] 12433K->12420K(20096K), [Perm : 367K->362K(12288K)], 0.0085821 secs] [Times: user=0.02 sys=0.00, rea Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at Hello.main(Hello.java:10) Heap def new generation total 3712K, used 133K [0x32690000, 0x32a90000, 0x32a90000) eden space 3328K, 4% used [0x32690000, 0x326b1500, 0x329d0000) from space 384K, 0% used [0x32a30000, 0x32a30000, 0x32a90000) to space 384K, 0% used [0x329d0000, 0x329d0000, 0x32a30000) tenured generation total 16384K, used 12420K [0x32a90000, 0x33a90000, 0x33a90000) the space 16384K, 75% used [0x32a90000, 0x336b1338, 0x336b1400, 0x33a90000) compacting perm gen total 12288K, used 362K [0x33a90000, 0x34690000, 0x37a90000) the space 12288K, 2% used [0x33a90000, 0x33aea960, 0x33aeaa00, 0x34690000) ro space 10240K, 54% used [0x37a90000, 0x3800c510, 0x3800c600, 0x38490000) rw space 12288K, 55% used [0x38490000, 0x38b2fb78, 0x38b2fc00, 0x39090000) 看到打印语句中,在输出2以后,也就是在下标3还没有输出来的时候(第四次),就出现了GC,也就是其实Eden始终是放不下4M的空间,而总的Heap只有20M,所以Tenured就是16M,当第4次放入4M内容到Tenured时,发现放不下,但是也回收不掉,所以就内存溢出了;我们看懂了为什么,但是奇怪的事情发生了,其实这个时候应该不需要前面的Young的GC,你看到它先发生了一次,这个算什么呢,这个算是一个BUG哈,不过知道就OK了,不算是什么大问题,后来在服务器端的一些回收就没有这个问题,不过有一些其他的问题,呵呵。 此时我们通过jps查看下进程号,然后通过,jstat跟踪下回收的过程,为了能够实时的跟踪到代码,在代码申请内存前(记住是在每次做new空间之前,也就是循环体前面),可以将其做一定时间的延时处理,代码细节就不多说了,一下为给出监控命令得到的结果: C:\>jstat -gcutil 5024 1000 100 S0 S1 E O P YGC YGCT FGC FGCT GCT 0.00 0.00 8.01 0.00 2.99 0 0.000 0 0.000 0.000 0.00 0.00 8.01 25.00 2.99 0 0.000 0 0.000 0.000 0.00 0.00 8.01 50.00 2.99 0 0.000 0 0.000 0.000 0.00 0.00 4.00 75.81 2.95 1 0.004 2 0.022 0.025 大致得出的结果如上所示,可以看出,Eden区域的空间几乎没有什么变化,因为它本身就放不下东西,不过为什么还是有一些空间呢,因为程序本身的一些开销的一些内容会放在这里,而O指代Old区域,其实就是Tenured,这也是非常原始的说法,可以看到它随着时间的偏移,内存使用按照比例上升,而比例上也是我们预期的,但是奇怪的事情发生了,就是YGC做了一次,而FGC做了两次下面我先解释下这些参数后再说明这个问题: 在前序的文章中已经说明目前绝大部分的hotspot JVM,都是用Eden+S0+S1+Old组成了Heap区域,以及一个Perm组成JVM所管辖区域,当然还包含native的内存,通过jstat命令可以查看到非native的区域,也就是堆栈的内存监控,更多的监控工具在本文中不做介绍。 上述关键字E代表Eden、O就是代表Old、P代表Perm、YGC就是Yong区域发生的GC次数、YGCT就是发生GC的时间开销、FGC就是Full GC的次数,GCT就是FGCT就是Full GC的时间开销,GCT就是总体的GC时间开销延迟,注意:所谓时间开销延迟就是指影响应用业务运行的延迟动作,这段时间,这部分内存是无法工作的,但是YGC仅仅影响Yong区域的内存而已。 jstat -gcutil 5024 1000 100这个命令,前两个不用多说,可以携带的参数可以使用jstat –help进行查看,而5024为查看到的进程号,在Linux下可以直接通过动态参数赋值进去即可,而1000代表每秒采集一次数据,最后100代表最多采集100次,如果对应的进程结束,采集也会结束。 再来解释下GC的情况: 上面看到的DefNew、Tenured这些个关键字,其实就是代码的名称,从这里就可以看出来我使用的GC是最原始的GC方法,串行而且需要等待的一种GC方法,当你使用各种GC的模式的时候会发现每种GC输出的日志是不一样的,而开头就是他们的代码类的类名(本文后面会介绍并行GC);而类似于这种数据:[Tenured: 12288K->12433K(16384K), 0.0078754 secs]应该如何看呢?这里代表Tenured这个类对Old区域进行回收,回收前的内存使用是:12288K,回收后的内存是:12433K,Old区域的总大小为:16384K,本次回收对应用的延迟为0.0078754秒,发现回收的内存非常少,因为这些内存本身就没有得到释放,但是也回收了一点点,因为程序为了配合测试本身就有一些开销在里面。 这是一种非常原始的GC,为什么发生了一次YONG GC,而且发生了两次FULL GC,理论上Yong区域不会有内存进去,不会有任何东西在里面,所以不会发生任何的YGC才对,而且应该只有一次Full GC,但是奇怪的事情发生了,最后得出的结论是这种GC机制是非常原始的,这是GC这段代码本身存在的BUG,也算是一种十分悲观的一种做法。 付:其实你在监控中如果时间控制得不是很好的话,有可能最后这条信息采集不到,因为内存溢出的时候,进程就会结束,进程结束,采集的jstat程序也采集不到内容了,所以这个模拟在时间上要把控得比较好才可以。 2、如果你第一个实验看得很明白我说的是什么,那么我们来看看第二个实验,我们将GC的方法改成比较老的并行GC方法,但是代码也稍微修改下: import java.util.*; public class Hello { private final static int BYTE_SIZE = 3 * 1024 * 1024; public static void main(String []args) { List <Object> List = new ArrayList<Object>(); for(int i = 0 ; i < 9 ; i ++) { listInfo.add(new byte[BYTE_SIZE]); if(i == 6) { listInfo.clear(); } sleepTime(1000); } } } 注意看下,这里循环10次,每次申请的内存变成3M,主要为了测试方便,另外sleepTime方法是我自己写的一个方法用于当前线程休眠,大家可以写一个方法来完成,代码很简单,这里就不给出源码了。 我们这里测试时为了方便,把Yong区域设置为10M,按照默认的话,Eden就会是8M,也就是最多装入2次循环的申请,也就是每两次后就让它晋升到Old区域,每次晋升6M。而Old区域我们设置为20M,也就是晋升3次后,就是18M了,此时i=6,我们将Old区域的内存清理掉,然后再申请内存看下有什么事情发生。本次测试用ParallelGC来完成,直接启用-server也是默认启动该参数。 运行时命令如下: C:\>java -Xmn10m -Xms30m -Xmx30m -XX:+UseParallelOldGC -XX:+PrintGCDetails Hello [GC [PSYoungGen: 6468K->176K(8960K)] 6468K->6320K(29440K), 0.0052248 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [PSYoungGen: 6400K->160K(8960K)] 12544K->12448K(29440K), 0.0056272 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [PSYoungGen: 6357K->160K(8960K)] 18645K->18592K(29440K), 0.0051462 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC [PSYoungGen: 160K->0K(8960K)] [PSOldGen: 18432K->18577K(20480K)] 18592K->18577K(29440K) [PSPermGen: 2081K->2081K(12288K)], 0.0064599 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [Full GC [PSYoungGen: 6178K->0K(8960K)] [PSOldGen: 18577K->3204K(20480K)] 24756K->3204K(29440K) [PSPermGen: 2081K->2081K(12288K)], 0.0067249 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] Heap PSYoungGen total 8960K, used 3225K [0x08ee0000, 0x098e0000, 0x098e0000) eden space 7680K, 42% used [0x08ee0000,0x092066e8,0x09660000) from space 1280K, 0% used [0x09660000,0x09660000,0x097a0000) to space 1280K, 0% used [0x097a0000,0x097a0000,0x098e0000) PSOldGen total 20480K, used 3204K [0x07ae0000, 0x08ee0000, 0x08ee0000) object space 20480K, 15% used [0x07ae0000,0x07e01268,0x08ee0000) PSPermGen total 12288K, used 2086K [0x03ae0000, 0x046e0000, 0x07ae0000) object space 12288K, 16% used [0x03ae0000,0x03ce9a30,0x046e0000) 此时你会发现进行了很多次GC,而GC的日志和我们第一个实验GC的日志输出不太一样,后面多出来一堆东西,而DefNew与Tenured已经不复存在,现在叫做:PSYoungGen、PSOldGen,同理你如果使用了-XX:+UseParallelOldGC打印出来的内容对Old的回首也会有所区别,你会看到ParOldGen这样的关键字出现,但是其它两个不会变化,注意:-XX:+UseParallelOldGC和-XX:+UseParallelGC两个参数不要混在一起使用,它们所调用的代码都是不一样的,另外还有CMS GC你看到的内容也是不一样的,那么我们这里阐述的关键性问题不是这个,而是在并行GC下的一个隐藏机制。 大家通过常规的笔算得到的结果应该是这样的(理论结果): i(下标) Yong Old YGC FGC 备注 0 3 0 0 0 第一次申请3M 1 6 0 0 0 第二次申请3M,Eden区域已经存放6M 2 3 6 1 0 再申请3M,发现Eden放不下(Eden只有8M默认为Yong的80%),先YGC发现Survivor区域也放不下,晋升到Old。 3 6 6 1 0 重复步骤【1】 4 3 12 2 0 重复步骤【2】 5 6 12 2 0 重复步骤【1】 6 3 18 3 0 重复步骤【2】但是执行申请后,执行了clear,Old区域的内存将全部被认为是垃圾内存,不过当前肯定还没有回收。 7 6 18 3 0 重复步骤【1】 8 3 6 3 1 重复步骤【2】不过此处由于Old区域18M内存需要回收,所以发生一次FullGC操作,循环到此结束 那么在理论上就应该发生3次YGC,1次FullGC,但是看下日志的输出,竟然发生了2次FullGC,怎么回事了呢,用jstat监控下看下: C:\ >jstat -gcutil 6088 1000 100 S0 S1 E O P YGC YGCT FGC FGCT GCT 0.00 0.00 4.22 0.00 16.88 0 0.000 0 0.000 0.000 0.00 0.00 44.22 0.00 16.88 0 0.000 0 0.000 0.000 0.00 0.00 84.22 0.00 16.88 0 0.000 0 0.000 0.000 0.00 13.13 40.00 30.00 16.94 1 0.009 0 0.000 0.009 0.00 13.13 81.05 30.00 16.94 1 0.009 0 0.000 0.009 12.50 0.00 40.00 60.00 16.94 2 0.018 0 0.000 0.018 12.50 0.00 80.69 60.00 16.94 2 0.018 0 0.000 0.018 0.00 0.00 40.00 90.71 16.93 3 0.031 1 0.018 0.049 0.00 0.00 80.45 90.71 16.93 3 0.031 1 0.018 0.049 0.00 0.00 40.00 15.65 16.89 3 0.031 2 0.028 0.059 果然发生了两次FullGC,奇怪了,为什么发生了两次FullGC,看下日志详情,在第三次刚开始发生YGC的时候,它发生了一次FullGC,后面又出现了一次,第三次YongGC理论上发生完成后,Old区域也只有18M的内存,而Old区域有20M的空间,为什么会发生这个GC呢,再回到开始Full GC的第一个日志输出看下: [Full GC [PSYoungGen: 160K->0K(8960K)] [PSOldGen: 18432K->18577K(20480K)] 18592K->18577K(29440K) [PSPermGen: 2081K->2081K(12288K)], 0.0064599 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 此时看下PSOldGen中的内存的确和我们理论值一样18M,但是它做了一个从18M回收到18M,第一次我看到觉得很无奈而且很搞笑,后来知道了一个内部一个很神奇的原理,就是平均晋升大小的问题,当每次从Yong向Old区域晋升过程中,都有一个内存大小的记录,平均记录将会影响JVM对于当前内存稳定性的判别,它发现每次从YGC中每次平均向OLD晋升大概6M,此时当第三次YGC晋升后,它发现Old只剩下2M,平均每次是6M,剩余空间已经不足以支撑平均晋升的大小了,所以应该做一次Full GC才能让下一次YGC能够成功晋升,不过我不知道JVM为什么要这样做,因为下次晋升不够再做FullGC也不迟,为什么要这个时候做呢?这样往往导致了更多的FullGC。 在GC的参数上,前面已经有一些文章说明了,其实很多时候在服务器端设置的时候,只需要有一个-server就行了,而服务器端一般这个参数也是默认的,在绝大部分情况下,不要去设置过多的参数,除非你的应用场景非常的特殊,因为JVM给的默认值很多都是正确的,不过建议将一些自适应和手动GC关闭掉是比较好的,一般关心的部分主要是-Xms、-Xmx、-Xss、-Xmn这几个参数就可以了,其余的类似并行GC线程数、平均YGC次数晋升、自适应等这些内容用默认的一般就是对的,特殊场景比如:你的系统是用来做cache类的系统和做高并发访问的系统设置就有一些区别(经常做cache的内容,如果cache住的内容几乎是长驻的,那么我想让稍微大一点的内容直接进入Old,而不要在Yong区域来回倒腾;而高并发一般请求量较小,而且请求完成后基本都释放掉了,所以一般不让他进入Old,因为Old会导致FullGC,Yong内部每次倒腾只会寻找活着的节点,如果也就是Yong内部几乎都是挂掉的节点),如果你的系统用于做大内存OS也有很大的区别(这个时候你要考虑使用并发GC即CMSGC去解决回收时间的问题,现在的G1),而四不像的系统,或者叫什么都有的系统,也就是既有一些高并发,又有自己的cache,而且cache的时间不长也不短,真的就有点郁闷了,G1的出现希望可以解决掉这个问题,不过也曾有人自己通过应用的代码来解决这个问题,当然应用也有一定的特殊性,那就是应用的数据行,每行数据大概都是1M多,而且不会超过2M,这群人是将处理的数据全部划分为2M等长度的空间片段,将数据向内部填充,然后回收时,只找寻垃圾内存,并回收掉,但是他们不会去做compaction的操作,也就是不会去做重新排序的操作,这样节省了大量的FullGC的时间,并使得内存不会出现碎片的问题。 OK,JVM内存的浪费有很多种情况,我们讨论很多就是要讨论OOM的问题,运行时最关心的问题就是GC会暂停多久的运行业务,一般在内存溢出在线上运行由于代码造成的可能性为绝大部分,少部分是因为配置引起,除非配置参数全部乱写的,而代码就要从很多方面去说明了,从java代码本身的角度来说一般有的情况: 1、大量使用session存放数据,甚至于存放List、Map这些集合类,逐步嵌套,只要session没有注销,这些集合类以及集合类所引用的对象(可能是多层引用)导致这些内存不会被GC认为是垃圾内存;这种唯一的办法就是杀掉,谁写这种代码,就把谁杀掉,曾经有人提出过独立管理这类session,甚至于存入数据库中,不过这样放数据放在什么地方都会爆掉,而且放在数据库中也会极大的降低session的提取时间,甚至于可以说根本不用session存放数据了,因为我可以自己从数据库中拿,session应当只存放一些用户关键信息,可以使用独立的分布式缓存来存放,这样保证session是可以被切换的,如果通过服务器本身的session切换的话会有很多序列化的问题存在。 2、自定义的静态句柄使用不当,我并不是不推荐大家使用这个,主要是有人经常用这个指向一些集合类而且做一些内存管理过程中会有很多集合类来回引用,本来想带的GC来回引用GC已经可以识别出来它也是垃圾,但是如果你的顶层有一个静态句柄,那么就没法了,如果你不做clear,你永远释放不掉。 3、线程broken住或者被stop等操作或者waiting的时间过长,该线程内部的栈针指向的内存片段将不会被得到释放,其实从程序效率的角度来说,就是当你的线程在等待一个网络反馈结果之前的这个时间范围内,你在这行代码之前申请的内存,都不会被认为是垃圾内存,除非你自己做过一个 = null的操作,其实 = null或clear也是在这种情况下使用会提高性能,正常情况下这种操作意义并不大。 4、由于读取数据比较多,导致网络时间较长,这个时候,也和第三种情况差不多,而且这些数据也会被转入到内存中,甚至于进入old区域,那么也是会导致急速上涨,对于这类情况,如果业务的确经常有这种情况,可以适当调整Yong区域的大小,另外代码上要注意对数据提取的流量控制,甚至于拒绝单机上的高并发,以及每次提取的数量;在必要时应当控制每次输出的流量,适当情况可以选取断点传送。 5、文件下载,文件下载和上面类似,只是它是文件而不是基本的数据,这类内容在读取和输出过程中都会占用内存很多的开销,有人说用gzip压缩,的确,这个技术的确不错,不过gzip唯一解决的问题是服务器在输出时向客户端传送的网络流量,但是它本身也是需要占用CPU的,用过压缩工具的人都知道,压缩是非常占用CPU的工具之一,所以在业务允许的情况下,在预先处理之前就将一些必要的大文件进行压缩存放,输出式也是一个压缩包,不论在内存还是向客户端输出时都是一个压缩文件。 6、大量使用ThreadLocal来作为数据存放的中间量,如果经常使用这个内容的朋友请多多看下这个类的源码到底是怎么写的,其实ThreadLocal只是一个中间量,数据根本不是存放在这个类里面的,也就是即使ThreadLocal这个类结束掉,这些数据依然存在,它是存放在当前被访问的Thread中的,Thread中有一段源码就是这样定义了对应的Map结构来存放当前线程的参数信息,而ThreadLocal只是一个中间传输信息的工具,并自动判定当前线程信息,它根本什么数据都不存放,而绝大部分应用中,Thread都是一个队列池,也就是这些线程基本都是不会被释放的,那么这些线程所对应的Map以及下面的节点内容将永远得不到释放,所以要善用这个类。 7、滥用Thread,或-Xmx设置得过大,导致没有native内存,Thread本身是占用非JVM堆栈内存之外的native内存,-Xss决定其大小,1.5以后默认是1M的大小,它用于存放当前Thread所申请的对象的栈针信息,也就是通过它找到对象的,也就是没申请一个Thread就会从OS中开销1M的内存,这样不用多说,你懂的。 8、JNI的滥用,在使用native方法,也是使用native的内存,比如去调用一些C、C++语言执行,执行完成后,在C、C++中使用自己的malloc、realloc、new等方法申请的内存空间,没有使用free或delete去释放掉,那么它将永远释放不掉,除非该JVM进程停止掉,由OS判定出来这块内存是由这个进程所使用的。 9、网络输出,在网络输出时,会产生buffer,而buffer的大小,超越了OS级别的限制,它使用的也不是堆栈中的内存,而是外部的内存,在输出时需要注意网络流量的限制。 10、其他的注意点其实并不多,代码上的细节问题说起来就太多了,前面有说明过代码细节上的一些常见注意事项,以及通过javap来查看代码被编译后的命令到底是怎么执行的方法,通过这种方法就可以看出代码为什么会执行得很慢;另外代码要跑得快除了一些常见的注意事项外,还需要注意就是如果程序等待需要考虑什么内存可以释放掉,复杂的逻辑程序什么动作可以不做或者简化做,算法方面不要纠结太多,因为常规的应用业务不会存在太复杂的算法,当你遇到的时候再去纠结也是可以的。 代码内存溢出一般要么是并发上的确是扛不住了引起,不过这种一般是大型网站才会遇到,一般的系统是不会遇到的;另一类就是代码的确太难了,就和上面的情况比较符合,很多人问我,为什么JVM不能自动识别出来回收掉这些内存呢,我只能说,java真的还需要学下才行,所谓自动并非什么东西都是自动的,首先需要明白GC认为什么是垃圾才可以,如果你有东西只想他,只要这个内存通过栈针、final static句柄、以及native的空间他们是可达的,那么它就不是垃圾内存,如果放在一些常驻内存或根本不会被释放的内存的引用下或者被间接引用的子对象下,那么JVM永远也不会认为它是垃圾,这也没有什么办法让JVM自动知道它就是垃圾。 其余的不想多说,后面专门写几篇文章说明一个java的应用工程如何从服务器的前端到后端设计上提高的访问量以及性能,由于内容较多会分解为多篇文章说明不同的部分。 最后文章推荐大家使用一些监控工具,如:jconsole、virsualVM集成插件virsualGC、jmap、jstat以及异常强大的btree工具,由于工具说起来比较多,而且每种工具都有自己的特征和优势所在,在不同的场景下发挥作用,本文也不是重点,只是推荐使用,这里就简单截图一张看看图形如何说明GC的运行状态的,这里看下:virsualVM中的virsualGC插件对内存监控的效果是什么样的(对堆栈部分的内存使用和GC状态展现的非常的清晰,不过再次强调,这部分仅仅针对于Hotspot VM,即开源版本的Oracle的JVM):
前一段写了一篇《认识JVM》,不过在一些方面可以继续阐述的,在这里继续探讨一下,本文重点在于在heap区域内部对象之间的组织关系,以及各种粒度之间的关系,以及JVM常见优化方法,文章目录如下所示: 1、回顾--java基础的对象大概有哪些特征 2、上一节中提到的Class加载是如何加载的 3、一个对象放在内存中的是如何存放的 4、调用的指令分析 5、对象宽度对其问题及空间浪费 6、指令优化 正文如下: 1、回顾--java基础的对象大概有哪些特征? 相信学习过java或者叫做面向对象的人至少能说出面向对象的三大特征:封装、继承、多态,在这里我们从另一个角度来看待问题,也就是从设计语言的角度来说,要设计一门类似于java的语言,它需要的特征是什么? ->首先所有的内容都应该当基于“类”来完成。 ->单继承特征,并且为单根继承(所有类的顶层父类都是java.lang.Object) ->重载(OverLoad)、重写(Overriding) ->每个区域可以划分为对象类型、原生态的变量、方法,他们都可以加上各种作用域、静态等修饰词等。 ->支持内部类和内部静态类。 ->支持反射模型。 通过上面,我们知道了java的对象都是由一个class的描述来完成的(其实class本身也是由一个数据结构的描述,只不过用它来描述对象的形状和模型,所以我们暂时理解class就是一个描述,不然这里一层一层向下想最终可能什么都想不出来或者可能想到的是汇编语言,呵呵,站在这一层要研究它就将下一层当成底层,原理上的支撑都是一样的道理),那么最关键就是如何通过构造出一个class的样子出来,此处我们不讨论关于javaCC方面的话题,也就是编译器的编译器问题,就单纯如何构建这种模型而探讨;在jvm的规范中明确说明了一点:java不强制规定用某种格式来限定对象的存储方法。也就是说,无论你怎么存储,只要你能将类的意义表达出来,并可以相互关联即可。 在语言构建语言的基础上,很多时候都是通过底层语言去编写高级语言的编译器或解释器,编写任何一门语言的基础都离不开这门语言的对象存储模型,也就是对象存储方式;如java,标准的SUN(现在是ORACLE),它是通过C++编写的,而BEA的jdk是纯C语言编写的,IBM的jdk有一部分是C,一部分是C++语言。 你也可以实现一个类似于java的对象模型,比如你用java再去编写一门更加高级的语言(如:Groovy),你要构建这门语言的对象模型就是这样一个思路,至于javaCC只是将其翻译为对应运行程序可以识别模型来表达出来而已,就像常规的应用设计中,要设计业务的实现,就要先设计业务的模型,也就是数据结构了;语言也是这样,没有结构什么也谈及不上,数据结构也就是在最基本、最底层的架构层面,脱离出一些逻辑结构,来达到某些编程方面的目的,如有些是为了高效、有些是为了清晰编码。 在内存中本身是不存在类这种概念的,甚至于可以说连C的结构体也是不存在的,内存中最基本的对象只有两种:一个是链表、一个是数组,所有其他的模型都是基于这些模型逻辑构建出来的,那么当要去构建一个java的对象的时候,根据上面的描述你应当如何去构建呢?下一章就事论事的方式来讨论一下。 2、上一篇文章中的class是如何加载和对象如何绑定的 在上一篇文章中已经提及到了class初始化加载到内存中的结构以及动态装载的基本说明;而在装载的过程中java本身有一个不成文的规定,那就是在默认情况下或者说一般情况下Perm区域的Class类的区域,是不会被修改的,也就是说一个class类在被装入内存后,它的地址是不会再被修改的,除非用一些特殊的API去完成,常规的应用中,我们也没有必要的说明这个问题,也不推荐去使用这个东西;在后面的运行时优化中会提到jvm会利用这个特征去做一些优化,如果失去这个特征就会失去一些优化的途径。 那么如果要组织一个对象,对象首先肯定需要划分代码段、数据段,代码段需要和数据段进行绑定; 首先我们用最基本、最简单的方法来做:就是当我们发起一个new XXX();时,此时将代码段从Perm中拷贝一份给对象所处的空间,首先我们的代码段应该写入的内容就是:属性列表、方法列表,而每一个列表中通过名称要找到对应的实体,通过最底层的数据结构是什么呢?最简单的就是我们定义一个数组,存放所有的目标的数据地址位置,而名称用另一个数组,此时遍历前一个数组逐个匹配,找到下标,通过相同下标,找到实际数据的地址。你看到这里是不是有一些疑惑,这样的思路就像小学生一样,太烂了,不过JVM还真经历过这个过程,我们先把基本的思路提出来,接下来再来看如何优化和重构模型。 通过上面的简单思路不难发现了两个问题:一个问题就是相同的对象经常被反复的构造,我们先不知道代码段的大小,先抛开这个问题,后面看看如果在内存中要构造一个代码段应该如何构造再看代码段的大小;另一个问题是你会发现这样是不是很慢,而且在对象的名称与地址之间,这个二元组上就很像我们所谓的K-V格式,那么Hash表呢,Hash表不正是表达K-V格式的吗,而且匹配效率要高出很多(其实Hash表也是通过数学算法加上数组和链表来实现的),只是又从逻辑上抽象了一下而已;而不论是通过什么数据结构来完成,它始终有一个名称对应值的结构,只要能实现它,java不在乎于你使用什么结构来实现(JVM规范中说明,只要你能表达出java的对象内部的描述信息,以及调用关系,你就是符合JVM规范的,它并不在乎于你是用什么具体的数据结构来表达),那么一起来看看如果你要构建一个java的语言的对象模型应当如何构建呢? 综上,我们要首先定义一个java的基本类,我们首先在逻辑上假设,要在代码段内部知道的事情是: HashMap<String,Class<? extends Object>> classes; 由此可以通过名称定位到代码段的空间。 而在一个Class内部要找到对应的属性,我们也需要定义它们的关系: Field []params;//表示该列的参数列表 Method[]methods;//表示该类的方法列表 Class []innerClass;//该类的内部类和静态内部类列表 Class parentClass;//该类的直接父亲类引用 Map<String , Object>;//用于存放hash表 其实代码段只是由相对底层的语言,构造的另一种结构,也就是它本身也是数据结构,只是从另一个角度来展现,所以你在理解class的定义的时候,你本身就可以将其理解为一个对象,存储在Perm中的一个对象; 上面为一种伪语言的描述,因为java不要求你用什么去实现,只要能描述它的结构就可以,所以这种描述有很多的版本,至于这个不想做过多的追究,继续深入的就是通过发现,要构造一个class的定义,是不容易的,它也会开销不小的内存;如果像上面我们说的,你定义一个对象,就把class拷贝过来,也就是上面说到存储在Perm定义部分的对象,那么这个空间浪费将会成倍数上涨,所以我们想到的下一个办法就是在利用JVM的class初始化后,它的地址就不会发生变化(上面说了,除非用特殊的API,否则不会发生变化),那么我们在定义对象的时候,就用一个变量指向这个class的首地址就可以了,这样对象的定义部分就只有一份公共的存储了,类似静态常量等JVM也采用相同的手段,抽象出来存储一份,这样来节约空间的目的。 好了,空间是节约下来了,接下来,当要对对象加锁synchronize的时候(这里也不讨论纯java实现的Lock实现类和Atomic相关包装类),加在哪里,当要对所有的同类对象加锁的时候加在哪里?它就是加在对象的头部,前面说了,class的定义也可以当成一个已经被初始化好的对象,所以锁就是可以在两个粒度的头部上去加锁了,当代码运行到要加锁头部的时候,就会去找这个对应的位置是否已经被加锁,如果已经被加锁,会处于一个等待池中,根据优先级然后被调用(顺便提及一下,java对线程优先级是10个级别(1-10),默认是5,但是操作系统未必支持,所以有些时候优先级太细在多数操作系统上是没有作用的,很多时候就设置为最大、最小或者不设置)。 顺便补充一下,在上一节中已经提到,关于对象头部,在早期的JVM中,对象是没有所谓的头部的,这部分内容在JVM的一块独立区域中(也就是有一块独立的handle区域,也就是一个引用首先是经过handle才会找到对象,java在对象引用之间关系比较长,这样会导致非常长的引用问题,另外在GC的算法上也会更加复杂,并且扩展空间时,handle和对象本身是在两块不同的空间,但是由于都需要扩展空间,可能会导致更多的问题出现;最后它将会在申请空间时由于处理的复杂性多使用更多的CPU指令,现在的JVM每个new的开销大概是10个CPU指令,效率上可以和C、C++媲美),不过后来发现这样的设计存在很多的问题,所以现在的jvm所有的都是有头部的问题,至于头部是什么在第五章中一起探讨一下。 上面探讨了一下关于Class定义的问题,假设出来是什么样的了,如果你要构造一个对象的基本描述,应该如何描述呢?下一章来详细说明一下。 3、一个对象在内存中是如何存放的? 有关一个对象在对象中如何移动以及申请在上一篇文章中已经描述,目前我们模拟一下,如果你要设计一个对象在内存中如何存放应当如何呢? 在上面说明了Class有定义部分,用独立的位置来存放,对象用一个指针指向它,在这里我们先只考虑变量的问题,那么有两种思路去存放,一种就是采用一个HashMap<String,? exntends Object>去存放对象的名称、和对象的的值,但是你发现这样又把代码段的名称拷贝过来了,我们不想这样做,那么就定义一个和代码段中数组等长的Object数组,即Object []obj = new Object[params.length];当然需要一个指向代码段Class的地址向量,此时我们用一个:Class clazz来代表,其实当你用对象.class就是获取这个地址,此时当需要获取某个对象的值的时候,通过这个地址,找到对应的Class定义部分,在Class定义内部做一个Hash处理,找到对应的下标,然后再返回回来找到我们对应变量的对应下标,此时再获取到对应的值。 问题是可以这样解决,是不是感觉很绕,其实不论找到一个变量还是一个方法去执行的时候,都要通过Class的统一入口地址进去,然后通过遍历或者Hash找到对应的下标位置或者说实际的地址,然后去调用对应的指令才开始执行;那么这样做我们感觉很绕,有没有什么方法来优化它呢,因为这样java肯定会很慢,答案是肯定的,只要有结构肯定就有办法优化,在下面说明了指令以及对象空间宽度问题后,在最后一章说明他有哪些优化方案。 貌似第三章就这么简单,也没有什么内容,java这个对象这么简单就被描述了?是的,往往越简单的对象可以解决越加复杂的内容,复杂的问题只有简单化才能解决问题,不过要深入探讨肯定还是有很多话题的,如:继承、实现等方法,在java中,要求继承中子类能够包含父亲类的protected、public的所有属性和方法,并且可以重写方法,在属性上,java在实例化时,是完全可以在Class定义部分就完成的,因为在Class定义部分就可以完全将父类的相应的内容包含进来(不过它会标记出那些是父类的东西,那些是当前类的东西,这样在this、super等多个重写方法调用上可以分辨出来),避免运行时去递归的过程,而在实例化时,由于相应的Class中有这些标记,那么就可以非常轻松的实现这些定义了,而在构造方法上,它通过子类构造方法入口,默认调用父亲类,逐层向上再反向回来即可。 那么目前看到这里,可能比较关心的问题就是方法是如何调用的?对象头部到底是什么?调用的优化是如何做的?继承关系的调用是怎么回事,好吧,我们下面来讨论下如何做这些事情: 4、调用的指令分析: 要明白调用的指令,那么首先要看看JVM为我们提供了哪些指令,在jdk 1.6已经提供的主要方法调用指令有: invokestatic、invokespecial、invokevirtual、invokeinterface,在jdk 1.7的时候,提出了一条invokedynamic的指令,用以应付在以前版本的jdk中对动态代码调用的性能问题,jdk 1.7以后用专门的指令要解决这一问题,至于怎么回事,我也不清楚,只是看文档是这样的,呵呵;下面简单介绍下前面几个指令大概怎么回事(先说指令是什么意思,后面再说怎么优化的)。 invokestatic一看就知道是调用静态代码段的,当你定义个static方法的时候,外部调用肯定是通过类名.静态方法名调用,那么运行时就会被解释为invokestatic的JVM指令;由于静态类型是非常特殊的,所以编译时我们就完全独立的确立它的位置,所以它的调用是无需再被通过一连串的跳转找到的。 invokespecial这个是由JVM内部的一个父类调用的指令,也就是但我们发生一个super()或super.xxx()时或super.super.xxx()等,就会调用这个指令。 invokevirtual由jvm提供的最基本的方法调用命令,也就是直接通过 对象.xxx() 来调用的指令。 invokeinterface当然就是接口调用啦,也就是通过一个interface的引用,指向一个实现类的实例,并通过调用interface的类的对应方法名,用以找到实现类的实际方法。 这里的指令在第一次运行时都需要去找到一个所谓的入口调用点,也成为call side,最基本的就是通过名称,找到对应的java的class的入口,找到一个非动态调用的方法以及其多个版本号,根据实际的指令调用的对应的方法,编译为指令运行。 明白了这些指令我们比较疑惑的就是在继承与接口的方法调用上如何做到快速,因为一门语言如果本身就很慢的话,外部要调优也是无济于事的,于是在找到多个实现类的时候,我们一般提出以下几种查找到底要调用哪一个方法的假设,每一种假设他们都有一个性能的标准值。 当存在多层的继承时,并存在着重写等情况的时候,要考虑到实际调用的方法的时候,我们做以下几种假设: 1、假如在初始化类中,将父类的相应的方法也包含进来,只是做相应的标识码,并且按照数组存放,此时,就会存在同名方法,做hash的话就有些困难了,当然你可以带上标识符做hash,但是hash的KEY是唯一的,此时需要的不仅仅是自己的方法调用,还需要一连串的,不过可以按照制定的规则逐个查找。 2、另一种是不包含进来自下而上递归查找,也是较为原始的方法,虽然效率上有点低,不过大部分集成关系不会超过3层以上。 3、在这个角度,另一种方法是基于方法名的地址做纵向向量,也就是在自下向上的查找中,只需要定位最后一个入口地址,直接调用便直接使用,当使用super的时候,就按照数组进行反向偏移量,这貌似是一个不错的方法,不过查找呢,我们将这个数组做为一个整体的Value,来让Hash找到,每个方法标识这自己来源于哪一个类,以及,由类关联出他们的子孙关系即可。也就是说,在一般情况下,jvm认为继承关系不是太长的,或者说是即使继承关系很长,在继承的关系链表中,自上而下任意找一条链上上去,重写的方法个数并不是很多,一般最多保持在3、4个左右,所以在直接记录层次上,是完全可行的;但是问题是,这种层次分析不允许在对象内部发生任何的层次变化,也就是纯静态的,但是java本身是支持动态Load的,所以静态编译器无法完成这个操作,而动态编译器可以,在变化的过程中需要知道退路。 其实这部分有点类似于调用优化了,不过后面还会说明更多的调用优化内容,因为从上述的阅读中你应该会发现,一个操作后的调用会导致非常多的寻址,而且很多是没有必要的,我们在最后一章配合一些简单例子再来说明(例子中会说到上述的一些指令的调用),下一章先说明下对象在内存中到底是如何存储和浪费空间的。 5、对象宽度及空间浪费 对象宽度这个说法很多时候都是C语言、C++这些底层语言中经常讨论的话题,而通过这些语言转变过来的人大多数对java比较反感的就是为什么没有一个sizeof的函数来让我知道这个对象占用了多大的内存空间;java其实即使让你知道大小也是不准确的,因为它中间有很多的对齐和中间开销,如在多维数组中,java的前面几个维度都是浪费的空间,只有最后一个维度的数据,也就是N多个一维数组才是真正的空间大小,而且它中间存在很多对象的对象等等概念。 那么一个简单对象,java的对象到底是如何存放的呢?首先明白一点,Hotspot的JVM中,java的所有对象要求都是按照8个byte对齐的,不论任何操作系统都是这样,主要是为了在寻址时的偏移量比较方便。 然后,对象内部各个变量按照类型,如果对象是按照类型long/double占用8个byte、int/float占用4个byte,而short/char是占用2个byte,byte当然是占用一个了,boolean要看情况,一般也是一个byte,而对象内部的指向其他对象的引用呢?这个也是需要占用空间的,这个空间和OS和JVM的地址位数有关系,当然OS为32位时,这个就占用4个byte,当OS为64位数时,就占用8个byte,在根引用中,操作系统的stack指向的那个引用大小也是这样,不过这里是对象内部指向目标对象的宽度。 对象内部的每个定义的变量是否按照顺序存储进去呢?可以是也可以不是(上面已经说了,JVM并不强制规定你在内存中是如何存放的,只需要表达出具体的描述),但是一般不是,因为当采用这种方式的时候,当再内部定义的变量由于顺序的问题,导致空间的浪费,比如在一个32位的OS中定义个byte,再定义一个int,再定义一个char,如果按照顺序来存储,byte占用一个字节,而int是4个字节,在一个内存单元下,下面只剩下3个byte,放不下了,所以只有另外找一个内存单元存放下来,接下来的char也不得不单独在用一块4byte的内存单元来存放,这样导致空间浪费(不过这样寻址是最快的,因为按照OS的位数进行,是因为这是寻址的基本单位,也就是一个CPU指令发出某个地址寻址时,是按照地址带宽为基本单位进行寻址的,而并非直接按照某个byte,如果将两个变量放在同一个地址单元,那么就会产生2次偏移量才能找到真正的数据,有关逻辑地址、线性地址、物理地址上的区别在上一篇文章说有概要的介绍); 不过在java默认的类中一般是按照顺序的(比如java的一个java.lang.String这些类内存的顺序都是按照定义变量的顺序的),虚拟机知道这些类,相当于一些硬代码或者说硬配置项,这也是虚拟机要认名字的一特征就像实例化序列化接口一样,其实什么都不用写只是告诉虚拟机而已;由于这些类在很多运行时虚拟机知道这些是自己的类,所以他们在内存上面会做一些特殊的优化方案,而和外部的不是一样的。 在Hotspot的JVM对参数FieldsAllocationStyle可以设置为0、1、2三种模式,默认情况下参数模式1,当采用0的时候:采用的是先将对象的引用放进去(记住,String或者数组,都是存放的引用地址),然后其他的基本变量类型的顺序为从大到小的顺序,这样就大量避免了空间开销;采用模式1的时候,也就是默认格式的时候,和0格式的唯一区别就是将对象引用放在了最后,其实没什么多大的区别;当采用模式2的时候,就会将继承关系的实例化类中父子关系的变量按照顺序进行0、1两种模式的交叉存放;而另一个参数CompactFields则是在分配变量时尝试将变量分配到前面地址单元的空隙中,设置为true或者false,默认是true。 那么一个对象分配出来到底有哪些内容呢,那我们来分析下一个对象除了正常的数据部分以及指向代码段的部分,一般需要存放些什么吧: 1、唯一标识码,每一个对象都应该有一个这样的编码,唯一hash码。 2、在标记清除时,需要标记出这个对象是否可以被GC,此时标记就应该标记在对象的头部,所以这里需要一个标识码。 3、在前一篇文章中说明,在Young区域的GC次数,那么就要记录下来需要多少次GC,那么这个也需要记录下来。 4、同步的标识,当发生synchronized的时候,需要将对象的头部记录下对象已经被同步,同时需要记录下同步该对象的线程ID。 5、描述自身对象的个数等内容的一个地方。等等。。也许还有很多,不过我们至少有这么一些内容。 不过这些内容是不是每个时候都需要呢,也就是对象申请就需要呢?其实不然,如线程同步的ID我们只需要在同步的时候在某个外部位置存放就可以了,因为我们可以认为线程同步一般是不会经常发生的,经常发生线程同步的系统也肯定性能不好,所以可以用一个单独的地方存放。 前面1-4,很多时候我们把这个区域叫做:_mark区域,而第五个地方很多时候叫做:_kclass区域。加在一起叫做对象的头部(这个头部一般是占用8个byte的空间,其中_mark和_kclass各自占用4个byte)。 现在明白了对象的头部了,那么对象除了头部以外,还有其他的空间开销吗?那就是前面提到Hotspot的java的对象都是按照8个byte的偏移量,也就是对象的宽度必须是8byte的整数倍,当对象的宽度不是8的整数倍数的时候,就会采用一些对其方式了,由于头部本身是8个byte,所以大家写程序可以注意一点,当你使用数据的空间byte为8的整数倍,这个对其空间就会被节约出来。 随着上面的说明,对其和头部,我们来看看几个基本变量和外包类的区别,Byte与byte、Integer与int、String a = "b"; 首先byte只占用一个byte,当使用Byte为一个对象时,对象头部为8个字节,数据本身占用1个byte,对其宽度需要7个byte,那么对象本身的开销将需要16个byte,此时,也就是说两者的空间开销是16倍的差距,你的空间利用率此只有6.25%,非常小;而int与Integer算下来是25%,String a = "b"的利用率是12.5%(其实String内部还有数组引用的开销、数组长度记录、数组offset记录、hash值的开销、以及序列化编码的开销,这里都没有计算进去, 这部分开销如果要计算进去,利用率就低得不好描述了,呵呵,当然如果数组长度长一点利用率会提高的,但是很多时候我们的数组并不是很长),呵呵,说起来蛮吓人的,其实空间利用还是靠个人,并不是说大家以后就不敢用对象了,关键是灵活应用,在jdk 1.5以后所谓的自动拆装箱,只是JVM帮你完成了相互之间的转换,中间的空间开销是免不掉的,只是如果你的系统对空间存储要求还是比较高的话,在能够使用原生态类型的情况下,用原生态的类型空间开销将会小很多。 补充说明一下,C、C++中的对象,直接在结构体得typedef后面定义的默认接的那个对象,是没有头部的,纯定义类型,当然C++中也有一个按照高位宽度对其的说法,并且和OS的地址宽度有关系,通过sizeof可以做下测试;但是通过指针=malloc等方式获取出来的堆对象仍然是有一个头部的,用于存放一些metadata内容,如对象的长度之类的。 好了。看到了指令,看到对象如何存储,迫不及待的想要看看如何去优化的了,那么我们看看虚拟机一般会对指令做哪些优化吧。 6、指令优化: 在谈到优化之前我们先看一个简单例子,非常简单的例子,查看编译后的文件的的指令是什么样子的,一个非常简单的java程序,Hello.java public class Hello { public String getName() { return "a"; } public static void main(String []args) { new Hello().getName(); } } 我们看看这段代码编译后指令会形成什么样子: C:\>javac Hello.java C:\>javap -verbose -private Hello Compiled from "Hello.java" public class Hello extends java.lang.Object SourceFile: "Hello.java" minor version: 0 major version: 50 Constant pool: const #1 = Method #6.#17; // java/lang/Object."<init>":()V const #2 = String #18; // aconst #3 = class #19; // Helloconst #4 = Method #3.#17; // Hello."<init>":()V const #5 = Method #3.#20; // Hello.getName:()Ljava/lang/Stri const #6 = class #21; // java/lang/Object const #7 = Asciz <init>; const #8 = Asciz ()V; const #9 = Asciz Code; const #10 = Asciz LineNumberTable; const #11 = Asciz getName; const #12 = Asciz ()Ljava/lang/String;; const #13 = Asciz main; const #14 = Asciz ([Ljava/lang/String;)V; const #15 = Asciz SourceFile; const #16 = Asciz Hello.java; const #17 = NameAndType #7:#8;// "<init>":()V const #18 = Asciz a;const #19 = Asciz Hello;const #20 = NameAndType #11:#12;// getName:()Ljava/lang/String; const #21 = Asciz java/lang/Object; { public Hello(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 11: 0 public java.lang.String getName(); Code: Stack=1, Locals=1, Args_size=1 0: ldc #2; //String a 2: areturn LineNumberTable: line 14: 0 public static void main(java.lang.String[]); Code: Stack=2, Locals=1, Args_size=1 0: new #3; //class Hello 3: dup 4: invokespecial #4; //Method "<init>":()V 7: invokevirtual #5; //Method getName:()Ljava/lang/String; 10: pop 11: return LineNumberTable: line 26: 0 line 30: 11 } 看起来乱七八糟,不要着急,这是一个最简单的java程序,我们按照正常的程序思路从main方法开始看,首先第一行是告诉你new #3;//class Hello,这个地方相当于执行了new Hello()这个命令,而#3是什么意思呢,在前面编译的指令列表中,找到对应的#3的位置,这就是我们所谓的入口位置,如果指令还要去寻找下一个指令就跟着#找到就可以了,就想刚才#3又找到#19,其实是要找到Hello的定义,也就是要引用到Class的定义的位置。 继续看下一步(关于内部入栈出栈的指令我们这里不多说明),invokespecial #4; //Method "<init>":()V,这个貌似看不太懂,不过可以看到后面是一个init方法,它到底初始化了什么,我们这里因为只有一行代码,我们姑且相信它初始化了Hello,不过invokespecial不是对super进行调用的时候才用到的吗?所以这里需要补充一下的就是当对象的初始化的时候,也会调用它,这里的初始化方法就是构造方法了,在指令的时候统一命名为init的说法; 那么调用它的构造方法,如果没有构造方法,肯定会进入Hello的默认构造方法,我们看看上面的public Hello(),发现它内部就执行了一条指令就是调用又调用一个invokespecial指令,这个指令其实就是初始化Object父对象的。 再继续看下一条指令:invokevirtual #5; //Method getName:()Ljava/lang/String;你会发现是调用了getName的方法,采用的就是我们原先说的invokevirtual的指令,那么根据到getName方法部分去: 会发现直接做了一个ldc #2; //String a操作就返回了,获取到对应的数据的地址后就直接返回了,执行的指令在位置#2,也就是在常量池中的一个2。 好了一个简单的程序指令就分析到这里了,更多的指令大家可以自己去分析,你就可以看明白java在指令上是如何处理的了,甚至于可以看出java在继承、内部类、静态内部类的包含关系是如何实现的了,它并不是没用,当你想成为一个更为专业和优秀的程序员,你应该知道这些,才能让你对这门驾驭得更加自如。 几个简单的测试下来,会发现一些常见的东西,比如 ==>你继承一个类,那个类里面有一个public方法,在编译后,你会发现这个父亲类的方法的指令部分会被拷贝到子类中的最后面来 ==>而当使用String做 “+” 的时候,那怕是多个 "+" ,JVM会自动编译指令时编译为StringBuilder的append的操作(JDK 1.5以前是StringBuffer),大家都知道append的操作将比 + 操作快非常的倍数,既然JVM做了这个指令转换,那么为什么还这么慢呢,当你发现java代码中的每一行做完这种+操作的时候,StringBuilder将会做一个toString()操作,如果下一次再运行就要申请一个新的StringBuilder,它的空间浪费在于toString和反复的空间申请;并且我们在前面探讨过,在默认情况下这个空间数组的大小是10,当超过这个大小时,将会申请一个双倍的空间来存放,并进行一次数组内容的拷贝,此时又存在一个内部空间转换的问题,就导致更多的问题,所以在单个String的加法操作中而且字符串不是太长的情况下,使用+是没有问题的,性能上也无所谓;当你采用很多循环、或者多条语句中字符串进行加法操作时,你就要注意了,比如读取文件这类;比如采用String a = "dd" + "bb" + "aa";它在运行时的效率将会等价于StringBuilder buf = new StringBuilder().append("dd").append("bb").append("aa"); 但是当发生以下情况的时候就不等价了(也就是不要在所有情况下寄希望于JVM为你优化所有的代码,因为代码具有很多不确定因素,JVM只是去优化一些常见的情况): 1、字符串总和长度超过默认10个字符长度(一般不是太长也看不出区别,因为本身也不慢)。 2、多次调用如上面的语句修改为String a = "dd";a += "bb"; a += "aa";与上面的那条语句的执行效率和空间开销都是完全不一样的,尤其是很多的时候。 3、循环,其实循环的基础就是来源于第二点的多次调用加法,当循环时肯定是多次调用这条语句;因为Java不知道你下一条语句要做什么,所以加法操作,它不得不将它toString返回给你。 ==>继续测试你会发现内部类、静态内部类的一些特征,其实是将他编辑成为一个外部的class文件,用了一些$标志符号来分隔,并且你会发现内部类编译后的指令会将外包类的内容包含进来,只是他们通过一些标志符号来标志出它是内部类,它是那个类的内部类,而它是静态的还是静态的特征,用以在运行时如何来完成调用。 ==>另外通过一些测试你还会发现java在编译时就优化的一个动作,当你的常量在编译时可能会在一些判定语句中直接被解析完成,比如一个boolean类型常量IS_PROD_SYS(表示是否为生产环境),如果这个常量如果是false,在一段代码中如果出现了代码片段: if(IS_PROD_SYS) { ..... } 此时JVM编译器在运行时将会直接放弃这段代码,认为这段代码是没有意义的;反之,当你的值为true的时候,编译器会认为这个判定语句是无效的,编译后的代码,将会直接抛弃掉if语句,而直接运行内部的代码;这个大家在编译后的class文件通过反编译工具也可以看得出来的;其实java在运行时还做了很多的动作,下面再说说一些简单的优化,不过很多细节还是需要在工作中去发现,或者参考一些JVM规范的说明来完善知识。 上面虽然说明了很多测试结果所表明的JVM所为程序所做的优化,但是实际的情况却远远不止如此,本文也无法完全诠释JVM的真谛,而只是一个开头,其余的希望各位自己可以做相应的测试操作; 说完了一些常见的指令如何查看,以及通过查看指令得到一些结论,我们现在来看下指令在调用时候一般的优化方法一般有哪些(这里主要是在跨方法调用上,大家都知道,java方法建议很小,而且来回层次调用非常多,但是java依然推荐这样写,由上面的分析不得不说明的是,这样写了后,java来回调用会经过非常的class寻址以及在class对对内部的方法名称进行符号查表操作,虽然Hash算法可以让我们的查表提速非常的倍数,但是毕竟还是需要查表的,这些不变化的东西,我们不愿意让他反复的去做,因为作为底层程序,这样的开销是伤不起的,JVM也不会那么傻,我们来看看它到底做了什么): ==>在上面都看到,要去调用一个方法的call site,是非常麻烦的事情,虽然说static的是可以直接定位的,但是我们很多方法都不是,都是需要找到class的入口(虽然说Class的转换只需要一次,但是内部的方法调用并不是),然后查表定位,如果每个请求都是这样,就太麻烦了,我们想让内部的放入入口地址也只有一次,怎么弄呢? ==>在前面我们说了,JVM在加载后,一般不使用特殊的API,是不会造成Class的变化的,那么它在计算偏移量的时候,就可以在指令执行的过程中,将目标指令记忆,也就是在当前方法第一次翻译为指令时,在查找到目标方法的调用点后,我们希望在指令的后面记录下调用点的位置,下次多个请求调用到这个位置时,就不用再去寻找一次代码段了,而直接可以调用到目标地址的指令。 ==>通过上面的优化我们貌似已经满足了自己的想法,不过很多时候我们愿意将性能进行到底,也就是在C++中有一种传说中的内联,inline,所以JVM在运行时优化中,如果发现目标方法的方法指令非常小的情况下,它会将目标方法的指令直接拷贝到自己的指令后面,而不需要再通过一次寻址时间,而是直接向下运行,所以JVM很多时候我们推荐使用小方法,这样对代码很清晰,对性能也不错,大的方法JVM是拒绝内联的(在C++中,这种内联需要自己去指定,而并非由系统完成,正常C++的指令也是按照入口+偏移量来找到的) ==>而对于继承关系的优化,通过层次模型的分析,我们在第四章中就已经说明,也就是利用一般情况下多态中的单个链中对应的对象的重写方法数组肯定不会太长,所以在Class的定义时我们就知道自下向上有多少个重写方法,而不是运行时才知道的,这个也叫做编译时的层次分析。 ==>从上面方法的应用上,我们在适当的条件下如何去编写代码,适当的条件下去选择单例和工厂、适当的条件下去选择静态和非静态、适当的条件下去选择继承和多态等在通过上面的指令说明后,可以自己做一些简单的实验,就更加清楚啦。 文章写到这里结束,欢迎拍砖!
本文牵扯的面积可能会比较泛,或者说比较大,在这个层面很多人也有自己的见解,所以我这也仅仅是抛砖引玉,结合前面讲述的一些基础技术,从思想中阐述更为深入的架构思想基础,因为最好的架构思想是架构师结合实际情况思考出来最适合的架构,这里仅仅说明下一些常用的原理和思想,主要包含的内容有(内容很泛,所以都是简单阐述入门知识,具体后续深入探讨): 1、app切分集群组扩展 2、app集群组负载均衡 3、Memcached原理 4、db cache应用 5、db存储类型以及存储cache说明 6、存储条带思想 7、数据库集群 8、数据库分布式存储 9、数据库容灾备份以及监控 10、nosql思想 11、无锁分析 1、app切分集群组扩展 应用系统架构随着外部并发量的增加,必然导致的是app应用的压力逐渐增加,并且绝大部分app对于线程的分配能力都是有限的,但是app应用在扩展上是非常容易的,最基础的就是一种应用的垂直切割,其次是水平切割。 在了解app切割原理的基础上先来了解一个其他的概念就是,就是Internate的路由器如何路由你请求的一个URL,你发送的URL并不知道路由器发送到哪里去了,最终路由到远端的服务器(互联网本身就是一个云计算为基础的平台),到了远程后,它如何找到自己的应用呢,以及应用下具体的服务内容呢?就是通过URL后面部分的标识符号。也就是在云的最终技术也是各自管理各自的内容,而云之所谓称之为云是因为你无须关心你发送的URL是如何路由到远端的服务器的,又是如何通过哪些路由器返回回来的。这里不深入讨论云的概念,回到主题上是说远端的服务器每一个目标都有自己处理的对象,或者说不同的路径下或者不同的端口下都会有自己的处理服务。所以app系统切割的基本思想就是路径。 当一个系统业务十分复杂,应用并发量很大的情况下,必然导致的一个步骤就是业务分解,将一个大的系统拆分为多个小系统,不论是软件本身设计的可扩展性还是软件性能扩展性都会有很大的帮助,比如在各个子系统之间他们的业务的复杂性以及并发量都会有很大的区别,我们可以将这些小系统拆分到不同的集群节点上去,这些集群节点可以是由同一个主机发布出来的不同端口或者URL,或者是不同的主机发布出来的内容,并且三者可以根据实际情况调整使得成本、软件扩展性、性能扩展性达到较好的程度,总之将一个大系统拆分为多个小系统是第一个需要做的,也就是app应用拆分,这种并不难,但是拆分的依据一定要把控好,而且还有一个总体架构,不然软件最终会做的五花八门;在很多的应用下都会使用,只是在这种拆分,拆分后除上述的总体设计要做好外,还需要主意的一点就是系统通信问题,子系统拆分后应该是高内聚的,但是免不了需呀通信,否则就根本不算是一个系统的,而且即使不是同一个系统也有可能因为服务需求而需要去通信,所以在通信上需要多下功夫,在不同服务器以及语言之间通信最麻烦的事情就是字符集,也是需要主意的,不过不是本文的重点。 在上面拆分完成后,当某一个子系统的并发量非常大的时候,我就需要单独对某一个子系统进行拆分了,这种没的选,一般不太可能通过URL来控制(除非申请不同的VIP或者在同一个主机上用不同的虚拟目录来做,不过是不是有点挫),这种一般是通过在不同的服务端口(也称为运行节点,在同一个主机上多个节点肯定是不同的端口的),或者发布到不同的主机上来完成;这部分拆分app应用不会受到太大的限制;这个地方需要主意的是,当你在app内部做静态内存时,就无法做到当一个机器的内存修改后同时修改到其他内存中,除非你自己写程序要么定时刷新要么相互之间传送数据,但是这两种都会付出巨大的成本,如何解决呢,我们后面会说到的Memcached就是解决的方法。 上述两者完成后,新的问题出现了,就是子系统之间的通信,他们不再是单机对单机的通信,而是集群组对集群组的通信,所以子系统中间件必然有一些非一对一的通信机制就出现了,以及中途产生的同步通信和异步通信等机制,如IBM MQ、EJB、Webservice、HttpClient、HttpInvokie、RPC、Socket甚至于借助于数据库或者文件作为中间层等等都是通信机制的基础,应用非常特殊的公司会自己写自己的通信机制。 有了上述的集群,URL可以通过网络路由到具体的服务器,但是服务器下每一个集群节点不可能都去申请一个URL吧,而且客户也不会自己知道我第一次用URL1,下一次用URL2、再下一次用URL3来给你服务器做负载均衡吧,客户只知道用一个URL访问你,而且那么多的IP在互联网上也会占用非常大的资源,所以很多时候,一个大网站后台可能数万的主机,前端暴露的IP可能只有几个,这个可以成为VIP,他们之间有一个绑定关系,由这个VIP来负责域名的帮顶,而VIP一般会绑定在一个负载均衡器上面,由负载均衡器根据实际请求内容负载到具体的主机上面去,下面第二章就是我们要写的负载均衡基本原理。 2、app集群组负载均衡 所谓负载均衡就是负载均衡了,呵呵,也就是不让某太机器单独忙,也不让某台机器太闲,将请求进行分发,这就是负载均衡器设计的初衷了。 随着发展的变化,负载均衡器需要承担更大的作用 第一个需要做的就是请求解析,也就是很多不同的应可能由一个负载均衡器来完成; 进一步,同一个应用发布的不同的节点或者不同的端口,负载均衡器可以识别出来并达到分发负载,将并发负载到很多不同的节点上去运行; 再进一步,某个客户端请求第一次访问了某个节点后,当session未失效时,应当做到继续访问同一台主机,这样保证客户在多次交互中session内容是一致的,至少不会导致重新登陆等现象; 再进一步,在节点失败是,负载均衡器应当识别出来,并可以将访问切换到其他主机,在有必要的情况下需要做session复制。 负载均衡最基本的需要做到以上几点内容才算负载均衡。 负载均衡器一般需要的内容是全局的,但是它并不关注与细节,所以它主要做的事情是全局资源定位,监控,负载均衡,切换动作;一般会有一个单独的管理节点和单独的分发节点,但是每一门负载均衡的机制在设计层面都会有很大的区别,所以无需一概而论。 因为负载均衡器在所有应用的最前端,所以我们非常关注于它的性能,有很多基于高级语言编写的负载均衡器,甚至于你可以直接通过你的JSP、ASP、PHP等等做一个简单的控制跳转上的负载均衡,但是他们的性能就很低了,扩展性受到明显的限制,Linux内核才是负载均衡器的王道,终极方案,要深入研究和负载均衡的方案,请大家多多参详Linux内核。 目前市面上非常常用的负载均衡器是apache,它本身也可以作为WEB服务器来应用(它的一些模块就可以直接用于php),另外weblogic自带的proxy+domain+managed模式也是一种负载均衡方法,不过我做过几个版本效果不理想,主要原因还是主要是因为实现的基础是高级语言吧;而apache虽然性能不错,而且大家广受喜爱的一种东西,不过随着互联网并发量的上升,apache在很多极为高并发的系统中仍然受到扩展性的限制,于是乎ngnix出现了,它是目前高并发网站应用中最广泛或者说在大网站中用得最多的负载均衡器,国内的大网站基本都有它的影子,它是俄罗斯一位工程师编写,而且是免费的,性能极高,占用资源极少,并且支持cache以及代理,很多客户端访问的机制都可以配置化,安装和使用都非常简单(要深入研究就没那么简单),而且故障率非常低,为什么那么好,因为它的基础就是unux内核,没有别的,当然写代码一定要写得很好才行;当然国内并非没有这样的人才存在,而且要看公司是否给这类人才一个机会去完成这样一个东西,因为自己写的可能会更加适合于自己,其实国内也有很多对Unix内核研究很深入的顶尖高手。 3、Memcached原理 这一章本身就想在数据库后面说明的,不过由于只是简单介绍,而且后面应该几乎都是技术,所以就这这里说明了。 一般应用程序除了处理业务逻辑和一定的计算后,就是访问数据库,做数据库的存、取、事务操作,在OLAP会有更多的是在数据库端的计算,OLAP不是本文的重点,因为OLAP不会涉及并发量的问题,所以更多偏重于OLTP,而且是极高并发的系统。 当前端app并发达到一定程度,即将考虑的问题就是数据库的压力,数据库面对的更多的数据,虽然它在各方面做了非常大的优化,不过它毕竟是存大量锁机制和磁盘读写操作,来保证数据一致性和安全性,而这两者往往是影响性能的关键指标,但是我们很多时候又不得不用数据库,因为他可以提供给我们的东西实在是太多了。 在互联网应用中有个几乎所有网站都会拥有的一个共同特征那就是读取次数非常多,而写的次数相对比例较少(并不代表没有写操作),此时人们在设计上第一个想法是让数据库来完成主备或者镜像方式上的读写分离,不过始终会与数据库交互,而且扩展上会受到非常大的限制,在极高并发下,人们又对应用做了对页面输出html的方式,但是最终发现在实施过程中会受到很多限制(尤其是ajax交互),虽然有很多软件可以支持,此时很多人想到将数据载入到内存中,按照某种方式刷新内存即可,不过我们上面已经讨论,在集群下它很难做到每个被切割开的节点他们之间的静态内存是一致的,所以Memcached出现了。 Memcached我看网上写它是分布式的,这个大家最好不要乱理解,因为从基本的设计上讲,它只是将app和静态内存分开了,而并非真正意义上做到分布式(真正意义上的分布式应当自动将多个Memcached节点的访问如同访问一个节点一样简单),而一般Memcached的访问方式还是通过程序去控制的,而多个不同节点划分,也是通过人为的完成的,你可以认为你访问的Memcached是数据库一样的东西,因为它的访问方式类似于数据库,但是它如果命中肯定比访问数据库要快很多,而且可以大量减少读的压力,因为一个大网站百分之八九十以上的压力来源于读;一个好的Memcached设计会使得读命中率达到95%以上,而其成本只需要大内存,并具有极大的扩展性;根据实际系统的场景讲Memcached划分数据的方法指定,当命中是获取,当修改时先修改数据库,然后让对应的cached失效即可;主意解决如果它挂掉会产生什么问题,它的基础原理是一种Key-Value方式,但是通用的东西往往不是性能最佳的东西,所以你在有必要的情况下可以适当做下修改,淘宝网的tair开源技术就是一套自己完成的分布式缓存技术,也是很不错的选择。 4、db cache应用 上述已经描述到数据库访问会有大量的磁盘操作,这里我们说下oracle是如何缓解这些问题的,以至于它一直在数据库领域处于行业界得老大哥形象出现。 它首先由一个SGA的全局区域,内部的其他区域已经在前面的文章中说明,中间对于数据层面,最重要的就是databuffer了,这个databuffer是采用基于LRU算法为基础的方式来完成的所以只要有足够大的内存,在读远大于写的情况下命中率也会非常高(其实oracle做写操作也是写内存的,即使你commit命令oracle也不会做磁盘写,但是它会写日志,当达到一定并发量日志写也是不可估量的,而且脏块列表也会非常频繁的被刷新到磁盘上,也很容易出现瓶颈),data buffer这也是db cache最为核心的部分,当然还有些其他区域也有一定的cache思想。 一般来说,对于极为高并发的系统,数据库的cached逐渐受到限制,虽然oracle rac可以非常高效的扩展,但是其限制最多是64节点的整列结构,而且在这个过程中并非没有锁,尤其是在集群下的全局锁机制,这个开销也是很大的,但是我们很多时候访问很多数据并非需要锁,也就是这些数据是在这段时间内我们确定不会被修改或者说根本不会被修改甚至于说修改了一个简单脏数据的延迟读也是无所谓的,这类数据我们没有必要让他来和其他需要做绝对一致性的事情套在一起,该做事务的被阻塞了,可以间接做事务的也被阻塞了,所以在更多的层面我们希望的是app端做好cache,才是更好的方案,通常app的性能会占用整个系统性能指标的50%以上,而有20%在于数据库端,另外还有设计方案、存储等等其他的,以及SQL了。 在app设计cached后,数据库更多的是做修改,读显得更加少,所以在app设计cached后,数据库端的内存可以保留,也可以节约一些出来也可以。 5、db存储类型以及存储cache说明 存储就是指最终数据存放的位置,有些地方也叫做整列(因为很多时候它是多个磁盘通过RAID完成的),存储一般会有低端存储、中端存储、高端存储。 存储设备中最挫的就是本地硬盘了,一般都可以不认为他是独立的存储设备;但是最终你会发现它在是最好的,呵呵,在分布式的架构上,我们更加愿意选择廉价的成本设备,并自己架构主机来完成使得性能达到更高的程度;比如在一种顺序写非常多、随机读非常多的场景下,我们就更加愿意选择SSD硬盘来做存储,因为它的总体设计就非常适合这种情况。 低端存储一般只有一个控制器,坏掉全部坏掉,没有任何存储cached,存磁盘操作。 中端存储一般有2个控制器,可以做均衡负载,而且可以冗余保护,坏掉一个性能会降低50%,并且有一定的cache设备,有些时候也会分读cache和写cache,IBM DS 8000属于一种中端存储,不过它自称是高端存储设备,外部一般说他是伪高端设备。 高端存储,多个控制器相互冗余,坏掉一两个性能影响较小,具体影响要看存储成本和具体需求;EMC高端存储就是非常流行的选择,DMX3中还有一种读cache镜像和写cache镜像,在某些应用下性能更加提升;不过高端存储的成本极高,在必要的环境下才会使用,绝大部分企业会使用中端存储设备。 存储成本并非和性能或者说高可用性完全成正比,尤其是本身高可用性很好的情况下;所以在选择存储的时候再考虑当前应用下需要考虑的就是成本,主要是:数据存储容量、电费、网路带宽;以及一个存储在多少年后报废等一起计算。 存储的基本考量标准也是系统性能重点指标:IOPS、QPS、TPS、带宽容量、单个请求响应时间。 这些目前不做深入探讨,以后我们再说(因为涉及内容非常多,而且和磁盘管理方式有关系,如下面的条带就会对其影响),只做下简单介绍: IOPS:磁盘阵列上每秒相应IO次数,这个IO次数不分读写,但是一般是OLTP系统中的小IO,一般用2K、4K这种来做测试(所以主意你在设计OLTP系统的数据库block时为什么要小,因为提取一条数据并不想用多次IO,而oracle提取数据的单位是block,mysql和sqlserver是页);一般单个硬盘的IOPS会根据设计有关系,一个15k rpm 的IOPS一般是150个,但是并非绝对,可能会管理方式以及每个IO的大小有关系。 QPS和TPS是对IOPS的一个分解,其实本身没这个概念,不过可以做这个来看出一个系统的读写比例以及让系统以后如何设计来更好的工作。这两个分别代表的是每秒的查询次数、事务次数;可以通过一些内部SQL抓取等方法来实现。 IO带宽:当上述内容完成后,就需要考虑带宽了,当你的IOPS可以上去后,但是带宽上不去就悲剧了,那刚才的15k rpm来说,一般带宽是13M/s,这里单位注意是字节(B),这里假设有120块磁盘,那么也就是1560M/s,此时就需要通信上做一些支持,也就是要支持1G多的流量,需要光纤带宽8Gb(这里是网络上的大小,也就是二进制大小),那么最少使用4块2Gb的光纤卡;这种考虑基本在OLAP中比较多,而在OLTP系统中IO都是小IO,带宽按照小IO的大小乘以IOPS已经足够。 响应速度:这个因素就多了,除了上述的IOPS以及吞吐量以外,还和存储cache有关系,甚至于和锁都有关系,总之响应速度算是一个最终结果,影响因数上面每一种都会有,具体需要根据实际系统来协调,一般来说一个IO如果存磁盘操作最少需要10ms甚至于更多,而如果在cache中命中可能2ms左右就响应了,也就是从单个IO来说,cache命中比正常磁盘操作要快5倍,而平均IO一般保持在10ms是比较良好的,很多时候非cache的情况下平均IO一般会达到20ms以上 6、存储条带思想 大家不要被这个词汇所吓到,它就是RAID0的另一种说法,其实RAID有很多种,从RAID0~RAID7每一种都有自己的特征所在,而且还有组合的,企业常用的有:RAID 10、RAID5、RAID3这几种,本文不对磁盘阵列做详细阐述,而只是通过条带给带出来一些思想。 RAID0,也就是条带,它的思想源于负载均衡,和散列存储,最终在磁盘上的统一实现,并将其作为磁盘组为中心,给外部调用,而无需关心磁盘的内部细节。 它按照一定的数据顺序,将数据分布逐个分布在多个磁盘上,所以看起来就像“条带”一样,同时不论在读还是写的过程中,它都将IO负载到了不同的磁盘上,使得IO的总体性能几乎可以与磁盘数成正比,极大提高IO性能。 但是RAID0本身没有保护,也就是当磁盘坏掉,数据就丢了,找不回来,所以后来出现各种各样的RAID,根据不同的情况每一种RAID都会有自己的方式来处理,实现补充程度的冗余,还是那句话,发展到一定的冗余度将会导致成本直线上升,但是并不一定会带来收益的直线上升;RAID10就是通过50%冗余完成,也就是一对一冗余完成,同一个整列下所有的数据坏掉也可以找回来,除非两块磁盘是相互冗余的磁盘同时坏掉;而RAID5属于从RAID3、RAID4做一些算法改进和性能提升上来的,其原理都是奇、偶校验码原则,数据分布式按照条带思想,冗余N+1块磁盘,数据随机存放在N块磁盘上,剩余一块做校验位,相对减少磁头同步粒度,其中任意一块磁盘坏掉,均可恢复,但同一个RAID5阵列同时坏掉2块不行。 顺便提及下,ORACLE个只疯狗什么东西都想独霸,他的ASM就是拿出来和RAID竞争的,它的管理可以基于裸机,更加优于基于操作系统层的调用,而在裸设备的管理上又会有很多新的讲究。 7、数据库集群 数据库集群上,最初是通过一种操作系统机制HA完成,但是它在数据库层面存在很多缺陷,相对管理数据库来说还存在很多专业上的个性化,所以ORACLE在10g推出了ORACLE RAC(其实是9i,但是9i的集群做得很烂,所以可以认为是10g才有的);另外10g之前的集群需要第三方的cluster软件完成,10g后就有了oracle自己的CRS软件,并且是免费的,可以到官方下载。 数据库集群除正常的app拥有的(load banlance)负载均衡、(failover)失败切换,还有很多机制在内,包含主从关系、切换机制、以及分布式计算(网格计算(Grid)在ORACLE RAC中是一种最简单的实现方法,真正的网格计算是指在实际的网格环境下去管理网格下多个应用的数据库包括集群,他们是同一的,甚至于你无须关心网格下集群组之间的关系,就能非常清晰得去做操作了),这里的网格计算是指在一些大的统计下,在配置数据库参数时,将相应的INSTANCE参数设置为集群分组,并开启并行,在做一些大操作时就会实现多实例配合完成,也是通过心跳完成的。 数据库集群的负载均衡一般是通过app端完成,这部分可能是client端的TNS配置(此时前提是通过cluster完成使用同一个service_name对应多个SID),或者类似TNS配置在链接数据库的URL中,它内部一个重要参数就是LOAD_BALANCE等等,它可以设置为:(yes、on、true是等价的,不区分大小写,即开启负载均衡),相反,设置为(no、off、false)则为取消负载均衡,此时按照配置的远程主机IP或者域名的顺序逐个访问到一个可用的即可,此时一般会导致一台机器忙一台机器闲的情况,不过另一台机器如果只是用来做备机器,当一台挂掉后切换过去也是可以的,一般用RAC我们也会将该参数开启。 failover就是将数据库的SQL切换到另一个机器上,但是事务会被回滚,具体是否切换或者如何切换要看其它参数配置,首先FAILOVER参数和上面参数的参数值一样都是那样设置,当设置为开启状态就会进行失败切换,否则这个连接池的请求就会失败;而其它几个参数一般是在开启状态下有默认值的,自己也可以设置的哦,在FAILOVER_MODE配置中很多: 首先是TYPE参数的配置中一般有:session(失败时候,所有内容被中止,已经操作的事务被回滚,创建新的session到另一个可用实例上)、select(设置为该参数和上面差不多,不过切换时,开始被操作的事务虽然被回滚,但是如果是select语句不会被中断,会继续执行),none(不做任何操作,直接回滚,也不接管,用于测试,客户端会直接报错) 其次METHOD参数,这个参数一般是有:basic(在发生失败时候再在另一个实例上创建session回话节点)、preconnect(预先设立回话节点,有一定开销,但是切换速度很快速,在主从模式下推荐)而RETRIES分别代表重试次数(默认5)、DELAY代表每次重试时间片信息(默认1秒)、BACKUP(备份节点的网路服务名) 集群RAC由于设计更加专业于数据库应用,所以他比起HA更加适用于数据库,也是众多企业的选择,它配合data guard(有些是extend rac是包含了这两种功能)来完成备份,也有oracle的一直以来的终极备份方案rman来完成,不过前者更加偏重于容灾,还有些关于复制以及迁移等功能不是本文重点,不便多提及。 ORACLE RAC和相关的东西都是烧钱的东西,价格不菲,对各项硬件要求非常高,所以注意成本预算,如高速网络以及各个INSTANCE连接共享存储阵列的SAN交换机一般需要多个来冗余,心跳的交换机也需要冗余等等。 ORACLE RAC依赖于一个共享存储,做相应INSTANCE和数据库级别的管理,这也是数据库和实例的区别了,那么它的瓶颈就在后端了,所以后端很多时候会选择高端存储来完成;另外它还有很多全局资源管理使得它的很多发展在这些瓶颈上出现问题,如它的节点一般最多支持64节点,而随着节点数量的增加,成本会直线上升,至于性能是否能直线上升呢,你应该可以考虑下当前的各种瓶颈在哪里,也需要和实际情况结合才好说。 8、数据库容灾备份以及监控 接下来一个系统设计应该如何?需要做的就是容灾以及监控运行状况是否良好,对于app端一般不需要容灾,只需要监控,而其一般是通过监控内存、CPU、磁盘使用量(主要是日志和本地缓存文件);如果监控系统做得不好,那么我想很多DBA晚上睡不着(至于夜间做生产变更这类可以通过其他的自动化程序完成),系统的发展也会受到限制,我们需要一个伸缩性很强的系统就必然会走这一步。 而数据库容灾现在又很多方案,上面已经说了,现在比较多的就是使用dataguard备份到一个或多个备份机器上,dataguard上有多种配置机制,来实现各种常用的要求,关于磁盘管理可以使用ASM来管理,数据库也可以负责制过去,也可以异步通过程序度过去,也可以通过触发器+dblink过去等等都可以实现。关键看实际需求。 数据库的监控,这个oracle也提供了系列的监控软件(Statspace、AWR、logmgr等等系列),不过很多时候我们需要更加精确的参数需要自己去编码,否则就还是需要自己去查询很多自己做报表什么的,而且很不直观;长期需要监控的除了常用的IOPS、TPS、QPS以外,还需要关心很多如latch征用、sql parser(硬解析和软解析的各方面指标)、cache命中率、锁等待、内存指标变化、CPU指标变化、索引、磁盘碎片等等都需要得到全方位的监控 数据库的管理应当自动化,首先从监控下手,完全自动化管理和资源调配方面是一个理想,不过半自动化也是很容易的,就是在有问题或者在一定情况下有某种方式的通知,如短信息吧。这样DBA就不用成天盯着监控或者后台的某个字典表一直看了。 9、CDN思想基础 后面几个章节不是本文重点,简单阐述下即可,在高可用性网站设计中,即使前端应用增加了Memcached这类东西,不过始终不能很好的效果,要达到极佳的效果,因为很多时候跨网段的开销是非常大的,经过的路由器越多开销越大;其次很多时候,不愿意因为大文件输出(如视频下载)导致应用服务器宕机的事情,这是没有必要的,因为应用服务器更多关心的应该是业务处理。 CDN的出现就是为了解决这个问题,也就是网站加速器,他需要运营商的配合(具体细节请自己查阅资料),在很多地方建立站点,它需要做的事情就是托管DNS,通常DNS是解析域名成IP并访问对应IP内容,而CDN做了一层重写,就是通过域名解析得到的是一个CNAME,它按照提供CNAME会按照最短路径找到对应的CDN网点,接受数据,客户端的数据接受更加快速,并且可以实现冗余保护,另外它只是缓存在这里,可以认为是本地的一个私服,也就是需要跨网段的流量都切换到本地了,这里做一个极端的假设,就是跨网段的开销是2,本网段拖数据是1,有100个请求时,跨网段需要200的开销,而本地网段就只需要101个开销。 大文件下载,是通过缓存到本地的私服上,如视频下载就很多时候这段时间大家看的都是热播电影,就可以通过CDN来进行网站加速。 10、nosql思想 根据上面的描述,我们很多时候就不想做到百分百的数据安全,或者一致性吧,比如做一个网站的留言板,数据有一点偏差也无所谓,而且数据库的sql parser一般是很慢的,很容易达到极限,所以nosql的诞生就出现了,现在很多开源的nosql平台,它也是现有云存储的基础,apache的hadoop以及谷歌的mapreduce后来做了一个Percolator,还有redis、mongodb等等,其实所谓nosql基础的原理就是没有sql,就想刚才说的Memcache一样,只是它有存储以及根据设计不同,会有一些会存在一些锁机制,并且只是面向对象;有基于行存储的、有基于列存储的他们是根据实际应用场景设计的一种类似于数据库的东西,它具有极高的扩展性和伸缩性,因为控制完全在于你本身的架构和设计,也是我们一直所崇尚的:最好的东西肯定是最优秀的人根据实际的场景所架构出来的。 不论是哪一门,nosql它首先抛开的是sql parser的一种,但是它没有了SQL的支持,在一些复杂操作上显得比较困难(这些就要看具体场景和nosql的设计了);我们在结合上述几种技术的基础上如何不将Cached、nosql、RDBMS、app几个结合起来,向后端移动,实现app调用完全无需关心很多调用的细节,那么这就是真正的云存储了,因为是在分布式存储基础上以及cache管理的基础上实现了对应用的透明调用。 如何设计待以后专门有文章来阐述,今天只是一个开头而已。 11、无锁分析 通过上面的文章内容,我们在很多时候很多不必要的信息没有必要使用RDBMS一样的锁和同步等等动作,所以所谓真正意义上的无锁或者几乎无锁,就是将很多内容抽象出来利用间接的方法来实现。 一般来说降低锁的粒度有以下几种方法: a.使用hash、range、位图对数据进行提前分布,让其分框,根据实际情况而定,如果一个框只有一个线程在处理那么就几乎可以算是无锁了。 b.在一些特殊必要的应用中,使用特殊的方法来控制,变通的方法来控制,如队列中的对头和队尾算法,如果只有一个生产者和一个消费者可以让他们在一个定长数组下跑圈圈即可,后者永远追不上前者,而多生产者多消费者模式又该如何呢?比如多个线程做push操作,那么你只需要在多个线程以当前队头下标开始位置分配到不同的下标,几个线程就可以无锁操作了,那么如何分配到不同的下标呢?用java的volatile,你可以认为它是锁的,不过它非常轻量级的锁,只是在对使用volatile变量修改和读取过程中强制从从新内存中获取,而不是寄存器,所以在计数器使用中,多个线程去同时修改这个变量并获取到的值都是不同的;pop也是如此,这些有一定的应用场景,栈也可以用变通的手段得到解决。 c.还有一些通过版本号码、向量复制、脏块列表等等思想来实现,都有一些应用场景和方法;以及java提供的乐观锁机制(适用于非常多线程调用同一段代码,而不是循环非常多次去调用同一段代码)。 还有很多其他的知识可以借鉴,曾经看到过非常复杂的图形算法,而且是多维度的,太复杂了,所以这里就不说明了。 根据上述N多知识可以看出很多知识都是相通的,无非就是分解、根据实际情况命中与解锁,让更快的地方替换最慢的地方,让复杂的管理变得更加简单。 另一种无锁是一种变通的手段,就是单线程写操作了,也就是完全无锁的一种机制,其实你会觉得它很慢,经过测试发现,如果你的操作全是或者基本是OLTP中的小IO单个线程的写已经可以达到非常快速度,这种非常适合于写不多,但读非常多的系统,也就是读写分离,写全部在内存中完成,但是需要写日志,读是从多个散列主机上获取,但是也会从这个内存中获取相应数据,内存中为最新修改后得数据列,他们之间会在对应字段上以内存为主进行返回,这个机器只要内存足够大(现在稍微好点的PC SERVER几十G的内存非常容易),就可以承受非常大的修改,这个数据只需要在业务量较小的时候合并到静态数据中即可;那么当业务进行扩大,单线程无法承受的时候应该如何呢?内存也写不下了,那么此时又需要对其进行切割分离了,在业务和逻辑表上做一定的标识符号,类似于上述说到的volatile一样的东西,而写操作也可以类似于读操作一样的分层,这就越来越像Memcache+app+RDBMS这种结构了,只是它在Memcached有日志记录和恢复,并对于应用来说透明化了这种分布式的调用,它将整个体系向后端移动和抽象出来使得app的编程更加简单和方便,也就是app无需关心数据的具体位置在哪里,以及写到哪里去了,缓存在哪里,他们如何同步的,这就逐步可以认为是云存储和计算了,另外其精巧的设计不得不说是非常优秀的。
由于最近工作原因,很久没有在CSDN上留下些啥,今天在这些篇文章,是关于java多线程的。 对于JAVA多线程的应用非常广泛,现在的系统没有多线程几乎什么也做不了,很多时候我们在何种场合如何应用多线程成为一种首先需要选择的问题,另外关于java多线程的知识也是非常的多,本文中先介绍和说明一些常用的,在后续文章中如果有必要再说明更加复杂的吧,本文主要说明多线程的一下几个内容: 1、在应用开发中什么时候选择多线程? 2、多线程应该注意些什么? 3、状态转换控制,如何解决死锁? 4、如何设计一个具有可扩展性的多线程处理器? 5、多线程联想:在多主机下的扩展-集群? 6、WEB应用的多线程以及长连接原理。 1、在应用开发中什么时候选择多线程。 在前序的文章中已经简单提及到过一些关于多线程应用的文章,通过对web的一些线程控制对下载流量的控制,其实那只是雕虫小技,也存在很多的问题需要去解决,不过面对用户量不大的人群一般问题不大而已。 多线程在生活中的体现就是将多个同样很多事情交给多个人来并行的完成,而中间有一个主线程起到调度者的作用,运行者可以强制依赖于主线程的存在而存在,也可以让主线程依赖于自身;曾经我听很多人说过如果你的机器是单CPU,多线程没有意义,其实我并不这么认为,以为内单个CPU只能证明在线程被调度的瞬间只能同时执行一条最底层的命令,而并不代表不可以在CPU的征用上提高效率,一个是内存级别的,而另一个是CPU级别的,效率上仍然存在很大差距的;(这个可以让一个程序单线程去循环10亿次(每次自增1),和让十个线程独立运行1亿次也是同样的动作,记住这里不要将每条数据System.out.println出来,一个是机器扛不住,另一个是这里会对测试数据产生影响,因为这个方法我前面的文章中已经说明会产生阻塞,尤其是在并发情况下的阻塞,即使在单CPU下结果肯定也是有很大差距的,我这暂时没有单核的PC机器,所以没法得到一些测试结果数据给大家,请有条件的朋友自己测试一下)。 在现在的系统中无时无刻都离不开多线程的思想,包括集群、分布式都可以理解为多线程的一种原理,那么什么是多线程的原理呢?多线程和多进程的是什么呢? 其实要实现分布最简单的思想就是多进程,其实类似于在系统分隔过程中的一种垂直分隔,将不同业务的系统分布在不同的节点上运行,他们彼此互不干扰,而多进程的申请、释放资源各方面的开销都很大,而且占用资源并非CPU级别的,而线程是属于进程内部更细节的内容,一个进程内部可以分配N个线程,这些线程会并行的征用CPU资源,如果你的机器是多核的处理器,并发将会带来异常的性能提升,其实原理上就是在有限的资源下,如何发挥出最大的性能优势(但是一定是资源有一定余量的情况下,正所谓做事不能做得太绝)。 在java中常用于实现多线程的方法有3中: 1、继承于Thread类,重写run方法 2、实现Runable接口,实现run方法 3、实现Callable接口,实现call方法(具有返回值) 至于调用的方法多种多样,可以直接用start启动,也可以使用java.util.concurrent.Executors来创建线程池来完成,创建的线程池也主要分为: 1、Executors.newSingleThreadScheduledExecutor() 创建一个顺序执行的线程池,你在run方法内部无需使用synchronized来同步,因为它本身是顺序的。 2、Executors.newCachedThreadPool()创建一个线程池,线程会并行的去执行它。 3、Executors.newFixedThreadPool(10)创建大小为10的一个线程池,这个线程池最多创建长度为10的队列,如果超过10个,就最多有10个线程在执行,即可以控制线程的数量,也可以让其并行执行。 如果你的系统是一个WEB应用,建议尽量不要再web应用中做多线程,因为这部分线程控制主要是由web容器控制的,如果在非得必要的情况下建立,尽量建立较少,或者尽量将可以不太频繁调度的线程使用完后直接释放掉,哪怕下次重建也无所谓。 如果你的多线程序是独立运行的,专门用于接受和和处理一些消息,那么我相信最少有一个线程是不断探测的(有很多程序会先休眠一点时间,如:TimeUnit.MINUTES.sleep(SLEEP_TIME)此方法是按照毫秒级进行休眠一段时间),这类程序,最好将线程设置为后台线程(setDaemon(true),一定要在线程调用run之前调用该方法有效),后台线程和非后台线程最大的区别在于:后台线程在所有非后台线程死掉后,后台线程自动会被杀死和回收;而正如你写其他的多线程程序,即使你的main方法完成(主线程),但是在main中申请的子线程没有完成,程序仍然不会结束。 总的来说,其实几乎每时每刻写的代码都是多线程的,只是很多事情容器帮助我们完成了,即使编写本地的AWT、SWING,也在很多控制处理中式异步的,只是这种异步相对较少,更多的异步可以由程序去编写,自定义的多线程一般用于在独立于前段容器应用的后台处理中。为什么类似web应用的前端会把多线程早就处理好呢,一个是因为为了减少程序和bug,另外一个就是要写好多线程的确不容易,这样会使得程序员去关心更多没有必要关心的东西,也需要程序员拥有很高的水准,但是如果要成为好的程序员就一定要懂多线程,我们接下来以几个问题入手,再进行说明: 如果一个系统专门用于时钟处理、触发器处理,这个系统可能是分布式的,那么在一个系统内部应该如何编写呢?另外多线程中编写的过程中我们最郁闷的事情、也是最难琢磨补丁的是什么:多线程现在的运行状况是怎样的?我的这个线程不能死掉,如果死掉了我怎么发现?发现到了如何处理(自动、人工、难道重启)? 带着这些问题,我们引出了文章下面的一些话题。 2、多线程应该注意些什么? 多线程用起来爽,出现问题你就不是那么爽了,简单说来,多线程你最纳闷的就是它的问题;但是不要害怕它,你害怕它就永远不能征服它,呵呵,只要摸清楚一些脾气,我们总有办法征服它的。 ◆明白多线程有状态信息,和之间的转换规则? ◆多线程一般在什么情况下会出现焊住或者死掉的现象? ◆多线程焊住或者死掉如何捕获和处理? 这里仅仅是提出问题,提出问题后,在说到问题之前,先提及一下扩展知识点,下面的章节来说明这些问题。 开源多线程调度任务框架中的一个很好选择是:Quartz,有关它的文章可以到http://wenku.baidu.com/view/3220792eb4daa58da0114a01.html 下载这个文档,这个文档也讲述了大部分该框架的使用方法,不过由于该框架本身的封装层次较多,所以很多底层的实现内容并不是那么明显,而且对于线程池的管理基本是透明的,自己只能通过一些其他的手段得到这些内容。 所以拿到这个框架首先学习好它的特性后,进一步就是看如何进一步封装它得到最适合你项目的内容。 另外多线程在数据结构选项上也有很多技巧,关于多线程并发资源共享上的数据结构选型专门来和大家探讨,因为技巧的确很多,尤其是jdk 1.6以后提出了很多的数据结构,它参考了类似于oracle的版本号原理,在内存中做了数据复制以及原子拷贝的方法,实现了即保证一致性读写又在很大程度上降低了并发的征用;另外还有对于乐观锁机制,也是高性能的多线程设计中非常重要知识体系。 3、状态转换控制,如何解决死锁。 3.1.java默认线程的状态有哪些?(所谓默认线程就是自己没有重写) NEW :刚刚创建的线程,什么也没有做,也就是还没有使用start命令启动的线程。 BLOCKED :阻塞或者叫梗阻,也就是线程此时由于锁或者某些网络原因造成阻塞,有焊住的迹象。 WAITING:等待锁状态,它在等待对一个资源的notify,即资源的一个锁机会,这个状态一般和一个静态资源绑定,并在使用中有synchronzed关键字的包装,当使用obj.wait()方法时,当前线程就会等待obj对象上的一个notify方法,这个对象可能是this,如果是this的话那么在方法体上面一般就会有一个synchronized关键字。 TIME_WAITDE:基于时间的等待,当线程使用了sleep命令后,就会处于时间等待状态,时间到的时候,恢复到running状态。 RUNNING:运行状态,即线程正在处于运行之中(当线程被梗阻)。 TERMINATED:线程已经完成,此时线程的isAlive()返回为false。 一般默认的线程状态就是这些,部分容器或者框架会把线程的状态等进行进一步的封装操作,线程的名称和状态的内容会有很多的变化,不过只要找好对应的原理也不会脱离于这个本质。 3.1.线程一般在什么情况下会死掉? 锁,相互交叉派对,最终导致死锁;可能是程序中自己导致,编写共享缓存以及自定义的一部分脱离于容器的线程池管理这里就需要注意了;还有就是有可能是分布式的一些共享文件或者分布式数据库的锁导致。 网络梗阻,网络不怕没有,也不怕太快,就怕时快时慢,现在的话叫太不给力了,伤不起啊!而国内现在往往还就是这样不给力;当去网络通信调用内容的时候(包括数据库交互一般也是通过网络的),就很容易产生焊住的现象,也就是假死,此时很难判定线程到底是怎么了,除非有提前的监控预案。 其他情况下线程还会死掉吗?就我个人的经验来说还没遇到过,但并非绝不可能,我想在常规的同一个JVM内部操作的线程会死掉的概率只有系统挂掉,不然SUN的java虚拟机也太不让人信任了;至少从这一点上我们可以决定在绝大部分情况下线程阻塞的主要原因是上述两个主要来源。 在明白绝大部分原因的基础上,这里已经提出了问题并初步分析了问题,那么继续来如何解决这些问题,或者说将问题的概率降低到非常低的程度(因为没有百分百的高可用性环境,我们只是要尽量去做到它尽量完美,亚马逊的云计算也有宕机的惊人时刻,呵呵)。 3.1. 多线程焊住或者死掉如何捕获和处理? 说到捕获,学习java朋友肯定第一想到的是try catch,但是线程假死根本不会抛异常,如何知道线程死掉了呢? 这需要从我们的设计层面下手,对于后来java提供的线程池也可以比较放心的使用,但是对于很多非常复杂的线程管理,需要我们自己来设计管理。如何捕获我们用一个生活中的例子来举例,下一长中将它反馈到实际的系统设计上。 首先多线程自己死掉了它肯定不知道,就想一个人自己喝醉了或者被被人打晕了一样,呵呵,那么如何才能知道它的现状了?提出两种现实思路,一个是有一个跟班的人,而另一种是它上面有一个领导带一群人出来玩,下面人丢了一个它肯定要去找。 先看看第一种思路,跟班那个我假如他平时什么也不做,就跟在前者后面,当发现前者倒下,自己马上跟上去顶替工作,这也是系统架构上经常采用的冗余主从切换,可能一主多从;而云计算也是在基础上的进一步做的异地分流切换和资源动态调度(也就是事情少了,这些人可以去做其他的事情或者睡觉养精神并且为国家节约粮食,当这边的事情忙不过来,会有做其它事情的人或者待命的人来帮着做这些事情;甚至于此地遭到地震洪水类天灾什么的,异地还有机构可以顶替同样的工作内容,这样让对外的服务永远不断间断下来,也就是传说中的24*7的高可用性服务),但是这样冗余太大,成本将会非常巨大。 再看看第二种服务,上面有一个老大,它过一小会看看这帮小弟在做什么,是不是遇到了困难,那里忙它在上面动态调配这资源;好像这种模式很好呢?小弟要是多了,它就忙不过来了,因为资源的分配是需要提前明白下面资源的细节的,不然这个领导不是好领导;那么再细想下去,我们可以用多个老大,每个老大带领一个小团队,团队之间可以资源调配,但是团队内部可以由老大自己掌控一切,老大的上面还有个老总它只关心于老大再做什么,而不需要关心小弟们的行为,这样大家的事情就平均起来了;那么问题了又出来了,小弟的问题是可以透明的看到了,要是那个老大出事了甚至于老总出事了怎么办?此时结合第一种思想,我们此时就只需要再老总下面挂一个跟班的,集合两种模式的特征,也就是小弟不需要配跟班的,这样就节约了很大的成本(因为叶子节点的数量是最多的),而上面的节点我们需要有跟班的,如果想最大程度节约成本,只需要让主节点配置一个或者多个跟班就可以,但是这样恢复成本就上去了,因为恢复信息需要逐层找到内容才行,一般我们没有必要在这个基础上再进一步去节约成本。 这些是现实的东西,如何结合到计算机系统架构中,再回到本文的多线程设计上,第四章中一起来探讨一下。 4、如何设计一个具有可扩展性的多线程处理器。 其实在第三章中,已经从生活的管理模式上找到了很多的解决方案,这也是我个人在解决问题上惯用的手段,因为个人认为再复杂的数学算法也没有人性本身的复杂,生活中的种种手段若用于计算机中可能会得到很多神奇的效果。 如果自己不使用任何开源技术,要做一个多线程处理的框架应该从何下手,在上面分析的基础上,我们一般会将一个专门处理多线程的系统至少分解为主次二层,也就是主线程引导多个运行线程去处理问题;好了,此时我们需要解决以下几个问题: a)多个线程处理的内容是类似的,如何控制并发征用数据或者说降低并发热点的粒度。 方法1:hash散列思想将会是优秀的原则,按照数据特征进行分解数据框,每个框的数据规则按照一种hash规则分布,hash散列对于编程容易遍历,而且计算速度非常迅速,几乎可以忽略掉定位分组的时间,但结构扩展过程比较麻烦,但在多线程设计中一般不需要考虑这个问题。 方法2:range分布,range范 围分布数据是提前让管理者知道数据的大致分布情况,并按照一种较为平均的规则交给下面的运作线程去去处理自己范围内的数据,相互之间的数据也是没有任何交叉的,其扩展性较好,可以任意扩展,如果分解的数量不受控制的话,分解过多,会造成定位范围比较慢一点,但是多线程设计中也一般不用考虑这个问题,因为程序是由自己编写的。 方法3:位图分布,即数据具有位图规则,一般是状态,这种数据按照位图分布后,线程可以设立为位图个数,找到自己的位图段数据即可做操作,而不需要做进一步的更新,但是往往位图数量有限,而需要处理的数据量很大,一个线程处理一个位图下的所有数据也往往力不从心,若多个线程处理一个位图又会重蹈覆辙。 三种方法各自有优缺点,所以我们往往采用组合模式来讲真个系统的架构达到比较完美的状态,当然没有完美的东西,只有最适应于当前应用环境的架构,所以设计前需要考虑很多预见性问题;关于这种数据分布更多的用于架构,但是架构的基础也来源于程序设计思想,两者思想都是一致的,有关架构和数据存储分布,后面有机会单独讨论。 b)线程死掉如何发现(以及处理): 管理线程除有运行动作的线程外,还有1~N跟班,个数根据实际情况决定,至少要有一个当管理线程挂掉可以马上顶替工作,另外还有应当有一个线两程去定期检测线程的运行情况,由于它只负责这件事情,所以很简单,而且这一组中的线程谁死掉都可以相互替换工作并重启新的线程去替代,这个检测的周期不用太快、也不用太慢,只要应用可以接受就可以,因为挂掉些东西,应用阻塞一点时间是非常正常的事情。 发现线程有阻塞现象,在执行中找到了某种以外而阻塞,导致的原因我们上面已经分析过,解决的方法一般是在探测几次(这个次数一般是基于配置的)后发现都是处于阻塞状态,就基本可以认为它是错误的了;错误的情况此时需要给该线程执行一个interrupt()方法,此时线程内部的执行会自动的抛出一个异常,也就是理解执行线程的内容的时候尤其是带有网络操作的时候需要带上一个try catch,执行部分都在try中,当出现假死等现状的时候,外部探测到使用一个interrupt()方法,运行程序就会跳转到catch之中,这个里面就不存在征用资源的问题,而快速的将自己的需要回滚的内容执行完,并认为线程执行结束,相应的资源也会得到释放,而使用stop方法之所以现在不推荐是因为它不会释放资源,会导致很多的问题。 另外写代码之前如果涉及到一些网络操作,一定要对你所使用的网络交互程序有很多的深入认识,如socket交互时,一般情况下如果对方由于网络原因(一般是有IP当时端口不对或者网段的协议不通)导致在启动连接对方时,socket连接对方好几分钟后才会显示是超时连接,这是默认的,所以你需要提前设置一个启动连接超时保证网络是可以通信的,再进行执行(注意socket里面还有一个超时是连接后不断的时间,前者为连接之前设置的一个启动连接超时时间,一般这个时间很短,一般是2秒就很长了,因为2秒都连接不上这个网络就基本连接不上了,而后者是运行,有些交互可能长达几小时也有可能,但类似这种交互建议采用异步交互,以保证稳定运行)。 C)如果启动和管理二级管理线程组: 上面有一个主线程来控制启动和关闭,这里可以将这些线程在start前的setDaemon(true),那么该线程将会被设立为后台线程,所谓后台线程就是当主线程执行完毕释放资源后,被主线程创建的这些线程将会自动释放资源并死掉,如果一个线程被设置为后台线程,若在其run方法内部创建的其他子线程,将会自动被创建为后台线程(如果在构造方法中创建则不是这样)。 管理线程也可以像二级线程一样来管理子节点,只要你的程序不怕写得够复杂,虽然需要使用非常好的代码来编写,并且需要通过很复杂的测试才会稳定运行,但是一旦成功,这个框架将会是非常漂亮和稳定,而且也是高可用的。 5、多线程在多主机下的扩展-集群 其实我们在上面以及提及了一些分布式的知识,也可以叫做数据的分区知识(在网络环境利用PC实现类似于同一个主机上的分区模式,基本就可以称为数据是分布式存储的)。 但是这里提到的集群和这个有一些区别,可以说分布式中包含了集群的概念,但是一般集群的概念也有很多的区别,并且要分app集群和数据库集群。 集群一般是指同一个机组下多个节点(同一台机器也可以部署多个节点),这些节点几乎去完成同样的事情,或者说类似的事情,这就和多线程扯在一起了,多线程也正是如此,对比来看就是多线程调度在多主机群组下的实现,所以参照app集群来说,一般有一个管理节点,它几乎干很少的事情,因为我们不想让它挂掉,因为他虽然干的事情少,但是却非常重要,一个是从它那里可以得到每一个节点的一些应用部署和配置,以及状态等等信息;另外是代理节点或者叫做分发节点,它几乎在管理节点的控制之下只做分发的,当然要保证session一致性。 集群在多线程中的另一个体现就是挂掉一台,其余的可以顶替,而不会导致全盘死掉;而集群组相当于一个大的线程组,相关牵制管理,也相互可以失败切换,而多个业务会或者多种工具项会划分为不同的集群组,这就类似于我们设计线程中的三层线程模式的中多组线程组的模式,每组线程组内部都有自己个性化的属性和共享属性。 而面对数据库集群,就相对比app集群要复杂,app在垂直扩展时几乎只会受到分发节点能力的限制,而这部分是可以调整的,所以它在垂直扩展的过程中非常方便,而数据库集群则不一样,它必须保证事务一致性,并实现事务级别切换和一定程度上的网格计算能力,中间比较复杂的也在内存这块,因为它的数据读入到内存中要将多个主机的内存配置得像一个内存一样(通过心跳完成),而且需要得到动态扩展的能力,这也是数据库集群下扩展性收到限制发展的一个原因之一。 App难道没有和数据库一样的困难吗?有,但是粒度相对较小,app集群一般不需要考虑事务,因为一个用户的session一般在不出现宕机的情况下,是不会出现复制要求的,而是一直会访问指定的一台机器,所以它们之间几乎不需要通信;而耦合的粒度在于应用本身的设计,有部分应用系统会自己写代码将一些内容初始化注入到内存中,或者注入到app本地的一个文件中作为文件缓存;这样当这些数据发生改变时他们先改数据库,再修改内存或者通知内存失效;数据库由于集群使用心跳连接,所以保持一致性,而app这边的数据由于只修改掉了自身的内存相关信息,没有修改掉其他机器的内存信息,所以必然导致访问其他数据的机器上的内容是不一致的;至于这部分的解决方案,根据实际项目而定,有通过通信完成的,也有通过共享缓冲区完成(但这种方式又回到共享池资源征用产生的锁了),也有通过其他方式完成。 大型系统架构最终数据分布,集中式管理,分布式存储计算,业务级别横向切割,同业务下app垂直分隔,数据级别散列+range+位图分布结构,异地分流容灾,待命机组和资源调配的整合,这一切的基础都来源于多线程的设计思想架构在分布式机组上的实现。 6、WEB应用的多线程以及长连接原理 WEB应用中会对一些特殊的业务服务做特殊的服务器定制,类似一些高并发访问系统甚至于专门用于瞬间高并发的系统(很多时候系统不怕高并发,而是怕瞬间高并发)但他们的访问往往比较简单,主要用于事务的处理以及数据的一致性保障,他们在数据的处理上要求在数据库端也不允许有太大的计算量,计算一般在app中去完成,数据库一般只是做存、取、事务一致性动作,这类一般属于特殊的OLTP系统;还有大分类一类是属于并发量不算太大,但每次处理的数据和计算往往比较多,一把说的是OLAP类的系统,而数据的来源一般是OLTP,OLAP每次处理的数据量可能会非常大,一般在类型收集和统计上进行数据dump,需要将OLTP中的数据按照某种业务规则方面查询和检索的方法提取出来组织为有效信息存储在另一个地方,这个地方有可能还是数据库,但也有可能不是(数据库的计算能力虽然是数据上最强的但是它在实际应用中它是最慢的一种东西,因为数据库更多的是需要保证很多事务一致性和锁机制问题,以及一些中间解析和优化等等产生的开销是非常大的,而且应用程序与之交互过程是需要通过网络完成,所以很多数据在实际的应用中并不一定非要用数据库);这两类系统在设计和架构上都有很大的区别,但普通系统两者都有特征,但是都不是那么极端,所以不用考虑太多,这里需要提到的是一类非常特殊的系统,是实时性推送数据并高并发的系统,到目前为止我个人不知道将它归并到哪一类系统中,这的确很特殊的一类系统。 这类系统如:高并发访问中,而且需要将同一个平台下的数据让客户端较为实时的得到内容,这类网站不太可能一次获取非常多的内容到客户端再访问,而肯定是通过很多异步交互过程来完成的,下面简单说下这个异步交互。 Web异步交互的所有框架基础都是ajax,其余的类似框架都是在这个基础上完成的;那么此时ajax应该如何来控制交互才能得到几乎接近于实时的内容呢?难道通过客户端不断去刷新相同的URL?那要是客户端非常多,类似于一个大型网站,可能服务器端很快会宕机,除非用比正常情况高出很多倍的服务器成本去做,而且更多的服务器可能在架构上也需要改造才能发挥出他们的性能(因为在服务器的架构上,1 + 1永远是小于2的性能,更多的服务器在开销)。 想到的另一种办法就是从服务器端向客户端推送数据,那么问题是如何推送,这类操作是基于一种长连接机制完成,长连接即不断开的连接,客户端采用ajax与后端通信时,后端的反馈信息只要未曾断开就可视为一种长连接的机制;很多是通过socket与服务器端通信,也可以使用ajax,不过ajax需要在其上面做很多的处理才行。 服务器端也是必须使用对应的策略,现在较多的是javaNIO,相对BIO性能要低一点,但是也是很不错的,它在获取到用户请求时并不是马上为用户请求分配线程去处理,而是将请求进行排队,而排队的过程可以自己去控制粒度,而线程也将作为线程池的队列进行分配处理,也就是服务器端对客户端的请求是异步响应(注意这里不是ajax单纯的异步交互,而是服务器端对请求的异步响应),它对很多请求的响应并非及时,当发生数据变化时,服务器第一时间通过请求列表获取到客户端session列表并与之输出内容,类似于服务器端主动推送数据向客户端;而异步交互的好处是服务器端并不会为每一个客户端分配或新申请一个线程,这样会导致高并发时引起的资源分配不过来导致的内存溢出现象;解决了上述两个问题后,另外还有一个问题需要解决的是,当一个线程在处理一个请求任务时,由于线程处理一个任务完成前除非死掉或者焊住,否则是不会断开下来的,这个是肯定的(我们可以将一些大任务切割为一些小任务,线程就处理的速度就会快很多了),但是有一个问题是,服务器端的这个线程可能很快处理好了需要处理的数据内容并向客户端推送,但是客户端由于各类网络通信问题,导致迟迟不能接受完成,此时该线程也会被占用些不必要的时间,那么是否在这个中间需要进一步做一层断点传送的缓存呢?缓存不仅仅是属于在断点数据需要时取代应用服务器的内容,异步断点向客户端输出信息,同时将应用服务器处理的时间几乎全部集中在数据和业务处理,而不是输出网络上的很多占用,有关网络缓存有很多种做法,后续有机会和大家一起探讨关于网络缓存的知识吧。
关于JAVA网络编程的技术非常繁多,如:SOCKET、RMI、EJB、WEBSERVICE、MQ、中间数据等等方法,但是万变都是源于基础中通信原理,有些是轻量级的、有重量级的;有实时调用、有异步调用;这么多的技术可以说什么都可以用,关键在什么场合用什么最适合你,这些技术主要用于多个子系统之间相互通信的方法,如一个大型的软件应用分多个子系统,它们可能由不同的厂商来完成,这些子系统最终需要整合为一个系统,那么整合的基础就是交互,要么是通过数据交互,要么是通过接口调用,要么通过中间数据等等。本文从最基本的网络编程开始说起,逐步引入SOCKET的编程,其余的后续逐步加入。 付:学SCOKET一定要学会流,但是流也是JAVA语言上最难的其中之一,不过不用畏惧,因为JAVA语言本身比较简单,再难也难不倒那里去,JAVA最好的是设计和架构的思想,当然语言本身也具有一定的魅力,但语言本身我个人认为他不是JAVA长久不衰的资本。 1、JAVA读网页流文件 2、IP地址解析 3、最简单的SCOKET程序 4、通过SCOKET传送对象 5、SCOKET多线程交互或服务 6、广播方法 下面写入正文: 1、JAVA读网页流文件 //下面的代码是用于将百度首页的HTML内容读取到本地 import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; public class URLConnectTest { public static void main(String []agrs) { URLConnection conn = null; BufferedReader reader = null; try { URL url = new URL("http://www.baidu.com"); conn = url.openConnection(); conn.connect(); String contentType = conn.getContentType(); System.out.println("类型&字符集:"+contentType); System.out.println("文本长度:"+conn.getContentLength()); //System.out.println(conn.getDate()); //System.out.println(conn.getLastModified()); //System.out.println(conn.getExpiration()); reader = new BufferedReader(new InputStreamReader(conn.getInputStream(),contentType.substring(contentType.indexOf("charset=")+8))); String str = null; System.out.println("资源文件内容如下:"); while((str = reader.readLine())!=null) { System.out.println(str); } reader.close(); } catch (MalformedURLException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); }finally { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } } 这段代码运行后将会输出百度首页的HTML代码,你可以通过这些方法去挖掘一些网络资源信息,将其通过一定的规则解析后,得到相应可用数据到本地的数据库或者参数文件中,为你本地的系统提供一些辅助性的数据(如:天气预报、相关新闻等等作为本地系统的一种友情提示)。 这段应该很容看懂,不用多说,红色部分稍微有点点晕,这部分主要是为了保证字符集统一,方便在输出时解析为对应字符集进行输出,如:网站输出是UTF-8,通过通过conn.getContentType()获取到字符集为:text/html;charset=utf-8 ,此时是将utf-8这部分截取出来说明是需要解析的对应字符集,所以在这里使用了一个substring操作。 至于细节,这部分代码就不用多说,非常简单。 2、IP地址解析 IP地址解析,首先基础获取INTERNET对象的一种方法: InetAddress.getLocalHost() 获取本地主机的相关信息 InetAddress.getByName("www.baidu.com") 通过名称获取相应域名的相关信息的对象 其获取到的对象类型为:java.net.InetAddress类型,该类型的对象可以通过方法:getAddress()可以获取到相应地址的byte信息以及getHostAddress直接获取到IP地址、getHostName获取到主机名或域名。 下面先直接给出一段测试代码,方便查看: import java.net.InetAddress; import java.net.UnknownHostException; public class InetAddressTest { public static void main(String[] args) { try { displayOneAddress(InetAddress.getLocalHost(), "通过方法获取本机信息"); displayOneAddress(InetAddress.getByName("localhost"), "通过名称本机信息"); displaySomeAddress(InetAddress.getAllByName("www.baidu.com"), "通过名称获取百度的信息"); } catch (UnknownHostException e) { e.printStackTrace(); } } private static void displaySomeAddress(InetAddress[] address, String file) { for (int i = 0, size = address.length; i < size; i++) { displayOneAddress(address[i], file + "第" + (i + 1) + "个主机。"); } } private static void displayOneAddress(InetAddress address, String title) { System.out.print("/n" + title + "/t"); System.out.println(address); byte[] byte1 = address.getAddress(); if (byte1.length == 4) { System.out.print("IPV4协议。IP地址为:"); } else { System.out.print("IPV6协议。IP地址为:"); } for (int i = 0, size = byte1.length; i < size; i++) { int tmp = (byte1[i] >= 0) ? byte1[i] : (256 + byte1[i]); System.out.print(tmp + "."); } System.out.println("/n"+address.getHostAddress()); System.out.println("主机名:" + address.getHostName()); } } 在我本机运行后将会输出: 通过方法获取本机信息 xieyu/192.168.0.111 IPV4协议。IP地址为:192.168.0.111. 192.168.0.111 主机名:xieyu 通过名称本机信息 localhost/127.0.0.1 IPV4协议。IP地址为:127.0.0.1. 127.0.0.1 主机名:localhost 通过名称获取百度的信息第1个主机。 www.baidu.com/119.75.218.45 IPV4协议。IP地址为:119.75.218.45. 119.75.218.45 主机名:www.baidu.com 通过名称获取百度的信息第2个主机。 www.baidu.com/119.75.217.56 IPV4协议。IP地址为:119.75.217.56. 119.75.217.56 主机名:www.baidu.com 这里使用displayOneAddress方法主要就是用于查看一个主机对象下面的相关信息,以及分别使用那一种解析方法进行解析,即获取到的地址和实际的地址与256之间的关系;另外使用displaySomeAddress主要为了显示多个主机信息的情况上面分别显示了本机和通过主机名获取本机、百度(直接对外的,内部负载无法得知)主机信息。 3、最简单的SCOKET程序 socket编程也是非常古老但是一直没有过时的一种技术,因为它真的很不错,现在很多通信协议也都是基于SOCKET为基础编写的,尤其是胖客户端的平台或C/S结构,很多时候需要一种长连接机制来完成,使用SOCKET的确是很好的选择,这里首先给一个最简单的SOCKET程序开个头吧: 要写一个SCOKET程序最少要写两段代码来实现,即一个服务端、一个客户端,而且两段代码都要一起运行才能使得运行成功(所谓一起运行就是开两个窗口别分JAVA两个JAVA文件或者有集成工具也有其它的办法,启动一般是先启动服务器端,然后再启动客户端)。 此时首先构造服务器端的一段简单代码: import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; public class NormalServer { public static void main(String []agrs) { ServerSocket server = null; try { server = new ServerSocket(8080); System.out.println("服务器信息为:"+server.getInetAddress().getHostAddress()+"/t"+server.getLocalPort()); while(true) { Socket socket = server.accept();//等待接收一个请求 System.out.println("接收到一个请求:"+socket.getInetAddress()); BufferedReader sin = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter writer = new PrintWriter(socket.getOutputStream(),true); String str; System.out.println("开始读取数据。"); while(!"baibai".equals(str = sin.readLine())) { System.out.println("/t"+str); } System.out.println("服务器端读取数据完毕。"); writer.println("服务器端读取完毕!!!!!!!!!!"); writer.flush(); writer.close(); sin.close(); } } catch (IOException e) { e.printStackTrace(); }finally { if(server != null) { try { server.close(); } catch (IOException e) { e.printStackTrace(); } } } } } 这里在服务器端开辟了一个8080端口,如果你的8080端口被占用,可以换用其它端口即可,服务端读取数据并输出客户端发送的数据,待客户端有结束符号“baibai”的时候,就终止读取,并向客户端输出读取完毕的标志。 此时来编写一个客户端向服务器端发送数据: import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.net.UnknownHostException; public class NoramlClient { public static void main(String []agrs) { try { Socket socket = new Socket("localhost",8080); //BufferedReader pin = new BufferedReader(new InputStreamReader(System.in)); BufferedReader sin = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter writer = new PrintWriter(socket.getOutputStream(),true); String line = "谢宇"; writer.println(line); writer.println("baibai"); writer.flush(); System.out.println(sin.readLine()); sin.close(); //pin.close(); writer.close(); socket.close(); } catch (UnknownHostException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 此时创建一个和服务器端的链接,我这里使用的localhost代表我本地,具体情况可以具体修改,端口也是这样,在这个SCOKET基础上开通一个输入流和输出流,此时向服务器端发送数据,并得到服务器端返回的数据。 此时将两个JAVA文件分别编译成class文件,然后开启两个终端,第一个终端先将服务器端的class运行起来,然后再运行第二个class,第二个class可以反复执行。 可能大家代码都看得懂,估计就是这个“流”有点点晕,在这里先不提及流的细节,因为这个可能提及起来专门写几十页也写不完,而就SOCKET这部分首先明确一点,就是这是服务器端还是客户端,SCOKET对象是谁,好了,分两种情况说明: 1、服务器端得到的scoket对象是客户端发送过来的,这个套接字包含了客户端的相关信息以及连接信息,这个对象客户创建输出输出流,这个输入流就是输入到本机内存的意思,而输出流是输出到终端的意思,所以服务器端使用输入流来读取客户端的信息,因为输入流的信息就是读入到本机内存的,而通过输出流向客户端发送信息,就是向一个终端发送信息,这个终端我相信大家用的最多的就是屏幕,而屏幕输出用得最多的就是System.out.println(),自己看看这段代码的源码你会发现它也是由输出流来完成的,所谓输出就是输出到终端(终端可以是网络、可以是屏幕、可以是外围设备、可以磁盘文件等等),而他们在很多情况下都有一些默认值,如在WEB编程中的内置对象out,就是向网络客户端输出信息,它也是基于流去实现的。 2、客户端发送socket也是同样的道理,相对客户端,此时的输出流就是向服务器端输出了,而输出流就是 从服务器端获取到的信息。 不知道我这样解释是否能够听明白,不过我自己是这样理解的,对于流的概念还很多,这里只是开个头而已,如上面提及的System.out.print你可以让他不输出到屏幕,只需要你做一个相应的PringStream对象(假如叫做outer对象),它可以指向其他地方,如一个文件,此时通过设置:System.setOut(outer)来完成设置过程,此时当你使用System.out.println()的时候,它将不会再输出到屏幕,而是输出到文件,其余输入流也可以;流也有很多种,很多流之间存在一种规则性的转换方法,用好了有些时候如鱼得水,用不好则到处是问题。 4、通过SCOKET传送对象 未完待续。。。。。。。。。。。 5、SCOKET多线程交互或服务 6、广播方法
上一文中对ORACLE逻辑架构的基本体系做了说明,这部分是专门对SEGMENT部分进行说明,因为段才是管理对象的关键点,也是管理对象的基本,本文主要写 1、TABLE SEGMENT 2、INDEX SEGMENT 3、UNDO SEGMENT 4、LOBSEGMENT、LOB PARTITION、LOBINDEX、INDEX PARTITION 下面切入正题: 1、TABLE SEGMENT: TABLE的分类大致有: a、常规的堆表(HEAP表) b、索引组织表,带有主键组织的表,主键自动创建唯一性索引,并将索引段放置于数据段中,所以主键是最快的索引。 c、分区表,在TABLE PARTITION中简要说明下(在前序文章中已有专门说明分区表的文章)。 d、CLUSTER表,将多个关联表存放在一个段中,并将关联字段只保存一份,此时在经常关联的表中,将会大量节约关联时间。 e、COMPRESS表,对表的数据块会进行相应的压缩存储,支持高水位插入;在UPDATE的时候会出点点问题。 f、TEMPORARY表(临时表),其自动存放于临时表空间,分事务级别和会话级别。 常规HEAP表和索引组织表不用多说,当你创建表的时候,会在DBA_SEGMENT的时候自动创建一个同名的SEGMENT,这里主要说一下在进行大量删除操作后如何释放的问题: 做一个简单试验: SQL> drop table t1 purge; 表已删除。 SQL> create table t1 as select * from e 表已创建。 SQL> insert into t1 select * from t1; 已创建14行。 SQL> r 1* insert into t1 select * from t1 已创建28行。 SQL> r 1* insert into t1 select * from t1 已创建56行。 SQL> r 1* insert into t1 select * from t1 已创建112行。 SQL> r 1* insert into t1 select * from t1 已创建224行。 SQL> r 1* insert into t1 select * from t1 已创建448行。 SQL> commit; 提交完成。 SQL> select segment_name,BLOCKS,HEADER_BLOCK FROM dba_segments 2 WHERE segment_name='T1' AND OWNER='SCOTT'; SEGMENT_NAME BLOCKS HEADER_BLOCK --------------------------------------------------------------------------------- ---------- ------------ T1 16 59 SQL> delete from t1; 已删除896行。 SQL> commit; 提交完成。 SQL> select segment_name,BLOCKS,HEADER_BLOCK FROM dba_segments 2 WHERE segment_name='T1' AND OWNER='SCOTT'; SEGMENT_NAME BLOCKS HEADER_BLOCK --------------------------------------------------------------------------------- ---------- ------------ T1 16 59 SQL> alter table t1 move; 表已更改。 SQL> select segment_name,BLOCKS,HEADER_BLOCK FROM dba_segments 2 WHERE segment_name='T1' AND OWNER='SCOTT'; SEGMENT_NAME BLOCKS HEADER_BLOCK --------------------------------------------------------------------------------- ---------- ------------ T1 8 635 此时发现:通过MOVE操作,将会对表进行重定义,其实MOVE等价于MOVE TABLESPACE 同一个表空间;其HEADER_BLOCK也发生了变化,其实如果深入试验可以发现其DATA_OBJECT_ID也会发生变化,也就是再次回顾一下内容: 1、TRUNCATE、MOVE、SHRINK SPACE、REBUILD会发生行迁移。 2、TRUNCATE、MOVE、REBUILD会使得DATA_OBJECT_ID变化,因为DATA_OBJECT_ID是物理的,而OBJECT_ID是逻辑的。 3、TRUNCATE、MOVE、REBUILD会释放表空间信息,在对表进行MOVE应当对表的相应索引进行REBUILD,在线REBUILD应当使用ONLINE,等会说索引段的时候再说。 4、回顾ROWID生成规则(其实是ORACLE 8以后ROWID才有DATA_OBJECT_ID的组成,用于解决数据库的数据文件不能超过1023个的问题),由于DATA_OBJECT_ID才是物理的,所以MOVE表空间的时候,就是修改ROWID上的DATA_OBJECT_ID,它可以唯一确定一个表空间,也就是一个段必然存在于同一个表空间,而OBJECT_ID是逻辑上的引用,当数据文件上涨的过程中,会发现达到数据文件编号1023后,RFILE#字段从新从1开始计算,FILE#会继续长大,通过仔细研究ROWID可以发现,其使用10bit来存放文件编号,所以其上限为1024,所以不可能使用的FILE#作为这几位的标识码了。 5、仔细研究可以发现,DATA_OBJECT_ID是与SYS用的是数据字典:SGE$的字段HWMINCR,每次做类似操作,肯定是这个值的最大值,而OBJECT_ID则为DBA_OBJECTS的OBJECT_ID的最大值。这部分就不用做实验了,可以自己测试即可。 对于分区表,在上一次已经有很详细的说明http://blog.csdn.net/xieyuooo/archive/2010/03/31/5437126.aspx,上次也简单说了下通过SHRINK SPACE压缩表空间的过程,这里说下分区表也可以通过MOVE释放表空间: 根据分区表原理,其实分区表就是子表,最大的区别就是可以统一按照指定的规则进行管理,所以对于分区表也是可以压缩的: ALTER TABLE <表名称> MOVE PARTITION <分区名称>; 二级分区为: ALTER TABLE <表名称> MOVE SUBPARTITION <子分区的名字>; 这里也不多做实验了,可以自己做点分区表测试下就可以。 CLUSTER表,也算是比较少用的,它存在不少的BUG,但是也是可以解决的,方便于经常于进行关联的几个表,它是将这些表的数据存放在一个段内部(分区除外),或者说存放到一个表中,并且将关联字段只存放一份的方式,来提高性能(这是它说的,我们看了才知道),它如何创建,不知道,那么跟着ORACLE学习一下: 首先找几个系统的CLUSTER表: SQL> select segment_name from dba_segments 2 where segment_type='CLUSTER' 3 and rownum<10; SEGMENT_NAME --------------------------------------------- C_COBJ# C_TS# C_FILE#_BLOCK# C_USER# C_OBJ# C_MLOG# C_TOID_VERSION# C_RG# C_OBJ#_INTCOL# 随便找一个看看: SQL> select dbms_metadata.get_ddl('CLUSTER','C_RG#','SYS') from dual; DBMS_METADATA.GET_DDL('CLUSTER','C_RG#','SYS') ------------------------------------------------------------------------ CREATE CLUSTER "SYS"."C_RG#" ( "REFGROUP" NUMBER ) PCTFREE 10 PCTUSED 40 INITRANS 2 MAXTRANS 255 STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT) TABLESPACE "SYSTEM" PARALLEL (DEGREE 1 INSTANCES 1) 再找和CLUSTER相关的表(因为CLUSTER是为表服务的): SQL> select TABLE_NAME,CLUSTER_NAME FROM tabs 2 where cluster_name is not null 3 and rownum=1; TABLE_NAME CLUSTER_NAME ------------------------------ -------------- ICOL$ C_OBJ# SQL> select dbms_metadata.get_ddl('TABLE','ICOL$','SYS') from dual; DBMS_METADATA.GET_DDL('TABLE','ICOL$','SYS') -------------------------------------------------------------------- CREATE TABLE "SYS"."ICOL$" ( "OBJ#" NUMBER NOT NULL ENABLE, "BO#" NUMBER NOT NULL ENABLE, "COL#" NUMBER NOT NULL ENABLE, "POS#" NUMBER NOT NULL ENABLE, "SEGCOL#" NUMBER NOT NULL ENABLE, "SEGCOLLENGTH" NUMBER NOT NULL ENABLE, "OFFSET" NUMBER NOT NULL ENABLE, "INTCOL#" NUMBER NOT NULL ENABLE, "SPARE1" NUMBER, "SPARE2" NUMBER, "SPARE3" NUMBER, "SPARE4" VARCHAR2(1000), "SPARE5" VARCHAR2(1000), "SPARE6" DATE ) CLUSTER "SYS"."C_OBJ#" ("BO#") 原来CLUSTER表是这样关联上的,它真的能提高性能吗?我们做个试验看看吧: SQL> drop table t1 purge; 表已删除。 SQL> create table t1 as select * from emp where 1=2; 表已创建。 SQL> begin 2 for i in 1..5000 loop 3 INSERT INTO t1 values(i,'a'||i,'abc',10,sysdate,2000+i,20,10); 4 end loop; 5 end; 6 / PL/SQL 过程已成功完成。 SQL> commit; 提交完成。 SQL> create table t2 as select * from t1; 表已创建。 SQL> set autotrace traceonly; SQL> alter table t1 add primary key(empno); 表已更改。 SQL> alter table t2 add primary key(empno); 表已更改。 SQL> select * from t1,t2 where t1.empno=t2.empno; 已选择5000行。 执行计划 ---------------------------------------------------------- Plan hash value: 1838229974 --------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 5000 | 849K| 20 (5)| 00:00:01 | |* 1 | HASH JOIN | | 5000 | 849K| 20 (5)| 00:00:01 | | 2 | TABLE ACCESS FULL| T1 | 5000 | 424K| 9 (0)| 00:00:01 | | 3 | TABLE ACCESS FULL| T2 | 5000 | 424K| 10 (0)| 00:00:01 | --------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("T1"."EMPNO"="T2"."EMPNO") Note ----- - dynamic sampling used for this statement 统计信息 ---------------------------------------------------------- 515 recursive calls 0 db block gets 536 consistent gets 0 physical reads 0 redo size 210967 bytes sent via SQL*Net to client 4048 bytes received via SQL*Net from client 335 SQL*Net roundtrips to/from client 12 sorts (memory) 0 sorts (disk) 5000 rows processed SQL> create cluster 2 TEST_EMPNO#(empno number(4)); 簇已创建。 SQL> create index idx_test_cluster on cluster test_empno#; 索引已创建。 SQL> CREATE TABLE "SCOTT"."T1_2" 2 ( "EMPNO" NUMBER(4,0), 3 "ENAME" VARCHAR2(10), 4 "JOB" VARCHAR2(9), 5 "MGR" NUMBER(4,0), 6 "HIREDATE" DATE, 7 "SAL" NUMBER(7,2), 8 "COMM" NUMBER(7,2), 9 "DEPTNO" NUMBER(2,0), 10 PRIMARY KEY ("EMPNO") 11 ) cluster test_empno#(empno); 表已创建。 SQL> CREATE TABLE "SCOTT"."T2_2" 2 ( "EMPNO" NUMBER(4,0), 3 "ENAME" VARCHAR2(10), 4 "JOB" VARCHAR2(9), 5 "MGR" NUMBER(4,0), 6 "HIREDATE" DATE, 7 "SAL" NUMBER(7,2), 8 "COMM" NUMBER(7,2), 9 "DEPTNO" NUMBER(2,0), 10 PRIMARY KEY ("EMPNO") 11 ) cluster test_empno#(empno); 表已创建。 SQL> insert into t1_2 select * from t1; 已创建5000行。 SQL> insert into t2_2 select * from t1; 已创建5000行。 SQL> select * from t1_2,t2_2 where t1_2.empno=t2_2.empno; 已选择5000行。 执行计划 ---------------------------------------------------------- Plan hash value: 2653609197 ------------------------------------------------------------------------------------------ | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 3994 | 678K| 2199 (1)| 00:00:27 | | 1 | MERGE JOIN | | 3994 | 678K| 2199 (1)| 00:00:27 | | 2 | TABLE ACCESS CLUSTER| T2_2 | 5351 | 454K| 827 (1)| 00:00:10 | | 3 | INDEX FULL SCAN | IDX_TEST_CLUSTER | 5351 | | 26 (0)| 00:00:01 | |* 4 | SORT JOIN | | 3994 | 339K| 1372 (1)| 00:00:17 | | 5 | TABLE ACCESS FULL | T1_2 | 3994 | 339K| 1370 (1)| 00:00:17 | ------------------------------------------------------------------------------------------ Predicate Information (identified by operation id): --------------------------------------------------- 4 - access("T1_2"."EMPNO"="T2_2"."EMPNO") filter("T1_2"."EMPNO"="T2_2"."EMPNO") Note ----- - dynamic sampling used for this statement 统计信息 ---------------------------------------------------------- 53 recursive calls 1 db block gets 10942 consistent gets 0 physical reads 176 redo size 210967 bytes sent via SQL*Net to client 4048 bytes received via SQL*Net from client 335 SQL*Net roundtrips to/from client 3 sorts (memory) 0 sorts (disk) 5000 rows processed 其实上面可以发现它未必可以提高性能,通过分页也是类似结果,除了返回的BYTES减少了,其余的都在变大,所以有些东西一定要经过试验才可以放心使用,这类东西慎用,此时来看下如何删除它: SQL> drop cluster test_empno#; drop cluster test_empno# * 第 1 行出现错误: ORA-00951: 簇非空 查看是不是因为有数据的问题,我们把数据干掉: SQL> truncate cluster test_empno#; 簇已截断。 SQL> select * from t1_2; 未选定行 SQL> select * from t2_2; 未选定行 SQL> drop cluster test_empno#; drop cluster test_empno# * 第 1 行出现错误: ORA-00951: 簇非空 还是不行看来不是数据的问题,里面应该是放置了表,那么把表干掉: SQL> drop table t1_2; 表已删除。 SQL> drop table t2_2; 表已删除。 SQL> drop cluster test_empno#; 簇已删除。 SQL> show recyclebin; 尽然回收站没有东西,所以注意了,CLUSTER干掉后,回收站是不会有任何东西的。 COMPRESS表,就是所谓的压缩表,它创建的区别就是创建语句上有一个COMPRESS符号,如下: SQL> conn scott/a 已连接。 SQL> drop table tt purge; 表已删除。 --先创建一个普通表看下: SQL> CREATE TABLE T1 AS SELECT * FROM EMP ; 表已创建。 SQL> insert into t1 select * from t1; 已创建14行。 SQL> / 已创建28行。 SQL> / 已创建56行。 SQL> / 已创建112行。 SQL> / 已创建224行。 SQL> / 已创建448行。 SQL> / 已创建896行。 SQL> / 已创建1792行。 SQL> / 已创建3584行。 SQL> / 已创建7168行。 SQL> / 已创建14336行。 SQL> / 已创建28672行。 SQL> / 已创建57344行。 SQL> / 已创建114688行。 SQL> / 已创建229376行。 SQL> / 已创建458752行。 SQL> commit; 提交完成。 SQL> analyze table t1 compute statistics; 表已分析。 SQL> select blocks,empty_blocks from tabs where table_name='T1'; BLOCKS EMPTY_BLOCKS ---------- ------------ 5413 91 此时创建一个COMPRESS表来对比下: SQL> create table t2 compress as select * from t1; 表已创建。 SQL> analyze table t2 compute statistics; 表已分析。 SQL> select blocks,empty_blocks from tabs where table_name='T2'; BLOCKS EMPTY_BLOCKS ---------- ------------ 1382 26 数据块果然少了不少,我们使用普通插入的方法测试一下: SQL> truncate table t2; 表被截断。 SQL> analyze table t2 compute statistics; 表已分析。 SQL> select blocks,empty_blocks from tabs where table_name='T2'; BLOCKS EMPTY_BLOCKS ---------- ------------ 0 8 SQL> INSERT INTO t2 select * from t1; 已创建917504行。 SQL> commit; 提交完成。 SQL> analyze table t2 compute statistics; 表已分析。 SQL> select blocks,empty_blocks from tabs where table_name='T2'; BLOCKS EMPTY_BLOCKS ---------- ------------ 4906 86 看来这样插入效果不佳呀,那么用APPEND数据试一试: SQL> truncate table t2; 表被截断。 SQL> analyze table t2 compute statistics; 表已分析。 SQL> INSERT INTO t2 select * from t1; 已创建917504行。 SQL> truncate table t2; 表被截断。 SQL> INSERT /*+append*/INTO t2 select * from t1; 已创建917504行。 SQL> commit; 提交完成。 SQL> truncate table t2; 表被截断。 SQL> INSERT /*+append*/INTO t2 select * from t1; 已创建917504行。 SQL> commit; 提交完成。 SQL> analyze table t2 compute statistics; 表已分析。 SQL> select blocks,empty_blocks from tabs where table_name='T2'; BLOCKS EMPTY_BLOCKS ---------- ------------ 1382 26 此时发现对APPEND的数据是有效地,但是对于普通插入无效,对上述数据再做一下UPDATE操作: SQL> update t1 set sal=sal+1; 已更新917504行。 SQL> commit; 提交完成。 SQL> analyze table t2 compute statistics; 表已分析。 SQL> select blocks,empty_blocks from tabs where table_name='T2'; BLOCKS EMPTY_BLOCKS ---------- ------------ 6532 92 现在是想做的事情没有做成功,压缩表不但没压缩,比原有表还大?试一试MOVE是否好用: SQL> alter table t2 move; 表已更改。 SQL> analyze table t2 compute statistics; 表已分析。 SQL> select blocks,empty_blocks from tabs where table_name='T2'; BLOCKS EMPTY_BLOCKS ---------- ------------ 1382 26 TEMPORARY表:是存储在临时表空间的,ORACLE有些时候自己去高一些临时表来用于辅助完成数据的中间处理,如:排序、连接、分组等。 人工建立临时表一般用在两个地方: 1、当一个非常复杂的SQL中,对于几个表的查询关联是反复调用的,而且计算的结果是很小的,我们为了避免大量重复计算得到的一个小结果集,把它第一次计算的结果放在临时表中,便于反复使用。 2、过程中一个结果集用于过程中各个部分调用,方便中间存储,并且要求事务或者会话之间的数据要相互隔离。 临时表用到恰到好处可以出奇制胜,因为有些时候ORACLE的CBO在遇到一些问题的时候真的很傻,用临时表就是将我们需要执行的大步骤修改为一个一个小步骤,由程序来控制,因为这个部分我们写程序的人最清楚如何一个顺序是最快速的。 一个实际的例子: 表A、B、C依次一对多对应下去,A为引导表,一个查询语句:关联后,要求过滤掉C中存在FLAG=2的所有的A表相应的的记录,B表中过滤只需要STATUS='T'的记录,但是C中的FLAG=2不以B中的STATUS是否为T作为参照,只要等于2,A中相应的KEY全部过滤掉,当时第一遍有人写下来是需要运行九十多秒的SQL,优化这类SQL一般从两个思路下手: 1、临时表,因为不等于于等于两个问题,都是查询一样的三个表来回查询,都是大表,但是不符合的挺多。 2、转换思维模式,从侧面思考,FLAG=2是不是指定A对应C对应FLAG=2的个数,此时反向根据个数定位A的关键字,再次提取数据也是不错的做法。 如何创建临时表呢,临时表分为事务级别和会话级别,两种临时表怎么创建,不清楚就学学ORACLE: SQL> conn / as sysdba 已连接。 SQL> select table_name,temporary,duration 2 FROM tabs 3 where temporary ='Y' and rownum<10; TABLE_NAME T DURATION ------------------------------ - --------------- ATEMPTAB$ Y SYS$TRANSACTION MAP_OBJECT Y SYS$SESSION CLUSTER_DATABASES Y SYS$SESSION CLUSTER_NODES Y SYS$SESSION CLUSTER_INSTANCES Y SYS$SESSION PSTUBTBL Y SYS$SESSION WRI$_ADV_ASA_RECO_DATA Y SYS$SESSION ODCI_SECOBJ$ Y SYS$SESSION ODCI_WARNINGS$ Y SYS$SESSION 可以看到事务级别和SESSION级别都有,那么分别看看他们是怎么创建的: SQL> select dbms_metadata.get_ddl('TABLE','ATEMPTAB$','SYS') FROM dual; DBMS_METADATA.GET_DDL('TABLE','ATEMPTAB$','SYS') ----------------------------------------------------------------------- CREATE GLOBAL TEMPORARY TABLE "SYS"."ATEMPTAB$" ( "ID" NUMBER ) ON COMMIT DELETE ROWS --这就是事务级别的临时表,代表你做COMMIT操作的时候数据就被删掉了,当然包含DDL、DCL的隐含式提交方法。 SQL> select dbms_metadata.get_ddl('TABLE','MAP_OBJECT','SYS') FROM dual; DBMS_METADATA.GET_DDL('TABLE','MAP_OBJECT','SYS') ------------------------------------------------------------------------ CREATE GLOBAL TEMPORARY TABLE "SYS"."MAP_OBJECT" ( "OBJECT_NAME" VARCHAR2(2000), "OBJECT_OWNER" VARCHAR2(2000), "OBJECT_TYPE" VARCHAR2(2000), "FILE_MAP_IDX" NUMBER, "DEPTH" NUMBER, "ELEM_IDX" NUMBER, "CU_SIZE" NUMBER, "STRIDE" NUMBER, "NUM_CU" NUMBER, "ELEM_OFFSET" NUMBER, "FILE_OFFSET" NUMBER, "DATA_TYPE" VARCHAR2(2000), "PARITY_POS" NUMBER, "PARITY_PERIOD" NUMBER ) ON COMMIT PRESERVE ROWS --这就是会话级别的临时表,COMMIT时保存数据,创建临时表就是这么简单,为什么,因为ORACLE把复杂的问题简单化了,你使用的时候就会觉得就这么回事,不过所有东西切忌滥用,适当使用,在适当的时候使用,用好了,就会的心应手。 至于细节这里就不多说了,自己可以写几个表来做测试。 2、INDEX SEGMENT 索引段,所有索引按照B+树管理模式,唯一性索引找到唯一ROWID直接回表,普通索引根据索引顺序链表查找相应符合条件的ROWID然后再回表,位图索引在叶子块中标记每个ROWID对于位图健是否符合条件的情况用0和1表达,所以位图用来统计很方便,但是OLTP系统中经不起频繁的修改。 索引也有分区索引,在分区表那篇文章中也已经详细说明,索引也是用块存储数据,存储键值+ROWID,非叶子节点记录:层次、叶子块、键值起始位。理论上一个块可以存放733行数据上线,实际一般存放六百多行就是上线了,根据实际键值大小有所关系,一个快存放不下,就找两个块,两个块需要一个管理者,树就加为2层,同理,当叶子块达到六七百个的时候,一个头管不住,就在请一个头,两个头就需要更高的管理者来管理,树就变为三层了。 还有更多的索引内容,为了说明这些,做一些简单试验: SQL> create table t1 as select * from emp; 表已创建。 SQL> create index idx_emp on t1(empno); 索引已创建。 SQL> desc user_extents; 名称 ---------------------- SEGMENT_NAME PARTITION_NAME SEGMENT_TYPE TABLESPACE_NAME EXTENT_ID BYTES BLOCKS SQL> select extent_id,blocks from user_extents; EXTENT_ID BLOCKS ---------- ---------- 0 8 SQL> analyze index IDX_EMP VALIDATE STRUCTURE; 索引已分析 SQL> select HEIGHT,BLOCKS,BR_BLKS,LF_BLKS,DEL_LF_ROWS 2 FROM index_stats; SQL> select HEIGHT,BLOCKS,BR_BLKS,LF_ROWS,DEL_LF_ROWS 2 FROM index_stats; HEIGHT BLOCKS BR_BLKS LF_BLKS LF_ROWS DEL_LF_ROWS ---------- ---------- ---------- ------------- ---------- ----------- 1 8 0 1 14 0 解释下这几个字段: HEIGHT:索引层数,也是树的高度 BLOCKS:索引块数 BR_BLKS:非叶子节点的块数目 LF_ROWS:叶子节点索引值的总行数 DEL_LF_ROWS:被删除的行 SQL> insert into t1 select * from emp; 已创建14行。 SQL> commit; 提交完成。 SQL> analyze index idx_emp validate structure; 索引已分析 SQL> select HEIGHT,BLOCKS,BR_BLKS,LF_BLKS,LF_ROWS,DEL_LF_ROWS 2 FROM index_stats; HEIGHT BLOCKS BR_BLKS LF_BLKS LF_ROWS DEL_LF_ROWS ---------- ---------- ---------- ------------- ---------- ----------- 1 8 0 1 28 0 重复执行这几行: insert into t1 select * from t1; commit; analyze index idx_emp validate structure; select HEIGHT,BLOCKS,BR_BLKS,LF_BLKS,LF_ROWS,DEL_LF_ROWS FROM index_stats; 执行多次后,当数据量有八百多行的时候(每个8K的数据块最多存储733行数据) SQL> SELECT HEIGHT,BLOCKS,BR_BLKS,LF_BLKS,LF_ROWS,DEL_LF_ROWS 2 FROM index_stats; HEIGHT BLOCKS BR_BLKS LF_BLKS LF_ROWS DEL_LF_ROWS ---------- ---------- ---------- ---------- ---------- ----------- 2 8 1 2 896 0 此时发现:BR_BLKS从0变成1,LF_BLKS从1变成2,而LF_ROWS始终与数据行数一致,HEIGHT变成了2。 SQL> insert into t1 select * from t1; 已创建896行。 SQL> / 已创建114688行。 SQL> commit; 提交完成。 SQL> analyze index idx_emp validate structure; 索引已分析 SQL> select HEIGHT,BLOCKS,BR_BLKS,LF_BLKS,LF_ROWS,DEL_LF_ROWS 2 FROM index_stats; HEIGHT BLOCKS BR_BLKS LF_BLKS LF_ROWS DEL_LF_ROWS ---------- ---------- ---------- ---------- ---------- ----------- 3 768 3 671 229376 0 此时发现:HEIGHT变成了3层结构 BR_BLKS有三个,即非叶子节点上面又有一个管理节点,即,第一层一个节点,下面管理两个中间节点,中间节点再在下面管理六百多个块,每个块内部最多管理七百多行ROWID,也就是两层的结构的索引极限时可以存储42万条数据的索引树,但是由于实际存储的建值数据,这个试验中的数据存储量是实际的一半,每个块大概有三百多行数据的ROWID信息,也就是三层结构内存储100万行数据应该是问题不大的,除非联合索引存储的数据导致。 这里做一下UPDATE语句: SQL> update t1 set empno=30 where rownum=1; 已更新 1 行。 SQL> commit; 提交完成。 SQL> analyze index idx_emp validate structure; 索引已分析 SQL> select height,blocks,br_blks,br_rows,lf_rows,lf_blks,DEL_LF_ROWS 2 from index_stats; HEIGHT BLOCKS BR_BLKS BR_ROWS LF_ROWS LF_BLKS DEL_LF_ROWS ---------- ---------- ---------- ---------- ---------- ---------- ----------- 3 768 3 670 229377 671 1 发现BR_ROWS增加了一个,而DEL_LF_ROWS变成了1,说明索引是先删除,再插入的,数据是直接删掉,但是索引不是,因为索引是有序的。 --删除数据: SQL> delete from t1 where rownum<229400; 已删除229374行。 SQL> commit; 提交完成。 SQL> analyze index idx_emp validate structure; 索引已分析 SQL> select height,blocks,br_blks,br_rows,lf_rows,lf_blks,DEL_LF_ROWS 2 from index_stats; HEIGHT BLOCKS BR_BLKS BR_ROWS LF_ROWS LF_BLKS DEL_LF_ROWS ---------- ---------- ---------- ---------- ---------- ---------- ----------- 3 768 3 670 229374 671 229374 此时发现索引的块个数和高度没有任何变化,要是CBO此时选择走索引,肯定是得不偿失,我们通过压缩来看看是否可以释放掉: (alter index idx_emp coalesce;和下面的语句一样的效果:) SQL> alter index idx_emp shrink space; 索引已更改。 SQL> analyze index idx_emp validate structure; 索引已分析 SQL> select height,blocks,br_blks,br_rows,lf_rows,lf_blks,DEL_LF_ROWS 2 from index_stats; HEIGHT BLOCKS BR_BLKS BR_ROWS LF_ROWS LF_BLKS DEL_LF_ROWS ---------- ---------- ---------- ---------- ---------- ---------- ----------- 3 8 2 0 0 1 0 发现压缩的确释放掉叶子节点的BLOCK以及索引行数,但是为什么层数以及非叶子节点个数没有任何变化呢?---索引最好的维护方法是:REBUILD,压缩时小动作,REBUILD是物理上重建: SQL> alter index idx_emp rebuild; 索引已更改。 SQL> analyze index idx_emp validate structure; 索引已分析 SQL> select height,blocks,br_blks,br_rows,lf_rows,lf_blks,DEL_LF_ROWS 2 from index_stats; HEIGHT BLOCKS BR_BLKS BR_ROWS LF_ROWS LF_BLKS DEL_LF_ROWS ---------- ---------- ---------- ---------- ---------- ---------- ----------- 1 8 0 0 0 1 0 如果是在线系统需要使用ONLINE: ALTER INDEX idx_emp rebuild online; 否则很可能报错,因为REBUILD重建时需要表处于静态情况下完成的(没有未提交的事务),ONLINE代表找到一个事务中的空隙去执行REBUILD操作。 最后做个测试,当数据大量删除后,MOVE表和TRUNCATE表是否需要维护索引: SQL> create index idx_t2 on t2(deptno); 索引已创建。 SQL> analyze index idx_t2 validate structure; 索引已分析 SQL> select height,blocks,LF_ROWS,LF_BLKS,BR_BLKS,DEL_LF_ROWS from index_stats; HEIGHT BLOCKS LF_ROWS LF_BLKS BR_BLKS DEL_LF_ROWS ---------- ---------- ---------- ---------- ---------- ----------- 2 16 5000 10 1 0 SQL> select status from user_indexes where index_name='IDX_T2'; STATUS -------- VALID SQL> SELECT OBJECT_ID,DATA_OBJECT_ID FROM DBA_OBJECTS WHERE OWNER='SCOTT' AND OBJECT_NAME='IDX_T2'; OBJECT_ID DATA_OBJECT_ID ---------- -------------- 58675 58675 SQL> delete from t2; 已删除5000行。 SQL> commit; 提交完成。 SQL> analyze index idx_t2 validate structure; 索引已分析 SQL> select height,blocks,LF_ROWS,LF_BLKS,BR_BLKS,DEL_LF_ROWS from index_stats; HEIGHT BLOCKS LF_ROWS LF_BLKS BR_BLKS DEL_LF_ROWS ---------- ---------- ---------- ---------- ---------- ----------- 2 16 5000 10 1 5000 SQL> alter table t2 move; 表已更改。 SQL> analyze index idx_t2 validate structure; 索引已分析 SQL> select height,blocks,LF_ROWS,LF_BLKS,BR_BLKS,DEL_LF_ROWS from index_stats; HEIGHT BLOCKS LF_ROWS LF_BLKS BR_BLKS DEL_LF_ROWS ---------- ---------- ---------- ---------- ---------- ----------- 2 16 5000 10 1 5000 --这里MOVE表后,索引没有直接维护,所以MOVE后索引需要手工维护。 SQL> truncate table t2; 表被截断。 SQL> analyze index idx_t2 validate structure; 索引已分析 SQL> select height,blocks,LF_ROWS,LF_BLKS,BR_BLKS,DEL_LF_ROWS from index_stats; HEIGHT BLOCKS LF_ROWS LF_BLKS BR_BLKS DEL_LF_ROWS ---------- ---------- ---------- ---------- ---------- ----------- 1 8 0 1 0 0 SQL> SELECT OBJECT_ID,DATA_OBJECT_ID FROM DBA_OBJECTS WHERE OWNER='SCOTT' AND OBJECT_NAME='IDX_T2'; OBJECT_ID DATA_OBJECT_ID ---------- -------------- 58675 58677 TRUNCATE后,连带索引一起自动维护了,所以TRUNCATE可以不用重新REBUILD索引。 关于索引的规则后面专门说明一些说明:INDEX UNQUE SCAN、INDEX RANGE SCAN、INDEX FAST FULL SCAN、INDEX FULL SCAN、INDEX SKIP SCAN,因为ORACLE SQL优化并不是一两句话可以说明白的,也不是肯定怎么优化就是正确的,很多时候要结合实际问题实际分析才可以解决问题,这里说明只是说明索引应当定期进行相应的维护,尤其是表在做一些大量变化后。 3、UNDO SEGMENT 首先看下系统那些段类型: SQL> select distinct segment_type from dba_segments; SEGMENT_TYPE ------------------ LOBINDEX INDEX PARTITION TABLE PARTITION NESTED TABLE ROLLBACK LOB PARTITION LOBSEGMENT INDEX TABLE CLUSTER TYPE2 UNDO 可以看到有ROLLBACK、TYPE2 UNDO两类回滚段信息,貌似看不懂,那么看下这两类回滚段是什么: SQL> SELECT segment_type,segment_name,tablespace_name from dba_segments 2 where segment_type in('ROLLBACK','TYPE2 UNDO'); SEGMENT_TYPE SEGMENT_NAME TABLESPACE_NAME ------------------ ----------------------------- --------------- ROLLBACK SYSTEM SYSTEM TYPE2 UNDO _SYSSMU1$ UNDOTBS1 TYPE2 UNDO _SYSSMU2$ UNDOTBS1 TYPE2 UNDO _SYSSMU3$ UNDOTBS1 TYPE2 UNDO _SYSSMU4$ UNDOTBS1 TYPE2 UNDO _SYSSMU5$ UNDOTBS1 TYPE2 UNDO _SYSSMU6$ UNDOTBS1 TYPE2 UNDO _SYSSMU7$ UNDOTBS1 TYPE2 UNDO _SYSSMU8$ UNDOTBS1 TYPE2 UNDO _SYSSMU9$ UNDOTBS1 TYPE2 UNDO _SYSSMU10$ UNDOTBS1 此时可以发现ROLLBACK是SYSTEM系统类并存在于SYSTEM表空间的一个回滚段,它不对外公开的,用于系统在创建和删除对象时对数据字典生成回滚段信息,而我们使用的是默认在UNDOTBS1表空间默认创建的回滚段,用另一种方式看下再用的回滚段信息: SQL> select * from v$rollname; USN NAME ---------- ------------------------------ 0 SYSTEM 1 _SYSSMU1$ 2 _SYSSMU2$ 3 _SYSSMU3$ 4 _SYSSMU4$ 5 _SYSSMU5$ 6 _SYSSMU6$ 7 _SYSSMU7$ 8 _SYSSMU8$ 9 _SYSSMU9$ 10 _SYSSMU10$ 已选择11行。 SQL> select tablespace_name,segment_name,status 2 from dba_rollback_segs 3 order by 1; TABLESPACE_NAME SEGMENT_NAME STATUS ------------------------------ ------------------------------ -------- SYSTEM SYSTEM ONLINE UNDOTBS1 _SYSSMU2$ ONLINE UNDOTBS1 _SYSSMU3$ ONLINE UNDOTBS1 _SYSSMU4$ ONLINE UNDOTBS1 _SYSSMU10$ ONLINE UNDOTBS1 _SYSSMU6$ ONLINE UNDOTBS1 _SYSSMU7$ ONLINE UNDOTBS1 _SYSSMU8$ ONLINE UNDOTBS1 _SYSSMU9$ ONLINE UNDOTBS1 _SYSSMU1$ ONLINE UNDOTBS1 _SYSSMU5$ ONLINE 回滚段用来干什么呢? 1、事务的回退:事务内部要么全部成功,要么全部失败,以前的数据会保存在回退段中,保证可以回退。 2、系统的恢复:当系统宕机、SHUTDOWN ABORT并启动时,此时通过日志将提交的事务提交写入数据文件,未提交的事务用回滚段信息覆盖回去。 3、读一致性,两个会话之间读取彼此隔离,一个会话未提交的事务,另一个回话无法读取;大的SELECT语句,以起始SCN号码为基准读取数据,若数据被修改,即使COMMIT,也从回滚段中获取,若还未读到那个数据,回滚段被冲掉,那么将会报错。 4、闪回历史数据信息,通过回滚段可以对历史数据进行闪回操作,ORACLE 10G已经非常容易简单。 --简单事务实验: SQL> SELECT xidusn from v$transaction; 未选定行 SQL> update scott.emp set sal=100 where rownum=1; 已更新 1 行。 SQL> SELECT xidusn from v$transaction; XIDUSN ---------- 9 此时说明此事务使用的回滚段为9号回滚段,即:_SYSSMU9$。那么回滚段如何管理: SQL> show parameter undo NAME TYPE VALUE ------------------------------------ ----------- -------- undo_management string AUTO undo_retention integer 900 undo_tablespace string UNDOTBS1 undo_management=AUTO,代表回滚段为自动管理,undo_retention=900,代表15分钟(900秒)内数据是肯定可以在回滚段中找到的,但是超过15分钟要看运气,因为它被冲掉的可能性也不一定15分钟马上就被冲掉了,undo_tablespace就不用多说了。 此时看下10g一般是怎么回滚的(9i比较麻烦一点,需要通过dbms_flashback包去做): 首先修改一个表: SQL> update scott.t1 set sal=1000 where empno=7369; 已更新 1 行。 SQL> commit; 提交完成。 SQL> select versions_starttime,versions_endtime, 2 versions_xid,versions_operation,t1.* 3 FROM scott.T1 versions between timestamp minvalue and maxvalue 4 where empno=7369; VERSIONS_STARTTIME VERSIONS_ENDTIME VERSIONS_XID V EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO ---------- --------- ------- ---------- ---------- --------- ---------- -------------- ---------- ---------- -- 16-8月 -10 02.56.09 下午 07001600C6320100 U 7369 SMITH CLERK 7902 17-12月-80 1000 20 16-8月 -10 02.56.09 下午 7369 SMITH CLERK 7902 17-12月-80 2000 20 可以看到回滚段中的数据、被修改的时间、被修改的事务号码、做的何种操作。 通过事务号码查询要进行回滚操作的SQL语句: SQL> SELECT UNDO_SQL FROM FLASHBACK_TRANSACTION_QUERY WHERE XID='07001600C6320100'; UNDO_SQL ------------------------------------------------------------------------------------- update "SCOTT"."T1" set "SAL" = '2000' where ROWID = 'AAAOYlAAGAAAAQ0AAA'; 通过表名称回滚数据信息: SQL> SELECT UNDO_SQL FROM FLASHBACK_TRANSACTION_QUERY WHERE TABLE_NAME='T1'; UNDO_SQL ----------------------------------------------------------------------------- update "SCOTT"."T1" set "SAL" = '2000' where ROWID = 'AAAOYlAAGAAAAQ0AAA'; 按照指定时间点查询数据: SQL> select * from SCOTT.t1 2 as of timestamp to_timestamp('2010-08-16 14:56:08','YYYY-MM-DD HH24:MI:SS'); EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO ---------- ---------- --------- ---------- -------------- ---------- ---------- ---------- 7369 SMITH CLERK 7902 17-12月-80 2000 20 7499 ALLEN SALESMAN 7698 20-2月 -81 1600 300 30 7521 WARD SALESMAN 7698 22-2月 -81 1250 500 30 7566 JONES MANAGER 7839 02-4月 -81 2975 20 7654 MARTIN SALESMAN 7698 28-9月 -81 1250 1400 30 7698 BLAKE MANAGER 7839 01-5月 -81 2850 30 7782 CLARK MANAGER 7839 09-6月 -81 2450 10 7788 SCOTT ANALYST 7566 19-4月 -87 3000 20 7839 KING PRESIDENT 17-11月-81 5000 10 7844 TURNER SALESMAN 7698 08-9月 -81 1500 0 30 7876 ADAMS CLERK 7788 23-5月 -87 1100 20 7900 JAMES CLERK 7698 03-12月-81 950 30 7902 FORD ANALYST 7566 03-12月-81 3000 20 7934 MILLER CLERK 7782 23-1月 -82 1300 10 回滚数据: SQL> alter table scott.t1 enable row movement; 表已更改。 SQL> flashback table scott.t1 to timestamp to_timestamp('2010-08-16 14:56:08','YYYY-MM-DD HH24:MI:SS'); 闪回完成。 SQL> alter table scott.t1 disable row movement; 表已更改。 也可以通过上面的闪回SQL去进行回滚,另外可以发现的是闪回SQL是按照ROWID去完成的,所以如果数据发生错乱,一定不要执行MOVE、SHRINK SPACE等等操作,否则就麻烦了。 回滚段常规管理中: 创建一个自己的回滚表空间: SQL> create undo tablespace ud datafile 'D:/ORACLE/ORADATA/ORCL102/uu.dd' size 2m; 表空间已创建。 SQL> select tablespace_name,segment_name,status 2 from dba_rollback_segs 3 order by 1; TABLESPACE_NAME SEGMENT_NAME STATUS ------------------------------ ------------------------------ ---------------- SYSTEM SYSTEM ONLINE UNDOTBS1 _SYSSMU1$ ONLINE UNDOTBS1 _SYSSMU10$ ONLINE UNDOTBS1 _SYSSMU9$ ONLINE UNDOTBS1 _SYSSMU8$ ONLINE UNDOTBS1 _SYSSMU7$ ONLINE UNDOTBS1 _SYSSMU6$ ONLINE UNDOTBS1 _SYSSMU5$ ONLINE UNDOTBS1 _SYSSMU4$ ONLINE UNDOTBS1 _SYSSMU3$ ONLINE UNDOTBS1 _SYSSMU2$ ONLINE TABLESPACE_NAME SEGMENT_NAME STATUS ------------------------------ ------------------------------ ---------------- UD _SYSSMU19$ OFFLINE UD _SYSSMU18$ OFFLINE UD _SYSSMU17$ OFFLINE UD _SYSSMU16$ OFFLINE UD _SYSSMU20$ OFFLINE UD _SYSSMU14$ OFFLINE UD _SYSSMU13$ OFFLINE UD _SYSSMU12$ OFFLINE UD _SYSSMU11$ OFFLINE UD _SYSSMU15$ OFFLINE SQL> alter system set undo_tablespace=UD; 系统已更改。 SQL> select * from v$rollname; USN NAME ---------- ------------------- 0 SYSTEM 11 _SYSSMU11$ 12 _SYSSMU12$ 13 _SYSSMU13$ 14 _SYSSMU14$ 15 _SYSSMU15$ 16 _SYSSMU16$ 17 _SYSSMU17$ 18 _SYSSMU18$ 19 _SYSSMU19$ 20 _SYSSMU20$ SQL> select tablespace_name,segment_name,status 2 from dba_rollback_segs 3 order by 1; TABLESPACE_NAME SEGMENT_NAME STATUS ------------------------------ ------------------------------ ------- SYSTEM SYSTEM ONLINE UNDOTBS1 _SYSSMU1$ OFFLINE UNDOTBS1 _SYSSMU10$ OFFLINE UNDOTBS1 _SYSSMU9$ OFFLINE UNDOTBS1 _SYSSMU8$ OFFLINE UNDOTBS1 _SYSSMU7$ OFFLINE UNDOTBS1 _SYSSMU6$ OFFLINE UNDOTBS1 _SYSSMU5$ OFFLINE UNDOTBS1 _SYSSMU4$ OFFLINE UNDOTBS1 _SYSSMU3$ OFFLINE UNDOTBS1 _SYSSMU2$ OFFLINE TABLESPACE_NAME SEGMENT_NAME STATUS ------------------------------ ------------------------------ ------- UD _SYSSMU19$ ONLINE UD _SYSSMU18$ ONLINE UD _SYSSMU17$ ONLINE UD _SYSSMU16$ ONLINE UD _SYSSMU20$ ONLINE UD _SYSSMU14$ ONLINE UD _SYSSMU13$ ONLINE UD _SYSSMU12$ ONLINE UD _SYSSMU11$ ONLINE UD _SYSSMU15$ ONLINE 此时系统的回滚段就切换到UD中来了,做一个非常小的回滚段表空间,先将系统的回滚段表空间设置回去,然后将UD删掉,并物理上删除文件: SQL> create undo tablespace ud datafile 'D:/ORACLE/ORADATA/ORCL102/uu.dd2' size 128k; 表空间已创建。 SQL> alter system set undo_tablespace=ud; 系统已更改。 SQL> select * from v$rollname; USN NAME ---------- ------------------------------ 0 SYSTEM 此时发现没有自己创建的回滚段信息,因为128K太小了,无法弄,那么此时修改操作会出现什么情况? SQL> update scott.emp set sal=2000 where empno=7369; update scott.emp set sal=2000 where empno=7369 * 第 1 行出现错误: ORA-01552: 非系统表空间 'USERS' 不能使用系统回退段 SQL> alter database datafile 'D:/ORACLE/ORADATA/ORCL102/uu.dd2' resize 2m; 数据库已更改。 SQL> update scott.emp set sal=2000 where empno=7369; 已更新 1 行。 SQL> select * from v$rollname; USN NAME ---------- ------------------------------ 0 SYSTEM 11 _SYSSMU11$ 12 _SYSSMU12$ 不够用的时候自动就扩大了,当然是保证表空间有空间的情况。 当认为干预回滚段表空间管理后,此时可以根据需要在表空间内部创建回滚段以及设置某个大的提交指定回滚段,一面造成回滚段不够出现的错误: 设置事务的回滚段为某一个指定的回滚段,可以手工调整这个回滚段的属性: SET TRANSTRACTION USE ROLLBACK SEGMENT rollback_segment 回滚段内部以区位单位进行事务的写操作,每个块内部最多包含一个事务的信息,顺序按照EXTENTS去编写回滚信息,写满后若收到空间限制,将会从新写第一个EXTENT。 设置一个回滚段的回收信息,当回滚段需要回收时,是否进行回收后到那个位置由参数OPTIMAL 决定的: ALTER ROLLBACK SEGMENT rollback_segment STORAGE(OPTIMAL 1M); 设置某个回滚段是否启用,前面是以表空间为单位,这里是以段为单位: ALTER ROLLBACK SEGMENT rollback_segment ONLINE|OFFLINE; 手工压缩回滚段信息: ALTER ROLLBACK SEGMENT rollback_segment SHRINK TO 2M; 若不加2M参数将会自动以OPTIMIAL参数为基准进行压缩。 删除某回滚段信息: DROP ROLLBACK SEGMENT rollback_segment ; 不过现在基本都是自动管理,自动管理下对于ORA-015555发生概率要低很多了。 4、LOBSEGMENT、LOB PARTITION、LOBINDEX、INDEX PARTITION 本来写这篇文章写到上面就差不多了,不过由于上次我在工作中远程和另外几个子系统的同事处理一个严重的表空间浪费的问题,刚开始我由于在远程,所以很多情况不是很清楚,就知道表空间浪费很严重,也只大概清楚使用情况,后面对内部的SEGMENTS使用情况排序了一下,发现尽然是LOB,在这个问题的解决过程中发现诸多的一些规律和过程,并根据部分规律推算到有几个表,一个表占用120G左右的空间,而且还释放不掉,以及一些ORACLE对于BLOB和CLOB的空间管理秘密吧,这里给大伙分享一下。 首先我们来创建一个具有BLOB和CLOB的表: SQL> conn scott/a 已连接。 SQL> drop table t1; 表已删除。 SQL> purge table t1; 表已清除。 SQL> create table t1(c1 number,c2 clob,c3 blob); 表已创建。 此时查看一下当前用户的段信息由哪些: SQL> select SEGMENT_NAME,SEGMENT_TYPE,BLOCKS 2 FROM dba_segments 3 where owner='SCOTT'; SEGMENT_NAME SEGMENT_TYPE BLOCKS ------------------------- ------------------ ---------- DEPT TABLE 8 EMP TABLE 8 BONUS TABLE 8 SALGRADE TABLE 8 TEST TABLE 8 V_USER_OBJECTS TABLE 1536 V_DBA_OBJECTS TABLE 1536 TEST_OBJECTS TABLE 32 TABLE_01 TABLE 8 TABLE_02 TABLE 8 PERSON TABLE 8 TABLE_AJ_TEST TABLE 8 AAA TABLE 8 LODER_EMP TABLE 8 PK_DEPT INDEX 8 PK_EMP INDEX 8 DEPT1 TABLE 8 SYS_IL0000052116C00003$$ LOBINDEX 8 SYS_LOB0000052116C00003$$ LOBSEGMENT 8 T1 TABLE 8 SYS_IL0000052116C00002$$ LOBINDEX 8 SYS_LOB0000052116C00002$$ LOBSEGMENT 8 注意:红色的是我们创建的表,蓝色的是什么呢?就是BLOB和CLOB,这么怪异的名字是怎么个组合方式,我琢磨了半天,去尝试发现到了,这个段名字首先以“SYS_”开头,标注为“IL”代表是LOB字段的索引名字(因为LOB是单独存放的),标注为“LOB”代表为LOB字段的实际存储,接着是10位数字编码,这10位是以OBJECT_ID为标准左边补充0直到10位(这部分就不是DATA_OBJECT_ID了,ORACLE使用OBJECT_ID的其中一个意义也在于此,就是逻辑引用对象不会发生改变,这样在做表的MOVE、TRUNCATE的时候,相应的LOB应用无需跟着变化),上述的T1表的OBJECT_ID也就是:52116,可以通过DBA_OBJECTS WHERE OBJECT_ID=对应值,进行测试。另外C开头后携带5位数字编码代表的是这个对象或者说是这个表的第几个字段,如何看第几个字段:DESC的顺序输出,或者DBA_TAB_COLUMNS中的COLUMN_ID字段确定顺序号码。当然你也可以通过第三方工具查看,它已经为你完成中间的排序过程。此时很容易发现就是: SYS_IL0000052116C00003$$ T1表第三个字段LOB字段,它是这个LOB字段的索引段。 SYS_LOB0000052116C00003$$ T1表第三个字段LOB的信息存储字段。 SYS_IL0000052116C00002$$ T1表第二个字段为LOB字段,它是这个LOB字段的索引段。 SYS_LOB0000052116C00002$$ 表是第二个字段LOB的信息存储字段。 分析完后,下面写一个过程来模拟插入BLOB和CLOB数据信息: 首先为了加载外部文件先要创建一个文件加载的字典信息,我们将其默认到C盘根目录下: SQL> CREATE DIRECTORY BFILE_DATA AS 'c:/'; 目录已创建。 在这个目录下,我放置了一个图片信息,名称为:xieyu.jpg,此时创建一个过程方便我们测试,代码如下: CREATE OR REPLACE PROCEDURE P_ADD_INFO(key_id NUMBER) IS V_FILE BFILE := BFILENAME('BFILE_DATA', 'xieyu.jpg'); V_CLOB CLOB := EMPTY_CLOB(); V_BLOB BLOB := EMPTY_BLOB(); V_DESC_OFF NUMBER; V_SRC_OFF NUMBER:=1; V_APPEND_INFO VARCHAR2(256) := 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxfdfdfsdxxxxxfdfdddddddddxxfdfdfsddddddddddddddddddddddddddddd'; BEGIN INSERT INTO t1 VALUES(key_id,V_CLOB,V_BLOB); DBMS_LOB.CREATETEMPORARY(V_CLOB, TRUE); DBMS_LOB.CREATETEMPORARY(V_BLOB, TRUE); DBMS_LOB.OPEN(V_CLOB, DBMS_LOB.LOB_READWRITE); DBMS_LOB.OPEN(V_BLOB, DBMS_LOB.LOB_READWRITE); --这里是为了将数据多复制几次 FOR i IN 1..50 LOOP DBMS_LOB.WRITE(V_CLOB, length(V_APPEND_INFO), DBMS_LOB.getlength(V_CLOB)+1, V_APPEND_INFO); END LOOP; dbms_lob.fileopen(V_FILE, dbms_lob.file_readonly); V_DESC_OFF := dbms_lob.getlength(V_FILE); DBMS_LOB.loadblobfromfile(V_BLOB,V_FILE,DBMS_LOB.lobmaxsize,dest_offset => V_DESC_OFF,src_offset => V_SRC_OFF); UPDATE t1 SET c2 = V_CLOB,c3=V_BLOB WHERE c1 = key_id; IF DBMS_LOB.ISOPEN(V_CLOB) = 1 THEN DBMS_LOB.CLOSE(V_CLOB); END IF; dbms_lob.fileclose(V_FILE); dbms_lob.close(V_BLOB); END P_ADD_INFO; 创建完成过程后,就准备开始调用过程,我们的KEY是自己知道的一个关键字就行了,我们顺序写入1~10条信息,看看情况如何: SQL> begin 2 for i in 1..10 loop 3 p_add_info(i); 4 end loop; 5 end; 6 / PL/SQL 过程已成功完成。 SQL> commit; 提交完成。 此时再来看下段的变化情况: SQL> select SEGMENT_NAME,SEGMENT_TYPE,BLOCKS 2 FROM dba_segments 3 where owner='SCOTT'; SEGMENT_NAME SEGMENT_TYPE BLOCKS ------------------------- ------------------ ---------- DEPT TABLE 8 EMP TABLE 8 BONUS TABLE 8 SALGRADE TABLE 8 TEST TABLE 8 V_USER_OBJECTS TABLE 1536 V_DBA_OBJECTS TABLE 1536 TEST_OBJECTS TABLE 32 TABLE_01 TABLE 8 TABLE_02 TABLE 8 PERSON TABLE 8 TABLE_AJ_TEST TABLE 8 AAA TABLE 8 LODER_EMP TABLE 8 PK_DEPT INDEX 8 PK_EMP INDEX 8 DEPT1 TABLE 8 SYS_IL0000052116C00003$$ LOBINDEX 8 SYS_LOB0000052116C00003$$ LOBSEGMENT 384 T1 TABLE 8 SYS_IL0000052116C00002$$ LOBINDEX 8 SYS_LOB0000052116C00002$$ LOBSEGMENT 32 好了,问题来了,当数据被删除的时候,数据能否像普通表一样压缩掉呢?做个测试吧: SQL> DELETE FROM T1; 已删除10行。 SQL> commit; 提交完成。 SQL> alter table t1 move; 表已更改。 SQL> select SEGMENT_NAME,SEGMENT_TYPE,BLOCKS 2 FROM dba_segments 3 where owner='SCOTT'; SEGMENT_NAME SEGMENT_TYPE BLOCKS ------------------------- ------------------ ---------- DEPT TABLE 8 EMP TABLE 8 BONUS TABLE 8 SALGRADE TABLE 8 TEST TABLE 8 V_USER_OBJECTS TABLE 1536 V_DBA_OBJECTS TABLE 1536 TEST_OBJECTS TABLE 32 TABLE_01 TABLE 8 TABLE_02 TABLE 8 PERSON TABLE 8 TABLE_AJ_TEST TABLE 8 AAA TABLE 8 LODER_EMP TABLE 8 PK_DEPT INDEX 8 PK_EMP INDEX 8 DEPT1 TABLE 8 SYS_IL0000052116C00003$$ LOBINDEX 8 SYS_LOB0000052116C00003$$ LOBSEGMENT 384 T1 TABLE 8 SYS_IL0000052116C00002$$ LOBINDEX 8 SYS_LOB0000052116C00002$$ LOBSEGMENT 32 看来MOVE表对LOB字段毫无作用,通过SHRINK SPACE或着DEALLOCATE等操作结果也是一样没有任何效果,很纳闷,难道这块空间真的就浪费了吗?还是它被删除掉后可以重复利用?到底怎么删除才有效? 我们先看下它是否可以重复利用吧: 此时数据已经被删掉,如果可以重复利用,再插入10条相同的数据,应该对它的空间影响不大才是,我们怀着这样的结论做一下试验: SQL> begin 2 for i in 1..10 loop 3 p_add_info(i); 4 end loop; 5 end; 6 / PL/SQL 过程已成功完成。 SQL> select SEGMENT_NAME,SEGMENT_TYPE,BLOCKS 2 FROM dba_segments 3 where owner='SCOTT'; SEGMENT_NAME SEGMENT_TYPE BLOCKS ------------------------- ------------------ ---------- DEPT TABLE 8 EMP TABLE 8 BONUS TABLE 8 SALGRADE TABLE 8 TEST TABLE 8 V_USER_OBJECTS TABLE 1536 V_DBA_OBJECTS TABLE 1536 TEST_OBJECTS TABLE 32 TABLE_01 TABLE 8 TABLE_02 TABLE 8 PERSON TABLE 8 TABLE_AJ_TEST TABLE 8 AAA TABLE 8 LODER_EMP TABLE 8 PK_DEPT INDEX 8 PK_EMP INDEX 8 DEPT1 TABLE 8 SYS_IL0000052116C00003$$ LOBINDEX 8 SYS_LOB0000052116C00003$$ LOBSEGMENT 768 T1 TABLE 8 SYS_IL0000052116C00002$$ LOBINDEX 8 SYS_LOB0000052116C00002$$ LOBSEGMENT 48 看来不会重复利用这些空间,或者说很大部分不能重复利用,再来一次一样的实验: SQL> delete from t1; 已删除10行。 SQL> commit; 提交完成。 SQL> begin 2 for i in 1..10 loop 3 p_add_info(i); 4 end loop; 5 end; 6 / PL/SQL 过程已成功完成。 SQL> select SEGMENT_NAME,SEGMENT_TYPE,BLOCKS 2 FROM dba_segments 3 where owner='SCOTT'; SEGMENT_NAME SEGMENT_TYPE BLOCKS ------------------------- ------------------ ---------- DEPT TABLE 8 EMP TABLE 8 BONUS TABLE 8 SALGRADE TABLE 8 TEST TABLE 8 V_USER_OBJECTS TABLE 1536 V_DBA_OBJECTS TABLE 1536 TEST_OBJECTS TABLE 32 TABLE_01 TABLE 8 TABLE_02 TABLE 8 PERSON TABLE 8 TABLE_AJ_TEST TABLE 8 AAA TABLE 8 LODER_EMP TABLE 8 PK_DEPT INDEX 8 PK_EMP INDEX 8 DEPT1 TABLE 8 SYS_IL0000052116C00003$$ LOBINDEX 8 SYS_LOB0000052116C00003$$ LOBSEGMENT 1024 T1 TABLE 8 SYS_IL0000052116C00002$$ LOBINDEX 8 SYS_LOB0000052116C00002$$ LOBSEGMENT 72 继续应征了它不可重复利用的事实,我们唯一可以做的就是手工回收: 1、 通过字段的SHRINK SPACE来完成 2、 删除表或者TRUNCATE表来完成 两者各有优缺点,如何根据实际业务来把握,等试验搞定后,我们来看下: 先测试下SHRINK SPACE: SQL> delete from t1; 已删除10行。 SQL> commit; 提交完成。 SQL> alter table t1 modify lob(c2) (shrink space); 表已更改。 SQL> select SEGMENT_NAME,SEGMENT_TYPE,BLOCKS 2 FROM dba_segments 3 where owner='SCOTT'; SEGMENT_NAME SEGMENT_TYPE BLOCKS ------------------------- ------------------ ---------- DEPT TABLE 8 EMP TABLE 8 BONUS TABLE 8 SALGRADE TABLE 8 TEST TABLE 8 V_USER_OBJECTS TABLE 1536 V_DBA_OBJECTS TABLE 1536 TEST_OBJECTS TABLE 32 TABLE_01 TABLE 8 TABLE_02 TABLE 8 PERSON TABLE 8 TABLE_AJ_TEST TABLE 8 AAA TABLE 8 LODER_EMP TABLE 8 PK_DEPT INDEX 8 PK_EMP INDEX 8 DEPT1 TABLE 8 SYS_IL0000052116C00003$$ LOBINDEX 8 SYS_LOB0000052116C00003$$ LOBSEGMENT 1024 T1 TABLE 8 SYS_IL0000052116C00002$$ LOBINDEX 8 SYS_LOB0000052116C00002$$ LOBSEGMENT 8 果然搞定了,继续将其用于C3也是一样的效果,我们这次用TRUNCATE来做: SQL> truncate table t1; 表被截断。 SQL> select SEGMENT_NAME,SEGMENT_TYPE,BLOCKS 2 FROM dba_segments 3 where owner='SCOTT'; SEGMENT_NAME SEGMENT_TYPE BLOCKS ------------------------- ------------------ ---------- DEPT TABLE 8 EMP TABLE 8 BONUS TABLE 8 SALGRADE TABLE 8 TEST TABLE 8 V_USER_OBJECTS TABLE 1536 V_DBA_OBJECTS TABLE 1536 TEST_OBJECTS TABLE 32 TABLE_01 TABLE 8 TABLE_02 TABLE 8 PERSON TABLE 8 TABLE_AJ_TEST TABLE 8 AAA TABLE 8 LODER_EMP TABLE 8 PK_DEPT INDEX 8 PK_EMP INDEX 8 DEPT1 TABLE 8 SYS_IL0000052116C00003$$ LOBINDEX 8 SYS_LOB0000052116C00003$$ LOBSEGMENT 8 T1 TABLE 8 SYS_IL0000052116C00002$$ LOBINDEX 8 SYS_LOB0000052116C00002$$ LOBSEGMENT 8 在SHRINK SPACE中我们很清楚他是将被删掉的行对应的LOB字段信息进行回收,上次对于远程的大表,我首选了這种方式去解决,在多次测试中我还是蛮有信心可以解决这个问题的,可以有些事实往往很难像试验那么顺利,因为表太大了,120G一个表,我们将这个命令在SQLPLUS下执行了一个通宵,最后窗口钉死在那里了也没有压缩完,也不知道后台成啥样子了,看来第一次失败了,于是考虑到后期维护方便,我们必须对这么大的表进行分割处理,即减小维护的粒度,做分区表。 注意:做完分区表后,对每一个分区有一个段,而LOB也是一样,每一个LOB字段对应每个分区都会产生一个LOB索引段和一个LOB段,这个测试细节就不多说了。 做完分区后对应的段类别为:LOB PARTITION 和INDEX PARTITION 根据现场的业务,保存将近3个月左右的信息,按照这类型的表的情况,首先创建了一个新的表空间,这个表空间的数据文件指定了足够的初始化大小以及手工指定为16K大小的BLOCKSIZE,专门用来存放LOB大字段信息,然后做分区表,将数据和索引信息按照原有规则防止与两个指定的表空间,LOB的放置于新创建这个表空间中,按照时间做了分区,将分区表命名为原表的名称的一个副本,然后白天将运行时数据的N-1一个分区开始后台复制(因为有时间的顺序性,除最后一个分区,其余的分区都是定好的数据),目标分区对于所有的LOB的STORAGE的大小进行初始化大小以及NEXT的设置来提高插入的效率,并设置PCTFREE为0来节约存储空间。 到晚上暂停掉和相关表的业务,将原表进行重命名,创建一个和原表同名的结构重启业务运行,这个过程大概几十秒钟搞定,保证业务正常运行,然后将原表最后一部分数据移植到分区表中,这个过程大概花了40多分钟,但是不影响正常运行,因为有一个临时创建的表正在运行,这个表在这段时间不会有太多数据,此时再次暂停掉这部分业务工作,将这个临时表的数据也转移到分区表中,然后将原表和这个临时表都删掉,将分区表命名为原表名称,中间总共停止业务时间不会超过2分钟,将120G的数据表完全移植到分区表中,经过测试,分区表通过对分区的管理(不过总体这个过程大概需要4个小时左右),不论使用SHRINK SPACE还是DROP、TRUNCATE都是非常快速的了,每次维护几乎都是瞬间完成,这几个大表的容量目前控制在40G左右的位置,将不可回收的资源可以自动或者手工的任意管理了。 我们在实际的生产运行中遇到的问题是和实验有些不一致,不过明白原理的基础上使用将会更加得心应手,在很多原理的基础上,构造一些自动化和半自动化的管理机制。
本文相对较为简单,简单介绍一下ORACLE后台进程(ORACLE的INSTANCE主体是由内存+后台进程组成),其中部分也是备份与恢复的关键点,本文主要说一下ORACLE后台进程的工作原理,首要分类的是将ORACLE后台进程分为:独立模式、共享模式,我们一般采用独立模式,也就是会话的后台进程是独立的,共享模式相对来说有一个分配资源和并行处理的,所以用于MTS系统中,暂时不考虑这方面的问题,简单说下进程吧: 1、ORACLE进程查询介绍 2、核心进程PMON说明 3、核心进程SMON说明 4、核心进程DBWR说明 5、核心进程LGWR说明 6、CKPT说明 8、其它一些进程 正文(跟着操作一遍即可): 1、ORACLE进程查询介绍 --首先登陆数据库: SQL> connect / as sysdba; 已连接。 --查看SGA的信息,10g才有的视图 --ORACLE 10G后才可以使用的命令SQL> select * from v$sgainfo; NAME BYTES RES-------------------------------- ---------- ---Fixed SGA Size 1249992 NoRedo Buffers 7135232 NoBuffer Cache Size 398458880 YesShared Pool Size 121634816 YesLarge Pool Size 4194304 YesJava Pool Size 4194304 YesStreams Pool Size 0 YesGranule Size 4194304 NoMaximum SGA Size 536870912 NoStartup overhead in Shared Pool 50331648 NoFree SGA Memory Available 0 --看下SGA大小: SQL> show parameter sga_t; NAME TYPE VALUE------------------------------------ ----------- -------------------sga_target big integer 512M --提取已经分配的后台进程:SQL> select name,DESCRIPTION from v$bgprocess where paddr<>'00'; NAME DESCRIPTION----- -----------------------------------------------------------PMON process cleanupPSP0 process spawner 0MMAN Memory ManagerDBW0 db writer process 0LGWR Redo etc.CKPT checkpointSMON System Monitor ProcessRECO distributed recoveryCJQ0 Job Queue CoordinatorQMNC AQ CoordinatorMMON Manageability Monitor Process NAME DESCRIPTION----- -----------------------------------------------------------MMNL Manageability Monitor Process 2 在这里看到很多进程,红色标注部分全部为核心进程,CKPT虽然不是核心进程,但是也很重要,所谓核心进程是,实例中要是这些进程死掉了,实例只有重启,没法处理,其余非核心进程死掉,可以处理,通过ALTER SYSTEM REGISTER,即通过配置重新注册一次,有部分修改的配置信息,须立即生效的,也是通过这个命令完成,下面对核心进程和非核心进程说明一下。 2、核心进程PMON说明 全称为:process cleanup ,表示进程清理,负责将死掉的进程杀掉,如连接到数据库后,断掉网络这些进场将会被杀掉,如果是死锁掉的,需要人工干预后才能回收。 3、核心进程SMON说明 System Monitor Process:1、做资源回收、数据库崩溃时自我修复。回收资源包含:排序的、回退的、DROP掉的表,将资源合并;2、当数据库异常关闭时,启动中SMON后台进程通过控制文件发现日志文件和数据文件不一致,通过日志文件恢复数据文件,而动作由SMON执行。 4、核心进程DBWR说明 db writer process 0(db代表DATABASE):带下标的,代表有一组后台进程,不带下标的,都是独立的一个进程运行;DBWR进程,最多20个,最少一个。一般和CPU有关系,根据CPU进行相应的设置。这类进程做一件事情就是写脏数据的过程。dirty buffer。 DBWR触发条件: 1、脏数据库太多,没有多余的缓冲区来存放了。2、超时 3秒左右3、CKPT,检查点触发4、数据库备份5、表空间离线或只读时6、停止数据库 可以看出,没有说明COMMIT时会进程DBWR,那么如果COMMIT了,但是没有写数据,在断电、SHUTDOWN ABORT等情况下,数据不就没有了吗,后面说道LGWR和CKPT时会连套起来说明。 SQL> select name,DESCRIPTION from v$bgprocess where name like 'DBW%'; NAME DESCRIPTION----- ----------------------------------------------------------------DBW0 db writer process 0DBW1 db writer process 1DBW2 db writer process 2DBW3 db writer process 3DBW4 db writer process 4DBW5 db writer process 5DBW6 db writer process 6DBW7 db writer process 7DBW8 db writer process 8DBW9 db writer process 9DBWa db writer process 10 (a) NAME DESCRIPTION----- ----------------------------------------------------------------DBWb db writer process 11 (b)DBWc db writer process 12 (c)DBWd db writer process 13 (d)DBWe db writer process 14 (e)DBWf db writer process 15 (f)DBWg db writer process 16 (g)DBWh db writer process 17 (h)DBWi db writer process 18 (i)DBWj db writer process 19 (j) 5、核心进程LGWR说明 LGWR Redo etc:写日志文件,将日志缓冲区的数据写入在线日志文件中。其写法为:循环写、顺序写,按组(GROUP)管理日志文件(在前面文章中有说明),一般默认三组,每个成员(GROUP下的一个日志文件)最少4M大小,默认50M,同组下多个成员一起使用过一次后成为孪生兄弟,即相互拷贝,相互备份,因为它是保护数据的,它自己只有自己保护自己,就想控制文件一样。 LGWR触发条件: 1、提交做COMMIT或ROLLBACK;2、大于1M日志未写入磁盘3、1/3日志缓冲区未写入磁盘4、DBWR之前须先写LGWR,也就是LGWR写入的日志文件的SCN号码肯定是大于等于数据文件的5、超时 LGWR会记录什么? 也就是做COMMIT的时候会将信息写入日志文件中,记录的大致是字段地址、字段值、事务标志。 很多人可能会问:既然这些信息都记录了所有的插入数据文件的信息干嘛还要写日志呢? 很简单的答案: 1、第一使用日志文件是轻量的,相对较为安全,若文件坏掉,绝大部分甚至全部数据可以找回。 2、日志文件写比数据文件写要快很多,大家会发现日志文件只有一个LGWR进程,而DBWR有20,但是20也没有LGWR一个快,主要有两个方面的原因:一个是日志文件是顺序写,循环写,它不用考虑下面的位置,而数据文件需要找到实际的位置去修改并且需要分配磁盘空间;其次,数据文件修改后牵涉相关视图的修改,若频繁使用DBWR,在高并发系统中会很容易就出现瓶颈了,所以你COMMIT的时候,如果ORACLE把成功写入在线日志文件,它就向客户反馈提交成功了。 6、CKPT说明 全称checkpoint:这个概念一直很模糊,因为有个命令是ALTER SYSTEM CHECKPOINT,这里的额CKPT是一个进程,而那个命令是调用这个进程来进程全局的磁盘写,而CHECKPOINT还有一个在中文上很多时候把他称为检查点的概念,这个检查点可以理解为一个已经被存盘的SCN号码,也就是一个特殊的SCN号码,他们都存在于数据文件头、控制文件头、日志文件内部,通过V$DATABASE视图以及表空间视图、数据文件视图、控制文件视图都可以查到(在前面文章已经说过),当日志文件进行增量或者全部存盘时,会将起始的CHECKPOINT_CHANGE#编号,与需要存盘的SCN号码对比(全量存盘以当前SCN号码为准),将这部分脏块写入数据文件,并修改数据文件头和控制文件头的检查点号码。 简单说下一下几个问题: 为什么用SCN号码,不用时间戳? 因为系统时间是可以被改变的,SCN号码永远向