Category Archives: JAVA

about java

java序列化Serializable小结

作者: dplord, 访问量 195





一、什么是java序列化

首先谈一下什么是序列化, 序列化简单的来说,序列化会用在把一个对象、一个变量,以数据形式保留。比如把对象的二进制存在缓存、文件中。等需要的时候再次拿出来,反序列化为你想要的变量、对象。Java序列化即为,在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。

由上面可知,java序列化是为了保存一个对象当前属性的,便于还原重新构造这个对象。因此,java中的序列化做的即为保存java对象的成员变量的。因此,java序列化并不会关注类中的静态变量。同时java也对一些不需要序列化的变量提供了关键词transient,用transient修饰的变量就不会被序列化跟反序列化。

序列化好的数据,可以便于本地写入文件、或者写入redis缓存供多个机器通过读缓存的方式一起使用这个对象。用处很多。

二、java序列化的用法示例

首先写一个类implements  java.io.Serializable,如下

package serialize;

import java.io.Serializable;

/**
 * Created by dengpan on 2016/12/27.
 */
public class A1 implements Serializable {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

这里简单的实现序列化一个对象到文件并从文件读出对象

package serialize;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * Created by dengpan on 2016/12/28.
 */
public class A1_2 {
    public static void main(String[] args) throws Exception {
        //序列化
        A1 obj = new A1();
        obj.setAge(12);
        obj.setName("nihao");

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/Users/dengpan/1.bin"));
        oos.writeObject(obj);
        oos.flush();
        oos.close();


        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/Users/dengpan/1.bin"));
        Object obj2 = ois.readObject();
        System.out.println(obj2.getClass());
        System.out.println(obj2.toString());
    }
}

即可正常的实现序列化到文件跟从文件反序列化到对象。

三、序列化原理

3.1 如何序列化一个对象

把一个对象从一个内存对象序列化为字节,如果让我做一个简单的序列化设计的话,即保存对象所有属性的值,对于int、short、float、boolean、byte、string各种类型,设定好相应的write、read方法。写的时候,按取到对象属性的顺序依次写入,读的时候按照对象属性的顺序依次独出。

下文3.3节会给出自己基于反射做的最简单的对象序列化。

 3.2 java序列化

序列化代码例子,java序列化的话,很简单。如果是把一个对象序列化的话,implement java.io.Serializable 接口即可。一个类如果是implements java.io.Serializable的话,它的子类也是可以序列化的。序列化的时候,如果一个成员变量是基本类型如int、double类型的,这些可以被序列化,但是如果成员变量的类型是未实现Serializable接口的类的话,需要把成员变量的类型这个类,实现序列化接口 Serializable。

3.3 实现Serializable接口的意义与自定义实现序列化过程

有人会问一个类仅仅implements Serializable,这个有什么意义呢。其实implements Serializable仅仅代表这个类可以被序列化。这个在下文会具体提到。如果你想自定义实现这个序列化、反序列化接口。那么在implements Serializable之后,复写writeObject、readObject即可。简单demo如下:

package serialize;

import java.io.IOException;
import java.io.Serializable;

/**
 * Created by dengpan on 2017/1/14.
 */
public class Person implements Serializable {
    private static final long serialVersionUID = 1L;

    private int age;
    private String name;

    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        //自定义序列化
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        //自定义反序列化
    }
}

3.4 序列化ID serialVersionUID的作用

java的serialVersionUID, 可以获取一个序列化ID,一个已经编译好的java class。序列化ID是已经决定的。这个序列化的ID用于我们做版本控制,比如我先写好了一个类的serialVersionUID = 1L,之后我把这个类serialVersionUID改为2L,这样之前序列化存下来的数据就因为serialVersionUID不一致,因此抛出InvalidClassException的异常。这样便于在一些传输序列化数据的过程中,做版本控制。这个serialVersionUID是会被写入文件序列化之后的字节里面去的。下文3.2.3会详细按照字节来解读序列化的过程与意义。

一般建议我们在一个实现了Serializable接口的类,手动写入serialVersionUID, 例如:private static final long serialVersionUID = 1L 当然这个1L是我自己写的,可以随便写入一个值。java IDE也提供了随机生成serialVersionUID的方法。这里介绍下我常用的jetbrains Idea 给一个类生成随机Serializable的方法。

 

QQ20170114-0@2x

 

在Serialization issues->Serializable class without “serialVersionUID”, 勾选此处。

在一个implements Serializable 接口的class里,光标停留在类名处,alt + enter。在弹出的框选择add “serialVersionUID” field。即可随机生成serialVersionUID。

QQ20170114-1@2x

 

也可用java官方自带的工具serialver,生成或者查看serialVersionUID。

用法为: serialver -classpath .  Person,即可查看或者生成serialVersionUID。可以看到输出 Person:    private static final long serialVersionUID = 1L;

3.5 序列化过程字节解读

比如一个类A1,定义如下:

package serialize;

import java.io.Serializable;

/**
 * Created by dengpan on 2016/12/27.
 */
public class A1 implements Serializable {
    private static final long serialVersionUID = 1L;
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

用如下代码序列化这个对象到1.bin这个文件

    //序列化
    A1 obj = new A1();
    obj.setName("王祖贤");
    obj.setAge(18);
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/Users/dengpan/tmp/1.bin"));
    oos.writeObject(obj);
    oos.flush();
    oos.close();

文件大小,文件字节信息如下图:

QQ20170114-5@2x

 

下面从序列化的顺序来分析下字节的含义与写入顺序。

这里给出ObjectOutputStream的writeObject的调用栈:

writeObject —> writeObject0 —>writeOrdinaryObject—>writeSerialData—>invokeWriteObject

上图可以看到, 序列化的对象是serialize.A1 的实例,该类有2个成员变量int age、String name,分别赋值为age=18,name=”王祖贤”。将这个对象序列化后字节为83b。

来分析下这个83b字节的含义。

① ac ed 00 05 只是写入序列化文件的一个固定标记,起标识作用。这个常量在接口ObjectStreamConstants定义如下:final static short STREAM_MAGIC = (short)0xaced;如下图红色框框表明。

QQ20170115-2@2x

这里到了到了java.io.ObjectOutputStream#writeObject0这一步,这个方法里面。有这么一段代码。

   // remaining cases
        if (obj instanceof String) {
        writeString((String) obj, unshared);
    } else if (cl.isArray()) {
        writeArray(obj, desc, unshared);
    } else if (obj instanceof Enum) {
        writeEnum((Enum<?>) obj, desc, unshared);
    } else if (obj instanceof Serializable) {
        writeOrdinaryObject(obj, desc, unshared);
    } else {
        if (extendedDebugInfo) {
            throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
        } else {
            throw new NotSerializableException(cl.getName());
        }
    }                     

可以看到序列化要检测obj是否是String、Array、Enum或者是实现了Serializable接口。因此implements Serializable是作为一个类可不可以被序列化的标记。

② 接下来的73 72 00 代表也是一种标识,在

73在java.io.ObjectOutputStream#writeOrdinaryObject  bout.writeByte(TC_OBJECT);这句写入。剩下的72 00 也是一种标志flag写入的。跟序列化到数据关系不大。

写入的是跟这个类相关的描述flag。

QQ20170115-3@2x

③然后开始调用writeNonProxyDesc 写类名了。类名调用的是out.writeUTF(name); 先写类名长度、再写内容。下图红色框框中的0c 是代表类名serialize.A1的长度为12,serialize.A1的16进制等于 73 65 72 69 61 6c 69 7a 65 2e 41 31。在下图绿色框框中表明。

QQ20170115-4@2x

④写完了类名,开始写序列序列化ID   serialVersionUID了。out.writeLong(getSerialVersionUID());讲序列化id作为一个long,8个字节写入。00 00 00 00 00 00 00 01就是我定义的 private static final long serialVersionUID = 1L;  从这里可以看到序列化id是写入文件的,反序列化的时候,也会读这个id,不一致就会抛异常。因此建议大家在要序列化的地方都指定一个序列化id。

⑤ 接下来开始写field了。写入成员变量数据与信息。下图红色框框的02,是检测该类是不是externalizable的,是一个bye的flag标志位。绿色框框的00 02,代表这个类有2个field。用short数字写入field长度。因此最多能定义65536个field,多了序列化写不进去。

QQ20170115-5@2x

其实java中, 根据编译之后的class文件有一个用无符号两个字节u2记录该class类有多少个显示声明的类或者字段,也就是2的16次方,65536个。跟这里设计是吻合的。

⑥ 写完field数量,开始写field数据了。下图中,红框中的49是写的代表第一个field age字段的类型I, 根据java.io.ObjectStreamField.getTypeCode() 得到。

QQ20170115-6@2x

java的各种typeCode列如下表:

B byte
C char
D double
F float
I int
J long
L class或者interface
S short
Z boolean
[ array

绿色框框的00 03,是一个short数字,代表field age的长度,61 67 65 就是字符串age的16进制表示。接下来的4c是L即第二个field name的typeCode,代表这个字段是一个Class、00 04代表是第二个field name的长度,6e 61 6d 65 是name的16进制表示。接下来下图中的74是bout.writeByte(TC_STRING);
bout.writeUTF(str, utflen);中的 TC_STRING,写入string的标识。00 12代表的是name的类型Ljava/lang/String; 的长度。前面的L代表是一个Class,有上表所示。接下来红框中的 18个字节代表的是 Ljava/lang/String; 这个字符串的16进制内容。

QQ20170115-14@2x

⑦ 类描述、以及field信息都写进去了,现在开始调用java.io.ObjectOutputStream#writeSerialData方法写序列化数据了。下图红色的78是bout.writeByte(TC_ENDBLOCKDATA); 写的一个block data分隔字节,是一个常量。接下来的绿色框框中的70, 是TC_BASE(112)的16进制表示。是代表接下来要写一个数字了。00 00 00 12是4个字节,标识的age = 18,这个18。74是TC_STRING的标识,代表接下来要写的是字符串。。00 09是1个short数字,占2个字节,代表字符串”王祖贤”占9个字节。紫红色的框中的e7 8e 8b e7 a5 96 e8 b4 a4代表字符串 “王祖贤” 的16进制表示。

QQ20170115-15@2x

四、java序列化小结

由以上分析可以看到,序列化中需要指定serialVersionUID,同时java序列化,是一个基本的序列化,也只能在java中保存一个对象到字节流,并不能很好的跟别的语言传输序列化的字节流做RPC用。基于java序列化的RMI并不能跟别的语言RPC,就是因为java序列化只是java语言内部用的。如果需要多语言间共享序列化数据或者追求更小体积、更好的序列化性能,可以选择thrift、protobuf等等序列化技术。

五、参考文章

 

关于一些language的部分性能对比

作者: dplord, 访问量 922





周五晚上闲着无聊跟lys 讨论语言性能

然后我随机一想想出了一个测试用例。

  1. 例子如下:
  2. 创建 10000 个文本,文本在当前目录的 out目录下,文本名为 r_1、r_2、r_3..
  3. 每个文本都有 10000 行,每一行是用 \t  间隔的5个数,每个数是1-10000 之间的随机数

随便我们用什么语言实现,暂时仅仅对比完成目标的时间

其实这个测试用例对比的就是以下几个方面:

  1. 文件IO性能
  2. 文件名需要拼接,字符串concat性能
  3. 大量的循环以及随机数的产生比拼语言的速度

我先顺手写了一个C的,代码如下:

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#define MAX 10000

char * one_mil();
int create_one(int filename);
int main()
{
	int i;
	for(i=0;i<MAX;i++)
	{
		create_one(i);
	}
	return 0;
}

int create_one(int filename)
{
	int f;
	char *file_name;
	file_name=malloc(12);
	sprintf(file_name,"out/r_1_%d",filename);
	mode_t f_attr;
	f_attr=S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH;
	f=open(file_name,O_RDWR | O_CREAT,f_attr);
	if(f==-1)
	{
		printf("创建%d出错\n",filename);	
	}else{
		char *temp_b=one_mil();
		write(f,temp_b,strlen(temp_b));
		close(f);
		free(temp_b);
	}
	return 0;
}

char * one_mil()
{
	char *p;
	int i,p_size=0;
	p=malloc(MAX*100);
	char *temp_a;
	temp_a=malloc(100);
	for(i=0;i<10000;i++)
	{
	    sprintf(temp_a,"%d\t%d\t%d\t%d\t%d\n",rand()%10000+1,rand()%10000+1,rand()%10000+1,rand()%10000+1,rand()%10000+1,rand()%10000+1);
	    memcpy(p+p_size,temp_a,strlen(temp_a));
	    p_size+=strlen(temp_a);	
	}	
	free(temp_a);
	return p;
}

编译为:

/home/dengpan/opt/gcc-4.9.1/bin/gcc -std=c99 myfile.c -o a.out -O2

注:后续C代码编译都是最新的gcc 4.9.1 -std=c99 -O2

执行时间为:

20

看来还是很快的,但是还有几个很大的优化点第一个就是频繁申请大块的内存与释放,第二就是sprintf与memcpy执行过多

 

下面来了一个JAVA版本的,几乎默认写法,测试结果惊人

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Random;

/**
 * Created by dengpan on 14-10-25.
 */
public class MyFile {
    private static final int MAX = 10000;

    private static final Random sRandom = new Random();

    private static String one_mil() {
        StringBuilder builder = new StringBuilder(MAX * 100);
        for(int i = 0; i < MAX; i++) {
            builder.append(sRandom.nextInt(10000) + 1);
            builder.append('\t');
            builder.append(sRandom.nextInt(10000) + 1);
            builder.append('\t');
            builder.append(sRandom.nextInt(10000) + 1);
            builder.append('\t');
            builder.append(sRandom.nextInt(10000) + 1);
            builder.append('\t');
            builder.append(sRandom.nextInt(10000) + 1);
            builder.append('\n');
        }

        return builder.toString();
    }

    private static void create_one(File dir, int filename) throws IOException {
        File out = new File(dir, "r_java_" + filename);
        if (!out.exists())
            out.createNewFile();
        String content = one_mil();
        FileOutputStream os = new FileOutputStream(out);
        os.write(content.getBytes());
        os.close();
    }

    public static void main(String[] args) {
        File dir = new File("out");
        if (!dir.exists())
            dir.mkdirs();
        for (int i = 0; i < MAX; i++) {
            try {
                create_one(dir, i);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

结果

1

但是多次测试结果不稳定,结果在39s-47s 之间,果然是JVM Snapshot复杂的动态调度有关,不过性能很吓人

因为linux系统所有的对文件读写,内存使用都是系统调用实现的,只不过不同的系统对系统调用有了封装。有了封装必然慢,java简直吓人

我觉得纯粹在文件操作,C用了open write绝对在IO上比JAVA快,只不过在buffer处理上还有sprintf这个函数比java慢抵消了IO优势

 

我决定处理一下刚才的那段C代码

优化方向为:

  • 减少函数调用
  • 少用sprintf把一行5个的sprintf合并
  • 减少内存malloc跟free的次数

处理后的代码为:

#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

#define FILE_COUNT 10000

int main()
{
	char filename[100];
	char *text_buf;
	char *p;
	int i, j;
	int fd;
	size_t count = 0;
	int nbytes;

	mode_t f_attr;
	f_attr = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;

	text_buf = malloc(FILE_COUNT * 100);
	for (i = 0; i < FILE_COUNT; i++) {
		sprintf(filename, "out/r_1_%d", i);		
		fd = open(filename, O_RDWR | O_CREAT, f_attr);
		if (fd < 0) {
			perror("create failed");
			exit(EXIT_FAILURE);
		}

		p = text_buf;
		count = 0;
		for (j = 0; j < FILE_COUNT; j++) {
			nbytes = sprintf(p, "%d\t%d\t%d\t%d\n", 
					rand() % FILE_COUNT + 1,
					rand() % FILE_COUNT + 1,
					rand() % FILE_COUNT + 1,
					rand() % FILE_COUNT + 1,
					rand() % FILE_COUNT + 1);
			if (nbytes <= 0) {
				perror("oome");
				exit(EXIT_FAILURE);
			}

			p += nbytes;
			count += nbytes;
		}
		write(fd, text_buf, count);
		close(fd);
	}
	free(text_buf);
	return 0;
}

编译以后执行

21

优化明显

但是还是比JAVA的慢或者不相上下,按理来说C在这种疯狂调用系统IO绝对比JAVA快,我觉得这段代码的慢在sprintf上,由于我们的格式化仅仅需要格式化%d 都可以自己写一个,于是决定改写sprintf

改写后代码如下:

#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

#define FILE_COUNT 10000

size_t sprint_int(char *buf, int val)
{
	size_t nbytes = 0;
	int tmp = val;
	char *p;

	do {
		tmp /= 10;
		nbytes++;
	} while (tmp != 0);
	
	p = buf + nbytes - 1;
	while (p >= buf) {
		tmp = val % 10;
		val /= 10;
		*p-- = '0' + tmp;
	}
	
	return nbytes;
}

int main()
{
	char filename[100];
	char *text_buf;
	char *p;
	int i, j;
	int fd;
	size_t count = 0;
	int nbytes;

	mode_t f_attr;
	f_attr = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;

	text_buf = malloc(FILE_COUNT * 100);
	for (i = 0; i < FILE_COUNT; i++) {
		sprintf(filename, "out/r_c_%d", i);		
		fd = open(filename, O_RDWR | O_CREAT, f_attr);
		if (fd < 0) {
			perror("create failed");
			exit(EXIT_FAILURE);
		}

		p = text_buf;
		count = 0;
		for (j = 0; j < FILE_COUNT; j++) {
			nbytes = sprint_int(p, rand() % FILE_COUNT + 1);
			count += nbytes + 1;
			p += nbytes;
			*p++ = '\t';

			nbytes = sprint_int(p, rand() % FILE_COUNT + 1);
			count += nbytes + 1;
			p += nbytes;
			*p++ = '\t';

			nbytes = sprint_int(p, rand() % FILE_COUNT + 1);
			count += nbytes + 1;
			p += nbytes;
			*p++ = '\t';

			nbytes = sprint_int(p, rand() % FILE_COUNT + 1);
			count += nbytes + 1;
			p += nbytes;
			*p++ = '\t';

			nbytes = sprint_int(p, rand() % FILE_COUNT + 1);
			count += nbytes + 1;
			p += nbytes;
			*p++ = '\n';

		}
		write(fd, text_buf, count);
		close(fd);
	}
	free(text_buf);
	return 0;
}

结果:

1

果然很不错

本来测试C的,以前学过glib

用glib写了一个版本,看看效果

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <glib.h>

#define MAX_NUM 10000

int main()
{
	mode_t f_attr;
	f_attr = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;

	for(int i=0; i < MAX_NUM; i++)
	{
		GString *s;
		s = g_string_new("");
		for(int j = 0; j < MAX_NUM; j++)			
		{
			g_string_append_printf(s,"%d",rand() % MAX_NUM + 1);
			g_string_append(s,"\t");
			g_string_append_printf(s,"%d",rand() % MAX_NUM + 1); 
			g_string_append(s,"\t");
			g_string_append_printf(s,"%d",rand() % MAX_NUM + 1); 
			g_string_append(s,"\t");
			g_string_append_printf(s,"%d",rand() % MAX_NUM + 1); 
			g_string_append(s,"\t");
			g_string_append_printf(s,"%d",rand() % MAX_NUM + 1); 
			g_string_append(s,"\n");
		}
		
		char *fname;
		fname = malloc(12);
		sprintf(fname,"out/r_%d",i);
		int f;
		f = open(fname,O_RDWR | O_CREAT,f_attr);
		write(f,s->str,s->len);
		close(f);
		free(fname);
		g_string_free(s,TRUE);
	}
}

结果:

1

看来glib在疯狂的内存释放跟申请方面很弱,所以各大服务器端软件基本都是自己用C实现一套基本数据结构

后面闲着没事都写了一个版本

  • PHP版本,拼接字符串用了
  • concat
  • 拼接成数组,再implode
  • 在github找php StringBuffer的实现

等,最快的如下,下面python ruby  nodejs也仅仅展现测试出来的 最好的版本

<?php
for($i = 0 ; $i < 10000 ; $i++)
{
	$text='';		
	for($j = 0 ; $j < 10000 ; $j++)
	{ 
		$temp='';
		//$text.= rand(1,10000)."\t".rand(1,10000)."\t".rand(1,10000)."\t".rand(1,10000)."\t".rand(1,10000)."\n";
		sprintf($temp,"%d\t%d\t%d\t%d\t%d\n",rand(1,10000),rand(1,10000),rand(1,10000),rand(1,10000),rand(1,10000));
		$text.=$temp;
	}
	file_put_contents("out/r_php_$i",$text);
}

时间

PHP5.6.0  1m51s

HHVM 3.3.0  58s

算是很快很快的

 

python版本的

#encoding:utf-8

import random
for i in xrange(10000):
	f = open('out/r_py_' + str(i) ,'w+')
	a = []
	for j in xrange(10000):
		a.append(random.randint(1,10000))
		a.append('\t')
		a.append(random.randint(1,10000))
		a.append('\t')
		a.append(random.randint(1,10000))
		a.append('\t')
		a.append(random.randint(1,10000))
		a.append('\t')
		a.append(random.randint(1,10000))
		a.append('\n')
	f.write(''.join(a))
	f.close()
	
	

结果 20+ min

ruby版本

#encoding:utf-8
	
require "stringio"

(1..10000).each do |i|
	a = ''
	(1..10000).each do |j|
		s = StringIO.new
		s << rand(1000)+1 << "\t" << rand(1000)+1 << "\t"<< rand(1000)+1 << "\t"<< rand(1000)+1 << "\t"<< rand(1000)+1 << "\n"
	end
	f = File.new("out/r_rb_"+i.to_s,"w+")
	f.syswrite(s.string)
	f.close()
end

结果 20+ min

 

lys写了一个nodejs版本的

var fs = require('fs');
var buf = new Buffer(10000);
var len = 0;
for (var i = 0; i < 10000; i++) {
	for (var j = 0; j < 10000; j++) {
		len += buf.write(Math.floor((Math.random() * 10000) + 1) + "\t", len);
		len += buf.write(Math.floor((Math.random() * 10000) + 1) + "\t", len);
		len += buf.write(Math.floor((Math.random() * 10000) + 1) + "\t", len);
		len += buf.write(Math.floor((Math.random() * 10000) + 1) + "\t", len);
		len += buf.write(Math.floor((Math.random() * 10000) + 1) + "\n", len);
		if (j % 100 == 0) {
			fs.writeFile("out/r_js_" + i, buf.toString('ascii', 0, len), function(err){
				if (err)
					console.log(err);
			});
			len = 0;
		}
	}
}

结果内存溢出跑不完

Continue reading