1. 程式人生 > >物件轉String過程中出現java.lang.StackOverflowError堆疊溢位錯誤的分析

物件轉String過程中出現java.lang.StackOverflowError堆疊溢位錯誤的分析

最近在做專案過程中多次遇到該問題,所以整理一下做個筆記。

該錯誤出現的原因一般都是因為不停的迴圈遞迴呼叫。
1、虛擬機器棧是什麼?
棧也叫棧記憶體,是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的用途在於:阻止例項中那些用此關鍵字宣告的變數持久化,所以加上該關鍵字後對應的屬性序列化的時候會被忽略。