以NIO通信例子结合Jconsole解释JVM内存分配机制

来源:互联网 发布:php foreach 多个数组 编辑:程序博客网 时间:2024/06/18 07:26
 JAVA的内存分配机制,在很多地方都已经解析很多次了,个人如何方便的来直观的了解,还有很多人不是很清楚,或者没有这样的机会,在这里我结合一个小例子,采用JDK自带的JConsole来说一下JVM的内存分配机制。
案例
首先解释下场景,服务端是一个通信服务器,接受客户端发过来的通信信息,并做业务处理;服务端采用JAVA中的MINA2框架,客户端可以任意,C++也好,JAVA也好,只要符合服务端规定的消息结构,发给通信服务器都能处理。
为了让大家更清楚,可以用MINA2框架中的时间服务器的例子来稍作修改模拟这个场景。系统环境,APACHE MINA2.0.4 + JDK1.6 + Eclipse3.7。
服务端代码如下:
Java代码 复制代码 收藏代码
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) );   
    }   
}  
业务处理器(这里当作一个时间服务器,接受到信息之后就立刻返回服务器当前时间) 
Java代码 复制代码 收藏代码
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代码: 
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();   
        //}     
           
    }   
}  
客户端处理类: 
Java代码 复制代码 收藏代码
public class TimeClientHandler extends IoHandlerAdapter {   
    public TimeClientHandler() {   
    }   
  
    @Override  
    public void messageReceived(IoSession session, Object message)   
            throws Exception {   
        System.out.println(message);// 显示接收到的消息   
    }   
}  

代码准备好之后,启动通信服务器服务端,然后启动客户端;
然后在打开你jconsole界面,选择需要观察的那个服务端的JAVA进程
首页就是类似这样的一个界面(注意:这篇文章里面所有的截图都是在模拟程序运行5个小时以后截下来的):
以NIO通信例子结合Jconsole解释JVM内存分配机制 - 火木棉 - 淡泊明智
JAVA内存模型简介
我们知道:JAVA的内存模型是这样的,简单的来说,可以分为堆和非堆内存,下面的著名的这个图大家经常会看到:
以NIO通信例子结合Jconsole解释JVM内存分配机制 - 火木棉 - 淡泊明智
内存由 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的方式来确保不存在内存碎片,采用空间换时间的方式来加速内存垃圾回收。
 
JConsole使用
JConsole是一个基于JMX的GUI工具,用于连接正在运行的JVM,不过此JVM需要使用可管理的模式启动。JConsole从java5开始,在JDK中提供。用于对JVM中内存,线程和类等的监控。
其使用很简单,JDK在你的环境变量之后,就可以在命令行中输入:
jconsole 
如果弹出窗口,说明配置可用。
弹出窗口如下:
以NIO通信例子结合Jconsole解释JVM内存分配机制 - 火木棉 - 淡泊明智
上图中的Eden Space就是JAVA对象初始进入的地方,
Survivor Space是A Suvivor Space空间
Tenured Gen 是Old Gen也就是大家说的养老区,
垃圾回收描述:
在New Generation块中,垃圾回收一般用Copying的算法,速度快。这里面的截图表示新生代已经执行了300多次的垃圾回收。
 
以NIO通信例子结合Jconsole解释JVM内存分配机制 - 火木棉 - 淡泊明智
每次GC的时候,存活下来的对象首先由Eden拷贝到某个Survivor Space, 当Survivor Space空间满了后, 剩下的live对象就被直接拷贝到Old Generation中去。因此,每次GC后,Eden内存块会被清空。在Old Generation块中,垃圾回收一般用mark-compact的算法,速度慢些,但减少内存要求.
还是上图
以NIO通信例子结合Jconsole解释JVM内存分配机制 - 火木棉 - 淡泊明智
说明采用mark-compact算法的养老区还没有执行GC,(0项收集);
回到这个例子,在堆内存的JCONSOLE面板中,有三个子项,分别是:
(一)Eden Space
其截图如下:
以NIO通信例子结合Jconsole解释JVM内存分配机制 - 火木棉 - 淡泊明智
这个是新生代内存,从图上来看,整体的运行内存还是比较稳定的。
(二)A Suvivor Space
以NIO通信例子结合Jconsole解释JVM内存分配机制 - 火木棉 - 淡泊明智
这个也是相当的稳定
(三)OLD
以NIO通信例子结合Jconsole解释JVM内存分配机制 - 火木棉 - 淡泊明智
OLD养老区的内存一直呈现上升趋势,而且在这5个多小时里面,一直没有下降的趋势。这个是为什么呢?大家知道, 内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。那么这个现象会不会出现内存溢出呢?我们在前面了解到,OLD养老区的内存回收mark-compact,一般是比较慢的,所以这5个小时,没有发生过一次养老区的垃圾回收。当OLD去的空间不够时,JVM会在OLD区进行完全的垃圾收集(0级),这样就会避免出现OLD区的OOM错误。
因为堆内存是上述三张图的内存集合,那么下张图中,堆内存的线性增长也就有合理的解释了,由于OLD还没有进行垃圾回收,里面的对象越来越多,占用内存越来越大,也就影响了整体堆内存
JCONSOLE还能看到什么呢?
第三个TAB页签,能看到当前的线程数
以NIO通信例子结合Jconsole解释JVM内存分配机制 - 火木棉 - 淡泊明智
第四个TAB页签,能够看到当前的JVM信息
以NIO通信例子结合Jconsole解释JVM内存分配机制 - 火木棉 - 淡泊明智
最有一个线程数能够看到当前JMX暴露出现的JVM运行的整体信息,比如说GC的次数,GC的类型,时间等等;
后续
这个模拟程序会随着时间的推迟而OLD去内存线性增长,也就意味着堆区内存线性增长,难道一直等着OLD区来做mark-compact的算法的GC?
当然也是有方法去改进GC的算法的,先来了解一下基本的知识:
垃圾回收策略
      评估垃圾回收策略的两个重要度量是:

    吞吐量(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启动
原创粉丝点击