关于ObjectOutputStream内存溢出和JVisualVM堆分析使用

最近做公司后台的关于数据同步的功能, 考虑到同步的速度和可控性,再者在自己的项目中已经打开了ServerSocket.因此决定自己编写Socket,两边进行数据同步. 因为写一个通信协议是不容易的事,我也懒得写了,直接用了Java的ObjectInputStream和ObjectOutputStream在两边序列化对象.当然程序从效能上和质量上都还满意.但是运行一段时间久出现了OutOfMemery,起初我还怀疑自己的List的声明为final和List.clear()方法出了问题,经过改写,几番下来仍然还是会出现OutOfMemery,我用了最熟悉的JVisualVM安装好几个插件(常用的Visual GC),决定来查看一下程序中的问题.

先贴上我的程序,代码中我删减了部分.

public class AliCloudSocketTest {
    private Socket socket = null;

    protected MongoTemplate mongoTemplate = null;
    private JdbcTemplate jdbcTemplate = null;

    private ObjectInputStream inputStream = null;
    private ObjectOutputStream outputStream = null;

    public AliCloudSocketTest() {
    }

    public void setUp(String host, int port) throws Exception {
        //创建Socket,和创建输入输出
        socket = new Socket(host, port);
        outputStream = new ObjectOutputStream(socket.getOutputStream());
        inputStream = new ObjectInputStream(socket.getInputStream());
    }

    public void run() {
        final long dwEntitySectionUrn = 3118395L;//媒体类型
        Query query = Query.query(Criteria.where("dwEntitySectionUrn").is(dwEntitySectionUrn));
//        query.skip(60000);

        mongoTemplate.executeQuery(query, "postStore", new DocumentCallbackHandler() {
            List<Object> postList = new ArrayList<Object>(50);
            @Override
            public void processDocument(final DBObject post) throws MongoException, DataAccessException {
                post.put("userUrn", "default_" + post.get("dwEntitySectionUrn")); //遍历发送每一个文档,每50条发送一次
                postList.add(post); //程序要攒够50条数据发送
                if(postList.size() < 50) {
                    return; 
                }
                //这些程序都用一个User
                String userUrn = "default_" + post.get("dwEntitySectionUrn");
                final Object[] user = new DBObject[1];
                if (userUrn != null) {
                    mongoTemplate.executeQuery(Query.query(Criteria.where("userUrn").is(userUrn)),
                            "userStore",
                            new DocumentCallbackHandler() {
                                @Override
                                public void processDocument(DBObject object) throws MongoException, DataAccessException {
                                    user[0] = object;
                                }
                            });
                }

                //如果没有查询到用户
                if (user[0] == null) {
                    //这里一定要查询到用户,哪怕构造一个
                    String sql = "SELECT DW_ENTITY_SECTION_URN, ENTITY_SECTION_NAME, ENTITY_SECTION_IMG_PATH, " +
                            " SERVICE_TYPE_URN, REPLY_TXT_LENGTH, REPLY_MIN_INTERVAL, " +
                            " PROFILE_URL_PATTERN, SERVICE_TYPE_NAME " +
                            " FROM WEB_ENTITY_SECTION WHERE DW_ENTITY_SECTION_URN = ? ";

                    user[0] = jdbcTemplate.queryForObject(sql, 
                             new Object[]{post.get("dwEntitySectionUrn")}, 
                             new UserStoreMapper());
                }

                try {
                    send(postList, Arrays.asList(user[0])); //Socket发送数据
                    postList.clear(); //清除原来的数据,释放内存
                    postList = null; //还OutoffMem? 重新new对象行不行?
                    postList = new ArrayList<Object>(50);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public void send(List<Object> storeList, List<Object> userList) throws Exception {
        try {
            final SocketCommandEntity<ReplicationEntity> entity = new SocketCommandEntity<ReplicationEntity>();
            entity.setAuth("****");
            entity.setCommand("****");

            //如果是最后一条数据, 就设置为false,表示没有了,这样可以节省很大的开销.默认是true
            entity.setMore(true);

            //把PostStore和UserStore序列化成JSON, 用户可以单独的上去,帖子必须携带用户.否则远程报错
            ReplicationEntity entity1 = new ReplicationEntity(JSON.toJSONString(storeList),
                    JSON.toJSONString(userList));
            entity1.setArray(true); //如果是JSON数组,则将其设置为true, 默认是单个的对象false.
            entity.setPayLoad(entity1);

            //把数据发送到远程
            outputStream.writeObject(entity);
            //flush buffere缓存
            outputStream.flush();

            //需要读取异常
            Object object = inputStream.readObject();
            if (object != null) {
                CommandResponseEntity responseEntity = (CommandResponseEntity)object;
                //responseEntity.isSuccess() 当返回true说明调用成功.
                System.out.println(ReflectionToStringBuilder.toString(responseEntity));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

大家可以看到,程序是非常简单. 起初的我List<Object> postList = new ArrayList<Object>(50);还是在run()方法中的局部变量, 因为匿名内部类要使用,因此还声明为final,在数据发送完成后把List,clear()清除数据.但是还是阻止不了程序OutOfMemery.经过用JVisualVM查看内存使用,发现Old区只增不减,最终吃尽所有的老年代.如图:

关于ObjectOutputStream内存溢出和JVisualVM堆分析使用_第1张图片

尽管我没有设置JVM参数来增大内存,但我相信我的程序是没有发生内存溢出的可能的,因为每次最多List 50条数据,正常的话内存都被释放掉.而且我手动的点击Visualvm上的GC,GC虽然发生了,但是Old还是不会降下来.
很明显的,有大量的数据没有被释放, 我使用visualvm的抽样器发现,内存中大量的char[]和String, 其实他们都是String.如图:
关于ObjectOutputStream内存溢出和JVisualVM堆分析使用_第2张图片

最终我通过VisualVM生成堆Dump,通过”文件“-”装入“,文件类型“选择‘堆Dump.Hprof”选择堆文件路径打开堆转述文件,-一步一步的查看.在Visualvm的首届面,我直接选择了查找 20 保留大小最大的对象。检查最大的对象如图:

关于ObjectOutputStream内存溢出和JVisualVM堆分析使用_第3张图片

正好,它出现了我的AliCloud××类,直接点击打开对象,如图:

关于ObjectOutputStream内存溢出和JVisualVM堆分析使用_第4张图片

这是直接打开的实例控制台,可以看到,我的AliCloud××对象使用了233M的内存,而他的内部属性outputStream占了内存的99%以上,而这个outputtream正就是ObjectOutputStream。而这个类是JDK中自带的类,我在继续对outputream继续分析(右键显示实例就可以把选中的对象当作根对象,继续打开子属性),我尽挑占用内存大的对象一级一级往下找,

关于ObjectOutputStream内存溢出和JVisualVM堆分析使用_第5张图片

关于ObjectOutputStream内存溢出和JVisualVM堆分析使用_第6张图片

这一看我惊呆了,发送了这么多对象,每一个对象都有保存,数据都是全的。至此,我隐约的发现问题的根本。outputStream把数据用Socket发送到另一端,而发送的数据对象默认并没有在完成后清除掉,在ObjectOutputStream内部还持有数据对象的句柄,使GC无法回收占用的内存,从而内存一步步的被耗尽。

后来,我用“ObjectOutputStream 内存泄漏”的关键字Google了一下,找到了内存泄漏的原因[见:http://blog.sina.com.cn/s/blog_7099ca0b0100n9n6.html]。关键在于没有调用reset方法.

在Java其他IO类设计中,OutputSteam是不需要什么reset方法的.这也是Java中ObjectOutputStream和其他IO类使用上不同的地方。


若不是这个OutOffMemery,我想我不会发现这个问题。而且大多数我们序列化对象到文件系统,或者发送少量的数据,都不会有什么大的问题。但是对于ObjectOutputStream发送大量数据时,没有调用reset()方法就会产生这个问题。这是我们使用上的原因,也同时是JDK类设计者准备留给程序员最大的程序控制能力。这是个简单的错误,一个简单的reset的调用,有的程序员很早就遇到,有的也可能工作几年都没有发现,我觉得有必要写这么一片文章给大家纠正一下。

你可能感兴趣的:(java,JVisualVM)