以NIO通信例子結合Jconsole解釋JVM內存分配機制

JAVA的內存分配機制,在很多地方都已經解析很多次了,個人如何方便的來直觀的瞭解,還有很多人不是很清楚,或者沒有這樣的機會,在這裏我結合一個小例子,採用JDK自帶的JConsole來說一下JVM的內存分配機制。

[size=large]案例[/size]
首先解釋下場景,服務端是一個通信服務器,接受客戶端發過來的通信信息,並做業務處理;服務端採用JAVA中的MINA2框架,客戶端可以任意,C++也好,JAVA也好,只要符合服務端規定的消息結構,發給通信服務器都能處理。

爲了讓大家更清楚,可以用MINA2框架中的時間服務器的例子來稍作修改模擬這個場景。系統環境,APACHE MINA2.0.4 + JDK1.6 + Eclipse3.7。

服務端代碼如下:
public class MinaTimeServer {
/** We will use a port above 1024 to be able to launch the server with a standard user */
private static final int PORT = 9123;

/**
* The server implementation. It's based on TCP, and uses a logging filter
* plus a text line decoder.
*/
public static void main(String[] args) throws IOException {
// Create the acceptor
IoAcceptor acceptor = new NioSocketAcceptor();

// Add two filters : a logger and a codec
acceptor.getFilterChain().addLast( "logger", new LoggingFilter() );
acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter( new TextLineCodecFactory( Charset.forName( "UTF-8" ))));
//acceptor.getFilterChain().addLast("exector", new ExecutorFilter(Executors.newCachedThreadPool()));
acceptor.getFilterChain().addLast("exector", new ExecutorFilter());

// Attach the business logic to the server
acceptor.setHandler( new TimeServerHandler() );


// Configurate the buffer size and the iddle time
acceptor.getSessionConfig().setReadBufferSize( 2048 );
acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 );

// And bind !
acceptor.bind( new InetSocketAddress(PORT) );
}
}



業務處理器(這裏當作一個時間服務器,接受到信息之後就立刻返回服務器當前時間)
public class TimeServerHandler extends IoHandlerAdapter
{
/**
* Trap exceptions.
*/
@Override
public void exceptionCaught( IoSession session, Throwable cause ) throws Exception
{
cause.printStackTrace();
}

/**
* If the message is 'quit', we exit by closing the session. Otherwise,
* we return the current date.
*/
@Override
public void messageReceived( IoSession session, Object message ) throws Exception
{
String str = message.toString();

if( str.trim().equalsIgnoreCase("quit") ) {
// "Quit" ? let's get out ...
session.close(true);
return;
}

// Send the current date back to the client
Date date = new Date();
session.write( date.getTime());
System.out.println("Message written...");
}

/**
* On idle, we just write a message on the console
*/
@Override
public void sessionIdle( IoSession session, IdleStatus status ) throws Exception
{
System.out.println( "IDLE " + session.getIdleCount( status ));
}
}



客戶端JAVA代碼:
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;

public class MinaTimeClient {

public void mutiSendMsg() {
// 創建客戶端連接器.
NioSocketConnector connector = new NioSocketConnector();
connector.getFilterChain().addLast("logger", new LoggingFilter());
connector.getFilterChain().addLast(
"codec",
new ProtocolCodecFilter(new TextLineCodecFactory(Charset
.forName("UTF-8")))); // 設置編碼過濾器
connector.setConnectTimeoutMillis(30000);
connector.setHandler(new TimeClientHandler());// 設置事件處理器
ConnectFuture cf = connector.connect(new InetSocketAddress("127.0.0.1",
9908));// 建立連接
cf.awaitUninterruptibly();// 等待連接創建完成
cf.getSession().write("hello");// 發送消息
cf.getSession().write("quit");// 發送消息
cf.getSession().getCloseFuture().awaitUninterruptibly();// 等待連接斷開
connector.dispose();
}
public static void main(String[] args) {
/*
MinaTimeClient client = new MinaTimeClient();
for(int i = 0; i < 1000; i ++) {
client.mutiSendMsg();
}*/
//int size = keywordMap.size();
long startTime = System.currentTimeMillis();
int thread_num = 100;
int client_num = 2000;

// TODO Auto-generated method stub
ExecutorService exec = Executors.newCachedThreadPool();
// 50個線程可以同時訪問
final Semaphore semp = new Semaphore(thread_num);

final CountDownLatch countDownLatch = new CountDownLatch(client_num);
List<Thread> list = new ArrayList<Thread>();
// 模擬2000個客戶端訪問
for (int index = 0; index < client_num; index++) {
final int NO = index;
Thread run = new Thread() {
public void run() {
while(true){
try {
// 獲取許可
//semp.acquire();

new MinaTimeClient().mutiSendMsg();
// System.out.println(result);
// Thread.sleep((long) (Math.random()) * 1000);
// 釋放
System.out.println("第:" + NO + " 個");
// semp.release();
Thread.sleep(500);
//countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}
};
list.add(run);
//exec.execute(run);
}
for (Thread thread : list) {
thread.start();
}

//try {
//countDownLatch.await();
System.out.println("end time::::" + (System.currentTimeMillis() - startTime));
// 退出線程池
// exec.shutdown();
//} catch (InterruptedException e) {
// TODO Auto-generated catch block
// e.printStackTrace();
//}

}
}


客戶端處理類:
public class TimeClientHandler extends IoHandlerAdapter {
public TimeClientHandler() {
}

@Override
public void messageReceived(IoSession session, Object message)
throws Exception {
System.out.println(message);// 顯示接收到的消息
}
}

代碼準備好之後,啓動通信服務器服務端,然後啓動客戶端;
然後在打開你的jconsole界面,選擇需要觀察的那個服務端的JAVA進程;
首頁就是類似這樣的一個界面(注意:這篇文章裏面所有的截圖都是在模擬程序運行5個小時以後截下來的):


[img]http://dl.iteye.com/upload/attachment/0065/3257/769865b8-dbe6-352d-ab24-2d6cded268b9.png[/img]

[size=large]JAVA內存模型簡介[/size]
我們知道:JAVA的內存模型是這樣的,簡單的來說,可以分爲堆和非堆內存,下面的著名的這個圖大家經常會看到:

[img]http://dl.iteye.com/upload/attachment/0065/3259/7c910f44-b813-396f-9af6-1d02c81fb588.jpg[/img]

內存由 Perm 和 Heap 組成. 其中

Heap(堆內存) = {Old + NEW = { Eden , from, to } }
NEW 也叫年輕代(Young Gen):
這裏解釋下,也有人說上圖中的from,to是suvivor space a,survivor b
而且這兩個區會交換的:
NEW:年輕代(Young Gen):年輕代主要存放新創建的對象,內存大小相對會比較小,垃圾回收會比較頻繁。年輕代分成1個Eden Space和2個Suvivor Space(命名爲A和B)
• 當對象在堆創建時,將進入年輕代的Eden Space。
• 垃圾回收器進行垃圾回收時,掃描Eden Space和A Suvivor Space,如果對象仍然存活,則複製到B Suvivor Space,如果B Suvivor Space已經滿,則複製 Old Gen
• 掃描A Suvivor Space時,如果對象已經經過了幾次的掃描仍然存活,JVM認爲其爲一個Old對象,則將其移到Old Gen。
• 掃描完畢後,JVM將Eden Space和A Suvivor Space清空,然後交換A和B的角色(即下次垃圾回收時會掃描Eden Space和BSuvivor Space。
我們可以看到:Young Gen垃圾回收時,採用將存活對象複製到到空的Suvivor Space的方式來確保不存在內存碎片,採用空間換時間的方式來加速內存垃圾回收。

[size=large]JConsole使用[/size]
JConsole是一個基於JMX的GUI工具,用於連接正在運行的JVM,不過此JVM需要使用可管理的模式啓動。JConsole從java5開始,在JDK中提供。用於對JVM中內存,線程和類等的監控。
其使用很簡單,JDK在你的環境變量之後,就可以在命令行中輸入:

jconsole

如果彈出窗口,說明配置可用。
彈出窗口如下:

[img]http://dl.iteye.com/upload/attachment/0065/3269/f8024561-45a9-3fc3-bb88-35d90d0c1cec.png[/img]

上圖中的Eden Space就是JAVA對象初始進入的地方,
Survivor Space是A Suvivor Space空間
Tenured Gen 是Old Gen也就是大家說的養老區,
垃圾回收描述:
在New Generation塊中,垃圾回收一般用Copying的算法,速度快。這裏面的截圖表示新生代已經執行了300多次的垃圾回收。

[img]http://dl.iteye.com/upload/attachment/0065/3271/18d57f40-c759-353d-a1b4-b3c91e750fb6.jpg[/img]

每次GC的時候,存活下來的對象首先由Eden拷貝到某個Survivor Space, 當Survivor Space空間滿了後, 剩下的live對象就被直接拷貝到Old Generation中去。因此,每次GC後,Eden內存塊會被清空。在Old Generation塊中,垃圾回收一般用mark-compact的算法,速度慢些,但減少內存要求.
還是上圖

[img]http://dl.iteye.com/upload/attachment/0065/3271/18d57f40-c759-353d-a1b4-b3c91e750fb6.jpg[/img]

說明採用mark-compact算法的養老區還沒有執行GC,(0項收集);
回到這個例子,在堆內存的JCONSOLE面板中,有三個子項,分別是:
[size=large](一)Eden Space[/size]
其截圖如下:

[img]http://dl.iteye.com/upload/attachment/0065/3274/29010e68-8f4f-3105-94c4-dd0c1b96026d.png[/img]

這個是新生代內存,從圖上來看,整體的運行內存還是比較穩定的。
[size=large](二)A Suvivor Space[/size]

[img]http://dl.iteye.com/upload/attachment/0065/3276/f26d3004-2811-3791-a7b4-443f5145dfc1.png[/img]

這個也是相當的穩定
[size=large](三)OLD
[/size]


[img]http://dl.iteye.com/upload/attachment/0065/3278/b1edd437-0cbf-3262-8de2-120bbcb458aa.png[/img]

OLD養老區的內存一直呈現上升趨勢,而且在這5個多小時裏面,一直沒有下降的趨勢。這個是爲什麼呢?大家知道, 內存溢出通常發生於OLD段或Perm段垃圾回收後,仍然無內存空間容納新的Java對象的情況。那麼這個現象會不會出現內存溢出呢?我們在前面瞭解到,OLD養老區的內存回收mark-compact,一般是比較慢的,所以這5個小時,沒有發生過一次養老區的垃圾回收。當OLD去的空間不夠時,JVM會在OLD區進行完全的垃圾收集(0級),這樣就會避免出現OLD區的OOM錯誤。
因爲堆內存是上述三張圖的內存集合,那麼下張圖中,堆內存的線性增長也就有合理的解釋了,由於OLD還沒有進行垃圾回收,裏面的對象越來越多,佔用內存越來越大,也就影響了整體堆內存


[size=large]JCONSOLE還能看到什麼呢?[/size]
第三個TAB頁籤,能看到當前的線程數

[img]http://dl.iteye.com/upload/attachment/0065/3284/a1cabcc7-56bc-378e-a477-70b01e7fc108.png[/img]

第四個TAB頁籤,能夠看到當前的JVM信息

[img]http://dl.iteye.com/upload/attachment/0065/3287/262a5620-85df-3e84-8e22-d62fdefb94b3.png[/img]

最有一個線程數能夠看到當前JMX暴露出現的JVM運行的整體信息,比如說GC的次數,GC的類型,時間等等;

[size=large]後續[/size]
這個模擬程序會隨着時間的推遲而OLD去內存線性增長,也就意味着堆區內存線性增長,難道一直等着OLD區來做mark-compact的算法的GC?
當然也是有方法去改進GC的算法的,先來了解一下基本的知識:

[size=large]垃圾回收策略[/size]
評估垃圾回收策略的兩個重要度量是:
• 吞吐量(Throughput ):JVM花費在垃圾回收上的時間越長,則吞吐量越低
• 暫停時間(Pause time):JVM垃圾回收過程當中有一個暫停期,在暫停期間,應用程序不能運行,暫停時間是暫停期的長度
非常遺憾的是,一般這兩個指標是相互衝突的,改善其中一個會影響到另外一個,根據情景的不同我們決定是優先考慮吞吐量還是暫停時間,對於需要實時響應的應用,我們需要優先考慮暫停時間,對於後臺運行應用,我們需要優先考慮吞吐量。
在考察各種垃圾回收器之前,我們需要了解一下幾個重要的策略
• 並行(Parallel):並行表示使用多個線程同時進行垃圾回收的工作,此策略一般會從同時改善暫停時間和吞吐量,在有多CPU內核的服務器上,這是基本上我們要使用的策略。
• 併發(Concurrent):並行表示垃圾回收器的一些工作(譬如垃圾標記)與應用程序同時進行,這將更進一步縮短暫停時間,需要注意的是,同時垃圾回收器的複雜性會大大增大,基本上是會降低吞吐量,
• 內存碎片處理:有不壓縮、壓縮和拷貝三種策略,從空間上講,拷貝將花費更多的內存(譬如如上內存管理的Young Gen,需要維持一個額外的Suvivor空間),從時間上來講,不壓縮會減低創建對象時的內存分配效率,在垃圾回收上,拷貝策略會比壓縮策略更高效。
Sun JVM有4垃圾回收器:
• Serial Collector:序列垃圾回收器,垃圾回收器對Young Gen和Tenured Gen都是使用單線的垃圾回收方式,對Young Gen,會使用拷貝策略避免內存碎片,對Old Gen,會使用壓縮策略避免內存碎片。基本上,在對內核的服務器上應該避免使用這種方式。在JVM啓動參數中使用-XX:+UseSerialGC啓用Serial Collector。
• Parallel Collector:併發垃圾回收器,垃圾回收器對Young Gen和Tenured Gen都是使用多線程並行垃圾回收的方式,對Young Gen,會使用拷貝策略避免內存碎片,對Old Gen,會使用壓縮策略避免內存碎片。
在JVM啓動參數中使用-XX:+UseParallelGC啓用Parallel Collector。
• Parallel Compacting Collector:並行壓縮垃圾回收器,與Parallel Collector垃圾回收類似,但對Tenured Gen會使用一種更有效的垃圾回收策略,此垃圾回收器在暫停時間上會更短。
在JVM啓動參數中使用-XX:+UseParallelOldGC啓用Parallel Compacting Collector。
• Concurrent Mark-Sweep (CMS) Collector:併發標誌清除垃圾回收器,對Young Gen會使用與Parallel Collector同樣的垃圾回收策略,對Tenured Gen,垃圾回收的垃圾標誌線程與應用線程同時進行,而垃圾清除則需要暫停應用線程,但暫停時間會大大縮減,需要注意的是,由於垃圾回收過程更加複雜,會降低總體的吞吐量。
在JVM啓動參數中使用:-XX:+UseConcMarkSweepGC啓動

具體的CMS方式,有個哥們解釋的更加清楚:
[url]http://softbeta.iteye.com/blog/1315103[/url]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章