对象转String过程中出现java.lang.StackOverflowError堆栈溢出错误的分析
最近在做项目过程中多次遇到该问题,所以整理一下做个笔记。该错误出现的原因一般都是因为不停的循环递归调用。1、虚拟机栈是什么? 栈也叫栈内存,是java虚拟机的内存模型之一。它的生命周期是在线程创建时创建,线程结束而消亡,释放内存。因此是私有的,不可共享 栈存储的数据,以栈帧(Stack Frame)为单位存储,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当
·
最近在做项目过程中多次遇到该问题,所以整理一下做个笔记。
该错误出现的原因一般都是因为不停的循环递归调用。
该错误出现的原因一般都是因为不停的循环递归调用。
1、虚拟机栈是什么?
栈也叫栈内存,是java虚拟机的内存模型之一。它的生命周期是在线程创建时创建,线程结束而消亡,释放内存。因此是私有的,不可共享
栈存储的数据,以栈帧(Stack Frame)为单位存储,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法执行完毕后,F2栈帧先出栈,F1栈帧再出栈,遵循“先进后出”原则。
2、栈帧:每当一个java方法被执行时都会在虚拟机中新创建一个栈帧,方法调用结束后即被销毁
栈帧存储数据包含以下5个部分:
①.局部变量表 :保存函数的参数以及局部变量用的,局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,随着函数栈帧的销毁,局部变量表也会随之销毁。
存放基本数据类型变量(boolean、byte、char、short、int、float)、引用类型的变量(reference)、returnAddress(指向一条字节码指令的地址)类型的变量。
②.操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。只支持出栈入栈操作。在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递。
③.动态链接
④.方法出口信息
⑤.其他
栈也叫栈内存,是java虚拟机的内存模型之一。它的生命周期是在线程创建时创建,线程结束而消亡,释放内存。因此是私有的,不可共享
栈存储的数据,以栈帧(Stack Frame)为单位存储,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法执行完毕后,F2栈帧先出栈,F1栈帧再出栈,遵循“先进后出”原则。
2、栈帧:每当一个java方法被执行时都会在虚拟机中新创建一个栈帧,方法调用结束后即被销毁
栈帧存储数据包含以下5个部分:
①.局部变量表 :保存函数的参数以及局部变量用的,局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,随着函数栈帧的销毁,局部变量表也会随之销毁。
存放基本数据类型变量(boolean、byte、char、short、int、float)、引用类型的变量(reference)、returnAddress(指向一条字节码指令的地址)类型的变量。
②.操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。只支持出栈入栈操作。在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递。
③.动态链接
④.方法出口信息
⑤.其他
所以当方法中递归调用出现死循环时会抛出该异常。
当对象转成String过程中,如果对象出现互相引用的情况,如果没有做好处理就会出现java.lang.StackOverflowError。
public class Student {
private int id;
private String name;
private List<Teacher> teachers = new ArrayList<Teacher>();
//get、set等方法省略
}
public class Teacher {
private int id;
private String name;
private List<Student> students = new ArrayList<Student>();
//get、set等方法省略
}
Student类里有个List集合存放Teacher对象,Teacher类中也有个List集合存放Studen对象。
public class Test {
public static void main(String[] args) throws Exception{
Student student = new Student();
student.setId(111);
student.setName("张三");
List<Student> students = new ArrayList<Student>();
students.add(student);
Teacher teacher = new Teacher();
teacher.setId(100);
teacher.setName("李四");
List<Teacher> teachers = new ArrayList<Teacher>();
teachers.add(teacher);
teacher.setStudents(students);
student.setTeachers(teachers);
ObjectAnalyzer analyzer = new ObjectAnalyzer();
System.out.println("方式一:");
System.out.println(analyzer.objToString(student));
System.out.println("方式二:");
System.out.println(JSON.toJSONString(student));
System.out.println("方式三:");
System.out.println(new Gson().toJson(student));
}
}
studen对象的list集合中包含了teacher,teacher对象的list集合中包含了studen。然后我使用了三种方式来将student对象转换成String。
三种方式分别是:1.通用的对象转String方法,参考于《Java核心技术》一书,就是遍历对象的所有属性,如果属性是对象递归遍历,直到属性是基本类型就转成String;2.使用阿里巴巴的
fastjson提供的toJSONString()方法,版本1.2.38;3.使用google的Gson提供的toJson()方法,版本号2.8.0;
三种方式执行结果:
方式一:
com.bawy.study.jdk.string.Student[id=111,name=张三,teachers=java.util.ArrayList[elementData=class java.lang.Object[]{com.bawy.study.jdk.string.Teacher[id=100,name=李四,students=java.util.ArrayList[elementData=class java.lang.Object[]{...,null,null,null,null,null,null,null,null,null},size=1][modCount=1][][]][],null,null,null,null,null,null,null,null,null},size=1][modCount=1][][]][]
方式二:
{"id":111,"name":"张三","teachers":[{"id":100,"name":"李四","students":[{"$ref":"$"}]}]}
方式三:
Exception in thread "main" java.lang.StackOverflowError
at java.io.StringWriter.write(StringWriter.java:112)
at com.google.gson.stream.JsonWriter.string(JsonWriter.java:591)
at com.google.gson.stream.JsonWriter.writeDeferredName(JsonWriter.java:402)
at com.google.gson.stream.JsonWriter.value(JsonWriter.java:527)
at com.google.gson.internal.bind.TypeAdapters$7.write(TypeAdapters.java:250)
at com.google.gson.internal.bind.TypeAdapters$7.write(TypeAdapters.java:235)
...
前两种方式均可以实现转换,第三种方式出现
java.lang.StackOverflowError。据说Gson也可以避免出现死循环,具体没有研究。
方式一转换后信息比较全,方式二比较简洁,但是方法二的缺点是如果属性没有get方法,那么在转成字符串后是不会有该属性的值的。
可以看到方式一中teaches集合中的studen对象变成了“...”,方式二中变成了'{"$ref":"$"}',下面贴一下方式一的代码,就可以知道其中原因。
public class ObjectAnalyzer {
private ArrayList<Object> visited = new ArrayList<Object>();
public String objToString(Object obj){
if (obj==null){
return "null";
}
//避免出现因为互相引用而出现死循环
if (visited.contains(obj)){
return "...";
}
visited.add(obj);
Class clz = obj.getClass();
if (clz==String.class){
return (String) obj;
}
if (clz.isArray()){
String r = clz.getComponentType()+"[]{";
for (int i = 0; i< Array.getLength(obj); i++){
if (i>0){
r += ",";
}
Object val = Array.get(obj,i);
if (clz.getComponentType().isPrimitive()){
r +=val;
}else{
r += objToString(val);
}
}
return r+"}";
}
String r = clz.getName();
do{
r = r+"[";
Field[] fields = clz.getDeclaredFields();
AccessibleObject.setAccessible(fields,true);
for (Field f:fields){
int modifier = f.getModifiers();
if (!Modifier.isStatic(modifier)){
if (!r.endsWith("[")){
r = r+",";
}
r +=f.getName()+"=";
try {
Class t = f.getType();
Object val = f.get(obj);
if (t.isPrimitive()){
r +=val;
}else{
r +=objToString(val);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
r += "]";
clz = clz.getSuperclass();
}while (clz!=null);
return r;
}
}
可以看到类ObjectAnalyzer中有一个集合visited,用来保存已经访问过的对象,即已经转换成String的对象,再次遇到该对象的时候不再访问对象的所有属性,而是直接返回“...”,以此避免死循环出现。fastJson大概也是进行类似处理,当遇到已经访问过的对象直接返回‘{"$ref":"$"}’。
方法一也有一个缺陷,假如多个对象的属性一模一样,转成String后信息不全。
public static void main(String[] args) throws Exception{
List<Student> students = new ArrayList<Student>();
Student student = new Student();
student.setId(111);
student.setName("张三");
students.add(student);
Student student2 = new Student();
student2.setId(111);
student2.setName("张三");
students.add(student2);
ObjectAnalyzer analyzer = new ObjectAnalyzer();
System.out.println("方式一:");
System.out.println(analyzer.objToString(students));
System.out.println("方式二:");
System.out.println(JSON.toJSONString(students));
}
结果如下:
方式一:
java.util.ArrayList[elementData=class java.lang.Object[]{com.bawy.study.jdk.string.Student[id=111,name=张三,teachers=java.util.ArrayList[elementData=class java.lang.Object[]{},size=0][modCount=0][][]][],...,null,null,null,null,null,null,null,null},size=2][modCount=2][][]
方式二:
[{"id":111,"name":"张三","teachers":[]},{"id":111,"name":"张三","teachers":[]}]
方式一中结合的第二个值变为了“...”,方式二是全的。因为使用了ArrayLsit自己的contains方法,该方法最后会调用对象本身的equals方法和集合中的所有对象进行比较,一般重写的equals方法就是比对所有属性是否相等,所以上述两个对象会被认为是同一个对象,导致二个对象不再访问。
if (visited.contains(obj)){
return "...";
}
建议判断的时候可以改成如下形式,直接使用“==”判断两个对象是否相等。
private boolean contain(List<Object> list, Object object){
for (Object obj:list){
if (obj==object){
return true;
}
}
return false;
}
当然对于这种互相引用的情况java也提供了关键字
transient
。使用该关键字修饰属性teachers。
private transient List<Teacher> teachers = new ArrayList<Teacher>();
再看三种情况结果:
方式一:
com.bawy.study.jdk.string.Student[id=111,name=张三,teachers=java.util.ArrayList[elementData=class java.lang.Object[]{com.bawy.study.jdk.string.Teacher[id=100,name=李四,students=java.util.ArrayList[elementData=class java.lang.Object[]{...,null,null,null,null,null,null,null,null,null},size=1][modCount=1][][]][],null,null,null,null,null,null,null,null,null},size=1][modCount=1][][]][]
方式二:
{"id":111,"name":"张三"}
方式三:
{"id":111,"name":"张三"}
可以看到使用关键字修饰后,三种方式均可以进行转换了。方式二和方式三中teachers属性直接没有了。方法一中也可以添加处理过滤被transient修饰的属性。transient的用途在于:阻止实例中那些用此关键字声明的变量持久化,所以加上该关键字后对应的属性序列化的时候会被忽略。
更多推荐
已为社区贡献1条内容
所有评论(0)