<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>编程之道</title>
	<atom:link href="http://www.wdcode.org/feed" rel="self" type="application/rss+xml" />
	<link>http://www.wdcode.org</link>
	<description>一个以技术为主的博客</description>
	<lastBuildDate>Wed, 15 Feb 2012 04:04:41 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3.1</generator>
<xhtml:meta xmlns:xhtml="http://www.w3.org/1999/xhtml" name="robots" content="noindex" />
		<item>
		<title>Java Socket实战</title>
		<link>http://www.wdcode.org/archives/547.html</link>
		<comments>http://www.wdcode.org/archives/547.html#comments</comments>
		<pubDate>Wed, 15 Feb 2012 04:04:41 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[J2SE]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[socket]]></category>

		<guid isPermaLink="false">http://www.wdcode.org/?p=547</guid>
		<description><![CDATA[<h1>一：单线程通信</h1>
现在做Java直接使用Socket的情况是越来越少，因为有很多的选择可选，比如说可以用spring，其中就可以支持很多种远程连接的操作，另外jboss的remoting也是不错的选择，还有Apache的Mina等等，但是在有些时候一些特殊情况仍然逃脱不了直接写Socket的情况，比如公司内部一些莫名其妙的游戏规则。

废话不说了，下面就看看如果自己写Socket应该怎么做吧。

首先是写一个Server类，这个类用来监听10000端口，并从这个端口接收消息然后输出，当收到“bye”时退出。
<ol>
	<li>package com.googlecode.garbagecan.test.socket.sample1;</li>
	<li></li>
	<li>import java.io.BufferedReader;</li>
	<li>import java.io.IOException;</li>
	<li>import java.io.InputStreamReader;</li>
	<li>import java.io.PrintWriter;</li>
	<li>import java.net.ServerSocket;</li>
	<li>import java.net.Socket;</li>
	<li></li>
	<li>public class MyServer {</li>
	<li>    public static void main(String[] args) throws IOException {</li>
	<li>        ServerSocket server = new ServerSocket(10000);</li>
	<li>        Socket socket = server.accept();</li>
	<li>        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));</li>
	<li>        PrintWriter out = new PrintWriter(socket.getOutputStream());</li>
	<li></li>
	<li>        while (true) {</li>
	<li>            String msg = in.readLine();</li>
	<li>            System.out.println(msg);</li>
	<li>            out.println("Server received " + msg);</li>
	<li>            out.flush();</li>
	<li>            if (msg.equals("bye")) {</li>
	<li>                break;</li>
	<li>            }</li>
	<li>        }</li>
	<li>        socket.close();</li>
	<li>    }</li>
	<li>}</li>
</ol>
然后是一个Client类，这个类连接上面启动的Server类，然后接收任何用户输入，当遇到回车时发送字符串到Server上，当输入“bye”是退出。
<ol>
	<li>package com.googlecode.garbagecan.test.socket.sample1;</li>
	<li></li>
	<li>import java.io.BufferedReader;</li>
	<li>import java.io.InputStreamReader;</li>
	<li>import java.io.PrintWriter;</li>
	<li>import java.net.Socket;</li>
	<li></li>
	<li>public class MyClient {</li>
	<li>    public static void main(String[] args) throws Exception {</li>
	<li>        Socket socket = new Socket("localhost", 10000);</li>
	<li>        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));</li>
	<li>        PrintWriter out = new PrintWriter(socket.getOutputStream());</li>
	<li>        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));</li>
	<li></li>
	<li>        while (true) {</li>
	<li>            String msg = reader.readLine();</li>
	<li>            out.println(msg);</li>
	<li>            out.flush();</li>
	<li>            if (msg.equals("bye")) {</li>
	<li>                break;</li>
	<li>            }</li>
	<li>            System.out.println(in.readLine());</li>
	<li>        }</li>
	<li>        socket.close();</li>
	<li>    }</li>
	<li>}</li>
</ol>
最后，首先运行MyServer类，然后MyClient类，然后在MyClient的控制台输入任意字符，可以看到当输入bye是server和client都会退出。
<h1>二：多线程通信</h1>
上一篇文章中的例子有一个问题就是Server只能接受一个Client请求，当第一个Client连接后就占据了这个位置，后续Client不能再继续连接，所以需要做些改动，当Server没接受到一个Client连接请求之后，都把处理流程放到一个独立的线程里去运行，然后等待下一个Client连接请求，这样就不会阻塞Server端接收请求了。每个独立运行的程序在使用完Socket对象之后要将其关闭。

具体代码如下：
<ol>
	<li>package com.googlecode.garbagecan.test.socket.sample2;</li>
	<li></li>
	<li>import java.io.BufferedReader;</li>
	<li>import java.io.IOException;</li>
	<li>import java.io.InputStreamReader;</li>
	<li>import java.io.PrintWriter;</li>
	<li>import java.net.ServerSocket;</li>
	<li>import java.net.Socket;</li>
	<li></li>
	<li>public class MyServer {</li>
	<li>    public static void main(String[] args) throws IOException {</li>
	<li>        ServerSocket server = new ServerSocket(10000);</li>
	<li></li>
	<li>        while (true) {</li>
	<li>            Socket socket = server.accept();</li>
	<li>            invoke(socket);</li>
	<li>        }</li>
	<li>    }</li>
	<li></li>
	<li>    private static void invoke(final Socket client) throws IOException {</li>
	<li>        new Thread(new Runnable() {</li>
	<li>            public void run() {</li>
	<li>                BufferedReader in = null;</li>
	<li>                PrintWriter out = null;</li>
	<li>                try {</li>
	<li>                    in = new BufferedReader(new InputStreamReader(client.getInputStream()));</li>
	<li>                    out = new PrintWriter(client.getOutputStream());</li>
	<li></li>
	<li>                    while (true) {</li>
	<li>                        String msg = in.readLine();</li>
	<li>                        System.out.println(msg);</li>
	<li>                        out.println("Server received " + msg);</li>
	<li>                        out.flush();</li>
	<li>                        if (msg.equals("bye")) {</li>
	<li>                            break;</li>
	<li>                        }</li>
	<li>                    }</li>
	<li>                } catch(IOException ex) {</li>
	<li>                    ex.printStackTrace();</li>
	<li>                } finally {</li>
	<li>                    try {</li>
	<li>                        in.close();</li>
	<li>                    } catch (Exception e) {}</li>
	<li>                    try {</li>
	<li>                        out.close();</li>
	<li>                    } catch (Exception e) {}</li>
	<li>                    try {</li>
	<li>                        client.close();</li>
	<li>                    } catch (Exception e) {}</li>
	<li>                }</li>
	<li>            }</li>
	<li>        }).start();</li>
	<li>    }</li>
	<li>}</li>
</ol>
下面是Client程序代码：
<ol>
	<li>package com.googlecode.garbagecan.test.socket.sample2;</li>
	<li></li>
	<li>import java.io.BufferedReader;</li>
	<li>import java.io.InputStreamReader;</li>
	<li>import java.io.PrintWriter;</li>
	<li>import java.net.Socket;</li>
	<li></li>
	<li>public class MyClient {</li>
	<li>    public static void main(String[] args) throws Exception {</li>
	<li>        Socket socket = new Socket("localhost", 10000);</li>
	<li>        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));</li>
	<li>        PrintWriter out = new PrintWriter(socket.getOutputStream());</li>
	<li>        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));</li>
	<li></li>
	<li>        while (true) {</li>
	<li>            String msg = reader.readLine();</li>
	<li>            out.println(msg);</li>
	<li>            out.flush();</li>
	<li>            if (msg.equals("bye")) {</li>
	<li>                break;</li>
	<li>            }</li>
	<li>            System.out.println(in.readLine());</li>
	<li>        }</li>
	<li>        socket.close();</li>
	<li>    }</li>
	<li>}</li>
</ol>
测试，首先运行MyServer类，然后运行两个MyClient类，然后分别在每个MyClient的提示符下输入字符串，就可以看到Server可以分别接收处理每个Client的请求了。
<h1>三：传输对象</h1>
首先需要一个普通的对象类，由于需要序列化这个对象以便在网络上传输，所以实现java.io.Serializable接口就是必不可少的了，入下：
<ol>
	<li>package com.googlecode.garbagecan.test.socket.sample3;</li>
	<li></li>
	<li>public class User implements java.io.Serializable {</li>
	<li>    private static final long serialVersionUID = 1L;</li>
	<li>    private String name;</li>
	<li>    private String password;</li>
	<li></li>
	<li>    public User() {</li>
	<li></li>
	<li>    }</li>
	<li></li>
	<li>    public User(String name, String password) {</li>
	<li>        this.name = name;</li>
	<li>        this.password = password;</li>
	<li>    }</li>
	<li></li>
	<li>    public String getName() {</li>
	<li>        return name;</li>
	<li>    }</li>
	<li></li>
	<li>    public void setName(String name) {</li>
	<li>        this.name = name;</li>
	<li>    }</li>
	<li></li>
	<li>    public String getPassword() {</li>
	<li>        return password;</li>
	<li>    }</li>
	<li></li>
	<li>    public void setPassword(String password) {</li>
	<li>        this.password = password;</li>
	<li>    }</li>
	<li>}</li>
</ol>
对于Server端的代码，代码中分别使用了ObjectInputStream和ObjectOutputStream来接收和发送socket中的InputStream和OutputStream，然后转换成Java对象，如下：
<ol>
	<li>package com.googlecode.garbagecan.test.socket.sample3;</li>
	<li></li>
	<li>import java.io.*;</li>
	<li>import java.net.ServerSocket;</li>
	<li>import java.net.Socket;</li>
	<li>import java.util.logging.Level;</li>
	<li>import java.util.logging.Logger;</li>
	<li></li>
	<li>public class MyServer {</li>
	<li></li>
	<li>    private final static Logger logger = Logger.getLogger(MyServer.class.getName());</li>
	<li></li>
	<li>    public static void main(String[] args) throws IOException {</li>
	<li>        ServerSocket server = new ServerSocket(10000);</li>
	<li></li>
	<li>        while (true) {</li>
	<li>            Socket socket = server.accept();</li>
	<li>            invoke(socket);</li>
	<li>        }</li>
	<li>    }</li>
	<li></li>
	<li>    private static void invoke(final Socket socket) throws IOException {</li>
	<li>        new Thread(new Runnable() {</li>
	<li>            public void run() {</li>
	<li>                ObjectInputStream is = null;</li>
	<li>                ObjectOutputStream os = null;</li>
	<li>                try {</li>
	<li>                    is = new ObjectInputStream(new BufferedInputStream(socket.getInputStream()));</li>
	<li>                    os = new ObjectOutputStream(socket.getOutputStream());</li>
	<li></li>
	<li>                    Object obj = is.readObject();</li>
	<li>                    User user = (User)obj;</li>
	<li>                    System.out.println("user: " + user.getName() + "/" + user.getPassword());</li>
	<li></li>
	<li>                    user.setName(user.getName() + "_new");</li>
	<li>                    user.setPassword(user.getPassword() + "_new");</li>
	<li></li>
	<li>                    os.writeObject(user);</li>
	<li>                    os.flush();</li>
	<li>                } catch (IOException ex) {</li>
	<li>                    logger.log(Level.SEVERE, null, ex);</li>
	<li>                } catch(ClassNotFoundException ex) {</li>
	<li>                    logger.log(Level.SEVERE, null, ex);</li>
	<li>                } finally {</li>
	<li>                    try {</li>
	<li>                        is.close();</li>
	<li>                    } catch(Exception ex) {}</li>
	<li>                    try {</li>
	<li>                        os.close();</li>
	<li>                    } catch(Exception ex) {}</li>
	<li>                    try {</li>
	<li>                        socket.close();</li>
	<li>                    } catch(Exception ex) {}</li>
	<li>                }</li>
	<li>            }</li>
	<li>        }).start();</li>
	<li>    }</li>
	<li>}</li>
</ol>
Client也和Server端类似，同样使用ObjectOutputStream和ObjectInputStream来处理，如下：
<ol>
	<li>package com.googlecode.garbagecan.test.socket.sample3;</li>
	<li></li>
	<li>import java.io.BufferedInputStream;</li>
	<li>import java.io.IOException;</li>
	<li>import java.io.ObjectInputStream;</li>
	<li>import java.io.ObjectOutputStream;</li>
	<li>import java.net.Socket;</li>
	<li>import java.util.logging.Level;</li>
	<li>import java.util.logging.Logger;</li>
	<li></li>
	<li>public class MyClient {</li>
	<li></li>
	<li>    private final static Logger logger = Logger.getLogger(MyClient.class.getName());</li>
	<li></li>
	<li>    public static void main(String[] args) throws Exception {</li>
	<li>        for (int i = 0; i &#60; 100; i++) {</li>
	<li>            Socket socket = null;</li>
	<li>            ObjectOutputStream os = null;</li>
	<li>            ObjectInputStream is = null;</li>
	<li></li>
	<li>            try {</li>
	<li>                socket = new Socket("localhost", 10000);</li>
	<li></li>
	<li>                os = new ObjectOutputStream(socket.getOutputStream());</li>
	<li>                User user = new User("user_" + i, "password_" + i);</li>
	<li>                os.writeObject(user);</li>
	<li>                os.flush();</li>
	<li></li>
	<li>                is = new ObjectInputStream(new BufferedInputStream(socket.getInputStream()));</li>
	<li>                Object obj = is.readObject();</li>
	<li>                if (obj != null) {</li>
	<li>                    user = (User)obj;</li>
	<li>                    System.out.println("user: " + user.getName() + "/" + user.getPassword());</li>
	<li>                }</li>
	<li>            } catch(IOException ex) {</li>
	<li>                logger.log(Level.SEVERE, null, ex);</li>
	<li>            } finally {</li>
	<li>                try {</li>
	<li>                    is.close();</li>
	<li>                } catch(Exception ex) {}</li>
	<li>                try {</li>
	<li>                    os.close();</li>
	<li>                } catch(Exception ex) {}</li>
	<li>                try {</li>
	<li>                    socket.close();</li>
	<li>                } catch(Exception ex) {}</li>
	<li>            }</li>
	<li>        }</li>
	<li>    }</li>
	<li>}</li>
</ol>
最后测试上面的代码，首先运行Server类，然后运行Client类，就可以分别在Server端和Client端控制台看到接收到的User对象实例了。
<h1>四：传输压缩对象</h1>
用Java Socket来传输对象，但是在有些情况下比如网络环境不好或者对象比较大的情况下需要把数据对象进行压缩然后在传输，此时就需要压缩这些对象流，此时就可以GZIPInputStream和GZIPOutputStream来处理一下socket的InputStream和OutputStream。

仍然需要一个实现了java.io.Serializable接口的简单Java对象：
<ol>
	<li>package com.googlecode.garbagecan.test.socket.sample4;</li>
	<li></li>
	<li>public class User implements java.io.Serializable {</li>
	<li>    private static final long serialVersionUID = 1L;</li>
	<li>    private String name;</li>
	<li>    private String password;</li>
	<li></li>
	<li>    public User() {</li>
	<li></li>
	<li>    }</li>
	<li></li>
	<li>    public User(String name, String password) {</li>
	<li>        this.name = name;</li>
	<li>        this.password = password;</li>
	<li>    }</li>
	<li></li>
	<li>    public String getName() {</li>
	<li>        return name;</li>
	<li>    }</li>
	<li></li>
	<li>    public void setName(String name) {</li>
	<li>        this.name = name;</li>
	<li>    }</li>
	<li></li>
	<li>    public String getPassword() {</li>
	<li>        return password;</li>
	<li>    }</li>
	<li></li>
	<li>    public void setPassword(String password) {</li>
	<li>        this.password = password;</li>
	<li>    }</li>
	<li></li>
	<li>}</li>
</ol>
在Server端使用，socket的InputStream首先被包装成GZIPInputStream，然后又被包装成ObjectInputStream，而socket的OutputStream首先被包装成GZIPOutputStream，然后又被包装成ObjectOutputStream，如下：
<ol>
	<li>package com.googlecode.garbagecan.test.socket.sample4;</li>
	<li></li>
	<li>import java.io.IOException;</li>
	<li>import java.io.ObjectInputStream;</li>
	<li>import java.io.ObjectOutputStream;</li>
	<li>import java.net.ServerSocket;</li>
	<li>import java.net.Socket;</li>
	<li>import java.util.logging.Level;</li>
	<li>import java.util.logging.Logger;</li>
	<li>import java.util.zip.GZIPInputStream;</li>
	<li>import java.util.zip.GZIPOutputStream;</li>
	<li></li>
	<li>public class MyServer {</li>
	<li></li>
	<li>    private final static Logger logger = Logger.getLogger(MyServer.class.getName());</li>
	<li></li>
	<li>    public static void main(String[] args) throws IOException {</li>
	<li>        ServerSocket server = new ServerSocket(10000);</li>
	<li></li>
	<li>        while (true) {</li>
	<li>            Socket socket = server.accept();</li>
	<li>            socket.setSoTimeout(10 * 1000);</li>
	<li>            invoke(socket);</li>
	<li>        }</li>
	<li>    }</li>
	<li></li>
	<li>    private static void invoke(final Socket socket) throws IOException {</li>
	<li>        new Thread(new Runnable() {</li>
	<li>            public void run() {</li>
	<li>                GZIPInputStream gzipis = null;</li>
	<li>                ObjectInputStream ois = null;</li>
	<li>                GZIPOutputStream gzipos = null;</li>
	<li>                ObjectOutputStream oos = null;</li>
	<li></li>
	<li>                try {</li>
	<li>                    gzipis = new GZIPInputStream(socket.getInputStream());</li>
	<li>                    ois = new ObjectInputStream(gzipis);</li>
	<li>                    gzipos = new GZIPOutputStream(socket.getOutputStream());</li>
	<li>                    oos = new ObjectOutputStream(gzipos);</li>
	<li></li>
	<li>                    Object obj = ois.readObject();</li>
	<li>                    User user = (User)obj;</li>
	<li>                    System.out.println("user: " + user.getName() + "/" + user.getPassword());</li>
	<li></li>
	<li>                    user.setName(user.getName() + "_new");</li>
	<li>                    user.setPassword(user.getPassword() + "_new");</li>
	<li></li>
	<li>                    oos.writeObject(user);</li>
	<li>                    oos.flush();</li>
	<li>                    gzipos.finish();</li>
	<li>                } catch (IOException ex) {</li>
	<li>                    logger.log(Level.SEVERE, null, ex);</li>
	<li>                } catch(ClassNotFoundException ex) {</li>
	<li>                    logger.log(Level.SEVERE, null, ex);</li>
	<li>                } finally {</li>
	<li>                    try {</li>
	<li>                        ois.close();</li>
	<li>                    } catch(Exception ex) {}</li>
	<li>                    try {</li>
	<li>                        oos.close();</li>
	<li>                    } catch(Exception ex) {}</li>
	<li>                    try {</li>
	<li>                        socket.close();</li>
	<li>                    } catch(Exception ex) {}</li>
	<li>                }</li>
	<li>            }</li>
	<li>        }).start();</li>
	<li>    }</li>
	<li>}</li>
</ol>
Client也和Server端类似，同样要不socket的XXXStream包装成GZIPXXXStream，然后再包装成ObjectXXXStream，如下：
<ol>
	<li>package com.googlecode.garbagecan.test.socket.sample4;</li>
	<li></li>
	<li>import java.io.IOException;</li>
	<li>import java.io.ObjectInputStream;</li>
	<li>import java.io.ObjectOutputStream;</li>
	<li>import java.net.InetSocketAddress;</li>
	<li>import java.net.Socket;</li>
	<li>import java.net.SocketAddress;</li>
	<li>import java.util.logging.Level;</li>
	<li>import java.util.logging.Logger;</li>
	<li>import java.util.zip.GZIPInputStream;</li>
	<li>import java.util.zip.GZIPOutputStream;</li>
	<li></li>
	<li>public class MyClient {</li>
	<li></li>
	<li>    private final static Logger logger = Logger.getLogger(MyClient.class.getName());</li>
	<li></li>
	<li>    public static void main(String[] args) throws Exception {</li>
	<li>        for (int i = 0; i &#60; 10; i++) {</li>
	<li>            Socket socket = null;</li>
	<li>            GZIPOutputStream gzipos = null;</li>
	<li>            ObjectOutputStream oos = null;</li>
	<li>            GZIPInputStream gzipis = null;</li>
	<li>            ObjectInputStream ois = null;</li>
	<li></li>
	<li>            try {</li>
	<li>                socket = new Socket();</li>
	<li>                SocketAddress socketAddress = new InetSocketAddress("localhost", 10000);</li>
	<li>                socket.connect(socketAddress, 10 * 1000);</li>
	<li>                socket.setSoTimeout(10 * 1000);</li>
	<li></li>
	<li>                gzipos = new GZIPOutputStream(socket.getOutputStream());</li>
	<li>                oos = new ObjectOutputStream(gzipos);</li>
	<li>                User user = new User("user_" + i, "password_" + i);</li>
	<li>                oos.writeObject(user);</li>
	<li>                oos.flush();</li>
	<li>                gzipos.finish();</li>
	<li></li>
	<li>                gzipis = new GZIPInputStream(socket.getInputStream());</li>
	<li>                ois = new ObjectInputStream(gzipis);</li>
	<li>                Object obj = ois.readObject();</li>
	<li>                if (obj != null) {</li>
	<li>                    user = (User)obj;</li>
	<li>                    System.out.println("user: " + user.getName() + "/" + user.getPassword());</li>
	<li>                }</li>
	<li>            } catch(IOException ex) {</li>
	<li>                logger.log(Level.SEVERE, null, ex);</li>
	<li>            }</li>
	<li>            try {</li>
	<li>                oos.close();</li>
	<li>            } catch (IOException e) {</li>
	<li>            }</li>
	<li>            try {</li>
	<li>                ois.close();</li>
	<li>            } catch (IOException e) {</li>
	<li>            }</li>
	<li>            try {</li>
	<li>                socket.close();</li>
	<li>            } catch (IOException e) {</li>
	<li>            }</li>
	<li>        }</li>
	<li>    }</li>
	<li>}</li>
</ol>
最后测试上面的代码，首先运行Server类，然后运行Client类，就可以分别在Server端和Client端控制台看到接收到的User对象实例了。
<h1>五:使用加密协议传输对象</h1>
前面几篇博文提到了Socket中一些常用的用法，但是对于一些有安全要求的应用就需要加密传输的数据，此时就需要用到SSLSocket了。

还是一样需要一个实现了java.io.Serializable接口的简单Java对象：
<ol>
	<li>package com.googlecode.garbagecan.test.socket.ssl;</li>
	<li></li>
	<li>public class User implements java.io.Serializable {</li>
	<li>    private static final long serialVersionUID = 1L;</li>
	<li>    private String name;</li>
	<li>    private String password;</li>
	<li></li>
	<li>    public User() {</li>
	<li></li>
	<li>    }</li>
	<li></li>
	<li>    public User(String name, String password) {</li>
	<li>        this.name = name;</li>
	<li>        this.password = password;</li>
	<li>    }</li>
	<li></li>
	<li>    public String getName() {</li>
	<li>        return name;</li>
	<li>    }</li>
	<li></li>
	<li>    public void setName(String name) {</li>
	<li>        this.name = name;</li>
	<li>    }</li>
	<li></li>
	<li>    public String getPassword() {</li>
	<li>        return password;</li>
	<li>    }</li>
	<li></li>
	<li>    public void setPassword(String password) {</li>
	<li>        this.password = password;</li>
	<li>    }</li>
	<li></li>
	<li>}</li>
</ol>
SSL Server类，这里需要用到ServerSocketFactory类来创建SSLServerSocket类实例，然后在通过SSLServerSocket来获取SSLSocket实例，这里考虑到面向对象中的面向接口编程的理念，所以代码中并没有出现SSLServerSocket和SSLSocket，而是用了他们的父类ServerSocket和Socket。在获取到ServerSocket和Socket实例以后，剩下的代码就和不使用加密方式一样了。
<ol>
	<li>package com.googlecode.garbagecan.test.socket.ssl;</li>
	<li></li>
	<li>import java.io.BufferedInputStream;</li>
	<li>import java.io.IOException;</li>
	<li>import java.io.ObjectInputStream;</li>
	<li>import java.io.ObjectOutputStream;</li>
	<li>import java.net.ServerSocket;</li>
	<li>import java.net.Socket;</li>
	<li>import java.util.logging.Level;</li>
	<li>import java.util.logging.Logger;</li>
	<li></li>
	<li>import javax.net.ServerSocketFactory;</li>
	<li>import javax.net.ssl.SSLServerSocketFactory;</li>
	<li></li>
	<li>public class MyServer {</li>
	<li></li>
	<li>    private final static Logger logger = Logger.getLogger(MyServer.class.getName());</li>
	<li></li>
	<li>    public static void main(String[] args) {</li>
	<li>        try {</li>
	<li>            ServerSocketFactory factory = SSLServerSocketFactory.getDefault();</li>
	<li>            ServerSocket server = factory.createServerSocket(10000);</li>
	<li></li>
	<li>            while (true) {</li>
	<li>                Socket socket = server.accept();</li>
	<li>                invoke(socket);</li>
	<li>            }</li>
	<li>        } catch (Exception ex) {</li>
	<li>            ex.printStackTrace();</li>
	<li>        }</li>
	<li>    }</li>
	<li></li>
	<li>    private static void invoke(final Socket socket) throws IOException {</li>
	<li>        new Thread(new Runnable() {</li>
	<li>            public void run() {</li>
	<li>                ObjectInputStream is = null;</li>
	<li>                ObjectOutputStream os = null;</li>
	<li>                try {</li>
	<li>                    is = new ObjectInputStream(new BufferedInputStream(socket.getInputStream()));</li>
	<li>                    os = new ObjectOutputStream(socket.getOutputStream());</li>
	<li></li>
	<li>                    Object obj = is.readObject();</li>
	<li>                    User user = (User)obj;</li>
	<li>                    System.out.println("user: " + user.getName() + "/" + user.getPassword());</li>
	<li></li>
	<li>                    user.setName(user.getName() + "_new");</li>
	<li>                    user.setPassword(user.getPassword() + "_new");</li>
	<li></li>
	<li>                    os.writeObject(user);</li>
	<li>                    os.flush();</li>
	<li>                } catch (IOException ex) {</li>
	<li>                    logger.log(Level.SEVERE, null, ex);</li>
	<li>                } catch(ClassNotFoundException ex) {</li>
	<li>                    logger.log(Level.SEVERE, null, ex);</li>
	<li>                } finally {</li>
	<li>                    try {</li>
	<li>                        is.close();</li>
	<li>                    } catch(Exception ex) {}</li>
	<li>                    try {</li>
	<li>                        os.close();</li>
	<li>                    } catch(Exception ex) {}</li>
	<li>                    try {</li>
	<li>                        socket.close();</li>
	<li>                    } catch(Exception ex) {}</li>
	<li>                }</li>
	<li>            }</li>
	<li>        }).start();</li>
	<li>    }</li>
	<li>}</li>
</ol>
SSL Client类和SSL Server类类似，只是将其中获取Socket的方式有所变化，其余的代码也和不使用加密方式一样。
<ol>
	<li>package com.googlecode.garbagecan.test.socket.ssl;</li>
	<li></li>
	<li>import java.io.BufferedInputStream;</li>
	<li>import java.io.IOException;</li>
	<li>import java.io.ObjectInputStream;</li>
	<li>import java.io.ObjectOutputStream;</li>
	<li>import java.net.Socket;</li>
	<li>import java.util.logging.Level;</li>
	<li>import java.util.logging.Logger;</li>
	<li></li>
	<li>import javax.net.SocketFactory;</li>
	<li>import javax.net.ssl.SSLSocketFactory;</li>
	<li></li>
	<li>public class MyClient {</li>
	<li></li>
	<li>private final static Logger logger = Logger.getLogger(MyClient.class.getName());</li>
	<li></li>
	<li>    public static void main(String[] args) throws Exception {</li>
	<li>        for (int i = 0; i &#60; 100; i++) {</li>
	<li>            Socket socket = null;</li>
	<li>            ObjectOutputStream os = null;</li>
	<li>            ObjectInputStream is = null;</li>
	<li></li>
	<li>            try {</li>
	<li>                SocketFactory factory = SSLSocketFactory.getDefault();</li>
	<li>                socket = factory.createSocket("localhost", 10000);</li>
	<li></li>
	<li>                os = new ObjectOutputStream(socket.getOutputStream());</li>
	<li>                User user = new User("user_" + i, "password_" + i);</li>
	<li>                os.writeObject(user);</li>
	<li>                os.flush();</li>
	<li></li>
	<li>                is = new ObjectInputStream(new BufferedInputStream(socket.getInputStream()));</li>
	<li>                Object obj = is.readObject();</li>
	<li>                if (obj != null) {</li>
	<li>                    user = (User)obj;</li>
	<li>                    System.out.println("user: " + user.getName() + "/" + user.getPassword());</li>
	<li>                }</li>
	<li>            } catch(IOException ex) {</li>
	<li>                logger.log(Level.SEVERE, null, ex);</li>
	<li>            } finally {</li>
	<li>                try {</li>
	<li>                    is.close();</li>
	<li>                } catch(Exception ex) {}</li>
	<li>                try {</li>
	<li>                    os.close();</li>
	<li>                } catch(Exception ex) {}</li>
	<li>                try {</li>
	<li>                    socket.close();</li>
	<li>                } catch(Exception ex) {}</li>
	<li>            }</li>
	<li>        }</li>
	<li>    }</li>
	<li>}</li>
</ol>
代码写完了，下面就需要产生keystore文件了，运行下面的命令
<ol>
	<li>keytool -genkey -alias mysocket -keyalg RSA -keystore mysocket.jks</li>
</ol>
在提示输入项中，密码项自己给定，其它都不改直接回车，这里我使用的密码是“mysocket”。

运行Server
<ol>
	<li>java -Djavax.net.ssl.keyStore=mysocket.jks -Djavax.net.ssl.keyStorePassword=mysocket com.googlecode.garbagecan.test.socket.ssl.MyServer</li>
</ol>
运行Client
<ol>
	<li>java -Djavax.net.ssl.trustStore=mysocket.jks  -Djavax.net.ssl.trustStorePassword=mysocket com.googlecode.garbagecan.test.socket.ssl.MyClient</li>
</ol>]]></description>
			<content:encoded><![CDATA[<h1>一：单线程通信</h1>
<p>现在做Java直接使用Socket的情况是越来越少，因为有很多的选择可选，比如说可以用spring，其中就可以支持很多种远程连接的操作，另外jboss的remoting也是不错的选择，还有Apache的Mina等等，但是在有些时候一些特殊情况仍然逃脱不了直接写Socket的情况，比如公司内部一些莫名其妙的游戏规则。</p>
<p>废话不说了，下面就看看如果自己写Socket应该怎么做吧。</p>
<p>首先是写一个Server类，这个类用来监听10000端口，并从这个端口接收消息然后输出，当收到“bye”时退出。</p>
<ol>
<li>package com.googlecode.garbagecan.test.socket.sample1;</li>
<li></li>
<li>import java.io.BufferedReader;</li>
<li>import java.io.IOException;</li>
<li>import java.io.InputStreamReader;</li>
<li>import java.io.PrintWriter;</li>
<li>import java.net.ServerSocket;</li>
<li>import java.net.Socket;</li>
<li></li>
<li>public class MyServer {</li>
<li>    public static void main(String[] args) throws IOException {</li>
<li>        ServerSocket server = new ServerSocket(10000);</li>
<li>        Socket socket = server.accept();</li>
<li>        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));</li>
<li>        PrintWriter out = new PrintWriter(socket.getOutputStream());</li>
<li></li>
<li>        while (true) {</li>
<li>            String msg = in.readLine();</li>
<li>            System.out.println(msg);</li>
<li>            out.println(“Server received ” + msg);</li>
<li>            out.flush();</li>
<li>            if (msg.equals(“bye”)) {</li>
<li>                break;</li>
<li>            }</li>
<li>        }</li>
<li>        socket.close();</li>
<li>    }</li>
<li>}</li>
</ol>
<p>然后是一个Client类，这个类连接上面启动的Server类，然后接收任何用户输入，当遇到回车时发送字符串到Server上，当输入“bye”是退出。</p>
<ol>
<li>package com.googlecode.garbagecan.test.socket.sample1;</li>
<li></li>
<li>import java.io.BufferedReader;</li>
<li>import java.io.InputStreamReader;</li>
<li>import java.io.PrintWriter;</li>
<li>import java.net.Socket;</li>
<li></li>
<li>public class MyClient {</li>
<li>    public static void main(String[] args) throws Exception {</li>
<li>        Socket socket = new Socket(“localhost”, 10000);</li>
<li>        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));</li>
<li>        PrintWriter out = new PrintWriter(socket.getOutputStream());</li>
<li>        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));</li>
<li></li>
<li>        while (true) {</li>
<li>            String msg = reader.readLine();</li>
<li>            out.println(msg);</li>
<li>            out.flush();</li>
<li>            if (msg.equals(“bye”)) {</li>
<li>                break;</li>
<li>            }</li>
<li>            System.out.println(in.readLine());</li>
<li>        }</li>
<li>        socket.close();</li>
<li>    }</li>
<li>}</li>
</ol>
<p>最后，首先运行MyServer类，然后MyClient类，然后在MyClient的控制台输入任意字符，可以看到当输入bye是server和client都会退出。</p>
<h1>二：多线程通信</h1>
<p>上一篇文章中的例子有一个问题就是Server只能接受一个Client请求，当第一个Client连接后就占据了这个位置，后续Client不能再继续连接，所以需要做些改动，当Server没接受到一个Client连接请求之后，都把处理流程放到一个独立的线程里去运行，然后等待下一个Client连接请求，这样就不会阻塞Server端接收请求了。每个独立运行的程序在使用完Socket对象之后要将其关闭。</p>
<p>具体代码如下：</p>
<ol>
<li>package com.googlecode.garbagecan.test.socket.sample2;</li>
<li></li>
<li>import java.io.BufferedReader;</li>
<li>import java.io.IOException;</li>
<li>import java.io.InputStreamReader;</li>
<li>import java.io.PrintWriter;</li>
<li>import java.net.ServerSocket;</li>
<li>import java.net.Socket;</li>
<li></li>
<li>public class MyServer {</li>
<li>    public static void main(String[] args) throws IOException {</li>
<li>        ServerSocket server = new ServerSocket(10000);</li>
<li></li>
<li>        while (true) {</li>
<li>            Socket socket = server.accept();</li>
<li>            invoke(socket);</li>
<li>        }</li>
<li>    }</li>
<li></li>
<li>    private static void invoke(final Socket client) throws IOException {</li>
<li>        new Thread(new Runnable() {</li>
<li>            public void run() {</li>
<li>                BufferedReader in = null;</li>
<li>                PrintWriter out = null;</li>
<li>                try {</li>
<li>                    in = new BufferedReader(new InputStreamReader(client.getInputStream()));</li>
<li>                    out = new PrintWriter(client.getOutputStream());</li>
<li></li>
<li>                    while (true) {</li>
<li>                        String msg = in.readLine();</li>
<li>                        System.out.println(msg);</li>
<li>                        out.println(“Server received ” + msg);</li>
<li>                        out.flush();</li>
<li>                        if (msg.equals(“bye”)) {</li>
<li>                            break;</li>
<li>                        }</li>
<li>                    }</li>
<li>                } catch(IOException ex) {</li>
<li>                    ex.printStackTrace();</li>
<li>                } finally {</li>
<li>                    try {</li>
<li>                        in.close();</li>
<li>                    } catch (Exception e) {}</li>
<li>                    try {</li>
<li>                        out.close();</li>
<li>                    } catch (Exception e) {}</li>
<li>                    try {</li>
<li>                        client.close();</li>
<li>                    } catch (Exception e) {}</li>
<li>                }</li>
<li>            }</li>
<li>        }).start();</li>
<li>    }</li>
<li>}</li>
</ol>
<p>下面是Client程序代码：</p>
<ol>
<li>package com.googlecode.garbagecan.test.socket.sample2;</li>
<li></li>
<li>import java.io.BufferedReader;</li>
<li>import java.io.InputStreamReader;</li>
<li>import java.io.PrintWriter;</li>
<li>import java.net.Socket;</li>
<li></li>
<li>public class MyClient {</li>
<li>    public static void main(String[] args) throws Exception {</li>
<li>        Socket socket = new Socket(“localhost”, 10000);</li>
<li>        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));</li>
<li>        PrintWriter out = new PrintWriter(socket.getOutputStream());</li>
<li>        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));</li>
<li></li>
<li>        while (true) {</li>
<li>            String msg = reader.readLine();</li>
<li>            out.println(msg);</li>
<li>            out.flush();</li>
<li>            if (msg.equals(“bye”)) {</li>
<li>                break;</li>
<li>            }</li>
<li>            System.out.println(in.readLine());</li>
<li>        }</li>
<li>        socket.close();</li>
<li>    }</li>
<li>}</li>
</ol>
<p>测试，首先运行MyServer类，然后运行两个MyClient类，然后分别在每个MyClient的提示符下输入字符串，就可以看到Server可以分别接收处理每个Client的请求了。</p>
<h1>三：传输对象</h1>
<p>首先需要一个普通的对象类，由于需要序列化这个对象以便在网络上传输，所以实现java.io.Serializable接口就是必不可少的了，入下：</p>
<ol>
<li>package com.googlecode.garbagecan.test.socket.sample3;</li>
<li></li>
<li>public class User implements java.io.Serializable {</li>
<li>    private static final long serialVersionUID = 1L;</li>
<li>    private String name;</li>
<li>    private String password;</li>
<li></li>
<li>    public User() {</li>
<li></li>
<li>    }</li>
<li></li>
<li>    public User(String name, String password) {</li>
<li>        this.name = name;</li>
<li>        this.password = password;</li>
<li>    }</li>
<li></li>
<li>    public String getName() {</li>
<li>        return name;</li>
<li>    }</li>
<li></li>
<li>    public void setName(String name) {</li>
<li>        this.name = name;</li>
<li>    }</li>
<li></li>
<li>    public String getPassword() {</li>
<li>        return password;</li>
<li>    }</li>
<li></li>
<li>    public void setPassword(String password) {</li>
<li>        this.password = password;</li>
<li>    }</li>
<li>}</li>
</ol>
<p>对于Server端的代码，代码中分别使用了ObjectInputStream和ObjectOutputStream来接收和发送socket中的InputStream和OutputStream，然后转换成Java对象，如下：</p>
<ol>
<li>package com.googlecode.garbagecan.test.socket.sample3;</li>
<li></li>
<li>import java.io.*;</li>
<li>import java.net.ServerSocket;</li>
<li>import java.net.Socket;</li>
<li>import java.util.logging.Level;</li>
<li>import java.util.logging.Logger;</li>
<li></li>
<li>public class MyServer {</li>
<li></li>
<li>    private final static Logger logger = Logger.getLogger(MyServer.class.getName());</li>
<li></li>
<li>    public static void main(String[] args) throws IOException {</li>
<li>        ServerSocket server = new ServerSocket(10000);</li>
<li></li>
<li>        while (true) {</li>
<li>            Socket socket = server.accept();</li>
<li>            invoke(socket);</li>
<li>        }</li>
<li>    }</li>
<li></li>
<li>    private static void invoke(final Socket socket) throws IOException {</li>
<li>        new Thread(new Runnable() {</li>
<li>            public void run() {</li>
<li>                ObjectInputStream is = null;</li>
<li>                ObjectOutputStream os = null;</li>
<li>                try {</li>
<li>                    is = new ObjectInputStream(new BufferedInputStream(socket.getInputStream()));</li>
<li>                    os = new ObjectOutputStream(socket.getOutputStream());</li>
<li></li>
<li>                    Object obj = is.readObject();</li>
<li>                    User user = (User)obj;</li>
<li>                    System.out.println(“user: ” + user.getName() + ”/” + user.getPassword());</li>
<li></li>
<li>                    user.setName(user.getName() + ”_new”);</li>
<li>                    user.setPassword(user.getPassword() + ”_new”);</li>
<li></li>
<li>                    os.writeObject(user);</li>
<li>                    os.flush();</li>
<li>                } catch (IOException ex) {</li>
<li>                    logger.log(Level.SEVERE, null, ex);</li>
<li>                } catch(ClassNotFoundException ex) {</li>
<li>                    logger.log(Level.SEVERE, null, ex);</li>
<li>                } finally {</li>
<li>                    try {</li>
<li>                        is.close();</li>
<li>                    } catch(Exception ex) {}</li>
<li>                    try {</li>
<li>                        os.close();</li>
<li>                    } catch(Exception ex) {}</li>
<li>                    try {</li>
<li>                        socket.close();</li>
<li>                    } catch(Exception ex) {}</li>
<li>                }</li>
<li>            }</li>
<li>        }).start();</li>
<li>    }</li>
<li>}</li>
</ol>
<p>Client也和Server端类似，同样使用ObjectOutputStream和ObjectInputStream来处理，如下：</p>
<ol>
<li>package com.googlecode.garbagecan.test.socket.sample3;</li>
<li></li>
<li>import java.io.BufferedInputStream;</li>
<li>import java.io.IOException;</li>
<li>import java.io.ObjectInputStream;</li>
<li>import java.io.ObjectOutputStream;</li>
<li>import java.net.Socket;</li>
<li>import java.util.logging.Level;</li>
<li>import java.util.logging.Logger;</li>
<li></li>
<li>public class MyClient {</li>
<li></li>
<li>    private final static Logger logger = Logger.getLogger(MyClient.class.getName());</li>
<li></li>
<li>    public static void main(String[] args) throws Exception {</li>
<li>        for (int i = 0; i &lt; 100; i++) {</li>
<li>            Socket socket = null;</li>
<li>            ObjectOutputStream os = null;</li>
<li>            ObjectInputStream is = null;</li>
<li></li>
<li>            try {</li>
<li>                socket = new Socket(“localhost”, 10000);</li>
<li></li>
<li>                os = new ObjectOutputStream(socket.getOutputStream());</li>
<li>                User user = new User(“user_” + i, ”password_” + i);</li>
<li>                os.writeObject(user);</li>
<li>                os.flush();</li>
<li></li>
<li>                is = new ObjectInputStream(new BufferedInputStream(socket.getInputStream()));</li>
<li>                Object obj = is.readObject();</li>
<li>                if (obj != null) {</li>
<li>                    user = (User)obj;</li>
<li>                    System.out.println(“user: ” + user.getName() + ”/” + user.getPassword());</li>
<li>                }</li>
<li>            } catch(IOException ex) {</li>
<li>                logger.log(Level.SEVERE, null, ex);</li>
<li>            } finally {</li>
<li>                try {</li>
<li>                    is.close();</li>
<li>                } catch(Exception ex) {}</li>
<li>                try {</li>
<li>                    os.close();</li>
<li>                } catch(Exception ex) {}</li>
<li>                try {</li>
<li>                    socket.close();</li>
<li>                } catch(Exception ex) {}</li>
<li>            }</li>
<li>        }</li>
<li>    }</li>
<li>}</li>
</ol>
<p>最后测试上面的代码，首先运行Server类，然后运行Client类，就可以分别在Server端和Client端控制台看到接收到的User对象实例了。</p>
<h1>四：传输压缩对象</h1>
<p>用Java Socket来传输对象，但是在有些情况下比如网络环境不好或者对象比较大的情况下需要把数据对象进行压缩然后在传输，此时就需要压缩这些对象流，此时就可以GZIPInputStream和GZIPOutputStream来处理一下socket的InputStream和OutputStream。</p>
<p>仍然需要一个实现了java.io.Serializable接口的简单Java对象：</p>
<ol>
<li>package com.googlecode.garbagecan.test.socket.sample4;</li>
<li></li>
<li>public class User implements java.io.Serializable {</li>
<li>    private static final long serialVersionUID = 1L;</li>
<li>    private String name;</li>
<li>    private String password;</li>
<li></li>
<li>    public User() {</li>
<li></li>
<li>    }</li>
<li></li>
<li>    public User(String name, String password) {</li>
<li>        this.name = name;</li>
<li>        this.password = password;</li>
<li>    }</li>
<li></li>
<li>    public String getName() {</li>
<li>        return name;</li>
<li>    }</li>
<li></li>
<li>    public void setName(String name) {</li>
<li>        this.name = name;</li>
<li>    }</li>
<li></li>
<li>    public String getPassword() {</li>
<li>        return password;</li>
<li>    }</li>
<li></li>
<li>    public void setPassword(String password) {</li>
<li>        this.password = password;</li>
<li>    }</li>
<li></li>
<li>}</li>
</ol>
<p>在Server端使用，socket的InputStream首先被包装成GZIPInputStream，然后又被包装成ObjectInputStream，而socket的OutputStream首先被包装成GZIPOutputStream，然后又被包装成ObjectOutputStream，如下：</p>
<ol>
<li>package com.googlecode.garbagecan.test.socket.sample4;</li>
<li></li>
<li>import java.io.IOException;</li>
<li>import java.io.ObjectInputStream;</li>
<li>import java.io.ObjectOutputStream;</li>
<li>import java.net.ServerSocket;</li>
<li>import java.net.Socket;</li>
<li>import java.util.logging.Level;</li>
<li>import java.util.logging.Logger;</li>
<li>import java.util.zip.GZIPInputStream;</li>
<li>import java.util.zip.GZIPOutputStream;</li>
<li></li>
<li>public class MyServer {</li>
<li></li>
<li>    private final static Logger logger = Logger.getLogger(MyServer.class.getName());</li>
<li></li>
<li>    public static void main(String[] args) throws IOException {</li>
<li>        ServerSocket server = new ServerSocket(10000);</li>
<li></li>
<li>        while (true) {</li>
<li>            Socket socket = server.accept();</li>
<li>            socket.setSoTimeout(10 * 1000);</li>
<li>            invoke(socket);</li>
<li>        }</li>
<li>    }</li>
<li></li>
<li>    private static void invoke(final Socket socket) throws IOException {</li>
<li>        new Thread(new Runnable() {</li>
<li>            public void run() {</li>
<li>                GZIPInputStream gzipis = null;</li>
<li>                ObjectInputStream ois = null;</li>
<li>                GZIPOutputStream gzipos = null;</li>
<li>                ObjectOutputStream oos = null;</li>
<li></li>
<li>                try {</li>
<li>                    gzipis = new GZIPInputStream(socket.getInputStream());</li>
<li>                    ois = new ObjectInputStream(gzipis);</li>
<li>                    gzipos = new GZIPOutputStream(socket.getOutputStream());</li>
<li>                    oos = new ObjectOutputStream(gzipos);</li>
<li></li>
<li>                    Object obj = ois.readObject();</li>
<li>                    User user = (User)obj;</li>
<li>                    System.out.println(“user: ” + user.getName() + ”/” + user.getPassword());</li>
<li></li>
<li>                    user.setName(user.getName() + ”_new”);</li>
<li>                    user.setPassword(user.getPassword() + ”_new”);</li>
<li></li>
<li>                    oos.writeObject(user);</li>
<li>                    oos.flush();</li>
<li>                    gzipos.finish();</li>
<li>                } catch (IOException ex) {</li>
<li>                    logger.log(Level.SEVERE, null, ex);</li>
<li>                } catch(ClassNotFoundException ex) {</li>
<li>                    logger.log(Level.SEVERE, null, ex);</li>
<li>                } finally {</li>
<li>                    try {</li>
<li>                        ois.close();</li>
<li>                    } catch(Exception ex) {}</li>
<li>                    try {</li>
<li>                        oos.close();</li>
<li>                    } catch(Exception ex) {}</li>
<li>                    try {</li>
<li>                        socket.close();</li>
<li>                    } catch(Exception ex) {}</li>
<li>                }</li>
<li>            }</li>
<li>        }).start();</li>
<li>    }</li>
<li>}</li>
</ol>
<p>Client也和Server端类似，同样要不socket的XXXStream包装成GZIPXXXStream，然后再包装成ObjectXXXStream，如下：</p>
<ol>
<li>package com.googlecode.garbagecan.test.socket.sample4;</li>
<li></li>
<li>import java.io.IOException;</li>
<li>import java.io.ObjectInputStream;</li>
<li>import java.io.ObjectOutputStream;</li>
<li>import java.net.InetSocketAddress;</li>
<li>import java.net.Socket;</li>
<li>import java.net.SocketAddress;</li>
<li>import java.util.logging.Level;</li>
<li>import java.util.logging.Logger;</li>
<li>import java.util.zip.GZIPInputStream;</li>
<li>import java.util.zip.GZIPOutputStream;</li>
<li></li>
<li>public class MyClient {</li>
<li></li>
<li>    private final static Logger logger = Logger.getLogger(MyClient.class.getName());</li>
<li></li>
<li>    public static void main(String[] args) throws Exception {</li>
<li>        for (int i = 0; i &lt; 10; i++) {</li>
<li>            Socket socket = null;</li>
<li>            GZIPOutputStream gzipos = null;</li>
<li>            ObjectOutputStream oos = null;</li>
<li>            GZIPInputStream gzipis = null;</li>
<li>            ObjectInputStream ois = null;</li>
<li></li>
<li>            try {</li>
<li>                socket = new Socket();</li>
<li>                SocketAddress socketAddress = new InetSocketAddress(“localhost”, 10000);</li>
<li>                socket.connect(socketAddress, 10 * 1000);</li>
<li>                socket.setSoTimeout(10 * 1000);</li>
<li></li>
<li>                gzipos = new GZIPOutputStream(socket.getOutputStream());</li>
<li>                oos = new ObjectOutputStream(gzipos);</li>
<li>                User user = new User(“user_” + i, ”password_” + i);</li>
<li>                oos.writeObject(user);</li>
<li>                oos.flush();</li>
<li>                gzipos.finish();</li>
<li></li>
<li>                gzipis = new GZIPInputStream(socket.getInputStream());</li>
<li>                ois = new ObjectInputStream(gzipis);</li>
<li>                Object obj = ois.readObject();</li>
<li>                if (obj != null) {</li>
<li>                    user = (User)obj;</li>
<li>                    System.out.println(“user: ” + user.getName() + ”/” + user.getPassword());</li>
<li>                }</li>
<li>            } catch(IOException ex) {</li>
<li>                logger.log(Level.SEVERE, null, ex);</li>
<li>            }</li>
<li>            try {</li>
<li>                oos.close();</li>
<li>            } catch (IOException e) {</li>
<li>            }</li>
<li>            try {</li>
<li>                ois.close();</li>
<li>            } catch (IOException e) {</li>
<li>            }</li>
<li>            try {</li>
<li>                socket.close();</li>
<li>            } catch (IOException e) {</li>
<li>            }</li>
<li>        }</li>
<li>    }</li>
<li>}</li>
</ol>
<p>最后测试上面的代码，首先运行Server类，然后运行Client类，就可以分别在Server端和Client端控制台看到接收到的User对象实例了。</p>
<h1>五:使用加密协议传输对象</h1>
<p>前面几篇博文提到了Socket中一些常用的用法，但是对于一些有安全要求的应用就需要加密传输的数据，此时就需要用到SSLSocket了。</p>
<p>还是一样需要一个实现了java.io.Serializable接口的简单Java对象：</p>
<ol>
<li>package com.googlecode.garbagecan.test.socket.ssl;</li>
<li></li>
<li>public class User implements java.io.Serializable {</li>
<li>    private static final long serialVersionUID = 1L;</li>
<li>    private String name;</li>
<li>    private String password;</li>
<li></li>
<li>    public User() {</li>
<li></li>
<li>    }</li>
<li></li>
<li>    public User(String name, String password) {</li>
<li>        this.name = name;</li>
<li>        this.password = password;</li>
<li>    }</li>
<li></li>
<li>    public String getName() {</li>
<li>        return name;</li>
<li>    }</li>
<li></li>
<li>    public void setName(String name) {</li>
<li>        this.name = name;</li>
<li>    }</li>
<li></li>
<li>    public String getPassword() {</li>
<li>        return password;</li>
<li>    }</li>
<li></li>
<li>    public void setPassword(String password) {</li>
<li>        this.password = password;</li>
<li>    }</li>
<li></li>
<li>}</li>
</ol>
<p>SSL Server类，这里需要用到ServerSocketFactory类来创建SSLServerSocket类实例，然后在通过SSLServerSocket来获取SSLSocket实例，这里考虑到面向对象中的面向接口编程的理念，所以代码中并没有出现SSLServerSocket和SSLSocket，而是用了他们的父类ServerSocket和Socket。在获取到ServerSocket和Socket实例以后，剩下的代码就和不使用加密方式一样了。</p>
<ol>
<li>package com.googlecode.garbagecan.test.socket.ssl;</li>
<li></li>
<li>import java.io.BufferedInputStream;</li>
<li>import java.io.IOException;</li>
<li>import java.io.ObjectInputStream;</li>
<li>import java.io.ObjectOutputStream;</li>
<li>import java.net.ServerSocket;</li>
<li>import java.net.Socket;</li>
<li>import java.util.logging.Level;</li>
<li>import java.util.logging.Logger;</li>
<li></li>
<li>import javax.net.ServerSocketFactory;</li>
<li>import javax.net.ssl.SSLServerSocketFactory;</li>
<li></li>
<li>public class MyServer {</li>
<li></li>
<li>    private final static Logger logger = Logger.getLogger(MyServer.class.getName());</li>
<li></li>
<li>    public static void main(String[] args) {</li>
<li>        try {</li>
<li>            ServerSocketFactory factory = SSLServerSocketFactory.getDefault();</li>
<li>            ServerSocket server = factory.createServerSocket(10000);</li>
<li></li>
<li>            while (true) {</li>
<li>                Socket socket = server.accept();</li>
<li>                invoke(socket);</li>
<li>            }</li>
<li>        } catch (Exception ex) {</li>
<li>            ex.printStackTrace();</li>
<li>        }</li>
<li>    }</li>
<li></li>
<li>    private static void invoke(final Socket socket) throws IOException {</li>
<li>        new Thread(new Runnable() {</li>
<li>            public void run() {</li>
<li>                ObjectInputStream is = null;</li>
<li>                ObjectOutputStream os = null;</li>
<li>                try {</li>
<li>                    is = new ObjectInputStream(new BufferedInputStream(socket.getInputStream()));</li>
<li>                    os = new ObjectOutputStream(socket.getOutputStream());</li>
<li></li>
<li>                    Object obj = is.readObject();</li>
<li>                    User user = (User)obj;</li>
<li>                    System.out.println(“user: ” + user.getName() + ”/” + user.getPassword());</li>
<li></li>
<li>                    user.setName(user.getName() + ”_new”);</li>
<li>                    user.setPassword(user.getPassword() + ”_new”);</li>
<li></li>
<li>                    os.writeObject(user);</li>
<li>                    os.flush();</li>
<li>                } catch (IOException ex) {</li>
<li>                    logger.log(Level.SEVERE, null, ex);</li>
<li>                } catch(ClassNotFoundException ex) {</li>
<li>                    logger.log(Level.SEVERE, null, ex);</li>
<li>                } finally {</li>
<li>                    try {</li>
<li>                        is.close();</li>
<li>                    } catch(Exception ex) {}</li>
<li>                    try {</li>
<li>                        os.close();</li>
<li>                    } catch(Exception ex) {}</li>
<li>                    try {</li>
<li>                        socket.close();</li>
<li>                    } catch(Exception ex) {}</li>
<li>                }</li>
<li>            }</li>
<li>        }).start();</li>
<li>    }</li>
<li>}</li>
</ol>
<p>SSL Client类和SSL Server类类似，只是将其中获取Socket的方式有所变化，其余的代码也和不使用加密方式一样。</p>
<ol>
<li>package com.googlecode.garbagecan.test.socket.ssl;</li>
<li></li>
<li>import java.io.BufferedInputStream;</li>
<li>import java.io.IOException;</li>
<li>import java.io.ObjectInputStream;</li>
<li>import java.io.ObjectOutputStream;</li>
<li>import java.net.Socket;</li>
<li>import java.util.logging.Level;</li>
<li>import java.util.logging.Logger;</li>
<li></li>
<li>import javax.net.SocketFactory;</li>
<li>import javax.net.ssl.SSLSocketFactory;</li>
<li></li>
<li>public class MyClient {</li>
<li></li>
<li>private final static Logger logger = Logger.getLogger(MyClient.class.getName());</li>
<li></li>
<li>    public static void main(String[] args) throws Exception {</li>
<li>        for (int i = 0; i &lt; 100; i++) {</li>
<li>            Socket socket = null;</li>
<li>            ObjectOutputStream os = null;</li>
<li>            ObjectInputStream is = null;</li>
<li></li>
<li>            try {</li>
<li>                SocketFactory factory = SSLSocketFactory.getDefault();</li>
<li>                socket = factory.createSocket(“localhost”, 10000);</li>
<li></li>
<li>                os = new ObjectOutputStream(socket.getOutputStream());</li>
<li>                User user = new User(“user_” + i, ”password_” + i);</li>
<li>                os.writeObject(user);</li>
<li>                os.flush();</li>
<li></li>
<li>                is = new ObjectInputStream(new BufferedInputStream(socket.getInputStream()));</li>
<li>                Object obj = is.readObject();</li>
<li>                if (obj != null) {</li>
<li>                    user = (User)obj;</li>
<li>                    System.out.println(“user: ” + user.getName() + ”/” + user.getPassword());</li>
<li>                }</li>
<li>            } catch(IOException ex) {</li>
<li>                logger.log(Level.SEVERE, null, ex);</li>
<li>            } finally {</li>
<li>                try {</li>
<li>                    is.close();</li>
<li>                } catch(Exception ex) {}</li>
<li>                try {</li>
<li>                    os.close();</li>
<li>                } catch(Exception ex) {}</li>
<li>                try {</li>
<li>                    socket.close();</li>
<li>                } catch(Exception ex) {}</li>
<li>            }</li>
<li>        }</li>
<li>    }</li>
<li>}</li>
</ol>
<p>代码写完了，下面就需要产生keystore文件了，运行下面的命令</p>
<ol>
<li>keytool -genkey -alias mysocket -keyalg RSA -keystore mysocket.jks</li>
</ol>
<p>在提示输入项中，密码项自己给定，其它都不改直接回车，这里我使用的密码是“mysocket”。</p>
<p>运行Server</p>
<ol>
<li>java -Djavax.net.ssl.keyStore=mysocket.jks -Djavax.net.ssl.keyStorePassword=mysocket com.googlecode.garbagecan.test.socket.ssl.MyServer</li>
</ol>
<p>运行Client</p>
<ol>
<li>java -Djavax.net.ssl.trustStore=mysocket.jks  -Djavax.net.ssl.trustStorePassword=mysocket com.googlecode.garbagecan.test.socket.ssl.MyClient</li>
</ol>
]]></content:encoded>
			<wfw:commentRss>http://www.wdcode.org/archives/547.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>JVM调优总结</title>
		<link>http://www.wdcode.org/archives/545.html</link>
		<comments>http://www.wdcode.org/archives/545.html#comments</comments>
		<pubDate>Wed, 11 Jan 2012 07:34:30 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[编程基础]]></category>
		<category><![CDATA[jvm]]></category>

		<guid isPermaLink="false">http://www.wdcode.org/?p=545</guid>
		<description><![CDATA[<h1>一些概念</h1>
<strong>数据类型</strong>

Java虚拟机中，数据类型可以分为两类：基本类型和引用类型。基本类型的变量保存原始值，即：他代表的值就是数值本身；而引用类型的变量保存引用值。“引用值”代表了某个对象的引用，而不是对象本身，对象本身存放在这个引用值所表示的地址的位置。

基本类型包括：byte,short,int,long,char,float,double,Boolean,returnAddress

引用类型包括：类类型，接口类型和数组。

<strong>堆与栈</strong>

堆和栈是程序运行的关键，很有必要把他们的关系说清楚。

<a href="http://images.51cto.com/files/uploadimg/20120109/161137614.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120109/161137614.gif" alt="" width="498" height="421" border="0" /></a>

<strong>栈是运行时的单位，而堆是存储的单位。</strong>

栈解决程序的运行问题，即程序如何执行，或者说如何处理数据；堆解决的是数据存储的问题，即数据怎么放、放在哪儿。

在Java中一个线程就会相应有一个线程栈与之对应，这点很容易理解，因为不同的线程执行逻辑有所不同，因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位，因此里面存储的信息都是跟当前线程（或程序）相关信息的。包括局部变量、程序运行状态、方法返回值等等；而堆只负责存储对象信息。

<strong>为什么要把堆和栈区分出来呢？栈中不是也可以存储数据吗？</strong>

第一，从软件设计的角度看，栈代表了处理逻辑，而堆代表了数据。这样分开，使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。

第二，堆与栈的分离，使得堆中的内容可以被多个栈共享（也可以理解为多个线程访问同一个对象）。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如：共享内存)，另一方面，堆中的共享常量和缓存可以被所有栈访问，节省了空间。

第三，栈因为运行时的需要，比如保存系统运行的上下文，需要进行地址段的划分。由于栈只能向上增长，因此就会限制住栈存储内容的能力。而堆不同，堆中的对象是可以根据需要动态增长的，因此栈和堆的拆分，使得动态增长成为可能，相应栈中只需记录堆中的一个地址即可。

第四，面向对象就是堆和栈的完美结合。其实，面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是，面向对象的引入，使得对待问题的思考方式发生了改变，而更接近于自然方式的思考。当我们把对象拆开，你会发现，对象的属性其实就是数据，存放在堆中；而对象的行为（方法），就是运行逻辑，放在栈中。我们在编写对象的时候，其实即编写了数据结构，也编写的处理数据的逻辑。不得不承认，面向对象的设计，确实很美。

<strong>在Java中，Main函数就是栈的起始点，也是程序的起始点。</strong>

程序要运行总是有一个起点的。同C语言一样，java中的Main就是那个起点。无论什么java程序，找到main就找到了程序执行的入口：）

<strong>堆中存什么？栈中存什么？</strong>

堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的，或者说是可以动态变化的，但是在栈中，一个对象只对应了一个4btye的引用（堆栈分离的好处：））。

为什么不把基本类型放堆中呢？因为其占用的空间一般是1~8个字节——需要空间比较少，而且因为是基本类型，所以不会出现动态增长的情况——长度固定，因此栈中存储就够了，如果把他存在堆中是没有什么意义的（还会浪费空间，后面说明）。可以这么说，基本类型和对象的引用都是存放在栈中，而且都是几个字节的一个数，因此在程序运行时，他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了，因为一个是栈中的数据一个是堆中的数据。最常见的一个问题就是，Java中参数传递时的问题。

<strong>Java中的参数传递时传值呢？还是传引用？</strong>

要说明这个问题，先要明确两点：

<strong>1. 不要试图与C进行类比，Java中没有指针的概念</strong>

<strong>2. 程序运行永远都是在栈中进行的，因而参数传递时，只存在传递基本类型和对象引用的问题。不会直接传对象本身。</strong>

明确以上两点后。Java在方法调用传递参数时，因为没有指针，所以<strong>它都是进行传值调用</strong>（这点可以参考C的传值调用）。因此，很多书里面都说Java是进行传值调用，这点没有问题，而且也简化的C中复杂性。

但是传引用的错觉是如何造成的呢？在运行栈中，基本类型和引用的处理是一样的，都是传值，所以，如果是传引用的方法调用，也同时可以理解为“传引用值”的传值调用，即引用的处理跟基本类型是完全一样的。但是当进入被调用方法时，被传递的这个引用的值，被程序解释（或者查找）到堆中的对象，这个时候才对应到真正的对象。如果此时进行修改，修改的是引用对应的对象，而不是引用本身，即：修改的是堆中的数据。所以这个修改是可以保持的了。

对象，从某种意义上说，是由基本类型组成的。可以把一个对象看作为一棵树，对象的属性如果还是对象，则还是一颗树（即非叶子节点），基本类型则为树的叶子节点。程序参数传递时，被传递的值本身都是不能进行修改的，但是，如果这个值是一个非叶子节点（即一个对象引用），则可以修改这个节点下面的所有内容。

堆和栈中，栈是程序运行最根本的东西。程序运行可以没有堆，但是不能没有栈。而堆是为栈进行数据存储服务，说白了堆就是一块共享的内存。不过，正是因为堆和栈的分离的思想，才使得Java的垃圾回收成为可能。

Java中，栈的大小通过-Xss来设置，当栈中存储数据比较多时，需要适当调大这个值，否则会出现java.lang.StackOverflowError异常。常见的出现这个异常的是无法返回的递归，因为此时栈中保存的信息都是方法返回的记录点。

<strong>Java对象的大小</strong>

基本数据的类型的大小是固定的，这里就不多说了。对于非基本类型的Java对象，其大小就值得商榷。

在Java中，一个空Object对象的大小是8byte，这个大小只是保存堆中一个没有任何属性的对象的大小。看下面语句：
<ol>
	<li>Object ob = new Object();</li>
</ol>
这样在程序中完成了一个Java对象的生命，但是它所占的空间为：4byte+8byte。4byte是上面部分所说的Java栈中保存引用的所需要的空间。而那8byte则是Java堆中对象的信息。因为所有的Java非基本类型的对象都需要默认继承Object对象，因此不论什么样的Java对象，其大小都必须是大于8byte。

有了Object对象的大小，我们就可以计算其他对象的大小了。
<ol>
	<li>Class NewObject {</li>
	<li>   int count;</li>
	<li>   boolean flag;</li>
	<li>   Object ob;</li>
	<li>   }</li>
	<li>//其大小为：空对象大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小(4byte)=17byte。</li>
	<li>但是因为Java在对对象内存分配时都是以8的整数倍来分，因此大于17byte的最接近8的整数倍的是24，因此此对象的大</li>
	<li>小为24byte。</li>
</ol>
这里需要注意一下基本类型的包装类型的大小。因为这种包装类型已经成为对象了，因此需要把他们作为对象来看待。包装类型的大小至少是12byte（声明一个空Object至少需要的空间），而且12byte没有包含任何有效信息，同时，因为Java对象大小是8的整数倍，因此一个基本类型包装类的大小至少是16byte。这个内存占用是很恐怖的，它是使用基本类型的N倍（N&#62;2），有些类型的内存占用更是夸张（随便想下就知道了）。因此，可能的话应尽量少使用包装类。在JDK5.0以后，因为加入了自动类型装换，因此，Java虚拟机会在存储方面进行相应的优化。

<strong>引用类型</strong>

对象引用类型分为<strong>强引用、软引用、弱引用和虚引用。</strong>

<strong>强引用：</strong>就是我们一般声明对象是时虚拟机生成的引用，强引用环境下，垃圾回收时需要严格判断当前对象是否被强引用，如果被强引用，则不会被垃圾回收

<strong>软引用：</strong>软引用一般被做为缓存来使用。与强引用的区别是，软引用在垃圾回收时，虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张，则虚拟机会回收软引用所引用的空间；如果剩余内存相对富裕，则不会进行回收。换句话说，虚拟机在发生OutOfMemory时，肯定是没有软引用存在的。

<strong>弱引用：</strong>弱引用与软引用类似，都是作为缓存来使用。但与软引用不同，弱引用在进行垃圾回收时，是一定会被回收掉的，因此其生命周期只存在于一个垃圾回收周期内。

强引用不用说，我们系统一般在使用时都是用的强引用。而“软引用”和“弱引用”比较少见。他们一般被作为缓存使用，而且一般是在内存大小比较受限的情况下做为缓存。因为如果内存足够大的话，可以直接使用强引用作为缓存即可，同时可控性更高。因而，他们常见的是被使用在桌面应用系统的缓存。
<h1>基本垃圾回收算法</h1>
可以从不同的的角度去划分垃圾回收算法：

<strong>按照基本回收策略分</strong>

<strong>引用计数（Reference Counting）：</strong>

比较古老的回收算法。原理是此对象有一个引用，即增加一个计数，删除一个引用则减少一个计数。垃圾回收时，只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。

<strong>标记-清除（Mark-Sweep）：</strong>

<a href="http://images.51cto.com/files/uploadimg/20120109/1705360.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120109/1705360.png" alt="" width="498" height="230" border="0" /></a>

此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象，第二阶段遍历整个堆，把未标记的对象清除。此算法需要暂停整个应用，同时，会产生内存碎片。

<strong>复制（Copying）：</strong>

<a href="http://images.51cto.com/files/uploadimg/20120109/1705361.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120109/1705361.png" alt="" width="498" height="230" border="0" /></a>

此算法把内存空间划为两个相等的区域，每次只使用其中一个区域。垃圾回收时，遍历当前使用区域，把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象，因此复制成本比较小，同时复制过去以后还能进行相应的内存整理，不会出现“碎片”问题。当然，此算法的缺点也是很明显的，就是需要两倍内存空间。

<strong>标记-整理（Mark-Compact）：</strong>

<a href="http://images.51cto.com/files/uploadimg/20120109/1705362.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120109/1705362.png" alt="" width="498" height="230" border="0" /></a>

此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段，第一阶段从根节点开始标记所有被引用对象，第二阶段遍历整个堆，把清除未标记对象并且把存活对象“压缩”到堆的其中一块，按顺序排放。此算法避免了“标记-清除”的碎片问题，同时也避免了“复制”算法的空间问题。

<strong>按分区对待的方式分</strong>

<strong>增量收集（Incremental Collecting）：</strong>实时垃圾回收算法，即：在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。

<strong>分代收集（Generational Collecting）：</strong>基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代，对不同生命周期的对象使用不同的算法（上述方式中的一个）进行回收。现在的垃圾回收器（从J2SE1.2开始）都是使用此算法的。

<strong>按系统线程分</strong>

<strong>串行收集：</strong>串行收集使用单线程处理所有垃圾回收工作，因为无需多线程交互，实现容易，而且效率比较高。但是，其局限性也比较明显，即无法使用多处理器的优势，所以此收集适合单处理器机器。当然，此收集器也可以用在小数据量（100M左右）情况下的多处理器机器上。

<strong>并行收集：</strong>并行收集使用多线程处理垃圾回收工作，因而速度快，效率高。而且理论上CPU数目越多，越能体现出并行收集器的优势。

<strong>并发收集：</strong>相对于串行收集和并行收集而言，前面两个在进行垃圾回收工作时，需要暂停整个运行环境，而只有垃圾回收程序在运行，因此，系统在垃圾回收时会有明显的暂停，而且暂停时间会因为堆越大而越长。
<h1>垃圾回收面临的问题</h1>
<strong>如何区分垃圾</strong>

上面说到的“引用计数”法，通过统计控制生成对象和删除对象时的引用数来判断。垃圾回收程序收集计数为0的对象即可。但是这种方法无法解决循环引用。所以，后来实现的垃圾判断算法中，都是从程序运行的根节点出发，遍历整个对象引用，查找存活的对象。那么在这种方式的实现中，<strong>垃圾回收从哪儿开始的呢？</strong>即，从哪儿开始查找哪些对象是正在被当前系统使用的。上面分析的堆和栈的区别，其中栈是真正进行程序执行地方，所以要获取哪些对象正在被使用，则需要从Java栈开始。同时，一个栈是与一个线程对应的，因此，如果有多个线程的话，则必须对这些线程对应的所有的栈进行检查。

<a href="http://images.51cto.com/files/uploadimg/20120109/1711460.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120109/1711460.png" alt="" width="498" height="230" border="0" /></a>

同时，除了栈外，还有系统运行时的寄存器等，也是存储程序运行数据的。这样，以栈或寄存器中的引用为起点，我们可以找到堆中的对象，又从这些对象找到对堆中其他对象的引用，这种引用逐步扩展，最终以null引用或者基本类型结束，这样就形成了一颗以Java栈中引用所对应的对象为根节点的一颗对象树，如果栈中有多个引用，则最终会形成多颗对象树。在这些对象树上的对象，都是当前系统运行所需要的对象，不能被垃圾回收。而其他剩余对象，则可以视为无法被引用到的对象，可以被当做垃圾进行回收。

因此，<strong>垃圾回收的起点是一些根对象（java栈, 静态变量, 寄存器...）</strong>。而最简单的Java栈就是Java程序执行的main函数。这种回收方式，也是上面提到的“标记-清除”的回收方式。

<strong>如何处理碎片</strong>

由于不同Java对象存活时间是不一定的，因此，在程序运行一段时间以后，如果不进行内存整理，就会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间，以及程序运行效率降低。所以，在上面提到的基本垃圾回收算法中，“复制”方式和“标记-整理”方式，都可以解决碎片的问题。

<strong>如何解决同时存在的对象创建和对象回收问题</strong>

垃圾回收线程是回收内存的，而程序运行线程则是消耗（或分配）内存的，<strong>一个回收内存，一个分配内存</strong>，从这点看，两者是矛盾的。因此，在现有的垃圾回收方式中，要进行垃圾回收前，一般都需要暂停整个应用（即：暂停内存的分配），然后进行垃圾回收，回收完成后再继续应用。这种实现方式是最直接，而且最有效的解决二者矛盾的方式。

但是<strong>这种方式有一个很明显的弊端，就是当堆空间持续增大时，垃圾回收的时间也将会相应的持续增大，对应应用暂停的时间也会相应的增大。</strong>一些对相应时间要求很高的应用，比如最大暂停时间要求是几百毫秒，那么当堆空间大于几个G时，就很有可能超过这个限制，在这种情况下，垃圾回收将会成为系统运行的一个瓶颈。为解决这种矛盾，有了并发垃圾回收算法，使用这种算法，垃圾回收线程与程序运行线程同时运行。在这种方式下，解决了暂停的问题，但是因为需要在新生成对象的同时又要回收对象，算法复杂性会大大增加，系统的处理能力也会相应降低，同时，“碎片”问题将会比较难解决。
<h1>分代垃圾回收详述</h1>
<strong>为什么要分代</strong>

分代的垃圾回收策略，是基于这样一个事实：不同的对象的生命周期是不一样的。因此，不同生命周期的对象可以采取不同的收集方式，以便提高回收效率。

在Java程序运行的过程中，会产生大量的对象，其中有些对象是与业务信息相关，比如Http请求中的Session对象、线程、Socket连接，这类对象跟业务直接挂钩，因此生命周期比较长。但是还有一些对象，主要是程序运行过程中生成的临时变量，这些对象生命周期会比较短，比如：String对象，由于其不变类的特性，系统会产生大量的这些对象，有些对象甚至只用一次即可回收。

试想，在不进行对象存活时间区分的情况下，每次垃圾回收都是对整个堆空间进行回收，花费时间相对会长，同时，因为每次回收都需要遍历所有存活对象，但实际上，对于生命周期长的对象而言，这种遍历是没有效果的，因为可能进行了很多次遍历，但是他们依旧存在。因此，分代垃圾回收采用分治的思想，进行代的划分，把不同生命周期的对象放在不同代上，不同代上采用最适合它的垃圾回收方式进行回收。

<strong>如何分代</strong>

<a href="http://images.51cto.com/files/uploadimg/20120110/1143270.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143270.png" alt="" width="498" height="318" border="0" /></a>

<strong>如图所示：</strong>

虚拟机中的共划分为三个代：年轻代（Young Generation）、年老点（Old Generation）和持久代（Permanent Generation）。其中持久代主要存放的是Java类的类信息，与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

<strong>年轻代：</strong>

所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区，两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时，还存活的对象将被复制到Survivor区（两个中的一个），当这个Survivor区满时，此区的存活对象将被复制到另外一个Survivor区，当这个Survivor去也满了的时候，从第一个Survivor区复制过来的并且此时还存活的对象，将被复制“年老区(Tenured)”。需要注意，Survivor的两个区是对称的，没先后关系，所以同一个区中可能同时存在从Eden复制过来 对象，和从前一个Survivor复制过来的对象，而复制到年老区的只有从第一个Survivor去过来的对象。而且，Survivor区总有一个是空的。同时，根据程序需要，Survivor区是可以配置为多个的（多于两个），这样可以增加对象在年轻代中的存在时间，减少被放到年老代的可能。

<strong>年老代：</strong>

在年轻代中经历了N次垃圾回收后仍然存活的对象，就会被放到年老代中。因此，可以认为年老代中存放的都是一些生命周期较长的对象。

<strong>持久代：</strong>

用于存放静态文件，如今Java类、方法等。持久代对垃圾回收没有显著影响，但是有些应用可能动态生成或者调用一些class，例如Hibernate等，在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=&#60;N&#62;进行设置。

<strong>什么情况下触发垃圾回收</strong>

由于对象进行了分代处理，因此垃圾回收区域、时间也不一样。GC有两种类型：Scavenge GC和Full GC。

<strong>Scavenge GC</strong>

一般情况下，当新对象生成，并且在Eden申请空间失败时，就会触发Scavenge GC，对Eden区域进行GC，清除非存活对象，并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行，不会影响到年老代。因为大部分对象都是从Eden区开始的，同时Eden区不会分配的很大，所以Eden区的GC会频繁进行。因而，一般在这里需要使用速度快、效率高的算法，使Eden去能尽快空闲出来。

<strong>Full GC</strong>

对整个堆进行整理，包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收，所以比Scavenge GC要慢，因此应该尽可能减少Full GC的次数。在对JVM调优的过程中，很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC：

&#160;
<table width="95%" border="0" cellspacing="0" cellpadding="6" align="center">
<tbody>
<tr>
<td bgcolor="#fdfddf"> 年老代（Tenured）被写满

持久代（Perm）被写满

System.gc()被显示调用

上一次GC之后Heap的各域分配策略动态变化</td>
</tr>
</tbody>
</table>
&#160;

<strong>分代垃圾回收流程示意</strong>

<a href="http://images.51cto.com/files/uploadimg/20120110/1143271.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143271.png" alt="" width="498" height="862" border="0" /></a>

<a href="http://images.51cto.com/files/uploadimg/20120110/1143272.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143272.png" alt="" width="498" height="999" border="0" /></a>

<a href="http://images.51cto.com/files/uploadimg/20120110/1143273.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143273.png" alt="" width="498" height="991" border="0" /></a>

<a href="http://images.51cto.com/files/uploadimg/20120110/1143274.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143274.png" alt="" width="498" height="984" border="0" /></a>

<strong>选择合适的垃圾收集算法</strong>

<strong>串行收集器</strong>

<a href="http://images.51cto.com/files/uploadimg/20120110/1143275.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143275.png" alt="" width="498" height="336" border="0" /></a>

用单线程处理所有垃圾回收工作，因为无需多线程交互，所以效率比较高。但是，也无法使用多处理器的优势，所以此收集器适合单处理器机器。当然，此收集器也可以用在小数据量（100M左右）情况下的多处理器机器上。可以使用-XX:+UseSerialGC打开。

<strong>并行收集器</strong>

<a href="http://images.51cto.com/files/uploadimg/20120110/1143276.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143276.png" alt="" width="498" height="308" border="0" /></a>

对年轻代进行并行垃圾回收，因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用-XX:+UseParallelGC.打开。并行收集器在J2SE5.0第六6更新上引入，在Java SE6.0中进行了增强--可以对年老代进行并行收集。如果年老代不使用并发收集的话，默认是使用单线程进行垃圾回收，因此会制约扩展能力。使用-XX:+UseParallelOldGC打开。

使用-XX:ParallelGCThreads=&#60;N&#62;设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。

<strong>此收集器可以进行如下配置：</strong>
&#160;
<table width="95%" border="0" cellspacing="0" cellpadding="6" align="center">
<tbody>
<tr>
<td bgcolor="#fdfddf"><strong><span style="color: #ff0000;">最大垃圾回收暂停：</span></strong><span style="color: #ff0000;"><span style="color: #ff0000;">指定垃圾回收时的最长暂停时间，通过-XX:MaxGCPauseMillis=&#60;N&#62;指定。&#60;N&#62;为毫秒.如果指定了此值的话，堆大小和垃圾回收相关参数会进行调整以达到指定值。设定此值可能会减少应用的吞吐量。</span></span><strong>吞吐量：</strong>吞吐量为垃圾回收时间与非垃圾回收时间的比值，通过-XX:GCTimeRatio=&#60;N&#62;来设定，公式为1/（1+N）。例如，-XX:GCTimeRatio=19时，表示5%的时间用于垃圾回收。默认情况为99，即1%的时间用于垃圾回收。</td>
</tr>
</tbody>
</table>
&#160;

<strong>并发收集器</strong>

可以保证大部分工作都并发进行（应用不停止），垃圾回收只暂停很少的时间，此收集器适合对响应时间要求比较高的中、大规模应用。使用-XX:+UseConcMarkSweepGC打开。

<a href="http://images.51cto.com/files/uploadimg/20120110/1143277.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143277.png" alt="" width="498" height="319" border="0" /></a>

并发收集器主要减少年老代的暂停时间，他在应用不停止的情况下使用独立的垃圾回收线程，跟踪可达对象。在每个年老代垃圾回收周期中，在收集初期并发收集器 会对整个应用进行简短的暂停，在收集中还会再暂停一次。第二次暂停会比第一次稍长，在此过程中多个线程同时进行垃圾回收工作。

并发收集器使用处理器换来短暂的停顿时间。在一个N个处理器的系统上，并发收集部分使用K/N个可用处理器进行回收，一般情况下1&#60;=K&#60;=N/4。

在只有一个处理器的主机上使用并发收集器，设置为incremental mode模式也可获得较短的停顿时间。

<strong>浮动垃圾：</strong>由于在应用运行的同时进行垃圾回收，所以有些垃圾可能在垃圾回收进行完成时产生，这样就造成了“Floating Garbage”，这些垃圾需要在下次垃圾回收周期时才能回收掉。所以，并发收集器一般需要20%的预留空间用于这些浮动垃圾。

<strong>Concurrent Mode Failure：</strong>并发收集器在应用运行时进行收集，所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用，否则，垃圾回收还未完成，堆空间先满了。这种情况下将会发生“并发模式失败”，此时整个应用将会暂停，进行垃圾回收。

<strong>启动并发收集器：</strong>因为并发收集在应用运行时进行收集，所以必须保证收集完成之前有足够的内存空间供程序使用，否则会出现“Concurrent Mode Failure”。通过设置-XX:CMSInitiatingOccupancyFraction=&#60;N&#62;指定还有多少剩余堆时开始执行并发收集。

<strong>小  结</strong>

<strong>串行处理器</strong>

◆  适用情况：数据量比较小（100M左右）；单处理器下并且对响应时间无要求的应用。

◆ 缺点：只能用于小型应用。

<strong>并行处理器：</strong>

◆ 适用情况：“对吞吐量有高要求”，多CPU、对应用响应时间无要求的中、大型应用。举例：后台处理、科学计算。
◆ 缺点：垃圾收集过程中应用响应时间可能加长。

<strong>并发处理器：</strong>

◆ 适用情况：“对响应时间有高要求”，多CPU、对应用响应时间有较高要求的中、大型应用。举例：Web服务器/应用服务器、电信交换、集成开发环境。
<h1>典型配置举例</h1>
以下配置主要针对分代垃圾回收算法而言。

<strong>堆大小设置</strong>

年轻代的设置很关键

JVM中最大堆大小有三方面限制：相关操作系统的数据模型（32-bt还是64-bit）限制；系统的可用虚拟内存限制；系统的可用物理内存限制。32位系统下，一般限制在1.5G~2G；64为操作系统对内存无限制。在Windows Server 2003 系统，3.5G物理内存，JDK5.0下测试，最大可设置为1478m。

<strong>典型设置：</strong>
<pre>java -Xmx3550m -Xms3550m -Xmn2g –Xss128k 
-Xmx3550m：设置JVM最大可用内存为3550M。 
-Xms3550m：设置JVM促使内存为3550m。此值可以设置与-Xmx相同，以避免每次垃圾回收完成后JVM重新分配内存。 
-Xmn2g：设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m，所以增大年轻代后，将会减小年老代大小。此值对系统性能影响较大，Sun官方推荐配置为整个堆的3/8。 
-Xss128k：设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M，以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下，减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的，不能无限生成，经验值在3000~5000左右。</pre>
<pre>java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 
-XX:SurvivorRatio=4 
-XX:MaxPermSize=16m 
-XX:MaxTenuringThreshold=0 
-XX:NewRatio=4:设置年轻代（包括Eden和两个Survivor区）与年老代的比值（除去持久代）。设置为4，则年轻代与年老代所占比值为1：4，年轻代占整个堆栈的1/5 
-XX:SurvivorRatio=4：设置年轻代中Eden区与Survivor区的大小比值。设置为4，则两个Survivor区与一个Eden区的比值为2:4，一个Survivor区占整个年轻代的1/6 
-XX:MaxPermSize=16m:设置持久代大小为16m。 
-XX:MaxTenuringThreshold=0：设置垃圾最大年龄。如果设置为0的话，则年轻代对象不经过Survivor区，直接进入年老代。对于年老代比较多的应用，可以提高效率。如果将此值设置为一个较大值，则年轻代对象会在Survivor区进行多次复制，这样可以增加对象再年轻代的存活时间，增加在年轻代即被回收的概论。</pre>
<strong>回收器选择</strong>

JVM给了三种选择：<strong>串行收集器、并行收集器、并发收集器</strong>，但是串行收集器只适用于小数据量的情况，所以这里的选择主要针对并行收集器和并发收集器。默认情况下，JDK5.0以前都是使用串行收集器，如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后，JVM会根据当前<a href="http://java.sun.com/j2se/1.5.0/docs/guide/vm/server-class.html">系统配置</a>进行判断。

<strong>吞吐量优先的并行收集器</strong>

如上文所述，并行收集器主要以到达一定的吞吐量为目标，适用于科学技术和后台处理等。

<strong>典型配置：</strong>
<pre>java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 
-XX:+UseParallelGC：选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下，年轻代使用并发收集，而年老代仍旧使用串行收集。 
-XX:ParallelGCThreads=20：配置并行收集器的线程数，即：同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。</pre>
<pre>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC 
-XX:+UseParallelOldGC：配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。</pre>
<pre>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间，如果无法满足此时间，JVM会自动调整年轻代大小，以满足此值。</pre>
<pre>n java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy 
-XX:+UseAdaptiveSizePolicy：设置此选项后，并行收集器会自动选择年轻代区大小和相应的Survivor区比例，以达到目标系统规定的最低相应时间或者收集频率等，此值建议使用并行收集器时，一直打开。</pre>
<strong>响应时间优先的并发收集器</strong>

如上文所述，并发收集器主要是保证系统的响应时间，减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。

<strong>典型配置：</strong>
<pre>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 
-XX:+UseConcMarkSweepGC：设置年老代为并发收集。测试中配置这个以后，-XX:NewRatio=4的配置失效了，原因不明。所以，此时年轻代大小最好用-Xmn设置。 
-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上，JVM会根据系统配置自行设置，所以无需再设置此值。</pre>
<pre>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection 
-XX:CMSFullGCsBeforeCompaction：由于并发收集器不对内存空间进行压缩、整理，所以运行一段时间以后会产生“碎片”，使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。 
-XX:+UseCMSCompactAtFullCollection：打开对年老代的压缩。可能会影响性能，但是可以消除碎片</pre>
<strong>辅助信息</strong>

JVM提供了大量命令行参数，打印信息，供调试使用。主要有以下一些：

<strong>-XX:+PrintGC：</strong>输出形式：[GC 118250K-&#62;113543K(130112K), 0.0094143 secs] [Full GC 121376K-&#62;10414K(130112K), 0.0650971 secs]

<strong>-XX:+PrintGCDetails：</strong>输出形式：[GC [DefNew: 8614K-&#62;781K(9088K), 0.0123035 secs] 118250K-&#62;113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K-&#62;8614K(9088K), 0.0000665 secs][Tenured: 112761K-&#62;10414K(121024K), 0.0433488 secs] 121376K-&#62;10414K(130112K), 0.0436268 secs]

<strong>-XX:+PrintGCTimeStamps </strong>-XX:+PrintGC：PrintGCTimeStamps可与上面两个混合使用
输出形式：11.851: [GC 98328K-&#62;93620K(130112K), 0.0082960 secs]

<strong>-XX:+PrintGCApplicationConcurrentTime：</strong>打印每次垃圾回收前，程序未中断的执行时间。可与上面混合使用。输出形式：Application time: 0.5291524

<strong>seconds</strong>

-XX:+PrintGCApplicationStoppedTime：打印垃圾回收期间程序暂停的时间。可与上面混合使用。输出形式：Total time for which application threads were stopped: 0.0468229 seconds

-XX:PrintHeapAtGC: 打印GC前后的详细堆栈信息。输出形式：
<ol>
	<li>34.702: [GC {Heap before gc invocations=7:</li>
	<li></li>
	<li>def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)</li>
	<li></li>
	<li>eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)</li>
	<li></li>
	<li>from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000)</li>
	<li></li>
	<li>to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000)</li>
	<li></li>
	<li>tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)</li>
	<li></li>
	<li>the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)</li>
	<li></li>
	<li>compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)</li>
	<li></li>
	<li>the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)</li>
	<li></li>
	<li>ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)</li>
	<li></li>
	<li>rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)</li>
	<li></li>
	<li>34.735: [DefNew: 52568K-&#62;3433K(55296K), 0.0072126 secs] 55264K-&#62;6615K(124928K)Heap after gc invocations=8:</li>
	<li></li>
	<li>def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)</li>
	<li></li>
	<li>eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)</li>
	<li></li>
	<li>from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)</li>
	<li></li>
	<li>to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)</li>
	<li></li>
	<li>tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)</li>
	<li></li>
	<li>the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)</li>
	<li></li>
	<li>compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)</li>
	<li></li>
	<li>the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)</li>
	<li></li>
	<li>ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)</li>
	<li></li>
	<li>rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)</li>
	<li></li>
	<li>}</li>
	<li></li>
	<li>, 0.0757599 secs]</li>
</ol>
<strong>-Xloggc:filename:</strong>与上面几个配合使用，把相关日志信息记录到文件以便分析。

<strong>常见配置汇总</strong>

<strong>堆设置</strong>

<strong>-Xms:</strong>初始堆大小

<strong>-Xmx:</strong>最大堆大小

<strong>-XX:NewSize=n:</strong>设置年轻代大小

<strong>-XX:NewRatio=n:</strong>设置年轻代和年老代的比值。如:为3，表示年轻代与年老代比值为1：3，年轻代占整个年轻代年老代和的1/4

<strong>-XX:SurvivorRatio=n:</strong>年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如：3，表示Eden：Survivor=3：2，一个Survivor区占整个年轻代的1/5

<strong>-XX:MaxPermSize=n:</strong>设置持久代大小

<strong>收集器设置</strong>

<strong>-XX:+UseSerialGC:</strong>设置串行收集器

<strong>-XX:+UseParallelGC:</strong>设置并行收集器

<strong>-XX:+UseParalledlOldGC:</strong>设置并行年老代收集器

<strong>-XX:+UseConcMarkSweepGC:</strong>设置并发收集器

<strong>垃圾回收统计信息</strong>

<strong>-XX:+PrintGC</strong>

<strong>-XX:+PrintGCDetails</strong>

<strong>-XX:+PrintGCTimeStamps</strong>

<strong>-Xloggc:filename</strong>

<strong>并行收集器设置</strong>

<strong>-XX:ParallelGCThreads=n:</strong>设置并行收集器收集时使用的CPU数。并行收集线程数。

<strong>-XX:MaxGCPauseMillis=n:</strong>设置并行收集最大暂停时间

<strong>-XX:GCTimeRatio=n:</strong>设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

<strong>并发收集器设置</strong>

<strong>-XX:+CMSIncrementalMode:</strong>设置为增量模式。适用于单CPU情况。

<strong>-XX:ParallelGCThreads=n:</strong>设置并发收集器年轻代收集方式为并行收集时，使用的CPU数。并行收集线程数。

<strong>调优总结</strong><strong>年轻代大小选择</strong>

<strong>响应时间优先的应用：</strong>尽可能设大，直到接近系统的最低响应时间限制（根据实际情况选择）。在此种情况下，年轻代收集发生的频率也是最小的。同时，减少到达年老代的对象。

<strong>吞吐量优先的应用：</strong>尽可能的设置大，可能到达Gbit的程度。因为对响应时间没有要求，垃圾收集可以并行进行，一般适合8CPU以上的应用。

<strong>年老代大小选择</strong>

响应时间优先的应用：年老代使用并发收集器，所以其大小需要小心设置，一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了，可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式；如果堆大了，则需要较长的收集时间。最优化的方案，一般需要参考以下数据获得：

1. 并发垃圾收集信息

2. 持久代并发收集次数

3. 传统GC信息

4. 花在年轻代和年老代回收上的时间比例

减少年轻代和年老代花费的时间，一般会提高应用的效率

<strong>吞吐量优先的应用</strong>

一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是，这样可以尽可能回收掉大部分短期对象，减少中期的对象，而年老代尽存放长期存活对象。

<strong>较小堆引起的碎片问题</strong>

因为年老代的并发收集器使用标记、清除算法，所以不会对堆进行压缩。当收集器回收时，他会把相邻的空间进行合并，这样可以分配给较大的对象。但是，当堆空间较小时，运行一段时间以后，就会出现“碎片”，如果并发收集器找不到足够的空间，那么并发收集器将会停止，然后使用传统的标记、清除方式进行回收。如果出现“碎片”，可能需要进行如下配置：

<strong>1. -XX:+UseCMSCompactAtFullCollection：</strong>使用并发收集器时，开启对年老代的压缩。

<strong>2. -XX:CMSFullGCsBeforeCompaction=0：</strong>上面配置开启的情况下，这里设置多少次Full GC后，对年老代进行压缩
<h1>新一代的垃圾回收算法</h1>
<strong>垃圾回收的瓶颈</strong>

传统分代垃圾回收方式，已经在一定程度上把垃圾回收给应用带来的负担降到了最小，把应用的吞吐量推到了一个极限。但是他无法解决的一个问题，就是Full GC所带来的应用暂停。在一些对实时性要求很高的应用场景下，GC暂停所带来的请求堆积和请求失败是无法接受的。这类应用可能要求请求的返回时间在几百甚至几十毫秒以内，如果分代垃圾回收方式要达到这个指标，只能把最大堆的设置限制在一个相对较小范围内，但是这样有限制了应用本身的处理能力，同样也是不可接收的。

分代垃圾回收方式确实也考虑了实时性要求而提供了并发回收器，支持最大暂停时间的设置，但是受限于分代垃圾回收的内存划分模型，其效果也不是很理想。

为了达到实时性的要求（其实Java语言最初的设计也是在嵌入式系统上的），一种新垃圾回收方式呼之欲出，它既支持短的暂停时间，又支持大的内存空间分配。可以很好的解决传统分代方式带来的问题。

<strong>增量收集的演进</strong>

增量收集的方式在理论上可以解决传统分代方式带来的问题。增量收集把对堆空间划分成一系列内存块，使用时，先使用其中一部分（不会全部用完），垃圾收集时把之前用掉的部分中的存活对象再放到后面没有用的空间中，这样可以实现一直边使用边收集的效果，避免了传统分代方式整个使用完了再暂停的回收的情况。

当然，传统分代收集方式也提供了并发收集，但是他有一个很致命的地方，就是把整个堆做为一个内存块，这样一方面会造成碎片（无法压缩），另一方面他的每次收集都是对整个堆的收集，无法进行选择，在暂停时间的控制上还是很弱。而增量方式，通过内存空间的分块，恰恰可以解决上面问题。

<strong>Garbage Firest（G1）</strong>

这部分的内容主要参考<a href="http://www.blogjava.net/BlueDavy/archive/2009/03/11/259230.html">这里</a>，这篇文章算是对G1算法论文的解读。我也没加什么东西了。

<strong>目  标</strong>

从设计目标看G1完全是为了大型应用而准备的。
<pre><strong>支持很大的堆 高吞吐量 </strong>--支持多CPU和垃圾回收线程
--在主线程暂停的情况下，使用并行收集
--在主线程运行的情况下，使用并发收
<strong>实时目标：</strong>可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收</pre>
当然G1要达到实时性的要求，相对传统的分代回收算法，在性能上会有一些损失。

<strong>算法详解</strong>

<a href="http://images.51cto.com/files/uploadimg/20120110/1434230.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1434230.png" alt="" width="481" height="172" border="0" /></a>

G1可谓博采众家之长，力求到达一种完美。他吸取了增量收集优点，把整个堆划分为一个一个等大小的区域（region）。内存的回收和划分都以region为单位；同时，他也吸取了CMS的特点，把这个垃圾回收过程分为几个阶段，分散一个垃圾回收过程；而且，G1也认同分代垃圾回收的思想，认为不同对象的生命周期不同，可以采取不同收集方式，因此，它也支持分代的垃圾回收。为了达到对回收时间的可预计性，G1在扫描了region以后，对其中的活跃对象的大小进行排序，首先会收集那些活跃对象小的region，以便快速回收空间（要复制的活跃对象少了），因为活跃对象小，里面可以认为多数都是垃圾，所以这种方式被称为Garbage First（G1）的垃圾回收算法，即：垃圾优先的回收。

回收步骤：

<strong>初始标记（Initial Marking）</strong>

G1对于每个region都保存了两个标识用的bitmap，一个为previous marking bitmap，一个为next marking bitmap，bitmap中包含了一个bit的地址信息来指向对象的起始点。

开始Initial Marking之前，首先并发的清空next marking bitmap，然后停止所有应用线程，并扫描标识出每个region中root可直接访问到的对象，将region中top的值放入next top at mark start（TAMS）中，之后恢复所有应用线程。

触发这个步骤执行的条件为：

&#160;
<table width="95%" border="0" cellspacing="0" cellpadding="6" align="center">
<tbody>
<tr>
<td bgcolor="#fdfddf"><span style="color: #ff0000;"><span style="color: #ff0000;">G1定义了一个JVM Heap大小的百分比的阀值，称为h，另外还有一个H，H的值为(1-h)*Heap Size，目前这个h的值是固定的，后续G1也许会将其改为动态的，根据jvm的运行情况来动态的调整，在分代方式下，G1还定义了一个u以及soft limit，soft limit的值为H-u*Heap Size，当Heap中使用的内存超过了soft limit值时，就会在一次clean up执行完毕后在应用允许的GC暂停时间范围内尽快的执行此步骤；</span></span>在pure方式下，G1将marking与clean up组成一个环，以便clean up能充分的使用marking的信息，当clean up开始回收时，首先回收能够带来最多内存空间的regions，当经过多次的clean up，回收到没多少空间的regions时，G1重新初始化一个新的marking与clean up构成的环。</td>
</tr>
</tbody>
</table>
&#160;

<strong>并发标记（Concurrent Marking）</strong>

按照之前Initial Marking扫描到的对象进行遍历，以识别这些对象的下层对象的活跃状态，对于在此期间应用线程并发修改的对象的以来关系则记录到remembered set logs中，新创建的对象则放入比top值更高的地址区间中，这些新创建的对象默认状态即为活跃的，同时修改top值。

<strong>最终标记暂停（Final Marking Pause）</strong>

当应用线程的remembered set logs未满时，是不会放入filled RS buffers中的，在这样的情况下，这些remebered set logs中记录的card的修改就会被更新了，因此需要这一步，这一步要做的就是把应用线程中存在的remembered set logs的内容进行处理，并相应的修改remembered sets，这一步需要暂停应用，并行的运行。

<strong>存活对象计算及清除（Live Data Counting and Cleanup）</strong>

值得注意的是，在G1中，并不是说Final Marking Pause执行完了，就肯定执行Cleanup这步的，由于这步需要暂停应用，G1为了能够达到准实时的要求，需要根据用户指定的最大的GC造成的暂停时间来合理的规划什么时候执行Cleanup，另外还有几种情况也是会触发这个步骤的执行的：

&#160;
<table width="95%" border="0" cellspacing="0" cellpadding="6" align="center">
<tbody>
<tr>
<td bgcolor="#fdfddf"><span style="color: #ff0000;"><span style="color: #ff0000;">G1采用的是复制方法来进行收集，必须保证每次的”to space”的空间都是够的，因此G1采取的策略是当已经使用的内存空间达到了H时，就执行Cleanup这个步骤；</span></span>对于full-young和partially-young的分代模式的G1而言，则还有情况会触发Cleanup的执行，full-young模式下，G1根据应用可接受的暂停时间、回收young regions需要消耗的时间来估算出一个yound regions的数量值，当JVM中分配对象的young regions的数量达到此值时，Cleanup就会执行；partially-young模式下，则会尽量频繁的在应用可接受的暂停时间范围内执行Cleanup，并最大限度的去执行non-young regions的Cleanup。</td>
</tr>
</tbody>
</table>
&#160;

<strong>展  望</strong>

以后JVM的调优或许跟多需要针对G1算法进行调优了。
<h1>调优方法</h1>
<strong>JVM调优工具</strong>

<strong>Jconsole，jProfile，VisualVM</strong>

<strong>Jconsole：</strong>jdk自带，功能简单，但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。详细说明参考这里

<strong>JProfiler：</strong>商业软件，需要付费。功能强大。详细说明参考这里

<strong>VisualVM：</strong>JDK自带，功能强大，与JProfiler类似。推荐。

<strong>如何调优</strong>

观察内存释放情况、集合类检查、对象树

上面这些调优工具都提供了强大的功能，但是总的来说一般分为以下几类功能

<strong>堆信息查看</strong>

<a href="http://images.51cto.com/files/uploadimg/20120110/1501570.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501570.png" alt="" width="411" height="323" border="0" /></a>

&#160;
<pre>可查看堆空间大小分配（年轻代、年老代、持久代分配）
提供即时的垃圾回收功能
垃圾监控（长时间监控回收情况）</pre>
<a href="http://images.51cto.com/files/uploadimg/20120110/1501571.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501571.png" alt="" width="498" height="244" border="0" /></a>
<pre>查看堆内类、对象信息查看：数量、类型等</pre>
<a href="http://images.51cto.com/files/uploadimg/20120110/1501572.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501572.png" alt="" width="428" height="343" border="0" /></a>
<pre>对象引用情况查看</pre>
有了堆信息查看方面的功能，我们一般可以顺利解决以下问题：

--年老代年轻代大小划分是否合理

--内存泄漏

--垃圾回收算法设置是否合理

<strong>线程监控</strong>

<a href="http://images.51cto.com/files/uploadimg/20120110/1501573.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501573.png" alt="" width="498" height="437" border="0" /></a>

&#160;
<pre>线程信息监控：系统线程数量
线程状态监控：各个线程都处在什么样的状态下</pre>
<a href="http://images.51cto.com/files/uploadimg/20120110/1501574.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501574.png" alt="" width="491" height="434" border="0" /></a>

&#160;
<pre>Dump线程详细信息：查看线程内部运行情况
死锁检查</pre>
<strong>热点分析</strong>

<a href="http://images.51cto.com/files/uploadimg/20120110/1501575.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501575.png" alt="" width="498" height="212" border="0" /></a>

<strong>CPU热点：</strong>检查系统哪些方法占用的大量CPU时间

<strong>内存热点：</strong>检查哪些对象在系统中数量最大（一定时间内存活对象和销毁对象一起统计）

这两个东西对于系统优化很有帮助。我们可以根据找到的热点，有针对性的进行系统的瓶颈查找和进行系统优化，而不是漫无目的的进行所有代码的优化。

<strong>快  照</strong>

快照是系统运行到某一时刻的一个定格。在我们进行调优的时候，不可能用眼睛去跟踪所有系统变化，依赖快照功能，我们就可以进行系统两个不同运行时刻，对象（或类、线程等）的不同，以便快速找到问题

举例说，我要检查系统进行垃圾回收以后，是否还有该收回的对象被遗漏下来的了。那么，我可以在进行垃圾回收前后，分别进行一次堆情况的快照，然后对比两次快照的对象情况。

<strong>内存泄漏检查</strong>

内存泄漏是比较常见的问题，而且解决方法也比较通用，这里可以重点说一下，而线程、热点方面的问题则是具体问题具体分析了。

内存泄漏一般可以理解为系统资源（各方面的资源，堆、栈、线程等）在错误使用的情况下，导致使用完毕的资源无法回收（或没有回收），从而导致新的资源分配请求无法完成，引起系统错误。

内存泄漏对系统危害比较大，因为他可以直接导致系统的崩溃。

需要区别一下，内存泄漏和系统超负荷两者是有区别的，虽然可能导致的最终结果是一样的。内存泄漏是用完的资源没有回收引起错误，而系统超负荷则是系统确实没有那么多资源可以分配了（其他的资源都在使用）。

<strong>年老代堆空间被占满</strong>

<strong>异常：</strong>java.lang.OutOfMemoryError: Java heap space

<strong>说明：</strong>

<a href="http://images.51cto.com/files/uploadimg/20120110/1501576.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501576.png" alt="" width="498" height="278" border="0" /></a>

这是最典型的内存泄漏方式，简单说就是所有堆空间都被无法回收的垃圾对象占满，虚拟机无法再在分配新空间。

如上图所示，这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都是一次垃圾回收点，所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点，可以发现一条由底到高的线，这说明，随时间的推移，系统的堆空间被不断占满，最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄漏。（上面的图仅供示例，在实际情况下收集数据的时间需要更长，比如几个小时或者几天）

<strong>解  决：</strong>

这种方式解决起来也比较容易，一般就是根据垃圾回收前后情况对比，同时根据对象引用情况（常见的集合对象引用）分析，基本都可以找到泄漏点。

<strong>持久代被占满</strong>

<strong>异常：</strong>java.lang.OutOfMemoryError: PermGen space

<strong>说明：</strong>

Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的，但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载，最终导致Perm区被占满。

更可怕的是，不同的classLoader即便使用了相同的类，但是都会对其进行加载，相当于同一个东西，如果有N个classLoader那么他将会被加载N次。因此，某些情况下，这个问题基本视为无解。当然，存在大量classLoader和大量反射类的情况其实也不多。

<strong>解  决：</strong>

1. -XX:MaxPermSize=16m

2. 换用JDK。比如JRocket。

<strong>堆栈溢出</strong>

<strong>异常：</strong>java.lang.StackOverflowError

<strong>说明：</strong>这个就不多说了，一般就是递归没返回，或者循环调用造成

<strong>线程堆栈满</strong>

<strong>异常：</strong>Fatal: Stack size too small

<strong>说明：</strong>java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后，将会出现上面异常。

<strong>解决：</strong>增加线程栈大小。-Xss2m。但这个配置无法解决根本问题，还要看代码部分是否有造成泄漏的部分。

<strong>系统内存被占满</strong>

<strong>异常：</strong>java.lang.OutOfMemoryError: unable to create new native thread

<strong>说明：</strong>

这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时，除了要在Java堆中分配内存外，操作系统本身也需要分配资源来创建线程。因此，当线程数量大到一定程度以后，堆中或许还有空间，但是操作系统分配不出资源来了，就出现这个异常了。

分配给Java虚拟机的内存愈多，系统剩余的资源就越少，因此，当系统内存固定时，分配给Java虚拟机的内存越多，那么，系统总共能够产生的线程也就越少，两者成反比的关系。同时，可以通过修改-Xss来减少分配给单个线程的空间，也可以增加系统总共内生产的线程数。

<strong>解  决：</strong>

1. 重新设计系统减少线程数量。

2. 线程数量不能减少的情况下，通过-Xss减小单个线程大小。以便能生产更多的线程。
<h1>反思</h1>
<strong>垃圾回收的悖论</strong>

所谓“成也萧何败萧何”。Java的垃圾回收确实带来了很多好处，为开发带来了便利。但是在一些高性能、高并发的情况下，垃圾回收确成为了制约Java应用的瓶颈。目前JDK的垃圾回收算法，始终无法解决垃圾回收时的暂停问题，因为这个暂停严重影响了程序的相应时间，造成拥塞或堆积。这也是后续JDK增加G1算法的一个重要原因。

当然，上面是从技术角度出发解决垃圾回收带来的问题，但是从系统设计方面我们就需要问一下了：
<pre><strong>我们需要分配如此大的内存空间给应用吗？ 我们是否能够通过有效使用内存而不是通过扩大内存的方式来设计我们的系统呢？ </strong></pre>
<strong>我们的内存中都放了什么</strong>

内存中需要放什么呢？个人认为，<strong>内存中需要放的是你的应用需要在不久的将来再次用到到的东西</strong>。想想看，如果你在将来不用这些东西，何必放内存呢？放文件、数据库不是更好？这些东西一般包括：
<pre>1. 系统运行时业务相关的数据。比如web应用中的session、即时消息的session等。这些数据一般在一个用户访问周期或者一个使用过程中都需要存在。 
2. 缓存。缓存就比较多了，你所要快速访问的都可以放这里面。其实上面的业务数据也可以理解为一种缓存。 
3. 线程。</pre>
因此，我们是不是可以这么认为，如果我们不把业务数据和缓存放在JVM中，或者把他们独立出来，那么Java应用使用时所需的内存将会大大减少，同时垃圾回收时间也会相应减少。

我认为这是可能的。

<strong>解决之道</strong>

数据库、文件系统

把所有数据都放入数据库或者文件系统，这是一种最为简单的方式。在这种方式下，Java应用的内存基本上等于处理一次峰值并发请求所需的内存。数据的获取都在每次请求时从数据库和文件系统中获取。也可以理解为，一次业务访问以后，所有对象都可以进行回收了。

这是一种内存使用最有效的方式，但是从应用角度来说，这种方式很低效。

内存-硬盘映射

上面的问题是因为我们使用了文件系统带来了低效。但是如果我们不是读写硬盘，而是写内存的话效率将会提高很多。

数据库和文件系统都是实实在在进行了持久化，但是当我们并不需要这样持久化的时候，我们可以做一些变通——把内存当硬盘使。

内存-硬盘映射很好很强大，既用了缓存又对Java应用的内存使用又没有影响。Java应用还是Java应用，他只知道读写的还是文件，但是实际上是内存。

这种方式兼得的Java应用与缓存两方面的好处。memcached的广泛使用也正是这一类的代表。

同一机器部署多个JVM

这也是一种很好的方式，可以分为纵拆和横拆。纵拆可以理解为把Java应用划分为不同模块，各个模块使用一个独立的Java进程。而横拆则是同样功能的应用部署多个JVM。

通过部署多个JVM，可以把每个JVM的内存控制一个垃圾回收可以忍受的范围内即可。但是这相当于进行了分布式的处理，其额外带来的复杂性也是需要评估的。另外，也有支持分布式的这种JVM可以考虑，不要要钱哦：）

程序控制的对象生命周期

这种方式是理想当中的方式，目前的虚拟机还没有，纯属假设。即：考虑由编程方式配置哪些对象在垃圾收集过程中可以直接跳过，减少垃圾回收线程遍历标记的时间。

这种方式相当于在编程的时候告诉虚拟机某些对象你可以在*时间后在进行收集或者由代码标识可以收集了（类似C、C++），在这之前你即便去遍历他也是没有效果的，他肯定是还在被引用的。

这种方式如果JVM可以实现，个人认为将是一个飞跃，Java即有了垃圾回收的优势，又有了C、C++对内存的可控性。

线程分配

Java的阻塞式的线程模型基本上可以抛弃了，目前成熟的NIO框架也比较多了。阻塞式IO带来的问题是线程数量的线性增长，而NIO则可以转换成为常数线程。因此，对于服务端的应用而言，NIO还是唯一选择。不过，JDK7中为我们带来的AIO是否能让人眼前一亮呢？我们拭目以待。

其他的JDK

本文说的都是Sun的JDK，目前常见的JDK还有JRocket和IBM的JDK。其中JRocket在IO方面比Sun的高很多，不过Sun JDK6.0以后提高也很大。而且JRocket在垃圾回收方面，也具有优势，其可设置垃圾回收的最大暂停时间也是很吸引人的。不过，系统Sun的G1实现以后，在这方面会有一个质的飞跃。]]></description>
			<content:encoded><![CDATA[<h1>一些概念</h1>
<p><strong>数据类型</strong></p>
<p>Java虚拟机中，数据类型可以分为两类：基本类型和引用类型。基本类型的变量保存原始值，即：他代表的值就是数值本身；而引用类型的变量保存引用值。“引用值”代表了某个对象的引用，而不是对象本身，对象本身存放在这个引用值所表示的地址的位置。</p>
<p>基本类型包括：byte,short,int,long,char,float,double,Boolean,returnAddress</p>
<p>引用类型包括：类类型，接口类型和数组。</p>
<p><strong>堆与栈</strong></p>
<p>堆和栈是程序运行的关键，很有必要把他们的关系说清楚。</p>
<p><a href="http://images.51cto.com/files/uploadimg/20120109/161137614.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120109/161137614.gif" alt="" width="498" height="421" border="0" /></a></p>
<p><strong>栈是运行时的单位，而堆是存储的单位。</strong></p>
<p>栈解决程序的运行问题，即程序如何执行，或者说如何处理数据；堆解决的是数据存储的问题，即数据怎么放、放在哪儿。</p>
<p>在Java中一个线程就会相应有一个线程栈与之对应，这点很容易理解，因为不同的线程执行逻辑有所不同，因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位，因此里面存储的信息都是跟当前线程（或程序）相关信息的。包括局部变量、程序运行状态、方法返回值等等；而堆只负责存储对象信息。</p>
<p><strong>为什么要把堆和栈区分出来呢？栈中不是也可以存储数据吗？</strong></p>
<p>第一，从软件设计的角度看，栈代表了处理逻辑，而堆代表了数据。这样分开，使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。</p>
<p>第二，堆与栈的分离，使得堆中的内容可以被多个栈共享（也可以理解为多个线程访问同一个对象）。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如：共享内存)，另一方面，堆中的共享常量和缓存可以被所有栈访问，节省了空间。</p>
<p>第三，栈因为运行时的需要，比如保存系统运行的上下文，需要进行地址段的划分。由于栈只能向上增长，因此就会限制住栈存储内容的能力。而堆不同，堆中的对象是可以根据需要动态增长的，因此栈和堆的拆分，使得动态增长成为可能，相应栈中只需记录堆中的一个地址即可。</p>
<p>第四，面向对象就是堆和栈的完美结合。其实，面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是，面向对象的引入，使得对待问题的思考方式发生了改变，而更接近于自然方式的思考。当我们把对象拆开，你会发现，对象的属性其实就是数据，存放在堆中；而对象的行为（方法），就是运行逻辑，放在栈中。我们在编写对象的时候，其实即编写了数据结构，也编写的处理数据的逻辑。不得不承认，面向对象的设计，确实很美。</p>
<p><strong>在Java中，Main函数就是栈的起始点，也是程序的起始点。</strong></p>
<p>程序要运行总是有一个起点的。同C语言一样，java中的Main就是那个起点。无论什么java程序，找到main就找到了程序执行的入口：）</p>
<p><strong>堆中存什么？栈中存什么？</strong></p>
<p>堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的，或者说是可以动态变化的，但是在栈中，一个对象只对应了一个4btye的引用（堆栈分离的好处：））。</p>
<p>为什么不把基本类型放堆中呢？因为其占用的空间一般是1~8个字节——需要空间比较少，而且因为是基本类型，所以不会出现动态增长的情况——长度固定，因此栈中存储就够了，如果把他存在堆中是没有什么意义的（还会浪费空间，后面说明）。可以这么说，基本类型和对象的引用都是存放在栈中，而且都是几个字节的一个数，因此在程序运行时，他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了，因为一个是栈中的数据一个是堆中的数据。最常见的一个问题就是，Java中参数传递时的问题。</p>
<p><strong>Java中的参数传递时传值呢？还是传引用？</strong></p>
<p>要说明这个问题，先要明确两点：</p>
<p><strong>1. 不要试图与C进行类比，Java中没有指针的概念</strong></p>
<p><strong>2. 程序运行永远都是在栈中进行的，因而参数传递时，只存在传递基本类型和对象引用的问题。不会直接传对象本身。</strong></p>
<p>明确以上两点后。Java在方法调用传递参数时，因为没有指针，所以<strong>它都是进行传值调用</strong>（这点可以参考C的传值调用）。因此，很多书里面都说Java是进行传值调用，这点没有问题，而且也简化的C中复杂性。</p>
<p>但是传引用的错觉是如何造成的呢？在运行栈中，基本类型和引用的处理是一样的，都是传值，所以，如果是传引用的方法调用，也同时可以理解为“传引用值”的传值调用，即引用的处理跟基本类型是完全一样的。但是当进入被调用方法时，被传递的这个引用的值，被程序解释（或者查找）到堆中的对象，这个时候才对应到真正的对象。如果此时进行修改，修改的是引用对应的对象，而不是引用本身，即：修改的是堆中的数据。所以这个修改是可以保持的了。</p>
<p>对象，从某种意义上说，是由基本类型组成的。可以把一个对象看作为一棵树，对象的属性如果还是对象，则还是一颗树（即非叶子节点），基本类型则为树的叶子节点。程序参数传递时，被传递的值本身都是不能进行修改的，但是，如果这个值是一个非叶子节点（即一个对象引用），则可以修改这个节点下面的所有内容。</p>
<p>堆和栈中，栈是程序运行最根本的东西。程序运行可以没有堆，但是不能没有栈。而堆是为栈进行数据存储服务，说白了堆就是一块共享的内存。不过，正是因为堆和栈的分离的思想，才使得Java的垃圾回收成为可能。</p>
<p>Java中，栈的大小通过-Xss来设置，当栈中存储数据比较多时，需要适当调大这个值，否则会出现java.lang.StackOverflowError异常。常见的出现这个异常的是无法返回的递归，因为此时栈中保存的信息都是方法返回的记录点。</p>
<p><strong>Java对象的大小</strong></p>
<p>基本数据的类型的大小是固定的，这里就不多说了。对于非基本类型的Java对象，其大小就值得商榷。</p>
<p>在Java中，一个空Object对象的大小是8byte，这个大小只是保存堆中一个没有任何属性的对象的大小。看下面语句：</p>
<ol>
<li>Object ob = new Object();</li>
</ol>
<p>这样在程序中完成了一个Java对象的生命，但是它所占的空间为：4byte+8byte。4byte是上面部分所说的Java栈中保存引用的所需要的空间。而那8byte则是Java堆中对象的信息。因为所有的Java非基本类型的对象都需要默认继承Object对象，因此不论什么样的Java对象，其大小都必须是大于8byte。</p>
<p>有了Object对象的大小，我们就可以计算其他对象的大小了。</p>
<ol>
<li>Class NewObject {</li>
<li>   int count;</li>
<li>   boolean flag;</li>
<li>   Object ob;</li>
<li>   }</li>
<li>//其大小为：空对象大小(8byte)+int大小(4byte)+Boolean大小(1byte)+空Object引用的大小(4byte)=17byte。</li>
<li>但是因为Java在对对象内存分配时都是以8的整数倍来分，因此大于17byte的最接近8的整数倍的是24，因此此对象的大</li>
<li>小为24byte。</li>
</ol>
<p>这里需要注意一下基本类型的包装类型的大小。因为这种包装类型已经成为对象了，因此需要把他们作为对象来看待。包装类型的大小至少是12byte（声明一个空Object至少需要的空间），而且12byte没有包含任何有效信息，同时，因为Java对象大小是8的整数倍，因此一个基本类型包装类的大小至少是16byte。这个内存占用是很恐怖的，它是使用基本类型的N倍（N&gt;2），有些类型的内存占用更是夸张（随便想下就知道了）。因此，可能的话应尽量少使用包装类。在JDK5.0以后，因为加入了自动类型装换，因此，Java虚拟机会在存储方面进行相应的优化。</p>
<p><strong>引用类型</strong></p>
<p>对象引用类型分为<strong>强引用、软引用、弱引用和虚引用。</strong></p>
<p><strong>强引用：</strong>就是我们一般声明对象是时虚拟机生成的引用，强引用环境下，垃圾回收时需要严格判断当前对象是否被强引用，如果被强引用，则不会被垃圾回收</p>
<p><strong>软引用：</strong>软引用一般被做为缓存来使用。与强引用的区别是，软引用在垃圾回收时，虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张，则虚拟机会回收软引用所引用的空间；如果剩余内存相对富裕，则不会进行回收。换句话说，虚拟机在发生OutOfMemory时，肯定是没有软引用存在的。</p>
<p><strong>弱引用：</strong>弱引用与软引用类似，都是作为缓存来使用。但与软引用不同，弱引用在进行垃圾回收时，是一定会被回收掉的，因此其生命周期只存在于一个垃圾回收周期内。</p>
<p>强引用不用说，我们系统一般在使用时都是用的强引用。而“软引用”和“弱引用”比较少见。他们一般被作为缓存使用，而且一般是在内存大小比较受限的情况下做为缓存。因为如果内存足够大的话，可以直接使用强引用作为缓存即可，同时可控性更高。因而，他们常见的是被使用在桌面应用系统的缓存。</p>
<h1>基本垃圾回收算法</h1>
<p>可以从不同的的角度去划分垃圾回收算法：</p>
<p><strong>按照基本回收策略分</strong></p>
<p><strong>引用计数（Reference Counting）：</strong></p>
<p>比较古老的回收算法。原理是此对象有一个引用，即增加一个计数，删除一个引用则减少一个计数。垃圾回收时，只用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。</p>
<p><strong>标记-清除（Mark-Sweep）：</strong></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120109/1705360.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120109/1705360.png" alt="" width="498" height="230" border="0" /></a></p>
<p>此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象，第二阶段遍历整个堆，把未标记的对象清除。此算法需要暂停整个应用，同时，会产生内存碎片。</p>
<p><strong>复制（Copying）：</strong></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120109/1705361.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120109/1705361.png" alt="" width="498" height="230" border="0" /></a></p>
<p>此算法把内存空间划为两个相等的区域，每次只使用其中一个区域。垃圾回收时，遍历当前使用区域，把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象，因此复制成本比较小，同时复制过去以后还能进行相应的内存整理，不会出现“碎片”问题。当然，此算法的缺点也是很明显的，就是需要两倍内存空间。</p>
<p><strong>标记-整理（Mark-Compact）：</strong></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120109/1705362.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120109/1705362.png" alt="" width="498" height="230" border="0" /></a></p>
<p>此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段，第一阶段从根节点开始标记所有被引用对象，第二阶段遍历整个堆，把清除未标记对象并且把存活对象“压缩”到堆的其中一块，按顺序排放。此算法避免了“标记-清除”的碎片问题，同时也避免了“复制”算法的空间问题。</p>
<p><strong>按分区对待的方式分</strong></p>
<p><strong>增量收集（Incremental Collecting）：</strong>实时垃圾回收算法，即：在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。</p>
<p><strong>分代收集（Generational Collecting）：</strong>基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代，对不同生命周期的对象使用不同的算法（上述方式中的一个）进行回收。现在的垃圾回收器（从J2SE1.2开始）都是使用此算法的。</p>
<p><strong>按系统线程分</strong></p>
<p><strong>串行收集：</strong>串行收集使用单线程处理所有垃圾回收工作，因为无需多线程交互，实现容易，而且效率比较高。但是，其局限性也比较明显，即无法使用多处理器的优势，所以此收集适合单处理器机器。当然，此收集器也可以用在小数据量（100M左右）情况下的多处理器机器上。</p>
<p><strong>并行收集：</strong>并行收集使用多线程处理垃圾回收工作，因而速度快，效率高。而且理论上CPU数目越多，越能体现出并行收集器的优势。</p>
<p><strong>并发收集：</strong>相对于串行收集和并行收集而言，前面两个在进行垃圾回收工作时，需要暂停整个运行环境，而只有垃圾回收程序在运行，因此，系统在垃圾回收时会有明显的暂停，而且暂停时间会因为堆越大而越长。</p>
<h1>垃圾回收面临的问题</h1>
<p><strong>如何区分垃圾</strong></p>
<p>上面说到的“引用计数”法，通过统计控制生成对象和删除对象时的引用数来判断。垃圾回收程序收集计数为0的对象即可。但是这种方法无法解决循环引用。所以，后来实现的垃圾判断算法中，都是从程序运行的根节点出发，遍历整个对象引用，查找存活的对象。那么在这种方式的实现中，<strong>垃圾回收从哪儿开始的呢？</strong>即，从哪儿开始查找哪些对象是正在被当前系统使用的。上面分析的堆和栈的区别，其中栈是真正进行程序执行地方，所以要获取哪些对象正在被使用，则需要从Java栈开始。同时，一个栈是与一个线程对应的，因此，如果有多个线程的话，则必须对这些线程对应的所有的栈进行检查。</p>
<p><a href="http://images.51cto.com/files/uploadimg/20120109/1711460.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120109/1711460.png" alt="" width="498" height="230" border="0" /></a></p>
<p>同时，除了栈外，还有系统运行时的寄存器等，也是存储程序运行数据的。这样，以栈或寄存器中的引用为起点，我们可以找到堆中的对象，又从这些对象找到对堆中其他对象的引用，这种引用逐步扩展，最终以null引用或者基本类型结束，这样就形成了一颗以Java栈中引用所对应的对象为根节点的一颗对象树，如果栈中有多个引用，则最终会形成多颗对象树。在这些对象树上的对象，都是当前系统运行所需要的对象，不能被垃圾回收。而其他剩余对象，则可以视为无法被引用到的对象，可以被当做垃圾进行回收。</p>
<p>因此，<strong>垃圾回收的起点是一些根对象（java栈, 静态变量, 寄存器&#8230;）</strong>。而最简单的Java栈就是Java程序执行的main函数。这种回收方式，也是上面提到的“标记-清除”的回收方式。</p>
<p><strong>如何处理碎片</strong></p>
<p>由于不同Java对象存活时间是不一定的，因此，在程序运行一段时间以后，如果不进行内存整理，就会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间，以及程序运行效率降低。所以，在上面提到的基本垃圾回收算法中，“复制”方式和“标记-整理”方式，都可以解决碎片的问题。</p>
<p><strong>如何解决同时存在的对象创建和对象回收问题</strong></p>
<p>垃圾回收线程是回收内存的，而程序运行线程则是消耗（或分配）内存的，<strong>一个回收内存，一个分配内存</strong>，从这点看，两者是矛盾的。因此，在现有的垃圾回收方式中，要进行垃圾回收前，一般都需要暂停整个应用（即：暂停内存的分配），然后进行垃圾回收，回收完成后再继续应用。这种实现方式是最直接，而且最有效的解决二者矛盾的方式。</p>
<p>但是<strong>这种方式有一个很明显的弊端，就是当堆空间持续增大时，垃圾回收的时间也将会相应的持续增大，对应应用暂停的时间也会相应的增大。</strong>一些对相应时间要求很高的应用，比如最大暂停时间要求是几百毫秒，那么当堆空间大于几个G时，就很有可能超过这个限制，在这种情况下，垃圾回收将会成为系统运行的一个瓶颈。为解决这种矛盾，有了并发垃圾回收算法，使用这种算法，垃圾回收线程与程序运行线程同时运行。在这种方式下，解决了暂停的问题，但是因为需要在新生成对象的同时又要回收对象，算法复杂性会大大增加，系统的处理能力也会相应降低，同时，“碎片”问题将会比较难解决。</p>
<h1>分代垃圾回收详述</h1>
<p><strong>为什么要分代</strong></p>
<p>分代的垃圾回收策略，是基于这样一个事实：不同的对象的生命周期是不一样的。因此，不同生命周期的对象可以采取不同的收集方式，以便提高回收效率。</p>
<p>在Java程序运行的过程中，会产生大量的对象，其中有些对象是与业务信息相关，比如Http请求中的Session对象、线程、Socket连接，这类对象跟业务直接挂钩，因此生命周期比较长。但是还有一些对象，主要是程序运行过程中生成的临时变量，这些对象生命周期会比较短，比如：String对象，由于其不变类的特性，系统会产生大量的这些对象，有些对象甚至只用一次即可回收。</p>
<p>试想，在不进行对象存活时间区分的情况下，每次垃圾回收都是对整个堆空间进行回收，花费时间相对会长，同时，因为每次回收都需要遍历所有存活对象，但实际上，对于生命周期长的对象而言，这种遍历是没有效果的，因为可能进行了很多次遍历，但是他们依旧存在。因此，分代垃圾回收采用分治的思想，进行代的划分，把不同生命周期的对象放在不同代上，不同代上采用最适合它的垃圾回收方式进行回收。</p>
<p><strong>如何分代</strong></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1143270.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143270.png" alt="" width="498" height="318" border="0" /></a></p>
<p><strong>如图所示：</strong></p>
<p>虚拟机中的共划分为三个代：年轻代（Young Generation）、年老点（Old Generation）和持久代（Permanent Generation）。其中持久代主要存放的是Java类的类信息，与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。</p>
<p><strong>年轻代：</strong></p>
<p>所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区，两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时，还存活的对象将被复制到Survivor区（两个中的一个），当这个Survivor区满时，此区的存活对象将被复制到另外一个Survivor区，当这个Survivor去也满了的时候，从第一个Survivor区复制过来的并且此时还存活的对象，将被复制“年老区(Tenured)”。需要注意，Survivor的两个区是对称的，没先后关系，所以同一个区中可能同时存在从Eden复制过来 对象，和从前一个Survivor复制过来的对象，而复制到年老区的只有从第一个Survivor去过来的对象。而且，Survivor区总有一个是空的。同时，根据程序需要，Survivor区是可以配置为多个的（多于两个），这样可以增加对象在年轻代中的存在时间，减少被放到年老代的可能。</p>
<p><strong>年老代：</strong></p>
<p>在年轻代中经历了N次垃圾回收后仍然存活的对象，就会被放到年老代中。因此，可以认为年老代中存放的都是一些生命周期较长的对象。</p>
<p><strong>持久代：</strong></p>
<p>用于存放静态文件，如今Java类、方法等。持久代对垃圾回收没有显著影响，但是有些应用可能动态生成或者调用一些class，例如Hibernate等，在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=&lt;N&gt;进行设置。</p>
<p><strong>什么情况下触发垃圾回收</strong></p>
<p>由于对象进行了分代处理，因此垃圾回收区域、时间也不一样。GC有两种类型：Scavenge GC和Full GC。</p>
<p><strong>Scavenge GC</strong></p>
<p>一般情况下，当新对象生成，并且在Eden申请空间失败时，就会触发Scavenge GC，对Eden区域进行GC，清除非存活对象，并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行，不会影响到年老代。因为大部分对象都是从Eden区开始的，同时Eden区不会分配的很大，所以Eden区的GC会频繁进行。因而，一般在这里需要使用速度快、效率高的算法，使Eden去能尽快空闲出来。</p>
<p><strong>Full GC</strong></p>
<p>对整个堆进行整理，包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收，所以比Scavenge GC要慢，因此应该尽可能减少Full GC的次数。在对JVM调优的过程中，很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC：</p>
<p>&nbsp;</p>
<table width="95%" border="0" cellspacing="0" cellpadding="6" align="center">
<tbody>
<tr>
<td bgcolor="#fdfddf"> 年老代（Tenured）被写满</p>
<p>持久代（Perm）被写满</p>
<p>System.gc()被显示调用</p>
<p>上一次GC之后Heap的各域分配策略动态变化</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p><strong>分代垃圾回收流程示意</strong></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1143271.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143271.png" alt="" width="498" height="862" border="0" /></a></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1143272.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143272.png" alt="" width="498" height="999" border="0" /></a></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1143273.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143273.png" alt="" width="498" height="991" border="0" /></a></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1143274.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143274.png" alt="" width="498" height="984" border="0" /></a></p>
<p><strong>选择合适的垃圾收集算法</strong></p>
<p><strong>串行收集器</strong></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1143275.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143275.png" alt="" width="498" height="336" border="0" /></a></p>
<p>用单线程处理所有垃圾回收工作，因为无需多线程交互，所以效率比较高。但是，也无法使用多处理器的优势，所以此收集器适合单处理器机器。当然，此收集器也可以用在小数据量（100M左右）情况下的多处理器机器上。可以使用-XX:+UseSerialGC打开。</p>
<p><strong>并行收集器</strong></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1143276.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143276.png" alt="" width="498" height="308" border="0" /></a></p>
<p>对年轻代进行并行垃圾回收，因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用-XX:+UseParallelGC.打开。并行收集器在J2SE5.0第六6更新上引入，在Java SE6.0中进行了增强&#8211;可以对年老代进行并行收集。如果年老代不使用并发收集的话，默认是使用单线程进行垃圾回收，因此会制约扩展能力。使用-XX:+UseParallelOldGC打开。</p>
<p>使用-XX:ParallelGCThreads=&lt;N&gt;设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。</p>
<p><strong>此收集器可以进行如下配置：</strong><br />
&nbsp;</p>
<table width="95%" border="0" cellspacing="0" cellpadding="6" align="center">
<tbody>
<tr>
<td bgcolor="#fdfddf"><strong><span style="color: #ff0000;">最大垃圾回收暂停：</span></strong><span style="color: #ff0000;"><span style="color: #ff0000;">指定垃圾回收时的最长暂停时间，通过-XX:MaxGCPauseMillis=&lt;N&gt;指定。&lt;N&gt;为毫秒.如果指定了此值的话，堆大小和垃圾回收相关参数会进行调整以达到指定值。设定此值可能会减少应用的吞吐量。</span></span><strong>吞吐量：</strong>吞吐量为垃圾回收时间与非垃圾回收时间的比值，通过-XX:GCTimeRatio=&lt;N&gt;来设定，公式为1/（1+N）。例如，-XX:GCTimeRatio=19时，表示5%的时间用于垃圾回收。默认情况为99，即1%的时间用于垃圾回收。</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p><strong>并发收集器</strong></p>
<p>可以保证大部分工作都并发进行（应用不停止），垃圾回收只暂停很少的时间，此收集器适合对响应时间要求比较高的中、大规模应用。使用-XX:+UseConcMarkSweepGC打开。</p>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1143277.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1143277.png" alt="" width="498" height="319" border="0" /></a></p>
<p>并发收集器主要减少年老代的暂停时间，他在应用不停止的情况下使用独立的垃圾回收线程，跟踪可达对象。在每个年老代垃圾回收周期中，在收集初期并发收集器 会对整个应用进行简短的暂停，在收集中还会再暂停一次。第二次暂停会比第一次稍长，在此过程中多个线程同时进行垃圾回收工作。</p>
<p>并发收集器使用处理器换来短暂的停顿时间。在一个N个处理器的系统上，并发收集部分使用K/N个可用处理器进行回收，一般情况下1&lt;=K&lt;=N/4。</p>
<p>在只有一个处理器的主机上使用并发收集器，设置为incremental mode模式也可获得较短的停顿时间。</p>
<p><strong>浮动垃圾：</strong>由于在应用运行的同时进行垃圾回收，所以有些垃圾可能在垃圾回收进行完成时产生，这样就造成了“Floating Garbage”，这些垃圾需要在下次垃圾回收周期时才能回收掉。所以，并发收集器一般需要20%的预留空间用于这些浮动垃圾。</p>
<p><strong>Concurrent Mode Failure：</strong>并发收集器在应用运行时进行收集，所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用，否则，垃圾回收还未完成，堆空间先满了。这种情况下将会发生“并发模式失败”，此时整个应用将会暂停，进行垃圾回收。</p>
<p><strong>启动并发收集器：</strong>因为并发收集在应用运行时进行收集，所以必须保证收集完成之前有足够的内存空间供程序使用，否则会出现“Concurrent Mode Failure”。通过设置-XX:CMSInitiatingOccupancyFraction=&lt;N&gt;指定还有多少剩余堆时开始执行并发收集。</p>
<p><strong>小  结</strong></p>
<p><strong>串行处理器</strong></p>
<p>◆  适用情况：数据量比较小（100M左右）；单处理器下并且对响应时间无要求的应用。</p>
<p>◆ 缺点：只能用于小型应用。</p>
<p><strong>并行处理器：</strong></p>
<p>◆ 适用情况：“对吞吐量有高要求”，多CPU、对应用响应时间无要求的中、大型应用。举例：后台处理、科学计算。<br />
◆ 缺点：垃圾收集过程中应用响应时间可能加长。</p>
<p><strong>并发处理器：</strong></p>
<p>◆ 适用情况：“对响应时间有高要求”，多CPU、对应用响应时间有较高要求的中、大型应用。举例：Web服务器/应用服务器、电信交换、集成开发环境。</p>
<h1>典型配置举例</h1>
<p>以下配置主要针对分代垃圾回收算法而言。</p>
<p><strong>堆大小设置</strong></p>
<p>年轻代的设置很关键</p>
<p>JVM中最大堆大小有三方面限制：相关操作系统的数据模型（32-bt还是64-bit）限制；系统的可用虚拟内存限制；系统的可用物理内存限制。32位系统下，一般限制在1.5G~2G；64为操作系统对内存无限制。在Windows Server 2003 系统，3.5G物理内存，JDK5.0下测试，最大可设置为1478m。</p>
<p><strong>典型设置：</strong></p>
<pre>java -Xmx3550m -Xms3550m -Xmn2g –Xss128k
-Xmx3550m：设置JVM最大可用内存为3550M。
-Xms3550m：设置JVM促使内存为3550m。此值可以设置与-Xmx相同，以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g：设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m，所以增大年轻代后，将会减小年老代大小。此值对系统性能影响较大，Sun官方推荐配置为整个堆的3/8。
-Xss128k：设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M，以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下，减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的，不能无限生成，经验值在3000~5000左右。</pre>
<pre>java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4
-XX:SurvivorRatio=4
-XX:MaxPermSize=16m
-XX:MaxTenuringThreshold=0
-XX:NewRatio=4:设置年轻代（包括Eden和两个Survivor区）与年老代的比值（除去持久代）。设置为4，则年轻代与年老代所占比值为1：4，年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4：设置年轻代中Eden区与Survivor区的大小比值。设置为4，则两个Survivor区与一个Eden区的比值为2:4，一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=0：设置垃圾最大年龄。如果设置为0的话，则年轻代对象不经过Survivor区，直接进入年老代。对于年老代比较多的应用，可以提高效率。如果将此值设置为一个较大值，则年轻代对象会在Survivor区进行多次复制，这样可以增加对象再年轻代的存活时间，增加在年轻代即被回收的概论。</pre>
<p><strong>回收器选择</strong></p>
<p>JVM给了三种选择：<strong>串行收集器、并行收集器、并发收集器</strong>，但是串行收集器只适用于小数据量的情况，所以这里的选择主要针对并行收集器和并发收集器。默认情况下，JDK5.0以前都是使用串行收集器，如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后，JVM会根据当前<a href="http://java.sun.com/j2se/1.5.0/docs/guide/vm/server-class.html">系统配置</a>进行判断。</p>
<p><strong>吞吐量优先的并行收集器</strong></p>
<p>如上文所述，并行收集器主要以到达一定的吞吐量为目标，适用于科学技术和后台处理等。</p>
<p><strong>典型配置：</strong></p>
<pre>java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelGC：选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下，年轻代使用并发收集，而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20：配置并行收集器的线程数，即：同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。</pre>
<pre>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
-XX:+UseParallelOldGC：配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。</pre>
<pre>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间，如果无法满足此时间，JVM会自动调整年轻代大小，以满足此值。</pre>
<pre>n java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy：设置此选项后，并行收集器会自动选择年轻代区大小和相应的Survivor区比例，以达到目标系统规定的最低相应时间或者收集频率等，此值建议使用并行收集器时，一直打开。</pre>
<p><strong>响应时间优先的并发收集器</strong></p>
<p>如上文所述，并发收集器主要是保证系统的响应时间，减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。</p>
<p><strong>典型配置：</strong></p>
<pre>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC：设置年老代为并发收集。测试中配置这个以后，-XX:NewRatio=4的配置失效了，原因不明。所以，此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上，JVM会根据系统配置自行设置，所以无需再设置此值。</pre>
<pre>java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction：由于并发收集器不对内存空间进行压缩、整理，所以运行一段时间以后会产生“碎片”，使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection：打开对年老代的压缩。可能会影响性能，但是可以消除碎片</pre>
<p><strong>辅助信息</strong></p>
<p>JVM提供了大量命令行参数，打印信息，供调试使用。主要有以下一些：</p>
<p><strong>-XX:+PrintGC：</strong>输出形式：[GC 118250K-&gt;113543K(130112K), 0.0094143 secs] [Full GC 121376K-&gt;10414K(130112K), 0.0650971 secs]</p>
<p><strong>-XX:+PrintGCDetails：</strong>输出形式：[GC [DefNew: 8614K-&gt;781K(9088K), 0.0123035 secs] 118250K-&gt;113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K-&gt;8614K(9088K), 0.0000665 secs][Tenured: 112761K-&gt;10414K(121024K), 0.0433488 secs] 121376K-&gt;10414K(130112K), 0.0436268 secs]</p>
<p><strong>-XX:+PrintGCTimeStamps </strong>-XX:+PrintGC：PrintGCTimeStamps可与上面两个混合使用<br />
输出形式：11.851: [GC 98328K-&gt;93620K(130112K), 0.0082960 secs]</p>
<p><strong>-XX:+PrintGCApplicationConcurrentTime：</strong>打印每次垃圾回收前，程序未中断的执行时间。可与上面混合使用。输出形式：Application time: 0.5291524</p>
<p><strong>seconds</strong></p>
<p>-XX:+PrintGCApplicationStoppedTime：打印垃圾回收期间程序暂停的时间。可与上面混合使用。输出形式：Total time for which application threads were stopped: 0.0468229 seconds</p>
<p>-XX:PrintHeapAtGC: 打印GC前后的详细堆栈信息。输出形式：</p>
<ol>
<li>34.702: [GC {Heap before gc invocations=7:</li>
<li></li>
<li>def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)</li>
<li></li>
<li>eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)</li>
<li></li>
<li>from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000)</li>
<li></li>
<li>to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000)</li>
<li></li>
<li>tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)</li>
<li></li>
<li>the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)</li>
<li></li>
<li>compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)</li>
<li></li>
<li>the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)</li>
<li></li>
<li>ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)</li>
<li></li>
<li>rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)</li>
<li></li>
<li>34.735: [DefNew: 52568K-&gt;3433K(55296K), 0.0072126 secs] 55264K-&gt;6615K(124928K)Heap after gc invocations=8:</li>
<li></li>
<li>def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)</li>
<li></li>
<li>eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)</li>
<li></li>
<li>from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)</li>
<li></li>
<li>to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)</li>
<li></li>
<li>tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)</li>
<li></li>
<li>the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)</li>
<li></li>
<li>compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)</li>
<li></li>
<li>the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)</li>
<li></li>
<li>ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)</li>
<li></li>
<li>rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)</li>
<li></li>
<li>}</li>
<li></li>
<li>, 0.0757599 secs]</li>
</ol>
<p><strong>-Xloggc:filename:</strong>与上面几个配合使用，把相关日志信息记录到文件以便分析。</p>
<p><strong>常见配置汇总</strong></p>
<p><strong>堆设置</strong></p>
<p><strong>-Xms:</strong>初始堆大小</p>
<p><strong>-Xmx:</strong>最大堆大小</p>
<p><strong>-XX:NewSize=n:</strong>设置年轻代大小</p>
<p><strong>-XX:NewRatio=n:</strong>设置年轻代和年老代的比值。如:为3，表示年轻代与年老代比值为1：3，年轻代占整个年轻代年老代和的1/4</p>
<p><strong>-XX:SurvivorRatio=n:</strong>年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如：3，表示Eden：Survivor=3：2，一个Survivor区占整个年轻代的1/5</p>
<p><strong>-XX:MaxPermSize=n:</strong>设置持久代大小</p>
<p><strong>收集器设置</strong></p>
<p><strong>-XX:+UseSerialGC:</strong>设置串行收集器</p>
<p><strong>-XX:+UseParallelGC:</strong>设置并行收集器</p>
<p><strong>-XX:+UseParalledlOldGC:</strong>设置并行年老代收集器</p>
<p><strong>-XX:+UseConcMarkSweepGC:</strong>设置并发收集器</p>
<p><strong>垃圾回收统计信息</strong></p>
<p><strong>-XX:+PrintGC</strong></p>
<p><strong>-XX:+PrintGCDetails</strong></p>
<p><strong>-XX:+PrintGCTimeStamps</strong></p>
<p><strong>-Xloggc:filename</strong></p>
<p><strong>并行收集器设置</strong></p>
<p><strong>-XX:ParallelGCThreads=n:</strong>设置并行收集器收集时使用的CPU数。并行收集线程数。</p>
<p><strong>-XX:MaxGCPauseMillis=n:</strong>设置并行收集最大暂停时间</p>
<p><strong>-XX:GCTimeRatio=n:</strong>设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)</p>
<p><strong>并发收集器设置</strong></p>
<p><strong>-XX:+CMSIncrementalMode:</strong>设置为增量模式。适用于单CPU情况。</p>
<p><strong>-XX:ParallelGCThreads=n:</strong>设置并发收集器年轻代收集方式为并行收集时，使用的CPU数。并行收集线程数。</p>
<p><strong>调优总结</strong><strong>年轻代大小选择</strong></p>
<p><strong>响应时间优先的应用：</strong>尽可能设大，直到接近系统的最低响应时间限制（根据实际情况选择）。在此种情况下，年轻代收集发生的频率也是最小的。同时，减少到达年老代的对象。</p>
<p><strong>吞吐量优先的应用：</strong>尽可能的设置大，可能到达Gbit的程度。因为对响应时间没有要求，垃圾收集可以并行进行，一般适合8CPU以上的应用。</p>
<p><strong>年老代大小选择</strong></p>
<p>响应时间优先的应用：年老代使用并发收集器，所以其大小需要小心设置，一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了，可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式；如果堆大了，则需要较长的收集时间。最优化的方案，一般需要参考以下数据获得：</p>
<p>1. 并发垃圾收集信息</p>
<p>2. 持久代并发收集次数</p>
<p>3. 传统GC信息</p>
<p>4. 花在年轻代和年老代回收上的时间比例</p>
<p>减少年轻代和年老代花费的时间，一般会提高应用的效率</p>
<p><strong>吞吐量优先的应用</strong></p>
<p>一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是，这样可以尽可能回收掉大部分短期对象，减少中期的对象，而年老代尽存放长期存活对象。</p>
<p><strong>较小堆引起的碎片问题</strong></p>
<p>因为年老代的并发收集器使用标记、清除算法，所以不会对堆进行压缩。当收集器回收时，他会把相邻的空间进行合并，这样可以分配给较大的对象。但是，当堆空间较小时，运行一段时间以后，就会出现“碎片”，如果并发收集器找不到足够的空间，那么并发收集器将会停止，然后使用传统的标记、清除方式进行回收。如果出现“碎片”，可能需要进行如下配置：</p>
<p><strong>1. -XX:+UseCMSCompactAtFullCollection：</strong>使用并发收集器时，开启对年老代的压缩。</p>
<p><strong>2. -XX:CMSFullGCsBeforeCompaction=0：</strong>上面配置开启的情况下，这里设置多少次Full GC后，对年老代进行压缩</p>
<h1>新一代的垃圾回收算法</h1>
<p><strong>垃圾回收的瓶颈</strong></p>
<p>传统分代垃圾回收方式，已经在一定程度上把垃圾回收给应用带来的负担降到了最小，把应用的吞吐量推到了一个极限。但是他无法解决的一个问题，就是Full GC所带来的应用暂停。在一些对实时性要求很高的应用场景下，GC暂停所带来的请求堆积和请求失败是无法接受的。这类应用可能要求请求的返回时间在几百甚至几十毫秒以内，如果分代垃圾回收方式要达到这个指标，只能把最大堆的设置限制在一个相对较小范围内，但是这样有限制了应用本身的处理能力，同样也是不可接收的。</p>
<p>分代垃圾回收方式确实也考虑了实时性要求而提供了并发回收器，支持最大暂停时间的设置，但是受限于分代垃圾回收的内存划分模型，其效果也不是很理想。</p>
<p>为了达到实时性的要求（其实Java语言最初的设计也是在嵌入式系统上的），一种新垃圾回收方式呼之欲出，它既支持短的暂停时间，又支持大的内存空间分配。可以很好的解决传统分代方式带来的问题。</p>
<p><strong>增量收集的演进</strong></p>
<p>增量收集的方式在理论上可以解决传统分代方式带来的问题。增量收集把对堆空间划分成一系列内存块，使用时，先使用其中一部分（不会全部用完），垃圾收集时把之前用掉的部分中的存活对象再放到后面没有用的空间中，这样可以实现一直边使用边收集的效果，避免了传统分代方式整个使用完了再暂停的回收的情况。</p>
<p>当然，传统分代收集方式也提供了并发收集，但是他有一个很致命的地方，就是把整个堆做为一个内存块，这样一方面会造成碎片（无法压缩），另一方面他的每次收集都是对整个堆的收集，无法进行选择，在暂停时间的控制上还是很弱。而增量方式，通过内存空间的分块，恰恰可以解决上面问题。</p>
<p><strong>Garbage Firest（G1）</strong></p>
<p>这部分的内容主要参考<a href="http://www.blogjava.net/BlueDavy/archive/2009/03/11/259230.html">这里</a>，这篇文章算是对G1算法论文的解读。我也没加什么东西了。</p>
<p><strong>目  标</strong></p>
<p>从设计目标看G1完全是为了大型应用而准备的。</p>
<pre><strong>支持很大的堆 高吞吐量 </strong>--支持多CPU和垃圾回收线程
--在主线程暂停的情况下，使用并行收集
--在主线程运行的情况下，使用并发收
<strong>实时目标：</strong>可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收</pre>
<p>当然G1要达到实时性的要求，相对传统的分代回收算法，在性能上会有一些损失。</p>
<p><strong>算法详解</strong></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1434230.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1434230.png" alt="" width="481" height="172" border="0" /></a></p>
<p>G1可谓博采众家之长，力求到达一种完美。他吸取了增量收集优点，把整个堆划分为一个一个等大小的区域（region）。内存的回收和划分都以region为单位；同时，他也吸取了CMS的特点，把这个垃圾回收过程分为几个阶段，分散一个垃圾回收过程；而且，G1也认同分代垃圾回收的思想，认为不同对象的生命周期不同，可以采取不同收集方式，因此，它也支持分代的垃圾回收。为了达到对回收时间的可预计性，G1在扫描了region以后，对其中的活跃对象的大小进行排序，首先会收集那些活跃对象小的region，以便快速回收空间（要复制的活跃对象少了），因为活跃对象小，里面可以认为多数都是垃圾，所以这种方式被称为Garbage First（G1）的垃圾回收算法，即：垃圾优先的回收。</p>
<p>回收步骤：</p>
<p><strong>初始标记（Initial Marking）</strong></p>
<p>G1对于每个region都保存了两个标识用的bitmap，一个为previous marking bitmap，一个为next marking bitmap，bitmap中包含了一个bit的地址信息来指向对象的起始点。</p>
<p>开始Initial Marking之前，首先并发的清空next marking bitmap，然后停止所有应用线程，并扫描标识出每个region中root可直接访问到的对象，将region中top的值放入next top at mark start（TAMS）中，之后恢复所有应用线程。</p>
<p>触发这个步骤执行的条件为：</p>
<p>&nbsp;</p>
<table width="95%" border="0" cellspacing="0" cellpadding="6" align="center">
<tbody>
<tr>
<td bgcolor="#fdfddf"><span style="color: #ff0000;"><span style="color: #ff0000;">G1定义了一个JVM Heap大小的百分比的阀值，称为h，另外还有一个H，H的值为(1-h)*Heap Size，目前这个h的值是固定的，后续G1也许会将其改为动态的，根据jvm的运行情况来动态的调整，在分代方式下，G1还定义了一个u以及soft limit，soft limit的值为H-u*Heap Size，当Heap中使用的内存超过了soft limit值时，就会在一次clean up执行完毕后在应用允许的GC暂停时间范围内尽快的执行此步骤；</span></span>在pure方式下，G1将marking与clean up组成一个环，以便clean up能充分的使用marking的信息，当clean up开始回收时，首先回收能够带来最多内存空间的regions，当经过多次的clean up，回收到没多少空间的regions时，G1重新初始化一个新的marking与clean up构成的环。</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p><strong>并发标记（Concurrent Marking）</strong></p>
<p>按照之前Initial Marking扫描到的对象进行遍历，以识别这些对象的下层对象的活跃状态，对于在此期间应用线程并发修改的对象的以来关系则记录到remembered set logs中，新创建的对象则放入比top值更高的地址区间中，这些新创建的对象默认状态即为活跃的，同时修改top值。</p>
<p><strong>最终标记暂停（Final Marking Pause）</strong></p>
<p>当应用线程的remembered set logs未满时，是不会放入filled RS buffers中的，在这样的情况下，这些remebered set logs中记录的card的修改就会被更新了，因此需要这一步，这一步要做的就是把应用线程中存在的remembered set logs的内容进行处理，并相应的修改remembered sets，这一步需要暂停应用，并行的运行。</p>
<p><strong>存活对象计算及清除（Live Data Counting and Cleanup）</strong></p>
<p>值得注意的是，在G1中，并不是说Final Marking Pause执行完了，就肯定执行Cleanup这步的，由于这步需要暂停应用，G1为了能够达到准实时的要求，需要根据用户指定的最大的GC造成的暂停时间来合理的规划什么时候执行Cleanup，另外还有几种情况也是会触发这个步骤的执行的：</p>
<p>&nbsp;</p>
<table width="95%" border="0" cellspacing="0" cellpadding="6" align="center">
<tbody>
<tr>
<td bgcolor="#fdfddf"><span style="color: #ff0000;"><span style="color: #ff0000;">G1采用的是复制方法来进行收集，必须保证每次的”to space”的空间都是够的，因此G1采取的策略是当已经使用的内存空间达到了H时，就执行Cleanup这个步骤；</span></span>对于full-young和partially-young的分代模式的G1而言，则还有情况会触发Cleanup的执行，full-young模式下，G1根据应用可接受的暂停时间、回收young regions需要消耗的时间来估算出一个yound regions的数量值，当JVM中分配对象的young regions的数量达到此值时，Cleanup就会执行；partially-young模式下，则会尽量频繁的在应用可接受的暂停时间范围内执行Cleanup，并最大限度的去执行non-young regions的Cleanup。</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p><strong>展  望</strong></p>
<p>以后JVM的调优或许跟多需要针对G1算法进行调优了。</p>
<h1>调优方法</h1>
<p><strong>JVM调优工具</strong></p>
<p><strong>Jconsole，jProfile，VisualVM</strong></p>
<p><strong>Jconsole：</strong>jdk自带，功能简单，但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。详细说明参考这里</p>
<p><strong>JProfiler：</strong>商业软件，需要付费。功能强大。详细说明参考这里</p>
<p><strong>VisualVM：</strong>JDK自带，功能强大，与JProfiler类似。推荐。</p>
<p><strong>如何调优</strong></p>
<p>观察内存释放情况、集合类检查、对象树</p>
<p>上面这些调优工具都提供了强大的功能，但是总的来说一般分为以下几类功能</p>
<p><strong>堆信息查看</strong></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1501570.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501570.png" alt="" width="411" height="323" border="0" /></a></p>
<p>&nbsp;</p>
<pre>可查看堆空间大小分配（年轻代、年老代、持久代分配）
提供即时的垃圾回收功能
垃圾监控（长时间监控回收情况）</pre>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1501571.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501571.png" alt="" width="498" height="244" border="0" /></a></p>
<pre>查看堆内类、对象信息查看：数量、类型等</pre>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1501572.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501572.png" alt="" width="428" height="343" border="0" /></a></p>
<pre>对象引用情况查看</pre>
<p>有了堆信息查看方面的功能，我们一般可以顺利解决以下问题：</p>
<p>&#8211;年老代年轻代大小划分是否合理</p>
<p>&#8211;内存泄漏</p>
<p>&#8211;垃圾回收算法设置是否合理</p>
<p><strong>线程监控</strong></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1501573.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501573.png" alt="" width="498" height="437" border="0" /></a></p>
<p>&nbsp;</p>
<pre>线程信息监控：系统线程数量
线程状态监控：各个线程都处在什么样的状态下</pre>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1501574.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501574.png" alt="" width="491" height="434" border="0" /></a></p>
<p>&nbsp;</p>
<pre>Dump线程详细信息：查看线程内部运行情况
死锁检查</pre>
<p><strong>热点分析</strong></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1501575.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501575.png" alt="" width="498" height="212" border="0" /></a></p>
<p><strong>CPU热点：</strong>检查系统哪些方法占用的大量CPU时间</p>
<p><strong>内存热点：</strong>检查哪些对象在系统中数量最大（一定时间内存活对象和销毁对象一起统计）</p>
<p>这两个东西对于系统优化很有帮助。我们可以根据找到的热点，有针对性的进行系统的瓶颈查找和进行系统优化，而不是漫无目的的进行所有代码的优化。</p>
<p><strong>快  照</strong></p>
<p>快照是系统运行到某一时刻的一个定格。在我们进行调优的时候，不可能用眼睛去跟踪所有系统变化，依赖快照功能，我们就可以进行系统两个不同运行时刻，对象（或类、线程等）的不同，以便快速找到问题</p>
<p>举例说，我要检查系统进行垃圾回收以后，是否还有该收回的对象被遗漏下来的了。那么，我可以在进行垃圾回收前后，分别进行一次堆情况的快照，然后对比两次快照的对象情况。</p>
<p><strong>内存泄漏检查</strong></p>
<p>内存泄漏是比较常见的问题，而且解决方法也比较通用，这里可以重点说一下，而线程、热点方面的问题则是具体问题具体分析了。</p>
<p>内存泄漏一般可以理解为系统资源（各方面的资源，堆、栈、线程等）在错误使用的情况下，导致使用完毕的资源无法回收（或没有回收），从而导致新的资源分配请求无法完成，引起系统错误。</p>
<p>内存泄漏对系统危害比较大，因为他可以直接导致系统的崩溃。</p>
<p>需要区别一下，内存泄漏和系统超负荷两者是有区别的，虽然可能导致的最终结果是一样的。内存泄漏是用完的资源没有回收引起错误，而系统超负荷则是系统确实没有那么多资源可以分配了（其他的资源都在使用）。</p>
<p><strong>年老代堆空间被占满</strong></p>
<p><strong>异常：</strong>java.lang.OutOfMemoryError: Java heap space</p>
<p><strong>说明：</strong></p>
<p><a href="http://images.51cto.com/files/uploadimg/20120110/1501576.png" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20120110/1501576.png" alt="" width="498" height="278" border="0" /></a></p>
<p>这是最典型的内存泄漏方式，简单说就是所有堆空间都被无法回收的垃圾对象占满，虚拟机无法再在分配新空间。</p>
<p>如上图所示，这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都是一次垃圾回收点，所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点，可以发现一条由底到高的线，这说明，随时间的推移，系统的堆空间被不断占满，最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄漏。（上面的图仅供示例，在实际情况下收集数据的时间需要更长，比如几个小时或者几天）</p>
<p><strong>解  决：</strong></p>
<p>这种方式解决起来也比较容易，一般就是根据垃圾回收前后情况对比，同时根据对象引用情况（常见的集合对象引用）分析，基本都可以找到泄漏点。</p>
<p><strong>持久代被占满</strong></p>
<p><strong>异常：</strong>java.lang.OutOfMemoryError: PermGen space</p>
<p><strong>说明：</strong></p>
<p>Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的，但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载，最终导致Perm区被占满。</p>
<p>更可怕的是，不同的classLoader即便使用了相同的类，但是都会对其进行加载，相当于同一个东西，如果有N个classLoader那么他将会被加载N次。因此，某些情况下，这个问题基本视为无解。当然，存在大量classLoader和大量反射类的情况其实也不多。</p>
<p><strong>解  决：</strong></p>
<p>1. -XX:MaxPermSize=16m</p>
<p>2. 换用JDK。比如JRocket。</p>
<p><strong>堆栈溢出</strong></p>
<p><strong>异常：</strong>java.lang.StackOverflowError</p>
<p><strong>说明：</strong>这个就不多说了，一般就是递归没返回，或者循环调用造成</p>
<p><strong>线程堆栈满</strong></p>
<p><strong>异常：</strong>Fatal: Stack size too small</p>
<p><strong>说明：</strong>java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后，将会出现上面异常。</p>
<p><strong>解决：</strong>增加线程栈大小。-Xss2m。但这个配置无法解决根本问题，还要看代码部分是否有造成泄漏的部分。</p>
<p><strong>系统内存被占满</strong></p>
<p><strong>异常：</strong>java.lang.OutOfMemoryError: unable to create new native thread</p>
<p><strong>说明：</strong></p>
<p>这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时，除了要在Java堆中分配内存外，操作系统本身也需要分配资源来创建线程。因此，当线程数量大到一定程度以后，堆中或许还有空间，但是操作系统分配不出资源来了，就出现这个异常了。</p>
<p>分配给Java虚拟机的内存愈多，系统剩余的资源就越少，因此，当系统内存固定时，分配给Java虚拟机的内存越多，那么，系统总共能够产生的线程也就越少，两者成反比的关系。同时，可以通过修改-Xss来减少分配给单个线程的空间，也可以增加系统总共内生产的线程数。</p>
<p><strong>解  决：</strong></p>
<p>1. 重新设计系统减少线程数量。</p>
<p>2. 线程数量不能减少的情况下，通过-Xss减小单个线程大小。以便能生产更多的线程。</p>
<h1>反思</h1>
<p><strong>垃圾回收的悖论</strong></p>
<p>所谓“成也萧何败萧何”。Java的垃圾回收确实带来了很多好处，为开发带来了便利。但是在一些高性能、高并发的情况下，垃圾回收确成为了制约Java应用的瓶颈。目前JDK的垃圾回收算法，始终无法解决垃圾回收时的暂停问题，因为这个暂停严重影响了程序的相应时间，造成拥塞或堆积。这也是后续JDK增加G1算法的一个重要原因。</p>
<p>当然，上面是从技术角度出发解决垃圾回收带来的问题，但是从系统设计方面我们就需要问一下了：</p>
<pre><strong>我们需要分配如此大的内存空间给应用吗？ 我们是否能够通过有效使用内存而不是通过扩大内存的方式来设计我们的系统呢？ </strong></pre>
<p><strong>我们的内存中都放了什么</strong></p>
<p>内存中需要放什么呢？个人认为，<strong>内存中需要放的是你的应用需要在不久的将来再次用到到的东西</strong>。想想看，如果你在将来不用这些东西，何必放内存呢？放文件、数据库不是更好？这些东西一般包括：</p>
<pre>1. 系统运行时业务相关的数据。比如web应用中的session、即时消息的session等。这些数据一般在一个用户访问周期或者一个使用过程中都需要存在。
2. 缓存。缓存就比较多了，你所要快速访问的都可以放这里面。其实上面的业务数据也可以理解为一种缓存。
3. 线程。</pre>
<p>因此，我们是不是可以这么认为，如果我们不把业务数据和缓存放在JVM中，或者把他们独立出来，那么Java应用使用时所需的内存将会大大减少，同时垃圾回收时间也会相应减少。</p>
<p>我认为这是可能的。</p>
<p><strong>解决之道</strong></p>
<p>数据库、文件系统</p>
<p>把所有数据都放入数据库或者文件系统，这是一种最为简单的方式。在这种方式下，Java应用的内存基本上等于处理一次峰值并发请求所需的内存。数据的获取都在每次请求时从数据库和文件系统中获取。也可以理解为，一次业务访问以后，所有对象都可以进行回收了。</p>
<p>这是一种内存使用最有效的方式，但是从应用角度来说，这种方式很低效。</p>
<p>内存-硬盘映射</p>
<p>上面的问题是因为我们使用了文件系统带来了低效。但是如果我们不是读写硬盘，而是写内存的话效率将会提高很多。</p>
<p>数据库和文件系统都是实实在在进行了持久化，但是当我们并不需要这样持久化的时候，我们可以做一些变通——把内存当硬盘使。</p>
<p>内存-硬盘映射很好很强大，既用了缓存又对Java应用的内存使用又没有影响。Java应用还是Java应用，他只知道读写的还是文件，但是实际上是内存。</p>
<p>这种方式兼得的Java应用与缓存两方面的好处。memcached的广泛使用也正是这一类的代表。</p>
<p>同一机器部署多个JVM</p>
<p>这也是一种很好的方式，可以分为纵拆和横拆。纵拆可以理解为把Java应用划分为不同模块，各个模块使用一个独立的Java进程。而横拆则是同样功能的应用部署多个JVM。</p>
<p>通过部署多个JVM，可以把每个JVM的内存控制一个垃圾回收可以忍受的范围内即可。但是这相当于进行了分布式的处理，其额外带来的复杂性也是需要评估的。另外，也有支持分布式的这种JVM可以考虑，不要要钱哦：）</p>
<p>程序控制的对象生命周期</p>
<p>这种方式是理想当中的方式，目前的虚拟机还没有，纯属假设。即：考虑由编程方式配置哪些对象在垃圾收集过程中可以直接跳过，减少垃圾回收线程遍历标记的时间。</p>
<p>这种方式相当于在编程的时候告诉虚拟机某些对象你可以在*时间后在进行收集或者由代码标识可以收集了（类似C、C++），在这之前你即便去遍历他也是没有效果的，他肯定是还在被引用的。</p>
<p>这种方式如果JVM可以实现，个人认为将是一个飞跃，Java即有了垃圾回收的优势，又有了C、C++对内存的可控性。</p>
<p>线程分配</p>
<p>Java的阻塞式的线程模型基本上可以抛弃了，目前成熟的NIO框架也比较多了。阻塞式IO带来的问题是线程数量的线性增长，而NIO则可以转换成为常数线程。因此，对于服务端的应用而言，NIO还是唯一选择。不过，JDK7中为我们带来的AIO是否能让人眼前一亮呢？我们拭目以待。</p>
<p>其他的JDK</p>
<p>本文说的都是Sun的JDK，目前常见的JDK还有JRocket和IBM的JDK。其中JRocket在IO方面比Sun的高很多，不过Sun JDK6.0以后提高也很大。而且JRocket在垃圾回收方面，也具有优势，其可设置垃圾回收的最大暂停时间也是很吸引人的。不过，系统Sun的G1实现以后，在这方面会有一个质的飞跃。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.wdcode.org/archives/545.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>如何成为“10倍效率”开发者</title>
		<link>http://www.wdcode.org/archives/542.html</link>
		<comments>http://www.wdcode.org/archives/542.html#comments</comments>
		<pubDate>Mon, 26 Dec 2011 02:50:37 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[编程之道]]></category>

		<guid isPermaLink="false">http://www.wdcode.org/?p=542</guid>
		<description><![CDATA[Brad Feld的一篇文章<a href="http://www.forbes.com/sites/venkateshrao/2011/12/05/the-rise-of-developeronomics/" target="_blank">The Rise of Developeronomics</a>中提到了“10倍效率的开发者（10x developer）”的概念（伟大的开发者的效率往往比一般的开发者高很多，而不只是一点点），Adam Loving在读了之后受到启发，并向多位大牛（Ben Sharpe、Collin Watson和Jonathan Locke）询问如何成为“10倍效率的开发者”，最后得到了以下的答案。

<strong>1. 只做需要做的工作</strong>
<ul>
	<li>使用敏捷方法；</li>
	<li>全心全意做UX设计；</li>
	<li>沟通第一；</li>
	<li>编码也许不是解决问题的办法；</li>
	<li>过早的优化是一切罪恶的根源；</li>
	<li>选择最简单的解决方案。</li>
</ul>
<strong>2. 站在巨人的肩膀上</strong>
<ul>
	<li>使用开源框架；</li>
	<li>使用简洁语言（如HAML、Jade、Coffeescript）；</li>
	<li>不要做重复的事情（不要重新发明轮子）；</li>
	<li>利用包管理器来进行公共和私有代码分配；</li>
	<li>不要任凭巨头（如微软）的摆布而修复库中的一个Bug；</li>
	<li>不要让你的雇主逼你学习；</li>
	<li>自主学习并为自己设定新的目标。</li>
</ul>
<strong>3. 了解数据结构和算法</strong>

如果你不知道什么时候应该使用快速排序、不懂辨认O(n2)程序、不会写递归函数，你将无法成为10倍效率的开发者。使用多种语言你才能清楚不同的框架是如何解决相同问题的。尽可能去了解底层命令（plumbing），以便能够作出明智的决定（Web框架是怎么存储session状态的？Cookie到底是什么？）。

<strong>4. 不要怕买工具，它可以节省你的时间</strong>

Ben说：“昨天我花50美元买了一个位图字体工具，它帮我节省的时间成本绝对超过200元。”

<strong>5. 集中注意力</strong>

不要整天开着你的电子邮件、Twitter、Facebook等，在工作时将它们最小化或关掉它们，戴上耳机。Tiny hack说：“即使不听音乐我也戴着耳机工作，这样便不会有人打扰到我。”

<strong>6. 尽早并且经常性地进行代码重构</strong>

有时，你不得不放弃漂亮的代码转而去寻找真正对项目有用的代码，但没关系，如果你的现有项目中有这样的代码，最好的方式便是不要看它，并重构。

<strong>7. 只管去做</strong>

将你的业余项目分享到<a href="http://startupweekend.org/" target="_blank">Startup Weekend</a>中。在我开始转到Unix和Ruby on Rails上之前，我买了一台Mac，使用Windows虚拟机花了一年时间做.NET项目。

<strong>8. 挑选一个编辑器，并掌握它</strong>

高效开发者喜欢用文本编辑器胜过IDE编辑器，因为这样可以学到更多东西。无论什么情况，尽量使用键盘快捷键，因为熟练使用一件工具的前提是熟悉它。

在选择编辑器时，认真考虑并挑选最好的（Emacs或Vim），因为它们是通用的。其次，挑选你的首选平台最支持的。使用宏，不断地写代码；使用Mac上的TextExpander为整个段落创建快捷方式；使用Visual Studio或SublimeText的自动补齐功能；使用支持按行/列分割窗口的编辑器，这样你便能同时看到单元测试和代码（或模型、视图）。

一定要想清楚后再写代码。Adam说，“我有朋友在一个大项目组里工作，他们组里最高效的程序员是一个高位截瘫用嘴叼着棍子敲代码的人，他总是在写代码之前想得很仔细且很少出错。”

<strong>9. 整洁的代码胜过巧妙的代码</strong>

要想让其他人能够读懂你的代码，尽量使用最少的代码来完成任务。遵循DRY（Don't repeat yourself）的原则，使用明确定义的对象和库，将任务分解成小而简单的代码段。

<strong>10. 潜意识是强大的工具</strong>

离开10分钟往往就可以解决一个问题。控制编程时间，给自己一个多姿多彩的生活，劳逸结合能让你在工作时更高效、更愉悦。当然，即便是上了年纪的程序员也知道，以最少的时间完成最高效的工作是成为10倍效率开发者的必要条件。

作为一个程序员，我觉得在职业生涯中最好的一件事儿就是从电脑前站起来，去拜访那些在某一领域有所建树的人们。

<strong>11. 推动自身和团队进步</strong>

重视批评，以包容的态度接受批评并提升自己是非常重要的事情。没有这个基础，你不可能成为一个高效的开发者。一位智者曾经说过：“聪明的人善于从自己的错误中学习，而智慧的人善于从别人的错误中学习。”]]></description>
			<content:encoded><![CDATA[<p>Brad Feld的一篇文章<a href="http://www.forbes.com/sites/venkateshrao/2011/12/05/the-rise-of-developeronomics/" target="_blank">The Rise of Developeronomics</a>中提到了“10倍效率的开发者（10x developer）”的概念（伟大的开发者的效率往往比一般的开发者高很多，而不只是一点点），Adam Loving在读了之后受到启发，并向多位大牛（Ben Sharpe、Collin Watson和Jonathan Locke）询问如何成为“10倍效率的开发者”，最后得到了以下的答案。</p>
<p><strong>1. 只做需要做的工作</strong></p>
<ul>
<li>使用敏捷方法；</li>
<li>全心全意做UX设计；</li>
<li>沟通第一；</li>
<li>编码也许不是解决问题的办法；</li>
<li>过早的优化是一切罪恶的根源；</li>
<li>选择最简单的解决方案。</li>
</ul>
<p><strong>2. 站在巨人的肩膀上</strong></p>
<ul>
<li>使用开源框架；</li>
<li>使用简洁语言（如HAML、Jade、Coffeescript）；</li>
<li>不要做重复的事情（不要重新发明轮子）；</li>
<li>利用包管理器来进行公共和私有代码分配；</li>
<li>不要任凭巨头（如微软）的摆布而修复库中的一个Bug；</li>
<li>不要让你的雇主逼你学习；</li>
<li>自主学习并为自己设定新的目标。</li>
</ul>
<p><strong>3. 了解数据结构和算法</strong></p>
<p>如果你不知道什么时候应该使用快速排序、不懂辨认O(n2)程序、不会写递归函数，你将无法成为10倍效率的开发者。使用多种语言你才能清楚不同的框架是如何解决相同问题的。尽可能去了解底层命令（plumbing），以便能够作出明智的决定（Web框架是怎么存储session状态的？Cookie到底是什么？）。</p>
<p><strong>4. 不要怕买工具，它可以节省你的时间</strong></p>
<p>Ben说：“昨天我花50美元买了一个位图字体工具，它帮我节省的时间成本绝对超过200元。”</p>
<p><strong>5. 集中注意力</strong></p>
<p>不要整天开着你的电子邮件、Twitter、Facebook等，在工作时将它们最小化或关掉它们，戴上耳机。Tiny hack说：“即使不听音乐我也戴着耳机工作，这样便不会有人打扰到我。”</p>
<p><strong>6. 尽早并且经常性地进行代码重构</strong></p>
<p>有时，你不得不放弃漂亮的代码转而去寻找真正对项目有用的代码，但没关系，如果你的现有项目中有这样的代码，最好的方式便是不要看它，并重构。</p>
<p><strong>7. 只管去做</strong></p>
<p>将你的业余项目分享到<a href="http://startupweekend.org/" target="_blank">Startup Weekend</a>中。在我开始转到Unix和Ruby on Rails上之前，我买了一台Mac，使用Windows虚拟机花了一年时间做.NET项目。</p>
<p><strong>8. 挑选一个编辑器，并掌握它</strong></p>
<p>高效开发者喜欢用文本编辑器胜过IDE编辑器，因为这样可以学到更多东西。无论什么情况，尽量使用键盘快捷键，因为熟练使用一件工具的前提是熟悉它。</p>
<p>在选择编辑器时，认真考虑并挑选最好的（Emacs或Vim），因为它们是通用的。其次，挑选你的首选平台最支持的。使用宏，不断地写代码；使用Mac上的TextExpander为整个段落创建快捷方式；使用Visual Studio或SublimeText的自动补齐功能；使用支持按行/列分割窗口的编辑器，这样你便能同时看到单元测试和代码（或模型、视图）。</p>
<p>一定要想清楚后再写代码。Adam说，“我有朋友在一个大项目组里工作，他们组里最高效的程序员是一个高位截瘫用嘴叼着棍子敲代码的人，他总是在写代码之前想得很仔细且很少出错。”</p>
<p><strong>9. 整洁的代码胜过巧妙的代码</strong></p>
<p>要想让其他人能够读懂你的代码，尽量使用最少的代码来完成任务。遵循DRY（Don&#8217;t repeat yourself）的原则，使用明确定义的对象和库，将任务分解成小而简单的代码段。</p>
<p><strong>10. 潜意识是强大的工具</strong></p>
<p>离开10分钟往往就可以解决一个问题。控制编程时间，给自己一个多姿多彩的生活，劳逸结合能让你在工作时更高效、更愉悦。当然，即便是上了年纪的程序员也知道，以最少的时间完成最高效的工作是成为10倍效率开发者的必要条件。</p>
<p>作为一个程序员，我觉得在职业生涯中最好的一件事儿就是从电脑前站起来，去拜访那些在某一领域有所建树的人们。</p>
<p><strong>11. 推动自身和团队进步</strong></p>
<p>重视批评，以包容的态度接受批评并提升自己是非常重要的事情。没有这个基础，你不可能成为一个高效的开发者。一位智者曾经说过：“聪明的人善于从自己的错误中学习，而智慧的人善于从别人的错误中学习。”</p>
]]></content:encoded>
			<wfw:commentRss>http://www.wdcode.org/archives/542.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>高性能网站建设的14个原则</title>
		<link>http://www.wdcode.org/archives/539.html</link>
		<comments>http://www.wdcode.org/archives/539.html#comments</comments>
		<pubDate>Wed, 21 Dec 2011 05:46:16 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[Web]]></category>

		<guid isPermaLink="false">http://www.wdcode.org/?p=539</guid>
		<description><![CDATA[今日大致浏览了一下《High Performance Web Sites》。本书的中文版是《高性能网站建设指南》。本书另有对其中个别问题深入探究的进阶篇《Even Faster Web Sites》，中译《高性能网站建设进阶指南》。这本书中给出了14条网站性能提升的原则，每个原则独立成章，配有示例。这些原则大多数都非常实用，适合站点架构师、前端工程师。其中对于前端工程师的意义更大一些。这次看的是原版。我对于Web开发较缺乏实践经验，加之看得匆忙，因此可能存在遗漏、表述不当之处，希望广大网友不吝指正。

51CTO推荐专题：<a href="http://developer.51cto.com/art/201104/257581.htm" target="_blank">高性能WEB开发应用指南</a>

<strong>原则1 减少HTTP请求数</strong>

构造请求、等待响应需要时间，因此请求数量越少越好。减少请求的总体思路就是合并资源，减少显示一个页面需要的文件数。

1. Image Map

通过设置&#60;img&#62;标签的usemap属性与使用&#60;map&#62;标签可以在一幅图片上切分出多个区域，指向不同的链接。比起使用多幅图片分别构造链接减少了请求数。

2. CSS Sprite（CSS贴图整合/贴图拼合/贴图定位）

通过设置元素的background-position样式做到。一般用于界面图标。典型的可以参考TinyMCE编辑器上方的那些小按钮。多个小图实质是从一个统一的大图通过不同的偏移量裁剪而来，这样加载界面上的众多按钮实际上只要请求一次（请求大图一次），从而减少HTTP请求数。

3. Inline Image（内联图片）

在&#60;img&#62;的src中不指定外部图片文件的URL，而是直接将图片信息放入。例如src=”data:image/gif;base64,R0lGODlhDAAMAL...”某些特殊情况下有用（例如一个不大的图片仅在当前页面用到）。

<strong>原则2 利用多线路CDN</strong>

为你的站点提供多种线路（例如国内电信、联通、移动）、多个地理位置（北方、南方、西部）的访问，使得所有用户都能够快速访问。

<strong>原则3 利用HTTP Cache</strong>

给不频繁更新的资源（例如静态图）加较长的Expires头信息，这些资源一经缓存，未来很长时间都可以不再重复传输了。

<strong>原则4 使用Gzip压缩</strong>

使用Gzip压缩HTTP报文，减小体积，减少传输时间。

<strong>原则5 将样式表置于页面前部</strong>

先加载样式表，这样页面渲染得以较早开始，给用户页面加载较快的感觉。

<strong>原则6 将脚本置于页面尾部</strong>

原因同5，先处理页面显示，页面渲染较早完成，而脚本逻辑稍后执行，这样给用户页面加载较快的感觉。

<strong>原则7 避免使用CSS表达式</strong>

过于复杂的JavaScript脚本逻辑、DOM查找、选择操作将会降低页面处理效率。

<strong>原则8 将JavaScript与CSS作为外联资源</strong>

这似乎与原则1中的合并思想相悖，但其实不然：考虑每个页面都引入了一个公共的JavaScript资源（例如jQuery或是ExtJS这样的JavaScript库），单就一个页面的表现来看，内联（即将JavaScript嵌入HTML）页面将比外联（使用&#60;script&#62;标签引入）页面加载更快（因为其较少的HTTP请求数）。但如果有很多页面都引入了这个公共JavaScript资源，那么内联方案会造成重复传输（因为这个资源内嵌在每个页面中了，所以每次打开一个页面都要将这部分资源传输一遍，从而造成网络传输资源的浪费）。而将这种资源独立出来外联引用可以解决这个问题。
由于JavaScript和CSS相对稳定，我们可以对其对应的资源设置较长的失效期（参考原则3）。

<strong>原则9 减少DNS查找</strong>

作者给出的建议是：

1. 使用Keep-Alive保持连接

如果连接断开，那么下次连接又要执行DNS查找，即使对应的域名-IP映射已被缓存，查找也是要消耗一些时间的

2. 减少域名

每次请求新域名都需要进行通过DNS查找不同的域名，且DNS缓存无法发挥作用。因此应该尽量将站点组织在一个统一域名下，避免使用过多子域名

<strong>原则10 压缩你的JavaScript</strong>

使用JS压缩工具压缩你的JavaScript吧，很有效哦。看看jQuery的两个不同的发行版本就知道区别了：

<a href="http://code.jquery.com/jquery-1.6.2.js">http://code.jquery.com/jquery-1.6.2.js</a> 阅读版jQuery代码，230KB

<a href="http://code.jquery.com/jquery-1.6.2.min.js">http://code.jquery.com/jquery-1.6.2.min.js</a> 压缩版jQuery代码（用于实际部署），89.4KB

<strong> 原则11 尽量避免重定向</strong>

一次重定向意味着在你真正访问到想要看到的页面前加入了一轮额外的HTTP请求（客户端发起HTTP请求→HTTP服务器返回重定向响应→客户端对新URL发起请求→HTTP服务器返回内容，下划线部分为额外的请求），因此消耗更多的时间（也就给人反应更慢的感觉）。因此除非必要，不要随意使用重定向。几个“必要”的情况：

1. 避免URL失效

旧站点迁移后，为了避免旧的URL失效，通常将对旧URL的请求重定向至新系统的对应地址。

2. URL美化

在可读性好的URL与实际资源URL之间转换，例如对于Google Toolbar，用户记得住http://toolbar.google.com这个对人类富有语义的地址，却很难记住http://www.google.com/tools/firefox/toolbar/FT3/intl/en/index.html这个真正的资源地址。因此有必要保留前者，并且将对前者的请求重定向至后者。

<strong>原则12 移除重复的脚本</strong>

不要在一个页面中重复引入相同的脚本。例如脚本B和C都依赖于A，那么在使用了B和C的页面中就有可能存在对A的重复引用。解决方法，对于简单的站点手动检查依赖性，消去重复引入；对于复杂的站点则需要构建自己的依赖管理/版本控制机制。

<strong>原则13 小心处理ETag</strong>

ETag是除Last-Modified之外的另一种HTTP Cache手段。通过hash的办法辨识资源是否被修改。但ETag存在一些问题，例如：

1. 不一致：不同Web服务器（Apache, IIS等）定义的ETag格式不同

2. ETag的计算是不稳定的（由于考虑过多因素），例如：

1) 相同资源在不同服务器上计算出来的ETag不一样，而大型Web应用通常由不止一台服务器提供服务，这就导致客户端在服务器A缓存好的资源明明仍然有效，而在下次请求B时由于ETag不同而被认定为失效，导致相同资源的重复传输。

2) 资源不变，而由于一些其他因素的变化，例如配置文件更改，导致ETag变化。直接后果是系统更新后客户端大规模发生Cache失效，导致传输量大增，站点性能下降。

作者给出的建议是：要么根据你的应用特点改进已有的ETag计算方法，要么干脆就不用ETag，而改用最简单的Last-Modified.

<strong>原则14 在Ajax中利用HTTP Cache</strong>

Ajax是异步请求，异步请求不会阻塞你现在的操作，而且当请求完成时，你马上就可以看到结果。但异步不代表能够瞬时完成，也不代表能够容忍它花无限多的时间完成。因此对于Ajax请求的性能也需要重视。有很多Ajax请求访问的是一些相对稳定的资源，因此别忘了对Ajax请求利用好HTTP Cache机制，具体参见原则3、13.]]></description>
			<content:encoded><![CDATA[<p>今日大致浏览了一下《High Performance Web Sites》。本书的中文版是《高性能网站建设指南》。本书另有对其中个别问题深入探究的进阶篇《Even Faster Web Sites》，中译《高性能网站建设进阶指南》。这本书中给出了14条网站性能提升的原则，每个原则独立成章，配有示例。这些原则大多数都非常实用，适合站点架构师、前端工程师。其中对于前端工程师的意义更大一些。这次看的是原版。我对于Web开发较缺乏实践经验，加之看得匆忙，因此可能存在遗漏、表述不当之处，希望广大网友不吝指正。</p>
<p>51CTO推荐专题：<a href="http://developer.51cto.com/art/201104/257581.htm" target="_blank">高性能WEB开发应用指南</a></p>
<p><strong>原则1 减少HTTP请求数</strong></p>
<p>构造请求、等待响应需要时间，因此请求数量越少越好。减少请求的总体思路就是合并资源，减少显示一个页面需要的文件数。</p>
<p>1. Image Map</p>
<p>通过设置&lt;img&gt;标签的usemap属性与使用&lt;map&gt;标签可以在一幅图片上切分出多个区域，指向不同的链接。比起使用多幅图片分别构造链接减少了请求数。</p>
<p>2. CSS Sprite（CSS贴图整合/贴图拼合/贴图定位）</p>
<p>通过设置元素的background-position样式做到。一般用于界面图标。典型的可以参考TinyMCE编辑器上方的那些小按钮。多个小图实质是从一个统一的大图通过不同的偏移量裁剪而来，这样加载界面上的众多按钮实际上只要请求一次（请求大图一次），从而减少HTTP请求数。</p>
<p>3. Inline Image（内联图片）</p>
<p>在&lt;img&gt;的src中不指定外部图片文件的URL，而是直接将图片信息放入。例如src=”data:image/gif;base64,R0lGODlhDAAMAL&#8230;”某些特殊情况下有用（例如一个不大的图片仅在当前页面用到）。</p>
<p><strong>原则2 利用多线路CDN</strong></p>
<p>为你的站点提供多种线路（例如国内电信、联通、移动）、多个地理位置（北方、南方、西部）的访问，使得所有用户都能够快速访问。</p>
<p><strong>原则3 利用HTTP Cache</strong></p>
<p>给不频繁更新的资源（例如静态图）加较长的Expires头信息，这些资源一经缓存，未来很长时间都可以不再重复传输了。</p>
<p><strong>原则4 使用Gzip压缩</strong></p>
<p>使用Gzip压缩HTTP报文，减小体积，减少传输时间。</p>
<p><strong>原则5 将样式表置于页面前部</strong></p>
<p>先加载样式表，这样页面渲染得以较早开始，给用户页面加载较快的感觉。</p>
<p><strong>原则6 将脚本置于页面尾部</strong></p>
<p>原因同5，先处理页面显示，页面渲染较早完成，而脚本逻辑稍后执行，这样给用户页面加载较快的感觉。</p>
<p><strong>原则7 避免使用CSS表达式</strong></p>
<p>过于复杂的JavaScript脚本逻辑、DOM查找、选择操作将会降低页面处理效率。</p>
<p><strong>原则8 将JavaScript与CSS作为外联资源</strong></p>
<p>这似乎与原则1中的合并思想相悖，但其实不然：考虑每个页面都引入了一个公共的JavaScript资源（例如jQuery或是ExtJS这样的JavaScript库），单就一个页面的表现来看，内联（即将JavaScript嵌入HTML）页面将比外联（使用&lt;script&gt;标签引入）页面加载更快（因为其较少的HTTP请求数）。但如果有很多页面都引入了这个公共JavaScript资源，那么内联方案会造成重复传输（因为这个资源内嵌在每个页面中了，所以每次打开一个页面都要将这部分资源传输一遍，从而造成网络传输资源的浪费）。而将这种资源独立出来外联引用可以解决这个问题。<br />
由于JavaScript和CSS相对稳定，我们可以对其对应的资源设置较长的失效期（参考原则3）。</p>
<p><strong>原则9 减少DNS查找</strong></p>
<p>作者给出的建议是：</p>
<p>1. 使用Keep-Alive保持连接</p>
<p>如果连接断开，那么下次连接又要执行DNS查找，即使对应的域名-IP映射已被缓存，查找也是要消耗一些时间的</p>
<p>2. 减少域名</p>
<p>每次请求新域名都需要进行通过DNS查找不同的域名，且DNS缓存无法发挥作用。因此应该尽量将站点组织在一个统一域名下，避免使用过多子域名</p>
<p><strong>原则10 压缩你的JavaScript</strong></p>
<p>使用JS压缩工具压缩你的JavaScript吧，很有效哦。看看jQuery的两个不同的发行版本就知道区别了：</p>
<p><a href="http://code.jquery.com/jquery-1.6.2.js">http://code.jquery.com/jquery-1.6.2.js</a> 阅读版jQuery代码，230KB</p>
<p><a href="http://code.jquery.com/jquery-1.6.2.min.js">http://code.jquery.com/jquery-1.6.2.min.js</a> 压缩版jQuery代码（用于实际部署），89.4KB</p>
<p><strong> 原则11 尽量避免重定向</strong></p>
<p>一次重定向意味着在你真正访问到想要看到的页面前加入了一轮额外的HTTP请求（客户端发起HTTP请求→HTTP服务器返回重定向响应→客户端对新URL发起请求→HTTP服务器返回内容，下划线部分为额外的请求），因此消耗更多的时间（也就给人反应更慢的感觉）。因此除非必要，不要随意使用重定向。几个“必要”的情况：</p>
<p>1. 避免URL失效</p>
<p>旧站点迁移后，为了避免旧的URL失效，通常将对旧URL的请求重定向至新系统的对应地址。</p>
<p>2. URL美化</p>
<p>在可读性好的URL与实际资源URL之间转换，例如对于Google Toolbar，用户记得住http://toolbar.google.com这个对人类富有语义的地址，却很难记住http://www.google.com/tools/firefox/toolbar/FT3/intl/en/index.html这个真正的资源地址。因此有必要保留前者，并且将对前者的请求重定向至后者。</p>
<p><strong>原则12 移除重复的脚本</strong></p>
<p>不要在一个页面中重复引入相同的脚本。例如脚本B和C都依赖于A，那么在使用了B和C的页面中就有可能存在对A的重复引用。解决方法，对于简单的站点手动检查依赖性，消去重复引入；对于复杂的站点则需要构建自己的依赖管理/版本控制机制。</p>
<p><strong>原则13 小心处理ETag</strong></p>
<p>ETag是除Last-Modified之外的另一种HTTP Cache手段。通过hash的办法辨识资源是否被修改。但ETag存在一些问题，例如：</p>
<p>1. 不一致：不同Web服务器（Apache, IIS等）定义的ETag格式不同</p>
<p>2. ETag的计算是不稳定的（由于考虑过多因素），例如：</p>
<p>1) 相同资源在不同服务器上计算出来的ETag不一样，而大型Web应用通常由不止一台服务器提供服务，这就导致客户端在服务器A缓存好的资源明明仍然有效，而在下次请求B时由于ETag不同而被认定为失效，导致相同资源的重复传输。</p>
<p>2) 资源不变，而由于一些其他因素的变化，例如配置文件更改，导致ETag变化。直接后果是系统更新后客户端大规模发生Cache失效，导致传输量大增，站点性能下降。</p>
<p>作者给出的建议是：要么根据你的应用特点改进已有的ETag计算方法，要么干脆就不用ETag，而改用最简单的Last-Modified.</p>
<p><strong>原则14 在Ajax中利用HTTP Cache</strong></p>
<p>Ajax是异步请求，异步请求不会阻塞你现在的操作，而且当请求完成时，你马上就可以看到结果。但异步不代表能够瞬时完成，也不代表能够容忍它花无限多的时间完成。因此对于Ajax请求的性能也需要重视。有很多Ajax请求访问的是一些相对稳定的资源，因此别忘了对Ajax请求利用好HTTP Cache机制，具体参见原则3、13.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.wdcode.org/archives/539.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>高性能WEB开发</title>
		<link>http://www.wdcode.org/archives/535.html</link>
		<comments>http://www.wdcode.org/archives/535.html#comments</comments>
		<pubDate>Wed, 21 Dec 2011 05:42:30 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[Web]]></category>
		<category><![CDATA[web]]></category>

		<guid isPermaLink="false">http://www.wdcode.org/?p=535</guid>
		<description><![CDATA[<strong>第一篇：HTTP服务器</strong>

因tomcat处理静态资源的速度比较慢，所以首先想到的就是把所有静态资源(JS,CSS,image,swf)

提到单独的服务器，用更加快速的HTTP服务器，这里选择了nginx了，nginx相比apache，更加轻量级，

配置更加简单，而且nginx不仅仅是高性能的HTTP服务器，还是高性能的反向代理服务器。

目前很多大型网站都使用了nginx,新浪、网易、QQ等都使用了nginx，说明nginx的稳定性和性能还是非常不错的。

<strong>1. nginx 安装(linux)</strong>

http://nginx.org/en/download.html 下载最新稳定版本

根据自己需要的功能先下载对应模板，这里下载了下面几个模块：

openssl-0.9.8l，zlib-1.2.3，pcre-8.00

编译安装nginx:
<ol>
	<li>./configure</li>
	<li>--without-http_rewrite_module</li>
	<li>--with-http_ssl_module</li>
	<li>--with-openssl=../../lib/openssl-0.9.8l</li>
	<li>--with-zlib=../../lib/zlib-1.2.3</li>
	<li>--with-pcre=../../lib/pcre-8.00</li>
	<li>--prefix=/usr/local/nginx</li>
	<li></li>
	<li>make</li>
	<li></li>
	<li>make install</li>
</ol>
<strong>2、nginx处理静态资源的配置</strong>
<ol>
	<li>#启动GZIP压缩CSS和JS</li>
	<li>     gzip  on;</li>
	<li>     # 压缩级别 1-9,默认是1，级别越高压缩率越大，当然压缩时间也就越长</li>
	<li>     gzip_comp_level 4;</li>
	<li>     # 压缩类型</li>
	<li>     gzip_types text/css application/x-javascript;</li>
	<li></li>
	<li>     # 定义静态资源访问的服务，对应的域名:res.abc.com</li>
	<li>     server {</li>
	<li>        listen       80;</li>
	<li>        server_name  res.abc.com;</li>
	<li></li>
	<li># 开启服务器读取文件的缓存，</li>
	<li>open_file_cache max=200 inactive=2h;</li>
	<li>open_file_cache_valid 3h;</li>
	<li>open_file_cache_errors off;</li>
	<li></li>
	<li>        charset utf-8;</li>
	<li></li>
	<li>     # 判断如果是图片或swf，客户端缓存5天</li>
	<li>location ~* ^.+.(ico&#124;gif&#124;bmp&#124;jpg&#124;jpeg&#124;png&#124;swf)$ {</li>
	<li>   root   /usr/local/resource/;</li>
	<li>   access_log off;</li>
	<li>   index  index.html index.htm;</li>
	<li>   expires 5d;</li>
	<li>        }</li>
	<li></li>
	<li># 因JS,CSS改动比较频繁，客户端缓存8小时</li>
	<li>location ~* ^.+.(js&#124;css)$ {</li>
	<li>   root   /usr/local/resource/;</li>
	<li>   access_log off;</li>
	<li>   index  index.html index.htm;</li>
	<li>   expires 8h;</li>
	<li>        }</li>
	<li></li>
	<li># 其他静态资源</li>
	<li>location / {</li>
	<li>   root   /usr/local/resource;</li>
	<li>   access_log off;</li>
	<li>   expires 8h;</li>
	<li>}</li>
	<li>    }</li>
</ol>
<strong>3、nginx 反向代理设置</strong>
<ol>
	<li> # 反向代理服务，绑定域名www.abc.com</li>
	<li>    server {</li>
	<li>listen       80;</li>
	<li>server_name  www.abc.com;</li>
	<li></li>
	<li>charset utf-8;</li>
	<li></li>
	<li># BBS使用Discuz!</li>
	<li># 因反向代理为了提高性能，一部分http头部信息不会转发给后台的服务器，</li>
	<li># 使用proxy_pass_header 和 proxy_set_header 把有需要的http头部信息转发给后台服务器</li>
	<li>location ^~ /bbs/ {</li>
	<li>   root   html;</li>
	<li>   access_log off;</li>
	<li>   index index.php;</li>
	<li>   # 转发host的信息，如果不设置host,在后台使用request.getServerName()取到的域名不是www.abc.com，而是127.0.0.1</li>
	<li>   proxy_set_header Host $host;</li>
	<li>   # 因Discuz! 为了安全，需要获取客户端User-Agent来判断每次POST数据是否跟第一次请求来自同1个浏览器，</li>
	<li>   # 如果不转发User-Agent,Discuz! 提交数据就会报"您的请求来路不正确，无法提交"的错误</li>
	<li>   proxy_pass_header User-Agent;</li>
	<li>   proxy_pass http://127.0.0.1:8081;</li>
	<li>}</li>
	<li></li>
	<li># 其他请求转发给tomcat</li>
	<li>location / {</li>
	<li>   root   html;</li>
	<li>   access_log off;</li>
	<li>   index index.jsp;</li>
	<li>   proxy_pass http://127.0.0.1:8080;</li>
	<li>}</li>
	<li></li>
	<li>error_page   500 502 503 504  /50x.html;</li>
	<li>        location = /50x.html {</li>
	<li>            root   html;</li>
	<li>        }</li>
	<li>    }</li>
</ol>
nginx详细配置参考：http://wiki.nginx.org/

PS：如果安装提示GCC not found，运行下面命令安装就可以(apt-get install build-essential)，仅限debian
<h1>Web性能测试工具推荐</h1>
<a href="http://getfirebug.com/" rel="”external" target="_blank">Firebug</a>:

Firebug 是firefox中最为经典的开发工具，可以监控请求头，响应头，显示资源加载瀑布图：

<a href="http://images.51cto.com/files/uploadimg/20110407/1423380.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423380.jpg" alt="web性能测试工具推荐" width="498" height="347" border="0" /></a>

<a href="http://www.httpwatch.com/" rel="”external" target="_blank">HttpWatch </a>:

httpwatch 功能类似firebug，可以监控请求头，响应头，显示资源加载瀑布图。但是httpwatch还能显示GZIP压缩信息，DNS查询，TCP链接信息，个人在监控http请求比较喜欢使用httpwatch，

httpwatch包含IE和firefox插件。不过httpwatch专业版本是收费的，免费版本有些功能限制。

<a href="http://images.51cto.com/files/uploadimg/20110407/1423381.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423381.jpg" alt="Web性能测试工具推荐" width="498" height="393" border="0" /></a>

<a href="http://ajax.dynatrace.com/ajax/en/" rel="”external" target="_blank">DynaTrace's Ajax Edition</a>：

dynaTrace 是本人常使用的1个免费工具，该工具不但可以检测资源加载瀑布图，而且还能监控页面呈现时间，CPU花销，JS分析和执行时间，CSS解析时间的等。

<a href="http://images.51cto.com/files/uploadimg/20110407/1423382.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423382.jpg" alt="Web性能测试工具推荐" width="498" height="308" border="0" /></a>

<a href="http://code.google.com/intl/zh-CN/webtoolkit/speedtracer/get-started.html" rel="”external" target="_blank">Speed Tracer</a>:

speed trace 是google chrome的1个插件，speed trace的优势点是用于监控JS的解析执行时间，还可以监控页面的重绘、回流，这个还是很强的(dynaTrace也能有这个功能)。

<a href="http://images.51cto.com/files/uploadimg/20110407/1423383.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423383.jpg" alt=" Web性能测试工具推荐" width="369" height="370" border="0" /></a>

注：安装这个插件，需要安装 Google Chrome Developer Channel 版本，但是这个链接的地址在国内好像打不开，如果打不开，请大家直接到这个地址去下载：

http://www.google.com/chrome/eula.html?extra=devchannel

<a href="http://code.google.com/intl/zh/speed/page-speed/" rel="”external" target="_blank">Page Speed</a> :

Page speed 是基于firebug的1个工具，主要可以对页面进行评分，总分100分，而且会显示对各项的改进意见，Page Speed也能检测到JS的解析时间。

<a href="http://images.51cto.com/files/uploadimg/20110407/1423384.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423384.jpg" alt="Web性能测试工具推荐" width="498" height="258" border="0" /></a>

<a href="http://developer.yahoo.com/yslow/" rel="”external" target="_blank">yslow </a>:

yslow跟pge speed一样是基于 firefox\firebug的插件，功能与page speed类似，对各种影响网站性能的因素进行评分，yslow是yahoo的工具，本人也一直在使用，推荐一下。

<a href="http://images.51cto.com/files/uploadimg/20110407/1423385.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423385.jpg" alt="Web性能测试工具推荐" width="498" height="335" border="0" /></a>

<a href="http://www.webpagetest.org/" rel="”external" target="_blank">webpagetest</a> :

webpagetest 是1个在线进行性能测试的网站，在该网站输入你的url，就会生成1个url加载的时间瀑布图，对所有加载的资源(css,js,image等等)列出优化的清单，也是非常好用的工具。
<a href="http://images.51cto.com/files/uploadimg/20110407/1423386.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423386.jpg" alt="Web性能测试工具推荐" width="378" height="335" border="0" /></a>
<h1>图片篇</h1>
<strong>一、缩小图片大小</strong>

当图片很多的时候，减少图片大小是提高下载速度最直接的方法。

1. 使用PNG8代替GIF(非动画图片)，因为PNG8在效果一样的情况，图片大小比GIF要小。

2. 用fireworks处理PNG图片，在我们产品中很多PNG图片是美工直接用photoshop导出的，

后来让美工用fireworks处理PNG(大概的方式是选择保存为PNG8,删除背景色)。

处理后100K的图片大小基本减少了3/4，但图片质量也会有少许降低，要看自己是否能接受。

3. 使用Smush.it(http://www.smushit.com/ysmush.it/)压缩图片，Smush.it是YUI团队做1个在线压缩图片的网站，

该网站在不影响原图片的质量下去掉图片中一些元数据，所以可以放心使用该网站进行压缩，

但这个压缩比例也是比较有限的。

<strong>二、合并图片和拆分图片</strong>

1. CSS Sprites合并图片以减少请求数来提高性能大家都知道。但不要把图片合并太多，太多太大了，

就会因为这1个图片影响这个页面的显示了。

2. 有时候我们需要把1个大图片拆分成多个小图片，比如产品首页图片比较少，就1个很大的banner图片，

因浏览器都可以并发下载图片，所以如果不拆分，只使用1个大图片的话，下载速度反而会比较慢

<strong>三、透明图片处理</strong>

IE6不能显示透明的PNG图片，是很多开发人员特别头疼的事，分别介绍下几种方式的优缺点。

1.使用AlphaImageLoader，IE6支持filter,使用下面的CSS代码，可以让IE6支持PNG
<ol>
	<li>#some-element {</li>
	<li>    background: url(image.png);</li>
	<li>    _background: none;</li>
	<li>    _filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='image.png', sizingMethod='crop');</li>
	<li>  }</li>
</ol>
优点：使用简单

缺点：性能损耗很大,AlphaImageLoader会花费很多资源去处理透明图片，使用AlphaImageLoader,IE使用内存会迅速上升。

而且AlphaImageLoader所有处理都在同1个线程中同步进行，所以当AlphaImageLoader多的时候，会阻塞UI的渲染。

使用_filter，IE7也可以识别，其实IE7是可以识别PNG透明图片的，如果在IE7下使用上面代码，IE7不会直接使用图片，而是使用AlphaImageLoader。

<strong>注：个人建议尽量避免使用AlphaImageLoader</strong>

2. JS处理

使用DD_belatedPNG(http://www.dillerdesign.com/experiment/DD_belatedPNG/),可以很简单的对界面上所有的透明图片进行同一处理。

优点：使用简单(比AlphaImageLoader还简单)

缺点：当页面上需要处理的图片比较多的时候，速度也比较慢，而且不能动态改变图片。

3. VML

IE6支持VML，VML可以使用透明图片，代码如下：

修改html代码头部
<ol>
	<li>html  xmlns ="http://www.w3.org/1999/xhtml"  xmlns:v &#62;</li>
	<li>           head &#62;</li>
	<li>        style  type ="text/css" &#62;</li>
	<li>            v\:* { behavior : url(#default#VML) ; }</li>
	<li>            span style="color: rgb(128, 0, 0);"&#62;style &#62;</li>
	<li>           span style="color: rgb(128, 0, 0);"&#62;head &#62;</li>
	<li>           body &#62;</li>
	<li>              v:image  src ="image.png"   /&#62;</li>
	<li>           span style="color: rgb(128, 0, 0);"&#62;body &#62;</li>
	<li>         span style="color: rgb(128, 0, 0);"&#62;html &#62;</li>
</ol>
优点：性能好，速度快

缺点：使用复杂，而且不支持firefox等浏览器，需要判断不同的浏览器输出不同的HTML代码。

<strong>四、多域名下载图片</strong>

因每个浏览器对同1个域名同时只能发送固定的请求，比如IE6好像是2个，所以可以对图片资源开通多个域名进行请求，

比如img1.abc.com,img2.abc.com。但域名不要开启太多，因为解析域名和打开新的连接都需要消耗时间，域名多了，说不定反而会更慢。一般2-4个域名就够了。

<strong>五、IE6下缓存背景图片</strong>

IE6背景图片缓存是个麻烦事，很多人知道使用下面的JS来让IE6缓存背景图片
<ol>
	<li>try{</li>
	<li>     document.execCommand("BackgroundImageCache", false, true);</li>
	<li>}catch(e){}</li>
</ol>
但是这样做的效果并不是非常好，当出现鼠标移动改变背景图片的时候，IE6老是会发送1个图片请求(尽管该背景图片已经下载)，

虽然返回结果是304，但还是要花费不少时间。在这种情况下，可以使用下面1个变通的方式来处理，

在页面上直接使用1个DIV元素来加载该图片，这样加载图片就能真正被缓存，鼠标移动也不会发送请求了。

<strong>六、预加载图片</strong>

使用下面代码可以在页面加载完毕后预加载下1个页面的图片，当进入下1个页面就不用再下载图片了。
<ol>
	<li>window.onload=function(){</li>
	<li>   var img = new Image();</li>
	<li>   img.src = "images/image.png";</li>
	<li>   img = null;</li>
	<li>};</li>
</ol>
<h1>疯狂的HTML压缩</h1>
上一篇随笔中网友 skyaspnet 问我如何压缩HTML，当时回答是推荐他使用gzip，后来想想，要是能把所有的html，jsp(aspx)在运行前都压缩成1行未免不是一件好事啊。一般我们启动gzip都比较少对html启动gzip，因为现在的html都是动态的，不会使用浏览器缓存，而启用gzip的话每次请求都需要压缩，会比较消耗服务器资源，对js,css启动gzip比较好是因为js,css都会使用缓存。我个人觉得的压缩html的最大好处就是一本万利，只要写好了一次，以后所有程序都可以使用，不会增加任何额外的开发工作。

在“<a href="http://developer.51cto.com/art/201104/257345.htm" target="_blank">JS、CSS的合并、压缩、缓存管理</a>”一文中说到自己写过的1个自动合并、压缩JS,CSS,并添加版本号的组件。这次把压缩html的功能也加入到该组件中，流程很简单，就是在程序启动(contextInitialized or Application_Start)的时候扫描所有html,jsp(aspx)进行压缩。

<strong>压缩的注意事项：</strong>

实现的方式主要是用正则表达式去查找，替换。在html压缩的时候，主要要注意下面几点：

1. pre,textarea 标签里面的内容格式需要保留，不能压缩。

2. 去掉html注释的时候，有些注释是不能去掉的，比如：&#60;!--[if IE 6]&#62; ..... &#60;![endif]--&#62;

3. 压缩嵌入式js中的注释要注意，因为可能注释符号会出现在字符串中，比如： var url = "http://www.cnblogs.com"; // 前面的//不是注释

去掉JS换行符的时候，不能直接跟一下行动内容，需要有空格，考虑下面的代码：
<ol>
	<li>else</li>
	<li>return;</li>
</ol>
如果不带空格，则变成elsereturn。

4. jsp(aspx) 中很有可能会使用嵌入一些服务器代码，这个时候也需要单独处理，里面注释的处理方法跟js的一样。

<strong>源代码：</strong>

下面是java实现的源代码，也可以 猛击此处 下载该代码，相信大家都看的懂，也很容易改成net代码：
<ol>
	<li>show source</li>
</ol>
<strong>使用注意事项：</strong>

使用了上面方法后，再运行程序，是不是发现每个页面查看源代码的时候都变成1行啦，还不错吧，但是在使用的时候还是要注意一些问题：

1. 嵌入js本来想调用yuicompressor来压缩，yuicompressor压缩JS前，会先编译js是否合法，因我们嵌入的js中可能很多会用到一些服务器端代码，比如 var now = ，这样的代码会编译不通过，所以无法使用yuicompressor。

最后只能自己写压缩JS代码，自己写的比较粗燥，所以有个问题还解决，就是如果开发人员在一句js代码后面没有加分号的话，压缩成1行就很有可能出问题。所以使用这个需要保证每条语句结束后都必须带分号。

2. 因为是在程序启动的时候压缩所有jsp(aspx)，所以如果是用户请求的时候动态产生的html就无法压缩。
<h1>网络传输环节</h1>
<strong>1.减少请求数.</strong>

◆ 缓存，使用Expires 等设置过期时间;如果内容没有过期就不发送请求

◆ 合并小体积内容，例如吧数量众多的小图片放在一个图片，之后用css部分呈现(大体积的内容就别合并了)

◆ 延迟加载;部分内容，例如图片在页面呈现的时候才加载 (常用的就是滚动条到了以后才加载);减少不必要的请求

◆ 合并重复内容和文件

◆ 考虑使用第三方CDN资源，例如jQuery有免费的CDN，有些用户已经在其他网站访问过该内容了，那么到我们的网站加载就更快了 (而且使用CDN降低对我们服务器的压力)

◆ 使用HTML 5 中的Local Storage等保存数据

<strong>2.减少响应内容的体积.</strong>

◆ 适当的时候只返回响应头304 (HTTP缓存，如ETag等)

◆ 使用Gzip等压缩文件内容

◆ 使用免费的第三方工具，压缩css，js和html等文件的大小 (例如我们常见的 jquery.min.js)

◆ 适当使用Ajax操作

◆ 在适当的时候，将样式,HTML和数据分离 (数据量很大的时候极大减小文件体积)
<ol>
	<li>&#60;ul id="id"&#62;</li>
	<li>    &#60;li style="一大堆的样式"&#62;数据 &#60;/li&#62;</li>
	<li>    &#60;li style="一大堆的样式"&#62;数据 &#60;/li&#62;</li>
	<li>    &#60;li style="一大堆的样式"&#62;数据 &#60;/li&#62;</li>
	<li>&#60;/ul&#62;</li>
	<li>分成HTML 样式 和数据3部分</li>
	<li></li>
	<li>HTML</li>
	<li>&#60;ul id="id"&#62;</li>
	<li>    &#60;li&#62;&#60;/li&#62;</li>
	<li>    &#60;!-- 如果使用数据模板等方式 只需要保留一个Li 否则需要多个 --&#62;</li>
	<li>&#60;/ul&#62;</li>
</ol>
样式保存在CSS文件中一些基本的小常识 虽然有很多个li 不用给每个li指定class

数据

◆ 使用JSON返回 (如果觉得麻烦也可以嵌入在页面中)

◆ 选择体积更小的数据格式，例如JSON一般就比XML体积来的小 (都经过压缩以后还是更小)

◆ 在设计上，只传送变化的部分数据 (例如要获取100条数据，可能已经加载了90条，那么再加载10条就好了)

◆ 移除请求和响应中不必要的HTTP Header (例如WCF Restful service中有的时候要传递表明当前数据是JSON还是XML的HTTP Header)

◆ 部分功能,如压缩会消耗CPU, 如ajax等会增加开发工作量,请谨慎选择

<strong>3.增加请求并发数.</strong>

◆ RFC中，浏览器对于同一个域名下的资源只能使用2个线程同时进行访问(很多新的浏览器支持6个或者更多);解决方法是使用子域名，例如1.abc.com 2.abc.com
<ol>
	<li>&#60;img src ="1.abc.com/1.png" /&#62;</li>
	<li>&#60;img src ="1.abc.com/2.png" /&#62;</li>
	<li>&#60;img src ="2.abc.com/3.png" /&#62;</li>
	<li>&#60;img src ="2.abc.com/4.png" /&#62;</li>
	<li>&#60;img src ="3.abc.com/5.png" /&#62;</li>
	<li>&#60;img src ="3.abc.com/6.png" /&#62;</li>
	<li>&#60;img src ="4.abc.com/7.png" /&#62;</li>
	<li>&#60;img src ="4.abc.com/8.png" /&#62;</li>
</ol>
◆ 将一个超大的文件(例如有些人喜欢吧整个网站的js都放在一个文件)拆成一系列的中小文件 (有利于并发加载和缓存!)这个文件大小的Size选择很重要 我个人建议是10k-200k (依赖于网络)

◆ 上一条并没有和1-2冲突，文件太小太多也不行，文件太少太大也不行，这是一个平衡的问题

◆ 通过分拆文件,使得最常用页面(例如首页)的加载速度变快了

◆ 控制加载顺序，例如先加载页面大体结构，然后多个javascript异步请求加载数据(把一个大的html变为多个小的html片段)

<strong>4.其他特殊技术.</strong>

◆ 利用HTTP 1.1的长连接特性，使得在一定程度上，服务器可以主动推送数据(减少了很多不必要的轮询)

<strong>5.工具.</strong>

◆ Fiddler (Free)

◆ FireDebug (Free)

◆ HttpWatch
<h1>如何加载JavaScript</h1>
<strong>外部JS的阻塞下载</strong>

所有浏览器在下载JS的时候，会阻止一切其他活动，比如其他资源的下载，内容的呈现等等。至到JS下载、解析、执行完毕后才开始继续并行下载其他资源并呈现内容。

<strong>有人会问</strong>：为什么JS不能像CSS、image一样并行下载了?这里需要简单介绍一下浏览器构造页面的原理，

当浏览器从服务器接收到了HTML文档，并把HTML在内存中转换成DOM树，在转换的过程中如果发现某个节点(node)上引用了CSS或者IMAGE，就会再发1个request去请求CSS或image,然后继续执行下面的转换，而不需要等待request的返回，当request返回后，只需要把返回的内容放入到DOM树中对应的位置就OK。但当引用了JS的时候，浏览器发送1个js request就会一直等待该request的返回。因为浏览器需要1个稳定的DOM树结构，而JS中很有可能有代码直接改变了DOM树结构，比如使用document.write 或 appendChild,甚至是直接使用的location.href进行跳转，浏览器为了防止出现JS修改DOM树，需要重新构建DOM树的情况，所以就会阻塞其他的下载和呈现.

阻塞下载图：下图是访问blogjava首页的时间瀑布图，可以看出来开始的2个image都是并行下载的，而后面的2个JS都是阻塞下载的(1个1个下载)。

<a href="http://images.51cto.com/files/uploadimg/20110419/1318570.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110419/1318570.jpg" alt="如何加载JS，JS应该放在什么位置？" width="1025" height="129" border="0" /></a>

<strong>嵌入JS的阻塞下载</strong>

嵌入JS是指直接写在HTML文档中的JS代码。上面说了引用外部的JS会阻塞其后的资源下载和其后的内容呈现，哪嵌入的JS又会是怎样阻塞的了，看下面的列2个代码：

代码1：
<ol>
	<li>&#60;&#60;/span&#62;div&#62;</li>
	<li>        ul&#62;</li>
	<li>            li&#62;blogjavaspan style="color: #800000;"&#62;li&#62;</li>
	<li>            li&#62;CSDNspan style="color: #800000;"&#62;li&#62;</li>
	<li>            li&#62;博客园span style="color: #800000;"&#62;li&#62;</li>
	<li>            li&#62;ABCspan style="color: #800000;"&#62;li&#62;</li>
	<li>            li&#62;AAAspan style="color: #800000;"&#62;li&#62;</li>
	<li>        ul&#62;</li>
	<li>    span style="color: #800000;"&#62;div&#62;</li>
	<li>    script type="text/javascript"&#62;</li>
	<li>        // 循环5秒钟</li>
	<li>        var n = Number(new Date());</li>
	<li>    var n2 = Number(new Date());</li>
	<li>   while((n2 - n)  (6*1000)){</li>
	<li>       n2 = Number(new Date());</li>
	<li>     }</li>
	<li>    span style="color: #800000;"&#62;script&#62;</li>
	<li>  div&#62;</li>
	<li>        ul&#62;</li>
	<li>            li&#62;MSNspan style="color: #800000;"&#62;li&#62;</li>
	<li>            li&#62;GOOGLEspan style="color: #800000;"&#62;li&#62;</li>
	<li>            li&#62;YAHOOspan style="color: #800000;"&#62;li&#62;</li>
	<li>        ul&#62;</li>
	<li>    span style="color: #800000;"&#62;div&#62;</li>
</ol>
代码2(test.zip里面的代码与代码1的JS代码一模一样)：
<ol>
	<li>div&#62;</li>
	<li>        ul&#62;</li>
	<li>            li&#62;blogjavaspan style="color: #800000;"&#62;li&#62;</li>
	<li>            li&#62;CSDNspan style="color: #800000;"&#62;li&#62;</li>
	<li>            li&#62;博客园span style="color: #800000;"&#62;li&#62;</li>
	<li>            li&#62;ABCspan style="color: #800000;"&#62;li&#62;</li>
	<li>            li&#62;AAAspan style="color: #800000;"&#62;li&#62;</li>
	<li>        ul&#62;</li>
	<li>    span style="color: #800000;"&#62;div&#62;</li>
	<li>    script type="text/javascript" src="http://www.blogjava.net/Files/BearRui/test.zip"&#62;span</li>
	<li>style="color: #800000;"&#62;script&#62;</li>
	<li>  div&#62;</li>
	<li>        ul&#62;</li>
	<li>            li&#62;MSNspan style="color: #800000;"&#62;li&#62;</li>
	<li>            li&#62;GOOGLEspan style="color: #800000;"&#62;li&#62;</li>
	<li>            li&#62;YAHOOspan style="color: #800000;"&#62;li&#62;</li>
	<li>        ul&#62;</li>
	<li>    span style="color: #800000;"&#62;div&#62;</li>
</ol>
运行后，会发现代码1中，在前5秒中页面上是一篇空白，5秒中后页面全部显示。 代码2中，前5秒中blogjava,csdn等先显示出来，5秒后MSN才显示出来。

可以看出嵌入JS会阻塞所有内容的呈现，而外部JS只会阻塞其后内容的显示，2种方式都会阻塞其后资源的下载。

<strong>嵌入JS导致CSS阻塞加载的问题</strong>

CSS怎么会阻塞加载了?CSS本来是可以并行下载的，在什么情况下会出现阻塞加载了(在测试观察中，IE6下CSS都是阻塞加载，下面的测试在非IE6下进行)：

代码1(为了效果，这里选择了1个国外服务器的CSS)：
<ol>
	<li>html xmlns="http://www.w3.org/1999/xhtml"&#62;</li>
	<li>            head&#62;</li>
	<li>                title&#62;js testspan style="color: #800000;"&#62;title&#62;</li>
	<li>                meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /&#62;</li>
	<li>              link type="text/css" rel="stylesheet" href="http://69.64.92.205/Css/Home3.css" /&#62;</li>
	<li>            span style="color: #800000;"&#62;head&#62;</li>
	<li>            body&#62;</li>
	<li>                img src="http://www.blogjava.net/images/logo.gif" /&#62;&#60;&#60;/span&#62;br /&#62;</li>
	<li>                img src="http://csdnimg.cn/www/images/csdnindex_piclogo.gif" /&#62;</li>
	<li>            span style="color: #800000;"&#62;body&#62;</li>
	<li>            span style="color: #800000;"&#62;html&#62;</li>
</ol>
时间瀑布图：

<a href="http://images.51cto.com/files/uploadimg/20110419/1318571.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110419/1318571.jpg" alt="如何加载JS，JS应该放在什么位置？" width="498" height="91" border="0" /></a>

代码2(只加了1个空的嵌入JS)：
<ol>
	<li>head&#62;</li>
	<li>    title&#62;js testspan style="color: #800000;"&#62;title&#62;</li>
	<li>    meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /&#62;</li>
	<li>  link type="text/css" rel="stylesheet" href="http://69.64.92.205/Css/Home3.css" /&#62;</li>
	<li>  script type="text/javascript"&#62;</li>
	<li>        function a(){}</li>
	<li>  span style="color: #800000;"&#62;script&#62;</li>
	<li>    span style="color: #800000;"&#62;head&#62;</li>
	<li>    body&#62;</li>
	<li>        img src="http://www.blogjava.net/images/logo.gif" /&#62;&#60;&#60;/span&#62;br /&#62;</li>
	<li>        img src="http://csdnimg.cn/www/images/csdnindex_piclogo.gif" /&#62;</li>
	<li>    span style="color: #800000;"&#62;body&#62;</li>
</ol>
时间瀑布图:

<a href="http://images.51cto.com/files/uploadimg/20110419/1318572.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110419/1318572.jpg" alt="如何加载JS，JS应该放在什么位置？" width="498" height="88" border="0" /></a>

从时间瀑布图中可以看出，代码2中，CSS和图片并没有并行下载，而是等待CSS下载完毕后才去并行下载后面的2个图片，当CSS后面跟着嵌入的JS的时候，该CSS就会出现阻塞后面资源下载的情况。

有人可能会问，这里为什么不说说嵌入的JS阻塞了后面的资源，而是说CSS阻塞了? 想想我们现在用的是1个空函数，解析这个空函数1ms就够，而后面2个图片是等CSS下载完1.3s后才开始下载。大家还可以试试把嵌入JS放到CSS前面，就不会出现阻塞的情况了。

<strong>根本原因：</strong>因为浏览器会维持html中css和js的顺序，样式表必须在嵌入的JS执行前先加载、解析完。而嵌入的JS会阻塞后面的资源加载，所以就会出现上面CSS阻塞下载的情况。

<strong>嵌入JS应该放在什么位置</strong>

1、放在底部，虽然放在底部照样会阻塞所有呈现，但不会阻塞资源下载。

2、如果嵌入JS放在head中，请把嵌入JS放在CSS头部。

3、使用defer

4、不要在嵌入的JS中调用运行时间较长的函数，如果一定要用，可以用setTimeout来调用

PS：很多网站喜欢在head中嵌入JS，并且习惯放在CSS后面，比如看到的<a href="http://www.qq.com/">www.qq.com</a>，当然也有很多网站是把JS放到CSS前面的，比如yahoo,google
<h1>如何减少请求数</h1>
我们先分析下请求头，看看每次请求都带了那些额外的数据.下面是监控的google的请求头

Host www.google.com.hk

User-Agent Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTBDFff GTB7.0

Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Accept-Language zh-cn,en-us;q=0.7,en;q=0.3

Accept-Encoding gzip,deflate

Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7

Keep-Alive 115

Proxy-Connection keep-alive

返回的response head

Date Sat, 17 Apr 2010 08:18:18 GMT

Expires -1

Cache-Control private, max-age=0

Content-Type text/html; charset=UTF-8

Set-Cookie PREF=ID=b94a24e8e90a0f50:NW=1:TM=1271492298:LM=1271492298:S=JH7CxsIx48Zoo8Nn; expires=Mon, 16-Apr-2012 08:18:18 GMT; path=/; domain=.google.com.hk NID=33=EJVyLQBv2CSgpXQTq8DLIT2JQ4aCAE9YKkU2x-h4hVw_ATrGx7njA69UUBMbzVHVnkAOe_jlGGzOoXhQACSFDP1i53C8hWjRTJd0vYtRNWhGYGv491mwbngkT6LCYbvg; expires=Sun, 17-Oct-2010 08:18:18 GMT; path=/; domain=.google.com.hk; HttpOnly

Content-Encoding gzip

Server gws

Content-Length 4344

这里发送的请求头的大小大概420 bytes，返回的请求头大概 600 bytes。

可见每次请求都会带上一些额外的信息进行传输(这次请求中还没有带cookie)，当请求的资源很小，比如1个不到1k的图标，可能request带的数据比实际图标的数据量还大。

所以当请求越多的时候，在网络上传输的数据自然就多，传输速度自然就慢了。

其实request自带的数据量还是小问题，毕竟request能带的数据量还是有限的。

<strong>http连接的开销</strong>

相比request头部多余的数据，http连接的开销则更加严重。先看看从用户输入1个URL到下载内容到客户端需要经过哪些阶段：

1. 域名解析

2. 开启TCP连接

3. 发送请求

4. 等待(主要包括网络延迟和服务器处理时间)

5. 下载资源

可能很多人认为每次请求大部分时间都花在下载资源上，让我们看看blogjava资源下载瀑布图(每种颜色代表的阶段与上面5个阶段对应)：

<a href="http://images.51cto.com/files/uploadimg/20110421/1014150.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1014150.jpg" alt="高性能WEB开发 - 为什么要减少请求数，如何减少请求数!" width="498" height="326" border="0" /></a>

看了上图你可能惊讶，花费在等待阶段的时间比实际下载的时间要多的多，上图告诉我们：

1. 每次请求花费的大部分时间在其他阶段，而不是在下载资源阶段

2. 再小的资源照样会花费很多时间在其他阶段，只是下载阶段会比较短(见上图的第6个资源，才284Byte)。

正对上面提到的2种情况，我们应该要怎么进行优化了?减少请求数来减少其他阶段的花销和网络中传输的数据。

<strong>如何减少请求数</strong>

<strong>1、合并文件</strong>

合并文件就是把很多JS文件合并成1个文件，很多CSS文件合并成1个文件，这种方法应该很多人用到过，这里不做详细介绍，

只推荐1个合并的工具：yuiCompressor 这个工具yahoo提供的。 http://developer.yahoo.com/yui/compressor/

<strong>2、合并图片</strong>

这是利用css sprite，通过控制背景图片的位置来显示不同的图片。这种技术也是大家都用过的，不做详细介绍，推荐1个在线合并图片的网站:http://csssprites.com/

<strong>3、把JS、CSS合并到1个文件</strong>

上面第1种方法说的只是把几个JS文件合并成1个JS文件，几个CSS文件合并成1个CSS文件，哪如何把CSS和JS都合并到1个文件中，见我的另1篇文章:

http://www.blogjava.net/BearRui/archive/2010/04/18/combin_css_js.html

<strong>4、使用Image maps</strong>

Image maps 是把多个图片合并成1个图片，然后使用html中的标签连接图片，并实现点击图片不同的区域执行不同的动作，image map在导航条中比较容易使用到。

image map的使用方法见： http://www.w3.org/TR/html401/struct/objects.html#h-13.6

<strong>5、data嵌入图片</strong>

这种方法把图片进行编码直接嵌入到html中进行使用，以减少HTTP请求，但这个会增加HTML页面的大小，而且这样嵌入的图片不能缓存。见下面这个图片：

上面的图片就是把图片进行base64编码后使用data:嵌入到html中，代码如下(后面的省略了，大家可以查看源代码看)：
<ol>
	<li>&#60;IMG SRC="data:image/gif;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/4QAWRXhpZgAASUkqAAgAA......"&#62;</li>
</ol>
其中google的视频搜索中，搜索出来的视频缩略图就都是使用嵌入的图片的，见下图：

<a href="http://images.51cto.com/files/uploadimg/20110421/1014151.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1014151.jpg" alt="高性能WEB开发 - 为什么要减少请求数，如何减少请求数!" width="701" height="291" border="0" /></a>

以上几种方法在都有利有弊，在不同情况下可以选择不同的使用方式，比如使用data嵌入图片虽然减少了请求数，但会增加页面大小。

所以微软的bing搜索在用户第一次访问的时候使用data嵌入图片，然后后台懒加载真真的图片，以后访问就直接使用缓存的图片，而不使用data。
<h1>减少请求、响应的数据量</h1>
上一篇中我们说到了 <a href="http://developer.51cto.com/art/201104/256656.htm" target="_blank">如何减少请求数</a>，这次说说如何减少请求、响应的数据量(即在网络中传输的数据量)，减少传输的数据量不仅仅可以加快页面加载速度，更可以节约服务器带宽，为你剩不少钱(好像很多机房托管都是按流量算钱的)。

<strong>GZIP压缩</strong>

gzip是目前所有浏览器都支持的一种压缩格式，IE6需要SP1及以上才支持(别说你还在用IE5,~_~)。gzip可以说是最方便而且也是最大减少响应数据量的1种方法。

说它方便，是因为你不需要为它写任何额外的代码，只需要在http服务器上加上配置都行了，现在主流的http服务器都支持gzip，各种服务器的配置这里就不一一介绍(其实是我不知道怎么配)，

nginx的配置可以参考我这篇文章：www.blogjava.net/BearRui/archive/2010/01/29/web_performance_server.html

我们先看看gzip的压缩比率能达到多少，这里用jquery 1.4.2的min和src2个版本进行测试，使用nginx服务器，gzip压缩级别使用的是4：

<a href="http://images.51cto.com/files/uploadimg/20110421/1126040.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1126040.jpg" alt="高性能WEB开发- 减少请求，响应的数据量" width="498" height="172" border="0" /></a>

注意看上图的红色部分，jquery src文件在启用gzip后大小减少了70%

<a href="http://images.51cto.com/files/uploadimg/20110421/1126041.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1126041.jpg" alt="高性能WEB开发- 减少请求，响应的数据量" width="498" height="175" border="0" /></a>

这张图片可以看出就算是已经压缩过min.js在启用gzip后大小也减少了65%。

<strong>别对图片启用gzip</strong>

在知道了gzip强大的压缩能力后，你是否想对服务器上的所有文件启用gzip了，先让我们看看图片中启用gzip后会是什么情况。

<a href="http://images.51cto.com/files/uploadimg/20110421/1126042.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1126042.jpg" alt="高性能WEB开发- 减少请求，响应的数据量" width="498" height="175" border="0" /></a>

hoho,1个gif图片经过gzip压缩后反而变大了???这是因为图片本来就是一种压缩格式，gzip不能再进行压缩，反而会添加1些额外的头部信息，所以图片会变大。

在测试过程中，发现jpg的图片经过gzip压缩后会变小,不知道为何，可能跟图片压缩方式有关。不过压缩比率也比较小，所以就算是jpg，建议也不要开启gzip压缩。

<strong>比较适合启用gzip压缩的文件有如下这些：</strong>

1. javascript

2. CSS

3. HTML，xml

4、plain text

<strong>别乱用cookie</strong>

现在几乎没有哪个网站不使用cookie了，可是该怎么使用cookie比较合适了，cookie有几个重要的属性:path(路径),domain(域),expires(过期时间)。浏览器就是根据这3个属性来判断在发送请求的时候是否需要带上这个cookie。

cookie使用最好的方式，就是当请求的资源需要cookie的时候才带上该cookie。其他任何请求都不带上cookie。但事实上很多人在使用cookie的时候已经习惯性的设置成：path=/ domain=.domain.com。这样的结果就是不管任何请求都会带上cookie，就算你是请求的图片(img.domain.com)、静态资源服务器(res.domain.com)这些根本不需要cookie的资源，浏览器照样会带上这些没用的cookie。咱们一起来看现实中的1个列子，博客园(www.cnblogs.com)：

先看看博客园的cookie是怎么设置的,下面是firefox查看博客园cookie的截图：

<a href="http://images.51cto.com/files/uploadimg/20110421/1126043.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1126043.jpg" alt="高性能WEB开发- 减少请求，响应的数据量" width="496" height="298" border="0" /></a>

cnblogs总共有5个cookie值，而且全部设置都是 path=/ domain=.cnblogs.com。知道了cookie的设置后，我们再来监控下博客园首页的请求，监控的统计信息如下：

总请求数：39(其中图片22个,JS7个,css2个)。

其中js、css、image 主要来自3个静态资源服务器: common.cnblogs.com , pic.cnblogs.com ,static.cnblogs.com

再看其中1个请求图片(http://static.cnblogs.com/images/a4/banner_job.gif)的请求头：

Host static.cnblogs.com

User-Agent Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTBDFff GTB7.0

Accept image/png,image/*;q=0.8,*/*;q=0.5

Accept-Language zh-cn,en-us;q=0.7,en;q=0.3

Accept-Encoding gzip,deflate

Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7

Keep-Alive 115

Proxy-Connection keep-alive

Referer http://www.cnblogs.com/

Cookie __gads=ID=a15d7cb5c3413e56:T=1272278620:S=ALNI_MZNMr6_d_PCjgkJNJeEQXkmZ3bxTQ; __utma=226521935.1697566422.1272278366.1272278366.1272278366.1; __utmb=226521935.2.10.1272278366; __utmc=226521935; __utmz=226521935.1272278367.1.1.utmcsr=(direct)&#124;utmccn=(direct)&#124;utmcmd=(none)

我们发现在请求banner_job.gif这个图片的时候，浏览器把cnblogs.com的所有cookie都带上了(其他图片的请求都是一样的)，我估计博客园在处理图片的时候应该不需要用到cookie吧?也许你认为这几个cookie的大小只有300个字节左右，无所谓啦。

我们做个简单的计算，假设博客园每天有50W个PV(实际情况应该不止吧)，每次PV大概有15次请求静态资源，15*500000*300/1024/1024=2145M。也就说这几个cookie每天大概会耗费博客园2G的带宽。当然这种简单的计算方式肯定会有偏差，毕竟我们还没把静态资源缓存考虑进去。但是个人觉得要是博客园要是把cookie的domain设置为www.cnblogs.com会更好一些。

<strong>妙用204状态</strong>

http中200,404,500状态大家都很清楚，但204状态大家可能用的比较少，204状态是指服务器成功处理了客户端请求，但服务器无返回内容。204是HTTP中数据量最少的响应状态，204的响应中没有body，而且Content-Length=0。很多人在使用ajax提交一些数据给服务器，而不需要服务器返回的时候，常常在服务端使用下面的代码：response.getWriter().print("")，这是返回1个空白的页面，是1个200请求。它还是有body，而且Content-Length不会等于0。其实这个时候你完全可以直接返回1个204状态(response.setStatus(204))。204在一些网站分析的代码中最常用到，只需要把客户端的一些信息提交给服务器就完事，让我们看看google首页的1个204响应,google首页的最后1个请求返回的就是204状态，但这个请求是干嘛用的就没猜出来了：
<a href="http://images.51cto.com/files/uploadimg/20110421/1126044.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1126044.jpg" alt="高性能WEB开发- 减少请求，响应的数据量" width="498" height="200" border="0" /></a>
<h1>JS、CSS的合并压缩</h1>
<strong>存在的问题：</strong>

合并、压缩文件主要有2方面的问题：

1. 每次发布的时候需要运行一下自己写的bat文件或者其他程序把文件按照自己的配置合并和压缩。

2. 因生产环境和开发环境需要加载的文件不一样，生产环境为了需要加载合并、压缩后的文件，而开发环境为了修改、调试方便，需要加载非合并、压缩的文件，所以我们常常需要在JSP中类似与下面的判断代码：
<ol>
	<li>&#60;c:if test="${env=='prod'}"&#62;</li>
	<li>   &#60;script type="text/javascript" src="/js/all.js"&#62;&#60;/script&#62;</li>
	<li>&#60;/c:if&#62;</li>
	<li>&#60;c:if test="${env=='dev'}"&#62;</li>
	<li>   &#60;script type="text/javascript" src="/js/1.js"&#62;&#60;/script&#62;</li>
	<li>   &#60;script type="text/javascript" src="/js/2.js"&#62;&#60;/script&#62;</li>
	<li>   &#60;script type="text/javascript" src="/js/3.js"&#62;&#60;/script&#62;</li>
	<li>&#60;/c:if&#62;</li>
</ol>
<strong>缓存问题：</strong>在现在JS满天飞的时代，大家都知道缓存能带来的巨大好处，但缓存确实非常麻烦的一个问题，相信很多人曾经历过下面的情况：为了让程序更快，在服务器上为JS加上缓冲5天的代码，但产品更新后第二天就接到电话说系统出错，详细了解后就发现是缓存引起的，让用户删除缓存后就会OK。原因很简单，就是你JS已经修改了，但用户还在使用缓存中的老JS。在经历几次这种情况，被领导数落了几次后。没办法只能把JS的缓冲去掉，或者改成8个小时。可这样就完全失去了缓存的优势了，哪我们到底需要解决哪些问题才能让我们使用缓冲顺心如意了?

1. 如何在修改了某个JS后，自动把所有引用该JS页面的代码中加上1个版本号?

2. 该如何生成版本号，根据什么来产生这个版本号。

可能有人为了解决上面的缓存问题，写了个JSP标签，通过标签读取JS、css文件的修改时间来作为版本号，从而来解决上面2个问题。但这种方法有下面几个缺点：

1. 每次请求都要通过标签读取读取文件的修改时间，速度慢。当然你可以把文件的修改时间放到缓存中，这样也会加到了内存使用量。

2. 在HTML静态页面中用不了

3. 如果你们公司是如下的部署发布方式(我们公司就是这样)，则会失效。每次发布，不是直接覆盖之前的WEB目录，运维的为的发布方便，要求每次发布直接给他们1个war包，他们会把之前WEB目录整个删除，然后上传现在的war包，这样就导致程序运行后，所有文件的最后修改时间都是解压war的时间。

<strong>分享自己项目中的处理方案：</strong>

为了解决上面讨论过的问题，在下写了1个如下的组件，组件中根据我们自己的实际情况使用了文件大小来做为文件的版本号，虽然在文件修改很小(比如把字符a改成b)，可能文件大小并没有变，导致版本号也不会变。

但这种机率还是非常低的。当然如果你觉的使用文件修改时间作为版本号适合你，只需要修改一行代码就行，下面看下这个组件的处理流程(本来想用流程图表达，最后还是觉的文字来的直白写)：

1. 程序启动(contextInitialized)

2. 搜索程序目录下的所有merge.txt文件，根据merge.txt文件的配置合并文件, merge.txt文件实例如下：

# 文件合并配置文件,多个文件以&#124;隔开，以/开头的表示从根目录开始,

# 空格之后的文件名表示合并之后的文件名

# 把1,2,3合并到all文件中

1.js&#124;2.js&#124;3.js all.js

#合并CSS

/css/mian.css&#124;/css/common.css all.css

3. 搜索程序目录下所有JS，CSS文件(包括合并后的)，每个文件都压缩后生成对应的1个新文件。

4. 搜索程序目录下所有JSP,html文件，把所有JS,css的引用代码改成压缩后并加了版本号的引用。

<strong>实例：</strong>

&#160;

实例的文件结构如下图：

<a href="http://images.51cto.com/files/uploadimg/20110422/1006360.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110422/1006360.jpg" alt="JS、CSS的合并、压缩、缓存管理" width="156" height="274" border="0" /></a>

看JSP原始代码(程序运行前)：
<ol>
	<li>&#60;%@page contentType="text/html" pageEncoding="UTF-8"%&#62;</li>
	<li>&#60;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"  "http://www.w3.org/TR/html4/loose.dtd"&#62;</li>
	<li>&#60;% boolean isDev = false;  // 是否开发环境%&#62;</li>
	<li>&#60;html&#62;</li>
	<li>    &#60;head&#62;</li>
	<li>        &#60;meta http-equiv="Content-Type" content="text/html; charset=UTF-8"&#62;</li>
	<li>        &#60;title&#62;JSP Page&#60;/title&#62;</li>
	<li>        &#60;% if(isDev){ %&#62;</li>
	<li>        &#60;script type="text/javascript" src="&#60;%=request.getContextPath() %&#62;/js/jquery-1.4.2.js"&#62;&#60;/script&#62;</li>
	<li>        &#60;script type="text/javascript" src="&#60;%=request.getContextPath() %&#62;/js/1.js"&#62;&#60;/script&#62;</li>
	<li>        &#60;script type="text/javascript" src="&#60;%=request.getContextPath() %&#62;/js/2.js"&#62;&#60;/script&#62;</li>
	<li>        &#60;link type="text/css" rel="stylesheet" href="&#60;%=request.getContextPath() %&#62;/css/1.css" /&#62;</li>
	<li>        &#60;link type="text/css" rel="stylesheet" href="&#60;%=request.getContextPath() %&#62;/css/2.css" /&#62;</li>
	<li>        &#60;% }else{ %&#62;</li>
	<li>        &#60;script type="text/javascript" src="&#60;%=request.getContextPath() %&#62;/js/jquery-1.4.2.js"&#62;&#60;/script&#62;</li>
	<li>        &#60;script type="text/javascript" src="&#60;%=request.getContextPath() %&#62;/js/all.js"&#62;&#60;/script&#62;</li>
	<li>        &#60;link type="text/css" rel="stylesheet"  href="&#60;%=request.getContextPath() %&#62;/css/all.css" /&#62;</li>
	<li>        &#60;% } %&#62;</li>
	<li>    &#60;/head&#62;</li>
	<li>    &#60;body&#62;</li>
	<li>        &#60;h1 class="c1"&#62;Hello World!&#60;/h1&#62;</li>
	<li>    &#60;/body&#62;</li>
	<li>&#60;/html&#62;</li>
</ol>
程序运行后JSP的代码：
<ol>
	<li>&#60;%@page contentType="text/html" pageEncoding="UTF-8"%&#62;</li>
	<li>&#60;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"&#62;</li>
	<li>&#60;%</li>
	<li>    boolean isDev = false;  // 是否开发环境</li>
	<li>%&#62;</li>
	<li>&#60;html&#62;</li>
	<li>    &#60;head&#62;</li>
	<li>        &#60;meta http-equiv="Content-Type" content="text/html; charset=UTF-8"&#62;</li>
	<li>        &#60;title&#62;JSP Page&#60;/title&#62;</li>
	<li>        &#60;% if(isDev){ %&#62;</li>
	<li>        &#60;script type="text/javascript" src="&#60;%=request.getContextPath() %&#62;/js/jquery-1.4.2-3gmin.js?99375"&#62;&#60;/script&#62;</li>
	<li>        &#60;script type="text/javascript" src="&#60;%=request.getContextPath() %&#62;/js/1-3gmin.js?90"&#62;&#60;/script&#62;</li>
	<li>        &#60;script type="text/javascript" src="&#60;%=request.getContextPath() %&#62;/js/2-3gmin.js?91"&#62;&#60;/script&#62;</li>
	<li>        &#60;link type="text/css" rel="stylesheet" href="&#60;%=request.getContextPath() %&#62;/css/1-3gmin.css?35" /&#62;</li>
	<li>        &#60;link type="text/css" rel="stylesheet" href="&#60;%=request.getContextPath() %&#62;/css/2-3gmin.css?18" /&#62;</li>
	<li>        &#60;% }else{ %&#62;</li>
	<li>        &#60;script type="text/javascript" src="&#60;%=request.getContextPath() %&#62;/js/jquery-1.4.2-3gmin.js?99375"&#62;&#60;/script&#62;</li>
	<li>        &#60;script type="text/javascript" src="&#60;%=request.getContextPath() %&#62;/js/all-3gmin.js?180"&#62;&#60;/script&#62;</li>
	<li>        &#60;link type="text/css" rel="stylesheet"  href="&#60;%=request.getContextPath() %&#62;/css/all-3gmin.css?53" /&#62;</li>
	<li>        &#60;% } %&#62;</li>
	<li>    &#60;/head&#62;</li>
	<li>    &#60;body&#62;</li>
	<li>        &#60;h1 class="c1"&#62;Hello World!&#60;/h1&#62;</li>
	<li>    &#60;/body&#62;</li>
	<li>&#60;/html&#62;</li>
</ol>
加3gmin后缀的文件全部是程序启动时自动生成的。
<h1>页面呈现、重绘、回流</h1>
在讨论页面重绘、回流之前。需要对页面的呈现流程有些了解，页面是怎么把html结合css等显示到浏览器上的，下面的流程图显示了浏览器对页面的呈现的处理流程。可能不同的浏览器略微会有些不同。但基本上都是类似的。

<a href="http://images.51cto.com/files/uploadimg/20110425/1025240.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110425/1025240.jpg" alt="高性能WEB开发- 页面呈现、重绘、回流" width="498" height="164" border="0" /></a>

1. 浏览器把获取到的html代码解析成1个Dom树，html中的每个tag都是Dom树中的1个节点，根节点就是我们常用的document对象( tag)。dom树就是我们用firebug或者IE Developer Toolbar等工具看到的html结构，里面包含了所有的html tag，包括display:none隐藏，还有用JS动态添加的元素等。

2. 浏览器把所有样式(主要包括css和浏览器的样式设置)解析成样式结构体，在解析的过程中会去掉浏览器不能识别的样式，比如IE会去掉-moz开头的样式，而firefox会去掉_开头的样式。

3、dom tree和样式结构体结合后构建呈现树(render tree),render tree有点类似于dom tree，但其实区别有很大，render tree能识别样式，render tree中每个node都有自己的style，而且render tree不包含隐藏的节点(比如display:none的节点，还有head节点)，因为这些节点不会用于呈现，而且不会影响呈现的，所以就不会包含到render tree中。注意 visibility:hidden隐藏的元素还是会包含到render tree中的，因为visibility:hidden 会影响布局(layout)，会占有空间。根据css2的标准，render tree中的每个节点都称为box(Box dimensions)，box所有属性：width,height,margin,padding,left,top,border等。

4. 一旦render tree构建完毕后，浏览器就可以根据render tree来绘制页面了。

<strong>回流与重绘</strong>

1. 当render tree中的一部分(或全部)因为元素的规模尺寸，布局，隐藏等改变而需要重新构建。这就称为回流(其实我觉得叫重新布局更简单明了些)。每个页面至少需要一次回流，就是在页面第一次加载的时候。

2. 当render tree中的一些元素需要更新属性，而这些属性只是影响元素的外观，风格，而不会影响布局的，比如background-color。则就叫称为重绘。

注：从上面可以看出，回流必将引起重绘，而重绘不一定会引起回流。

<strong>什么操作会引起重绘、回流</strong>

其实任何对render tree中元素的操作都会引起回流或者重绘，比如：

1. 添加、删除元素(回流+重绘)

2. 隐藏元素，display:none(回流+重绘)，visibility:hidden(只重绘，不回流)

3. 移动元素，比如改变top,left(jquery的animate方法就是,改变top,left不一定会影响回流)，或者移动元素到另外1个父元素中。(重绘+回流)

4. 对style的操作(对不同的属性操作，影响不一样)

5. 还有一种是用户的操作，比如改变浏览器大小，改变浏览器的字体大小等(回流+重绘)

让我们看看下面的代码是如何影响回流和重绘的:
<ol>
	<li>var s = document.body.style;</li>
	<li></li>
	<li>s.padding = "2px"; // 回流+重绘</li>
	<li>s.border = "1px solid red"; // 再一次 回流+重绘</li>
	<li></li>
	<li>s.color = "blue"; // 再一次重绘</li>
	<li>s.backgroundColor = "#ccc"; // 再一次 重绘</li>
	<li></li>
	<li>s.fontSize = "14px"; // 再一次 回流+重绘</li>
	<li></li>
	<li>// 添加node，再一次 回流+重绘</li>
	<li>document.body.appendChild(document.createTextNode('abc!'));</li>
</ol>
<em>请注意我上面用了多少个再一次。</em>

说到这里大家都知道回流比重绘的代价要更高，回流的花销跟render tree有多少节点需要重新构建有关系，假设你直接操作body，比如在body最前面插入1个元素，会导致整个render tree回流，这样代价当然会比较高，但如果是指body后面插入1个元素，则不会影响前面元素的回流。

<strong>聪明的浏览器</strong>

从上个实例代码中可以看到几行简单的JS代码就引起了6次左右的回流、重绘。而且我们也知道回流的花销也不小，如果每句JS操作都去回流重绘的话，浏览器可能就会受不了。所以很多浏览器都会优化这些操作，浏览器会维护1个队列，把所有会引起回流、重绘的操作放入这个队列，等队列中的操作到了一定的数量或者到了一定的时间间隔，浏览器就会把flush队列，进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。

虽然有了浏览器的优化，但有时候我们写的一些代码可能会强制浏览器提前flush队列，这样浏览器的优化可能就起不到作用了。当你请求向浏览器请求一些style信息的时候，就会让浏览器flush队列，比如：

1. offsetTop, offsetLeft, offsetWidth, offsetHeight

2. scrollTop/Left/Width/Height

3. clientTop/Left/Width/Height

4. width,height

5. 请求了getComputedStyle(), 或者 ie的 currentStyle

当你请求上面的一些属性的时候，浏览器为了给你最精确的值，需要flush队列，因为队列中可能会有影响到这些值的操作。

<strong>如何减少回流、重绘</strong>

减少回流、重绘其实就是需要减少对render tree的操作，并减少对一些style信息的请求，尽量利用好浏览器的优化策略。具体方法有：

1. 不要1个1个改变元素的样式属性，最好直接改变className，但className是预先定义好的样式，不是动态的，如果你要动态改变一些样式，则使用cssText来改变，见下面代码：
<ol>
	<li>// 不好的写法</li>
	<li>var left = 1;</li>
	<li>var top = 1;</li>
	<li>el.style.left = left + "px";</li>
	<li>el.style.top  = top  + "px";</li>
	<li></li>
	<li>// 比较好的写法</li>
	<li>el.className += " className1";</li>
	<li></li>
	<li>// 比较好的写法</li>
	<li>el.style.cssText += "; left: " + left + "px; top: " + top + "px;";</li>
</ol>
2. 让要操作的元素进行"离线处理"，处理完后一起更新，这里所谓的"离线处理"即让元素不存在于render tree中，比如：

a) 使用documentFragment或div等元素进行缓存操作，这个主要用于添加元素的时候，大家应该都用过，就是先把所有要添加到元素添加到1个div(这个div也是新加的)，

最后才把这个div append到body中。

b) 先display:none 隐藏元素，然后对该元素进行所有的操作，最后再显示该元素。因对display:none的元素进行操作不会引起回流、重绘。所以只要操作只会有2次回流。

3 不要经常访问会引起浏览器flush队列的属性，如果你确实要访问，就先读取到变量中进行缓存，以后用的时候直接读取变量就可以了，见下面代码：
<ol>
	<li>// 别这样写，大哥</li>
	<li>for(循环) {</li>
	<li>    elel.style.left = el.offsetLeft + 5 + "px";</li>
	<li>    elel.style.top  = el.offsetTop  + 5 + "px";</li>
	<li>}</li>
	<li></li>
	<li>// 这样写好点</li>
	<li>var left = el.offsetLeft,top  = el.offsetTop,s = el.style;</li>
	<li>for(循环) {</li>
	<li>    left += 10;</li>
	<li>    top  += 10;</li>
	<li>    s.left = left + "px";</li>
	<li>    s.top  = top  + "px";</li>
	<li>}</li>
</ol>
4. 考虑你的操作会影响到render tree中的多少节点以及影响的方式，影响越多，花费肯定就越多。比如现在很多人使用jquery的animate方法移动元素来展示一些动画效果，想想下面2种移动的方法：
<ol>
	<li>// block1是position:absolute 定位的元素，它移动会影响到它父元素下的所有子元素。</li>
	<li>// 因为在它移动过程中，所有子元素需要判断block1的z-index是否在自己的上面，</li>
	<li>// 如果是在自己的上面,则需要重绘,这里不会引起回流</li>
	<li>$("#block1").animate({left:50});</li>
	<li>// block2是相对定位的元素,这个影响的元素与block1一样，但是因为block2非绝对定位</li>
	<li>// 而且改变的是marginLeft属性，所以这里每次改变不但会影响重绘，</li>
	<li>// 还会引起父元素及其下元素的回流</li>
	<li>$("#block2").animate({marginLeft:50});</li>
</ol>
&#160;

<strong>实例测试</strong>

最后用2个工具对上面的理论进行一些测试，这2个工具是在我 "<a href="http://developer.51cto.com/art/201104/254038.htm" target="_blank">web 性能测试工具推荐</a>" 文章中推荐过的工具，分别是：dynaTrace(测试ie),Speed Tracer(测试Chrome)。

第一个测试代码不改变元素的规则，大小，位置。只改变颜色，所以不存在回流，仅测试重绘，代码如下：
<ol>
	<li>&#60;body&#62;</li>
	<li>    &#60;script type="text/javascript"&#62;</li>
	<li>        var s = document.body.style;</li>
	<li>        var computed;</li>
	<li>        if (document.body.currentStyle) {</li>
	<li>          computed = document.body.currentStyle;</li>
	<li>        } else {</li>
	<li>          computed = document.defaultView.getComputedStyle(document.body, '');</li>
	<li>        }</li>
	<li>    function testOneByOne(){</li>
	<li>      s.color = 'red';;</li>
	<li>      tmp = computed.backgroundColor;</li>
	<li>      s.color = 'white';</li>
	<li>      tmp = computed.backgroundImage;</li>
	<li>      s.color = 'green';</li>
	<li>      tmp = computed.backgroundAttachment;</li>
	<li>    }</li>
	<li></li>
	<li>    function testAll() {</li>
	<li>      s.color = 'yellow';</li>
	<li>      s.color = 'pink';</li>
	<li>      s.color = 'blue';</li>
	<li></li>
	<li>      tmp = computed.backgroundColor;</li>
	<li>      tmp = computed.backgroundImage;</li>
	<li>      tmp = computed.backgroundAttachment;</li>
	<li>    }</li>
	<li>    &#60;/script&#62;</li>
	<li>    color test &#60;br /&#62;</li>
	<li>    &#60;button onclick="testOneByOne()"&#62;Test One by One&#60;/button&#62;</li>
	<li>    &#60;button onclick="testAll()"&#62;Test All&#60;/button&#62;</li>
	<li>&#60;/body&#62;</li>
</ol>
testOneByOne 函数改变3次color,其中每次改变后调用getComputedStyle,读取属性值(按我们上面的讨论，这里会引起队列的flush)，testAll同样是改变3次color，但是每次改变后并不马上调用getComputedStyle。

我们先点击Test One by One按钮，然后点击 Test All,用dynaTrace监控如下：

<a href="http://images.51cto.com/files/uploadimg/20110425/1025241.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110425/1025241.jpg" alt="高性能WEB开发- 页面呈现、重绘、回流" width="498" height="377" border="0" /></a>

上图可以看到我们执行了2次button的click事件，每次click后都跟一次rendering(页面重绘)，2次click函数执行的时间都差不多,0.25ms,0.26ms，但其后的rendering时间就相差一倍多。(这里也可以看出，其实很多时候前端的性能瓶颈并不在于JS的执行，而是在于页面的呈现，这种情况在用JS做到富客户端中更为突出)。我们再看图的下面部分，这是第一次rendering的详细信息，可以看到里面有2行是 Scheduleing layout task，这个就是我们前面讨论过的浏览器优化过的队列，可以看出我们引发2次的flush。

<a href="http://images.51cto.com/files/uploadimg/20110425/1025242.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110425/1025242.jpg" alt="高性能WEB开发- 页面呈现、重绘、回流" width="498" height="297" border="0" /></a>

再看第二次rendering的详细信息，可以看出并没有Scheduleing layout task,所以这次rendering的时间也比较短。

测试代码2：这个测试跟第一次测试的代码很类似，但加上了对layout的改变，为的是测试回流。
<ol>
	<li>&#60;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"</li>
	<li>"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&#62;</li>
	<li>&#60;html xmlns="http://www.w3.org/1999/xhtml"&#62;</li>
	<li>&#60;head&#62;</li>
	<li>&#60;/head&#62;</li>
	<li>&#60;body&#62;</li>
	<li>    &#60;script type="text/javascript"&#62;</li>
	<li>        var s = document.body.style;</li>
	<li>        var computed;</li>
	<li>        if (document.body.currentStyle) {</li>
	<li>          computed = document.body.currentStyle;</li>
	<li>        } else {</li>
	<li>          computed = document.defaultView.getComputedStyle(document.body, '');</li>
	<li>        }</li>
	<li>    function testOneByOne(){</li>
	<li>      s.color = 'red';</li>
	<li>      s.padding = '1px';</li>
	<li>      tmp = computed.backgroundColor;</li>
	<li>      s.color = 'white';</li>
	<li>      s.padding = '2px';</li>
	<li>      tmp = computed.backgroundImage;</li>
	<li>      s.color = 'green';</li>
	<li>      s.padding = '3px';</li>
	<li>      tmp = computed.backgroundAttachment;</li>
	<li>    }</li>
	<li></li>
	<li>    function testAll() {</li>
	<li>      s.color = 'yellow';</li>
	<li>      s.padding = '4px';</li>
	<li>      s.color = 'pink';</li>
	<li>      s.padding = '5px';</li>
	<li>      s.color = 'blue';</li>
	<li>      s.padding = '6px';</li>
	<li></li>
	<li>      tmp = computed.backgroundColor;</li>
	<li>      tmp = computed.backgroundImage;</li>
	<li>      tmp = computed.backgroundAttachment;</li>
	<li>    }</li>
	<li>    &#60;/script&#62;</li>
	<li>    color test &#60;br /&#62;</li>
	<li>    &#60;button onclick="testOneByOne()"&#62;Test One by One&#60;/button&#62;</li>
	<li>    &#60;button onclick="testAll()"&#62;Test All&#60;/button&#62;</li>
	<li>&#60;/body&#62;</li>
</ol>
用dynaTrace监控如下：

<a href="http://images.51cto.com/files/uploadimg/20110425/1025243.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110425/1025243.jpg" alt="高性能WEB开发- 页面呈现、重绘、回流" width="498" height="343" border="0" /></a>

相信这图不用多说大家都能看懂了吧，可以看出有了回流后，rendering的时间相比之前的只重绘，时间翻了3倍了，可见回流的高成本性啊。

大家看到时候注意明细处相比之前的多了个 Calcalating flow layout。

最后再使用Speed Tracer测试一下，其实结果是一样的，只是让大家了解下2个测试工具：

测试1：

<a href="http://images.51cto.com/files/uploadimg/20110425/1025244.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110425/1025244.jpg" alt="高性能WEB开发- 页面呈现、重绘、回流" width="498" height="647" border="0" /></a>

图上第一次点击执行2ms(其中有50% 用于style Recalculation), 第二次1ms，而且第一次click后面也跟了2次style Recalculation,而第二次点击却没有style Recalculation。

但是这次测试发现paint重绘的时间竟然是一样的，都是3ms，这可能就是chrome比IE强的地方吧。

测试2：

<a href="http://images.51cto.com/files/uploadimg/20110425/1025245.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110425/1025245.jpg" alt="高性能WEB开发- 页面呈现、重绘、回流" width="498" height="582" border="0" /></a>

从图中竟然发现第二次的测试结果在时间上跟第一次的完全一样，这可能是因为操作太少，而chrome又比较强大，所以没能测试明显结果出来，

但注意图中多了1个紫色部分，就是layout的部分。也就是我们说的回流。
<h1>flush让页面分块逐步呈现</h1>
正对这种情况，还有一种处理方法，就是让response分块编码进行传输。response分块编码，可以先传输一部分不需要处理的html代码到客户端，等其他耗时代码执行完毕后再传输另外的html代码。

<strong>分块编码(chunked encoding)</strong>

chunked encoding 是http1.1 才支持编码格式(当然目前没有哪个浏览器不支持1.1了)，chunked encoding 与一般的响应区别如下：
<ol>
	<li>正常的响应：</li>
	<li>    HTTP/1.1 200 OK</li>
	<li>    Cache-Control: private, max-age=60</li>
	<li>    Content-Length: 75785</li>
	<li>    Content-Type: text/html; charset=utf-8</li>
	<li>    ..其他response headers</li>
	<li></li>
	<li>   &#60;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"</li>
</ol>
<ol>
	<li>chunked encoding 响应:</li>
	<li>   HTTP/1.1 200 OK</li>
	<li>   Cache-Control: private, max-age=60</li>
	<li>   Content-Length: 75785</li>
	<li>   Content-Type: text/html; charset=utf-8</li>
	<li>   Transfer-Encoding: chunked</li>
	<li>   ..其他response headers</li>
	<li></li>
	<li>   chunk #1(这里通常是16进制的数字，标志这个块的大小)</li>
	<li>   &#60;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"....</li>
	<li></li>
	<li>   chunk #2</li>
	<li>   &#60;div .....</li>
	<li></li>
	<li>   chunk #3</li>
	<li>   ....&#60;/body&#62;&#60;/html&#62;</li>
</ol>
<strong>实例(JSP)</strong>

一个简单的页面，分为头部(header)和内容(部分)，假设内容部分需要读取数据库，花费3秒时间,然后显示csdn的logo。header部分显示cnblogs的logo。代码如下：
<ol>
	<li>&#60;body&#62;</li>
	<li>    &#60;div id="head" style="border:1px solid #ccc;"&#62;</li>
	<li>       cnblogs logo &#60;img src="http://images.cnblogs.com/logo_small.gif" /&#62;</li>
	<li>    &#60;/div&#62;</li>
	<li>    &#60;br /&#62;</li>
	<li>    &#60;div id="content" style="border:1px solid blue;"&#62;</li>
	<li>    &#60;%</li>
	<li>        // 睡眠3秒</li>
	<li>        Thread.currentThread().sleep(3000);</li>
	<li>    %&#62;</li>
	<li>        csdn logo&#60;br /&#62;</li>
	<li>        &#60;img src="http://csdnimg.cn/www/images/csdnindex_piclogo.gif" /&#62;</li>
	<li>    &#60;/div&#62;</li>
	<li>&#60;/body&#62;</li>
</ol>
演示地址：http://213.186.44.204:8080/ChunkTest/nochunk.jsp (服务器比较差，请大家温柔点)

打开这个演示地址发现很正常的页面，在3秒后才开始下载显示2个logo，资源加载瀑布图如下：

<a href="http://images.51cto.com/files/uploadimg/20110428/0949340.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110428/0949340.jpg" alt="flush让页面分块,逐步呈现" width="793" height="90" border="0" /></a>

现在把代码改成如下，加上flush，让response把之前的html分块输出：
<ol>
	<li>&#60;div id="head" style="border:1px solid #ccc;"&#62;</li>
	<li>   cnblogs logo &#60;img src="http://images.cnblogs.com/logo_small.gif" /&#62;</li>
	<li>&#60;/div&#62;</li>
	<li>&#60;%</li>
	<li>    out.flush(); // flush response，分块输出</li>
	<li>%&#62;</li>
	<li>&#60;br /&#62;</li>
	<li>&#60;div id="content" style="border:1px solid blue;"&#62;</li>
	<li>    &#60;%</li>
	<li>        // 睡眠3秒</li>
	<li>        Thread.currentThread().sleep(3000);</li>
	<li>    %&#62;</li>
	<li>    csdn logo&#60;br /&#62;</li>
	<li>    &#60;img src="http://csdnimg.cn/www/images/csdnindex_piclogo.gif" /&#62;</li>
	<li>&#60;/div&#62;</li>
</ol>
演示地址：http://213.186.44.204:8080/ChunkTest/chunk.jsp

打开这个演示地址，是不是发现cnblogs logo先下载显示出来，3秒后csdn logo才显示，资源加载图如下：

<a href="http://images.51cto.com/files/uploadimg/20110428/0949341.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110428/0949341.jpg" alt="高性能WEB开发(11) - flush让页面分块,逐步呈现" width="786" height="91" border="0" /></a>

从这个图发现，cnblogs的logo在jsp页面还没执行完就开始下载了，这就是分块输出的效果。

监控工具：

如何知道我们是否成功使用了chunk encoding了 ，只要用工具查看response header 中是否包含了Transfer-Encoding: chunked，如果包含了，则是分块了。但要想监控分块的详细信息，据我所知，目前只有httpwatch支持，可以查看我们到底分了多少块，但是数量好像都多显示了1个，如下图：

<a href="http://images.51cto.com/files/uploadimg/20110428/0949342.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110428/0949342.jpg" alt="高性能WEB开发(11) - flush让页面分块,逐步呈现" width="605" height="179" border="0" /></a>

系列专题：<a href="http://developer.51cto.com/art/201104/257581.htm" target="_blank">高性能WEB开发应用指南</a>]]></description>
			<content:encoded><![CDATA[<p><strong>第一篇：HTTP服务器</strong></p>
<p>因tomcat处理静态资源的速度比较慢，所以首先想到的就是把所有静态资源(JS,CSS,image,swf)</p>
<p>提到单独的服务器，用更加快速的HTTP服务器，这里选择了nginx了，nginx相比apache，更加轻量级，</p>
<p>配置更加简单，而且nginx不仅仅是高性能的HTTP服务器，还是高性能的反向代理服务器。</p>
<p>目前很多大型网站都使用了nginx,新浪、网易、QQ等都使用了nginx，说明nginx的稳定性和性能还是非常不错的。</p>
<p><strong>1. nginx 安装(linux)</strong></p>
<p>http://nginx.org/en/download.html 下载最新稳定版本</p>
<p>根据自己需要的功能先下载对应模板，这里下载了下面几个模块：</p>
<p>openssl-0.9.8l，zlib-1.2.3，pcre-8.00</p>
<p>编译安装nginx:</p>
<ol>
<li>./configure</li>
<li>&#8211;without-http_rewrite_module</li>
<li>&#8211;with-http_ssl_module</li>
<li>&#8211;with-openssl=../../lib/openssl-0.9.8l</li>
<li>&#8211;with-zlib=../../lib/zlib-1.2.3</li>
<li>&#8211;with-pcre=../../lib/pcre-8.00</li>
<li>&#8211;prefix=/usr/local/nginx</li>
<li></li>
<li>make</li>
<li></li>
<li>make install</li>
</ol>
<p><strong>2、nginx处理静态资源的配置</strong></p>
<ol>
<li>#启动GZIP压缩CSS和JS</li>
<li>     gzip  on;</li>
<li>     # 压缩级别 1-9,默认是1，级别越高压缩率越大，当然压缩时间也就越长</li>
<li>     gzip_comp_level 4;</li>
<li>     # 压缩类型</li>
<li>     gzip_types text/css application/x-javascript;</li>
<li></li>
<li>     # 定义静态资源访问的服务，对应的域名:res.abc.com</li>
<li>     server {</li>
<li>        listen       80;</li>
<li>        server_name  res.abc.com;</li>
<li></li>
<li># 开启服务器读取文件的缓存，</li>
<li>open_file_cache max=200 inactive=2h;</li>
<li>open_file_cache_valid 3h;</li>
<li>open_file_cache_errors off;</li>
<li></li>
<li>        charset utf-8;</li>
<li></li>
<li>     # 判断如果是图片或swf，客户端缓存5天</li>
<li>location ~* ^.+.(ico|gif|bmp|jpg|jpeg|png|swf)$ {</li>
<li>   root   /usr/local/resource/;</li>
<li>   access_log off;</li>
<li>   index  index.html index.htm;</li>
<li>   expires 5d;</li>
<li>        }</li>
<li></li>
<li># 因JS,CSS改动比较频繁，客户端缓存8小时</li>
<li>location ~* ^.+.(js|css)$ {</li>
<li>   root   /usr/local/resource/;</li>
<li>   access_log off;</li>
<li>   index  index.html index.htm;</li>
<li>   expires 8h;</li>
<li>        }</li>
<li></li>
<li># 其他静态资源</li>
<li>location / {</li>
<li>   root   /usr/local/resource;</li>
<li>   access_log off;</li>
<li>   expires 8h;</li>
<li>}</li>
<li>    }</li>
</ol>
<p><strong>3、nginx 反向代理设置</strong></p>
<ol>
<li> # 反向代理服务，绑定域名www.abc.com</li>
<li>    server {</li>
<li>listen       80;</li>
<li>server_name  www.abc.com;</li>
<li></li>
<li>charset utf-8;</li>
<li></li>
<li># BBS使用Discuz!</li>
<li># 因反向代理为了提高性能，一部分http头部信息不会转发给后台的服务器，</li>
<li># 使用proxy_pass_header 和 proxy_set_header 把有需要的http头部信息转发给后台服务器</li>
<li>location ^~ /bbs/ {</li>
<li>   root   html;</li>
<li>   access_log off;</li>
<li>   index index.php;</li>
<li>   # 转发host的信息，如果不设置host,在后台使用request.getServerName()取到的域名不是www.abc.com，而是127.0.0.1</li>
<li>   proxy_set_header Host $host;</li>
<li>   # 因Discuz! 为了安全，需要获取客户端User-Agent来判断每次POST数据是否跟第一次请求来自同1个浏览器，</li>
<li>   # 如果不转发User-Agent,Discuz! 提交数据就会报”您的请求来路不正确，无法提交”的错误</li>
<li>   proxy_pass_header User-Agent;</li>
<li>   proxy_pass http://127.0.0.1:8081;</li>
<li>}</li>
<li></li>
<li># 其他请求转发给tomcat</li>
<li>location / {</li>
<li>   root   html;</li>
<li>   access_log off;</li>
<li>   index index.jsp;</li>
<li>   proxy_pass http://127.0.0.1:8080;</li>
<li>}</li>
<li></li>
<li>error_page   500 502 503 504  /50x.html;</li>
<li>        location = /50x.html {</li>
<li>            root   html;</li>
<li>        }</li>
<li>    }</li>
</ol>
<p>nginx详细配置参考：http://wiki.nginx.org/</p>
<p>PS：如果安装提示GCC not found，运行下面命令安装就可以(apt-get install build-essential)，仅限debian</p>
<h1>Web性能测试工具推荐</h1>
<p><a href="http://getfirebug.com/" rel="”external" target="_blank">Firebug</a>:</p>
<p>Firebug 是firefox中最为经典的开发工具，可以监控请求头，响应头，显示资源加载瀑布图：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110407/1423380.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423380.jpg" alt="web性能测试工具推荐" width="498" height="347" border="0" /></a></p>
<p><a href="http://www.httpwatch.com/" rel="”external" target="_blank">HttpWatch </a>:</p>
<p>httpwatch 功能类似firebug，可以监控请求头，响应头，显示资源加载瀑布图。但是httpwatch还能显示GZIP压缩信息，DNS查询，TCP链接信息，个人在监控http请求比较喜欢使用httpwatch，</p>
<p>httpwatch包含IE和firefox插件。不过httpwatch专业版本是收费的，免费版本有些功能限制。</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110407/1423381.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423381.jpg" alt="Web性能测试工具推荐" width="498" height="393" border="0" /></a></p>
<p><a href="http://ajax.dynatrace.com/ajax/en/" rel="”external" target="_blank">DynaTrace&#8217;s Ajax Edition</a>：</p>
<p>dynaTrace 是本人常使用的1个免费工具，该工具不但可以检测资源加载瀑布图，而且还能监控页面呈现时间，CPU花销，JS分析和执行时间，CSS解析时间的等。</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110407/1423382.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423382.jpg" alt="Web性能测试工具推荐" width="498" height="308" border="0" /></a></p>
<p><a href="http://code.google.com/intl/zh-CN/webtoolkit/speedtracer/get-started.html" rel="”external" target="_blank">Speed Tracer</a>:</p>
<p>speed trace 是google chrome的1个插件，speed trace的优势点是用于监控JS的解析执行时间，还可以监控页面的重绘、回流，这个还是很强的(dynaTrace也能有这个功能)。</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110407/1423383.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423383.jpg" alt=" Web性能测试工具推荐" width="369" height="370" border="0" /></a></p>
<p>注：安装这个插件，需要安装 Google Chrome Developer Channel 版本，但是这个链接的地址在国内好像打不开，如果打不开，请大家直接到这个地址去下载：</p>
<p>http://www.google.com/chrome/eula.html?extra=devchannel</p>
<p><a href="http://code.google.com/intl/zh/speed/page-speed/" rel="”external" target="_blank">Page Speed</a> :</p>
<p>Page speed 是基于firebug的1个工具，主要可以对页面进行评分，总分100分，而且会显示对各项的改进意见，Page Speed也能检测到JS的解析时间。</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110407/1423384.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423384.jpg" alt="Web性能测试工具推荐" width="498" height="258" border="0" /></a></p>
<p><a href="http://developer.yahoo.com/yslow/" rel="”external" target="_blank">yslow </a>:</p>
<p>yslow跟pge speed一样是基于 firefox\firebug的插件，功能与page speed类似，对各种影响网站性能的因素进行评分，yslow是yahoo的工具，本人也一直在使用，推荐一下。</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110407/1423385.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423385.jpg" alt="Web性能测试工具推荐" width="498" height="335" border="0" /></a></p>
<p><a href="http://www.webpagetest.org/" rel="”external" target="_blank">webpagetest</a> :</p>
<p>webpagetest 是1个在线进行性能测试的网站，在该网站输入你的url，就会生成1个url加载的时间瀑布图，对所有加载的资源(css,js,image等等)列出优化的清单，也是非常好用的工具。<br />
<a href="http://images.51cto.com/files/uploadimg/20110407/1423386.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110407/1423386.jpg" alt="Web性能测试工具推荐" width="378" height="335" border="0" /></a></p>
<h1>图片篇</h1>
<p><strong>一、缩小图片大小</strong></p>
<p>当图片很多的时候，减少图片大小是提高下载速度最直接的方法。</p>
<p>1. 使用PNG8代替GIF(非动画图片)，因为PNG8在效果一样的情况，图片大小比GIF要小。</p>
<p>2. 用fireworks处理PNG图片，在我们产品中很多PNG图片是美工直接用photoshop导出的，</p>
<p>后来让美工用fireworks处理PNG(大概的方式是选择保存为PNG8,删除背景色)。</p>
<p>处理后100K的图片大小基本减少了3/4，但图片质量也会有少许降低，要看自己是否能接受。</p>
<p>3. 使用Smush.it(http://www.smushit.com/ysmush.it/)压缩图片，Smush.it是YUI团队做1个在线压缩图片的网站，</p>
<p>该网站在不影响原图片的质量下去掉图片中一些元数据，所以可以放心使用该网站进行压缩，</p>
<p>但这个压缩比例也是比较有限的。</p>
<p><strong>二、合并图片和拆分图片</strong></p>
<p>1. CSS Sprites合并图片以减少请求数来提高性能大家都知道。但不要把图片合并太多，太多太大了，</p>
<p>就会因为这1个图片影响这个页面的显示了。</p>
<p>2. 有时候我们需要把1个大图片拆分成多个小图片，比如产品首页图片比较少，就1个很大的banner图片，</p>
<p>因浏览器都可以并发下载图片，所以如果不拆分，只使用1个大图片的话，下载速度反而会比较慢</p>
<p><strong>三、透明图片处理</strong></p>
<p>IE6不能显示透明的PNG图片，是很多开发人员特别头疼的事，分别介绍下几种方式的优缺点。</p>
<p>1.使用AlphaImageLoader，IE6支持filter,使用下面的CSS代码，可以让IE6支持PNG</p>
<ol>
<li>#some-element {</li>
<li>    background: url(image.png);</li>
<li>    _background: none;</li>
<li>    _filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src=&#8217;image.png&#8217;, sizingMethod=&#8217;crop&#8217;);</li>
<li>  }</li>
</ol>
<p>优点：使用简单</p>
<p>缺点：性能损耗很大,AlphaImageLoader会花费很多资源去处理透明图片，使用AlphaImageLoader,IE使用内存会迅速上升。</p>
<p>而且AlphaImageLoader所有处理都在同1个线程中同步进行，所以当AlphaImageLoader多的时候，会阻塞UI的渲染。</p>
<p>使用_filter，IE7也可以识别，其实IE7是可以识别PNG透明图片的，如果在IE7下使用上面代码，IE7不会直接使用图片，而是使用AlphaImageLoader。</p>
<p><strong>注：个人建议尽量避免使用AlphaImageLoader</strong></p>
<p>2. JS处理</p>
<p>使用DD_belatedPNG(http://www.dillerdesign.com/experiment/DD_belatedPNG/),可以很简单的对界面上所有的透明图片进行同一处理。</p>
<p>优点：使用简单(比AlphaImageLoader还简单)</p>
<p>缺点：当页面上需要处理的图片比较多的时候，速度也比较慢，而且不能动态改变图片。</p>
<p>3. VML</p>
<p>IE6支持VML，VML可以使用透明图片，代码如下：</p>
<p>修改html代码头部</p>
<ol>
<li>html  xmlns =”http://www.w3.org/1999/xhtml”  xmlns:v &gt;</li>
<li>           head &gt;</li>
<li>        style  type =”text/css” &gt;</li>
<li>            v\:* { behavior : url(#default#VML) ; }</li>
<li>            span style=”color: rgb(128, 0, 0);”&gt;style &gt;</li>
<li>           span style=”color: rgb(128, 0, 0);”&gt;head &gt;</li>
<li>           body &gt;</li>
<li>              v:image  src =”image.png”   /&gt;</li>
<li>           span style=”color: rgb(128, 0, 0);”&gt;body &gt;</li>
<li>         span style=”color: rgb(128, 0, 0);”&gt;html &gt;</li>
</ol>
<p>优点：性能好，速度快</p>
<p>缺点：使用复杂，而且不支持firefox等浏览器，需要判断不同的浏览器输出不同的HTML代码。</p>
<p><strong>四、多域名下载图片</strong></p>
<p>因每个浏览器对同1个域名同时只能发送固定的请求，比如IE6好像是2个，所以可以对图片资源开通多个域名进行请求，</p>
<p>比如img1.abc.com,img2.abc.com。但域名不要开启太多，因为解析域名和打开新的连接都需要消耗时间，域名多了，说不定反而会更慢。一般2-4个域名就够了。</p>
<p><strong>五、IE6下缓存背景图片</strong></p>
<p>IE6背景图片缓存是个麻烦事，很多人知道使用下面的JS来让IE6缓存背景图片</p>
<ol>
<li>try{</li>
<li>     document.execCommand(“BackgroundImageCache”, false, true);</li>
<li>}catch(e){}</li>
</ol>
<p>但是这样做的效果并不是非常好，当出现鼠标移动改变背景图片的时候，IE6老是会发送1个图片请求(尽管该背景图片已经下载)，</p>
<p>虽然返回结果是304，但还是要花费不少时间。在这种情况下，可以使用下面1个变通的方式来处理，</p>
<p>在页面上直接使用1个DIV元素来加载该图片，这样加载图片就能真正被缓存，鼠标移动也不会发送请求了。</p>
<p><strong>六、预加载图片</strong></p>
<p>使用下面代码可以在页面加载完毕后预加载下1个页面的图片，当进入下1个页面就不用再下载图片了。</p>
<ol>
<li>window.onload=function(){</li>
<li>   var img = new Image();</li>
<li>   img.src = ”images/image.png”;</li>
<li>   img = null;</li>
<li>};</li>
</ol>
<h1>疯狂的HTML压缩</h1>
<p>上一篇随笔中网友 skyaspnet 问我如何压缩HTML，当时回答是推荐他使用gzip，后来想想，要是能把所有的html，jsp(aspx)在运行前都压缩成1行未免不是一件好事啊。一般我们启动gzip都比较少对html启动gzip，因为现在的html都是动态的，不会使用浏览器缓存，而启用gzip的话每次请求都需要压缩，会比较消耗服务器资源，对js,css启动gzip比较好是因为js,css都会使用缓存。我个人觉得的压缩html的最大好处就是一本万利，只要写好了一次，以后所有程序都可以使用，不会增加任何额外的开发工作。</p>
<p>在“<a href="http://developer.51cto.com/art/201104/257345.htm" target="_blank">JS、CSS的合并、压缩、缓存管理</a>”一文中说到自己写过的1个自动合并、压缩JS,CSS,并添加版本号的组件。这次把压缩html的功能也加入到该组件中，流程很简单，就是在程序启动(contextInitialized or Application_Start)的时候扫描所有html,jsp(aspx)进行压缩。</p>
<p><strong>压缩的注意事项：</strong></p>
<p>实现的方式主要是用正则表达式去查找，替换。在html压缩的时候，主要要注意下面几点：</p>
<p>1. pre,textarea 标签里面的内容格式需要保留，不能压缩。</p>
<p>2. 去掉html注释的时候，有些注释是不能去掉的，比如：&lt;!&#8211;[if IE 6]&gt; &#8230;.. &lt;![endif]&#8211;&gt;</p>
<p>3. 压缩嵌入式js中的注释要注意，因为可能注释符号会出现在字符串中，比如： var url = “http://www.cnblogs.com”; // 前面的//不是注释</p>
<p>去掉JS换行符的时候，不能直接跟一下行动内容，需要有空格，考虑下面的代码：</p>
<ol>
<li>else</li>
<li>return;</li>
</ol>
<p>如果不带空格，则变成elsereturn。</p>
<p>4. jsp(aspx) 中很有可能会使用嵌入一些服务器代码，这个时候也需要单独处理，里面注释的处理方法跟js的一样。</p>
<p><strong>源代码：</strong></p>
<p>下面是java实现的源代码，也可以 猛击此处 下载该代码，相信大家都看的懂，也很容易改成net代码：</p>
<ol>
<li>show source</li>
</ol>
<p><strong>使用注意事项：</strong></p>
<p>使用了上面方法后，再运行程序，是不是发现每个页面查看源代码的时候都变成1行啦，还不错吧，但是在使用的时候还是要注意一些问题：</p>
<p>1. 嵌入js本来想调用yuicompressor来压缩，yuicompressor压缩JS前，会先编译js是否合法，因我们嵌入的js中可能很多会用到一些服务器端代码，比如 var now = ，这样的代码会编译不通过，所以无法使用yuicompressor。</p>
<p>最后只能自己写压缩JS代码，自己写的比较粗燥，所以有个问题还解决，就是如果开发人员在一句js代码后面没有加分号的话，压缩成1行就很有可能出问题。所以使用这个需要保证每条语句结束后都必须带分号。</p>
<p>2. 因为是在程序启动的时候压缩所有jsp(aspx)，所以如果是用户请求的时候动态产生的html就无法压缩。</p>
<h1>网络传输环节</h1>
<p><strong>1.减少请求数.</strong></p>
<p>◆ 缓存，使用Expires 等设置过期时间;如果内容没有过期就不发送请求</p>
<p>◆ 合并小体积内容，例如吧数量众多的小图片放在一个图片，之后用css部分呈现(大体积的内容就别合并了)</p>
<p>◆ 延迟加载;部分内容，例如图片在页面呈现的时候才加载 (常用的就是滚动条到了以后才加载);减少不必要的请求</p>
<p>◆ 合并重复内容和文件</p>
<p>◆ 考虑使用第三方CDN资源，例如jQuery有免费的CDN，有些用户已经在其他网站访问过该内容了，那么到我们的网站加载就更快了 (而且使用CDN降低对我们服务器的压力)</p>
<p>◆ 使用HTML 5 中的Local Storage等保存数据</p>
<p><strong>2.减少响应内容的体积.</strong></p>
<p>◆ 适当的时候只返回响应头304 (HTTP缓存，如ETag等)</p>
<p>◆ 使用Gzip等压缩文件内容</p>
<p>◆ 使用免费的第三方工具，压缩css，js和html等文件的大小 (例如我们常见的 jquery.min.js)</p>
<p>◆ 适当使用Ajax操作</p>
<p>◆ 在适当的时候，将样式,HTML和数据分离 (数据量很大的时候极大减小文件体积)</p>
<ol>
<li>&lt;ul id=”id”&gt;</li>
<li>    &lt;li style=”一大堆的样式”&gt;数据 &lt;/li&gt;</li>
<li>    &lt;li style=”一大堆的样式”&gt;数据 &lt;/li&gt;</li>
<li>    &lt;li style=”一大堆的样式”&gt;数据 &lt;/li&gt;</li>
<li>&lt;/ul&gt;</li>
<li>分成HTML 样式 和数据3部分</li>
<li></li>
<li>HTML</li>
<li>&lt;ul id=”id”&gt;</li>
<li>    &lt;li&gt;&lt;/li&gt;</li>
<li>    &lt;!&#8211; 如果使用数据模板等方式 只需要保留一个Li 否则需要多个 &#8211;&gt;</li>
<li>&lt;/ul&gt;</li>
</ol>
<p>样式保存在CSS文件中一些基本的小常识 虽然有很多个li 不用给每个li指定class</p>
<p>数据</p>
<p>◆ 使用JSON返回 (如果觉得麻烦也可以嵌入在页面中)</p>
<p>◆ 选择体积更小的数据格式，例如JSON一般就比XML体积来的小 (都经过压缩以后还是更小)</p>
<p>◆ 在设计上，只传送变化的部分数据 (例如要获取100条数据，可能已经加载了90条，那么再加载10条就好了)</p>
<p>◆ 移除请求和响应中不必要的HTTP Header (例如WCF Restful service中有的时候要传递表明当前数据是JSON还是XML的HTTP Header)</p>
<p>◆ 部分功能,如压缩会消耗CPU, 如ajax等会增加开发工作量,请谨慎选择</p>
<p><strong>3.增加请求并发数.</strong></p>
<p>◆ RFC中，浏览器对于同一个域名下的资源只能使用2个线程同时进行访问(很多新的浏览器支持6个或者更多);解决方法是使用子域名，例如1.abc.com 2.abc.com</p>
<ol>
<li>&lt;img src =”1.abc.com/1.png” /&gt;</li>
<li>&lt;img src =”1.abc.com/2.png” /&gt;</li>
<li>&lt;img src =”2.abc.com/3.png” /&gt;</li>
<li>&lt;img src =”2.abc.com/4.png” /&gt;</li>
<li>&lt;img src =”3.abc.com/5.png” /&gt;</li>
<li>&lt;img src =”3.abc.com/6.png” /&gt;</li>
<li>&lt;img src =”4.abc.com/7.png” /&gt;</li>
<li>&lt;img src =”4.abc.com/8.png” /&gt;</li>
</ol>
<p>◆ 将一个超大的文件(例如有些人喜欢吧整个网站的js都放在一个文件)拆成一系列的中小文件 (有利于并发加载和缓存!)这个文件大小的Size选择很重要 我个人建议是10k-200k (依赖于网络)</p>
<p>◆ 上一条并没有和1-2冲突，文件太小太多也不行，文件太少太大也不行，这是一个平衡的问题</p>
<p>◆ 通过分拆文件,使得最常用页面(例如首页)的加载速度变快了</p>
<p>◆ 控制加载顺序，例如先加载页面大体结构，然后多个javascript异步请求加载数据(把一个大的html变为多个小的html片段)</p>
<p><strong>4.其他特殊技术.</strong></p>
<p>◆ 利用HTTP 1.1的长连接特性，使得在一定程度上，服务器可以主动推送数据(减少了很多不必要的轮询)</p>
<p><strong>5.工具.</strong></p>
<p>◆ Fiddler (Free)</p>
<p>◆ FireDebug (Free)</p>
<p>◆ HttpWatch</p>
<h1>如何加载JavaScript</h1>
<p><strong>外部JS的阻塞下载</strong></p>
<p>所有浏览器在下载JS的时候，会阻止一切其他活动，比如其他资源的下载，内容的呈现等等。至到JS下载、解析、执行完毕后才开始继续并行下载其他资源并呈现内容。</p>
<p><strong>有人会问</strong>：为什么JS不能像CSS、image一样并行下载了?这里需要简单介绍一下浏览器构造页面的原理，</p>
<p>当浏览器从服务器接收到了HTML文档，并把HTML在内存中转换成DOM树，在转换的过程中如果发现某个节点(node)上引用了CSS或者IMAGE，就会再发1个request去请求CSS或image,然后继续执行下面的转换，而不需要等待request的返回，当request返回后，只需要把返回的内容放入到DOM树中对应的位置就OK。但当引用了JS的时候，浏览器发送1个js request就会一直等待该request的返回。因为浏览器需要1个稳定的DOM树结构，而JS中很有可能有代码直接改变了DOM树结构，比如使用document.write 或 appendChild,甚至是直接使用的location.href进行跳转，浏览器为了防止出现JS修改DOM树，需要重新构建DOM树的情况，所以就会阻塞其他的下载和呈现.</p>
<p>阻塞下载图：下图是访问blogjava首页的时间瀑布图，可以看出来开始的2个image都是并行下载的，而后面的2个JS都是阻塞下载的(1个1个下载)。</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110419/1318570.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110419/1318570.jpg" alt="如何加载JS，JS应该放在什么位置？" width="1025" height="129" border="0" /></a></p>
<p><strong>嵌入JS的阻塞下载</strong></p>
<p>嵌入JS是指直接写在HTML文档中的JS代码。上面说了引用外部的JS会阻塞其后的资源下载和其后的内容呈现，哪嵌入的JS又会是怎样阻塞的了，看下面的列2个代码：</p>
<p>代码1：</p>
<ol>
<li>&lt;&lt;/span&gt;div&gt;</li>
<li>        ul&gt;</li>
<li>            li&gt;blogjavaspan style=”color: #800000;”&gt;li&gt;</li>
<li>            li&gt;CSDNspan style=”color: #800000;”&gt;li&gt;</li>
<li>            li&gt;博客园span style=”color: #800000;”&gt;li&gt;</li>
<li>            li&gt;ABCspan style=”color: #800000;”&gt;li&gt;</li>
<li>            li&gt;AAAspan style=”color: #800000;”&gt;li&gt;</li>
<li>        ul&gt;</li>
<li>    span style=”color: #800000;”&gt;div&gt;</li>
<li>    script type=”text/javascript”&gt;</li>
<li>        // 循环5秒钟</li>
<li>        var n = Number(new Date());</li>
<li>    var n2 = Number(new Date());</li>
<li>   while((n2 - n)  (6*1000)){</li>
<li>       n2 = Number(new Date());</li>
<li>     }</li>
<li>    span style=”color: #800000;”&gt;script&gt;</li>
<li>  div&gt;</li>
<li>        ul&gt;</li>
<li>            li&gt;MSNspan style=”color: #800000;”&gt;li&gt;</li>
<li>            li&gt;GOOGLEspan style=”color: #800000;”&gt;li&gt;</li>
<li>            li&gt;YAHOOspan style=”color: #800000;”&gt;li&gt;</li>
<li>        ul&gt;</li>
<li>    span style=”color: #800000;”&gt;div&gt;</li>
</ol>
<p>代码2(test.zip里面的代码与代码1的JS代码一模一样)：</p>
<ol>
<li>div&gt;</li>
<li>        ul&gt;</li>
<li>            li&gt;blogjavaspan style=”color: #800000;”&gt;li&gt;</li>
<li>            li&gt;CSDNspan style=”color: #800000;”&gt;li&gt;</li>
<li>            li&gt;博客园span style=”color: #800000;”&gt;li&gt;</li>
<li>            li&gt;ABCspan style=”color: #800000;”&gt;li&gt;</li>
<li>            li&gt;AAAspan style=”color: #800000;”&gt;li&gt;</li>
<li>        ul&gt;</li>
<li>    span style=”color: #800000;”&gt;div&gt;</li>
<li>    script type=”text/javascript” src=”http://www.blogjava.net/Files/BearRui/test.zip”&gt;span</li>
<li>style=”color: #800000;”&gt;script&gt;</li>
<li>  div&gt;</li>
<li>        ul&gt;</li>
<li>            li&gt;MSNspan style=”color: #800000;”&gt;li&gt;</li>
<li>            li&gt;GOOGLEspan style=”color: #800000;”&gt;li&gt;</li>
<li>            li&gt;YAHOOspan style=”color: #800000;”&gt;li&gt;</li>
<li>        ul&gt;</li>
<li>    span style=”color: #800000;”&gt;div&gt;</li>
</ol>
<p>运行后，会发现代码1中，在前5秒中页面上是一篇空白，5秒中后页面全部显示。 代码2中，前5秒中blogjava,csdn等先显示出来，5秒后MSN才显示出来。</p>
<p>可以看出嵌入JS会阻塞所有内容的呈现，而外部JS只会阻塞其后内容的显示，2种方式都会阻塞其后资源的下载。</p>
<p><strong>嵌入JS导致CSS阻塞加载的问题</strong></p>
<p>CSS怎么会阻塞加载了?CSS本来是可以并行下载的，在什么情况下会出现阻塞加载了(在测试观察中，IE6下CSS都是阻塞加载，下面的测试在非IE6下进行)：</p>
<p>代码1(为了效果，这里选择了1个国外服务器的CSS)：</p>
<ol>
<li>html xmlns=”http://www.w3.org/1999/xhtml”&gt;</li>
<li>            head&gt;</li>
<li>                title&gt;js testspan style=”color: #800000;”&gt;title&gt;</li>
<li>                meta http-equiv=”Content-Type” content=”text/html; charset=UTF-8&#8243; /&gt;</li>
<li>              link type=”text/css” rel=”stylesheet” href=”http://69.64.92.205/Css/Home3.css” /&gt;</li>
<li>            span style=”color: #800000;”&gt;head&gt;</li>
<li>            body&gt;</li>
<li>                img src=”http://www.blogjava.net/images/logo.gif” /&gt;&lt;&lt;/span&gt;br /&gt;</li>
<li>                img src=”http://csdnimg.cn/www/images/csdnindex_piclogo.gif” /&gt;</li>
<li>            span style=”color: #800000;”&gt;body&gt;</li>
<li>            span style=”color: #800000;”&gt;html&gt;</li>
</ol>
<p>时间瀑布图：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110419/1318571.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110419/1318571.jpg" alt="如何加载JS，JS应该放在什么位置？" width="498" height="91" border="0" /></a></p>
<p>代码2(只加了1个空的嵌入JS)：</p>
<ol>
<li>head&gt;</li>
<li>    title&gt;js testspan style=”color: #800000;”&gt;title&gt;</li>
<li>    meta http-equiv=”Content-Type” content=”text/html; charset=UTF-8&#8243; /&gt;</li>
<li>  link type=”text/css” rel=”stylesheet” href=”http://69.64.92.205/Css/Home3.css” /&gt;</li>
<li>  script type=”text/javascript”&gt;</li>
<li>        function a(){}</li>
<li>  span style=”color: #800000;”&gt;script&gt;</li>
<li>    span style=”color: #800000;”&gt;head&gt;</li>
<li>    body&gt;</li>
<li>        img src=”http://www.blogjava.net/images/logo.gif” /&gt;&lt;&lt;/span&gt;br /&gt;</li>
<li>        img src=”http://csdnimg.cn/www/images/csdnindex_piclogo.gif” /&gt;</li>
<li>    span style=”color: #800000;”&gt;body&gt;</li>
</ol>
<p>时间瀑布图:</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110419/1318572.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110419/1318572.jpg" alt="如何加载JS，JS应该放在什么位置？" width="498" height="88" border="0" /></a></p>
<p>从时间瀑布图中可以看出，代码2中，CSS和图片并没有并行下载，而是等待CSS下载完毕后才去并行下载后面的2个图片，当CSS后面跟着嵌入的JS的时候，该CSS就会出现阻塞后面资源下载的情况。</p>
<p>有人可能会问，这里为什么不说说嵌入的JS阻塞了后面的资源，而是说CSS阻塞了? 想想我们现在用的是1个空函数，解析这个空函数1ms就够，而后面2个图片是等CSS下载完1.3s后才开始下载。大家还可以试试把嵌入JS放到CSS前面，就不会出现阻塞的情况了。</p>
<p><strong>根本原因：</strong>因为浏览器会维持html中css和js的顺序，样式表必须在嵌入的JS执行前先加载、解析完。而嵌入的JS会阻塞后面的资源加载，所以就会出现上面CSS阻塞下载的情况。</p>
<p><strong>嵌入JS应该放在什么位置</strong></p>
<p>1、放在底部，虽然放在底部照样会阻塞所有呈现，但不会阻塞资源下载。</p>
<p>2、如果嵌入JS放在head中，请把嵌入JS放在CSS头部。</p>
<p>3、使用defer</p>
<p>4、不要在嵌入的JS中调用运行时间较长的函数，如果一定要用，可以用setTimeout来调用</p>
<p>PS：很多网站喜欢在head中嵌入JS，并且习惯放在CSS后面，比如看到的<a href="http://www.qq.com/">www.qq.com</a>，当然也有很多网站是把JS放到CSS前面的，比如yahoo,google</p>
<h1>如何减少请求数</h1>
<p>我们先分析下请求头，看看每次请求都带了那些额外的数据.下面是监控的google的请求头</p>
<p>Host www.google.com.hk</p>
<p>User-Agent Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTBDFff GTB7.0</p>
<p>Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8</p>
<p>Accept-Language zh-cn,en-us;q=0.7,en;q=0.3</p>
<p>Accept-Encoding gzip,deflate</p>
<p>Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7</p>
<p>Keep-Alive 115</p>
<p>Proxy-Connection keep-alive</p>
<p>返回的response head</p>
<p>Date Sat, 17 Apr 2010 08:18:18 GMT</p>
<p>Expires -1</p>
<p>Cache-Control private, max-age=0</p>
<p>Content-Type text/html; charset=UTF-8</p>
<p>Set-Cookie PREF=ID=b94a24e8e90a0f50:NW=1:TM=1271492298:LM=1271492298:S=JH7CxsIx48Zoo8Nn; expires=Mon, 16-Apr-2012 08:18:18 GMT; path=/; domain=.google.com.hk NID=33=EJVyLQBv2CSgpXQTq8DLIT2JQ4aCAE9YKkU2x-h4hVw_ATrGx7njA69UUBMbzVHVnkAOe_jlGGzOoXhQACSFDP1i53C8hWjRTJd0vYtRNWhGYGv491mwbngkT6LCYbvg; expires=Sun, 17-Oct-2010 08:18:18 GMT; path=/; domain=.google.com.hk; HttpOnly</p>
<p>Content-Encoding gzip</p>
<p>Server gws</p>
<p>Content-Length 4344</p>
<p>这里发送的请求头的大小大概420 bytes，返回的请求头大概 600 bytes。</p>
<p>可见每次请求都会带上一些额外的信息进行传输(这次请求中还没有带cookie)，当请求的资源很小，比如1个不到1k的图标，可能request带的数据比实际图标的数据量还大。</p>
<p>所以当请求越多的时候，在网络上传输的数据自然就多，传输速度自然就慢了。</p>
<p>其实request自带的数据量还是小问题，毕竟request能带的数据量还是有限的。</p>
<p><strong>http连接的开销</strong></p>
<p>相比request头部多余的数据，http连接的开销则更加严重。先看看从用户输入1个URL到下载内容到客户端需要经过哪些阶段：</p>
<p>1. 域名解析</p>
<p>2. 开启TCP连接</p>
<p>3. 发送请求</p>
<p>4. 等待(主要包括网络延迟和服务器处理时间)</p>
<p>5. 下载资源</p>
<p>可能很多人认为每次请求大部分时间都花在下载资源上，让我们看看blogjava资源下载瀑布图(每种颜色代表的阶段与上面5个阶段对应)：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110421/1014150.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1014150.jpg" alt="高性能WEB开发 - 为什么要减少请求数，如何减少请求数!" width="498" height="326" border="0" /></a></p>
<p>看了上图你可能惊讶，花费在等待阶段的时间比实际下载的时间要多的多，上图告诉我们：</p>
<p>1. 每次请求花费的大部分时间在其他阶段，而不是在下载资源阶段</p>
<p>2. 再小的资源照样会花费很多时间在其他阶段，只是下载阶段会比较短(见上图的第6个资源，才284Byte)。</p>
<p>正对上面提到的2种情况，我们应该要怎么进行优化了?减少请求数来减少其他阶段的花销和网络中传输的数据。</p>
<p><strong>如何减少请求数</strong></p>
<p><strong>1、合并文件</strong></p>
<p>合并文件就是把很多JS文件合并成1个文件，很多CSS文件合并成1个文件，这种方法应该很多人用到过，这里不做详细介绍，</p>
<p>只推荐1个合并的工具：yuiCompressor 这个工具yahoo提供的。 http://developer.yahoo.com/yui/compressor/</p>
<p><strong>2、合并图片</strong></p>
<p>这是利用css sprite，通过控制背景图片的位置来显示不同的图片。这种技术也是大家都用过的，不做详细介绍，推荐1个在线合并图片的网站:http://csssprites.com/</p>
<p><strong>3、把JS、CSS合并到1个文件</strong></p>
<p>上面第1种方法说的只是把几个JS文件合并成1个JS文件，几个CSS文件合并成1个CSS文件，哪如何把CSS和JS都合并到1个文件中，见我的另1篇文章:</p>
<p>http://www.blogjava.net/BearRui/archive/2010/04/18/combin_css_js.html</p>
<p><strong>4、使用Image maps</strong></p>
<p>Image maps 是把多个图片合并成1个图片，然后使用html中的标签连接图片，并实现点击图片不同的区域执行不同的动作，image map在导航条中比较容易使用到。</p>
<p>image map的使用方法见： http://www.w3.org/TR/html401/struct/objects.html#h-13.6</p>
<p><strong>5、data嵌入图片</strong></p>
<p>这种方法把图片进行编码直接嵌入到html中进行使用，以减少HTTP请求，但这个会增加HTML页面的大小，而且这样嵌入的图片不能缓存。见下面这个图片：</p>
<p>上面的图片就是把图片进行base64编码后使用data:嵌入到html中，代码如下(后面的省略了，大家可以查看源代码看)：</p>
<ol>
<li>&lt;IMG SRC=”data:image/gif;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/4QAWRXhpZgAASUkqAAgAA&#8230;&#8230;”&gt;</li>
</ol>
<p>其中google的视频搜索中，搜索出来的视频缩略图就都是使用嵌入的图片的，见下图：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110421/1014151.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1014151.jpg" alt="高性能WEB开发 - 为什么要减少请求数，如何减少请求数!" width="701" height="291" border="0" /></a></p>
<p>以上几种方法在都有利有弊，在不同情况下可以选择不同的使用方式，比如使用data嵌入图片虽然减少了请求数，但会增加页面大小。</p>
<p>所以微软的bing搜索在用户第一次访问的时候使用data嵌入图片，然后后台懒加载真真的图片，以后访问就直接使用缓存的图片，而不使用data。</p>
<h1>减少请求、响应的数据量</h1>
<p>上一篇中我们说到了 <a href="http://developer.51cto.com/art/201104/256656.htm" target="_blank">如何减少请求数</a>，这次说说如何减少请求、响应的数据量(即在网络中传输的数据量)，减少传输的数据量不仅仅可以加快页面加载速度，更可以节约服务器带宽，为你剩不少钱(好像很多机房托管都是按流量算钱的)。</p>
<p><strong>GZIP压缩</strong></p>
<p>gzip是目前所有浏览器都支持的一种压缩格式，IE6需要SP1及以上才支持(别说你还在用IE5,~_~)。gzip可以说是最方便而且也是最大减少响应数据量的1种方法。</p>
<p>说它方便，是因为你不需要为它写任何额外的代码，只需要在http服务器上加上配置都行了，现在主流的http服务器都支持gzip，各种服务器的配置这里就不一一介绍(其实是我不知道怎么配)，</p>
<p>nginx的配置可以参考我这篇文章：www.blogjava.net/BearRui/archive/2010/01/29/web_performance_server.html</p>
<p>我们先看看gzip的压缩比率能达到多少，这里用jquery 1.4.2的min和src2个版本进行测试，使用nginx服务器，gzip压缩级别使用的是4：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110421/1126040.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1126040.jpg" alt="高性能WEB开发- 减少请求，响应的数据量" width="498" height="172" border="0" /></a></p>
<p>注意看上图的红色部分，jquery src文件在启用gzip后大小减少了70%</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110421/1126041.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1126041.jpg" alt="高性能WEB开发- 减少请求，响应的数据量" width="498" height="175" border="0" /></a></p>
<p>这张图片可以看出就算是已经压缩过min.js在启用gzip后大小也减少了65%。</p>
<p><strong>别对图片启用gzip</strong></p>
<p>在知道了gzip强大的压缩能力后，你是否想对服务器上的所有文件启用gzip了，先让我们看看图片中启用gzip后会是什么情况。</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110421/1126042.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1126042.jpg" alt="高性能WEB开发- 减少请求，响应的数据量" width="498" height="175" border="0" /></a></p>
<p>hoho,1个gif图片经过gzip压缩后反而变大了???这是因为图片本来就是一种压缩格式，gzip不能再进行压缩，反而会添加1些额外的头部信息，所以图片会变大。</p>
<p>在测试过程中，发现jpg的图片经过gzip压缩后会变小,不知道为何，可能跟图片压缩方式有关。不过压缩比率也比较小，所以就算是jpg，建议也不要开启gzip压缩。</p>
<p><strong>比较适合启用gzip压缩的文件有如下这些：</strong></p>
<p>1. javascript</p>
<p>2. CSS</p>
<p>3. HTML，xml</p>
<p>4、plain text</p>
<p><strong>别乱用cookie</strong></p>
<p>现在几乎没有哪个网站不使用cookie了，可是该怎么使用cookie比较合适了，cookie有几个重要的属性:path(路径),domain(域),expires(过期时间)。浏览器就是根据这3个属性来判断在发送请求的时候是否需要带上这个cookie。</p>
<p>cookie使用最好的方式，就是当请求的资源需要cookie的时候才带上该cookie。其他任何请求都不带上cookie。但事实上很多人在使用cookie的时候已经习惯性的设置成：path=/ domain=.domain.com。这样的结果就是不管任何请求都会带上cookie，就算你是请求的图片(img.domain.com)、静态资源服务器(res.domain.com)这些根本不需要cookie的资源，浏览器照样会带上这些没用的cookie。咱们一起来看现实中的1个列子，博客园(www.cnblogs.com)：</p>
<p>先看看博客园的cookie是怎么设置的,下面是firefox查看博客园cookie的截图：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110421/1126043.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1126043.jpg" alt="高性能WEB开发- 减少请求，响应的数据量" width="496" height="298" border="0" /></a></p>
<p>cnblogs总共有5个cookie值，而且全部设置都是 path=/ domain=.cnblogs.com。知道了cookie的设置后，我们再来监控下博客园首页的请求，监控的统计信息如下：</p>
<p>总请求数：39(其中图片22个,JS7个,css2个)。</p>
<p>其中js、css、image 主要来自3个静态资源服务器: common.cnblogs.com , pic.cnblogs.com ,static.cnblogs.com</p>
<p>再看其中1个请求图片(http://static.cnblogs.com/images/a4/banner_job.gif)的请求头：</p>
<p>Host static.cnblogs.com</p>
<p>User-Agent Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTBDFff GTB7.0</p>
<p>Accept image/png,image/*;q=0.8,*/*;q=0.5</p>
<p>Accept-Language zh-cn,en-us;q=0.7,en;q=0.3</p>
<p>Accept-Encoding gzip,deflate</p>
<p>Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7</p>
<p>Keep-Alive 115</p>
<p>Proxy-Connection keep-alive</p>
<p>Referer http://www.cnblogs.com/</p>
<p>Cookie __gads=ID=a15d7cb5c3413e56:T=1272278620:S=ALNI_MZNMr6_d_PCjgkJNJeEQXkmZ3bxTQ; __utma=226521935.1697566422.1272278366.1272278366.1272278366.1; __utmb=226521935.2.10.1272278366; __utmc=226521935; __utmz=226521935.1272278367.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none)</p>
<p>我们发现在请求banner_job.gif这个图片的时候，浏览器把cnblogs.com的所有cookie都带上了(其他图片的请求都是一样的)，我估计博客园在处理图片的时候应该不需要用到cookie吧?也许你认为这几个cookie的大小只有300个字节左右，无所谓啦。</p>
<p>我们做个简单的计算，假设博客园每天有50W个PV(实际情况应该不止吧)，每次PV大概有15次请求静态资源，15*500000*300/1024/1024=2145M。也就说这几个cookie每天大概会耗费博客园2G的带宽。当然这种简单的计算方式肯定会有偏差，毕竟我们还没把静态资源缓存考虑进去。但是个人觉得要是博客园要是把cookie的domain设置为www.cnblogs.com会更好一些。</p>
<p><strong>妙用204状态</strong></p>
<p>http中200,404,500状态大家都很清楚，但204状态大家可能用的比较少，204状态是指服务器成功处理了客户端请求，但服务器无返回内容。204是HTTP中数据量最少的响应状态，204的响应中没有body，而且Content-Length=0。很多人在使用ajax提交一些数据给服务器，而不需要服务器返回的时候，常常在服务端使用下面的代码：response.getWriter().print(“”)，这是返回1个空白的页面，是1个200请求。它还是有body，而且Content-Length不会等于0。其实这个时候你完全可以直接返回1个204状态(response.setStatus(204))。204在一些网站分析的代码中最常用到，只需要把客户端的一些信息提交给服务器就完事，让我们看看google首页的1个204响应,google首页的最后1个请求返回的就是204状态，但这个请求是干嘛用的就没猜出来了：<br />
<a href="http://images.51cto.com/files/uploadimg/20110421/1126044.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110421/1126044.jpg" alt="高性能WEB开发- 减少请求，响应的数据量" width="498" height="200" border="0" /></a></p>
<h1>JS、CSS的合并压缩</h1>
<p><strong>存在的问题：</strong></p>
<p>合并、压缩文件主要有2方面的问题：</p>
<p>1. 每次发布的时候需要运行一下自己写的bat文件或者其他程序把文件按照自己的配置合并和压缩。</p>
<p>2. 因生产环境和开发环境需要加载的文件不一样，生产环境为了需要加载合并、压缩后的文件，而开发环境为了修改、调试方便，需要加载非合并、压缩的文件，所以我们常常需要在JSP中类似与下面的判断代码：</p>
<ol>
<li>&lt;c:if test=”${env==&#8217;prod&#8217;}”&gt;</li>
<li>   &lt;script type=”text/javascript” src=”/js/all.js”&gt;&lt;/script&gt;</li>
<li>&lt;/c:if&gt;</li>
<li>&lt;c:if test=”${env==&#8217;dev&#8217;}”&gt;</li>
<li>   &lt;script type=”text/javascript” src=”/js/1.js”&gt;&lt;/script&gt;</li>
<li>   &lt;script type=”text/javascript” src=”/js/2.js”&gt;&lt;/script&gt;</li>
<li>   &lt;script type=”text/javascript” src=”/js/3.js”&gt;&lt;/script&gt;</li>
<li>&lt;/c:if&gt;</li>
</ol>
<p><strong>缓存问题：</strong>在现在JS满天飞的时代，大家都知道缓存能带来的巨大好处，但缓存确实非常麻烦的一个问题，相信很多人曾经历过下面的情况：为了让程序更快，在服务器上为JS加上缓冲5天的代码，但产品更新后第二天就接到电话说系统出错，详细了解后就发现是缓存引起的，让用户删除缓存后就会OK。原因很简单，就是你JS已经修改了，但用户还在使用缓存中的老JS。在经历几次这种情况，被领导数落了几次后。没办法只能把JS的缓冲去掉，或者改成8个小时。可这样就完全失去了缓存的优势了，哪我们到底需要解决哪些问题才能让我们使用缓冲顺心如意了?</p>
<p>1. 如何在修改了某个JS后，自动把所有引用该JS页面的代码中加上1个版本号?</p>
<p>2. 该如何生成版本号，根据什么来产生这个版本号。</p>
<p>可能有人为了解决上面的缓存问题，写了个JSP标签，通过标签读取JS、css文件的修改时间来作为版本号，从而来解决上面2个问题。但这种方法有下面几个缺点：</p>
<p>1. 每次请求都要通过标签读取读取文件的修改时间，速度慢。当然你可以把文件的修改时间放到缓存中，这样也会加到了内存使用量。</p>
<p>2. 在HTML静态页面中用不了</p>
<p>3. 如果你们公司是如下的部署发布方式(我们公司就是这样)，则会失效。每次发布，不是直接覆盖之前的WEB目录，运维的为的发布方便，要求每次发布直接给他们1个war包，他们会把之前WEB目录整个删除，然后上传现在的war包，这样就导致程序运行后，所有文件的最后修改时间都是解压war的时间。</p>
<p><strong>分享自己项目中的处理方案：</strong></p>
<p>为了解决上面讨论过的问题，在下写了1个如下的组件，组件中根据我们自己的实际情况使用了文件大小来做为文件的版本号，虽然在文件修改很小(比如把字符a改成b)，可能文件大小并没有变，导致版本号也不会变。</p>
<p>但这种机率还是非常低的。当然如果你觉的使用文件修改时间作为版本号适合你，只需要修改一行代码就行，下面看下这个组件的处理流程(本来想用流程图表达，最后还是觉的文字来的直白写)：</p>
<p>1. 程序启动(contextInitialized)</p>
<p>2. 搜索程序目录下的所有merge.txt文件，根据merge.txt文件的配置合并文件, merge.txt文件实例如下：</p>
<p># 文件合并配置文件,多个文件以|隔开，以/开头的表示从根目录开始,</p>
<p># 空格之后的文件名表示合并之后的文件名</p>
<p># 把1,2,3合并到all文件中</p>
<p>1.js|2.js|3.js all.js</p>
<p>#合并CSS</p>
<p>/css/mian.css|/css/common.css all.css</p>
<p>3. 搜索程序目录下所有JS，CSS文件(包括合并后的)，每个文件都压缩后生成对应的1个新文件。</p>
<p>4. 搜索程序目录下所有JSP,html文件，把所有JS,css的引用代码改成压缩后并加了版本号的引用。</p>
<p><strong>实例：</strong></p>
<p>&nbsp;</p>
<p>实例的文件结构如下图：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110422/1006360.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110422/1006360.jpg" alt="JS、CSS的合并、压缩、缓存管理" width="156" height="274" border="0" /></a></p>
<p>看JSP原始代码(程序运行前)：</p>
<ol>
<li>&lt;%@page contentType=”text/html” pageEncoding=”UTF-8&#8243;%&gt;</li>
<li>&lt;!DOCTYPE HTML PUBLIC ”-//W3C//DTD HTML 4.01 Transitional//EN”  ”http://www.w3.org/TR/html4/loose.dtd”&gt;</li>
<li>&lt;% boolean isDev = false;  // 是否开发环境%&gt;</li>
<li>&lt;html&gt;</li>
<li>    &lt;head&gt;</li>
<li>        &lt;meta http-equiv=”Content-Type” content=”text/html; charset=UTF-8&#8243;&gt;</li>
<li>        &lt;title&gt;JSP Page&lt;/title&gt;</li>
<li>        &lt;% if(isDev){ %&gt;</li>
<li>        &lt;script type=”text/javascript” src=”&lt;%=request.getContextPath() %&gt;/js/jquery-1.4.2.js”&gt;&lt;/script&gt;</li>
<li>        &lt;script type=”text/javascript” src=”&lt;%=request.getContextPath() %&gt;/js/1.js”&gt;&lt;/script&gt;</li>
<li>        &lt;script type=”text/javascript” src=”&lt;%=request.getContextPath() %&gt;/js/2.js”&gt;&lt;/script&gt;</li>
<li>        &lt;link type=”text/css” rel=”stylesheet” href=”&lt;%=request.getContextPath() %&gt;/css/1.css” /&gt;</li>
<li>        &lt;link type=”text/css” rel=”stylesheet” href=”&lt;%=request.getContextPath() %&gt;/css/2.css” /&gt;</li>
<li>        &lt;% }else{ %&gt;</li>
<li>        &lt;script type=”text/javascript” src=”&lt;%=request.getContextPath() %&gt;/js/jquery-1.4.2.js”&gt;&lt;/script&gt;</li>
<li>        &lt;script type=”text/javascript” src=”&lt;%=request.getContextPath() %&gt;/js/all.js”&gt;&lt;/script&gt;</li>
<li>        &lt;link type=”text/css” rel=”stylesheet”  href=”&lt;%=request.getContextPath() %&gt;/css/all.css” /&gt;</li>
<li>        &lt;% } %&gt;</li>
<li>    &lt;/head&gt;</li>
<li>    &lt;body&gt;</li>
<li>        &lt;h1 class=”c1&#8243;&gt;Hello World!&lt;/h1&gt;</li>
<li>    &lt;/body&gt;</li>
<li>&lt;/html&gt;</li>
</ol>
<p>程序运行后JSP的代码：</p>
<ol>
<li>&lt;%@page contentType=”text/html” pageEncoding=”UTF-8&#8243;%&gt;</li>
<li>&lt;!DOCTYPE HTML PUBLIC ”-//W3C//DTD HTML 4.01 Transitional//EN” ”http://www.w3.org/TR/html4/loose.dtd”&gt;</li>
<li>&lt;%</li>
<li>    boolean isDev = false;  // 是否开发环境</li>
<li>%&gt;</li>
<li>&lt;html&gt;</li>
<li>    &lt;head&gt;</li>
<li>        &lt;meta http-equiv=”Content-Type” content=”text/html; charset=UTF-8&#8243;&gt;</li>
<li>        &lt;title&gt;JSP Page&lt;/title&gt;</li>
<li>        &lt;% if(isDev){ %&gt;</li>
<li>        &lt;script type=”text/javascript” src=”&lt;%=request.getContextPath() %&gt;/js/jquery-1.4.2-3gmin.js?99375&#8243;&gt;&lt;/script&gt;</li>
<li>        &lt;script type=”text/javascript” src=”&lt;%=request.getContextPath() %&gt;/js/1-3gmin.js?90&#8243;&gt;&lt;/script&gt;</li>
<li>        &lt;script type=”text/javascript” src=”&lt;%=request.getContextPath() %&gt;/js/2-3gmin.js?91&#8243;&gt;&lt;/script&gt;</li>
<li>        &lt;link type=”text/css” rel=”stylesheet” href=”&lt;%=request.getContextPath() %&gt;/css/1-3gmin.css?35&#8243; /&gt;</li>
<li>        &lt;link type=”text/css” rel=”stylesheet” href=”&lt;%=request.getContextPath() %&gt;/css/2-3gmin.css?18&#8243; /&gt;</li>
<li>        &lt;% }else{ %&gt;</li>
<li>        &lt;script type=”text/javascript” src=”&lt;%=request.getContextPath() %&gt;/js/jquery-1.4.2-3gmin.js?99375&#8243;&gt;&lt;/script&gt;</li>
<li>        &lt;script type=”text/javascript” src=”&lt;%=request.getContextPath() %&gt;/js/all-3gmin.js?180&#8243;&gt;&lt;/script&gt;</li>
<li>        &lt;link type=”text/css” rel=”stylesheet”  href=”&lt;%=request.getContextPath() %&gt;/css/all-3gmin.css?53&#8243; /&gt;</li>
<li>        &lt;% } %&gt;</li>
<li>    &lt;/head&gt;</li>
<li>    &lt;body&gt;</li>
<li>        &lt;h1 class=”c1&#8243;&gt;Hello World!&lt;/h1&gt;</li>
<li>    &lt;/body&gt;</li>
<li>&lt;/html&gt;</li>
</ol>
<p>加3gmin后缀的文件全部是程序启动时自动生成的。</p>
<h1>页面呈现、重绘、回流</h1>
<p>在讨论页面重绘、回流之前。需要对页面的呈现流程有些了解，页面是怎么把html结合css等显示到浏览器上的，下面的流程图显示了浏览器对页面的呈现的处理流程。可能不同的浏览器略微会有些不同。但基本上都是类似的。</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110425/1025240.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110425/1025240.jpg" alt="高性能WEB开发- 页面呈现、重绘、回流" width="498" height="164" border="0" /></a></p>
<p>1. 浏览器把获取到的html代码解析成1个Dom树，html中的每个tag都是Dom树中的1个节点，根节点就是我们常用的document对象( tag)。dom树就是我们用firebug或者IE Developer Toolbar等工具看到的html结构，里面包含了所有的html tag，包括display:none隐藏，还有用JS动态添加的元素等。</p>
<p>2. 浏览器把所有样式(主要包括css和浏览器的样式设置)解析成样式结构体，在解析的过程中会去掉浏览器不能识别的样式，比如IE会去掉-moz开头的样式，而firefox会去掉_开头的样式。</p>
<p>3、dom tree和样式结构体结合后构建呈现树(render tree),render tree有点类似于dom tree，但其实区别有很大，render tree能识别样式，render tree中每个node都有自己的style，而且render tree不包含隐藏的节点(比如display:none的节点，还有head节点)，因为这些节点不会用于呈现，而且不会影响呈现的，所以就不会包含到render tree中。注意 visibility:hidden隐藏的元素还是会包含到render tree中的，因为visibility:hidden 会影响布局(layout)，会占有空间。根据css2的标准，render tree中的每个节点都称为box(Box dimensions)，box所有属性：width,height,margin,padding,left,top,border等。</p>
<p>4. 一旦render tree构建完毕后，浏览器就可以根据render tree来绘制页面了。</p>
<p><strong>回流与重绘</strong></p>
<p>1. 当render tree中的一部分(或全部)因为元素的规模尺寸，布局，隐藏等改变而需要重新构建。这就称为回流(其实我觉得叫重新布局更简单明了些)。每个页面至少需要一次回流，就是在页面第一次加载的时候。</p>
<p>2. 当render tree中的一些元素需要更新属性，而这些属性只是影响元素的外观，风格，而不会影响布局的，比如background-color。则就叫称为重绘。</p>
<p>注：从上面可以看出，回流必将引起重绘，而重绘不一定会引起回流。</p>
<p><strong>什么操作会引起重绘、回流</strong></p>
<p>其实任何对render tree中元素的操作都会引起回流或者重绘，比如：</p>
<p>1. 添加、删除元素(回流+重绘)</p>
<p>2. 隐藏元素，display:none(回流+重绘)，visibility:hidden(只重绘，不回流)</p>
<p>3. 移动元素，比如改变top,left(jquery的animate方法就是,改变top,left不一定会影响回流)，或者移动元素到另外1个父元素中。(重绘+回流)</p>
<p>4. 对style的操作(对不同的属性操作，影响不一样)</p>
<p>5. 还有一种是用户的操作，比如改变浏览器大小，改变浏览器的字体大小等(回流+重绘)</p>
<p>让我们看看下面的代码是如何影响回流和重绘的:</p>
<ol>
<li>var s = document.body.style;</li>
<li></li>
<li>s.padding = ”2px”; // 回流+重绘</li>
<li>s.border = ”1px solid red”; // 再一次 回流+重绘</li>
<li></li>
<li>s.color = ”blue”; // 再一次重绘</li>
<li>s.backgroundColor = ”#ccc”; // 再一次 重绘</li>
<li></li>
<li>s.fontSize = ”14px”; // 再一次 回流+重绘</li>
<li></li>
<li>// 添加node，再一次 回流+重绘</li>
<li>document.body.appendChild(document.createTextNode(&#8216;abc!&#8217;));</li>
</ol>
<p><em>请注意我上面用了多少个再一次。</em></p>
<p>说到这里大家都知道回流比重绘的代价要更高，回流的花销跟render tree有多少节点需要重新构建有关系，假设你直接操作body，比如在body最前面插入1个元素，会导致整个render tree回流，这样代价当然会比较高，但如果是指body后面插入1个元素，则不会影响前面元素的回流。</p>
<p><strong>聪明的浏览器</strong></p>
<p>从上个实例代码中可以看到几行简单的JS代码就引起了6次左右的回流、重绘。而且我们也知道回流的花销也不小，如果每句JS操作都去回流重绘的话，浏览器可能就会受不了。所以很多浏览器都会优化这些操作，浏览器会维护1个队列，把所有会引起回流、重绘的操作放入这个队列，等队列中的操作到了一定的数量或者到了一定的时间间隔，浏览器就会把flush队列，进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。</p>
<p>虽然有了浏览器的优化，但有时候我们写的一些代码可能会强制浏览器提前flush队列，这样浏览器的优化可能就起不到作用了。当你请求向浏览器请求一些style信息的时候，就会让浏览器flush队列，比如：</p>
<p>1. offsetTop, offsetLeft, offsetWidth, offsetHeight</p>
<p>2. scrollTop/Left/Width/Height</p>
<p>3. clientTop/Left/Width/Height</p>
<p>4. width,height</p>
<p>5. 请求了getComputedStyle(), 或者 ie的 currentStyle</p>
<p>当你请求上面的一些属性的时候，浏览器为了给你最精确的值，需要flush队列，因为队列中可能会有影响到这些值的操作。</p>
<p><strong>如何减少回流、重绘</strong></p>
<p>减少回流、重绘其实就是需要减少对render tree的操作，并减少对一些style信息的请求，尽量利用好浏览器的优化策略。具体方法有：</p>
<p>1. 不要1个1个改变元素的样式属性，最好直接改变className，但className是预先定义好的样式，不是动态的，如果你要动态改变一些样式，则使用cssText来改变，见下面代码：</p>
<ol>
<li>// 不好的写法</li>
<li>var left = 1;</li>
<li>var top = 1;</li>
<li>el.style.left = left + ”px”;</li>
<li>el.style.top  = top  + ”px”;</li>
<li></li>
<li>// 比较好的写法</li>
<li>el.className += ” className1&#8243;;</li>
<li></li>
<li>// 比较好的写法</li>
<li>el.style.cssText += ”; left: ” + left + ”px; top: ” + top + ”px;”;</li>
</ol>
<p>2. 让要操作的元素进行”离线处理”，处理完后一起更新，这里所谓的”离线处理”即让元素不存在于render tree中，比如：</p>
<p>a) 使用documentFragment或div等元素进行缓存操作，这个主要用于添加元素的时候，大家应该都用过，就是先把所有要添加到元素添加到1个div(这个div也是新加的)，</p>
<p>最后才把这个div append到body中。</p>
<p>b) 先display:none 隐藏元素，然后对该元素进行所有的操作，最后再显示该元素。因对display:none的元素进行操作不会引起回流、重绘。所以只要操作只会有2次回流。</p>
<p>3 不要经常访问会引起浏览器flush队列的属性，如果你确实要访问，就先读取到变量中进行缓存，以后用的时候直接读取变量就可以了，见下面代码：</p>
<ol>
<li>// 别这样写，大哥</li>
<li>for(循环) {</li>
<li>    elel.style.left = el.offsetLeft + 5 + ”px”;</li>
<li>    elel.style.top  = el.offsetTop  + 5 + ”px”;</li>
<li>}</li>
<li></li>
<li>// 这样写好点</li>
<li>var left = el.offsetLeft,top  = el.offsetTop,s = el.style;</li>
<li>for(循环) {</li>
<li>    left += 10;</li>
<li>    top  += 10;</li>
<li>    s.left = left + ”px”;</li>
<li>    s.top  = top  + ”px”;</li>
<li>}</li>
</ol>
<p>4. 考虑你的操作会影响到render tree中的多少节点以及影响的方式，影响越多，花费肯定就越多。比如现在很多人使用jquery的animate方法移动元素来展示一些动画效果，想想下面2种移动的方法：</p>
<ol>
<li>// block1是position:absolute 定位的元素，它移动会影响到它父元素下的所有子元素。</li>
<li>// 因为在它移动过程中，所有子元素需要判断block1的z-index是否在自己的上面，</li>
<li>// 如果是在自己的上面,则需要重绘,这里不会引起回流</li>
<li>$(“#block1&#8243;).animate({left:50});</li>
<li>// block2是相对定位的元素,这个影响的元素与block1一样，但是因为block2非绝对定位</li>
<li>// 而且改变的是marginLeft属性，所以这里每次改变不但会影响重绘，</li>
<li>// 还会引起父元素及其下元素的回流</li>
<li>$(“#block2&#8243;).animate({marginLeft:50});</li>
</ol>
<p>&nbsp;</p>
<p><strong>实例测试</strong></p>
<p>最后用2个工具对上面的理论进行一些测试，这2个工具是在我 “<a href="http://developer.51cto.com/art/201104/254038.htm" target="_blank">web 性能测试工具推荐</a>” 文章中推荐过的工具，分别是：dynaTrace(测试ie),Speed Tracer(测试Chrome)。</p>
<p>第一个测试代码不改变元素的规则，大小，位置。只改变颜色，所以不存在回流，仅测试重绘，代码如下：</p>
<ol>
<li>&lt;body&gt;</li>
<li>    &lt;script type=”text/javascript”&gt;</li>
<li>        var s = document.body.style;</li>
<li>        var computed;</li>
<li>        if (document.body.currentStyle) {</li>
<li>          computed = document.body.currentStyle;</li>
<li>        } else {</li>
<li>          computed = document.defaultView.getComputedStyle(document.body, ”);</li>
<li>        }</li>
<li>    function testOneByOne(){</li>
<li>      s.color = &#8217;red&#8217;;;</li>
<li>      tmp = computed.backgroundColor;</li>
<li>      s.color = &#8217;white&#8217;;</li>
<li>      tmp = computed.backgroundImage;</li>
<li>      s.color = &#8217;green&#8217;;</li>
<li>      tmp = computed.backgroundAttachment;</li>
<li>    }</li>
<li></li>
<li>    function testAll() {</li>
<li>      s.color = &#8217;yellow&#8217;;</li>
<li>      s.color = &#8217;pink&#8217;;</li>
<li>      s.color = &#8217;blue&#8217;;</li>
<li></li>
<li>      tmp = computed.backgroundColor;</li>
<li>      tmp = computed.backgroundImage;</li>
<li>      tmp = computed.backgroundAttachment;</li>
<li>    }</li>
<li>    &lt;/script&gt;</li>
<li>    color test &lt;br /&gt;</li>
<li>    &lt;button onclick=”testOneByOne()”&gt;Test One by One&lt;/button&gt;</li>
<li>    &lt;button onclick=”testAll()”&gt;Test All&lt;/button&gt;</li>
<li>&lt;/body&gt;</li>
</ol>
<p>testOneByOne 函数改变3次color,其中每次改变后调用getComputedStyle,读取属性值(按我们上面的讨论，这里会引起队列的flush)，testAll同样是改变3次color，但是每次改变后并不马上调用getComputedStyle。</p>
<p>我们先点击Test One by One按钮，然后点击 Test All,用dynaTrace监控如下：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110425/1025241.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110425/1025241.jpg" alt="高性能WEB开发- 页面呈现、重绘、回流" width="498" height="377" border="0" /></a></p>
<p>上图可以看到我们执行了2次button的click事件，每次click后都跟一次rendering(页面重绘)，2次click函数执行的时间都差不多,0.25ms,0.26ms，但其后的rendering时间就相差一倍多。(这里也可以看出，其实很多时候前端的性能瓶颈并不在于JS的执行，而是在于页面的呈现，这种情况在用JS做到富客户端中更为突出)。我们再看图的下面部分，这是第一次rendering的详细信息，可以看到里面有2行是 Scheduleing layout task，这个就是我们前面讨论过的浏览器优化过的队列，可以看出我们引发2次的flush。</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110425/1025242.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110425/1025242.jpg" alt="高性能WEB开发- 页面呈现、重绘、回流" width="498" height="297" border="0" /></a></p>
<p>再看第二次rendering的详细信息，可以看出并没有Scheduleing layout task,所以这次rendering的时间也比较短。</p>
<p>测试代码2：这个测试跟第一次测试的代码很类似，但加上了对layout的改变，为的是测试回流。</p>
<ol>
<li>&lt;!DOCTYPE html PUBLIC ”-//W3C//DTD XHTML 1.0 Transitional//EN”</li>
<li>“http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”&gt;</li>
<li>&lt;html xmlns=”http://www.w3.org/1999/xhtml”&gt;</li>
<li>&lt;head&gt;</li>
<li>&lt;/head&gt;</li>
<li>&lt;body&gt;</li>
<li>    &lt;script type=”text/javascript”&gt;</li>
<li>        var s = document.body.style;</li>
<li>        var computed;</li>
<li>        if (document.body.currentStyle) {</li>
<li>          computed = document.body.currentStyle;</li>
<li>        } else {</li>
<li>          computed = document.defaultView.getComputedStyle(document.body, ”);</li>
<li>        }</li>
<li>    function testOneByOne(){</li>
<li>      s.color = &#8217;red&#8217;;</li>
<li>      s.padding = &#8217;1px&#8217;;</li>
<li>      tmp = computed.backgroundColor;</li>
<li>      s.color = &#8217;white&#8217;;</li>
<li>      s.padding = &#8217;2px&#8217;;</li>
<li>      tmp = computed.backgroundImage;</li>
<li>      s.color = &#8217;green&#8217;;</li>
<li>      s.padding = &#8217;3px&#8217;;</li>
<li>      tmp = computed.backgroundAttachment;</li>
<li>    }</li>
<li></li>
<li>    function testAll() {</li>
<li>      s.color = &#8217;yellow&#8217;;</li>
<li>      s.padding = &#8217;4px&#8217;;</li>
<li>      s.color = &#8217;pink&#8217;;</li>
<li>      s.padding = &#8217;5px&#8217;;</li>
<li>      s.color = &#8217;blue&#8217;;</li>
<li>      s.padding = &#8217;6px&#8217;;</li>
<li></li>
<li>      tmp = computed.backgroundColor;</li>
<li>      tmp = computed.backgroundImage;</li>
<li>      tmp = computed.backgroundAttachment;</li>
<li>    }</li>
<li>    &lt;/script&gt;</li>
<li>    color test &lt;br /&gt;</li>
<li>    &lt;button onclick=”testOneByOne()”&gt;Test One by One&lt;/button&gt;</li>
<li>    &lt;button onclick=”testAll()”&gt;Test All&lt;/button&gt;</li>
<li>&lt;/body&gt;</li>
</ol>
<p>用dynaTrace监控如下：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110425/1025243.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110425/1025243.jpg" alt="高性能WEB开发- 页面呈现、重绘、回流" width="498" height="343" border="0" /></a></p>
<p>相信这图不用多说大家都能看懂了吧，可以看出有了回流后，rendering的时间相比之前的只重绘，时间翻了3倍了，可见回流的高成本性啊。</p>
<p>大家看到时候注意明细处相比之前的多了个 Calcalating flow layout。</p>
<p>最后再使用Speed Tracer测试一下，其实结果是一样的，只是让大家了解下2个测试工具：</p>
<p>测试1：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110425/1025244.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110425/1025244.jpg" alt="高性能WEB开发- 页面呈现、重绘、回流" width="498" height="647" border="0" /></a></p>
<p>图上第一次点击执行2ms(其中有50% 用于style Recalculation), 第二次1ms，而且第一次click后面也跟了2次style Recalculation,而第二次点击却没有style Recalculation。</p>
<p>但是这次测试发现paint重绘的时间竟然是一样的，都是3ms，这可能就是chrome比IE强的地方吧。</p>
<p>测试2：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110425/1025245.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110425/1025245.jpg" alt="高性能WEB开发- 页面呈现、重绘、回流" width="498" height="582" border="0" /></a></p>
<p>从图中竟然发现第二次的测试结果在时间上跟第一次的完全一样，这可能是因为操作太少，而chrome又比较强大，所以没能测试明显结果出来，</p>
<p>但注意图中多了1个紫色部分，就是layout的部分。也就是我们说的回流。</p>
<h1>flush让页面分块逐步呈现</h1>
<p>正对这种情况，还有一种处理方法，就是让response分块编码进行传输。response分块编码，可以先传输一部分不需要处理的html代码到客户端，等其他耗时代码执行完毕后再传输另外的html代码。</p>
<p><strong>分块编码(chunked encoding)</strong></p>
<p>chunked encoding 是http1.1 才支持编码格式(当然目前没有哪个浏览器不支持1.1了)，chunked encoding 与一般的响应区别如下：</p>
<ol>
<li>正常的响应：</li>
<li>    HTTP/1.1 200 OK</li>
<li>    Cache-Control: private, max-age=60</li>
<li>    Content-Length: 75785</li>
<li>    Content-Type: text/html; charset=utf-8</li>
<li>    ..其他response headers</li>
<li></li>
<li>   &lt;!DOCTYPE html PUBLIC ”-//W3C//DTD XHTML 1.0 Transitional//EN”</li>
</ol>
<ol>
<li>chunked encoding 响应:</li>
<li>   HTTP/1.1 200 OK</li>
<li>   Cache-Control: private, max-age=60</li>
<li>   Content-Length: 75785</li>
<li>   Content-Type: text/html; charset=utf-8</li>
<li>   Transfer-Encoding: chunked</li>
<li>   ..其他response headers</li>
<li></li>
<li>   chunk #1(这里通常是16进制的数字，标志这个块的大小)</li>
<li>   &lt;!DOCTYPE html PUBLIC ”-//W3C//DTD XHTML 1.0 Transitional//EN”&#8230;.</li>
<li></li>
<li>   chunk #2</li>
<li>   &lt;div &#8230;..</li>
<li></li>
<li>   chunk #3</li>
<li>   &#8230;.&lt;/body&gt;&lt;/html&gt;</li>
</ol>
<p><strong>实例(JSP)</strong></p>
<p>一个简单的页面，分为头部(header)和内容(部分)，假设内容部分需要读取数据库，花费3秒时间,然后显示csdn的logo。header部分显示cnblogs的logo。代码如下：</p>
<ol>
<li>&lt;body&gt;</li>
<li>    &lt;div id=”head” style=”border:1px solid #ccc;”&gt;</li>
<li>       cnblogs logo &lt;img src=”http://images.cnblogs.com/logo_small.gif” /&gt;</li>
<li>    &lt;/div&gt;</li>
<li>    &lt;br /&gt;</li>
<li>    &lt;div id=”content” style=”border:1px solid blue;”&gt;</li>
<li>    &lt;%</li>
<li>        // 睡眠3秒</li>
<li>        Thread.currentThread().sleep(3000);</li>
<li>    %&gt;</li>
<li>        csdn logo&lt;br /&gt;</li>
<li>        &lt;img src=”http://csdnimg.cn/www/images/csdnindex_piclogo.gif” /&gt;</li>
<li>    &lt;/div&gt;</li>
<li>&lt;/body&gt;</li>
</ol>
<p>演示地址：http://213.186.44.204:8080/ChunkTest/nochunk.jsp (服务器比较差，请大家温柔点)</p>
<p>打开这个演示地址发现很正常的页面，在3秒后才开始下载显示2个logo，资源加载瀑布图如下：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110428/0949340.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110428/0949340.jpg" alt="flush让页面分块,逐步呈现" width="793" height="90" border="0" /></a></p>
<p>现在把代码改成如下，加上flush，让response把之前的html分块输出：</p>
<ol>
<li>&lt;div id=”head” style=”border:1px solid #ccc;”&gt;</li>
<li>   cnblogs logo &lt;img src=”http://images.cnblogs.com/logo_small.gif” /&gt;</li>
<li>&lt;/div&gt;</li>
<li>&lt;%</li>
<li>    out.flush(); // flush response，分块输出</li>
<li>%&gt;</li>
<li>&lt;br /&gt;</li>
<li>&lt;div id=”content” style=”border:1px solid blue;”&gt;</li>
<li>    &lt;%</li>
<li>        // 睡眠3秒</li>
<li>        Thread.currentThread().sleep(3000);</li>
<li>    %&gt;</li>
<li>    csdn logo&lt;br /&gt;</li>
<li>    &lt;img src=”http://csdnimg.cn/www/images/csdnindex_piclogo.gif” /&gt;</li>
<li>&lt;/div&gt;</li>
</ol>
<p>演示地址：http://213.186.44.204:8080/ChunkTest/chunk.jsp</p>
<p>打开这个演示地址，是不是发现cnblogs logo先下载显示出来，3秒后csdn logo才显示，资源加载图如下：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110428/0949341.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110428/0949341.jpg" alt="高性能WEB开发(11) - flush让页面分块,逐步呈现" width="786" height="91" border="0" /></a></p>
<p>从这个图发现，cnblogs的logo在jsp页面还没执行完就开始下载了，这就是分块输出的效果。</p>
<p>监控工具：</p>
<p>如何知道我们是否成功使用了chunk encoding了 ，只要用工具查看response header 中是否包含了Transfer-Encoding: chunked，如果包含了，则是分块了。但要想监控分块的详细信息，据我所知，目前只有httpwatch支持，可以查看我们到底分了多少块，但是数量好像都多显示了1个，如下图：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20110428/0949342.jpg" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20110428/0949342.jpg" alt="高性能WEB开发(11) - flush让页面分块,逐步呈现" width="605" height="179" border="0" /></a></p>
<p>系列专题：<a href="http://developer.51cto.com/art/201104/257581.htm" target="_blank">高性能WEB开发应用指南</a></p>
]]></content:encoded>
			<wfw:commentRss>http://www.wdcode.org/archives/535.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Hadoop 集群 傻瓜式搭建手记</title>
		<link>http://www.wdcode.org/archives/532.html</link>
		<comments>http://www.wdcode.org/archives/532.html#comments</comments>
		<pubDate>Thu, 15 Dec 2011 06:49:26 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[集群]]></category>
		<category><![CDATA[Hadoop]]></category>

		<guid isPermaLink="false">http://www.wdcode.org/?p=532</guid>
		<description><![CDATA[听说Hadoop很久了，今天开始尝试自己搭建一个集群来玩玩，同时学习下Linux各种操作。

主要参考资料当然是官方文档了：

Hadoop 集群搭建：http://hadoop.apache.org/common/docs/r0.19.2/cn/cluster_setup.html

Hadoop 快速入门：http://hadoop.apache.org/common/docs/r0.19.2/cn/quickstart.html

采用的操作系统是 Ubuntu 11.04.03 64-bit Server。

通过VMware 来创建虚拟网络，这也是现实问题，没有那么多机器啊<img src="http://static.blog.csdn.net/xheditor/xheditor_emot/default/laugh.gif" alt="大笑" />

&#160;

预计的设计是这样，4台虚拟机：NameServer, JobTracker, DataNode01, DataNode02。

主机为Win7。

&#160;

首先安装系统，这个没有什么好说的，VMware有Easy Install，中间偶尔干涉一下，就完事了。

然后安装必须的软件，第一个就是JAVA，Hadoop推荐使用Sun公司的Java, 但是用apt-get install java， 只有openjdk之类的。于是上网搜了半天，找到一篇文章（http://blog.csdn.net/ansomray/article/details/5825096），根据其说明，添加apt source，再重新安装，命令如下：
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7061964#">view plain</a></div>
</div>
<ol>
	<li>sudo add-apt-repository "deb http://archive.canonical.com/ lucid partner"</li>
	<li></li>
	<li>sudo apt-get update</li>
	<li></li>
	<li>sudo apt-get install sun-java6-jdk</li>
</ol>
</div>
&#160;

然后设置JAVA_HOME：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7061964#">view plain</a></div>
</div>
<ol>
	<li>sudo vi /etc/environment</li>
</ol>
</div>
在其中添加

&#160;

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7061964#">view plain</a></div>
</div>
<ol>
	<li>&#60;pre name="code" class="plain"&#62;JAVA_HOME=/usr/lib/jvm/java-6-sun</li>
	<li>CLASSPATH=.:/usr/lib/jvm/java-6-sun/lib</li>
</ol>
</div>
&#160;

接着再安装ssh，这个没有什么好说的：

&#160;

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7061964#">view plain</a></div>
</div>
<ol>
	<li>sudo apt-get install ssh</li>
	<li>sudo apt-get install rsync</li>
</ol>
</div>
再来就是获取Hadoop的发行版，这个可以从<a href="http://hadoop.apache.org/common/releases.html">这里</a>获取稳定发行版。

&#160;

在Win7中下载下来后，问题就来了，怎么从Ubuntu Server中访问Win7的东西，这玩意儿只有命令行啊。继续搜索，发现是通过mount 的方式来处理： 先在mnt下建立一个目录，然后把Win7中共享的文件mount 到那个目录上，这样就可以访问了。命令如下：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7061964#">view plain</a></div>
</div>
<ol>
	<li>sudo mkdir /mnt/HostShare</li>
	<li>sudo mount -t cifs -o username=myname,password=11111 //192.168.0.100/share /mnt/HostShare</li>
</ol>
</div>
这里本来是查到的用smbfs，结果Ubuntu说这个好像将从2.6.27内核中删除，不推荐使用，于是采用了cifs。同时好像必须要同时提供username和password，不然就会说writen-protected，mount不上去。

&#160;

最后当然是建立Hadoop目录，解压弄下来的文件，我下载的是.tar.gz文件，所以命令如下：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7061964#">view plain</a></div>
</div>
<ol>
	<li>sudo tar zxvf hadoop.tar.gz</li>
	<li>sudo mv hadoop-hadoop-0.20.203 /hadoop</li>
</ol>
</div>
第二个命令是把解压后的文件夹移动到根目录并重命名为hadoop，这个是为了管理方便。

&#160;

&#160;

这样，基本的软件准备就搞定了，下面就是拷贝装好的第一台虚拟机，再复制三台出来，分别按照预定各自命名，然后就是组建网络了。

软件准备好了，开始搭建网络。

我用的是VMware WorkStation 6.5，而VMware提供了三种网络支持：Bridge, Nat, Host Only。

关于VMware的网络概念，我严重参考了这篇文章：深入理解VMware虚拟网络（http://wangchunhai.blog.51cto.com/225186/381225）

简单来说，我需要这4台虚拟机能上网（Internet），同时相互能互联，并且能够与主机互联。拓扑结构可以参考那篇文章，只是其中的一个子集而已（所有虚拟机在一个子网中）。

这个目标我选择使用NAT虚拟网络来实现，这好像也是VMware装机时默认的网络配置。不过在默认情况下，好像只有一台机器可以通过NAT上网，这时就需要手动配置网络了。

首先寻找NAT的网段等信息，先启动一台虚拟机，假设就是NameNode。

使用下面命令来查看：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7062272#">view plain</a></div>
</div>
<ol>
	<li>ifconfig</li>
</ol>
</div>
这时，应该会有两个网络设备，eth0和lo，不去管lo，eth0的IP地址是192.168.100.138，这个是通过DHCP自动获取的，由此我们可以得到网段是192.168.100.*。

&#160;

再看看网关：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7062272#">view plain</a></div>
</div>
<ol>
	<li>route</li>
</ol>
</div>
&#160;

&#160;

得到了默认的网关是192.168.100.2。这里比较奇怪的是我在Win7的网络适配器中看到VmNet8的IP是192.168.100.1，难道网关不是它吗？不过我同时也发现VmNet8也是被禁用了的，很晕。

好了，现在得到了足够的信息，开始进行网络配置：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7062272#">view plain</a></div>
</div>
<ol>
	<li>sudo vi /etc/network/interfaces</li>
</ol>
</div>
将eth0相关的内容修改为如下配置：

&#160;

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7062272#">view plain</a></div>
</div>
<ol>
	<li>auto eth0</li>
	<li></li>
	<li>iface eth0 inet static</li>
	<li>address 192.168.100.101</li>
	<li>netmask 255.255.255.0</li>
	<li>gateway 192.168.100.2</li>
</ol>
</div>
然后再重启网络服务：

&#160;

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7062272#">view plain</a></div>
</div>
<ol>
	<li>sudo /etc/init.d/networking restart</li>
</ol>
</div>
最后再用ifconfig和route来看看是更改过来了，再ping一下sina，看看Internet是否可用：

&#160;

<img src="http://hi.csdn.net/attachment/201112/12/0_13236535891zEu.gif" alt="" />

&#160;

按照同样的方法来配置其余3台虚拟机即可。这里要注意的是不知道是不是由于我是直接拷贝虚拟机文件的原因，在剩下的三台虚拟机中，eth0不见了，只有eth1，第一次用ifconfig时也只是显示出lo来，后来使用 ifconfig -a才发现有个eth1的。那么在之前的网络配置/etc/network/interfaces 中，就需要添加为eth1的。

&#160;

网络配好了后，可以通过ssh或ping来检测是否可以相互联通。

接下来就是配置各个服务器了。

本来以为网络搭建好了就可以了，没有想到还有ssh这个东西需要配置免密码访问，这个东西应该也算是软件上互联吧。

ssh免密码本来是很简单的一件事情，不过我折腾了2天<img src="http://static.blog.csdn.net/xheditor/xheditor_emot/default/wail.gif" alt="大哭" />，这就是菜鸟的必经过程了，我算是觉悟了。

经过无数次的尝试，终于拼出了一条正确的道路：

&#160;

1. 首先在本地生成空密码的公钥和私钥：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
	<li>ssh-keygen -t dsa -P '' -f ~/.ssh/id_dsa</li>
</ol>
</div>
在网上看到有些使用rsa，不过在Hadoop网站上写的是dsa，就使用dsa吧，生成的密钥文件放在当前用户的.ssh目录下，据我观察，貌似ssh所需要的用户配置文件都存放在这里。执行命令后应该会有 id_dsa, id_dsa.pub两个文件。

&#160;

2. 配置本地访问不需要密码：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
	<li>cat ~/.ssh/id_dsa.pub &#62;&#62; ~/.ssh/authorized_keys</li>
</ol>
</div>
其实就是把生成的公钥导入到authorized_keys中，估计ssh 会使用这个文件来进行验证。

&#160;

3. 执行下面的命令来测试本地访问

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
	<li>ssh localhost</li>
</ol>
</div>
如果之前没有使用过ssh连接localhost， 那么会有提示添加localhost到knowhosts中去，然后要求输入密码。第二次及以后的连接就不需要密码了。

&#160;

这里要注意，一定要执行exit来退出ssh，否则这样嵌套执行会搞晕人的。我就这么晕了半天，不知道是在ssh上干活还是直接登录干活。后来在直接操作时把目录切换到了～/.ssh下，这样使用ssh后命令提示符就不一样了。呵呵，菜鸟就是需要多搞一点提示。

4. 把本地的公钥复制到另外的机器（比如192.168.100.102）上：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
	<li>scp ~/.ssh/id_dsa.pub fox@192.168.100.102:/home/fox/.ssh/101_dsa.pub</li>
</ol>
</div>
&#160;

这个会要求输入102的密码，照提示输入即可。

这里就是折腾了我2天的罪魁祸首，在配置从NameNode(192.168.100.101免密码ssh访问)JobTracker（192.168.100.102）时，一定要在NameNode上执行这个命令。虽然我不知道为什么，但是多次尝试下来，就只有这样才能成功。或许还有其它办法，以后有机会了再研究研究。

5. 在另外一台机器上JobTracker（192.168.100.102），把刚刚拷贝过来的公钥导入authorized_keys

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
	<li>cat ~/.ssh/101_dsa.pub &#62;&#62; authorized_keys</li>
</ol>
</div>
6. 从101上连接102：

&#160;

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
	<li>ssh 192.168.100.102</li>
</ol>
</div>
同样第一次连接的话会询问是否添加机器以及要求输入密码，第二次就不用了。

&#160;

&#160;

至此就算配置好了ssh从其中一台到另外一台的免密码访问了，把这个操作在各个机器上两两执行，就可以让任意一台机器免密码访问另外任意一台机器了。

如果配置过程中有失误，想要重新来，可以删除.ssh下的所有东西，从头来过。

&#160;

&#160;

顺便提一下，我用的是VMware文件拷贝生成的虚拟机，故而所有虚拟机的机器名都是一致的，不知道会有什么问题，所以我在执行上述操作时更改了机器名。

改机器名要改两个地方：
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
	<li>sudo vi /etc/hostname</li>
	<li>sudo vi /etc/hosts</li>
</ol>
</div>
&#160;

/etc/hostname 里面的字符串就是机器名，这个要重启后才能生效。Ubuntu下才是这样的，其它Linux貌似不是这个文件。

/etc/hosts 里面应该是用于访问网络时的一个DNS式的东西，把所有出现原机器名的地方都替换成新的机器名即可。

&#160;

改好后重启，可以用hostname命令来查看是否成功。

&#160;

好了，暂时就到这里，明天再研究真正的Hadoop配置文件。

实际配置这部分时，才发现在（一）当中提到的那两个官网文档已经过时了，现在我实际下载的版本是 0.20.203，也即那个stable版本。于是只好找到相应版本的文档来参考了：http://hadoop.apache.org/common/docs/stable/cluster_setup.html

&#160;

关于Hadoop的配置，按照我的理解，应该有六个文件需要修改，它们都在Hadoop的conf文件夹下，分别是：

masters/slavers：配置masters和slavers的机器IP

hadoop-env.sh ：Hadoop 运行时的环境变量，比如JAVA_HOME，LOG之类的

core-site.xml ：Hadoop 的核心配置文件，对应并覆盖core-default.xml 中的配置项

hdfs-site.xml ：HDFS 的配置文件，对应并覆盖hdfs-default.xml 中的配置项

mapred-site.xml ：Map/Reduce的配置文件，对应并覆盖mapred-default.xml 中的配置项

上面提到的三个*-default.xml 是Hadoop的默认配置项，理论上都是只读的，如果要修改，就通过对应的用户配置文件来覆盖其设置。

&#160;

1、先配置masters/slavers，NameNode和JobTracker是master， DataNode01和DataNode02是salvers

Masters：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
	<li>vi /hadoop/conf/masters</li>
</ol>
</div>
masters文件内容如下：

&#160;

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
	<li>192.168.100.101</li>
	<li>192.168.100.102</li>
</ol>
</div>
&#160;

Slavers：
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
	<li>vi /hadoop/conf/slavers</li>
</ol>
</div>
slavers文件内容如下：

&#160;

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
	<li>192.168.100.103</li>
	<li>192.168.100.104</li>
</ol>
</div>
&#160;

2、先配置hadoop-env.sh，这里最重要的是配置JAVA_HOME，在我的机器上是这样的：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
	<li>export JAVA_HOME=/usr/lib/jvm/java-6-sun</li>
</ol>
</div>
其余的可以考虑配置日志文件路径：

&#160;

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
	<li>export HADOOP_LOG_DIR=${HADOOP_HOME}/logs</li>
</ol>
</div>
3、配置core-site.xml，通过文档可以知道这里一般是配置NameNode的地址，即机器名或IP：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
	<li>&#60;configuration&#62;</li>
	<li> &#60;property&#62;</li>
	<li> &#60;name&#62;fs.default.name&#60;/name&#62;</li>
	<li> &#60;value&#62;hdfs://192.168.100.101:9000&#60;/value&#62;</li>
	<li> &#60;/property&#62;</li>
	<li>&#60;/configuration&#62;</li>
</ol>
</div>
4、配置hdfs-site.xml，这里一般配置文件存放路径以及文件权限：

&#160;

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
	<li>&#60;configuration&#62;</li>
	<li> &#60;property&#62;</li>
	<li> &#60;!-- DFS中存储文件命名空间信息的目录 --&#62;</li>
	<li> &#60;name&#62;dfs.name.dir&#60;/name&#62;</li>
	<li> &#60;value&#62;/hadoop/dfs/name&#60;/value&#62;</li>
	<li> &#60;/property&#62;</li>
	<li> &#60;property&#62;</li>
	<li> &#60;!-- DFS中存储文件数据的目录 --&#62;</li>
	<li> &#60;name&#62;dfs.data.dir&#60;/name&#62;</li>
	<li> &#60;value&#62;/hadoop/dfs/data&#60;/value&#62;</li>
	<li> &#60;/property&#62;</li>
	<li> &#60;property&#62;</li>
	<li> &#60;!-- 是否对DFS中的文件进行权限控制(测试中一般用false)--&#62;</li>
	<li> &#60;name&#62;dfs.permissions&#60;/name&#62;</li>
	<li> &#60;value&#62;false&#60;/value&#62;</li>
	<li> &#60;/property&#62;</li>
	<li>&#60;/configuration&#62;</li>
</ol>
</div>
5、配置mapred-site.xml， 这里配置的东西有很多，都是跟Map-Reduce相关的，不过暂时先配置如下几项：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
	<li>&#60;configuration&#62;</li>
	<li> &#60;property&#62;</li>
	<li> &#60;!-- JobTracker节点 --&#62;</li>
	<li> &#60;name&#62;mapred.job.tracker&#60;/name&#62;</li>
	<li> &#60;value&#62;192.168.100.102:9001&#60;/value&#62;</li>
	<li> &#60;/property&#62;</li>
	<li> &#60;property&#62;</li>
	<li> &#60;!-- map/reduce的系统目录（使用的HDFS的路径） --&#62;</li>
	<li> &#60;name&#62;mapred.system.dir&#60;/name&#62;</li>
	<li> &#60;value&#62;/hadoop/mapred/system&#60;/value&#62;</li>
	<li> &#60;/property&#62;</li>
	<li> &#60;property&#62;</li>
	<li> &#60;!-- map/reduce的临时目录（可使用“,”隔开，设置多重路径来分摊磁盘IO） --&#62;</li>
	<li> &#60;name&#62;mapred.local.dir&#60;/name&#62;</li>
	<li> &#60;value&#62;/hadoop/mapred/local&#60;/value&#62;</li>
	<li> &#60;/property&#62;</li>
	<li>&#60;/configuration&#62;</li>
</ol>
</div>
&#160;

&#160;

&#160;

这些配置都可以在一台机器上搞定，由于Hadoop所有机器是使用同样的配置，所以可以通过scp命令将conf下的内容拷贝复制到各个机器上：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
	<li>scp -rp /hadoop/conf fox@192.168.100.102/hadoop/</li>
</ol>
</div>
只复制conf是由于我拷贝虚拟机时就已经把JAVA,Hadoop 都安装好后才拷贝的，这样可以保证所有文件在同一目录。

&#160;

&#160;

<strong>启动</strong>

然后，激动人心的时刻到了，所有配置都搞定了，我们可以启动了！

不过还有一件事情必须要先做， 格式化名称空间。

在NameNode上，执行如下命令：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
	<li>cd /hadoop/bin</li>
	<li>./hadoop namenode -format</li>
</ol>
</div>
执行后结果如下：

&#160;

<img src="http://hi.csdn.net/attachment/201112/14/0_132387386935zw.gif" alt="" />

&#160;

然后就可以执行最后一个命令了：

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
	<li>./start-all.sh</li>
</ol>
</div>
如果一切顺利的话，应该就成功了：

&#160;

<img src="http://hi.csdn.net/attachment/201112/14/0_1323874846ilsX.gif" alt="" />

&#160;

如果要关闭的话，可以执行

&#160;
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
	<li>./stop-all.sh</li>
</ol>
</div>
&#160;

&#160;

&#160;

&#160;

&#160;]]></description>
			<content:encoded><![CDATA[<p>听说Hadoop很久了，今天开始尝试自己搭建一个集群来玩玩，同时学习下Linux各种操作。</p>
<p>主要参考资料当然是官方文档了：</p>
<p>Hadoop 集群搭建：http://hadoop.apache.org/common/docs/r0.19.2/cn/cluster_setup.html</p>
<p>Hadoop 快速入门：http://hadoop.apache.org/common/docs/r0.19.2/cn/quickstart.html</p>
<p>采用的操作系统是 Ubuntu 11.04.03 64-bit Server。</p>
<p>通过VMware 来创建虚拟网络，这也是现实问题，没有那么多机器啊<img src="http://static.blog.csdn.net/xheditor/xheditor_emot/default/laugh.gif" alt="大笑" /></p>
<p>&nbsp;</p>
<p>预计的设计是这样，4台虚拟机：NameServer, JobTracker, DataNode01, DataNode02。</p>
<p>主机为Win7。</p>
<p>&nbsp;</p>
<p>首先安装系统，这个没有什么好说的，VMware有Easy Install，中间偶尔干涉一下，就完事了。</p>
<p>然后安装必须的软件，第一个就是JAVA，Hadoop推荐使用Sun公司的Java, 但是用apt-get install java， 只有openjdk之类的。于是上网搜了半天，找到一篇文章（http://blog.csdn.net/ansomray/article/details/5825096），根据其说明，添加apt source，再重新安装，命令如下：</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7061964#">view plain</a></div>
</div>
<ol>
<li>sudo add-apt-repository ”deb http://archive.canonical.com/ lucid partner”</li>
<li></li>
<li>sudo apt-get update</li>
<li></li>
<li>sudo apt-get install sun-java6-jdk</li>
</ol>
</div>
<p>&nbsp;</p>
<p>然后设置JAVA_HOME：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7061964#">view plain</a></div>
</div>
<ol>
<li>sudo vi /etc/environment</li>
</ol>
</div>
<p>在其中添加</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7061964#">view plain</a></div>
</div>
<ol>
<li>&lt;pre name=”code” class=”plain”&gt;JAVA_HOME=/usr/lib/jvm/java-6-sun</li>
<li>CLASSPATH=.:/usr/lib/jvm/java-6-sun/lib</li>
</ol>
</div>
<p>&nbsp;</p>
<p>接着再安装ssh，这个没有什么好说的：</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7061964#">view plain</a></div>
</div>
<ol>
<li>sudo apt-get install ssh</li>
<li>sudo apt-get install rsync</li>
</ol>
</div>
<p>再来就是获取Hadoop的发行版，这个可以从<a href="http://hadoop.apache.org/common/releases.html">这里</a>获取稳定发行版。</p>
<p>&nbsp;</p>
<p>在Win7中下载下来后，问题就来了，怎么从Ubuntu Server中访问Win7的东西，这玩意儿只有命令行啊。继续搜索，发现是通过mount 的方式来处理： 先在mnt下建立一个目录，然后把Win7中共享的文件mount 到那个目录上，这样就可以访问了。命令如下：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7061964#">view plain</a></div>
</div>
<ol>
<li>sudo mkdir /mnt/HostShare</li>
<li>sudo mount -t cifs -o username=myname,password=11111 //192.168.0.100/share /mnt/HostShare</li>
</ol>
</div>
<p>这里本来是查到的用smbfs，结果Ubuntu说这个好像将从2.6.27内核中删除，不推荐使用，于是采用了cifs。同时好像必须要同时提供username和password，不然就会说writen-protected，mount不上去。</p>
<p>&nbsp;</p>
<p>最后当然是建立Hadoop目录，解压弄下来的文件，我下载的是.tar.gz文件，所以命令如下：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7061964#">view plain</a></div>
</div>
<ol>
<li>sudo tar zxvf hadoop.tar.gz</li>
<li>sudo mv hadoop-hadoop-0.20.203 /hadoop</li>
</ol>
</div>
<p>第二个命令是把解压后的文件夹移动到根目录并重命名为hadoop，这个是为了管理方便。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>这样，基本的软件准备就搞定了，下面就是拷贝装好的第一台虚拟机，再复制三台出来，分别按照预定各自命名，然后就是组建网络了。</p>
<p>软件准备好了，开始搭建网络。</p>
<p>我用的是VMware WorkStation 6.5，而VMware提供了三种网络支持：Bridge, Nat, Host Only。</p>
<p>关于VMware的网络概念，我严重参考了这篇文章：深入理解VMware虚拟网络（http://wangchunhai.blog.51cto.com/225186/381225）</p>
<p>简单来说，我需要这4台虚拟机能上网（Internet），同时相互能互联，并且能够与主机互联。拓扑结构可以参考那篇文章，只是其中的一个子集而已（所有虚拟机在一个子网中）。</p>
<p>这个目标我选择使用NAT虚拟网络来实现，这好像也是VMware装机时默认的网络配置。不过在默认情况下，好像只有一台机器可以通过NAT上网，这时就需要手动配置网络了。</p>
<p>首先寻找NAT的网段等信息，先启动一台虚拟机，假设就是NameNode。</p>
<p>使用下面命令来查看：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7062272#">view plain</a></div>
</div>
<ol>
<li>ifconfig</li>
</ol>
</div>
<p>这时，应该会有两个网络设备，eth0和lo，不去管lo，eth0的IP地址是192.168.100.138，这个是通过DHCP自动获取的，由此我们可以得到网段是192.168.100.*。</p>
<p>&nbsp;</p>
<p>再看看网关：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7062272#">view plain</a></div>
</div>
<ol>
<li>route</li>
</ol>
</div>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>得到了默认的网关是192.168.100.2。这里比较奇怪的是我在Win7的网络适配器中看到VmNet8的IP是192.168.100.1，难道网关不是它吗？不过我同时也发现VmNet8也是被禁用了的，很晕。</p>
<p>好了，现在得到了足够的信息，开始进行网络配置：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7062272#">view plain</a></div>
</div>
<ol>
<li>sudo vi /etc/network/interfaces</li>
</ol>
</div>
<p>将eth0相关的内容修改为如下配置：</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7062272#">view plain</a></div>
</div>
<ol>
<li>auto eth0</li>
<li></li>
<li>iface eth0 inet static</li>
<li>address 192.168.100.101</li>
<li>netmask 255.255.255.0</li>
<li>gateway 192.168.100.2</li>
</ol>
</div>
<p>然后再重启网络服务：</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7062272#">view plain</a></div>
</div>
<ol>
<li>sudo /etc/init.d/networking restart</li>
</ol>
</div>
<p>最后再用ifconfig和route来看看是更改过来了，再ping一下sina，看看Internet是否可用：</p>
<p>&nbsp;</p>
<p><img src="http://hi.csdn.net/attachment/201112/12/0_13236535891zEu.gif" alt="" /></p>
<p>&nbsp;</p>
<p>按照同样的方法来配置其余3台虚拟机即可。这里要注意的是不知道是不是由于我是直接拷贝虚拟机文件的原因，在剩下的三台虚拟机中，eth0不见了，只有eth1，第一次用ifconfig时也只是显示出lo来，后来使用 ifconfig -a才发现有个eth1的。那么在之前的网络配置/etc/network/interfaces 中，就需要添加为eth1的。</p>
<p>&nbsp;</p>
<p>网络配好了后，可以通过ssh或ping来检测是否可以相互联通。</p>
<p>接下来就是配置各个服务器了。</p>
<p>本来以为网络搭建好了就可以了，没有想到还有ssh这个东西需要配置免密码访问，这个东西应该也算是软件上互联吧。</p>
<p>ssh免密码本来是很简单的一件事情，不过我折腾了2天<img src="http://static.blog.csdn.net/xheditor/xheditor_emot/default/wail.gif" alt="大哭" />，这就是菜鸟的必经过程了，我算是觉悟了。</p>
<p>经过无数次的尝试，终于拼出了一条正确的道路：</p>
<p>&nbsp;</p>
<p>1. 首先在本地生成空密码的公钥和私钥：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
<li>ssh-keygen -t dsa -P ” -f ~/.ssh/id_dsa</li>
</ol>
</div>
<p>在网上看到有些使用rsa，不过在Hadoop网站上写的是dsa，就使用dsa吧，生成的密钥文件放在当前用户的.ssh目录下，据我观察，貌似ssh所需要的用户配置文件都存放在这里。执行命令后应该会有 id_dsa, id_dsa.pub两个文件。</p>
<p>&nbsp;</p>
<p>2. 配置本地访问不需要密码：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
<li>cat ~/.ssh/id_dsa.pub &gt;&gt; ~/.ssh/authorized_keys</li>
</ol>
</div>
<p>其实就是把生成的公钥导入到authorized_keys中，估计ssh 会使用这个文件来进行验证。</p>
<p>&nbsp;</p>
<p>3. 执行下面的命令来测试本地访问</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
<li>ssh localhost</li>
</ol>
</div>
<p>如果之前没有使用过ssh连接localhost， 那么会有提示添加localhost到knowhosts中去，然后要求输入密码。第二次及以后的连接就不需要密码了。</p>
<p>&nbsp;</p>
<p>这里要注意，一定要执行exit来退出ssh，否则这样嵌套执行会搞晕人的。我就这么晕了半天，不知道是在ssh上干活还是直接登录干活。后来在直接操作时把目录切换到了～/.ssh下，这样使用ssh后命令提示符就不一样了。呵呵，菜鸟就是需要多搞一点提示。</p>
<p>4. 把本地的公钥复制到另外的机器（比如192.168.100.102）上：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
<li>scp ~/.ssh/id_dsa.pub fox@192.168.100.102:/home/fox/.ssh/101_dsa.pub</li>
</ol>
</div>
<p>&nbsp;</p>
<p>这个会要求输入102的密码，照提示输入即可。</p>
<p>这里就是折腾了我2天的罪魁祸首，在配置从NameNode(192.168.100.101免密码ssh访问)JobTracker（192.168.100.102）时，一定要在NameNode上执行这个命令。虽然我不知道为什么，但是多次尝试下来，就只有这样才能成功。或许还有其它办法，以后有机会了再研究研究。</p>
<p>5. 在另外一台机器上JobTracker（192.168.100.102），把刚刚拷贝过来的公钥导入authorized_keys</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
<li>cat ~/.ssh/101_dsa.pub &gt;&gt; authorized_keys</li>
</ol>
</div>
<p>6. 从101上连接102：</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
<li>ssh 192.168.100.102</li>
</ol>
</div>
<p>同样第一次连接的话会询问是否添加机器以及要求输入密码，第二次就不用了。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>至此就算配置好了ssh从其中一台到另外一台的免密码访问了，把这个操作在各个机器上两两执行，就可以让任意一台机器免密码访问另外任意一台机器了。</p>
<p>如果配置过程中有失误，想要重新来，可以删除.ssh下的所有东西，从头来过。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>顺便提一下，我用的是VMware文件拷贝生成的虚拟机，故而所有虚拟机的机器名都是一致的，不知道会有什么问题，所以我在执行上述操作时更改了机器名。</p>
<p>改机器名要改两个地方：</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7068193#">view plain</a></div>
</div>
<ol>
<li>sudo vi /etc/hostname</li>
<li>sudo vi /etc/hosts</li>
</ol>
</div>
<p>&nbsp;</p>
<p>/etc/hostname 里面的字符串就是机器名，这个要重启后才能生效。Ubuntu下才是这样的，其它Linux貌似不是这个文件。</p>
<p>/etc/hosts 里面应该是用于访问网络时的一个DNS式的东西，把所有出现原机器名的地方都替换成新的机器名即可。</p>
<p>&nbsp;</p>
<p>改好后重启，可以用hostname命令来查看是否成功。</p>
<p>&nbsp;</p>
<p>好了，暂时就到这里，明天再研究真正的Hadoop配置文件。</p>
<p>实际配置这部分时，才发现在（一）当中提到的那两个官网文档已经过时了，现在我实际下载的版本是 0.20.203，也即那个stable版本。于是只好找到相应版本的文档来参考了：http://hadoop.apache.org/common/docs/stable/cluster_setup.html</p>
<p>&nbsp;</p>
<p>关于Hadoop的配置，按照我的理解，应该有六个文件需要修改，它们都在Hadoop的conf文件夹下，分别是：</p>
<p>masters/slavers：配置masters和slavers的机器IP</p>
<p>hadoop-env.sh ：Hadoop 运行时的环境变量，比如JAVA_HOME，LOG之类的</p>
<p>core-site.xml ：Hadoop 的核心配置文件，对应并覆盖core-default.xml 中的配置项</p>
<p>hdfs-site.xml ：HDFS 的配置文件，对应并覆盖hdfs-default.xml 中的配置项</p>
<p>mapred-site.xml ：Map/Reduce的配置文件，对应并覆盖mapred-default.xml 中的配置项</p>
<p>上面提到的三个*-default.xml 是Hadoop的默认配置项，理论上都是只读的，如果要修改，就通过对应的用户配置文件来覆盖其设置。</p>
<p>&nbsp;</p>
<p>1、先配置masters/slavers，NameNode和JobTracker是master， DataNode01和DataNode02是salvers</p>
<p>Masters：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
<li>vi /hadoop/conf/masters</li>
</ol>
</div>
<p>masters文件内容如下：</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
<li>192.168.100.101</li>
<li>192.168.100.102</li>
</ol>
</div>
<p>&nbsp;</p>
<p>Slavers：</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
<li>vi /hadoop/conf/slavers</li>
</ol>
</div>
<p>slavers文件内容如下：</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
<li>192.168.100.103</li>
<li>192.168.100.104</li>
</ol>
</div>
<p>&nbsp;</p>
<p>2、先配置hadoop-env.sh，这里最重要的是配置JAVA_HOME，在我的机器上是这样的：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
<li>export JAVA_HOME=/usr/lib/jvm/java-6-sun</li>
</ol>
</div>
<p>其余的可以考虑配置日志文件路径：</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
<li>export HADOOP_LOG_DIR=${HADOOP_HOME}/logs</li>
</ol>
</div>
<p>3、配置core-site.xml，通过文档可以知道这里一般是配置NameNode的地址，即机器名或IP：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
<li>&lt;configuration&gt;</li>
<li> &lt;property&gt;</li>
<li> &lt;name&gt;fs.default.name&lt;/name&gt;</li>
<li> &lt;value&gt;hdfs://192.168.100.101:9000&lt;/value&gt;</li>
<li> &lt;/property&gt;</li>
<li>&lt;/configuration&gt;</li>
</ol>
</div>
<p>4、配置hdfs-site.xml，这里一般配置文件存放路径以及文件权限：</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
<li>&lt;configuration&gt;</li>
<li> &lt;property&gt;</li>
<li> &lt;!&#8211; DFS中存储文件命名空间信息的目录 &#8211;&gt;</li>
<li> &lt;name&gt;dfs.name.dir&lt;/name&gt;</li>
<li> &lt;value&gt;/hadoop/dfs/name&lt;/value&gt;</li>
<li> &lt;/property&gt;</li>
<li> &lt;property&gt;</li>
<li> &lt;!&#8211; DFS中存储文件数据的目录 &#8211;&gt;</li>
<li> &lt;name&gt;dfs.data.dir&lt;/name&gt;</li>
<li> &lt;value&gt;/hadoop/dfs/data&lt;/value&gt;</li>
<li> &lt;/property&gt;</li>
<li> &lt;property&gt;</li>
<li> &lt;!&#8211; 是否对DFS中的文件进行权限控制(测试中一般用false)&#8211;&gt;</li>
<li> &lt;name&gt;dfs.permissions&lt;/name&gt;</li>
<li> &lt;value&gt;false&lt;/value&gt;</li>
<li> &lt;/property&gt;</li>
<li>&lt;/configuration&gt;</li>
</ol>
</div>
<p>5、配置mapred-site.xml， 这里配置的东西有很多，都是跟Map-Reduce相关的，不过暂时先配置如下几项：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
<li>&lt;configuration&gt;</li>
<li> &lt;property&gt;</li>
<li> &lt;!&#8211; JobTracker节点 &#8211;&gt;</li>
<li> &lt;name&gt;mapred.job.tracker&lt;/name&gt;</li>
<li> &lt;value&gt;192.168.100.102:9001&lt;/value&gt;</li>
<li> &lt;/property&gt;</li>
<li> &lt;property&gt;</li>
<li> &lt;!&#8211; map/reduce的系统目录（使用的HDFS的路径） &#8211;&gt;</li>
<li> &lt;name&gt;mapred.system.dir&lt;/name&gt;</li>
<li> &lt;value&gt;/hadoop/mapred/system&lt;/value&gt;</li>
<li> &lt;/property&gt;</li>
<li> &lt;property&gt;</li>
<li> &lt;!&#8211; map/reduce的临时目录（可使用“,”隔开，设置多重路径来分摊磁盘IO） &#8211;&gt;</li>
<li> &lt;name&gt;mapred.local.dir&lt;/name&gt;</li>
<li> &lt;value&gt;/hadoop/mapred/local&lt;/value&gt;</li>
<li> &lt;/property&gt;</li>
<li>&lt;/configuration&gt;</li>
</ol>
</div>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>这些配置都可以在一台机器上搞定，由于Hadoop所有机器是使用同样的配置，所以可以通过scp命令将conf下的内容拷贝复制到各个机器上：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
<li>scp -rp /hadoop/conf fox@192.168.100.102/hadoop/</li>
</ol>
</div>
<p>只复制conf是由于我拷贝虚拟机时就已经把JAVA,Hadoop 都安装好后才拷贝的，这样可以保证所有文件在同一目录。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>启动</strong></p>
<p>然后，激动人心的时刻到了，所有配置都搞定了，我们可以启动了！</p>
<p>不过还有一件事情必须要先做， 格式化名称空间。</p>
<p>在NameNode上，执行如下命令：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
<li>cd /hadoop/bin</li>
<li>./hadoop namenode -format</li>
</ol>
</div>
<p>执行后结果如下：</p>
<p>&nbsp;</p>
<p><img src="http://hi.csdn.net/attachment/201112/14/0_132387386935zw.gif" alt="" /></p>
<p>&nbsp;</p>
<p>然后就可以执行最后一个命令了：</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
<li>./start-all.sh</li>
</ol>
</div>
<p>如果一切顺利的话，应该就成功了：</p>
<p>&nbsp;</p>
<p><img src="http://hi.csdn.net/attachment/201112/14/0_1323874846ilsX.gif" alt="" /></p>
<p>&nbsp;</p>
<p>如果要关闭的话，可以执行</p>
<p>&nbsp;</p>
<div>
<div>
<div><a title="view plain" href="http://blog.csdn.net/lee_leefox/article/details/7071692#">view plain</a></div>
</div>
<ol>
<li>./stop-all.sh</li>
</ol>
</div>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>http://www.wdcode.org/archives/532.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>全面解读Java NIO工作原理</title>
		<link>http://www.wdcode.org/archives/530.html</link>
		<comments>http://www.wdcode.org/archives/530.html#comments</comments>
		<pubDate>Thu, 15 Dec 2011 06:47:34 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[J2SE]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[NIO]]></category>

		<guid isPermaLink="false">http://www.wdcode.org/?p=530</guid>
		<description><![CDATA[<strong>本文简介：</strong> JDK 1.4 中引入的新输入输出 (NIO) 库在标准 Java 代码中提供了高速的、面向块的 I/O。本实用教程从高级概念到底层的编程细节，非常详细地介绍了 NIO 库。您将学到诸如缓冲区和通道这样的关键 I/O 元素的知识，并考察更新后的库中的标准 I/O 是如何工作的。您还将了解只能通过 NIO 来完成的工作，如异步 I/O 和直接缓冲区。

<strong>◆  输入/输出：概念性描述</strong>

<strong>I/O 简介</strong>

I/O ? 或者输入/输出 ? 指的是计算机与外部世界或者一个程序与计算机的其余部分的之间的接口。它对于任何计算机系统都非常关键，因而所有 I/O 的主体实际上是内置在操作系统中的。单独的程序一般是让系统为它们完成大部分的工作。

在 Java 编程中，直到最近一直使用 流 的方式完成 I/O。所有 I/O 都被视为单个的字节的移动，通过一个称为 Stream 的对象一次移动一个字节。流 I/O 用于与外部世界接触。它也在内部使用，用于将对象转换为字节，然后再转换回对象。

NIO 与原来的 I/O 有同样的作用和目的，但是它使用不同的方式? 块 I/O。正如您将在本教程中学到的，块 I/O 的效率可以比流 I/O 高许多。

<strong>为什么要使用 NIO?</strong>

NIO 的创建目的是为了让 Java 程序员可以实现高速 I/O 而无需编写自定义的本机代码。NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统，因而可以极大地提高速度。

<strong>流与块的比较</strong>

原来的 I/O 库(在 java.io.*中) 与 NIO 最重要的区别是数据打包和传输的方式。正如前面提到的，原来的 I/O 以流的方式处理数据，而 NIO 以块的方式处理数据。

面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据，一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器，以便每个过滤器只负责单个复杂处理机制的一部分，这样也是相对简单的。不利的一面是，面向流的 I/O 通常相当慢。

一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

<strong>集成的 I/O</strong>

在 JDK 1.4 中原来的 I/O 包和 NIO 已经很好地集成了。 java.io.* 已经以 NIO 为基础重新实现了，所以现在它可以利用 NIO 的一些特性。例如， java.io.* 包中的一些类包含以块的形式读写数据的方法，这使得即使在更面向流的系统中，处理速度也会更快。

也可以用 NIO 库实现标准 I/O 功能。例如，可以容易地使用块 I/O 一次一个字节地移动数据。但是正如您会看到的，NIO 还提供了原 I/O 包中所没有的许多好处。

<strong>◆ 通道和缓冲区</strong>

<strong>概  述</strong>

通道 和 缓冲区 是 NIO 中的核心对象，几乎在每一个 I/O 操作中都要使用它们。

通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。一个 Buffer 实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中；同样地，从通道中读取的任何数据都要读到缓冲区中。

在本节中，您会了解到 NIO 中通道和缓冲区是如何工作的。

<strong>什么是缓冲区？</strong>

Buffer 是一个对象， 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer 对象，体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中，您将数据直接写入或者将数据直接读到 Stream 对象中。

在 NIO 库中，所有数据都是用缓冲区处理的。在读取数据时，它是直接读到缓冲区中的。在写入数据时，它是写入到缓冲区中的。任何时候访问 NIO 中的数据，您都是将它放到缓冲区中。

缓冲区实质上是一个数组。通常它是一个字节数组，但是也可以使用其他种类的数组。但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问，而且还可以跟踪系统的读/写进程。

<strong>缓冲区类型</strong>

最常用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。

ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上，对于每一种基本 Java 类型都有一种缓冲区类型：

• ByteBuffer

• CharBuffer

• ShortBuffer

• IntBuffer

• LongBuffer

• FloatBuffer

• DoubleBuffer

每一个 Buffer 类都是 Buffer 接口的一个实例。 除了 ByteBuffer，每一个 Buffer 类都有完全一样的操作，只是它们所处理的数据类型不一样。因为大多数标准 I/O 操作都使用 ByteBuffer，所以它具有所有共享的缓冲区操作以及一些特有的操作。

现在您可以花一点时间运行 UseFloatBuffer.java，它包含了类型化的缓冲区的一个应用例子。

<strong>什么是通道？</strong>

Channel是一个对象，可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较，通道就像是流。

正如前面提到的，所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中，相反，您是将数据写入包含一个或者多个字节的缓冲区。同样，您不会直接从通道中读取字节，而是将数据从通道读入缓冲区，再从缓冲区获取这个字节。

<strong>通道类型</strong>

通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类)， 而 通道 可以用于读、写或者同时用于读写。

因为它们是双向的，所以通道可以比流更好地反映底层操作系统的真实情况。特别是在 UNIX 模型中，底层操作系统通道是双向的。

<strong>◆ 从理论到实践：NIO 中的读和写</strong>

<strong>概  述</strong>

读和写是 I/O 的基本过程。从一个通道中读取很简单：只需创建一个缓冲区，然后让通道将数据读到这个缓冲区中。写入也相当简单：创建一个缓冲区，用数据填充它，然后让通道用这些数据来执行写入操作。

在本节中，我们将学习有关在 Java 程序中读取和写入数据的一些知识。我们将回顾 NIO 的主要组件(缓冲区、通道和一些相关的方法)，看看它们是如何交互以进行读写的。在接下来的几节中，我们将更详细地分析这其中的每个组件以及其交互。

<strong>从文件中读取</strong>

在我们第一个练习中，我们将从一个文件中读取一些数据。如果使用原来的 I/O，那么我们只需创建一个 FileInputStream 并从它那里读取。而在 NIO 中，情况稍有不同：我们首先从 FileInputStream 获取一个 FileInputStream 对象，然后使用这个通道来读取数据。

在 NIO 系统中，任何时候执行一个读操作，您都是从通道中读取，但是您不是 直接 从通道读取。因为所有数据最终都驻留在缓冲区中，所以您是从通道读到缓冲区中。

因此读取文件涉及三个步骤：(1) 从 FileInputStream 获取 Channel，(2) 创建 Buffer，(3) 将数据从 Channel 读到 Buffer 中。

现在，让我们看一下这个过程。

<strong>三个容易的步骤</strong>

第一步是获取通道。我们从 FileInputStream 获取通道：
<pre>
<ol>
	<li>FileInputStream fin = new FileInputStream( "readandshow.txt" );</li>
	<li>FileChannel fc = fin.getChannel();</li>
</ol>
</pre>
下一步是创建缓冲区：
<pre>
<ol>
	<li>ByteBuffer buffer = ByteBuffer.allocate( 1024 );</li>
</ol>
</pre>
最后，需要将数据从通道读到缓冲区中，如下所示：
<pre>
<ol>
	<li>fc.read( buffer );</li>
</ol>
</pre>
您会注意到，我们不需要告诉通道要读 多少数据 到缓冲区中。每一个缓冲区都有复杂的内部统计机制，它会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据

<strong>写入文件</strong>

在 NIO 中写入文件类似于从文件中读取。首先从 FileOutputStream 获取一个通道：
<pre>
<ol>
	<li>FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );</li>
	<li>FileChannel fc = fout.getChannel();</li>
</ol>
</pre>
下一步是创建一个缓冲区并在其中放入一些数据 - 在这里，数据将从一个名为 message 的数组中取出，这个数组包含字符串 "Some bytes" 的 ASCII 字节(本教程后面将会解释 buffer.flip() 和 buffer.put() 调用)。
<pre>
<ol>
	<li>ByteBuffer buffer = ByteBuffer.allocate( 1024 );</li>
	<li> for (int i=0; i&#60;message.length; ++i) {</li>
	<li>     buffer.put( message[i] );</li>
	<li>}</li>
	<li>buffer.flip();</li>
</ol>
</pre>
最后一步是写入缓冲区中
<pre>
<ol>
	<li>fc.write( buffer );</li>
</ol>
</pre>
注意在这里同样不需要告诉通道要写入多数据。缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入。

<strong>读写结合</strong>

下面我们将看一下在结合读和写时会有什么情况。我们以一个名为 CopyFile.java 的简单程序作为这个练习的基础，它将一个文件的所有内容拷贝到另一个文件中。CopyFile.java 执行三个基本操作：首先创建一个 Buffer，然后从源文件中将数据读到这个缓冲区中，然后将缓冲区写入目标文件。这个程序不断重复 ― 读、写、读、写 ― 直到源文件结束。

CopyFile 程序让您看到我们如何检查操作的状态，以及如何使用 clear() 和 flip() 方法重设缓冲区，并准备缓冲区以便将新读取的数据写到另一个通道中。

<strong>运行 CopyFile 例子</strong>

因为缓冲区会跟踪它自己的数据，所以 CopyFile 程序的内部循环 (inner loop) 非常简单，如下所示：
<pre>
<ol>
	<li>fcin.read( buffer );</li>
	<li>fcout.write( buffer );</li>
</ol>
</pre>
第一行将数据从输入通道 fcin 中读入缓冲区，第二行将这些数据写到输出通道 fcout 。

<strong>检查状态</strong>

下一步是检查拷贝何时完成。当没有更多的数据时，拷贝就算完成，并且可以在 read() 方法返回 -1 是判断这一点，如下所示：
<pre>
<ol>
	<li>int r = fcin.read( buffer );</li>
	<li> if (r==-1) {</li>
	<li>     break;</li>
	<li>}</li>
</ol>
</pre>
<strong>重设缓冲区</strong>

最后，在从输入通道读入缓冲区之前，我们调用 clear() 方法。同样，在将缓冲区写入输出通道之前，我们调用 flip() 方法，如下所示
<pre>
<ol>
	<li>buffer.clear();int r = fcin.read( buffer );</li>
	<li> if (r==-1) {</li>
	<li>     break;</li>
	<li>}</li>
	<li> buffer.flip();</li>
	<li>fcout.write( buffer );</li>
</ol>
</pre>
clear() 方法重设缓冲区，使它可以接受读入的数据。 flip() 方法让缓冲区可以将新读入的数据写入另一个通道。

<strong>◆  缓冲区内部细节</strong>

<strong>概  述</strong>

本节将介绍 NIO 中两个重要的缓冲区组件：状态变量和访问方法 (accessor)。

状态变量是前一节中提到的"内部统计机制"的关键。每一个读/写操作都会改变缓冲区的状态。通过记录和跟踪这些变化，缓冲区就可能够内部地管理自己的资源。

在从通道读取数据时，数据被放入到缓冲区。在有些情况下，可以将这个缓冲区直接写入另一个通道，但是在一般情况下，您还需要查看数据。这是使用 访问方法 get() 来完成的。同样，如果要将原始数据放入缓冲区中，就要使用访问方法 put()。

在本节中，您将学习关于 NIO 中的状态变量和访问方法的内容。我们将描述每一个组件，并让您有机会看到它的实际应用。虽然 NIO 的内部统计机制初看起来可能很复杂，但是您很快就会看到大部分的实际工作都已经替您完成了。您可能习惯于通过手工编码进行簿记 ― 即使用字节数组和索引变量，现在它已在 NIO 中内部地处理了。

<strong>状态变量</strong>

可以用三个值指定缓冲区在任意时刻的状态：

• position

• limit

• capacity

这三个变量一起可以跟踪缓冲区的状态和它所包含的数据。我们将在下面的小节中详细分析每一个变量，还要介绍它们如何适应典型的读/写(输入/输出)进程。在这个例子中，我们假定要将数据从一个输入通道拷贝到一个输出通道。

<strong>Position</strong>

您可以回想一下，缓冲区实际上就是美化了的数组。在从通道读取时，您将所读取的数据放到底层的数组中。 position 变量跟踪已经写了多少数据。更准确地说，它指定了下一个字节将放到数组的哪一个元素中。因此，如果您从通道中读三个字节到缓冲区中，那么缓冲区的 position 将会设置为3，指向数组中第四个元素。

同样，在写入通道时，您是从缓冲区中获取数据。 position 值跟踪从缓冲区中获取了多少数据。更准确地说，它指定下一个字节来自数组的哪一个元素。因此如果从缓冲区写了5个字节到通道中，那么缓冲区的 position 将被设置为5，指向数组的第六个元素。

<strong>Limit</strong>

limit 变量表明还有多少数据需要取出(在从缓冲区写入通道时)，或者还有多少空间可以放入数据(在从通道读入缓冲区时)。

position 总是小于或者等于 limit。

<strong>Capacity</strong>

缓冲区的 capacity 表明可以储存在缓冲区中的最大数据容量。实际上，它指定了底层数组的大小 ― 或者至少是指定了准许我们使用的底层数组的容量。

limit 决不能大于 capacity。

<strong>观察变量</strong>

我们首先观察一个新创建的缓冲区。出于本例子的需要，我们假设这个缓冲区的 总容量 为8个字节。 Buffer 的状态如下所示：

<a href="http://images.51cto.com/files/uploadimg/20111214/1127220.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127220.gif" border="0" alt="" width="381" height="58" /></a>

回想一下 ，limit 决不能大于 capacity，此例中这两个值都被设置为 8。我们通过将它们指向数组的尾部之后(如果有第8个槽，则是第8个槽所在的位置)来说明这点。

<a href="http://images.51cto.com/files/uploadimg/20111214/1127221.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127221.gif" border="0" alt="" width="441" height="226" /></a>

position 设置为0。如果我们读一些数据到缓冲区中，那么下一个读取的数据就进入 slot 0 。如果我们从缓冲区写一些数据，从缓冲区读取的下一个字节就来自 slot 0 。 position 设置如下所示：

<a href="http://images.51cto.com/files/uploadimg/20111214/1127222.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127222.gif" border="0" alt="" width="442" height="143" /></a>

由于 capacity 不会改变，所以我们在下面的讨论中可以忽略它。

<strong>第一次读取</strong>

现在我们可以开始在新创建的缓冲区上进行读/写操作。首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从 position 开始的位置，这时 position 被设置为 0。读完之后，position 就增加到 3，如下所示：

<a href="http://images.51cto.com/files/uploadimg/20111214/1127223.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127223.gif" border="0" alt="" width="430" height="124" /></a>

limit 没有改变。

<strong>第二次读取</strong>

在第二次读取时，我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由 position 所指定的位置上， position 因而增加 2：

<a href="http://images.51cto.com/files/uploadimg/20111214/1127224.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127224.gif" border="0" alt="" width="430" height="124" /></a>

limit 没有改变。

<strong>flip</strong>

现在我们要将数据写到输出通道中。在这之前，我们必须调用 flip() 方法。这个方法做两件非常重要的事：

1.它将 limit 设置为当前 position。

2.它将 position 设置为 0。

前一小节中的图显示了在 flip 之前缓冲区的情况。下面是在 flip 之后的缓冲区：

<a href="http://images.51cto.com/files/uploadimg/20111214/1127225.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127225.gif" border="0" alt="" width="433" height="124" /></a>

我们现在可以将数据从缓冲区写入通道了。 position 被设置为 0，这意味着我们得到的下一个字节是第一个字节。 limit 已被设置为原来的 position，这意味着它包括以前读到的所有字节，并且一个字节也不多。

<strong>第一次写入</strong>

在第一次写入时，我们从缓冲区中取四个字节并将它们写入输出通道。这使得 position 增加到 4，而 limit 不变，如下所示：

<a href="http://images.51cto.com/files/uploadimg/20111214/1127226.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127226.gif" border="0" alt="" width="430" height="124" /></a>

<strong>第二次写入</strong>

我们只剩下一个字节可写了。 limit在我们调用 flip() 时被设置为 5，并且 position 不能超过 limit。所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。这使得 position 增加到 5，并保持 limit 不变，如下所示：

<a href="http://images.51cto.com/files/uploadimg/20111214/1127227.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127227.gif" border="0" alt="" width="430" height="124" /></a>

<strong>clear</strong>

最后一步是调用缓冲区的 clear() 方法。这个方法重设缓冲区以便接收更多的字节。 Clear 做两种非常重要的事情：

1.它将 limit 设置为与 capacity 相同。

2.它设置 position 为 0。

下图显示了在调用 clear() 后缓冲区的状态：

<a href="http://images.51cto.com/files/uploadimg/20111214/1127228.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127228.gif" border="0" alt="" width="434" height="128" /></a>

缓冲区现在可以接收新的数据了。

<strong>访问方法</strong>

到目前为止，我们只是使用缓冲区将数据从一个通道转移到另一个通道。然而，程序经常需要直接处理数据。例如，您可能需要将用户数据保存到磁盘。在这种情况下，您必须将这些数据直接放入缓冲区，然后用通道将缓冲区写入磁盘。

或者，您可能想要从磁盘读取用户数据。在这种情况下，您要将数据从通道读到缓冲区中，然后检查缓冲区中的数据。

在本节的最后，我们将详细分析如何使用 ByteBuffer 类的 get() 和 put() 方法直接访问缓冲区中的数据。

<strong>get() 方法</strong>

ByteBuffer 类中有四个 get() 方法：

1.byte get();

2.ByteBuffer get( byte dst[] );

3.ByteBuffer get( byte dst[], int offset, int length );

4.byte get( int index );

第一个方法获取单个字节。第二和第三个方法将一组字节读到一个数组中。第四个方法从缓冲区中的特定位置获取字节。那些返回ByteBuffer 的方法只是返回调用它们的缓冲区的 this 值。

此外，我们认为前三个 get() 方法是相对的，而最后一个方法是绝对的。 相对 意味着 get() 操作服从 limit 和 position 值 ― 更明确地说，字节是从当前 position 读取的，而 position 在 get 之后会增加。另一方面，一个 绝对 方法会忽略 limit 和 position 值，也不会影响它们。事实上，它完全绕过了缓冲区的统计方法。

上面列出的方法对应于 ByteBuffer 类。其他类有等价的 get() 方法，这些方法除了不是处理字节外，其它方面是是完全一样的，它们处理的是与该缓冲区类相适应的类型。

<strong>put()方法</strong>

ByteBuffer 类中有五个 put() 方法：

1.ByteBuffer put( byte b );

2.ByteBuffer put( byte src[] );

3.ByteBuffer put( byte src[], int offset, int length );

4.ByteBuffer put( ByteBuffer src );

5.ByteBuffer put( int index, byte b );

第一个方法 写入（put） 单个字节。第二和第三个方法写入来自一个数组的一组字节。第四个方法将数据从一个给定的源ByteBuffer 写入这个 ByteBuffer。第五个方法将字节写入缓冲区中特定的 位置 。那些返回 ByteBuffer 的方法只是返回调用它们的缓冲区的 this 值。

与 get() 方法一样，我们将把 put() 方法划分为 相对 或者 绝对 的。前四个方法是相对的，而第五个方法是绝对的。

上面显示的方法对应于 ByteBuffer 类。其他类有等价的 put() 方法，这些方法除了不是处理字节之外，其它方面是完全一样的。它们处理的是与该缓冲区类相适应的类型。

<strong>类型化的 get() 和 put() 方法</strong>

除了前些小节中描述的 get() 和 put() 方法， ByteBuffer 还有用于读写不同类型的值的其他方法，如下所示：

• getByte()

• getChar()

• getShort()

• getInt()

• getLong()

• getFloat()

• getDouble()

• putByte()

• putChar()

• putShort()

• putInt()

• putLong()

• putFloat()

• putDouble()

事实上，这其中的每个方法都有两种类型 ― 一种是相对的，另一种是绝对的。它们对于读取格式化的二进制数据（如图像文件的头部）很有用。

您可以在例子程序 TypesInByteBuffer.java 中看到这些方法的实际应用。

<strong>缓冲区的使用：一个内部循环</strong>

下面的内部循环概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程。
<pre>
<ol>
	<li>while (true) {</li>
	<li>     buffer.clear();</li>
	<li>     int r = fcin.read( buffer );</li>
	<li>      if (r==-1) {</li>
	<li>       break;</li>
	<li>     }</li>
	<li>      buffer.flip();</li>
	<li>     fcout.write( buffer );}</li>
</ol>
</pre>
read() 和 write() 调用得到了极大的简化，因为许多工作细节都由缓冲区完成了。 clear() 和 flip() 方法用于让缓冲区在读和写之间切换。

<strong>◆  关于缓冲区的更多内容</strong>

<strong>概  述</strong>

到目前为止，您已经学习了使用缓冲区进行日常工作所需要掌握的大部分内容。我们的例子没怎么超出标准的读/写过程种类，在原来的 I/O 中可以像在 NIO 中一样容易地实现这样的标准读写过程。

本节将讨论使用缓冲区的一些更复杂的方面，比如缓冲区分配、包装和分片。我们还会讨论 NIO 带给 Java 平台的一些新功能。您将学到如何创建不同类型的缓冲区以达到不同的目的，如可保护数据不被修改的 只读 缓冲区，和直接映射到底层操作系统缓冲区的 直接 缓冲区。我们将在本节的最后介绍如何在 NIO 中创建内存映射文件。

<strong>缓冲区分配和包装</strong>

在能够读和写之前，必须有一个缓冲区。要创建缓冲区，您必须 分配 它。我们使用静态方法 allocate() 来分配缓冲区：
<pre>
<ol>
	<li>ByteBuffer buffer = ByteBuffer.allocate( 1024 );</li>
</ol>
</pre>
allocate() 方法分配一个具有指定大小的底层数组，并将它包装到一个缓冲区对象中 ― 在本例中是一个 ByteBuffer。

您还可以将一个现有的数组转换为缓冲区，如下所示：
<pre>
<ol>
	<li>byte array[] = new byte[1024];</li>
	<li>ByteBuffer buffer = ByteBuffer.wrap( array );</li>
</ol>
</pre>
本例使用了 wrap() 方法将一个数组包装为缓冲区。必须非常小心地进行这类操作。一旦完成包装，底层数据就可以通过缓冲区或者直接访问。

<strong>缓冲区分片</strong>

slice() 方法根据现有的缓冲区创建一种 子缓冲区 。也就是说，它创建一个新的缓冲区，新缓冲区与原来的缓冲区的一部分共享数据。

使用例子可以最好地说明这点。让我们首先创建一个长度为 10 的 ByteBuffer：
<pre>
<ol>
	<li>ByteBuffer buffer = ByteBuffer.allocate( 10 )</li>
</ol>
</pre>
然后使用数据来填充这个缓冲区，在第 n 个槽中放入数字 n：
<pre>
<ol>
	<li>for (int i=0; i&#60;buffer.capacity(); ++i) {</li>
	<li>     buffer.put( (byte)i );</li>
	<li>}</li>
</ol>
</pre>
现在我们对这个缓冲区 分片 ，以创建一个包含槽 3 到槽 6 的子缓冲区。在某种意义上，子缓冲区就像原来的缓冲区中的一个 窗口 。

窗口的起始和结束位置通过设置 position 和 limit 值来指定，然后调用 Buffer 的 slice() 方法：
<pre>
<ol>
	<li>buffer.position( 3 );</li>
	<li>buffer.limit( 7 );</li>
	<li>ByteBuffer slice = buffer.slice();</li>
</ol>
</pre>
片 是缓冲区的 子缓冲区 。不过， 片段 和 缓冲区 共享同一个底层数据数组，我们在下一节将会看到这一点。

<strong>缓冲区份片和数据共享</strong>

我们已经创建了原缓冲区的子缓冲区，并且我们知道缓冲区和子缓冲区共享同一个底层数据数组。让我们看看这意味着什么。

我们遍历子缓冲区，将每一个元素乘以 11 来改变它。例如，5 会变成 55。
<pre>
<ol>
	<li>for (int i=0; i&#60;slice.capacity(); ++i) {</li>
	<li>     byte b = slice.get( i );</li>
	<li>     b *= 11;</li>
	<li>     slice.put( i, b );</li>
	<li>}</li>
</ol>
</pre>
最后，再看一下原缓冲区中的内容：
<pre>
<ol>
	<li>buffer.position( 0 );</li>
	<li>buffer.limit( buffer.capacity() );</li>
	<li> while (buffer.remaining()&#62;0) {</li>
	<li>     System.out.println( buffer.get() );</li>
	<li>}</li>
</ol>
</pre>
结果表明只有在子缓冲区窗口中的元素被改变了：
<pre>$ java SliceBuffer 
0 
1 
2 
33 
44 
55 
66 
7 
8 
9</pre>
缓冲区片对于促进抽象非常有帮助。可以编写自己的函数处理整个缓冲区，而且如果想要将这个过程应用于子缓冲区上，您只需取主缓冲区的一个片，并将它传递给您的函数。这比编写自己的函数来取额外的参数以指定要对缓冲区的哪一部分进行操作更容易。

<strong>只读缓冲区</strong>

只读缓冲区非常简单 ― 您可以读取它们，但是不能向它们写入。可以通过调用缓冲区的 asReadOnlyBuffer() 方法，将任何常规缓冲区转换为只读缓冲区，这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据)，只不过它是只读的。

只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时，您无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以 保证 该缓冲区不会被修改。

不能将只读的缓冲区转换为可写的缓冲区。

<strong>直接和间接缓冲区</strong>

另一种有用的 ByteBuffer 是直接缓冲区。 直接缓冲区 是为加快 I/O 速度，而以一种特殊的方式分配其内存的缓冲区。

实际上，直接缓冲区的准确定义是与实现相关的。Sun 的文档是这样描述直接缓冲区的：

给定一个直接字节缓冲区，Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说，它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后)，尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。

您可以在例子程序 FastCopyFile.java 中看到直接缓冲区的实际应用，这个程序是 CopyFile.java 的另一个版本，它使用了直接缓冲区以提高速度。

还可以用内存映射文件创建直接缓冲区。

<strong>内存映射文件 I/O</strong>

内存映射文件 I/O 是一种读和写文件数据的方法，它可以比常规的基于流或者基于通道的 I/O 快得多。

内存映射文件 I/O 是通过使文件中的数据神奇般地出现为内存数组的内容来完成的。这其初听起来似乎不过就是将整个文件读到内存中，但是事实上并不是这样。一般来说，只有文件中实际读取或者写入的部分才会送入（或者 映射 ）到内存中。

内存映射并不真的神奇或者多么不寻常。现代操作系统一般根据需要将文件的部分映射为内存的部分，从而实现文件系统。Java 内存映射机制不过是在底层操作系统中可以采用这种机制时，提供了对该机制的访问。

尽管创建内存映射文件相当简单，但是向它写入可能是危险的。仅只是改变数组的单个元素这样的简单操作，就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。

<strong>将文件映射到内存</strong>

了解内存映射的最好方法是使用例子。在下面的例子中，我们要将一个 FileChannel (它的全部或者部分)映射到内存中。为此我们将使用 FileChannel.map() 方法。下面代码行将文件的前 1024 个字节映射到内存中：
<pre>
<ol>
	<li>MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE,     0, 1024 );</li>
</ol>
</pre>
map() 方法返回一个 MappedByteBuffer，它是 ByteBuffer 的子类。因此，您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区，操作系统会在需要时负责执行行映射。

<strong>◆  分散和聚集</strong>

<strong>概  述</strong>

分散/聚集 I/O 是使用多个而不是单个缓冲区来保存数据的读写方法。

一个分散的读取就像一个常规通道读取，只不过它是将数据读到一个缓冲区数组中而不是读到单个缓冲区中。同样地，一个聚集写入是向缓冲区数组而不是向单个缓冲区写入数据。

分散/聚集 I/O 对于将数据流划分为单独的部分很有用，这有助于实现复杂的数据格式。

<strong>分散/聚集 I/O</strong>

通道可以有选择地实现两个新的接口： ScatteringByteChannel 和 GatheringByteChannel。一个 ScatteringByteChannel 是一个具有两个附加读方法的通道：

• long read( ByteBuffer[] dsts );

• long read( ByteBuffer[] dsts, int offset, int length );

这些 long read() 方法很像标准的 read 方法，只不过它们不是取单个缓冲区而是取一个缓冲区数组。

在 分散读取 中，通道依次填充每个缓冲区。填满一个缓冲区后，它就开始填充下一个。在某种意义上，缓冲区数组就像一个大缓冲区。

<strong>分散/聚集的应用</strong>

分散/聚集 I/O 对于将数据划分为几个部分很有用。例如，您可能在编写一个使用消息对象的网络应用程序，每一个消息被划分为固定长度的头部和固定长度的正文。您可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容难正文的缓冲区。当您将它们放入一个数组中并使用分散读取来向它们读入消息时，头部和正文将整齐地划分到这两个缓冲区中。

我们从缓冲区所得到的方便性对于缓冲区数组同样有效。因为每一个缓冲区都跟踪自己还可以接受多少数据，所以分散读取会自动找到有空间接受数据的第一个缓冲区。在这个缓冲区填满后，它就会移动到下一个缓冲区。

<strong>聚集写入</strong>

聚集写入 类似于分散读取，只不过是用来写入。它也有接受缓冲区数组的方法：

• long write( ByteBuffer[] srcs );

• long write( ByteBuffer[] srcs, int offset, int length );

聚集写对于把一组单独的缓冲区中组成单个数据流很有用。为了与上面的消息例子保持一致，您可以使用聚集写入来自动将网络消息的各个部分组装为单个数据流，以便跨越网络传输消息。

从例子程序 UseScatterGather.java 中可以看到分散读取和聚集写入的实际应用。

<strong>◆  文件锁定</strong>

<strong>概  述</strong>

文件锁定初看起来可能让人迷惑。它 似乎 指的是防止程序或者用户访问特定文件。事实上，文件锁就像常规的 Java 对象锁 ― 它们是 劝告式的（advisory） 锁。它们不阻止任何形式的数据访问，相反，它们通过锁的共享和获取赖允许系统的不同部分相互协调。

您可以锁定整个文件或者文件的一部分。如果您获取一个排它锁，那么其他人就不能获得同一个文件或者文件的一部分上的锁。如果您获得一个共享锁，那么其他人可以获得同一个文件或者文件一部分上的共享锁，但是不能获得排它锁。文件锁定并不总是出于保护数据的目的。例如，您可能临时锁定一个文件以保证特定的写操作成为原子的，而不会有其他程序的干扰。

大多数操作系统提供了文件系统锁，但是它们并不都是采用同样的方式。有些实现提供了共享锁，而另一些仅提供了排它锁。事实上，有些实现使得文件的锁定部分不可访问，尽管大多数实现不是这样的。

在本节中，您将学习如何在 NIO 中执行简单的文件锁过程，我们还将探讨一些保证被锁定的文件尽可能可移植的方法。

<strong>锁定文件</strong>

要获取文件的一部分上的锁，您要调用一个打开的 FileChannel 上的 lock() 方法。注意，如果要获取一个排它锁，您必须以写方式打开文件。
<pre>
<ol>
	<li>RandomAccessFile raf = new RandomAccessFile( "usefilelocks.txt", "rw" );</li>
	<li>FileChannel fc = raf.getChannel();</li>
	<li>FileLock lock = fc.lock( start, end, false );</li>
</ol>
</pre>
在拥有锁之后，您可以执行需要的任何敏感操作，然后再释放锁：
<pre>
<ol>
	<li>lock.release();</li>
</ol>
</pre>
在释放锁后，尝试获得锁的其他任何程序都有机会获得它。

本小节的例子程序 UseFileLocks.java 必须与它自己并行运行。这个程序获取一个文件上的锁，持有三秒钟，然后释放它。如果同时运行这个程序的多个实例，您会看到每个实例依次获得锁。

文件锁定可能是一个复杂的操作，特别是考虑到不同的操作系统是以不同的方式实现锁这一事实。下面的指导原则将帮助您尽可能保持代码的可移植性：

• 只使用排它锁。

• 将所有的锁视为劝告式的（advisory）。

<strong>◆  连网和异步 I/O</strong>

<strong>概  述</strong>

连网是学习异步 I/O 的很好基础，而异步 I/O 对于在 Java 语言中执行任何输入/输出过程的人来说，无疑都是必须具备的知识。NIO 中的连网与 NIO 中的其他任何操作没有什么不同 ― 它依赖通道和缓冲区，而您通常使用 InputStream 和 OutputStream 来获得通道。

本节首先介绍异步 I/O 的基础 ― 它是什么以及它不是什么，然后转向更实用的、程序性的例子。

<strong>异步 I/O</strong>

异步 I/O 是一种 没有阻塞地 读写数据的方法。通常，在代码进行 read() 调用时，代码会阻塞直至有可供读取的数据。同样，write() 调用将会阻塞直至数据能够写入。

另一方面，异步 I/O 调用不会阻塞。相反，您将注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、新的套接字连接，等等，而在发生这样的事件时，系统将会告诉您。

异步 I/O 的一个优势在于，它允许您同时根据大量的输入和输出执行 I/O。同步程序常常要求助于轮询，或者创建许许多多的线程以处理大量的连接。使用异步 I/O，您可以监听任何数量的通道上的事件，不用轮询，也不用额外的线程。

我们将通过研究一个名为 MultiPortEcho.java 的例子程序来查看异步 I/O 的实际应用。这个程序就像传统的 echo server，它接受网络连接并向它们回响它们可能发送的数据。不过它有一个附加的特性，就是它能同时监听多个端口，并处理来自所有这些端口的连接。并且它只在单个线程中完成所有这些工作。

<strong>Selectors</strong>

本节的阐述对应于 MultiPortEcho 的源代码中的 go() 方法的实现，因此应该看一下源代码，以便对所发生的事情有个更全面的了解。

异步 I/O 中的核心对象名为 Selector。Selector 就是您注册对各种 I/O 事件的兴趣的地方，而且当那些事件发生时，就是这个对象告诉您所发生的事件。

所以，我们需要做的第一件事就是创建一个 Selector：
<pre>
<ol>
	<li>Selector selector = Selector.open();</li>
</ol>
</pre>
然后，我们将对不同的通道对象调用 register() 方法，以便注册我们对这些对象中发生的 I/O 事件的兴趣。register() 的第一个参数总是这个 Selector。

<strong>打开一个 ServerSocketChannel</strong>

为了接收连接，我们需要一个 ServerSocketChannel。事实上，我们要监听的每一个端口都需要有一个 ServerSocketChannel 。对于每一个端口，我们打开一个 ServerSocketChannel，如下所示：
<pre>
<ol>
	<li>ServerSocketChannel ssc = ServerSocketChannel.open();</li>
	<li>ssc.configureBlocking( false );</li>
	<li>ServerSocket ss = ssc.socket();</li>
	<li>InetSocketAddress address = new InetSocketAddress( ports[i] );</li>
	<li>ss.bind( address );</li>
</ol>
</pre>
第一行创建一个新的 ServerSocketChannel ，最后三行将它绑定到给定的端口。第二行将 ServerSocketChannel 设置为 非阻塞的 。我们必须对每一个要使用的套接字通道调用这个方法，否则异步 I/O 就不能工作。

<strong>选择键</strong>

下一步是将新打开的 ServerSocketChannels 注册到 Selector上。为此我们使用 ServerSocketChannel.register() 方法，如下所示
<pre>
<ol>
	<li>SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );</li>
</ol>
</pre>
register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT，这里它指定我们想要监听 accept 事件，也就是在新的连接建立时所发生的事件。这是适用于 ServerSocketChannel 的唯一事件类型。

请注意对 register() 的调用的返回值。 SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时，它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。

<strong>内部循环</strong>

现在已经注册了我们对一些 I/O 事件的兴趣，下面将进入主循环。使用 Selectors 的几乎每个程序都像下面这样使用内部循环：
<pre>
<ol>
	<li>int num = selector.select();</li>
	<li>Set selectedKeys = selector.selectedKeys();</li>
	<li>Iterator it = selectedKeys.iterator();</li>
	<li>while (it.hasNext()) {</li>
	<li>     SelectionKey key = (SelectionKey)it.next();</li>
	<li>     // ... deal with I/O event ...}</li>
</ol>
</pre>
首先，我们调用 Selector 的 select() 方法。这个方法会阻塞，直到至少有一个已注册的事件发生。当一个或者更多的事件发生时，select() 方法将返回所发生的事件的数量。

接下来，我们调用 Selector 的 selectedKeys() 方法，它返回发生了事件的 SelectionKey 对象的一个 集合。

我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey，您必须确定发生的是什么 I/O 事件，以及这个事件影响哪些 I/O 对象。

<strong>监听新连接</strong>

程序执行到这里，我们仅注册了 ServerSocketChannel，并且仅注册它们“接收”事件。为确认这一点，我们对 SelectionKey 调用readyOps() 方法，并检查发生了什么类型的事件：
<pre>
<ol>
	<li>if ((key.readyOps() &#38; SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {</li>
	<li>        // Accept the new connection</li>
	<li>       // ...</li>
	<li>}</li>
</ol>
</pre>
可以肯定地说， readOps() 方法告诉我们该事件是新的连接。

<strong>接受新的连接</strong>

因为我们知道这个服务器套接字上有一个传入连接在等待，所以可以安全地接受它；也就是说，不用担心 accept() 操作会阻塞：
<pre>
<ol>
	<li>ServerSocketChannel ssc = (ServerSocketChannel)key.channel();</li>
	<li>SocketChannel sc = ssc.accept();</li>
</ol>
</pre>
下一步是将新连接的 SocketChannel 配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据，所以我们还必须将 SocketChannel 注册到 Selector上，如下所示：
<pre>
<ol>
	<li>sc.configureBlocking( false );</li>
	<li>SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );</li>
</ol>
</pre>
注意我们使用 register() 的 OP_READ 参数，将 SocketChannel 注册用于 读取 而不是 接受 新连接。

<strong>删除处理过的 SelectionKey</strong>

在处理 SelectionKey 之后，我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键，那么它仍然会在主集合中以一个激活的键出现，这会导致我们尝试再次处理它。我们调用迭代器的remove() 方法来删除处理过的 SelectionKey：
<pre>
<ol>
	<li>it.remove();</li>
</ol>
</pre>
现在我们可以返回主循环并接受从一个套接字中传入的数据(或者一个传入的 I/O 事件)了。

<strong>传入的 I/O</strong>

当来自一个套接字的数据到达时，它会触发一个 I/O 事件。这会导致在主循环中调用 Selector.select()，并返回一个或者多个 I/O 事件。这一次， SelectionKey 将被标记为 OP_READ 事件，如下所示：
<pre>
<ol>
	<li>} else if ((key.readyOps() &#38; SelectionKey.OP_READ)     == SelectionKey.OP_READ) {</li>
	<li>     // Read the data</li>
	<li>     SocketChannel sc = (SocketChannel)key.channel();</li>
	<li>     // ...}</li>
</ol>
</pre>
与以前一样，我们取得发生 I/O 事件的通道并处理它。在本例中，由于这是一个 echo server，我们只希望从套接字中读取数据并马上将它发送回去。

每次返回主循环，我们都要调用 select 的 Selector()方法，并取得一组 SelectionKey。每个键代表一个 I/O 事件。我们处理事件，从选定的键集中删除 SelectionKey，然后返回主循环的顶部。

这个程序有点过于简单，因为它的目的只是展示异步 I/O 所涉及的技术。在现实的应用程序中，您需要通过将通道从 Selector 中删除来处理关闭的通道。而且您可能要使用多个线程。这个程序可以仅使用一个线程，因为它只是一个演示，但是在现实场景中，创建一个线程池来负责 I/O 事件处理中的耗时部分会更有意义。

<strong>字符集</strong>

根据 Sun 的文档，一个 Charset 是“十六位 Unicode 字符序列与字节序列之间的一个命名的映射”。实际上，一个 Charset 允许您以尽可能最具可移植性的方式读写字符序列。

Java 语言被定义为基于 Unicode。然而在实际上，许多人编写代码时都假设一个字符在磁盘上或者在网络流中用一个字节表示。这种假设在许多情况下成立，但是并不是在所有情况下都成立，而且随着计算机变得对 Unicode 越来越友好，这个假设就日益变得不能成立了。

在本节中，我们将看一下如何使用 Charsets 以适合现代文本格式的方式处理文本数据。这里将使用的示例程序相当简单，不过，它触及了使用 Charset 的所有关键方面：为给定的字符编码创建 Charset，以及使用该 Charset 解码和编码文本数据。

<strong>编码/解码</strong>

要读和写文本，我们要分别使用 CharsetDecoder 和 CharsetEncoder。将它们称为 编码器 和 解码器 是有道理的。一个 字符 不再表示一个特定的位模式，而是表示字符系统中的一个实体。因此，由某个实际的位模式表示的字符必须以某种特定的 编码 来表示。

CharsetDecoder 用于将逐位表示的一串字符转换为具体的 char 值。同样，一个 CharsetEncoder 用于将字符转换回位。

在下一个小节中，我们将考察一个使用这些对象来读写数据的程序。

<strong>处理文本的正确方式</strong>

现在我们将分析这个例子程序 UseCharsets.java。这个程序非常简单 ― 它从一个文件中读取一些文本，并将该文本写入另一个文件。但是它把该数据当作文本数据，并使用 CharBuffer 来将该数句读入一个 CharsetDecoder 中。同样，它使用 CharsetEncoder 来写回该数据。

我们将假设字符以 ISO-8859-1(Latin1) 字符集（这是 ASCII 的标准扩展）的形式储存在磁盘上。尽管我们必须为使用 Unicode 做好准备，但是也必须认识到不同的文件是以不同的格式储存的，而 ASCII 无疑是非常普遍的一种格式。事实上，每种 Java 实现都要求对以下字符编码提供完全的支持：

• US-ASCII

• ISO-8859-1

• UTF-8

• UTF-16BE

• UTF-16LE

• UTF-16

<strong>示例程序</strong>

在打开相应的文件、将输入数据读入名为 inputData 的 ByteBuffer 之后，我们的程序必须创建 ISO-8859-1 (Latin1) 字符集的一个实例：
<pre>
<ol>
	<li>Charset latin1 = Charset.forName( "ISO-8859-1" );</li>
</ol>
</pre>
然后，创建一个解码器（用于读取）和一个编码器 （用于写入）：
<pre>
<ol>
	<li>CharsetDecoder decoder = latin1.newDecoder();</li>
	<li>CharsetEncoder encoder = latin1.newEncoder();</li>
</ol>
</pre>
为了将字节数据解码为一组字符，我们把 ByteBuffer 传递给 CharsetDecoder，结果得到一个 CharBuffer
<pre>
<ol>
	<li>CharBuffer cb = decoder.decode( inputData );</li>
</ol>
</pre>
如果想要处理字符，我们可以在程序的此处进行。但是我们只想无改变地将它写回，所以没有什么要做的。

要写回数据，我们必须使用 CharsetEncoder 将它转换回字节：
<pre>
<ol>
	<li>ByteBuffer outputData = encoder.encode( cb );</li>
</ol>
</pre>
在转换完成之后，我们就可以将数据写到文件中了。

结束语和参考资料

<strong>结束语</strong>

正如您所看到的， NIO 库有大量的特性。在一些新特性（例如文件锁定和字符集）提供新功能的同时，许多特性在优化方面也非常优秀。

在基础层次上，通道和缓冲区可以做的事情几乎都可以用原来的面向流的类来完成。但是通道和缓冲区允许以 快得多 的方式完成这些相同的旧操作 ― 事实上接近系统所允许的最大速度。

不过 NIO 最强大的长度之一在于，它提供了一种在 Java 语言中执行进行输入/输出的新的（也是迫切需要的）结构化方式。随诸如缓冲区、通道和异步 I/O 这些概念性（且可实现的）实体而来的，是我们重新思考 Java 程序中的 I/O过程的机会。这样，NIO 甚至为我们最熟悉的 I/O 过程也带来了新的活力，同时赋予我们通过和以前不同并且更好的方式执行它们的机会。

&#160;

&#160;

&#160;

&#160;]]></description>
			<content:encoded><![CDATA[<p><strong>本文简介：</strong> JDK 1.4 中引入的新输入输出 (NIO) 库在标准 Java 代码中提供了高速的、面向块的 I/O。本实用教程从高级概念到底层的编程细节，非常详细地介绍了 NIO 库。您将学到诸如缓冲区和通道这样的关键 I/O 元素的知识，并考察更新后的库中的标准 I/O 是如何工作的。您还将了解只能通过 NIO 来完成的工作，如异步 I/O 和直接缓冲区。</p>
<p><strong>◆  输入/输出：概念性描述</strong></p>
<p><strong>I/O 简介</strong></p>
<p>I/O ? 或者输入/输出 ? 指的是计算机与外部世界或者一个程序与计算机的其余部分的之间的接口。它对于任何计算机系统都非常关键，因而所有 I/O 的主体实际上是内置在操作系统中的。单独的程序一般是让系统为它们完成大部分的工作。</p>
<p>在 Java 编程中，直到最近一直使用 流 的方式完成 I/O。所有 I/O 都被视为单个的字节的移动，通过一个称为 Stream 的对象一次移动一个字节。流 I/O 用于与外部世界接触。它也在内部使用，用于将对象转换为字节，然后再转换回对象。</p>
<p>NIO 与原来的 I/O 有同样的作用和目的，但是它使用不同的方式? 块 I/O。正如您将在本教程中学到的，块 I/O 的效率可以比流 I/O 高许多。</p>
<p><strong>为什么要使用 NIO?</strong></p>
<p>NIO 的创建目的是为了让 Java 程序员可以实现高速 I/O 而无需编写自定义的本机代码。NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统，因而可以极大地提高速度。</p>
<p><strong>流与块的比较</strong></p>
<p>原来的 I/O 库(在 java.io.*中) 与 NIO 最重要的区别是数据打包和传输的方式。正如前面提到的，原来的 I/O 以流的方式处理数据，而 NIO 以块的方式处理数据。</p>
<p>面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据，一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器，以便每个过滤器只负责单个复杂处理机制的一部分，这样也是相对简单的。不利的一面是，面向流的 I/O 通常相当慢。</p>
<p>一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。</p>
<p><strong>集成的 I/O</strong></p>
<p>在 JDK 1.4 中原来的 I/O 包和 NIO 已经很好地集成了。 java.io.* 已经以 NIO 为基础重新实现了，所以现在它可以利用 NIO 的一些特性。例如， java.io.* 包中的一些类包含以块的形式读写数据的方法，这使得即使在更面向流的系统中，处理速度也会更快。</p>
<p>也可以用 NIO 库实现标准 I/O 功能。例如，可以容易地使用块 I/O 一次一个字节地移动数据。但是正如您会看到的，NIO 还提供了原 I/O 包中所没有的许多好处。</p>
<p><strong>◆ 通道和缓冲区</strong></p>
<p><strong>概  述</strong></p>
<p>通道 和 缓冲区 是 NIO 中的核心对象，几乎在每一个 I/O 操作中都要使用它们。</p>
<p>通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。一个 Buffer 实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中；同样地，从通道中读取的任何数据都要读到缓冲区中。</p>
<p>在本节中，您会了解到 NIO 中通道和缓冲区是如何工作的。</p>
<p><strong>什么是缓冲区？</strong></p>
<p>Buffer 是一个对象， 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer 对象，体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中，您将数据直接写入或者将数据直接读到 Stream 对象中。</p>
<p>在 NIO 库中，所有数据都是用缓冲区处理的。在读取数据时，它是直接读到缓冲区中的。在写入数据时，它是写入到缓冲区中的。任何时候访问 NIO 中的数据，您都是将它放到缓冲区中。</p>
<p>缓冲区实质上是一个数组。通常它是一个字节数组，但是也可以使用其他种类的数组。但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问，而且还可以跟踪系统的读/写进程。</p>
<p><strong>缓冲区类型</strong></p>
<p>最常用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。</p>
<p>ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上，对于每一种基本 Java 类型都有一种缓冲区类型：</p>
<p>• ByteBuffer</p>
<p>• CharBuffer</p>
<p>• ShortBuffer</p>
<p>• IntBuffer</p>
<p>• LongBuffer</p>
<p>• FloatBuffer</p>
<p>• DoubleBuffer</p>
<p>每一个 Buffer 类都是 Buffer 接口的一个实例。 除了 ByteBuffer，每一个 Buffer 类都有完全一样的操作，只是它们所处理的数据类型不一样。因为大多数标准 I/O 操作都使用 ByteBuffer，所以它具有所有共享的缓冲区操作以及一些特有的操作。</p>
<p>现在您可以花一点时间运行 UseFloatBuffer.java，它包含了类型化的缓冲区的一个应用例子。</p>
<p><strong>什么是通道？</strong></p>
<p>Channel是一个对象，可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较，通道就像是流。</p>
<p>正如前面提到的，所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中，相反，您是将数据写入包含一个或者多个字节的缓冲区。同样，您不会直接从通道中读取字节，而是将数据从通道读入缓冲区，再从缓冲区获取这个字节。</p>
<p><strong>通道类型</strong></p>
<p>通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类)， 而 通道 可以用于读、写或者同时用于读写。</p>
<p>因为它们是双向的，所以通道可以比流更好地反映底层操作系统的真实情况。特别是在 UNIX 模型中，底层操作系统通道是双向的。</p>
<p><strong>◆ 从理论到实践：NIO 中的读和写</strong></p>
<p><strong>概  述</strong></p>
<p>读和写是 I/O 的基本过程。从一个通道中读取很简单：只需创建一个缓冲区，然后让通道将数据读到这个缓冲区中。写入也相当简单：创建一个缓冲区，用数据填充它，然后让通道用这些数据来执行写入操作。</p>
<p>在本节中，我们将学习有关在 Java 程序中读取和写入数据的一些知识。我们将回顾 NIO 的主要组件(缓冲区、通道和一些相关的方法)，看看它们是如何交互以进行读写的。在接下来的几节中，我们将更详细地分析这其中的每个组件以及其交互。</p>
<p><strong>从文件中读取</strong></p>
<p>在我们第一个练习中，我们将从一个文件中读取一些数据。如果使用原来的 I/O，那么我们只需创建一个 FileInputStream 并从它那里读取。而在 NIO 中，情况稍有不同：我们首先从 FileInputStream 获取一个 FileInputStream 对象，然后使用这个通道来读取数据。</p>
<p>在 NIO 系统中，任何时候执行一个读操作，您都是从通道中读取，但是您不是 直接 从通道读取。因为所有数据最终都驻留在缓冲区中，所以您是从通道读到缓冲区中。</p>
<p>因此读取文件涉及三个步骤：(1) 从 FileInputStream 获取 Channel，(2) 创建 Buffer，(3) 将数据从 Channel 读到 Buffer 中。</p>
<p>现在，让我们看一下这个过程。</p>
<p><strong>三个容易的步骤</strong></p>
<p>第一步是获取通道。我们从 FileInputStream 获取通道：</p>
<pre>
<ol>
<li>FileInputStream fin = new FileInputStream( "readandshow.txt" );</li>
<li>FileChannel fc = fin.getChannel();</li>
</ol>
</pre>
<p>下一步是创建缓冲区：</p>
<pre>
<ol>
<li>ByteBuffer buffer = ByteBuffer.allocate( 1024 );</li>
</ol>
</pre>
<p>最后，需要将数据从通道读到缓冲区中，如下所示：</p>
<pre>
<ol>
<li>fc.read( buffer );</li>
</ol>
</pre>
<p>您会注意到，我们不需要告诉通道要读 多少数据 到缓冲区中。每一个缓冲区都有复杂的内部统计机制，它会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据</p>
<p><strong>写入文件</strong></p>
<p>在 NIO 中写入文件类似于从文件中读取。首先从 FileOutputStream 获取一个通道：</p>
<pre>
<ol>
<li>FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );</li>
<li>FileChannel fc = fout.getChannel();</li>
</ol>
</pre>
<p>下一步是创建一个缓冲区并在其中放入一些数据 &#8211; 在这里，数据将从一个名为 message 的数组中取出，这个数组包含字符串 “Some bytes” 的 ASCII 字节(本教程后面将会解释 buffer.flip() 和 buffer.put() 调用)。</p>
<pre>
<ol>
<li>ByteBuffer buffer = ByteBuffer.allocate( 1024 );</li>
<li> for (int i=0; i&lt;message.length; ++i) {</li>
<li>     buffer.put( message[i] );</li>
<li>}</li>
<li>buffer.flip();</li>
</ol>
</pre>
<p>最后一步是写入缓冲区中</p>
<pre>
<ol>
<li>fc.write( buffer );</li>
</ol>
</pre>
<p>注意在这里同样不需要告诉通道要写入多数据。缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入。</p>
<p><strong>读写结合</strong></p>
<p>下面我们将看一下在结合读和写时会有什么情况。我们以一个名为 CopyFile.java 的简单程序作为这个练习的基础，它将一个文件的所有内容拷贝到另一个文件中。CopyFile.java 执行三个基本操作：首先创建一个 Buffer，然后从源文件中将数据读到这个缓冲区中，然后将缓冲区写入目标文件。这个程序不断重复 ― 读、写、读、写 ― 直到源文件结束。</p>
<p>CopyFile 程序让您看到我们如何检查操作的状态，以及如何使用 clear() 和 flip() 方法重设缓冲区，并准备缓冲区以便将新读取的数据写到另一个通道中。</p>
<p><strong>运行 CopyFile 例子</strong></p>
<p>因为缓冲区会跟踪它自己的数据，所以 CopyFile 程序的内部循环 (inner loop) 非常简单，如下所示：</p>
<pre>
<ol>
<li>fcin.read( buffer );</li>
<li>fcout.write( buffer );</li>
</ol>
</pre>
<p>第一行将数据从输入通道 fcin 中读入缓冲区，第二行将这些数据写到输出通道 fcout 。</p>
<p><strong>检查状态</strong></p>
<p>下一步是检查拷贝何时完成。当没有更多的数据时，拷贝就算完成，并且可以在 read() 方法返回 -1 是判断这一点，如下所示：</p>
<pre>
<ol>
<li>int r = fcin.read( buffer );</li>
<li> if (r==-1) {</li>
<li>     break;</li>
<li>}</li>
</ol>
</pre>
<p><strong>重设缓冲区</strong></p>
<p>最后，在从输入通道读入缓冲区之前，我们调用 clear() 方法。同样，在将缓冲区写入输出通道之前，我们调用 flip() 方法，如下所示</p>
<pre>
<ol>
<li>buffer.clear();int r = fcin.read( buffer );</li>
<li> if (r==-1) {</li>
<li>     break;</li>
<li>}</li>
<li> buffer.flip();</li>
<li>fcout.write( buffer );</li>
</ol>
</pre>
<p>clear() 方法重设缓冲区，使它可以接受读入的数据。 flip() 方法让缓冲区可以将新读入的数据写入另一个通道。</p>
<p><strong>◆  缓冲区内部细节</strong></p>
<p><strong>概  述</strong></p>
<p>本节将介绍 NIO 中两个重要的缓冲区组件：状态变量和访问方法 (accessor)。</p>
<p>状态变量是前一节中提到的”内部统计机制”的关键。每一个读/写操作都会改变缓冲区的状态。通过记录和跟踪这些变化，缓冲区就可能够内部地管理自己的资源。</p>
<p>在从通道读取数据时，数据被放入到缓冲区。在有些情况下，可以将这个缓冲区直接写入另一个通道，但是在一般情况下，您还需要查看数据。这是使用 访问方法 get() 来完成的。同样，如果要将原始数据放入缓冲区中，就要使用访问方法 put()。</p>
<p>在本节中，您将学习关于 NIO 中的状态变量和访问方法的内容。我们将描述每一个组件，并让您有机会看到它的实际应用。虽然 NIO 的内部统计机制初看起来可能很复杂，但是您很快就会看到大部分的实际工作都已经替您完成了。您可能习惯于通过手工编码进行簿记 ― 即使用字节数组和索引变量，现在它已在 NIO 中内部地处理了。</p>
<p><strong>状态变量</strong></p>
<p>可以用三个值指定缓冲区在任意时刻的状态：</p>
<p>• position</p>
<p>• limit</p>
<p>• capacity</p>
<p>这三个变量一起可以跟踪缓冲区的状态和它所包含的数据。我们将在下面的小节中详细分析每一个变量，还要介绍它们如何适应典型的读/写(输入/输出)进程。在这个例子中，我们假定要将数据从一个输入通道拷贝到一个输出通道。</p>
<p><strong>Position</strong></p>
<p>您可以回想一下，缓冲区实际上就是美化了的数组。在从通道读取时，您将所读取的数据放到底层的数组中。 position 变量跟踪已经写了多少数据。更准确地说，它指定了下一个字节将放到数组的哪一个元素中。因此，如果您从通道中读三个字节到缓冲区中，那么缓冲区的 position 将会设置为3，指向数组中第四个元素。</p>
<p>同样，在写入通道时，您是从缓冲区中获取数据。 position 值跟踪从缓冲区中获取了多少数据。更准确地说，它指定下一个字节来自数组的哪一个元素。因此如果从缓冲区写了5个字节到通道中，那么缓冲区的 position 将被设置为5，指向数组的第六个元素。</p>
<p><strong>Limit</strong></p>
<p>limit 变量表明还有多少数据需要取出(在从缓冲区写入通道时)，或者还有多少空间可以放入数据(在从通道读入缓冲区时)。</p>
<p>position 总是小于或者等于 limit。</p>
<p><strong>Capacity</strong></p>
<p>缓冲区的 capacity 表明可以储存在缓冲区中的最大数据容量。实际上，它指定了底层数组的大小 ― 或者至少是指定了准许我们使用的底层数组的容量。</p>
<p>limit 决不能大于 capacity。</p>
<p><strong>观察变量</strong></p>
<p>我们首先观察一个新创建的缓冲区。出于本例子的需要，我们假设这个缓冲区的 总容量 为8个字节。 Buffer 的状态如下所示：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20111214/1127220.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127220.gif" border="0" alt="" width="381" height="58" /></a></p>
<p>回想一下 ，limit 决不能大于 capacity，此例中这两个值都被设置为 8。我们通过将它们指向数组的尾部之后(如果有第8个槽，则是第8个槽所在的位置)来说明这点。</p>
<p><a href="http://images.51cto.com/files/uploadimg/20111214/1127221.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127221.gif" border="0" alt="" width="441" height="226" /></a></p>
<p>position 设置为0。如果我们读一些数据到缓冲区中，那么下一个读取的数据就进入 slot 0 。如果我们从缓冲区写一些数据，从缓冲区读取的下一个字节就来自 slot 0 。 position 设置如下所示：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20111214/1127222.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127222.gif" border="0" alt="" width="442" height="143" /></a></p>
<p>由于 capacity 不会改变，所以我们在下面的讨论中可以忽略它。</p>
<p><strong>第一次读取</strong></p>
<p>现在我们可以开始在新创建的缓冲区上进行读/写操作。首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从 position 开始的位置，这时 position 被设置为 0。读完之后，position 就增加到 3，如下所示：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20111214/1127223.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127223.gif" border="0" alt="" width="430" height="124" /></a></p>
<p>limit 没有改变。</p>
<p><strong>第二次读取</strong></p>
<p>在第二次读取时，我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由 position 所指定的位置上， position 因而增加 2：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20111214/1127224.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127224.gif" border="0" alt="" width="430" height="124" /></a></p>
<p>limit 没有改变。</p>
<p><strong>flip</strong></p>
<p>现在我们要将数据写到输出通道中。在这之前，我们必须调用 flip() 方法。这个方法做两件非常重要的事：</p>
<p>1.它将 limit 设置为当前 position。</p>
<p>2.它将 position 设置为 0。</p>
<p>前一小节中的图显示了在 flip 之前缓冲区的情况。下面是在 flip 之后的缓冲区：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20111214/1127225.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127225.gif" border="0" alt="" width="433" height="124" /></a></p>
<p>我们现在可以将数据从缓冲区写入通道了。 position 被设置为 0，这意味着我们得到的下一个字节是第一个字节。 limit 已被设置为原来的 position，这意味着它包括以前读到的所有字节，并且一个字节也不多。</p>
<p><strong>第一次写入</strong></p>
<p>在第一次写入时，我们从缓冲区中取四个字节并将它们写入输出通道。这使得 position 增加到 4，而 limit 不变，如下所示：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20111214/1127226.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127226.gif" border="0" alt="" width="430" height="124" /></a></p>
<p><strong>第二次写入</strong></p>
<p>我们只剩下一个字节可写了。 limit在我们调用 flip() 时被设置为 5，并且 position 不能超过 limit。所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。这使得 position 增加到 5，并保持 limit 不变，如下所示：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20111214/1127227.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127227.gif" border="0" alt="" width="430" height="124" /></a></p>
<p><strong>clear</strong></p>
<p>最后一步是调用缓冲区的 clear() 方法。这个方法重设缓冲区以便接收更多的字节。 Clear 做两种非常重要的事情：</p>
<p>1.它将 limit 设置为与 capacity 相同。</p>
<p>2.它设置 position 为 0。</p>
<p>下图显示了在调用 clear() 后缓冲区的状态：</p>
<p><a href="http://images.51cto.com/files/uploadimg/20111214/1127228.gif" target="_blank"><img src="http://images.51cto.com/files/uploadimg/20111214/1127228.gif" border="0" alt="" width="434" height="128" /></a></p>
<p>缓冲区现在可以接收新的数据了。</p>
<p><strong>访问方法</strong></p>
<p>到目前为止，我们只是使用缓冲区将数据从一个通道转移到另一个通道。然而，程序经常需要直接处理数据。例如，您可能需要将用户数据保存到磁盘。在这种情况下，您必须将这些数据直接放入缓冲区，然后用通道将缓冲区写入磁盘。</p>
<p>或者，您可能想要从磁盘读取用户数据。在这种情况下，您要将数据从通道读到缓冲区中，然后检查缓冲区中的数据。</p>
<p>在本节的最后，我们将详细分析如何使用 ByteBuffer 类的 get() 和 put() 方法直接访问缓冲区中的数据。</p>
<p><strong>get() 方法</strong></p>
<p>ByteBuffer 类中有四个 get() 方法：</p>
<p>1.byte get();</p>
<p>2.ByteBuffer get( byte dst[] );</p>
<p>3.ByteBuffer get( byte dst[], int offset, int length );</p>
<p>4.byte get( int index );</p>
<p>第一个方法获取单个字节。第二和第三个方法将一组字节读到一个数组中。第四个方法从缓冲区中的特定位置获取字节。那些返回ByteBuffer 的方法只是返回调用它们的缓冲区的 this 值。</p>
<p>此外，我们认为前三个 get() 方法是相对的，而最后一个方法是绝对的。 相对 意味着 get() 操作服从 limit 和 position 值 ― 更明确地说，字节是从当前 position 读取的，而 position 在 get 之后会增加。另一方面，一个 绝对 方法会忽略 limit 和 position 值，也不会影响它们。事实上，它完全绕过了缓冲区的统计方法。</p>
<p>上面列出的方法对应于 ByteBuffer 类。其他类有等价的 get() 方法，这些方法除了不是处理字节外，其它方面是是完全一样的，它们处理的是与该缓冲区类相适应的类型。</p>
<p><strong>put()方法</strong></p>
<p>ByteBuffer 类中有五个 put() 方法：</p>
<p>1.ByteBuffer put( byte b );</p>
<p>2.ByteBuffer put( byte src[] );</p>
<p>3.ByteBuffer put( byte src[], int offset, int length );</p>
<p>4.ByteBuffer put( ByteBuffer src );</p>
<p>5.ByteBuffer put( int index, byte b );</p>
<p>第一个方法 写入（put） 单个字节。第二和第三个方法写入来自一个数组的一组字节。第四个方法将数据从一个给定的源ByteBuffer 写入这个 ByteBuffer。第五个方法将字节写入缓冲区中特定的 位置 。那些返回 ByteBuffer 的方法只是返回调用它们的缓冲区的 this 值。</p>
<p>与 get() 方法一样，我们将把 put() 方法划分为 相对 或者 绝对 的。前四个方法是相对的，而第五个方法是绝对的。</p>
<p>上面显示的方法对应于 ByteBuffer 类。其他类有等价的 put() 方法，这些方法除了不是处理字节之外，其它方面是完全一样的。它们处理的是与该缓冲区类相适应的类型。</p>
<p><strong>类型化的 get() 和 put() 方法</strong></p>
<p>除了前些小节中描述的 get() 和 put() 方法， ByteBuffer 还有用于读写不同类型的值的其他方法，如下所示：</p>
<p>• getByte()</p>
<p>• getChar()</p>
<p>• getShort()</p>
<p>• getInt()</p>
<p>• getLong()</p>
<p>• getFloat()</p>
<p>• getDouble()</p>
<p>• putByte()</p>
<p>• putChar()</p>
<p>• putShort()</p>
<p>• putInt()</p>
<p>• putLong()</p>
<p>• putFloat()</p>
<p>• putDouble()</p>
<p>事实上，这其中的每个方法都有两种类型 ― 一种是相对的，另一种是绝对的。它们对于读取格式化的二进制数据（如图像文件的头部）很有用。</p>
<p>您可以在例子程序 TypesInByteBuffer.java 中看到这些方法的实际应用。</p>
<p><strong>缓冲区的使用：一个内部循环</strong></p>
<p>下面的内部循环概括了使用缓冲区将数据从输入通道拷贝到输出通道的过程。</p>
<pre>
<ol>
<li>while (true) {</li>
<li>     buffer.clear();</li>
<li>     int r = fcin.read( buffer );</li>
<li>      if (r==-1) {</li>
<li>       break;</li>
<li>     }</li>
<li>      buffer.flip();</li>
<li>     fcout.write( buffer );}</li>
</ol>
</pre>
<p>read() 和 write() 调用得到了极大的简化，因为许多工作细节都由缓冲区完成了。 clear() 和 flip() 方法用于让缓冲区在读和写之间切换。</p>
<p><strong>◆  关于缓冲区的更多内容</strong></p>
<p><strong>概  述</strong></p>
<p>到目前为止，您已经学习了使用缓冲区进行日常工作所需要掌握的大部分内容。我们的例子没怎么超出标准的读/写过程种类，在原来的 I/O 中可以像在 NIO 中一样容易地实现这样的标准读写过程。</p>
<p>本节将讨论使用缓冲区的一些更复杂的方面，比如缓冲区分配、包装和分片。我们还会讨论 NIO 带给 Java 平台的一些新功能。您将学到如何创建不同类型的缓冲区以达到不同的目的，如可保护数据不被修改的 只读 缓冲区，和直接映射到底层操作系统缓冲区的 直接 缓冲区。我们将在本节的最后介绍如何在 NIO 中创建内存映射文件。</p>
<p><strong>缓冲区分配和包装</strong></p>
<p>在能够读和写之前，必须有一个缓冲区。要创建缓冲区，您必须 分配 它。我们使用静态方法 allocate() 来分配缓冲区：</p>
<pre>
<ol>
<li>ByteBuffer buffer = ByteBuffer.allocate( 1024 );</li>
</ol>
</pre>
<p>allocate() 方法分配一个具有指定大小的底层数组，并将它包装到一个缓冲区对象中 ― 在本例中是一个 ByteBuffer。</p>
<p>您还可以将一个现有的数组转换为缓冲区，如下所示：</p>
<pre>
<ol>
<li>byte array[] = new byte[1024];</li>
<li>ByteBuffer buffer = ByteBuffer.wrap( array );</li>
</ol>
</pre>
<p>本例使用了 wrap() 方法将一个数组包装为缓冲区。必须非常小心地进行这类操作。一旦完成包装，底层数据就可以通过缓冲区或者直接访问。</p>
<p><strong>缓冲区分片</strong></p>
<p>slice() 方法根据现有的缓冲区创建一种 子缓冲区 。也就是说，它创建一个新的缓冲区，新缓冲区与原来的缓冲区的一部分共享数据。</p>
<p>使用例子可以最好地说明这点。让我们首先创建一个长度为 10 的 ByteBuffer：</p>
<pre>
<ol>
<li>ByteBuffer buffer = ByteBuffer.allocate( 10 )</li>
</ol>
</pre>
<p>然后使用数据来填充这个缓冲区，在第 n 个槽中放入数字 n：</p>
<pre>
<ol>
<li>for (int i=0; i&lt;buffer.capacity(); ++i) {</li>
<li>     buffer.put( (byte)i );</li>
<li>}</li>
</ol>
</pre>
<p>现在我们对这个缓冲区 分片 ，以创建一个包含槽 3 到槽 6 的子缓冲区。在某种意义上，子缓冲区就像原来的缓冲区中的一个 窗口 。</p>
<p>窗口的起始和结束位置通过设置 position 和 limit 值来指定，然后调用 Buffer 的 slice() 方法：</p>
<pre>
<ol>
<li>buffer.position( 3 );</li>
<li>buffer.limit( 7 );</li>
<li>ByteBuffer slice = buffer.slice();</li>
</ol>
</pre>
<p>片 是缓冲区的 子缓冲区 。不过， 片段 和 缓冲区 共享同一个底层数据数组，我们在下一节将会看到这一点。</p>
<p><strong>缓冲区份片和数据共享</strong></p>
<p>我们已经创建了原缓冲区的子缓冲区，并且我们知道缓冲区和子缓冲区共享同一个底层数据数组。让我们看看这意味着什么。</p>
<p>我们遍历子缓冲区，将每一个元素乘以 11 来改变它。例如，5 会变成 55。</p>
<pre>
<ol>
<li>for (int i=0; i&lt;slice.capacity(); ++i) {</li>
<li>     byte b = slice.get( i );</li>
<li>     b *= 11;</li>
<li>     slice.put( i, b );</li>
<li>}</li>
</ol>
</pre>
<p>最后，再看一下原缓冲区中的内容：</p>
<pre>
<ol>
<li>buffer.position( 0 );</li>
<li>buffer.limit( buffer.capacity() );</li>
<li> while (buffer.remaining()&gt;0) {</li>
<li>     System.out.println( buffer.get() );</li>
<li>}</li>
</ol>
</pre>
<p>结果表明只有在子缓冲区窗口中的元素被改变了：</p>
<pre>$ java SliceBuffer
0
1
2
33
44
55
66
7
8
9</pre>
<p>缓冲区片对于促进抽象非常有帮助。可以编写自己的函数处理整个缓冲区，而且如果想要将这个过程应用于子缓冲区上，您只需取主缓冲区的一个片，并将它传递给您的函数。这比编写自己的函数来取额外的参数以指定要对缓冲区的哪一部分进行操作更容易。</p>
<p><strong>只读缓冲区</strong></p>
<p>只读缓冲区非常简单 ― 您可以读取它们，但是不能向它们写入。可以通过调用缓冲区的 asReadOnlyBuffer() 方法，将任何常规缓冲区转换为只读缓冲区，这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享数据)，只不过它是只读的。</p>
<p>只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时，您无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以 保证 该缓冲区不会被修改。</p>
<p>不能将只读的缓冲区转换为可写的缓冲区。</p>
<p><strong>直接和间接缓冲区</strong></p>
<p>另一种有用的 ByteBuffer 是直接缓冲区。 直接缓冲区 是为加快 I/O 速度，而以一种特殊的方式分配其内存的缓冲区。</p>
<p>实际上，直接缓冲区的准确定义是与实现相关的。Sun 的文档是这样描述直接缓冲区的：</p>
<p>给定一个直接字节缓冲区，Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说，它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后)，尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。</p>
<p>您可以在例子程序 FastCopyFile.java 中看到直接缓冲区的实际应用，这个程序是 CopyFile.java 的另一个版本，它使用了直接缓冲区以提高速度。</p>
<p>还可以用内存映射文件创建直接缓冲区。</p>
<p><strong>内存映射文件 I/O</strong></p>
<p>内存映射文件 I/O 是一种读和写文件数据的方法，它可以比常规的基于流或者基于通道的 I/O 快得多。</p>
<p>内存映射文件 I/O 是通过使文件中的数据神奇般地出现为内存数组的内容来完成的。这其初听起来似乎不过就是将整个文件读到内存中，但是事实上并不是这样。一般来说，只有文件中实际读取或者写入的部分才会送入（或者 映射 ）到内存中。</p>
<p>内存映射并不真的神奇或者多么不寻常。现代操作系统一般根据需要将文件的部分映射为内存的部分，从而实现文件系统。Java 内存映射机制不过是在底层操作系统中可以采用这种机制时，提供了对该机制的访问。</p>
<p>尽管创建内存映射文件相当简单，但是向它写入可能是危险的。仅只是改变数组的单个元素这样的简单操作，就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。</p>
<p><strong>将文件映射到内存</strong></p>
<p>了解内存映射的最好方法是使用例子。在下面的例子中，我们要将一个 FileChannel (它的全部或者部分)映射到内存中。为此我们将使用 FileChannel.map() 方法。下面代码行将文件的前 1024 个字节映射到内存中：</p>
<pre>
<ol>
<li>MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE,     0, 1024 );</li>
</ol>
</pre>
<p>map() 方法返回一个 MappedByteBuffer，它是 ByteBuffer 的子类。因此，您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区，操作系统会在需要时负责执行行映射。</p>
<p><strong>◆  分散和聚集</strong></p>
<p><strong>概  述</strong></p>
<p>分散/聚集 I/O 是使用多个而不是单个缓冲区来保存数据的读写方法。</p>
<p>一个分散的读取就像一个常规通道读取，只不过它是将数据读到一个缓冲区数组中而不是读到单个缓冲区中。同样地，一个聚集写入是向缓冲区数组而不是向单个缓冲区写入数据。</p>
<p>分散/聚集 I/O 对于将数据流划分为单独的部分很有用，这有助于实现复杂的数据格式。</p>
<p><strong>分散/聚集 I/O</strong></p>
<p>通道可以有选择地实现两个新的接口： ScatteringByteChannel 和 GatheringByteChannel。一个 ScatteringByteChannel 是一个具有两个附加读方法的通道：</p>
<p>• long read( ByteBuffer[] dsts );</p>
<p>• long read( ByteBuffer[] dsts, int offset, int length );</p>
<p>这些 long read() 方法很像标准的 read 方法，只不过它们不是取单个缓冲区而是取一个缓冲区数组。</p>
<p>在 分散读取 中，通道依次填充每个缓冲区。填满一个缓冲区后，它就开始填充下一个。在某种意义上，缓冲区数组就像一个大缓冲区。</p>
<p><strong>分散/聚集的应用</strong></p>
<p>分散/聚集 I/O 对于将数据划分为几个部分很有用。例如，您可能在编写一个使用消息对象的网络应用程序，每一个消息被划分为固定长度的头部和固定长度的正文。您可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容难正文的缓冲区。当您将它们放入一个数组中并使用分散读取来向它们读入消息时，头部和正文将整齐地划分到这两个缓冲区中。</p>
<p>我们从缓冲区所得到的方便性对于缓冲区数组同样有效。因为每一个缓冲区都跟踪自己还可以接受多少数据，所以分散读取会自动找到有空间接受数据的第一个缓冲区。在这个缓冲区填满后，它就会移动到下一个缓冲区。</p>
<p><strong>聚集写入</strong></p>
<p>聚集写入 类似于分散读取，只不过是用来写入。它也有接受缓冲区数组的方法：</p>
<p>• long write( ByteBuffer[] srcs );</p>
<p>• long write( ByteBuffer[] srcs, int offset, int length );</p>
<p>聚集写对于把一组单独的缓冲区中组成单个数据流很有用。为了与上面的消息例子保持一致，您可以使用聚集写入来自动将网络消息的各个部分组装为单个数据流，以便跨越网络传输消息。</p>
<p>从例子程序 UseScatterGather.java 中可以看到分散读取和聚集写入的实际应用。</p>
<p><strong>◆  文件锁定</strong></p>
<p><strong>概  述</strong></p>
<p>文件锁定初看起来可能让人迷惑。它 似乎 指的是防止程序或者用户访问特定文件。事实上，文件锁就像常规的 Java 对象锁 ― 它们是 劝告式的（advisory） 锁。它们不阻止任何形式的数据访问，相反，它们通过锁的共享和获取赖允许系统的不同部分相互协调。</p>
<p>您可以锁定整个文件或者文件的一部分。如果您获取一个排它锁，那么其他人就不能获得同一个文件或者文件的一部分上的锁。如果您获得一个共享锁，那么其他人可以获得同一个文件或者文件一部分上的共享锁，但是不能获得排它锁。文件锁定并不总是出于保护数据的目的。例如，您可能临时锁定一个文件以保证特定的写操作成为原子的，而不会有其他程序的干扰。</p>
<p>大多数操作系统提供了文件系统锁，但是它们并不都是采用同样的方式。有些实现提供了共享锁，而另一些仅提供了排它锁。事实上，有些实现使得文件的锁定部分不可访问，尽管大多数实现不是这样的。</p>
<p>在本节中，您将学习如何在 NIO 中执行简单的文件锁过程，我们还将探讨一些保证被锁定的文件尽可能可移植的方法。</p>
<p><strong>锁定文件</strong></p>
<p>要获取文件的一部分上的锁，您要调用一个打开的 FileChannel 上的 lock() 方法。注意，如果要获取一个排它锁，您必须以写方式打开文件。</p>
<pre>
<ol>
<li>RandomAccessFile raf = new RandomAccessFile( "usefilelocks.txt", "rw" );</li>
<li>FileChannel fc = raf.getChannel();</li>
<li>FileLock lock = fc.lock( start, end, false );</li>
</ol>
</pre>
<p>在拥有锁之后，您可以执行需要的任何敏感操作，然后再释放锁：</p>
<pre>
<ol>
<li>lock.release();</li>
</ol>
</pre>
<p>在释放锁后，尝试获得锁的其他任何程序都有机会获得它。</p>
<p>本小节的例子程序 UseFileLocks.java 必须与它自己并行运行。这个程序获取一个文件上的锁，持有三秒钟，然后释放它。如果同时运行这个程序的多个实例，您会看到每个实例依次获得锁。</p>
<p>文件锁定可能是一个复杂的操作，特别是考虑到不同的操作系统是以不同的方式实现锁这一事实。下面的指导原则将帮助您尽可能保持代码的可移植性：</p>
<p>• 只使用排它锁。</p>
<p>• 将所有的锁视为劝告式的（advisory）。</p>
<p><strong>◆  连网和异步 I/O</strong></p>
<p><strong>概  述</strong></p>
<p>连网是学习异步 I/O 的很好基础，而异步 I/O 对于在 Java 语言中执行任何输入/输出过程的人来说，无疑都是必须具备的知识。NIO 中的连网与 NIO 中的其他任何操作没有什么不同 ― 它依赖通道和缓冲区，而您通常使用 InputStream 和 OutputStream 来获得通道。</p>
<p>本节首先介绍异步 I/O 的基础 ― 它是什么以及它不是什么，然后转向更实用的、程序性的例子。</p>
<p><strong>异步 I/O</strong></p>
<p>异步 I/O 是一种 没有阻塞地 读写数据的方法。通常，在代码进行 read() 调用时，代码会阻塞直至有可供读取的数据。同样，write() 调用将会阻塞直至数据能够写入。</p>
<p>另一方面，异步 I/O 调用不会阻塞。相反，您将注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、新的套接字连接，等等，而在发生这样的事件时，系统将会告诉您。</p>
<p>异步 I/O 的一个优势在于，它允许您同时根据大量的输入和输出执行 I/O。同步程序常常要求助于轮询，或者创建许许多多的线程以处理大量的连接。使用异步 I/O，您可以监听任何数量的通道上的事件，不用轮询，也不用额外的线程。</p>
<p>我们将通过研究一个名为 MultiPortEcho.java 的例子程序来查看异步 I/O 的实际应用。这个程序就像传统的 echo server，它接受网络连接并向它们回响它们可能发送的数据。不过它有一个附加的特性，就是它能同时监听多个端口，并处理来自所有这些端口的连接。并且它只在单个线程中完成所有这些工作。</p>
<p><strong>Selectors</strong></p>
<p>本节的阐述对应于 MultiPortEcho 的源代码中的 go() 方法的实现，因此应该看一下源代码，以便对所发生的事情有个更全面的了解。</p>
<p>异步 I/O 中的核心对象名为 Selector。Selector 就是您注册对各种 I/O 事件的兴趣的地方，而且当那些事件发生时，就是这个对象告诉您所发生的事件。</p>
<p>所以，我们需要做的第一件事就是创建一个 Selector：</p>
<pre>
<ol>
<li>Selector selector = Selector.open();</li>
</ol>
</pre>
<p>然后，我们将对不同的通道对象调用 register() 方法，以便注册我们对这些对象中发生的 I/O 事件的兴趣。register() 的第一个参数总是这个 Selector。</p>
<p><strong>打开一个 ServerSocketChannel</strong></p>
<p>为了接收连接，我们需要一个 ServerSocketChannel。事实上，我们要监听的每一个端口都需要有一个 ServerSocketChannel 。对于每一个端口，我们打开一个 ServerSocketChannel，如下所示：</p>
<pre>
<ol>
<li>ServerSocketChannel ssc = ServerSocketChannel.open();</li>
<li>ssc.configureBlocking( false );</li>
<li>ServerSocket ss = ssc.socket();</li>
<li>InetSocketAddress address = new InetSocketAddress( ports[i] );</li>
<li>ss.bind( address );</li>
</ol>
</pre>
<p>第一行创建一个新的 ServerSocketChannel ，最后三行将它绑定到给定的端口。第二行将 ServerSocketChannel 设置为 非阻塞的 。我们必须对每一个要使用的套接字通道调用这个方法，否则异步 I/O 就不能工作。</p>
<p><strong>选择键</strong></p>
<p>下一步是将新打开的 ServerSocketChannels 注册到 Selector上。为此我们使用 ServerSocketChannel.register() 方法，如下所示</p>
<pre>
<ol>
<li>SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );</li>
</ol>
</pre>
<p>register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT，这里它指定我们想要监听 accept 事件，也就是在新的连接建立时所发生的事件。这是适用于 ServerSocketChannel 的唯一事件类型。</p>
<p>请注意对 register() 的调用的返回值。 SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时，它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。</p>
<p><strong>内部循环</strong></p>
<p>现在已经注册了我们对一些 I/O 事件的兴趣，下面将进入主循环。使用 Selectors 的几乎每个程序都像下面这样使用内部循环：</p>
<pre>
<ol>
<li>int num = selector.select();</li>
<li>Set selectedKeys = selector.selectedKeys();</li>
<li>Iterator it = selectedKeys.iterator();</li>
<li>while (it.hasNext()) {</li>
<li>     SelectionKey key = (SelectionKey)it.next();</li>
<li>     // ... deal with I/O event ...}</li>
</ol>
</pre>
<p>首先，我们调用 Selector 的 select() 方法。这个方法会阻塞，直到至少有一个已注册的事件发生。当一个或者更多的事件发生时，select() 方法将返回所发生的事件的数量。</p>
<p>接下来，我们调用 Selector 的 selectedKeys() 方法，它返回发生了事件的 SelectionKey 对象的一个 集合。</p>
<p>我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey，您必须确定发生的是什么 I/O 事件，以及这个事件影响哪些 I/O 对象。</p>
<p><strong>监听新连接</strong></p>
<p>程序执行到这里，我们仅注册了 ServerSocketChannel，并且仅注册它们“接收”事件。为确认这一点，我们对 SelectionKey 调用readyOps() 方法，并检查发生了什么类型的事件：</p>
<pre>
<ol>
<li>if ((key.readyOps() &amp; SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {</li>
<li>        // Accept the new connection</li>
<li>       // ...</li>
<li>}</li>
</ol>
</pre>
<p>可以肯定地说， readOps() 方法告诉我们该事件是新的连接。</p>
<p><strong>接受新的连接</strong></p>
<p>因为我们知道这个服务器套接字上有一个传入连接在等待，所以可以安全地接受它；也就是说，不用担心 accept() 操作会阻塞：</p>
<pre>
<ol>
<li>ServerSocketChannel ssc = (ServerSocketChannel)key.channel();</li>
<li>SocketChannel sc = ssc.accept();</li>
</ol>
</pre>
<p>下一步是将新连接的 SocketChannel 配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据，所以我们还必须将 SocketChannel 注册到 Selector上，如下所示：</p>
<pre>
<ol>
<li>sc.configureBlocking( false );</li>
<li>SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );</li>
</ol>
</pre>
<p>注意我们使用 register() 的 OP_READ 参数，将 SocketChannel 注册用于 读取 而不是 接受 新连接。</p>
<p><strong>删除处理过的 SelectionKey</strong></p>
<p>在处理 SelectionKey 之后，我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键，那么它仍然会在主集合中以一个激活的键出现，这会导致我们尝试再次处理它。我们调用迭代器的remove() 方法来删除处理过的 SelectionKey：</p>
<pre>
<ol>
<li>it.remove();</li>
</ol>
</pre>
<p>现在我们可以返回主循环并接受从一个套接字中传入的数据(或者一个传入的 I/O 事件)了。</p>
<p><strong>传入的 I/O</strong></p>
<p>当来自一个套接字的数据到达时，它会触发一个 I/O 事件。这会导致在主循环中调用 Selector.select()，并返回一个或者多个 I/O 事件。这一次， SelectionKey 将被标记为 OP_READ 事件，如下所示：</p>
<pre>
<ol>
<li>} else if ((key.readyOps() &amp; SelectionKey.OP_READ)     == SelectionKey.OP_READ) {</li>
<li>     // Read the data</li>
<li>     SocketChannel sc = (SocketChannel)key.channel();</li>
<li>     // ...}</li>
</ol>
</pre>
<p>与以前一样，我们取得发生 I/O 事件的通道并处理它。在本例中，由于这是一个 echo server，我们只希望从套接字中读取数据并马上将它发送回去。</p>
<p>每次返回主循环，我们都要调用 select 的 Selector()方法，并取得一组 SelectionKey。每个键代表一个 I/O 事件。我们处理事件，从选定的键集中删除 SelectionKey，然后返回主循环的顶部。</p>
<p>这个程序有点过于简单，因为它的目的只是展示异步 I/O 所涉及的技术。在现实的应用程序中，您需要通过将通道从 Selector 中删除来处理关闭的通道。而且您可能要使用多个线程。这个程序可以仅使用一个线程，因为它只是一个演示，但是在现实场景中，创建一个线程池来负责 I/O 事件处理中的耗时部分会更有意义。</p>
<p><strong>字符集</strong></p>
<p>根据 Sun 的文档，一个 Charset 是“十六位 Unicode 字符序列与字节序列之间的一个命名的映射”。实际上，一个 Charset 允许您以尽可能最具可移植性的方式读写字符序列。</p>
<p>Java 语言被定义为基于 Unicode。然而在实际上，许多人编写代码时都假设一个字符在磁盘上或者在网络流中用一个字节表示。这种假设在许多情况下成立，但是并不是在所有情况下都成立，而且随着计算机变得对 Unicode 越来越友好，这个假设就日益变得不能成立了。</p>
<p>在本节中，我们将看一下如何使用 Charsets 以适合现代文本格式的方式处理文本数据。这里将使用的示例程序相当简单，不过，它触及了使用 Charset 的所有关键方面：为给定的字符编码创建 Charset，以及使用该 Charset 解码和编码文本数据。</p>
<p><strong>编码/解码</strong></p>
<p>要读和写文本，我们要分别使用 CharsetDecoder 和 CharsetEncoder。将它们称为 编码器 和 解码器 是有道理的。一个 字符 不再表示一个特定的位模式，而是表示字符系统中的一个实体。因此，由某个实际的位模式表示的字符必须以某种特定的 编码 来表示。</p>
<p>CharsetDecoder 用于将逐位表示的一串字符转换为具体的 char 值。同样，一个 CharsetEncoder 用于将字符转换回位。</p>
<p>在下一个小节中，我们将考察一个使用这些对象来读写数据的程序。</p>
<p><strong>处理文本的正确方式</strong></p>
<p>现在我们将分析这个例子程序 UseCharsets.java。这个程序非常简单 ― 它从一个文件中读取一些文本，并将该文本写入另一个文件。但是它把该数据当作文本数据，并使用 CharBuffer 来将该数句读入一个 CharsetDecoder 中。同样，它使用 CharsetEncoder 来写回该数据。</p>
<p>我们将假设字符以 ISO-8859-1(Latin1) 字符集（这是 ASCII 的标准扩展）的形式储存在磁盘上。尽管我们必须为使用 Unicode 做好准备，但是也必须认识到不同的文件是以不同的格式储存的，而 ASCII 无疑是非常普遍的一种格式。事实上，每种 Java 实现都要求对以下字符编码提供完全的支持：</p>
<p>• US-ASCII</p>
<p>• ISO-8859-1</p>
<p>• UTF-8</p>
<p>• UTF-16BE</p>
<p>• UTF-16LE</p>
<p>• UTF-16</p>
<p><strong>示例程序</strong></p>
<p>在打开相应的文件、将输入数据读入名为 inputData 的 ByteBuffer 之后，我们的程序必须创建 ISO-8859-1 (Latin1) 字符集的一个实例：</p>
<pre>
<ol>
<li>Charset latin1 = Charset.forName( "ISO-8859-1" );</li>
</ol>
</pre>
<p>然后，创建一个解码器（用于读取）和一个编码器 （用于写入）：</p>
<pre>
<ol>
<li>CharsetDecoder decoder = latin1.newDecoder();</li>
<li>CharsetEncoder encoder = latin1.newEncoder();</li>
</ol>
</pre>
<p>为了将字节数据解码为一组字符，我们把 ByteBuffer 传递给 CharsetDecoder，结果得到一个 CharBuffer</p>
<pre>
<ol>
<li>CharBuffer cb = decoder.decode( inputData );</li>
</ol>
</pre>
<p>如果想要处理字符，我们可以在程序的此处进行。但是我们只想无改变地将它写回，所以没有什么要做的。</p>
<p>要写回数据，我们必须使用 CharsetEncoder 将它转换回字节：</p>
<pre>
<ol>
<li>ByteBuffer outputData = encoder.encode( cb );</li>
</ol>
</pre>
<p>在转换完成之后，我们就可以将数据写到文件中了。</p>
<p>结束语和参考资料</p>
<p><strong>结束语</strong></p>
<p>正如您所看到的， NIO 库有大量的特性。在一些新特性（例如文件锁定和字符集）提供新功能的同时，许多特性在优化方面也非常优秀。</p>
<p>在基础层次上，通道和缓冲区可以做的事情几乎都可以用原来的面向流的类来完成。但是通道和缓冲区允许以 快得多 的方式完成这些相同的旧操作 ― 事实上接近系统所允许的最大速度。</p>
<p>不过 NIO 最强大的长度之一在于，它提供了一种在 Java 语言中执行进行输入/输出的新的（也是迫切需要的）结构化方式。随诸如缓冲区、通道和异步 I/O 这些概念性（且可实现的）实体而来的，是我们重新思考 Java 程序中的 I/O过程的机会。这样，NIO 甚至为我们最熟悉的 I/O 过程也带来了新的活力，同时赋予我们通过和以前不同并且更好的方式执行它们的机会。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>http://www.wdcode.org/archives/530.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Java NIO2 AIO开发核心流程</title>
		<link>http://www.wdcode.org/archives/526.html</link>
		<comments>http://www.wdcode.org/archives/526.html#comments</comments>
		<pubDate>Thu, 15 Dec 2011 06:45:05 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[J2SE]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[NIO]]></category>

		<guid isPermaLink="false">http://www.wdcode.org/?p=526</guid>
		<description><![CDATA[按照《Unix网络编程》的划分，IO模型可以分为：阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO，按照POSIX标准来划分只分为两类：同步IO和异步IO。如何区分呢？首先一个IO操作其实分成了两个步骤：发起IO请求和实际的IO操作，同步IO和异步IO的区别就在于第二个步骤是否阻塞，如果实际的IO读写阻塞请求进程，那么就是同步IO，因此阻塞IO、非阻塞IO、IO服用、信号驱动IO都是同步IO，如果不阻塞，而是操作系统帮你做完IO操作再将结果返回给你，那么就是异步IO。阻塞IO和非阻塞IO的区别在于第一步，发起IO请求是否会被阻塞，如果阻塞直到完成那么就是传统的阻塞IO，如果不阻塞，那么就是非阻塞IO。

Java nio 2.0的主要改进就是引入了异步IO（包括文件和网络），这里主要介绍下异步网络IO API的使用以及框架的设计，以TCP服务端为例。首先看下为了支持AIO引入的新的类和接口：

java.nio.channels.AsynchronousChannel

标记一个channel支持异步IO操作。

java.nio.channels.AsynchronousServerSocketChannel

ServerSocket的aio版本，创建TCP服务端，绑定地址，监听端口等。

java.nio.channels.AsynchronousSocketChannel

面向流的异步socket channel，表示一个连接。

java.nio.channels.AsynchronousChannelGroup

异步channel的分组管理，目的是为了资源共享。一个AsynchronousChannelGroup绑定一个线程池，这个线程池执行两个任务：处理IO事件和派发CompletionHandler。AsynchronousServerSocketChannel创建的时候可以传入一个 AsynchronousChannelGroup，那么通过AsynchronousServerSocketChannel创建的 AsynchronousSocketChannel将同属于一个组，共享资源。

java.nio.channels.CompletionHandler

异步IO操作结果的回调接口，用于定义在IO操作完成后所作的回调工作。AIO的API允许两种方式来处理异步操作的结果：返回的Future模式或者注册CompletionHandler，我更推荐用CompletionHandler的方式，这些handler的调用是由 AsynchronousChannelGroup的线程池派发的。显然，线程池的大小是性能的关键因素。AsynchronousChannelGroup允许绑定不同的线程池，通过三个静态方法来创建：
<pre>
<ol>
	<li>public static AsynchronousChannelGroup withFixedThreadPool(int nThreads,</li>
	<li>                                                              ThreadFactory threadFactory)</li>
	<li>       throws IOException</li>
	<li></li>
	<li>public static AsynchronousChannelGroup withCachedThreadPool(ExecutorService executor,</li>
	<li>                                                               int initialSize)</li>
	<li></li>
	<li>public static AsynchronousChannelGroup withThreadPool(ExecutorService executor)</li>
	<li>       throws IOException</li>
</ol>
</pre>
需要根据具体应用相应调整，从框架角度出发，需要暴露这样的配置选项给用户。

在介绍完了aio引入的TCP的主要接口和类之后，我们来设想下一个aio框架应该怎么设计。参考非阻塞nio框架的设计，一般都是采用Reactor模式，Reacot负责事件的注册、select、事件的派发；相应地，异步IO有个Proactor模式，Proactor负责 CompletionHandler的派发，查看一个典型的IO写操作的流程来看两者的区别：

Reactor: send(msg) -&#62; 消息队列是否为空，如果为空 -&#62; 向Reactor注册OP_WRITE，然后返回 -&#62; Reactor select -&#62; 触发Writable，通知用户线程去处理 -&#62;先注销Writable(很多人遇到的cpu 100%的问题就在于没有注销）,处理Writeable，如果没有完全写入，继续注册OP_WRITE。注意到，写入的工作还是用户线程在处理。

Proactor: send(msg) -&#62; 消息队列是否为空，如果为空,发起read异步调用，并注册CompletionHandler，然后返回。 -&#62; 操作系统负责将你的消息写入，并返回结果（写入的字节数）给Proactor -&#62; Proactor派发CompletionHandler。可见，写入的工作是操作系统在处理，无需用户线程参与。事实上在aio的API 中,AsynchronousChannelGroup就扮演了Proactor的角色。

CompletionHandler有三个方法，分别对应于处理成功、失败、被取消（通过返回的Future)情况下的回调处理：
<pre>
<ol>
	<li>public interface CompletionHandler&#60;V,A&#62; {</li>
	<li></li>
	<li>     void completed(V result, A attachment);</li>
	<li></li>
	<li>    void failed(Throwable exc, A attachment);</li>
	<li></li>
	<li></li>
	<li>    void cancelled(A attachment);</li>
	<li>}</li>
</ol>
</pre>
&#160;

&#160;

其中的泛型参数V表示IO调用的结果，而A是发起调用时传入的attchment。

在初步介绍完aio引入的类和接口后，我们看看一个典型的tcp服务端是怎么启动的，怎么接受连接并处理读和写，这里引用的代码都是yanf4j 的aio分支中的代码，可以从svn checkout，svn地址: http://yanf4j.googlecode.com/svn/branches/yanf4j-aio

第一步，创建一个AsynchronousServerSocketChannel，创建之前先创建一个 AsynchronousChannelGroup，上文提到AsynchronousServerSocketChannel可以绑定一个 AsynchronousChannelGroup，那么通过这个AsynchronousServerSocketChannel建立的连接都将同属于一个AsynchronousChannelGroup并共享资源：
<pre>
<ol>
	<li>this.asynchronousChannelGroup = AsynchronousChannelGroup</li>
	<li>                    .withCachedThreadPool(Executors.newCachedThreadPool(),</li>
	<li>                            this.threadPoolSize);</li>
</ol>
</pre>
&#160;

然后初始化一个AsynchronousServerSocketChannel，通过open方法：
<pre>
<ol>
	<li>this.serverSocketChannel = AsynchronousServerSocketChannel</li>
	<li>                .open(this.asynchronousChannelGroup);</li>
</ol>
</pre>
通过nio 2.0引入的SocketOption类设置一些TCP选项：
<pre>
<ol>
	<li>this.serverSocketChannel</li>
	<li>                    .setOption(</li>
	<li>                            StandardSocketOption.SO_REUSEADDR,true);</li>
	<li>this.serverSocketChannel</li>
	<li>                    .setOption(</li>
	<li>                            StandardSocketOption.SO_RCVBUF,16*1024);</li>
</ol>
</pre>
绑定本地地址：
<pre>
<ol>
	<li>this.serverSocketChannel</li>
	<li>                    .bind(new InetSocketAddress("localhost",8080), 100);</li>
</ol>
</pre>
其中的100用于指定等待连接的队列大小(backlog)。完了吗？还没有，最重要的监听工作还没开始，监听端口是为了等待连接上来以便accept产生一个AsynchronousSocketChannel来表示一个新建立的连接，因此需要发起一个accept调用，调用是异步的，操作系统将在连接建立后，将最后的结果——AsynchronousSocketChannel返回给你
<pre>
<ol>
	<li>public void pendingAccept() {</li>
	<li>        if (this.started &#38;&#38; this.serverSocketChannel.isOpen()) {</li>
	<li>            this.acceptFuture = this.serverSocketChannel.accept(null,</li>
	<li>                    new AcceptCompletionHandler());</li>
	<li></li>
	<li>        } else {</li>
	<li>            throw new IllegalStateException("Controller has been closed");</li>
	<li>        }</li>
	<li>    }</li>
</ol>
</pre>
注意，重复的accept调用将会抛出PendingAcceptException，后文提到的read和write也是如此。accept方法的第一个参数是你想传给CompletionHandler的attchment，第二个参数就是注册的用于回调的CompletionHandler，最后返回结果Future&#60;AsynchronousSocketChannel&#62;。你可以对future做处理，这里采用更推荐的方式就是注册一个CompletionHandler。那么accept的CompletionHandler中做些什么工作呢？显然一个赤裸裸的 AsynchronousSocketChannel是不够的，我们需要将它封装成session，一个session表示一个连接（mina里就叫 IoSession了），里面带了一个缓冲的消息队列以及一些其他资源等。在连接建立后，除非你的服务器只准备接受一个连接，不然你需要在后面继续调用pendingAccept来发起另一个accept请求：
<pre>
<ol>
	<li>private final class AcceptCompletionHandler implements</li>
	<li>            CompletionHandler&#60;AsynchronousSocketChannel, Object&#62; {</li>
	<li></li>
	<li>        @Override</li>
	<li>        public void cancelled(Object attachment) {</li>
	<li>            logger.warn("Accept operation was canceled");</li>
	<li>        }</li>
	<li></li>
	<li>        @Override</li>
	<li>        public void completed(AsynchronousSocketChannel socketChannel,</li>
	<li>                Object attachment) {</li>
	<li>            try {</li>
	<li>                logger.debug("Accept connection from "</li>
	<li>                        + socketChannel.getRemoteAddress());</li>
	<li>                configureChannel(socketChannel);</li>
	<li>                AioSessionConfig sessionConfig = buildSessionConfig(socketChannel);</li>
	<li>                Session session = new AioTCPSession(sessionConfig,</li>
	<li>                        AioTCPController.this.configuration</li>
	<li>                                .getSessionReadBufferSize(),</li>
	<li>                        AioTCPController.this.sessionTimeout);</li>
	<li>                session.start();</li>
	<li>                registerSession(session);</li>
	<li>            } catch (Exception e) {</li>
	<li>                e.printStackTrace();</li>
	<li>                logger.error("Accept error", e);</li>
	<li>                notifyException(e);</li>
	<li>            } finally {</li>
	<li>                &#60;strong&#62;pendingAccept&#60;/strong&#62;();</li>
	<li>            }</li>
	<li>        }</li>
	<li></li>
	<li>        @Override</li>
	<li>        public void failed(Throwable exc, Object attachment) {</li>
	<li>            logger.error("Accept error", exc);</li>
	<li>            try {</li>
	<li>                notifyException(exc);</li>
	<li>            } finally {</li>
	<li>                &#60;strong&#62;pendingAccept&#60;/strong&#62;();</li>
	<li>            }</li>
	<li>        }</li>
	<li>    }</li>
</ol>
</pre>
注意到了吧，我们在failed和completed方法中在最后都调用了pendingAccept来继续发起accept调用，等待新的连接上来。有的同学可能要说了，这样搞是不是递归调用，会不会堆栈溢出？实际上不会，因为发起accept调用的线程与CompletionHandler回调的线程并非同一个，不是一个上下文中，两者之间没有耦合关系。要注意到，CompletionHandler的回调共用的是 AsynchronousChannelGroup绑定的线程池，因此千万别在CompletionHandler回调方法中调用阻塞或者长时间的操作，例如sleep，回调方法最好能支持超时，防止线程池耗尽。

连接建立后，怎么读和写呢？回忆下在nonblocking nio框架中，连接建立后的第一件事是干什么？注册OP_READ事件等待socket可读。异步IO也同样如此，连接建立后马上发起一个异步read调用，等待socket可读，这个是Session.start方法中所做的事情：
<pre>
<ol>
	<li>public class AioTCPSession {</li>
	<li>    protected void start0() {</li>
	<li>        pendingRead();</li>
	<li>    }</li>
	<li></li>
	<li>    protected final void pendingRead() {</li>
	<li>        if (!isClosed() &#38;&#38; this.asynchronousSocketChannel.isOpen()) {</li>
	<li>            if (!this.readBuffer.hasRemaining()) {</li>
	<li>                this.readBuffer = ByteBufferUtils</li>
	<li>                        .increaseBufferCapatity(this.readBuffer);</li>
	<li>            }</li>
	<li>            this.readFuture = this.asynchronousSocketChannel.read(</li>
	<li>                    this.readBuffer, this, this.readCompletionHandler);</li>
	<li>        } else {</li>
	<li>            throw new IllegalStateException(</li>
	<li>                    "Session Or Channel has been closed");</li>
	<li>        }</li>
	<li>    }</li>
	<li></li>
	<li>}</li>
</ol>
</pre>
AsynchronousSocketChannel的read调用与AsynchronousServerSocketChannel的accept调用类似，同样是非阻塞的，返回结果也是一个Future，但是写的结果是整数，表示写入了多少字节，因此read调用返回的是 Future&#60;Integer&#62;，方法的第一个参数是读的缓冲区，操作系统将IO读到数据拷贝到这个缓冲区，第二个参数是传递给 CompletionHandler的attchment，第三个参数就是注册的用于回调的CompletionHandler。这里保存了read的结果Future，这是为了在关闭连接的时候能够主动取消调用，accept也是如此。现在可以看看read的CompletionHandler的实现：
<pre>
<ol>
	<li>public final class ReadCompletionHandler implements</li>
	<li>        CompletionHandler&#60;Integer, AbstractAioSession&#62; {</li>
	<li></li>
	<li>    private static final Logger log = LoggerFactory</li>
	<li>            .getLogger(ReadCompletionHandler.class);</li>
	<li>    protected final AioTCPController controller;</li>
	<li></li>
	<li>    public ReadCompletionHandler(AioTCPController controller) {</li>
	<li>        this.controller = controller;</li>
	<li>    }</li>
	<li></li>
	<li>    @Override</li>
	<li>    public void cancelled(AbstractAioSession session) {</li>
	<li>        log.warn("Session(" + session.getRemoteSocketAddress()</li>
	<li>                + ") read operation was canceled");</li>
	<li>    }</li>
	<li></li>
	<li>    @Override</li>
	<li>    public void completed(Integer result, AbstractAioSession session) {</li>
	<li>        if (log.isDebugEnabled())</li>
	<li>            log.debug("Session(" + session.getRemoteSocketAddress()</li>
	<li>                    + ") read +" + result + " bytes");</li>
	<li>        if (result &#60; 0) {</li>
	<li>            session.close();</li>
	<li>            return;</li>
	<li>        }</li>
	<li>        try {</li>
	<li>            if (result &#62; 0) {</li>
	<li>                session.updateTimeStamp();</li>
	<li>                session.getReadBuffer().flip();</li>
	<li>                session.decode();</li>
	<li>                session.getReadBuffer().compact();</li>
	<li>            }</li>
	<li>        } finally {</li>
	<li>            try {</li>
	<li>                session.pendingRead();</li>
	<li>            } catch (IOException e) {</li>
	<li>                session.onException(e);</li>
	<li>                session.close();</li>
	<li>            }</li>
	<li>        }</li>
	<li>        controller.checkSessionTimeout();</li>
	<li>    }</li>
	<li></li>
	<li>    @Override</li>
	<li>    public void failed(Throwable exc, AbstractAioSession session) {</li>
	<li>        log.error("Session read error", exc);</li>
	<li>        session.onException(exc);</li>
	<li>        session.close();</li>
	<li>    }</li>
	<li></li>
	<li>}</li>
</ol>
</pre>
如果IO读失败，会返回失败产生的异常，这种情况下我们就主动关闭连接，通过session.close()方法，这个方法干了两件事情：关闭channel和取消read调用：
<pre>
<ol>
	<li>if (null != this.readFuture) {</li>
	<li>            this.readFuture.cancel(true);</li>
	<li>        }</li>
	<li>this.asynchronousSocketChannel.close();</li>
</ol>
</pre>
在读成功的情况下，我们还需要判断结果result是否小于0，如果小于0就表示对端关闭了，这种情况下我们也主动关闭连接并返回。如果读到一定字节，也就是result大于0的情况下，我们就尝试从读缓冲区中decode出消息，并派发给业务处理器的回调方法，最终通过pendingRead继续发起read调用等待socket的下一次可读。可见，我们并不需要自己去调用channel来进行IO读，而是操作系统帮你直接读到了缓冲区，然后给你一个结果表示读入了多少字节，你处理这个结果即可。而nonblocking IO框架中，是reactor通知用户线程socket可读了，然后用户线程自己去调用read进行实际读操作。这里还有个需要注意的地方，就是decode出来的消息的派发给业务处理器工作最好交给一个线程池来处理，避免阻塞group绑定的线程池。

&#160;

IO写的操作与此类似，不过通常写的话我们会在session中关联一个缓冲队列来处理，没有完全写入或者等待写入的消息都存放在队列中，队列为空的情况下发起write调用：
<pre>
<ol>
	<li>protected void write0(WriteMessage message) {</li>
	<li>      boolean needWrite = false;</li>
	<li>      synchronized (this.writeQueue) {</li>
	<li>          needWrite = this.writeQueue.isEmpty();</li>
	<li>          this.writeQueue.offer(message);</li>
	<li>      }</li>
	<li>      if (needWrite) {</li>
	<li>          pendingWrite(message);</li>
	<li>      }</li>
	<li>  }</li>
	<li></li>
	<li>  protected final void pendingWrite(WriteMessage message) {</li>
	<li>      message = preprocessWriteMessage(message);</li>
	<li>      if (!isClosed() &#38;&#38; this.asynchronousSocketChannel.isOpen()) {</li>
	<li>          this.asynchronousSocketChannel.write(message.getWriteBuffer(),</li>
	<li>                  this, this.writeCompletionHandler);</li>
	<li>      } else {</li>
	<li>          throw new IllegalStateException(</li>
	<li>                  "Session Or Channel has been closed");</li>
	<li>      }</li>
	<li>  }</li>
</ol>
</pre>
write调用返回的结果与read一样是一个Future&#60;Integer&#62;，而write的CompletionHandler处理的核心逻辑大概是这样：
<pre>
<ol>
	<li>@Override</li>
	<li>    public void completed(Integer result, AbstractAioSession session) {</li>
	<li>        if (log.isDebugEnabled())</li>
	<li>            log.debug("Session(" + session.getRemoteSocketAddress()</li>
	<li>                    + ") writen " + result + " bytes");</li>
	<li></li>
	<li>        WriteMessage writeMessage;</li>
	<li>        Queue&#60;WriteMessage&#62; writeQueue = session.getWriteQueue();</li>
	<li>        synchronized (writeQueue) {</li>
	<li>            writeMessage = writeQueue.peek();</li>
	<li>            if (writeMessage.getWriteBuffer() == null</li>
	<li>                    &#124;&#124; !writeMessage.getWriteBuffer().hasRemaining()) {</li>
	<li>                writeQueue.remove();</li>
	<li>                if (writeMessage.getWriteFuture() != null) {</li>
	<li>                    writeMessage.getWriteFuture().setResult(Boolean.TRUE);</li>
	<li>                }</li>
	<li>                try {</li>
	<li>                    session.getHandler().onMessageSent(session,</li>
	<li>                            writeMessage.getMessage());</li>
	<li>                } catch (Exception e) {</li>
	<li>                    session.onException(e);</li>
	<li>                }</li>
	<li>                writeMessage = writeQueue.peek();</li>
	<li>            }</li>
	<li>        }</li>
	<li>        if (writeMessage != null) {</li>
	<li>            try {</li>
	<li>                session.pendingWrite(writeMessage);</li>
	<li>            } catch (IOException e) {</li>
	<li>                session.onException(e);</li>
	<li>                session.close();</li>
	<li>            }</li>
	<li>        }</li>
	<li>    }</li>
</ol>
</pre>
compete方法中的result就是实际写入的字节数，然后我们判断消息的缓冲区是否还有剩余，如果没有就将消息从队列中移除，如果队列中还有消息，那么继续发起write调用。

重复一下，这里引用的代码都是yanf4j aio分支中的源码，感兴趣的朋友可以直接check out出来看看: http://yanf4j.googlecode.com/svn/branches/yanf4j-aio。

在引入了aio之后，java对于网络层的支持已经非常完善，该有的都有了，java也已经成为服务器开发的首选语言之一。java的弱项在于对内存的管理上，由于这一切都交给了GC，因此在高性能的网络服务器上还是Cpp的天下。java这种单一堆模型比之erlang的进程内堆模型还是有差距，很难做到高效的垃圾回收和细粒度的内存管理。

这里仅仅是介绍了aio开发的核心流程，对于一个网络框架来说，还需要考虑超时的处理、缓冲buffer的处理、业务层和网络层的切分、可扩展性、性能的可调性以及一定的通用性要求。

&#160;]]></description>
			<content:encoded><![CDATA[<p>按照《Unix网络编程》的划分，IO模型可以分为：阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO，按照POSIX标准来划分只分为两类：同步IO和异步IO。如何区分呢？首先一个IO操作其实分成了两个步骤：发起IO请求和实际的IO操作，同步IO和异步IO的区别就在于第二个步骤是否阻塞，如果实际的IO读写阻塞请求进程，那么就是同步IO，因此阻塞IO、非阻塞IO、IO服用、信号驱动IO都是同步IO，如果不阻塞，而是操作系统帮你做完IO操作再将结果返回给你，那么就是异步IO。阻塞IO和非阻塞IO的区别在于第一步，发起IO请求是否会被阻塞，如果阻塞直到完成那么就是传统的阻塞IO，如果不阻塞，那么就是非阻塞IO。</p>
<p>Java nio 2.0的主要改进就是引入了异步IO（包括文件和网络），这里主要介绍下异步网络IO API的使用以及框架的设计，以TCP服务端为例。首先看下为了支持AIO引入的新的类和接口：</p>
<p>java.nio.channels.AsynchronousChannel</p>
<p>标记一个channel支持异步IO操作。</p>
<p>java.nio.channels.AsynchronousServerSocketChannel</p>
<p>ServerSocket的aio版本，创建TCP服务端，绑定地址，监听端口等。</p>
<p>java.nio.channels.AsynchronousSocketChannel</p>
<p>面向流的异步socket channel，表示一个连接。</p>
<p>java.nio.channels.AsynchronousChannelGroup</p>
<p>异步channel的分组管理，目的是为了资源共享。一个AsynchronousChannelGroup绑定一个线程池，这个线程池执行两个任务：处理IO事件和派发CompletionHandler。AsynchronousServerSocketChannel创建的时候可以传入一个 AsynchronousChannelGroup，那么通过AsynchronousServerSocketChannel创建的 AsynchronousSocketChannel将同属于一个组，共享资源。</p>
<p>java.nio.channels.CompletionHandler</p>
<p>异步IO操作结果的回调接口，用于定义在IO操作完成后所作的回调工作。AIO的API允许两种方式来处理异步操作的结果：返回的Future模式或者注册CompletionHandler，我更推荐用CompletionHandler的方式，这些handler的调用是由 AsynchronousChannelGroup的线程池派发的。显然，线程池的大小是性能的关键因素。AsynchronousChannelGroup允许绑定不同的线程池，通过三个静态方法来创建：</p>
<pre>
<ol>
<li>public static AsynchronousChannelGroup withFixedThreadPool(int nThreads,</li>
<li>                                                              ThreadFactory threadFactory)</li>
<li>       throws IOException</li>
<li></li>
<li>public static AsynchronousChannelGroup withCachedThreadPool(ExecutorService executor,</li>
<li>                                                               int initialSize)</li>
<li></li>
<li>public static AsynchronousChannelGroup withThreadPool(ExecutorService executor)</li>
<li>       throws IOException</li>
</ol>
</pre>
<p>需要根据具体应用相应调整，从框架角度出发，需要暴露这样的配置选项给用户。</p>
<p>在介绍完了aio引入的TCP的主要接口和类之后，我们来设想下一个aio框架应该怎么设计。参考非阻塞nio框架的设计，一般都是采用Reactor模式，Reacot负责事件的注册、select、事件的派发；相应地，异步IO有个Proactor模式，Proactor负责 CompletionHandler的派发，查看一个典型的IO写操作的流程来看两者的区别：</p>
<p>Reactor: send(msg) -&gt; 消息队列是否为空，如果为空 -&gt; 向Reactor注册OP_WRITE，然后返回 -&gt; Reactor select -&gt; 触发Writable，通知用户线程去处理 -&gt;先注销Writable(很多人遇到的cpu 100%的问题就在于没有注销）,处理Writeable，如果没有完全写入，继续注册OP_WRITE。注意到，写入的工作还是用户线程在处理。</p>
<p>Proactor: send(msg) -&gt; 消息队列是否为空，如果为空,发起read异步调用，并注册CompletionHandler，然后返回。 -&gt; 操作系统负责将你的消息写入，并返回结果（写入的字节数）给Proactor -&gt; Proactor派发CompletionHandler。可见，写入的工作是操作系统在处理，无需用户线程参与。事实上在aio的API 中,AsynchronousChannelGroup就扮演了Proactor的角色。</p>
<p>CompletionHandler有三个方法，分别对应于处理成功、失败、被取消（通过返回的Future)情况下的回调处理：</p>
<pre>
<ol>
<li>public interface CompletionHandler&lt;V,A&gt; {</li>
<li></li>
<li>     void completed(V result, A attachment);</li>
<li></li>
<li>    void failed(Throwable exc, A attachment);</li>
<li></li>
<li></li>
<li>    void cancelled(A attachment);</li>
<li>}</li>
</ol>
</pre>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>其中的泛型参数V表示IO调用的结果，而A是发起调用时传入的attchment。</p>
<p>在初步介绍完aio引入的类和接口后，我们看看一个典型的tcp服务端是怎么启动的，怎么接受连接并处理读和写，这里引用的代码都是yanf4j 的aio分支中的代码，可以从svn checkout，svn地址: http://yanf4j.googlecode.com/svn/branches/yanf4j-aio</p>
<p>第一步，创建一个AsynchronousServerSocketChannel，创建之前先创建一个 AsynchronousChannelGroup，上文提到AsynchronousServerSocketChannel可以绑定一个 AsynchronousChannelGroup，那么通过这个AsynchronousServerSocketChannel建立的连接都将同属于一个AsynchronousChannelGroup并共享资源：</p>
<pre>
<ol>
<li>this.asynchronousChannelGroup = AsynchronousChannelGroup</li>
<li>                    .withCachedThreadPool(Executors.newCachedThreadPool(),</li>
<li>                            this.threadPoolSize);</li>
</ol>
</pre>
<p>&nbsp;</p>
<p>然后初始化一个AsynchronousServerSocketChannel，通过open方法：</p>
<pre>
<ol>
<li>this.serverSocketChannel = AsynchronousServerSocketChannel</li>
<li>                .open(this.asynchronousChannelGroup);</li>
</ol>
</pre>
<p>通过nio 2.0引入的SocketOption类设置一些TCP选项：</p>
<pre>
<ol>
<li>this.serverSocketChannel</li>
<li>                    .setOption(</li>
<li>                            StandardSocketOption.SO_REUSEADDR,true);</li>
<li>this.serverSocketChannel</li>
<li>                    .setOption(</li>
<li>                            StandardSocketOption.SO_RCVBUF,16*1024);</li>
</ol>
</pre>
<p>绑定本地地址：</p>
<pre>
<ol>
<li>this.serverSocketChannel</li>
<li>                    .bind(new InetSocketAddress("localhost",8080), 100);</li>
</ol>
</pre>
<p>其中的100用于指定等待连接的队列大小(backlog)。完了吗？还没有，最重要的监听工作还没开始，监听端口是为了等待连接上来以便accept产生一个AsynchronousSocketChannel来表示一个新建立的连接，因此需要发起一个accept调用，调用是异步的，操作系统将在连接建立后，将最后的结果——AsynchronousSocketChannel返回给你</p>
<pre>
<ol>
<li>public void pendingAccept() {</li>
<li>        if (this.started &amp;&amp; this.serverSocketChannel.isOpen()) {</li>
<li>            this.acceptFuture = this.serverSocketChannel.accept(null,</li>
<li>                    new AcceptCompletionHandler());</li>
<li></li>
<li>        } else {</li>
<li>            throw new IllegalStateException("Controller has been closed");</li>
<li>        }</li>
<li>    }</li>
</ol>
</pre>
<p>注意，重复的accept调用将会抛出PendingAcceptException，后文提到的read和write也是如此。accept方法的第一个参数是你想传给CompletionHandler的attchment，第二个参数就是注册的用于回调的CompletionHandler，最后返回结果Future&lt;AsynchronousSocketChannel&gt;。你可以对future做处理，这里采用更推荐的方式就是注册一个CompletionHandler。那么accept的CompletionHandler中做些什么工作呢？显然一个赤裸裸的 AsynchronousSocketChannel是不够的，我们需要将它封装成session，一个session表示一个连接（mina里就叫 IoSession了），里面带了一个缓冲的消息队列以及一些其他资源等。在连接建立后，除非你的服务器只准备接受一个连接，不然你需要在后面继续调用pendingAccept来发起另一个accept请求：</p>
<pre>
<ol>
<li>private final class AcceptCompletionHandler implements</li>
<li>            CompletionHandler&lt;AsynchronousSocketChannel, Object&gt; {</li>
<li></li>
<li>        @Override</li>
<li>        public void cancelled(Object attachment) {</li>
<li>            logger.warn("Accept operation was canceled");</li>
<li>        }</li>
<li></li>
<li>        @Override</li>
<li>        public void completed(AsynchronousSocketChannel socketChannel,</li>
<li>                Object attachment) {</li>
<li>            try {</li>
<li>                logger.debug("Accept connection from "</li>
<li>                        + socketChannel.getRemoteAddress());</li>
<li>                configureChannel(socketChannel);</li>
<li>                AioSessionConfig sessionConfig = buildSessionConfig(socketChannel);</li>
<li>                Session session = new AioTCPSession(sessionConfig,</li>
<li>                        AioTCPController.this.configuration</li>
<li>                                .getSessionReadBufferSize(),</li>
<li>                        AioTCPController.this.sessionTimeout);</li>
<li>                session.start();</li>
<li>                registerSession(session);</li>
<li>            } catch (Exception e) {</li>
<li>                e.printStackTrace();</li>
<li>                logger.error("Accept error", e);</li>
<li>                notifyException(e);</li>
<li>            } finally {</li>
<li>                &lt;strong&gt;pendingAccept&lt;/strong&gt;();</li>
<li>            }</li>
<li>        }</li>
<li></li>
<li>        @Override</li>
<li>        public void failed(Throwable exc, Object attachment) {</li>
<li>            logger.error("Accept error", exc);</li>
<li>            try {</li>
<li>                notifyException(exc);</li>
<li>            } finally {</li>
<li>                &lt;strong&gt;pendingAccept&lt;/strong&gt;();</li>
<li>            }</li>
<li>        }</li>
<li>    }</li>
</ol>
</pre>
<p>注意到了吧，我们在failed和completed方法中在最后都调用了pendingAccept来继续发起accept调用，等待新的连接上来。有的同学可能要说了，这样搞是不是递归调用，会不会堆栈溢出？实际上不会，因为发起accept调用的线程与CompletionHandler回调的线程并非同一个，不是一个上下文中，两者之间没有耦合关系。要注意到，CompletionHandler的回调共用的是 AsynchronousChannelGroup绑定的线程池，因此千万别在CompletionHandler回调方法中调用阻塞或者长时间的操作，例如sleep，回调方法最好能支持超时，防止线程池耗尽。</p>
<p>连接建立后，怎么读和写呢？回忆下在nonblocking nio框架中，连接建立后的第一件事是干什么？注册OP_READ事件等待socket可读。异步IO也同样如此，连接建立后马上发起一个异步read调用，等待socket可读，这个是Session.start方法中所做的事情：</p>
<pre>
<ol>
<li>public class AioTCPSession {</li>
<li>    protected void start0() {</li>
<li>        pendingRead();</li>
<li>    }</li>
<li></li>
<li>    protected final void pendingRead() {</li>
<li>        if (!isClosed() &amp;&amp; this.asynchronousSocketChannel.isOpen()) {</li>
<li>            if (!this.readBuffer.hasRemaining()) {</li>
<li>                this.readBuffer = ByteBufferUtils</li>
<li>                        .increaseBufferCapatity(this.readBuffer);</li>
<li>            }</li>
<li>            this.readFuture = this.asynchronousSocketChannel.read(</li>
<li>                    this.readBuffer, this, this.readCompletionHandler);</li>
<li>        } else {</li>
<li>            throw new IllegalStateException(</li>
<li>                    "Session Or Channel has been closed");</li>
<li>        }</li>
<li>    }</li>
<li></li>
<li>}</li>
</ol>
</pre>
<p>AsynchronousSocketChannel的read调用与AsynchronousServerSocketChannel的accept调用类似，同样是非阻塞的，返回结果也是一个Future，但是写的结果是整数，表示写入了多少字节，因此read调用返回的是 Future&lt;Integer&gt;，方法的第一个参数是读的缓冲区，操作系统将IO读到数据拷贝到这个缓冲区，第二个参数是传递给 CompletionHandler的attchment，第三个参数就是注册的用于回调的CompletionHandler。这里保存了read的结果Future，这是为了在关闭连接的时候能够主动取消调用，accept也是如此。现在可以看看read的CompletionHandler的实现：</p>
<pre>
<ol>
<li>public final class ReadCompletionHandler implements</li>
<li>        CompletionHandler&lt;Integer, AbstractAioSession&gt; {</li>
<li></li>
<li>    private static final Logger log = LoggerFactory</li>
<li>            .getLogger(ReadCompletionHandler.class);</li>
<li>    protected final AioTCPController controller;</li>
<li></li>
<li>    public ReadCompletionHandler(AioTCPController controller) {</li>
<li>        this.controller = controller;</li>
<li>    }</li>
<li></li>
<li>    @Override</li>
<li>    public void cancelled(AbstractAioSession session) {</li>
<li>        log.warn("Session(" + session.getRemoteSocketAddress()</li>
<li>                + ") read operation was canceled");</li>
<li>    }</li>
<li></li>
<li>    @Override</li>
<li>    public void completed(Integer result, AbstractAioSession session) {</li>
<li>        if (log.isDebugEnabled())</li>
<li>            log.debug("Session(" + session.getRemoteSocketAddress()</li>
<li>                    + ") read +" + result + " bytes");</li>
<li>        if (result &lt; 0) {</li>
<li>            session.close();</li>
<li>            return;</li>
<li>        }</li>
<li>        try {</li>
<li>            if (result &gt; 0) {</li>
<li>                session.updateTimeStamp();</li>
<li>                session.getReadBuffer().flip();</li>
<li>                session.decode();</li>
<li>                session.getReadBuffer().compact();</li>
<li>            }</li>
<li>        } finally {</li>
<li>            try {</li>
<li>                session.pendingRead();</li>
<li>            } catch (IOException e) {</li>
<li>                session.onException(e);</li>
<li>                session.close();</li>
<li>            }</li>
<li>        }</li>
<li>        controller.checkSessionTimeout();</li>
<li>    }</li>
<li></li>
<li>    @Override</li>
<li>    public void failed(Throwable exc, AbstractAioSession session) {</li>
<li>        log.error("Session read error", exc);</li>
<li>        session.onException(exc);</li>
<li>        session.close();</li>
<li>    }</li>
<li></li>
<li>}</li>
</ol>
</pre>
<p>如果IO读失败，会返回失败产生的异常，这种情况下我们就主动关闭连接，通过session.close()方法，这个方法干了两件事情：关闭channel和取消read调用：</p>
<pre>
<ol>
<li>if (null != this.readFuture) {</li>
<li>            this.readFuture.cancel(true);</li>
<li>        }</li>
<li>this.asynchronousSocketChannel.close();</li>
</ol>
</pre>
<p>在读成功的情况下，我们还需要判断结果result是否小于0，如果小于0就表示对端关闭了，这种情况下我们也主动关闭连接并返回。如果读到一定字节，也就是result大于0的情况下，我们就尝试从读缓冲区中decode出消息，并派发给业务处理器的回调方法，最终通过pendingRead继续发起read调用等待socket的下一次可读。可见，我们并不需要自己去调用channel来进行IO读，而是操作系统帮你直接读到了缓冲区，然后给你一个结果表示读入了多少字节，你处理这个结果即可。而nonblocking IO框架中，是reactor通知用户线程socket可读了，然后用户线程自己去调用read进行实际读操作。这里还有个需要注意的地方，就是decode出来的消息的派发给业务处理器工作最好交给一个线程池来处理，避免阻塞group绑定的线程池。</p>
<p>&nbsp;</p>
<p>IO写的操作与此类似，不过通常写的话我们会在session中关联一个缓冲队列来处理，没有完全写入或者等待写入的消息都存放在队列中，队列为空的情况下发起write调用：</p>
<pre>
<ol>
<li>protected void write0(WriteMessage message) {</li>
<li>      boolean needWrite = false;</li>
<li>      synchronized (this.writeQueue) {</li>
<li>          needWrite = this.writeQueue.isEmpty();</li>
<li>          this.writeQueue.offer(message);</li>
<li>      }</li>
<li>      if (needWrite) {</li>
<li>          pendingWrite(message);</li>
<li>      }</li>
<li>  }</li>
<li></li>
<li>  protected final void pendingWrite(WriteMessage message) {</li>
<li>      message = preprocessWriteMessage(message);</li>
<li>      if (!isClosed() &amp;&amp; this.asynchronousSocketChannel.isOpen()) {</li>
<li>          this.asynchronousSocketChannel.write(message.getWriteBuffer(),</li>
<li>                  this, this.writeCompletionHandler);</li>
<li>      } else {</li>
<li>          throw new IllegalStateException(</li>
<li>                  "Session Or Channel has been closed");</li>
<li>      }</li>
<li>  }</li>
</ol>
</pre>
<p>write调用返回的结果与read一样是一个Future&lt;Integer&gt;，而write的CompletionHandler处理的核心逻辑大概是这样：</p>
<pre>
<ol>
<li>@Override</li>
<li>    public void completed(Integer result, AbstractAioSession session) {</li>
<li>        if (log.isDebugEnabled())</li>
<li>            log.debug("Session(" + session.getRemoteSocketAddress()</li>
<li>                    + ") writen " + result + " bytes");</li>
<li></li>
<li>        WriteMessage writeMessage;</li>
<li>        Queue&lt;WriteMessage&gt; writeQueue = session.getWriteQueue();</li>
<li>        synchronized (writeQueue) {</li>
<li>            writeMessage = writeQueue.peek();</li>
<li>            if (writeMessage.getWriteBuffer() == null</li>
<li>                    || !writeMessage.getWriteBuffer().hasRemaining()) {</li>
<li>                writeQueue.remove();</li>
<li>                if (writeMessage.getWriteFuture() != null) {</li>
<li>                    writeMessage.getWriteFuture().setResult(Boolean.TRUE);</li>
<li>                }</li>
<li>                try {</li>
<li>                    session.getHandler().onMessageSent(session,</li>
<li>                            writeMessage.getMessage());</li>
<li>                } catch (Exception e) {</li>
<li>                    session.onException(e);</li>
<li>                }</li>
<li>                writeMessage = writeQueue.peek();</li>
<li>            }</li>
<li>        }</li>
<li>        if (writeMessage != null) {</li>
<li>            try {</li>
<li>                session.pendingWrite(writeMessage);</li>
<li>            } catch (IOException e) {</li>
<li>                session.onException(e);</li>
<li>                session.close();</li>
<li>            }</li>
<li>        }</li>
<li>    }</li>
</ol>
</pre>
<p>compete方法中的result就是实际写入的字节数，然后我们判断消息的缓冲区是否还有剩余，如果没有就将消息从队列中移除，如果队列中还有消息，那么继续发起write调用。</p>
<p>重复一下，这里引用的代码都是yanf4j aio分支中的源码，感兴趣的朋友可以直接check out出来看看: http://yanf4j.googlecode.com/svn/branches/yanf4j-aio。</p>
<p>在引入了aio之后，java对于网络层的支持已经非常完善，该有的都有了，java也已经成为服务器开发的首选语言之一。java的弱项在于对内存的管理上，由于这一切都交给了GC，因此在高性能的网络服务器上还是Cpp的天下。java这种单一堆模型比之erlang的进程内堆模型还是有差距，很难做到高效的垃圾回收和细粒度的内存管理。</p>
<p>这里仅仅是介绍了aio开发的核心流程，对于一个网络框架来说，还需要考虑超时的处理、缓冲buffer的处理、业务层和网络层的切分、可扩展性、性能的可调性以及一定的通用性要求。</p>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>http://www.wdcode.org/archives/526.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Java NIO开发实例</title>
		<link>http://www.wdcode.org/archives/523.html</link>
		<comments>http://www.wdcode.org/archives/523.html#comments</comments>
		<pubDate>Thu, 15 Dec 2011 06:44:36 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[J2SE]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[NIO]]></category>

		<guid isPermaLink="false">http://www.wdcode.org/?p=523</guid>
		<description><![CDATA[最近一直在研究java nio，提出一点浅见，希望能和大家分享！！！废话不多说了，直接进入主题！

首先了解下所谓的java nio是个什么东西！

传统的并发型服务器设计是利用阻塞型网络I/O 以多线程的模式来实现的，然而由于系统常常在进行网络读写时处于阻塞状态，会大大影响系统的性能；自Java1. 4 开始引入了NIO(新I/O) API，通过使用非阻塞型I/O，实现流畅的网络读写操作，为开发高性能并发型服务器程序提供了一个很好的解决方案。这就是java nio

<strong>首先来看下传统的阻塞型网络I/O的不足</strong>

Java 平台传统的I/O 系统都是基于Byte（字节）和Stream（数据流）的，相应的I/O 操作都是阻塞型的，所以服务器程序也采用阻塞型I/O 进行数据的读、写操作。本文以TCP长连接模式来讨论并发型服务器的相关设计，为了实现服务器程序的并发性要求，系统由一个单独的主线程来监听用户发起的连接请求，一直处于阻塞状态；当有用户连接请求到来时，程序都会启一个新的线程来统一处理用户数据的读、写操作。

这种模式的优点是简单、实用、易管理；然而缺点也是显而易见的：由于是为每一个客户端分配一个线程来处理输入、输出数据，其线程与客户机的比例近似为1：1，随着线程数量的不断增加，服务器启动了大量的并发线程，会大大加大系统对线程的管理开销，这将成为吞吐量瓶颈的主要原因；其次由于底层的I/O 操作采用的同步模式，I/O 操作的阻塞管理粒度是以服务于请求的线程为单位的,有可能大量的线程会闲置,处于盲等状态，造成I/O资源利用率不高，影响整个系统的性能。

对于并发型服务器，系统用在阻塞型I/O 等待和线程间切换的时间远远多于CPU 在内存中处理数据的时间，因此传统的阻塞型I/O 已经成为制约系统性能的瓶颈。Java1.4 版本后推出的NIO 工具包，提供了非阻塞型I/O 的异步输入输出机制，为提高系统的性能提供了可实现的基础机制。

<strong>NIO 包及工作原理</strong>

针对传统I/O 工作模式的不足，NIO 工具包提出了基于Buffer（缓冲区）、Channel（通道）、Selector（选择器）的新模式；Selector（选择器）、可选择的Channel（通道）和SelectionKey（选择键）配合起来使用，可以实现并发的非阻塞型I/O 能力。

<strong>NIO 工具包的成员</strong>

<strong>Buffer（缓冲器）</strong>

Buffer 类是一个抽象类，它有7 个子类分别对应于七种基本的数据类型：ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer 和ShortBuffer。每一个Buffer对象相当于一个数据容器，可以把它看作内存中的一个大的数组，用来存储和提取所有基本类型(boolean 型除外)的数据。Buffer 类的核心是一块内存区，可以直接对其执行与内存有关的操作，利用操作系统特性和能力提高和改善Java 传统I/O 的性能。

<strong>Channel（通道）</strong>

Channel 被认为是NIO 工具包的一大创新点，是(Buffer)缓冲器和I/O 服务之间的通道，具有双向性，既可以读入也可以写出，可以更高效的传递数据。我们这里主要讨ServerSocketChannel 和SocketChannel，它们都继承了SelectableChannel，是可选择的通道，分别可以工作在同步和异步两种方式下（这里的可选择不是指可以选择两种工作方式，而是指可以有选择的注册自己感兴趣的事件）。当通道工作在同步方式时，它的功能和编程方法与传统的ServerSocket、Socket 对象相似；当通道工作在异步工作方式时，进行输入输出处理不必等到输入输出完毕才返回，并且可以将其感兴趣的（如：接受操作、连接操作、读出操作、写入操作）事件注册到Selector 对象上，与Selector 对象协同工作可以更有效率的支持和管理并发的网络套接字连接。

<strong>Selector（选择器）和SelectionKey（选择键）</strong>

各类 Buffer 是数据的容器对象；各类Channel 实现在各类Buffer 与各类I/O 服务间传输数据。Selector 是实现并发型非阻塞I/O 的核心，各种可选择的通道将其感兴趣的事件注册到Selector 对象上，Selector 在一个循环中不断轮循监视这各些注册在其上的Socket 通道。SelectionKey 类则封装了SelectableChannel 对象在Selector 中的注册信息。当Selector 监测到在某个注册的SelectableChannel 上发生了感兴趣的事件时,自动激活产生一个SelectionKey对象,在这个对象中记录了哪一个SelectableChannel 上发生了哪种事件，通过对被激活的SelectionKey 的分析,外界可以知道每个SelectableChannel 发生的具体事件类型,进行相应的处理。

<strong>NIO 工作原理</strong>

通过上面的讨论，我们可以看出在并发型服务器程序中使用NIO，实际上是通过网络事件驱动模型实现的。我们应用Select 机制，不用为每一个客户端连接新启线程处理，而是将其注册到特定的Selector 对象上，这就可以在单线程中利用Selector 对象管理大量并发的网络连接，更好的利用了系统资源；采用非阻塞I/O 的通信方式，不要求阻塞等待I/O 操作完成即可返回，从而减少了管理I/O 连接导致的系统开销，大幅度提高了系统性能。

当有读或写等任何注册的事件发生时，可以从Selector 中获得相应的SelectionKey ， 从SelectionKey 中可以找到发生的事件和该事件所发生的具体的SelectableChannel，以获得客户端发送过来的数据。由于在非阻塞网络I/O 中采用了事件触发机制，处理程序可以得到系统的主动通知，从而可以实现底层网络I/O 无阻塞、流畅地读写，而不像在原来的阻塞模式下处理程序需要不断循环等待。使用NIO，可以编写出性能更好、更易扩展的并发型服务器程序。

<strong>并发型服务器程序的实现代码</strong>

应用 NIO 工具包，基于非阻塞网络I/O 设计的并发型服务器程序与以往基于阻塞I/O 的实现程序有很大不同，在使用非阻塞网络I/O 的情况下，程序读取数据和写入数据的时机不是由程序员控制的，而是Selector 决定的。下面便给出基于非阻塞网络I/O 的并发型服务器程序的核心代码片段：
<pre>
<ol>
	<li>import java.io.*； //引入Java.io包</li>
	<li>import java.net.*； //引入Java.net包</li>
	<li>import java.nio.channels.*； //引入Java.nio.channels包</li>
	<li>import java.util.*； //引入Java.util包</li>
	<li>public class TestServer implements Runnable</li>
	<li></li>
	<li>{</li>
	<li>/**</li>
	<li></li>
	<li>* 服务器Channel对象，负责接受用户连接</li>
	<li>*/</li>
	<li>private ServerSocketChannel server；</li>
	<li>/**</li>
	<li>* Selector对象，负责监控所有的连接到服务器的网络事件的发生</li>
	<li>*/</li>
	<li>private Selector selector；</li>
	<li>/**</li>
	<li>* 总的活动连接数</li>
	<li>*/</li>
	<li>private int activeSockets；</li>
	<li>/**</li>
	<li>* 服务器Channel绑定的端口号</li>
	<li>*/</li>
	<li>private int port ；</li>
	<li>/**</li>
	<li>*</li>
	<li>* 构造函数</li>
	<li>*/</li>
	<li>public TestServer()throws IOException</li>
	<li>{</li>
	<li>activeSockets=0；</li>
	<li>port=9999；//初始化服务器Channel绑定的端口号为9999</li>
	<li>selector= Selector.open()；//初始化Selector对象</li>
	<li>server=ServerSocketChannel.open()；//初始化服务器Channel对象</li>
	<li>ServerSocket socket=server.socket()；//获取服务器Channel对应的//ServerSocket对象</li>
	<li>socket.bind(new InetSocketAddress(port))；//把Socket绑定到监听端口9999上</li>
	<li>server.configureBlocking(false)；//将服务器Channel设置为非阻塞模式</li>
	<li>server.register(selector,SelectionKey.OP_ACCEPT)；//将服务器Channel注册到</li>
	<li>Selector对象，并指出服务器Channel所感兴趣的事件为可接受请求操作</li>
	<li>}</li>
	<li>public void run()</li>
	<li>{</li>
	<li>while(true)</li>
	<li>{</li>
	<li>try</li>
	<li>{</li>
	<li>/**</li>
	<li>*应用Select机制轮循是否有用户感兴趣的新的网络事件发生，当没有</li>
	<li></li>
	<li>* 新的网络事件发生时，此方法会阻塞，直到有新的网络事件发生为止</li>
	<li>*/</li>
	<li>selector.select()；</li>
	<li></li>
	<li>}</li>
	<li>catch(IOException e)</li>
	<li>{</li>
	<li>continue；//当有异常发生时，继续进行循环操作</li>
	<li>}</li>
	<li>/**</li>
	<li>* 得到活动的网络连接选择键的集合</li>
	<li>*/</li>
	<li>Set&#60;SelectionKey&#62; keys=selector.selectedKeys()；</li>
	<li>activeSockets=keys.size()；//获取活动连接的数目</li>
	<li>if(activeSockets==0)</li>
	<li>{</li>
	<li>continue；//如果连接数为0，则继续进行循环操作</li>
	<li>}</li>
	<li>/**</li>
	<li></li>
	<li>/**</li>
	<li>* 应用For—Each循环遍历整个选择键集合</li>
	<li>*/</li>
	<li>for(SelectionKey key :keys)</li>
	<li>{</li>
	<li>/**</li>
	<li>* 如果关键字状态是为可接受，则接受连接，注册通道，以接受更多的*</li>
	<li>事件，进行相关的服务器程序处理</li>
	<li>*/</li>
	<li>if(key.isAcceptable())</li>
	<li>{</li>
	<li>doServerSocketEvent(key)；</li>
	<li>continue；</li>
	<li>}</li>
	<li>/**</li>
	<li>* 如果关键字状态为可读，则说明Channel是一个客户端的连接通道，</li>
	<li>* 进行相应的读取客户端数据的操作</li>
	<li>*/</li>
	<li>if(key.isReadable())</li>
	<li>{</li>
	<li>doClientReadEvent(key)；</li>
	<li>continue；</li>
	<li>}</li>
	<li>/**</li>
	<li>* 如果关键字状态为可写，则也说明Channel是一个客户端的连接通道，</li>
	<li>* 进行相应的向客户端写数据的操作</li>
	<li>*/</li>
	<li>if(key.isWritable())</li>
	<li>{</li>
	<li>doClinetWriteEvent(key)；</li>
	<li>continue；</li>
	<li>}</li>
	<li>}</li>
	<li>}</li>
	<li>}</li>
	<li></li>
	<li>/**</li>
	<li>* 处理服务器事件操作</li>
	<li>* @param key 服务器选择键对象</li>
	<li>*/</li>
	<li>private void doServerSocketEvent(SelectionKey key)</li>
	<li>{</li>
	<li>SocketChannel client=null；</li>
	<li>try</li>
	<li>{</li>
	<li>ServerSocketChannel server=(ServerSocketChannel)key.channel()；</li>
	<li>client=server.accept()；</li>
	<li>if(client==null)</li>
	<li>{</li>
	<li>return；</li>
	<li>}</li>
	<li>client.configureBlocking(false)；//将客户端Channel设置为非阻塞型</li>
	<li>/**</li>
	<li></li>
	<li>/**</li>
	<li>* 将客户端Channel注册到Selector对象上，并且指出客户端Channel所感</li>
	<li>* 兴趣的事件为可读和可写</li>
	<li>*/</li>
	<li>client.register(selector,SelectionKey.OP_READ&#124;SelectionKey.OP_READ)；</li>
	<li>}catch(IOException e)</li>
	<li>{</li>
	<li>try</li>
	<li>{</li>
	<li>client.close()；</li>
	<li></li>
	<li>}catch(IOException e1){}</li>
	<li>}</li>
	<li>}</li>
	<li>/**</li>
	<li>* 进行向客户端写数据操作</li>
	<li>* @param key 客户端选择键对象</li>
	<li>*/</li>
	<li>private void doClinetWriteEvent(SelectionKey key)</li>
	<li>{</li>
	<li>代码实现略；</li>
	<li>}</li>
	<li>/**</li>
	<li>* 进行读取客户短数据操作</li>
	<li>* @param key 客户端选择键对象</li>
	<li>*/</li>
	<li>private void doClientReadEvent(SelectionKey key)</li>
	<li>{</li>
	<li>代码实现略；</li>
	<li>}</li>
	<li>}</li>
</ol>
</pre>
从上面对代码可以看出，使用非阻塞性I/O进行并发型服务器程序设计分三个部分：1.向Selector对象注册感兴趣的事件；2.从Selector中获取所感兴趣的事件；3.根据不同的事件进行相应的处理。

<strong>结  语</strong>

通过使用NIO 工具包进行并发型服务器程序设计，一个或者很少几个Socket 线程就可以处理成千上万个活动的Socket 连接，大大降低了服务器端程序的开销；同时网络I/O 采取非阻塞模式，线程不再在读或写时阻塞，操作系统可以更流畅的读写数据并可以更有效地向CPU 传递数据进行处理，以便更有效地提高系统的性能。

&#160;]]></description>
			<content:encoded><![CDATA[<p>最近一直在研究java nio，提出一点浅见，希望能和大家分享！！！废话不多说了，直接进入主题！</p>
<p>首先了解下所谓的java nio是个什么东西！</p>
<p>传统的并发型服务器设计是利用阻塞型网络I/O 以多线程的模式来实现的，然而由于系统常常在进行网络读写时处于阻塞状态，会大大影响系统的性能；自Java1. 4 开始引入了NIO(新I/O) API，通过使用非阻塞型I/O，实现流畅的网络读写操作，为开发高性能并发型服务器程序提供了一个很好的解决方案。这就是java nio</p>
<p><strong>首先来看下传统的阻塞型网络I/O的不足</strong></p>
<p>Java 平台传统的I/O 系统都是基于Byte（字节）和Stream（数据流）的，相应的I/O 操作都是阻塞型的，所以服务器程序也采用阻塞型I/O 进行数据的读、写操作。本文以TCP长连接模式来讨论并发型服务器的相关设计，为了实现服务器程序的并发性要求，系统由一个单独的主线程来监听用户发起的连接请求，一直处于阻塞状态；当有用户连接请求到来时，程序都会启一个新的线程来统一处理用户数据的读、写操作。</p>
<p>这种模式的优点是简单、实用、易管理；然而缺点也是显而易见的：由于是为每一个客户端分配一个线程来处理输入、输出数据，其线程与客户机的比例近似为1：1，随着线程数量的不断增加，服务器启动了大量的并发线程，会大大加大系统对线程的管理开销，这将成为吞吐量瓶颈的主要原因；其次由于底层的I/O 操作采用的同步模式，I/O 操作的阻塞管理粒度是以服务于请求的线程为单位的,有可能大量的线程会闲置,处于盲等状态，造成I/O资源利用率不高，影响整个系统的性能。</p>
<p>对于并发型服务器，系统用在阻塞型I/O 等待和线程间切换的时间远远多于CPU 在内存中处理数据的时间，因此传统的阻塞型I/O 已经成为制约系统性能的瓶颈。Java1.4 版本后推出的NIO 工具包，提供了非阻塞型I/O 的异步输入输出机制，为提高系统的性能提供了可实现的基础机制。</p>
<p><strong>NIO 包及工作原理</strong></p>
<p>针对传统I/O 工作模式的不足，NIO 工具包提出了基于Buffer（缓冲区）、Channel（通道）、Selector（选择器）的新模式；Selector（选择器）、可选择的Channel（通道）和SelectionKey（选择键）配合起来使用，可以实现并发的非阻塞型I/O 能力。</p>
<p><strong>NIO 工具包的成员</strong></p>
<p><strong>Buffer（缓冲器）</strong></p>
<p>Buffer 类是一个抽象类，它有7 个子类分别对应于七种基本的数据类型：ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer 和ShortBuffer。每一个Buffer对象相当于一个数据容器，可以把它看作内存中的一个大的数组，用来存储和提取所有基本类型(boolean 型除外)的数据。Buffer 类的核心是一块内存区，可以直接对其执行与内存有关的操作，利用操作系统特性和能力提高和改善Java 传统I/O 的性能。</p>
<p><strong>Channel（通道）</strong></p>
<p>Channel 被认为是NIO 工具包的一大创新点，是(Buffer)缓冲器和I/O 服务之间的通道，具有双向性，既可以读入也可以写出，可以更高效的传递数据。我们这里主要讨ServerSocketChannel 和SocketChannel，它们都继承了SelectableChannel，是可选择的通道，分别可以工作在同步和异步两种方式下（这里的可选择不是指可以选择两种工作方式，而是指可以有选择的注册自己感兴趣的事件）。当通道工作在同步方式时，它的功能和编程方法与传统的ServerSocket、Socket 对象相似；当通道工作在异步工作方式时，进行输入输出处理不必等到输入输出完毕才返回，并且可以将其感兴趣的（如：接受操作、连接操作、读出操作、写入操作）事件注册到Selector 对象上，与Selector 对象协同工作可以更有效率的支持和管理并发的网络套接字连接。</p>
<p><strong>Selector（选择器）和SelectionKey（选择键）</strong></p>
<p>各类 Buffer 是数据的容器对象；各类Channel 实现在各类Buffer 与各类I/O 服务间传输数据。Selector 是实现并发型非阻塞I/O 的核心，各种可选择的通道将其感兴趣的事件注册到Selector 对象上，Selector 在一个循环中不断轮循监视这各些注册在其上的Socket 通道。SelectionKey 类则封装了SelectableChannel 对象在Selector 中的注册信息。当Selector 监测到在某个注册的SelectableChannel 上发生了感兴趣的事件时,自动激活产生一个SelectionKey对象,在这个对象中记录了哪一个SelectableChannel 上发生了哪种事件，通过对被激活的SelectionKey 的分析,外界可以知道每个SelectableChannel 发生的具体事件类型,进行相应的处理。</p>
<p><strong>NIO 工作原理</strong></p>
<p>通过上面的讨论，我们可以看出在并发型服务器程序中使用NIO，实际上是通过网络事件驱动模型实现的。我们应用Select 机制，不用为每一个客户端连接新启线程处理，而是将其注册到特定的Selector 对象上，这就可以在单线程中利用Selector 对象管理大量并发的网络连接，更好的利用了系统资源；采用非阻塞I/O 的通信方式，不要求阻塞等待I/O 操作完成即可返回，从而减少了管理I/O 连接导致的系统开销，大幅度提高了系统性能。</p>
<p>当有读或写等任何注册的事件发生时，可以从Selector 中获得相应的SelectionKey ， 从SelectionKey 中可以找到发生的事件和该事件所发生的具体的SelectableChannel，以获得客户端发送过来的数据。由于在非阻塞网络I/O 中采用了事件触发机制，处理程序可以得到系统的主动通知，从而可以实现底层网络I/O 无阻塞、流畅地读写，而不像在原来的阻塞模式下处理程序需要不断循环等待。使用NIO，可以编写出性能更好、更易扩展的并发型服务器程序。</p>
<p><strong>并发型服务器程序的实现代码</strong></p>
<p>应用 NIO 工具包，基于非阻塞网络I/O 设计的并发型服务器程序与以往基于阻塞I/O 的实现程序有很大不同，在使用非阻塞网络I/O 的情况下，程序读取数据和写入数据的时机不是由程序员控制的，而是Selector 决定的。下面便给出基于非阻塞网络I/O 的并发型服务器程序的核心代码片段：</p>
<pre>
<ol>
<li>import java.io.*； //引入Java.io包</li>
<li>import java.net.*； //引入Java.net包</li>
<li>import java.nio.channels.*； //引入Java.nio.channels包</li>
<li>import java.util.*； //引入Java.util包</li>
<li>public class TestServer implements Runnable</li>
<li></li>
<li>{</li>
<li>/**</li>
<li></li>
<li>* 服务器Channel对象，负责接受用户连接</li>
<li>*/</li>
<li>private ServerSocketChannel server；</li>
<li>/**</li>
<li>* Selector对象，负责监控所有的连接到服务器的网络事件的发生</li>
<li>*/</li>
<li>private Selector selector；</li>
<li>/**</li>
<li>* 总的活动连接数</li>
<li>*/</li>
<li>private int activeSockets；</li>
<li>/**</li>
<li>* 服务器Channel绑定的端口号</li>
<li>*/</li>
<li>private int port ；</li>
<li>/**</li>
<li>*</li>
<li>* 构造函数</li>
<li>*/</li>
<li>public TestServer()throws IOException</li>
<li>{</li>
<li>activeSockets=0；</li>
<li>port=9999；//初始化服务器Channel绑定的端口号为9999</li>
<li>selector= Selector.open()；//初始化Selector对象</li>
<li>server=ServerSocketChannel.open()；//初始化服务器Channel对象</li>
<li>ServerSocket socket=server.socket()；//获取服务器Channel对应的//ServerSocket对象</li>
<li>socket.bind(new InetSocketAddress(port))；//把Socket绑定到监听端口9999上</li>
<li>server.configureBlocking(false)；//将服务器Channel设置为非阻塞模式</li>
<li>server.register(selector,SelectionKey.OP_ACCEPT)；//将服务器Channel注册到</li>
<li>Selector对象，并指出服务器Channel所感兴趣的事件为可接受请求操作</li>
<li>}</li>
<li>public void run()</li>
<li>{</li>
<li>while(true)</li>
<li>{</li>
<li>try</li>
<li>{</li>
<li>/**</li>
<li>*应用Select机制轮循是否有用户感兴趣的新的网络事件发生，当没有</li>
<li></li>
<li>* 新的网络事件发生时，此方法会阻塞，直到有新的网络事件发生为止</li>
<li>*/</li>
<li>selector.select()；</li>
<li></li>
<li>}</li>
<li>catch(IOException e)</li>
<li>{</li>
<li>continue；//当有异常发生时，继续进行循环操作</li>
<li>}</li>
<li>/**</li>
<li>* 得到活动的网络连接选择键的集合</li>
<li>*/</li>
<li>Set&lt;SelectionKey&gt; keys=selector.selectedKeys()；</li>
<li>activeSockets=keys.size()；//获取活动连接的数目</li>
<li>if(activeSockets==0)</li>
<li>{</li>
<li>continue；//如果连接数为0，则继续进行循环操作</li>
<li>}</li>
<li>/**</li>
<li></li>
<li>/**</li>
<li>* 应用For—Each循环遍历整个选择键集合</li>
<li>*/</li>
<li>for(SelectionKey key :keys)</li>
<li>{</li>
<li>/**</li>
<li>* 如果关键字状态是为可接受，则接受连接，注册通道，以接受更多的*</li>
<li>事件，进行相关的服务器程序处理</li>
<li>*/</li>
<li>if(key.isAcceptable())</li>
<li>{</li>
<li>doServerSocketEvent(key)；</li>
<li>continue；</li>
<li>}</li>
<li>/**</li>
<li>* 如果关键字状态为可读，则说明Channel是一个客户端的连接通道，</li>
<li>* 进行相应的读取客户端数据的操作</li>
<li>*/</li>
<li>if(key.isReadable())</li>
<li>{</li>
<li>doClientReadEvent(key)；</li>
<li>continue；</li>
<li>}</li>
<li>/**</li>
<li>* 如果关键字状态为可写，则也说明Channel是一个客户端的连接通道，</li>
<li>* 进行相应的向客户端写数据的操作</li>
<li>*/</li>
<li>if(key.isWritable())</li>
<li>{</li>
<li>doClinetWriteEvent(key)；</li>
<li>continue；</li>
<li>}</li>
<li>}</li>
<li>}</li>
<li>}</li>
<li></li>
<li>/**</li>
<li>* 处理服务器事件操作</li>
<li>* @param key 服务器选择键对象</li>
<li>*/</li>
<li>private void doServerSocketEvent(SelectionKey key)</li>
<li>{</li>
<li>SocketChannel client=null；</li>
<li>try</li>
<li>{</li>
<li>ServerSocketChannel server=(ServerSocketChannel)key.channel()；</li>
<li>client=server.accept()；</li>
<li>if(client==null)</li>
<li>{</li>
<li>return；</li>
<li>}</li>
<li>client.configureBlocking(false)；//将客户端Channel设置为非阻塞型</li>
<li>/**</li>
<li></li>
<li>/**</li>
<li>* 将客户端Channel注册到Selector对象上，并且指出客户端Channel所感</li>
<li>* 兴趣的事件为可读和可写</li>
<li>*/</li>
<li>client.register(selector,SelectionKey.OP_READ|SelectionKey.OP_READ)；</li>
<li>}catch(IOException e)</li>
<li>{</li>
<li>try</li>
<li>{</li>
<li>client.close()；</li>
<li></li>
<li>}catch(IOException e1){}</li>
<li>}</li>
<li>}</li>
<li>/**</li>
<li>* 进行向客户端写数据操作</li>
<li>* @param key 客户端选择键对象</li>
<li>*/</li>
<li>private void doClinetWriteEvent(SelectionKey key)</li>
<li>{</li>
<li>代码实现略；</li>
<li>}</li>
<li>/**</li>
<li>* 进行读取客户短数据操作</li>
<li>* @param key 客户端选择键对象</li>
<li>*/</li>
<li>private void doClientReadEvent(SelectionKey key)</li>
<li>{</li>
<li>代码实现略；</li>
<li>}</li>
<li>}</li>
</ol>
</pre>
<p>从上面对代码可以看出，使用非阻塞性I/O进行并发型服务器程序设计分三个部分：1.向Selector对象注册感兴趣的事件；2.从Selector中获取所感兴趣的事件；3.根据不同的事件进行相应的处理。</p>
<p><strong>结  语</strong></p>
<p>通过使用NIO 工具包进行并发型服务器程序设计，一个或者很少几个Socket 线程就可以处理成千上万个活动的Socket 连接，大大降低了服务器端程序的开销；同时网络I/O 采取非阻塞模式，线程不再在读或写时阻塞，操作系统可以更流畅的读写数据并可以更有效地向CPU 传递数据进行处理，以便更有效地提高系统的性能。</p>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>http://www.wdcode.org/archives/523.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>用nio实现Echo服务</title>
		<link>http://www.wdcode.org/archives/521.html</link>
		<comments>http://www.wdcode.org/archives/521.html#comments</comments>
		<pubDate>Thu, 15 Dec 2011 06:43:23 +0000</pubDate>
		<dc:creator>admin</dc:creator>
				<category><![CDATA[J2SE]]></category>
		<category><![CDATA[NIO]]></category>

		<guid isPermaLink="false">http://www.wdcode.org/?p=521</guid>
		<description><![CDATA[今天突然间想用nio实现个Echo服务，程序实现起来实现不算困难，但跑起来后，在Server端的ServerSocket完成accept之后，我的CPU总是跳到100%。嗯，小郁闷，后来，才发现自己在Server端注册了多余的监听事件SelectionKey.OP_WRITE，改过来后好多了，希望记住这个教训。

<strong>EchoServer.java</strong>
<pre>
<ol>
	<li>package edu.dlut.zxf.nio;</li>
	<li></li>
	<li>import java.io.IOException;</li>
	<li>import java.net.InetAddress;</li>
	<li>import java.net.InetSocketAddress;</li>
	<li>import java.nio.ByteBuffer;</li>
	<li>import java.nio.channels.SelectionKey;</li>
	<li>import java.nio.channels.Selector;</li>
	<li>import java.nio.channels.ServerSocketChannel;</li>
	<li>import java.nio.channels.SocketChannel;</li>
	<li>import java.util.Set;</li>
	<li></li>
	<li>/**</li>
	<li> * Echo服务器</li>
	<li> * @author finux</li>
	<li> */</li>
	<li>public class EchoServer {</li>
	<li>    public final static int BUFFER_SIZE = 1024; //默认端口</li>
	<li>    public final static String HOST = "210.30.107.17";</li>
	<li>    public final static int PORT = 8888;</li>
	<li></li>
	<li>    public static void main(String[] args) {</li>
	<li>        ServerSocketChannel ssc = null;</li>
	<li>        //缓冲区</li>
	<li>        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);</li>
	<li>        Selector selector = null;</li>
	<li>        try {</li>
	<li>            selector = Selector.open();</li>
	<li>            ssc = ServerSocketChannel.open();</li>
	<li>            ssc.socket().bind(new InetSocketAddress(InetAddress.getByName(HOST), PORT));</li>
	<li>            ssc.configureBlocking(false);</li>
	<li>            ssc.register(selector, SelectionKey.OP_ACCEPT);</li>
	<li>            print("服务器启动，准备好连接...");</li>
	<li>            while (selector.select() &#62; 0) {</li>
	<li>                Set&#60;SelectionKey&#62; selectionKeys = selector.selectedKeys();</li>
	<li>                for (SelectionKey key: selectionKeys) {</li>
	<li>                    if (key.isAcceptable()) {</li>
	<li>                        SocketChannel sc = ssc.accept();</li>
	<li>                        print("有新的连接！地址：" + sc.socket().getRemoteSocketAddress());</li>
	<li>                        sc.configureBlocking(false);</li>
	<li>                        sc.register(selector, SelectionKey.OP_READ);</li>
	<li>                        // 不要写成:</li>
	<li>                        // sc.register(selector, SelectionKey.OP_READ &#124; SelectionKey.OP_WRITE);</li>
	<li>                        // 毕竟这样多注册的无用的事件SelectionKey.OP_WRTE</li>
	<li>                        // 如果是这样，在完成accept后，CPU也许会跑到100%</li>
	<li></li>
	<li>                    }</li>
	<li>                    //same to if ((ops &#38; SelectionKey.OP_READ) == SelectionKey.OP_READ) {</li>
	<li>                    if (key.isReadable()) {</li>
	<li>                        SocketChannel sc = (SocketChannel)key.channel();</li>
	<li>                        print("有新的读取！地址：" + sc.socket().getRemoteSocketAddress());</li>
	<li>                        buffer.clear();</li>
	<li>                        sc.read(buffer);</li>
	<li>                        buffer.flip();</li>
	<li>                        byte[] b = new byte[buffer.limit()];</li>
	<li>                        buffer.get(b);</li>
	<li>                        String s = new String(b);</li>
	<li>                        if (s.equals("bye")) {</li>
	<li>                            print("断开连接：" + sc.socket().getRemoteSocketAddress());</li>
	<li>                            //断开连接后，取消此键的通道到其选择器的注册</li>
	<li>                            key.cancel();</li>
	<li>                            sc.close();</li>
	<li>                            continue;</li>
	<li>                        }</li>
	<li>                        print("读取的内容为：" + s);</li>
	<li>                        buffer.clear();</li>
	<li>                        s = "echo: " + s;</li>
	<li>                        buffer.put(s.getBytes());</li>
	<li>                        buffer.flip();</li>
	<li>                        sc.write(buffer);</li>
	<li>                    }</li>
	<li>                }</li>
	<li>                selectionKeys.clear();</li>
	<li>            }</li>
	<li>        } catch(IOException e) {</li>
	<li>            e.printStackTrace();</li>
	<li>        }</li>
	<li>    }</li>
	<li></li>
	<li>    private static void print(String s) {</li>
	<li>        System.out.println(s);</li>
	<li>    }</li>
	<li>}</li>
</ol>
</pre>
<strong>EchoClient.java</strong>
<pre>
<ol>
	<li>package edu.dlut.zxf.nio;</li>
	<li></li>
	<li>import java.util.Set;</li>
	<li>import java.io.BufferedReader;</li>
	<li>import java.io.IOException;</li>
	<li>import java.io.InputStreamReader;</li>
	<li>import java.net.InetSocketAddress;</li>
	<li>import java.net.InetAddress;</li>
	<li>import java.nio.ByteBuffer;</li>
	<li>import java.nio.channels.SelectionKey;</li>
	<li>import java.nio.channels.Selector;</li>
	<li>import java.nio.channels.SocketChannel;</li>
	<li></li>
	<li>/**</li>
	<li> * Echo客户端</li>
	<li> * @author finux</li>
	<li> */</li>
	<li>public class EchoClient {</li>
	<li>    public static void main(String[] args) {</li>
	<li>        ByteBuffer buffer = ByteBuffer.allocate(EchoServer.BUFFER_SIZE);</li>
	<li>        Selector selector = null;</li>
	<li>        SocketChannel sc = null;</li>
	<li>        try {</li>
	<li>            selector = Selector.open();</li>
	<li>            sc = SocketChannel.open();</li>
	<li>            sc.configureBlocking(false);</li>
	<li>            sc.connect(new InetSocketAddress(InetAddress.getByName(EchoServer.HOST), EchoServer.PORT));</li>
	<li>            print("客户端启动，准备连接...");</li>
	<li>            if (sc.isConnectionPending()) {</li>
	<li>                sc.finishConnect();</li>
	<li>            }</li>
	<li>            print("完成连接");</li>
	<li>            sc.register(selector, SelectionKey.OP_READ &#124; SelectionKey.OP_WRITE);</li>
	<li></li>
	<li>            boolean writed = false;</li>
	<li>            boolean down = false;</li>
	<li>            while (!down &#38;&#38; selector.select() &#62; 0) {</li>
	<li>                Set&#60;SelectionKey&#62; selectionKeys = selector.selectedKeys();</li>
	<li>                for (SelectionKey key: selectionKeys) {</li>
	<li>                    //int ops = key.readyOps();</li>
	<li>                    //if ((ops &#38; SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE &#38;&#38; !writed) {</li>
	<li>                    if (key.isWritable() &#38;&#38; !writed) {</li>
	<li>                        System.out.print("Input(bye to end): ");</li>
	<li>                        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));</li>
	<li>                        String s = br.readLine();</li>
	<li>                        if (s != null &#38;&#38; !s.trim().equals("")) {</li>
	<li>                            buffer.clear();</li>
	<li>                            buffer.put(s.getBytes());</li>
	<li>                            buffer.flip();</li>
	<li>                            sc.write(buffer);</li>
	<li>                            writed = true;</li>
	<li>                            if (s.equals("bye")) {</li>
	<li>                                down = true;</li>
	<li>                                break;</li>
	<li>                            }</li>
	<li>                        }</li>
	<li>                    }</li>
	<li>                    //if ((ops &#38; SelectionKey.OP_READ) == SelectionKey.OP_READ &#38;&#38; writed) {</li>
	<li>                    if (key.isReadable() &#38;&#38; writed) {</li>
	<li>                        buffer.clear();</li>
	<li>                        sc.read(buffer);</li>
	<li>                        buffer.flip();</li>
	<li>                        byte[] b = new byte[buffer.limit()];</li>
	<li>                        buffer.get(b);</li>
	<li>                        print(new String(b));</li>
	<li>                        writed = false;</li>
	<li>                    }</li>
	<li>                }</li>
	<li>                selectionKeys.clear();</li>
	<li>            }</li>
	<li>        } catch(IOException e) {</li>
	<li>            e.printStackTrace();</li>
	<li>        }</li>
	<li>    }</li>
	<li></li>
	<li>    private static void print(String s) {</li>
	<li>        System.out.println(s);</li>
	<li>    }</li>
	<li>}</li>
</ol>
</pre>
当然EchoClient也可以像下面这样来实现：

<strong>EchoClient2.java</strong>
<pre>
<ol>
	<li>package edu.dlut.zxf.nio;</li>
	<li></li>
	<li>import java.util.Set;</li>
	<li>import java.io.BufferedReader;</li>
	<li>import java.io.IOException;</li>
	<li>import java.io.InputStreamReader;</li>
	<li>import java.net.InetSocketAddress;</li>
	<li>import java.net.InetAddress;</li>
	<li>import java.nio.ByteBuffer;</li>
	<li>import java.nio.channels.SelectionKey;</li>
	<li>import java.nio.channels.Selector;</li>
	<li>import java.nio.channels.SocketChannel;</li>
	<li></li>
	<li>/**</li>
	<li> * Echo客户端2</li>
	<li> * @author finux</li>
	<li> */</li>
	<li>public class EchoClient2 {</li>
	<li>    public static void main(String[] args) {</li>
	<li>        ByteBuffer buffer = ByteBuffer.allocate(EchoServer.BUFFER_SIZE);</li>
	<li>        Selector selector = null;</li>
	<li>        SocketChannel sc = null;</li>
	<li>        try {</li>
	<li>            selector = Selector.open();</li>
	<li>            sc = SocketChannel.open();</li>
	<li>            sc.configureBlocking(false);</li>
	<li>            sc.register(selector, SelectionKey.OP_CONNECT);</li>
	<li>            sc.connect(new InetSocketAddress(InetAddress.getByName(EchoServer.HOST), EchoServer.PORT));</li>
	<li>            print("客户端启动，准备连接...");</li>
	<li></li>
	<li>            boolean writed = false;</li>
	<li>            boolean down = false;</li>
	<li>            while (!down &#38;&#38; selector.select() &#62; 0) {</li>
	<li>                Set&#60;SelectionKey&#62; selectionKeys = selector.selectedKeys();</li>
	<li>                for (SelectionKey key: selectionKeys) {</li>
	<li>                    //int ops = key.readyOps();</li>
	<li>                    //if ((ops &#38; SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT) {</li>
	<li>                    if (key.isConnectable()) {</li>
	<li>                        print("完成连接！");</li>
	<li>                        if (sc.isConnectionPending()) {</li>
	<li>                            sc.finishConnect();</li>
	<li>                        }</li>
	<li>                        sc.register(selector, SelectionKey.OP_READ &#124; SelectionKey.OP_WRITE);</li>
	<li>                    }</li>
	<li>                    //if ((ops &#38; SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE &#38;&#38; !writed) {</li>
	<li>                    if (key.isWritable() &#38;&#38; !writed) {</li>
	<li>                        //从准备IO中读取内容</li>
	<li>                        System.out.print("Input(bye to end): ");</li>
	<li>                        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));</li>
	<li>                        String s = br.readLine();</li>
	<li>                        if (s != null &#38;&#38; !s.trim().equals("")) {</li>
	<li>                            buffer.clear();</li>
	<li>                            buffer.put(s.getBytes());</li>
	<li>                            buffer.flip();</li>
	<li>                            sc.write(buffer);</li>
	<li>                            writed = true;</li>
	<li>                            if (s.equals("bye")) {</li>
	<li>                                down = true;</li>
	<li>                                break;</li>
	<li>                            }</li>
	<li>                        }</li>
	<li>                    }</li>
	<li>                    //if ((ops &#38; SelectionKey.OP_READ) == SelectionKey.OP_READ &#38;&#38; writed) {</li>
	<li>                    if (key.isReadable() &#38;&#38; writed) {</li>
	<li>                        buffer.clear();</li>
	<li>                        sc.read(buffer);</li>
	<li>                        buffer.flip();</li>
	<li>                        byte[] b = new byte[buffer.limit()];</li>
	<li>                        buffer.get(b);</li>
	<li>                        print(new String(b));</li>
	<li>                        writed = false;</li>
	<li>                    }</li>
	<li>                }</li>
	<li>                selectionKeys.clear();</li>
	<li>            }</li>
	<li>        } catch(IOException e) {</li>
	<li>            e.printStackTrace();</li>
	<li>        }</li>
	<li>    }</li>
	<li></li>
	<li>    private static void print(String s) {</li>
	<li>        System.out.println(s);</li>
	<li>    }</li>
	<li>}</li>
</ol>
</pre>
但是这样的话，显然EchoClient2中的while循环中的for循环（若有n次），在每次循环中都会多出n-1次if判断，就是下面这个：
<pre>
<ol>
	<li>if (key.isConnectable()) {</li>
</ol>
</pre>
所以，我个人更喜欢第一个EchoClient，呵呵，不用注册SelectionKey.OP_CONNECT监听事件。呵呵...

&#160;]]></description>
			<content:encoded><![CDATA[<p>今天突然间想用nio实现个Echo服务，程序实现起来实现不算困难，但跑起来后，在Server端的ServerSocket完成accept之后，我的CPU总是跳到100%。嗯，小郁闷，后来，才发现自己在Server端注册了多余的监听事件SelectionKey.OP_WRITE，改过来后好多了，希望记住这个教训。</p>
<p><strong>EchoServer.java</strong></p>
<pre>
<ol>
<li>package edu.dlut.zxf.nio;</li>
<li></li>
<li>import java.io.IOException;</li>
<li>import java.net.InetAddress;</li>
<li>import java.net.InetSocketAddress;</li>
<li>import java.nio.ByteBuffer;</li>
<li>import java.nio.channels.SelectionKey;</li>
<li>import java.nio.channels.Selector;</li>
<li>import java.nio.channels.ServerSocketChannel;</li>
<li>import java.nio.channels.SocketChannel;</li>
<li>import java.util.Set;</li>
<li></li>
<li>/**</li>
<li> * Echo服务器</li>
<li> * @author finux</li>
<li> */</li>
<li>public class EchoServer {</li>
<li>    public final static int BUFFER_SIZE = 1024; //默认端口</li>
<li>    public final static String HOST = "210.30.107.17";</li>
<li>    public final static int PORT = 8888;</li>
<li></li>
<li>    public static void main(String[] args) {</li>
<li>        ServerSocketChannel ssc = null;</li>
<li>        //缓冲区</li>
<li>        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);</li>
<li>        Selector selector = null;</li>
<li>        try {</li>
<li>            selector = Selector.open();</li>
<li>            ssc = ServerSocketChannel.open();</li>
<li>            ssc.socket().bind(new InetSocketAddress(InetAddress.getByName(HOST), PORT));</li>
<li>            ssc.configureBlocking(false);</li>
<li>            ssc.register(selector, SelectionKey.OP_ACCEPT);</li>
<li>            print("服务器启动，准备好连接...");</li>
<li>            while (selector.select() &gt; 0) {</li>
<li>                Set&lt;SelectionKey&gt; selectionKeys = selector.selectedKeys();</li>
<li>                for (SelectionKey key: selectionKeys) {</li>
<li>                    if (key.isAcceptable()) {</li>
<li>                        SocketChannel sc = ssc.accept();</li>
<li>                        print("有新的连接！地址：" + sc.socket().getRemoteSocketAddress());</li>
<li>                        sc.configureBlocking(false);</li>
<li>                        sc.register(selector, SelectionKey.OP_READ);</li>
<li>                        // 不要写成:</li>
<li>                        // sc.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);</li>
<li>                        // 毕竟这样多注册的无用的事件SelectionKey.OP_WRTE</li>
<li>                        // 如果是这样，在完成accept后，CPU也许会跑到100%</li>
<li></li>
<li>                    }</li>
<li>                    //same to if ((ops &amp; SelectionKey.OP_READ) == SelectionKey.OP_READ) {</li>
<li>                    if (key.isReadable()) {</li>
<li>                        SocketChannel sc = (SocketChannel)key.channel();</li>
<li>                        print("有新的读取！地址：" + sc.socket().getRemoteSocketAddress());</li>
<li>                        buffer.clear();</li>
<li>                        sc.read(buffer);</li>
<li>                        buffer.flip();</li>
<li>                        byte[] b = new byte[buffer.limit()];</li>
<li>                        buffer.get(b);</li>
<li>                        String s = new String(b);</li>
<li>                        if (s.equals("bye")) {</li>
<li>                            print("断开连接：" + sc.socket().getRemoteSocketAddress());</li>
<li>                            //断开连接后，取消此键的通道到其选择器的注册</li>
<li>                            key.cancel();</li>
<li>                            sc.close();</li>
<li>                            continue;</li>
<li>                        }</li>
<li>                        print("读取的内容为：" + s);</li>
<li>                        buffer.clear();</li>
<li>                        s = "echo: " + s;</li>
<li>                        buffer.put(s.getBytes());</li>
<li>                        buffer.flip();</li>
<li>                        sc.write(buffer);</li>
<li>                    }</li>
<li>                }</li>
<li>                selectionKeys.clear();</li>
<li>            }</li>
<li>        } catch(IOException e) {</li>
<li>            e.printStackTrace();</li>
<li>        }</li>
<li>    }</li>
<li></li>
<li>    private static void print(String s) {</li>
<li>        System.out.println(s);</li>
<li>    }</li>
<li>}</li>
</ol>
</pre>
<p><strong>EchoClient.java</strong></p>
<pre>
<ol>
<li>package edu.dlut.zxf.nio;</li>
<li></li>
<li>import java.util.Set;</li>
<li>import java.io.BufferedReader;</li>
<li>import java.io.IOException;</li>
<li>import java.io.InputStreamReader;</li>
<li>import java.net.InetSocketAddress;</li>
<li>import java.net.InetAddress;</li>
<li>import java.nio.ByteBuffer;</li>
<li>import java.nio.channels.SelectionKey;</li>
<li>import java.nio.channels.Selector;</li>
<li>import java.nio.channels.SocketChannel;</li>
<li></li>
<li>/**</li>
<li> * Echo客户端</li>
<li> * @author finux</li>
<li> */</li>
<li>public class EchoClient {</li>
<li>    public static void main(String[] args) {</li>
<li>        ByteBuffer buffer = ByteBuffer.allocate(EchoServer.BUFFER_SIZE);</li>
<li>        Selector selector = null;</li>
<li>        SocketChannel sc = null;</li>
<li>        try {</li>
<li>            selector = Selector.open();</li>
<li>            sc = SocketChannel.open();</li>
<li>            sc.configureBlocking(false);</li>
<li>            sc.connect(new InetSocketAddress(InetAddress.getByName(EchoServer.HOST), EchoServer.PORT));</li>
<li>            print("客户端启动，准备连接...");</li>
<li>            if (sc.isConnectionPending()) {</li>
<li>                sc.finishConnect();</li>
<li>            }</li>
<li>            print("完成连接");</li>
<li>            sc.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);</li>
<li></li>
<li>            boolean writed = false;</li>
<li>            boolean down = false;</li>
<li>            while (!down &amp;&amp; selector.select() &gt; 0) {</li>
<li>                Set&lt;SelectionKey&gt; selectionKeys = selector.selectedKeys();</li>
<li>                for (SelectionKey key: selectionKeys) {</li>
<li>                    //int ops = key.readyOps();</li>
<li>                    //if ((ops &amp; SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE &amp;&amp; !writed) {</li>
<li>                    if (key.isWritable() &amp;&amp; !writed) {</li>
<li>                        System.out.print("Input(bye to end): ");</li>
<li>                        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));</li>
<li>                        String s = br.readLine();</li>
<li>                        if (s != null &amp;&amp; !s.trim().equals("")) {</li>
<li>                            buffer.clear();</li>
<li>                            buffer.put(s.getBytes());</li>
<li>                            buffer.flip();</li>
<li>                            sc.write(buffer);</li>
<li>                            writed = true;</li>
<li>                            if (s.equals("bye")) {</li>
<li>                                down = true;</li>
<li>                                break;</li>
<li>                            }</li>
<li>                        }</li>
<li>                    }</li>
<li>                    //if ((ops &amp; SelectionKey.OP_READ) == SelectionKey.OP_READ &amp;&amp; writed) {</li>
<li>                    if (key.isReadable() &amp;&amp; writed) {</li>
<li>                        buffer.clear();</li>
<li>                        sc.read(buffer);</li>
<li>                        buffer.flip();</li>
<li>                        byte[] b = new byte[buffer.limit()];</li>
<li>                        buffer.get(b);</li>
<li>                        print(new String(b));</li>
<li>                        writed = false;</li>
<li>                    }</li>
<li>                }</li>
<li>                selectionKeys.clear();</li>
<li>            }</li>
<li>        } catch(IOException e) {</li>
<li>            e.printStackTrace();</li>
<li>        }</li>
<li>    }</li>
<li></li>
<li>    private static void print(String s) {</li>
<li>        System.out.println(s);</li>
<li>    }</li>
<li>}</li>
</ol>
</pre>
<p>当然EchoClient也可以像下面这样来实现：</p>
<p><strong>EchoClient2.java</strong></p>
<pre>
<ol>
<li>package edu.dlut.zxf.nio;</li>
<li></li>
<li>import java.util.Set;</li>
<li>import java.io.BufferedReader;</li>
<li>import java.io.IOException;</li>
<li>import java.io.InputStreamReader;</li>
<li>import java.net.InetSocketAddress;</li>
<li>import java.net.InetAddress;</li>
<li>import java.nio.ByteBuffer;</li>
<li>import java.nio.channels.SelectionKey;</li>
<li>import java.nio.channels.Selector;</li>
<li>import java.nio.channels.SocketChannel;</li>
<li></li>
<li>/**</li>
<li> * Echo客户端2</li>
<li> * @author finux</li>
<li> */</li>
<li>public class EchoClient2 {</li>
<li>    public static void main(String[] args) {</li>
<li>        ByteBuffer buffer = ByteBuffer.allocate(EchoServer.BUFFER_SIZE);</li>
<li>        Selector selector = null;</li>
<li>        SocketChannel sc = null;</li>
<li>        try {</li>
<li>            selector = Selector.open();</li>
<li>            sc = SocketChannel.open();</li>
<li>            sc.configureBlocking(false);</li>
<li>            sc.register(selector, SelectionKey.OP_CONNECT);</li>
<li>            sc.connect(new InetSocketAddress(InetAddress.getByName(EchoServer.HOST), EchoServer.PORT));</li>
<li>            print("客户端启动，准备连接...");</li>
<li></li>
<li>            boolean writed = false;</li>
<li>            boolean down = false;</li>
<li>            while (!down &amp;&amp; selector.select() &gt; 0) {</li>
<li>                Set&lt;SelectionKey&gt; selectionKeys = selector.selectedKeys();</li>
<li>                for (SelectionKey key: selectionKeys) {</li>
<li>                    //int ops = key.readyOps();</li>
<li>                    //if ((ops &amp; SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT) {</li>
<li>                    if (key.isConnectable()) {</li>
<li>                        print("完成连接！");</li>
<li>                        if (sc.isConnectionPending()) {</li>
<li>                            sc.finishConnect();</li>
<li>                        }</li>
<li>                        sc.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);</li>
<li>                    }</li>
<li>                    //if ((ops &amp; SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE &amp;&amp; !writed) {</li>
<li>                    if (key.isWritable() &amp;&amp; !writed) {</li>
<li>                        //从准备IO中读取内容</li>
<li>                        System.out.print("Input(bye to end): ");</li>
<li>                        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));</li>
<li>                        String s = br.readLine();</li>
<li>                        if (s != null &amp;&amp; !s.trim().equals("")) {</li>
<li>                            buffer.clear();</li>
<li>                            buffer.put(s.getBytes());</li>
<li>                            buffer.flip();</li>
<li>                            sc.write(buffer);</li>
<li>                            writed = true;</li>
<li>                            if (s.equals("bye")) {</li>
<li>                                down = true;</li>
<li>                                break;</li>
<li>                            }</li>
<li>                        }</li>
<li>                    }</li>
<li>                    //if ((ops &amp; SelectionKey.OP_READ) == SelectionKey.OP_READ &amp;&amp; writed) {</li>
<li>                    if (key.isReadable() &amp;&amp; writed) {</li>
<li>                        buffer.clear();</li>
<li>                        sc.read(buffer);</li>
<li>                        buffer.flip();</li>
<li>                        byte[] b = new byte[buffer.limit()];</li>
<li>                        buffer.get(b);</li>
<li>                        print(new String(b));</li>
<li>                        writed = false;</li>
<li>                    }</li>
<li>                }</li>
<li>                selectionKeys.clear();</li>
<li>            }</li>
<li>        } catch(IOException e) {</li>
<li>            e.printStackTrace();</li>
<li>        }</li>
<li>    }</li>
<li></li>
<li>    private static void print(String s) {</li>
<li>        System.out.println(s);</li>
<li>    }</li>
<li>}</li>
</ol>
</pre>
<p>但是这样的话，显然EchoClient2中的while循环中的for循环（若有n次），在每次循环中都会多出n-1次if判断，就是下面这个：</p>
<pre>
<ol>
<li>if (key.isConnectable()) {</li>
</ol>
</pre>
<p>所以，我个人更喜欢第一个EchoClient，呵呵，不用注册SelectionKey.OP_CONNECT监听事件。呵呵&#8230;</p>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>http://www.wdcode.org/archives/521.html/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

