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。