본문 바로가기
컴퓨터/전산, 그 외

자바의 garbage collector가 파일을 곧바로 닫지 않는 문제

by adnoctum 2011. 3. 21.


   자바의 garbage collector는 자원이 필요하지 않을 때 JVM에서 해제를 하는 것으로 보이며, 따라서 여전히 운영체제한테 제어권을 넘기지 않는 것으로 보인다. 이 경우, 파일을 열어서 특정 처리를 한 후 파일에 연결된 변수가 파괴되어도 그 파일의 제어권이 아직 JVM에 잡혀 있어서 다른 프로그램이 그 파일을 수정하지 못하는 문제가 발생할 수도 있다. 이런 경우를 방지하기 위해서는 꼭 close 나 finalize, terminate 와 같은 함수를 호출해 준다.

우선 이 글은 다소 추정에 의한 것임을 미리 밝힌다. 자바로 Hello, World! 찍은 다음 곧바로 테스트 해보는 것일만큼 나는 자바 코딩을 거의 처음 한 것이기 때문이다.

   문제의 발단은 Cytoscape 에서 엑셀로 된 node attribute 파일을 import 한 후 그 excel 파일을 수정한 후 저장하려 할 때 지속적으로 공유 위반(sharing violation)이 일어나는 것에서 시작했다. 이런 일이 꽤 잦았는데, 그 때마다 나는 운영체제나 이 노트북 자체의 문제로 생각해서 짜증만 내는 선에서 그쳤었는데, 어제 곰곰히 생각해 보니, 아마도 지역 변수를 사용하여 연 파일이 스택에서 삭제가 되었음에도 불구하고 아직 garbage collector에 의해 제대로 해제가 되지 않았기 때문이 아닐까, 하는 생각이 들었고, 그래서 테스트 해보고 검색을 해 본 결과 위 첫 단락과 같은 것을 알게 되었다.

   즉, 비록 지역 변수를 이용하여 파일을 열었음에도 불구하고, 스택이 파괴되면서 그 지역 변수가 사라졌을 때도 제어권은 여전히 운영체제로 넘어가지 않을 수 있는 것이다. 즉, 비록 스택에서 파괴되면서 JVM이 그 변수에 연결된 자원을 해제했을지라도 그것은 JVM 입장에서 그렇게 한 것이고, 아직 제어권이 OS로 넘어간 것은 아니다. 그래서 그러한 경우, JVM 위에서 다시 그 파일을 열어서 수정할 수는 있겠지만(왜냐 하면 JVM 위에선 제대로 해제가 된 것이니까), 아예 별도의 다른 프로그램(JVM에 의존하지 않고 돌아가는)에서는 그 파일을 제대로 조절할 수 없게 되는 것이다. 다음과 같은 자바 코드를 보자.

    private void jButton1MouseClicked(java.awt.event.MouseEvent evt) {
        // TODO add your handling code here:
        String curdir = System.getProperty("user.dir");
        JOptionPane.showMessageDialog(mainPanel, curdir);
        FileInputStream in = null;
        FileOutputStream out = null;
        try{
            in = new FileInputStream("aa.xlsx");
            out = new FileOutputStream("copied_aa_by_java.xlsx");
            int c = 0;
            while((c = in.read()) != -1){
                out.write(c);
               
            }
        }
        catch (IOException e){
            if(in != null){
                try {
                    in.close();
                } catch (IOException ex) {
                    //Logger.getLogger(DesktopApplication1View.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            if(out != null){
                try {
                    out.close();
                } catch (IOException ex) {
                   // Logger.getLogger(DesktopApplication1View.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
       }
        /*
        if(in != null){
            try {
                in.close();
            } catch (IOException ex) {
                //Logger.getLogger(DesktopApplication1View.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        if(out != null){
            try {
                out.close();
            } catch (IOException ex) {
               // Logger.getLogger(DesktopApplication1View.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        */       
    }


위 코드는 버튼의 click event에 연결한 handler이다. 매우 안정적인 것처럼 보인다. 그러나 문제가 있는데, 만약 try 구문에서 예외가 걸리지 않으면 catch 블럭이 실행이 되지 않고, 따라서 각 파일에 대한 close method가 호출되지 않는다. 만약 C++의 경우였다면 어차피 함수가 끝나면서 변수가 사라지기 때문에 close가 호출될테고, 따라서 저 함수가 실행된 이후에 다른 프로그램으로 위에서 사용된 파일(aa.xlsx)을 열고 수정하여 저장할 수 있다. 테스트를 위하여 다음과 같은 코드를 이용했다.


위의 자바 코드와 비교했을 때, 지역적으로 사용된 파일이 함수가 호출된 이후 사라진다는 점에 있어서는 완전히 똑같다. 그러나 c++의 경우엔 aa.xlsx 파일을 당연히 수정하여 저장할 수 있지만 자바 코드는 다음과 같은 접근 거부가 일어 난다.


위에서 표시된 sharing violoation은 결국 이런 의미일 것이다. 일반적으로 파일을 열 때 사용하는 공유 방법은 읽기만 공유, 쓰기만 공유, 읽기/쓰기 공유, 삭제 공유[각주:1], 공유하지 않음, 이 있다. 즉, 한 프로그램이 공유하지 않는 모드로 파일을 열면 다른 프로그램이 그 파일을 읽거나 쓰기 위해 열 수 없다. 만약 읽기 공유 모드로 파일을 열었다면 그 파일을 읽기만 하는 프로그램은 동시에 그 파일을 열어서 읽을 수 있다. 그런데 위의 경우, 자바의 FileInputStream 은 아마도 읽기 공유 모드로 파일을 열테고, 그렇게 열린 파일을 엑셀에서 수정하여 '저장'하려 했으니 엑셀은 쓰기 공유까지 된 경우에만 위 작업을 마칠 수 있었겠지만 현재는 읽기만 공유되어 있었으므로 sharing violation 이 일어나는 것으로 추정된다.

   반면 위의 자바 코드에서 주석으로 처리한 부분을 살리면 예외에 상관없이 무조건 파일을 close 하게 되는데, 그렇게 되면 위와 같은 공유 위반 문제가 발생하지 않는다. 아마도 JVM이 close method 가 호출되면 곧바로 제어권을 운영체제로 넘기는 것으로 보인다. 물론 이런 일은 c++과 같은 경우 stack에서 파괴되면서 저절로 일어나는 일일테지만 자바의 경우는 보다 명시적으로 이런 작업을 해 주어야 한다.

   결과적으로 자바를 사용할 때 close나 terminate 등과 같은 method가 있을 경우 그러한 것을 명시적으로 호출해 주는 것이 좋아 보인다. 무조건 garbage collector만 믿을 수는 없겠지.


ps. 내가 여전히 자바보다는 C++에 머무는 이유는, STL과 바로 이 garbage collector 때문이다. 다 쓴 메모리는 그때그때 곧바로 해제해 주고 싶거든. 난 내가 직접 해제하는 것을 선호해서 smart pointer 같은 것도 사용하지 않는다.


  1. 좀 불명확한데, 한 프로그램이 파일을 열고 있는 상태에서 다른 프로그램이 그 파일을 지울 수 있다는 것을 의미한다. 리눅스에서 vi 가 그렇게 파일을 여는 것으로 보인다. vi 로 파일을 열어 놓은 상태에서 그 파일을 지울 수 있다. [본문으로]