1. 程式人生 > >java nio多執行緒引起的full gc問題

java nio多執行緒引起的full gc問題

1.在寫nio的例子時,服務端採用執行緒池處理請求,遇到一個full gc問題,下面給程式碼貼出來。
nioserver端程式碼

package com.nio.study;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import
java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 類NIOServer.java的實現描述:TODO 類實現描述 * * @author macun 2015年11月29日 下午4:18:46 */ public class NIOServer { private static final int DEFAULT_PORT = 8888
; private static Selector selector; private Object lock = new Object(); private Boolean isStart = false; private ExecutorService threadPool = null; public void init() throws IOException { selector = Selector.open(); ServerSocketChannel serverChannel = ServerSocketChannel.open(); ServerSocket ss = serverChannel.socket(); ss.bind(new
InetSocketAddress(DEFAULT_PORT)); serverChannel.configureBlocking(false); serverChannel.register(selector, SelectionKey.OP_ACCEPT); threadPool = Executors.newFixedThreadPool(10); // threadPool = new ThreadPoolExecutor(10, 10, // 1000L, TimeUnit.MILLISECONDS, // new ArrayBlockingQueue<Runnable>(100)); synchronized (lock) { isStart = true; } System.out.println("nio server init successful."); } public void run() throws IOException { synchronized (lock) { if (!isStart) { try { init(); } catch (IOException e) { throw new IOException("nio server init failed."); } } } while (true) { selector.select(); Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while(iterator.hasNext()){ SelectionKey key = (SelectionKey) iterator.next(); iterator.remove(); if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel client = server.accept(); client.configureBlocking(false); SelectionKey clientKey = client.register(selector, SelectionKey.OP_READ); ByteBuffer buffer = ByteBuffer.allocate(100); clientKey.attach(buffer); } if (key.isReadable()) { NIOHandler handler = new NIOHandler(key); threadPool.execute(handler); } } } } }

NIOHandler程式碼

package com.nio.study;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;

/**
 * 類NIOHandler.java的實現描述:TODO 類實現描述
 * 
 * @author macun 2015年11月29日 下午4:40:35
 */
public class NIOHandler implements Runnable {

    private SelectionKey key;

    public NIOHandler(SelectionKey key){
        this.key = key;
    }

    @Override
    public void run() {
        System.out.println("handler:"+key);
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        try {
            int n = client.read(buffer);
            String temp = null;
            if (n > 0) {
                temp = new String(buffer.array());
                System.out.println(temp);
                buffer.clear();

                buffer.put(temp.getBytes());
                buffer.flip();
                client.write(buffer);
                buffer.clear();
            }

//              key.cancel();
//            client.close();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        } catch (IOException e) {
            try {
                client.close();
            } catch (IOException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }
            System.out.println(e.getMessage());
        }

    }

    public void setSelectionKey(SelectionKey key) {
        this.key = key;
    }

}

Server 啟動測試類

public class NIOServerTest {
    public static void main(String[] args){
        NIOServer server = new NIOServer();
        try {
            server.run();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

client端10個socket請求

public class NIOClient1 {

    public static final String IP_ADDR = "localhost"; // ip adress
    public static final int    PORT    = 8888;       // listen port

    public static void main(String[] args) throws Exception {

        System.out.println("client start .....");
        int i = 1;
        Socket socket = null;
        for (int j = 0; j < 20; j++) {
            try {
                socket = new Socket(IP_ADDR, PORT);
                DataInputStream input = new DataInputStream(socket.getInputStream());
                DataOutputStream out = new DataOutputStream(socket.getOutputStream());

                String str = "send " + i + " message.";
                i++;
                out.writeUTF(str);
                String ret = input.readUTF();
                System.out.println("from server:" + ret);

                socket.close();

//                Thread.sleep(1000);
            } catch (Exception e) {
                System.out.println("client " + e.getMessage());
            } finally {
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        socket = null;
                        System.out.println("client finally " + e.getMessage());
                    }

                }

            }

        }
    }

}

2.在沒有用執行緒池的時候,發現正常;當用執行緒池處理的時候,發現程式卡死了。一看cpu使用率超過300%,再看full gc一直釋放不掉垃圾。
問題現象:full gc,並且一直釋放不掉垃圾。
分析:
1.在Handler處理邏輯部分,Thread.sleep(1000),讓其休眠1秒鐘。
2.執行緒池是由Executors工廠生成的10個固定大小的執行緒池和無界佇列。
3.Selector監聽是無條件迴圈的。
由以上3個條件猜測是10個固定大小的執行緒池在等待1秒的時候,Selector無條件迴圈接受到的SelectionKey放入到無界佇列裡,導致佇列爆滿,並且這些又是強引用,fullgc無法釋放,導致越積越多,程式卡死。
猜測驗證,把無界佇列換成有界佇列ArrayBlockingQueue,大小1000,很快就會丟擲異常,包佇列大小不夠。
解決方法:
1.無界佇列改成有界佇列
2.因為selector是無條件迴圈,當SelectionKey的channel讀寫沒有完成,它會一直取出來。handler裡sleep(1000)的時候,它的SelectionKey仍然會被NIOServer 裡的selector讀取,那麼就需要保持SelectionKey不被重複讀取。
3.在handler完成後,需要把SelectionKey 從Holder裡remove,並cancel掉。
基於以上3點,針對 NIOServer和Handler類進行改造,程式碼如下:
NIOServer程式碼片段更改如下

//        threadPool = Executors.newFixedThreadPool(10);
        threadPool = new ThreadPoolExecutor(10, 10,
                                            1000L, TimeUnit.MILLISECONDS,
                                            new ArrayBlockingQueue<Runnable>(100));

SelectionKeyHolder保持SelectionKey

if (key.isReadable()) {
                    if(SelectionKeyHolder.isContainKey(key)){
                        continue;
                    }
                    SelectionKeyHolder.put(key);
                    System.out.println(key);
                    NIOHandler handler = new NIOHandler(key);
                    threadPool.execute(handler);

SelectionKeyHolder類,程式碼片段如下:

import java.nio.channels.SelectionKey;
import java.util.HashSet;

/**
 * 類SelectionKeyHolder.java的實現描述:TODO 類實現描述
 * 
 * @author macun 2015年11月30日 下午12:21:26
 */
public class SelectionKeyHolder {

    private static HashSet<SelectionKey> keySet = new HashSet<SelectionKey>();

    private static Object                lock   = new Object();

    public static void put(SelectionKey key) {
        synchronized (lock) {
            keySet.add(key);
        }

    }

    public static  boolean isContainKey(SelectionKey key) {
        return keySet.contains(key);
    }

    public static void putIfAbsent(SelectionKey key) {
        if (keySet.contains(key)) {
            return;
        }
        put(key);
    }

    public static void remove(SelectionKey key) {
        synchronized (lock) {
            keySet.remove(key);
        }

    }
}

Handler類改造

public void run() {
        System.out.println("handler:"+key);
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        try {
            int n = client.read(buffer);
            String temp = null;
            if (n > 0) {
                temp = new String(buffer.array());
                System.out.println(temp);
                buffer.clear();

                buffer.put(temp.getBytes());
                buffer.flip();
                client.write(buffer);
                buffer.clear();
            }
            SelectionKeyHolder.remove(key);
              key.cancel();
//            client.close();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        } catch (IOException e) {
            try {
                client.close();
            } catch (IOException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }
            System.out.println(e.getMessage());
        }

    }

通過以上幾個重構點,就可以解決以上nio服務端多執行緒full gc的問題。但是對於使用HashSet保持SelectionKey並不是最優的,可以替換成CurrentHashMap。