시작하며..

자바언어에서는 RandomAccessFile 을 이용해서, native heap 영역의 메모리를 얻어올 수 있다. jvm 내의 heap 영역이 아니기 때문에 따로 잡은 메모리 공간을 이용한다. 이를 통해서 자바는 속도를 향상시켜 read/write를 빨리 할 수 있다. java 1.6 update 22 내부 소스를 바탕으로 어떤 구조로 되어 있는지 살펴본다.

 

본론

간단하게 RandomAccessFile 클래스를 이용해서 Channel을 얻어온 후 READ only용으로 메모리 mapping 하는 메서드를 이용해서 MappedByteBuffer 클래스 타입의 인스턴스를 리턴하는 메서드이다.


 

private static MappedByteBuffer mmap(String filename, long start, int size) throws IOException
{
    RandomAccessFile raf;
    try
    {
        raf = new RandomAccessFile(filename, "r");
    }
    catch (FileNotFoundException e)
    {
        throw new IOError(e);
    }

    try
    {
        return raf.getChannel().map(FileChannel.MapMode.READ_ONLY, start, size);
    }
    finally
    {
        raf.close();
    }
}

 


RandomAccess의 getChannel() 메서드는 sun.nio.ch.FileChannelImpl 클래스를 리턴한다.  FileChannelImpl 클래스의 원형은 다음과 같다.

class FileChannelImpl  {

public MappedByteBuffer map(MapMode mode, long position, long size)
    throws IOException {


addr = map0(imode, mapPosition, mapSize);

Unmapper um = new Unmapper(addr, size + pagePosition); // 큰 의미없는 data structure
return Util.newMappedByteBufferr(isize, addr + pagePosition, um);

}

}


 

FileChannelImpl 클래스의 map 메서드에서는 map0() 메서드를 호출하여 주소 번지를 얻어오고, ByteBuffer로 만들어 리턴한다.

map0() 메서드의 원형은 native 로 연결되어 있다.

// Creates a new mapping
private native long map0(int prot, long position, long length)
    throws IOException;

 

이 메서드는 jni 코드인 FileChannelImpl.c 파일로 연결되어 있다. 역시 mmap64 메서드를 사용하고 그 주소값을 리턴하고 있다. 디폴트는 MAP_SHARED이다.

 


JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
                     jint prot, jlong off, jlong len)
{
    void *mapAddress = 0;
    jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
    jint fd = fdval(env, fdo);
    int protections = 0;
    int flags = 0;

    if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
        protections = PROT_READ;
        flags = MAP_SHARED;
    } else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {
        protections = PROT_WRITE | PROT_READ;
        flags = MAP_SHARED;
    } else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
        protections =  PROT_WRITE | PROT_READ;
        flags = MAP_PRIVATE;
    }

    mapAddress = mmap64(
        0,                    /* Let OS decide location */
        len,                  /* Number of bytes to map */
        protections,          /* File permissions */
        flags,                /* Changes are shared */
        fd,                   /* File descriptor of mapped file */
        off);                 /* Offset into file */

    if (mapAddress == MAP_FAILED) {
    if (errno == ENOMEM) {
        JNU_ThrowOutOfMemoryError(env, "Map failed");
        return IOS_THROWN;
    }
    return handle(env, -1, "Map failed");
    }

    return ((jlong) (unsigned long) mapAddress); 
}

 


 

참고로..

다른 함수를 살펴보면, postion0 () 메서드에서는 lseek64를, size0() 메서드에서는 fstat64를, close0() 메서드에서는 close를, release에서는 fcntl를, forces0 메서드에서는 fsync를, unmap0 메서드는 munmap를 쓰는등 파일 관련된 시스템 콜을 사용하고 있다. 

특별히 transfer0은 sendfile 또는 sendfile64 (동적 라이브러리를 이용한 function pointer의 형태)를 사용한다. sendfile64 구현을 안한 리눅스가 있나 보다…

 

주소번지를 받고 나서 메모리 매핑을 하는Util.newMappedByteBufferr(isize, addr + pagePosition, um) 메서드를 살펴본다. Reflection의 Constructor인 directByteBufferConstructor를 이용해서 MappedByteBuffer 클래스를 생성한다.

static MappedByteBuffer newMappedByteBuffer(int size, long addr, Runnable unmapper)


MappedByteBuffer dbb;
if (directByteBufferConstructor == null)
     initDBBConstructor();

dbb = (MappedByteBuffer)directByteBufferConstructor.newInstance(
new Object[] { new Integer(size), new Long(addr), unmapper });

}

private static void initDBBRConstructor() {

Class th = Class.forName("java.nio.DirectByteBufferR"); // java api에는 나오지 않는 녀석
directByteBufferRConstructor = th.getDeclaredConstructor(
                    new Class[] { int.class, long.class,
                              Runnable.class });
directByteBufferRConstructor.setAccessible(true);

}

 



설명도 잘 나와 있다.

class DirectByteBuffer extends DirectByteBuffer implements DirectBuffer  {
..

// For memory-mapped buffers -- invoked by FileChannelImpl via reflection
    //
    protected DirectByteBufferR(int cap, long addr, FileDescriptor fd, Runnable unmapper)   {
        super(cap, addr, fd, unmapper);
   }


// For memory-mapped buffers -- invoked by FileChannelImpl via reflection
//
protected DirectByteBuffer(int cap, long addr, FileDescriptor fd, Runnable unmapper){
    super(-1, 0, cap, cap, fd);
    address = addr;
    viewedBuffer = null;
    cleaner = Cleaner.create(this, unmapper);

}
..
}


 

DirectByteBufferR 클래스는 래퍼류로서 DirectByteBuffer를 상속받았기 때문에 생성자 레벨에서 편하게 가공할 수 있다. DirectByteBuffer클래스는 MappedByteBuffer를 상속받은 클래스이기 때문에 MappedByteBuffer Casting 이 가능하다.

 

참고로.. Cleaner 클래스는 PhantomReference 를 상속해서 쓰고 있다. GC를 편리하게 하기 위함으로 추가할 때마다 내부적으로 linked list로 객체를 만들어 정리될 때 Runnable로 들어온 객체의 run() 메서드를 호출한다.

public class sun.misc.Cleaner extends java.lang.ref.PhantomReference ..

 

private static class Unmapper
    implements Runnable
{

    private long address;
    private long size;

    private Unmapper(long address, long size) {
        assert (address != 0);
        this.address = address;
        this.size = size;
    }

    public void run() { // 메모리가 부족할 때 종료되게 함.. PhantomReference 임.
        if (address == 0)
            return;
        unmap0(address, size);
        address = 0;
    }

}



unmap0 메서드를 호출하여 항상 gc가 되게 한다.

 

 

마치며..

자바에서 nio map 사용시 리눅스 mmap 시스템 콜에 의해서 매핑된 데이터를 처리 하는 로직을 살펴보았다. 자연스럽게 생명주기도 살펴보았다.

다음에는 Cassandra의 Linux swap 이슈에 대해서 살펴봐야지..  mmap이슈가 있었던 내용을 쓰면서 정리하고자 한다.

Posted by 김용환 '김용환'
TAG MAP, 자바

댓글을 달아 주세요

 

아두이노 개발 툴 1.0에 대한 소스 분석 글이다.

http://knight76.tistory.com/entry/아두이노-개발-툴-소스-10-분석을-위한-환경-셋팅 에 이어 소스 분석을 시작한다.  프로세싱 개발 툴과 너무 흡사한 UI를 가졌기 때문에 소스 분석이 조금 더 쉬운 것 같다.

먼저 빌드 순서를 파악하고, 소스를 파악한다. (짜잘한 것은 제외) 그리고 주석이 약간 틀린 부분이 있었다.ㅠ

1. 빌드 순서


(1) build/build.xml

운영체제(플랫폼)에 맞는 빌드 콜을 호출한다. 윈도우의 경우 windows-build target이 가장 먼저 실행이 된다.

    1) todo.txt 파일의 revistion 숫자, processing/app/Base.java 파일의 revision 정보와 비교해서 revision 번호를 맞춘다.

    2) 윈도우 운영체제인지 확인한다.

    3) core 디렉토리와 app 디렉토리에 ant 컴파일을 시킨다.

       -  core/build.xml
           core 디렉토리를 컴파일하고 bin/core.jar 파일을 만든다.

       -  app/build.xml
           app 디렉토리를 컴파일하고, bin/pde.jar 파일을 만든다.

4) windows/work 디렉토리를 생성 후, 실행에 필요한 libraray (jar, dll)과 driver 파일들을 복사한다. 그리고, window/work/hardware 디렉토리에 윈도우에서 동작할 수 있는 avr 툴(avr 체인) 압축 파일을 풀어둔다.

5) windows/work 디렉토리의 여러 디렉토리에 셋팅을 한다.

    1) windows/work/tools 디렉토리에 Mangler.java 소스를 복사한다.

    2) window/work/library 디렉토리에 아두이노 library 파일을 복사한다.

    3) window/work/hardware 디렉토리에  아두이노 부트로더, 펌웨어등을 복사한다.

    3) window/work/example 디렉토리에는 간단히 예제들을 복사해둔다.

6) launch관련 파일들을 모은다. launch4j와 설정파일, 관련 클래스들을 모아둔다.

7) launch4j를 실행하고, 컴파일하고 링킁하고 실행파일(build\windows\work\arduino.exe)을 생성한다.

launch4j를 이용하여 exe 파일을 만드는 config.xml 파일이다.

<launch4jConfig>
  <dontWrapJar>true</dontWrapJar>
  <headerType>gui</headerType>
  <jar>lib</jar>
  <outfile>arduino.exe</outfile>
  <errTitle></errTitle>
  <cmdLine></cmdLine>
  <chdir>.</chdir>
  <priority>normal</priority>
  <downloadUrl>http://java.sun.com/javase/downloads/</downloadUrl>
  <supportUrl></supportUrl>
  <customProcName>false</customProcName>
  <stayAlive>false</stayAlive>
  <manifest></manifest>
  <icon>application.ico</icon>
  <classPath>
   <mainClass>processing.app.Base</mainClass>
    <cp>lib/pde.jar</cp>
    <cp>lib/core.jar</cp>
    <cp>lib/jna.jar</cp>
    <cp>lib/ecj.jar</cp>
    <cp>lib/RXTXcomm.jar</cp>

  </classPath>
  <jre>
    <path>java</path>
    <minVersion>1.5.0</minVersion>
    <maxVersion></maxVersion>
    <jdkPreference>preferJre</jdkPreference>
    <opt>-Xms128m -Xmx128m</opt>
  </jre>
  <splash>
    <file>about.bmp</file>
    <waitForWindow>true</waitForWindow>
    <timeout>60</timeout>
    <timeoutErr>true</timeoutErr>

  </splash>
  <messages>
    <startupErr>An error occurred while starting the application.</startupErr>
    <bundledJreErr>This application was configured to use a bundled Java Runtime Environment but the runtime is missing or corrupted.</bundledJreErr>
    <jreVersionErr>This application requires at least Java Development Kit</jreVersionErr>
    <launcherErr>The registry refers to a nonexistent Java Development Kit  installation or the runtime is corrupted.</launcherErr>
    <instanceAlreadyExistsMsg>An application instance is already running.</instanceAlreadyExistsMsg>
  </messages>
</launch4jConfig>

 

2. UI (Editor)

processing.app.Base.java 소스에 main 메서드를 따라가 본다.

OS(플랫폼)을 확인하고, preferences.txt 프로퍼티 화일(크기..)을 읽는다. theme/theme.txt 프로퍼티 설정을 읽어 editor에 대한 프로퍼티를 확인한다. look&feel 을 설정후, 임시폴더를 만들고 Base 객체를 생성한다.

Base 생성자 코드를 따라가면, examples, libraries, tools 디렉토리 패스와 프로젝트 파일로 저장할 path (sketchbook path) 를 지정해둔다. hardware 디렉토리를 잘 기억한다. Editor 클래스를 초기화하여 UI를 실행한다. 이전에 아두이노 개발 툴을 종료했을 때 사용했던 스케치 파일이 있었으면 툴 실행시 스케치 파일을 열어둔다.

UI에 관련된 대부분의 특징은 프로세싱 개발 툴과 흡사하다. processing.app 패키지 밑에 잘 들어가 있다.

운영체제에 맞는 특징에 대해서는 운영체제별로 Platform.java 소스에 dependant한 코드들이 들어가 있다. 재미있는 것은 아두이노 오픈 소스 개발자가 macosx 쪽에 Think Different 클래스를 추가했다. 오픈소스의 센스라고 해야 하나 ㅎㅎ 먼가 대단하다고 생각한 코드는 아니지만 웃음코드에 박수~

public class Platform extends processing.app.Platform {

….

public void init(Base base) {
  System.setProperty("apple.laf.useScreenMenuBar", "true");
ThinkDifferent.init(base);
  ..

}

 

public class ThinkDifferent implements ApplicationListener {

… OS X application 이벤트 (open, about 등등)에 대해서 처리하려고 만들었다.

}

아두이노 개발 툴을 UI 초기화하고 menu를 생성하고 editor를 만들면서.. serial port 정보와 타겟 보드가 나와있는데, 이는 processing.app.EditorLineStatus.java 클래스에서 설정 파일을 읽어오는 코드로 동작하게 되어 있다.

맨 처음 읽을 때는 preference.txt 파일에 있는 설정 정보를 읽는다. 이전에 Serial 포트 변경이 있을 때마다 설정값을 변경시키도록 되어 있따.

# ARDUINO PREFERENCES
board = uno
target = arduino

serial.port=COM1

 

3. 아두이노 개발 툴의 스케치 파일 읽고 쓰기

파일을 읽는 시퀀스는 그냥 평범하다.

EditorHeader.rebuildMenu() –> SkechCode.load() –> Base.loadFile() –> PApplet.loadStrings() –>PApplet.createInput() 로 이어져 있다. 파일을 line을 seperator로 String[](내부적으로는 BufferedReader(FileInputStream)클래스를 사용)타입으로 간단하게 파일을 일고, PApplet.join() 메서드에서 String[]에 line separator를 넣어서 String 객체로 리턴한다.

파일을 쓰는 구조는 추측할 수 있도록 파일을 읽는 반대로 구성되어 있다.

 

4. Sketch 파일

아두이노 개발 툴에서 저장하는 파일들을 스케치(Sketch) 라고 한다. 무엇인가를 그리고 거기에 맞게 그린다는 차원에서는 좋아 보이는 개념인 것 같다. processing.app.Sketch.java 클래스는 pde, java 확장자를 지원하고 cpp를 지원하지 않는다. 또한 여러 버그로 인한 많은 땜질 코드들이 보인다. 

processing.app.SketchCode.java 클래스는 코드 자체를 의미하는 클래스로서, 코드가 읽고, 쓰고, 변경될 때 사용되는 클래스이다.

 

 

5. Compile

빌드 Call stack은 다음과 같다.

에디터에서 빌드메뉴를 선택하면, Editor.buildSketchMenu() –>Editor.handleRun()->Editor.presentHandler()
->Thread인 headerListFromIncludePath.run() –> Sketch.build() 로 진행한다.

 

   (1) 전처리 (preprocessor)

컴파일을 하기 전에 전처리(preprocess) 과정을 거친다. Sketch.preprocess() , PdePreprocessor.preprocess()을 참조한다. 소스에 맨 끝에는 \n과 멀티 라인으로 된 주석은 정리되도록 했다. OOME 나 NPE가 발생한 적이 있는 듯 하다. unicode 설정에 되어 있으면 소스를 unicode로 바꾼다.

pde 나 ino 확장자는 가진 파일들은 include 헤더 파일 스트링을 찾아서 저장해 둔다. 소스에서 컴파일과 관련없이 주석, 전처리 지시자, 싱글/더블 따옴표를 모두 스페이스로 만들어버리고 함수 이외 지역에 사용했던 top level brace를 뺀다.  include header 와 정리된 prototype의 line수를 저장해서 나중에 소스를 만들 때 이용한다.

C++ 클래스를 하나 만들어 pde/ino파일에서 사용했던 include 헤더 파일과 #include “Arduino.h”를 추가하고, 잘 정리한 prototype을 넣은 후 cpp 파일로 만들고 컴파일 한다.

 

  (2) 컴파일

Compiler.compile() 메서드에서 정의하고 있는 컴파일러는 avr-gcc (호스트환경은 윈도우이지만, 타겟보드에서 실행할 수 있도록 cross 컴파일을 지원하는 컴파일러) 를 사용하고 있다.  avr base path는 hardware/tools/avr/bin/ 디렉토리를 기준으로 한다.

보드(아두이노 우노)에 맞는 core 라이브러리들이 참조될 수 있도록 라이브러리(hardware\arduino\cores\arduino 디렉토리 밑에 있는 h, cpp 파일들) 들을 잘 정리해둔다.  sketch 파일을 찾아 리스트에 추가하고 컴파일한다.   variants도 필요하면 path에 추가해준다.

    1) 스케치 파일 컴파일

    --- path 디렉토리에 s 확장자로 끝나는 파일을 컴파일

command 창에서 동작할 수 있도록 명령어를 조합하고 object 파일을 만들기 위해서 gcc 컴파일을 실행한다.

    avr-gcc –c –g -assembler-with-cpp -mmcu={$mcu} -DF_CPU={$cpu} -DARDUINO={$number}  –I{IncludePath} {$source file} –o {$object name}

    --- path 디렉토리에 c 확장자로 끝나는 파일을 컴파일

avr-gcc –c –g –Os –Wall –ffunction-section –fdata-section -mmcu={$mcu} -DF_CPU={$cpu} –MMD -DARDUINO={$number}  –I{IncludePath} {$source file} –o {$object name}

    --- path 디렉토리에 cpp 확장자로 끝나는 파일을 컴파일

avr-gcc –c –g –Os –Wall -fno-exceptions –ffunction-section –fdata-section -mmcu={$mcu} -DF_CPU={$cpu} –MMD -DARDUINO={$number}  –I{IncludePath} {$source file} –o {$object name}

 

     2) 라이브러리 컴파일

아두이노 개발 툴에서 짠 소스에서의 include 헤더에 대한 정보들을 모아 관련 디렉토리에서 컴파일한다. 컴파일 하는 방법은 s, c, cpp 확장자로 끝나는 파일로 위의 컴파일 방법과 같다.

    3) 코어 파일 컴파일

cores 디렉토리 밑에 있는 파일들을 찾아 컴파일하고 core.a 파일로 만든다.

avr-ar rcs core.a  파일들

     4) 링킹

avr-gcc –Os -Wl,--gc-sections -mmcu=atmega328p –o  {$primaryclass}.elf  {$objectfile} core.a –L{$buildpath} –lm

     5) EEPROM data 를 만들기

avr-objcopy –O –R  ihex  -j .eeprom --set-section-flags=.eeprom=alloc,load  --no-change-warnings  --change-section-lma .eeprom=0 {$primaryclass}.elf  {$primaryclass}.eep

     6) .hex 파일 만들기

avr-objcopy –O –R  ihex  .eeprom {$primaryclass}.elf  {$primaryclass}.eep

 

 

(3)  보드에 이미지 업로드

EEPROM data를 잘 구워 아두이노 보드에 넣는(upload) 작업을 할 때, processing.app.Upload 클래스, AvrdudeUploader 클래스를 사용한다. 부트 로더를 굽거나 (burn bootlaoder) 할 떄도 사용된다.

Sketch.upload() –> AvrdudeUploader.uploadUsingPreferences() –>AvrdudeUploader.avrdude() 를 호출하는 구조로 되어 있으며, avrdude 라는 명령어를 이용해서 플래쉬에 hex 파일을 굽는다. (-Uflash:w:파일.hex:i’ )

 아두이노의 부트로더를 굽는 방법은 avrdude 명령어에 -Uefuse:w:~~~ 옵션을 넣는다. 

통신방법은 여러가지가 있다. 아마도 운영체제 때문으로 생각된다. 만약 -Pusb 파라미터를 주어서 usb로 통신할 수 있으며, -P {serial port 번호 } 파라미터를 주어서 serial port로 통신할수도 있다.

-b 파라미터로 굽는 속도를 조절할 수 있다. 
 


그리고, Serial 객체를 생성해서 통신하도록 되어 있다.
Serial 클래스는 내부적으로 gnu.io 패키지의 포트 정보를 확인하는 CommPortIdentifier 클래스를 사용하고 있으며, MessageConsumer에 대한 리스너 등록, 이벤트 발생, 데이터를 읽는 형태를 가지고 있다.

 

(4) 윈도우 사용시 레지스트리 이용 - jna

처음 시작할 때, 윈도우에서 ino, pde 파일을 더블 클릭했을 때 아두이노.exe 파일을 실행시키기 위해서, 윈도우 레지스트리에 추가된다. 관련 파일은 Platform.checkAssociations(), Registry 클래스, Advapi32 클래스이다. 여기서도 processing처럼 jna를 아주 잘 쓰고 있다.

public interface Advapi32  extends StdCallLibrary  {

Advapi32 INSTANCE = (Advapi32) Native.loadLibrary("Advapi32", Advapi32.class, Options.UNICODE_OPTIONS);
..
}

 

이상 끝.. 자바로 만들어져서 소스가 어렵지 않다.


참고...
아두이노 개발 툴 없이 소스와 툴만 가지고 컴파일할 때 사용하는 방법은 다음과 같다. 소스에서 사용하는 컴파일 방법과 동일하다.

http://andrew-j-norman.blogspot.com/2011/12/bypassing-ide.html

Posted by 김용환 '김용환'

댓글을 달아 주세요

JAXB 잘 사용하기

general java 2011. 12. 21. 16:01

2010년 초에 정리한 줄 알았는데, 과거 메일을 참조로 정리한다.
이 내용은 현재 JAXB 버전과 다를 수 있다.


JAXB에 대해서..
java XML 파서중에 Xstream, JAXB 가 있다. 취향에 따라서 다양하게 쓰기는 하는데..
아직 까지는 잘만 쓴다면 JAXB가 성능이 조금 좋은 것 같았다. JAXB 에 대한 이슈와 오해를 정리하고 어떻게 해야 잘 쓸 수 있는 것인지 확인한다.


JAXB의 초기화
XML 파서인 JAXB는 DOM을 사용해서 상당히 느렸는데, 최근에는 SAX와 STAX를 사용하여 성능이 좋아지고 있으나, 주의해야 하는 것은 JAXBContext를 매번 생성하지 않고 한번 생성된 것을 재사용하는 구조로 써야 한다. QName의 String.intern을 많이 호출하는 성능 상 이슈와 GC상 perm 영역에서 minor GC가 많이 일어난다. 만약 Perm gen 영역을 작게 했을 때는 OOME가 perm gen에서 일어날 수도 있다.

많은 개발자들이 JAXB를 쓸 때 항상 조심해야 하는 것으로 한번만 JAXBContext 객체를 하라는 내용이 많으나 그 중의 hudson 개발자인 가와구치도 Hudson commitor들에게 얘기한 내용이 있다.

http://markmail.org/message/hy2tmioxev7skqkl#query:+page:1+mid:hy2tmioxev7skqkl+state:results

TIP 1. Never create the same JAXBContext more than once

-------------------------------------------------------
Always, always assign JAXBContext to a static final field. A JAXBContext object can be shared safely by multiple threads. This is a very expensive to create, so create it once, and share it.





JAXB의 Thread Safe

JAXBContext 가 Thread safe하지 않다고 알고 있는 사람이 있기도 하다. Thread safe 하다.다만..
마셜러와 언마셜러와 밸리데이터는 thread safe하지 않기 때문에 잘 써야 한다.  JAXBContext 를 단독으로 쓸 때 잘 유의할 필요는 있다. 그러나 Spring과 연계되어서 쓸 때는 내부적으로 잘 thread safe하게 해서 편하게 쓸 수 있다.

http://jaxb.java.net/guide/Performance_and_thread_safety.html


8.1. Performance and thread-safety
The JAXBContext class is thread safe, but the Marshaller, Unmarshaller, and Validator classes are not thread safe.

For example, suppose you have a multi-thread server application that processes incoming XML documents by JAXB. In this case, for the best performance you should have just one instance of JAXBContext in your whole application like this:

Singleton JAXBContext
class MyServlet extends HttpServlet {   
    static final JAXBContext context = initContext();   
    private static JAXBContext initContext() {       
        return JAXBContext.newInstance(Foo.class,Bar.class);   
    }
}

And each time you need to unmarshal/marshal/validate a document. Just create a new Unmarshaller/Marshaller/Validator from this context, like this:

Thread local Unmarshaller
    public void doGet( HttpServletRequest req, HttpServletResponse ) {       
         Unmarshaller u = context.createUnmarshaller();       
          u.unmarshal(...);   
    }

This is the simplest safe way to use the JAXB RI from multi-threaded applications.

If you really care about the performance, and/or your application is going to read a lot of small documents, then creating Unmarshaller could be relatively an expensive operation. In that case, consider pooling Unmarshaller objects. Different threads may reuse one Unmarshaller instance, as long as you don't use one instance from two threads at the same time.


 




JAXB의 Accessor
jaxb-api, jaxb-impl 2.0 library을 기준으로 사용했고, 다음의 2가지 기준이 맞아야 Permgen영역에서 OOME 가 발생한다.,

JAXB의 plain object는 다음과 같이 사용할 수 있다.




@XmlRootElement(name = "message")
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlType(propOrder = {"to", "from", "body"})
public class Message2 {
 private String to;

 private String from;

 private String body;
 
 @XmlElement(name = "TO")
 public String getTo() {
  return to;
 }
 public void setTo(String to) {
  this.to = to;
 }
 
 @XmlElement(name = "from")
 public String getFrom() {
  return from;
 }
 
 public void setFrom(String from) {
  this.from = from;
 }

 
 @XmlElement(name = "body")
 public String getBody() {
  return body;
 }
 
 public void setBody(String body) {
  this.body = body;
 }

}


 





원인을 잘 살펴본다.



Accessor의 GetterSetterReflection 인스턴스의 optimize가 호출되면, property에 대해서 getter와 setter 가 있는지 확인합니다.


         @Override
        public Accessor<BeanT,ValueT> optimize(JAXBContextImpl context) {
            if(getter==null || setter==null)
                // if we aren't complete, OptimizedAccessor won't always work
                return this;
            if(context!=null && context.fastBoot)
                // let's not waste time on doing this for the sake of faster boot.
                return this;

            Accessor<BeanT,ValueT> acc = OptimizedAccessorFactory.get(getter,setter);
            if(acc!=null)
                return acc;
            else
                return this;
        }
 


 

만약 getter/setter 메서드 중 하나라도 존재하지 않으면, 그냥 JAXB 내부의 Optimize 작업을 하지 않는다. 클래스 로딩 작업이 없게 된다. 만약 setter, getter가 존재하면, 내부 Optimize작업을 하는데,  Optimize 작업이 바로 내부 클래스를 직접 생성한다.

 OptimizedAccessorFactory 클래스 내부 소스를 보면, 선언된 클래스이름에 JaxbAccessorM_getter이름_setter이름_property타입의 클래스를 생성한다.

 String newClassName = toVMClassName(getter.getDeclaringClass())+"$JaxbAccessorM_"+getter.getName()+'_'+setter.getName()+'_'+typeName;

 

* 참고 : Accessor에 대한 개념 파악에 도움되는 글

Improving field get and set performance with ASM or Javassist

http://stackoverflow.com/questions/3022741/improving-field-get-and-set-performance-with-asm-or-javassist


public class AccessorGenerator { 
 
   
private final ClassPool pool; 
 
   
public PropertyGenerator() { 
        pool
= new ClassPool(); 
        pool
.appendSystemPath(); 
   
} 
 
   
public Map<String, PropertyAccessor> createAccessors(Class<?> klazz) throws Exception { 
       
Field[] fields = klazz.getDeclaredFields(); 
 
       
Map<String, PropertyAccessor> temp = new HashMap<String, PropertyAccessor>(); 
       
for (Field field : fields) { 
           
PropertyAccessor accessor = createAccessor(klazz, field); 
            temp
.put(field.getName(), accessor); 
       
} 
 
       
return Collections.unmodifiableMap(temp); 
   
} 
 
   
private PropertyAccessor createAccessor(Class<?> klazz, Field field) throws Exception { 
       
final String classTemplate = "%s_%s_accessor"; 
       
final String getTemplate = "public Object get(Object source) { return ((%s)source).%s; }"; 
       
final String setTemplate = "public void set(Object dest, Object value) { return ((%s)dest).%s = (%s) value; }"; 
 
       
final String getMethod = String.format(getTemplate,  
                                               klazz
.getName(), 
                                               field
.getName()); 
       
final String setMethod = String.format(setTemplate,  
                                               klazz
.getName(),  
                                               field
.getName(),  
                                               field
.getType().getName()); 
 
       
final String className = String.format(classTemplate, klazz.getName(), field.getName()); 
 
       
CtClass ctClass = pool.makeClass(className); 
        ctClass
.addMethod(CtNewMethod.make(getMethod, ctClass)); 
        ctClass
.addMethod(CtNewMethod.make(setMethod, ctClass)); 
        ctClass
.setInterfaces(new CtClass[] { pool.get(PropertyAccessor.class.getName()) }); 
       
Class<?> generated = ctClass.toClass(); 
       
return (PropertyAccessor) generated.newInstance(); 
   
} 
 
   
public static void main(String[] args) throws Exception { 
       
AccessorGenerator generator = new AccessorGenerator(); 
 
       
Map<String, PropertyAccessor> accessorsByName = generator.createAccessors(PurchaseOrder.class); 
 
       
PurchaseOrder purchaseOrder = new PurchaseOrder("foo", new Customer()); 
 
        accessorsByName
.get("name").set(purchaseOrder, "bar"); 
       
String name = (String) accessorsByName.get("name").get(purchaseOrder); 
       
System.out.println(name); 
   
} 
} 


jaxb가 reflection을 사용하면 속도가 너무 느리기 때문에 Accessor 클래스를 만들어서 속도를 향상시키고 private 멤버도 쉽게 접근하는 구조를 가지고 있다. 따라서 메소드를 확인하는 작업이 줄어들어서 속도를 향상시키는 패턴을 가지고 있다.로그를 보면 구체적으로 알 수 있다.


Plain Object인  Message2 를 만들고 JAXBContext로 클래스를 매번 생성하게 하였다.

@XmlRootElement(name = "message")
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlType(propOrder = {"to", "from", "body"})
public class Message2 {
 private String to;

 private String from;

 private String body;
 
 @XmlElement(name = "x")

 public String getTo() {
  return to;
 }
 public void setTo(String to) {
  this.to = to;
 }

 




jvm 출력 로그는 다음과 같다. AccessroM_ 클래스가 생성되었음을 확인할 수 있다.

[Loaded com.google.spring.bean.Message2$JaxbAccessorM_getBody_setBody_java_lang_String from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorM_getBody_setBody_java_lang_String from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorM_getFrom_setFrom_java_lang_String from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorM_getFrom_setFrom_java_lang_String from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorM_getTo_setTo_java_lang_String from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorM_getTo_setTo_java_lang_String from __JVM_DefineClass__]


 

만약 getter/setter 메서드를 없앤 클래스를 테스트해보았다.

@XmlRootElement(name = "message")
@XmlAccessorType(XmlAccessType.FIELD)
public class Message2 {
 public String to;

 public String from;

 public String body;
}




출력되는 jvm 로그는 다음처럼 Accessor가 생기는 것을 볼 수 있다.

Loaded com.google.spring.bean.Message2$JaxbAccessorF_to from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorF_to from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorF_from from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorF_from from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorF_body from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorF_body from __JVM_DefineClass__]

 
JAXB OOME 테스트

매번 JAXBContext를 생성하면 과연 OOME가 일어날까?
jvm 의 perm gen 영역과 heap 영역을 작게 했다.

-XX:PermSize=2500k
-XX:MaxPermSize=2500k
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+TraceClassUnloading
-XX:+TraceClassLoading
-Xms10m
-Xmx10m





모니터링시
생성된 자바 loaded class 가 계속 상승하여 jvm에 부하를 주지만, heap, perm gen 영역은 그리 메모리가 크게 늘어나지 않았다.



아마도 OOME가 나는 이유는 web에서 session 단위로 hashmap으로 jsp로 전달되는 과정에서 (서블릿 스펙상 session은 최소 1초로 사용) 일어난 것으로 생각된다. 일반 Application에서는 큰 영향을 없을 수 있다.


마치며..
JAXB는 성능이 좋아 사람들에게 많이 쓰이고 있다. 매번 클래스를 Reflection 해서 정보를 얻어오기 보다  Accessor클래스를 생성하고 있다. 만약 JAXBContext 를 매번 호출하면 Accessor 클래스 로딩이 호출시마다 일어나 jvm, gc에 부하를 줄 수 있다. JAXBContext는 한번만 초기화하고 객체를 재사용할 필요가 있다.


Posted by 김용환 '김용환'
TAG JAXB, 자바

댓글을 달아 주세요

  1. 2017.12.22 10:10  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

 

첫번째 정리 : http://knight76.tistory.com/entry/프로세싱-언어와-PDE-코드-살펴보기-1

 

2번째 내용 시작

process/app/src 소스를 보면서 접근했다. JDI 코드 부분을 제외한 전반적인 코드를 분석한다.

 

1. Base, Editor 클래스

main 클래스는 processing.app.Base.java 이다.

Base 클래스의 createAndShowGUI() 메서드는 간단하게 버전 정도를 확인하고 swing component를 생성하고 나서 jdk 의 버전을 확인한다. 그리고, 설정 정보(윈도우 크기 등)을 읽는다. OS에 맞는 환경(platform)을 잘 생성하여 어디에서도 문제없이 동작하게 한다. 임시 디렉토리도 확인한 후, Base 클래스의 생성자를 호출한다.

 

* Base 클래스의 main 함수와 관련 메서드

public class Base {


  static public void main(final String[] args) {
    EventQueue.invokeLater(new Runnable() {
        public void run() {
          createAndShowGUI(args);
        }
    });
  }

  static private void createAndShowGUI(String[] args) {
    try {
      File versionFile = getContentFile("lib/version.txt");
      if (versionFile.exists()) {
        String version = PApplet.loadStrings(versionFile)[0];
        if (!version.equals(VERSION_NAME)) {
          VERSION_NAME = version;
          RELEASE = true;
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }

    initPlatform();

    // Use native popups so they don't look so crappy on osx
    JPopupMenu.setDefaultLightWeightPopupEnabled(false);

    // Don't put anything above this line that might make GUI,
    // because the platform has to be inited properly first.

    // Make sure a full JDK is installed
    initRequirements();

    // run static initialization that grabs all the prefs
    Preferences.init(null);

//    String filename = args.length > 1 ? args[0] : null;
    if (!SingleInstance.exists(args)) {
      SingleInstance.createServer(platform);

      // Set the look and feel before opening the window
      try {
        platform.setLookAndFeel();
      } catch (Exception e) {
        String mess = e.getMessage();
        if (mess.indexOf("ch.randelshofer.quaqua.QuaquaLookAndFeel") == -1) {
          System.err.println("Non-fatal error while setting the Look & Feel.");
          System.err.println("The error message follows, however Processing should run fine.");
          System.err.println(mess);
        }
      }

      // Create a location for untitled sketches
      try {
        untitledFolder = Base.createTempFolder("untitled", "sketches");
        untitledFolder.deleteOnExit();
      } catch (IOException e) {
        //e.printStackTrace();
        Base.showError("Trouble without a name",
                       "Could not create a place to store untitled sketches.\n" +
                       "That's gonna prevent us from continuing.", e);
      }

//  System.out.println("about to create base...");
      new Base(args);
//  System.out.println("done creating base...");
    }
  }

 

Base 클래스의 생성자에서는 스케치 북 (프로세싱 디렉토리) 폴더를 확인한다. mode 디렉토리 안에 있는  jar나 zip으로 끝나는 파일들을 모아서 classpath에 넣어서 컴파일시 유용하게 사용할 수 있도록 한다. java/android/javascript를 지원하고 있다.

OS에 맞는 UI (look & Feel)에 맞게 Editor 클래스(JFrame)을 초기화한다. 프로세싱 pde 툴을 작업하고 종료했으면, 기존에 저장된 소스를 불러온다. 만약 그런 경우가 없다면 빈 화면만 보이게 한다.

만약  PDE 개발툴의 업데이트가 존재하면 알려준다.

public Base(String[] args) {
    // Get the sketchbook path, and make sure it's set properly
    determineSketchbookFolder();

    // Delete all modes and tools that have been flagged for deletion before
    // they are initialized by an editor.
    ArrayList<InstalledContribution> contribs = new ArrayList<InstalledContribution>();
    contribs.addAll(ModeContribution.list(this, getSketchbookModesFolder()));
    contribs.addAll(ToolContribution.list(getSketchbookToolsFolder(), false));
    for (InstalledContribution contrib : contribs) {
      if (ContributionManager.isFlaggedForDeletion(contrib)) {
        removeDir(contrib.getFolder());
      }
    }

    buildCoreModes();
    rebuildContribModes();

    libraryManagerFrame = new ContributionManagerDialog("Library Manager",
                                                        new ContributionListing.Filter() {
      public boolean matches(Contribution contrib) {
        return contrib.getType() == Contribution.Type.LIBRARY
            || contrib.getType() == Contribution.Type.LIBRARY_COMPILATION;
      }
    });
    toolManagerFrame = new ContributionManagerDialog("Tool Manager",
                                                     new ContributionListing.Filter() {
      public boolean matches(Contribution contrib) {
        return contrib.getType() == Contribution.Type.TOOL;
      }
    });
    modeManagerFrame = new ContributionManagerDialog("Mode Manager",
                                                     new ContributionListing.Filter() {
      public boolean matches(Contribution contrib) {
        return contrib.getType() == Contribution.Type.MODE;
      }
    });
    updateManagerFrame = new ContributionManagerDialog("Update Manager",
                                                       new ContributionListing.Filter() {
      public boolean matches(Contribution contrib) {
        if (contrib instanceof InstalledContribution) {
          return ContributionListing.getInstance().hasUpdates(contrib);
        }

        return false;
      }
    });

    // Make sure ThinkDifferent has library examples too
    defaultMode.rebuildLibraryList();

    // Put this after loading the examples, so that building the default file
    // menu works on Mac OS X (since it needs examplesFolder to be set).
    platform.init(this);

    toolsFolder = getContentFile("tools");

//    // Get the sketchbook path, and make sure it's set properly
//    determineSketchbookFolder();

//    // Check if there were previously opened sketches to be restored
//    boolean opened = restoreSketches();
    boolean opened = false;

    // Check if any files were passed in on the command line
    for (int i = 0; i < args.length; i++) {
      String path = args[i];
      // Fix a problem with systems that use a non-ASCII languages. Paths are
      // being passed in with 8.3 syntax, which makes the sketch loader code
      // unhappy, since the sketch folder naming doesn't match up correctly.
      // http://dev.processing.org/bugs/show_bug.cgi?id=1089
      if (isWindows()) {
        try {
          File file = new File(args[i]);
          path = file.getCanonicalPath();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
      if (handleOpen(path) != null) {
        opened = true;
      }
    }

    // Create a new empty window (will be replaced with any files to be opened)
    if (!opened) {
//      System.out.println("opening a new window");
      handleNew();
//    } else {
//      System.out.println("something else was opened");
    }

    // check for updates
    if (Preferences.getBoolean("update.check")) {
      new UpdateCheck(this);
    }
  }

 

Editor 클래스는 JFrame을 상속했으며, 화면 UI를 담당한다.

public abstract class Editor extends JFrame implements RunnerListener

에디터의 기본 내용들은 processing.app 패키지에 담겨져 있다.

 

2. Contribution

여기에 기존 library외에 특정 폴더에 lib 또는 zip파일을 복사하면 프로세싱 pde 툴이 시작하면서 그 파일들을 모두 읽는다. 이런 파일들을 processing 툴에서는 contribution이라는 단어를 써서 하고 있다. 파일을 다운받고 설치할 수 있기도 하고, 메뉴에서 refresh 해서 다시 library 정보를 취합할 수 있다. 관련 내용은 processing.app.contrib.ContributionManager.java 과 processing.app.contrib.Contribution.java 에 담겨있다.

 

3. Process

안드로이드환경에서 avd를 생성하거나 출력, adb, keytool, mv 명령을 내릴 때 필요한 정보들은 processing.app.exec 패키지에 담겨 있다.

 

4. Platform

linux, mac, window 에 맞는 UI를 실행시키거나 OS-dependent한 코드들을 실행할 때 필요한 내용들이 담겨 있다. 예를 들어 윈도우의 경우 폴더를 실행시킬 때, explorer를 실행시켜야 한다. 또한 msvcrt.dll(printf와 같은 stdio/stdout에 해당되는 c 런타임 라이브러리로서 sta)를 jna로 연결한다. 그리고, 마우스 오른쪽 버튼 연동을 하여 자바 Swing 어플에서 폴더를 열 때, 윈도우에서 폴더 열어서 이것 저것(예, 폴더 생성)을 할 수 있게 처리하는 코드가 담겨 있다.

관련 패키지는 processing.app.windows, processing.app.linux, processing.app.macosx  이다.

 

5. Syntax

processing.app.DefaultInputHander 클래스 소스에서는 Editor를 사용하기 때문에 키보드로부터 다양한 입력이 들어오는데. 미리 저장해둔 키보드 액션 값(Ctrl + C와 같은 복사)인지 코드(Code)인지를 분간한다. 또한, 예전 JEdit Syntax (syntax.jedit.org) 의 Code highlight 형식을 적용하여 예쁘게 색칠해 준다. 키워드일 때는 색깔을 입혀 코드 작성자가 깜박하지 않도록 도와준다.

코드를 토큰을 줄 단위로 나눈 후 토큰을 토큰 리스트(linked list)로 연결했다.

 

관련 코드는 processing.app.syntax에 있다.

 

6. tools

소스를 압축하고, 색깔을 선택하거나, 인코딩, 폰트 설정과 같은 Tool 성격의 클래스가 담겨져 있다.

 

7, mode

프로세싱은 android, javascript, java 중 세가지 모드로 export할 수 있는 코드로 구성되어 있다.

이 중 특별하게 javascript mode는 javascript 코드를 작성하면, 이를 processing.js 파일로 만든 후, 소스는 그냥 PApplet 클래스로 해서  웹 브라우져로 보여주도록 코드 작성이 되어 있다. processing.mode.javascript 패키지에 내용이 담겨 있다.

그리고, android mode는 avd도 연동할 수 있도록 편리한 클래스를 제공하고 있다. processing.mode.android 패키지에 잘 설명이 되어 있다. 완전히 안드로이드 향에서 동작되어 있는 코드로 되어 있다. 자세한 것은 안보고 패쓰.. 중요한 것은 java에 있으니 그곳으로 집중.

마지막으로 남은  java mode를 살펴본다. java mode에서는 antlr과 java debug interface를 사용하고 있었다. (사실 이 것 때문에 소스 분석한 것이긴 하다.)

 

먼저 빌드(app/build.xml) 소스를 확인해 본다.

가정 먼저 antlr 문법에 맞게 전처리 작업을 전행한다. processing/mode/java/preporc/java15.g 파일을 읽어서 소스 generate 한다. 그 다음 같은 디렉토리에 있는 pde.g 파일을 읽고 소스를 generate 한다.

 

 

  <property name="generated"
        value="${basedir}/generated/processing/mode/java/preproc" />
  <mkdir dir="${generated}" />

  <property name="grammars"
        value="${basedir}/src/processing/mode/java/preproc" />

 

……………..

<target name="preproc" description="Compile ANTLR grammar">
    <antlr target="${grammars}/java15.g" outputdirectory="${generated}">
      <classpath path="${antlrjar}" />
    </antlr>
    <antlr target="${grammars}/pde.g"
       outputdirectory="${generated}"
       glib="${grammars}/java15.g">
      <classpath path="${antlrjar}" />
    </antlr>
  </target>

  <target name="compile" depends="preproc" description="Compile sources">
    <condition property="core-built">
      <available file="../core/core.jar" />
    </condition>
    <fail unless="core-built"
      message="Please build the core library first and make sure it sits in ../core/core.jar" />

    <mkdir dir="bin" />

 

java15.g는 antlr이 이해할 수 있는 java1.5 언어 문법 파일이다. 이 파일이 antlr에 의해서 생성되는 클래스는 파서(class JavaRecognizer extends Parser) 과 스캐너 (class JavaLexer extends Lexer) 이다. 그리고, pde.g 은 antlr에 의해서 파서(class PdeRecognizer extends JavaRecognizer)와 스캐너 (class PdeLexer extends JavaLexer)와 토큰에 대한 정의 정보 (interface PdeTokenTypes)가 생성된다.

컴파일된 클래스는 아래 위치에서 확인해 볼 수 있었다.

G:\android\workspace\processing\app\bin\processing\mode\java\preproc

 

프로세싱 PDE에서 application 또는 applet으로 export 할 때, 빌드가 일어나고 실행할 수 있게 되어 있다. processing.mode.java.JavaBuild.java가 중요 핵심 클래스가 된다. 

processing.mode.java.JavaBuild 클래스의 exportApplication() 메서드에서 application빌드하고  exportApplication(File destFolder, int exportPlatform,int exportBits) 메서드에서 실행을 한다.

만약 윈도우라면 아래와 같이 자바가 실행이 된다.


if (exportPlatform == PConstants.WINDOWS) {
      if (exportBits == 64) {
        // We don't yet have a 64-bit launcher, so this is a workaround for now.
        File batFile = new File(destFolder, sketch.getName() + ".bat");
        PrintWriter writer = PApplet.createWriter(batFile);
        writer.println("@echo off");
        writer.println("java -Djava.ext.dirs=lib -Djava.library.path=lib " + sketch.getName());
        writer.flush();
        writer.close();
      } else {
        Base.copyFile(mode.getContentFile("application/template.exe"),
                      new File(destFolder, sketch.getName() + ".exe"));
      }
    }

 

빌드과정을 조금 따라가 본다.

processing.mode.java.JavaBuild.java 파일의 build 파일이 실행되는 시점부터 코드를 따라간다. 먼저 전처리 과정을 한 후, 컴파일을 한다.

  public String build(File srcFolder, File binFolder) throws SketchException {
    this.srcFolder = srcFolder;
    this.binFolder = binFolder;

//    Base.openFolder(srcFolder);
//    Base.openFolder(binFolder);
   
    // run the preprocessor
    String classNameFound = preprocess(srcFolder);

    // compile the program. errors will happen as a RunnerException
    // that will bubble up to whomever called build().
//    Compiler compiler = new Compiler(this);
//    String bootClasses = System.getProperty("sun.boot.class.path");
//    if (compiler.compile(this, srcFolder, binFolder, primaryClassName, getClassPath(), bootClasses)) {
    if (Compiler.compile(this)) {
      sketchClassName = classNameFound;
      return classNameFound;
    }
    return null;
  }

 

processing.mode.java.preproc.PdePreprocessor.preprocess() 메서드가 호출되면, 파서가 가장 먼저 동작한다. 파서가 만든 AST tree를 traverse할 수 있는 pdeEmitter를 사용한다. PrintWriter 객체에 java로 컴파일가능한 소스를 생성한다. 그리고 classpath 에 library를 추가하고, 임시 파일을 생성(Base.saveFile()호출)한다. 그리고, 원래대로 돌아가 컴파일 하는 구조이다.


8. JDI (java debugger interface)
JPDA의 하이 레벨의 인터페이스인 JDI를 정의하는 코드가 processing.mode.java.runner 패키지에 담겨있다.

어플리케이션에서 run 또는 present를 실행시 JavaMode클래스의 handleRun메서드를 실행하고,
Runner  클래스의   launchVirtualMachine()메소드 실행으로 이어진다.
실제 어플리케이션에서 어플리케이션을 실행시키면서 jpda로 연결해서 테스트가 가능할 수 있도록 한다.


 Runner 클래스의 launch 메서드는 다음처럼 먼저 vm을 실행시키고, trace 를 생성한다.

public void launch(boolean presenting) {
    this.presenting = presenting;
   
    // all params have to be stored as separate items,
    // so a growable array needs to be used. i.e. -Xms128m -Xmx1024m
    // will throw an error if it's shoved into a single array element
    //Vector params = new Vector();

    // get around Apple's Java 1.5 bugs
    //params.add("/System/Library/Frameworks/JavaVM.framework/Versions/1.4.2/Commands/java");
    //params.add("java");
    //System.out.println("0");

    String[] machineParamList = getMachineParams();
    String[] sketchParamList = getSketchParams();

    vm = launchVirtualMachine(machineParamList, sketchParamList);
    if (vm != null) {
      generateTrace(null);
//      try {
//        generateTrace(new PrintWriter("/Users/fry/Desktop/output.txt"));
//      } catch (Exception e) {
//        e.printStackTrace();
//      }
    }
  }




VM 실행에 대한 코드이다.  debug에 대한 정보가 보인다.


  protected VirtualMachine launchVirtualMachine(String[] vmParams,
                                                String[] classParams) {
    //vm = launchTarget(sb.toString());
    LaunchingConnector connector = (LaunchingConnector)
      findConnector("com.sun.jdi.RawCommandLineLaunch");
    //PApplet.println(connector);  // gets the defaults

    //Map arguments = connectorArguments(connector, mainArgs);
    Map arguments = connector.defaultArguments();

    Connector.Argument commandArg =
      (Connector.Argument)arguments.get("command");
    // Using localhost instead of 127.0.0.1 sometimes causes a
    // "Transport Error 202" error message when trying to run.
    // http://dev.processing.org/bugs/show_bug.cgi?id=895
    String addr = "127.0.0.1:" + (8000 + (int) (Math.random() * 1000));
    //String addr = "localhost:" + (8000 + (int) (Math.random() * 1000));
    //String addr = "" + (8000 + (int) (Math.random() * 1000));

    String commandArgs =
      "java -Xrunjdwp:transport=dt_socket,address=" + addr + ",suspend=y ";
    if (Base.isWindows()) {
      commandArgs =
        "java -Xrunjdwp:transport=dt_shmem,address=" + addr + ",suspend=y ";
    } else if (Base.isMacOS()) {
      commandArgs =
        "java -d" + Base.getNativeBits() + //Preferences.get("run.options.bits") + 
        " -Xrunjdwp:transport=dt_socket,address=" + addr + ",suspend=y ";
    }

    for (int i = 0; i < vmParams.length; i++) {
      commandArgs = addArgument(commandArgs, vmParams[i], ' ');
    }
    if (classParams != null) {
      for (int i = 0; i < classParams.length; i++) {
        commandArgs = addArgument(commandArgs, classParams[i], ' ');
      }
    }
    commandArg.setValue(commandArgs);

    Connector.Argument addressArg =
      (Connector.Argument)arguments.get("address");
    addressArg.setValue(addr);

    //PApplet.println(connector);  // prints the current
    //com.sun.tools.jdi.AbstractLauncher al;
    //com.sun.tools.jdi.RawCommandLineLauncher rcll;

    //System.out.println(PApplet.javaVersion);
    // http://java.sun.com/j2se/1.5.0/docs/guide/jpda/conninv.html#sunlaunch

      return connector.launch(arguments);






trace를 생성하는 메서드이다. VM에 대해서 debug trace를 걸고, event thread 를 생성한다.
VM을 띄운 후, 어플리케이션 출력코드를 다 받아와 editor에 화면에 보여줄 수 있도록 한다.

  /**
   * Generate the trace.
   * Enable events, start thread to display events,
   * start threads to forward remote error and output streams,
   * resume the remote VM, wait for the final event, and shutdown.
   */
  protected void generateTrace(PrintWriter writer) {
    vm.setDebugTraceMode(debugTraceMode);

    EventThread eventThread = null;
    //if (writer != null) {
    eventThread = new EventThread(this, vm, excludes, writer);
    eventThread.setEventRequests(watchFields);
    eventThread.start();
    //}

    //redirectOutput();

    Process process = vm.process();

//  processInput = new SystemOutSiphon(process.getInputStream());
//  processError = new MessageSiphon(process.getErrorStream(), this);

    // Copy target's output and error to our output and error.
//    errThread = new StreamRedirectThread("error reader",
//        process.getErrorStream(),
//        System.err);
    MessageSiphon ms = new MessageSiphon(process.getErrorStream(), this);
    errThread = ms.getThread();

    outThread = new StreamRedirectThread("output reader",
        process.getInputStream(),
        System.out);

    errThread.start();
    outThread.start();

    vm.resume();
    //System.out.println("done with resume");

    // Shutdown begins when event thread terminates
    try {
      if (eventThread != null) eventThread.join();
//      System.out.println("in here");
      // Bug #852 tracked to this next line in the code.
      // http://dev.processing.org/bugs/show_bug.cgi?id=852
      errThread.join(); // Make sure output is forwarded
//      System.out.println("and then");
      outThread.join(); // before we exit
//      System.out.println("out of it");

      // At this point, disable the run button.
      // This happens when the sketch is exited by hitting ESC,
      // or the user manually closes the sketch window.
      // TODO this should be handled better, should it not?
      if (editor != null) {
        editor.deactivateRun();
      }
    } catch (InterruptedException exc) {
      // we don't interrupt
    }
    //System.out.println("and leaving");
    if (writer != null) writer.close();
  }




전형적인 JPDA FE 쓰레드를 사용하고 있으며, 어플리케이션에서 어플리케이션을 띄우고 관련 로그를 부모 어플리케이션으로 보내거나 할 때 많이 유용하다. eclipse 의 어플리케이션 디버그도 이와 비슷하다.



(출처 : http://openjdk.java.net/groups/serviceability/jpdaFEThreads.jpg)
                                               

Posted by 김용환 '김용환'

댓글을 달아 주세요

  1. Favicon of http://cafe.naver.com/iphonediy BlogIcon 제갈식 2013.05.06 17:57  댓글주소  수정/삭제  댓글쓰기

    내용이 좋아 퍼갑니다.

    좋은 프로세싱 팁이군요~

    http://cafe.naver.com/iphonediy




(이미지 출처 : http://www.identropy.com/blog/. 사진만 링크걸음)


2010년 하반기, 나는 회사에서 FOSS 거버넌스 정책에 대한 초안을 잡았다. 조직이동을 하면서 아이디어와 개념만 잡는 일만 하고, 거버넌스에 대한 실무는 내가 직접 하지 않았다는 점이 가장 흠이지만. 큰 틀에서는 내가 생각한 큰 틀은 지금도 이어지고 있다. 사실 내가 하고 싶어서 한 것은 아니고, 시간이 약간 널널한 관계로 정리했었던 것 같다.

(내 생각에는 내가 뛰어나서기 보다는 시간이 많아서 된 것 같다. 절대로 내가 특출난 능력이 있어서 한 것은 아니다. 사실 난 많이 부족한 사람이다. )

회사 특성상, 오픈 소스를 많이 활용하고 있다. 웹 서비스 및 솔루션 특성상 많은 오픈 소스 라이브러리를 알고 언제든기 개발할 준비가 되어있어야 한다. 그러나 워낙 많은 오픈 소스들이 범람하고 있고, 잘못하다가는 장애로 이어질 수 있는 수많은 장애가 있기 때문에 반드시 이 부분이 있어야 했다.

단순히 오픈 소스 소프트웨어 는 아무거나 쓰세요 하기에는 중요한 결정을 하는 조직에 있었고, 많은 개발자들이 새로 나온 오픈 소스 소프트웨어 또는 버그가 충만한(?) 오픈 소스 소프트웨어의 특이 버전을 쓰면서 이슈가 될 수 있었다. 오픈 소스 소프트웨어를 쓰면서 문제나 장애가 생기면 누구의 책임인가? 하는 고민들이 생기던 시점이었다. 그리고 회사에 많은 서비스조직이 있다보니 계속 반복된 장애는 계속 다른 부서에서도 나타났다. 마치 메아리처럼 이 산, 저 산 부딪히듯이 계속 발생되는 경우가 있었다.

또한 거버넌스 (Governance)라는 단어는 사람마다 달리 생각할 수 있는 모호한 단어였다. 거버넌스란 무엇인가? 거버넌스의 한계는 어디까지인가? 정부가 통제하듯이 하는 것인가? 가이던스(Guidance) 라는 단어와 많이 비슷한 느낌이면서 먼가 Push하는 느낌 때문에 초안 잡는 부분에 힘이 들었다. 너무 관여하면 자율성에 문제가 일어나고, 너무 멀리 있으면 장애나 서비스개발에 어려운 점들이 많았다.

인터넷을 통해서 다양한 정보를 얻으려 했지만, 결코 정보를 취합하지는 못했다. 거버넌스 하는 회사가 한국 뿐 아니라 외국에도 없는 독특한 사례였다. 단순히 Test 의 개념이나 Regressive Test 개념정도로 거버넌스를 바라보는 자료도 있었다. 좋은 선례가 없어서 결국 우리 회사안에서의 FOSS 거버넌스를 직접 하기로 했다.

나는 여러가지 인문서와 성경을 참조했다.
- 헤겔의 역사철학 강의
- 예링의 권리를 위한 투쟁
- 키케로의 의무론
- 루소의 사회계약론
- 홉스의 리바이던
- 애덤스미스의 국부론
- 토마스 모어의 유토피아
- 로크의 정부론
- 아리스토텔레스의 정치학
- 하이데거의 존재와 시간

우선 거버넌스를 하기전에 많은 것을 알아햐 했다. 사람에 대해서 알아야 했었다. 조직이란, 사람이란 무엇인가? 그리고, 어떻게 해야 유익할 것인가? 선한 게 행동할 것인가? 개발자는 어떤 사람인가? 관리자는 어떤 사람인가? 이 조직의 롤은 무엇이고, 다른 조직의 롤은 무엇인가? 바람직한 거버넌스는 무엇인가? 협업은 어떻게 할 것인가?하는 많은 고민을 하고 또 고민했다. 사람을 이해하고 조직을 이해하는 과정 속에서 다른 사람들은 어떻게 문제를 해결하려고 했을까 하는 수많은 생각들을 접하면서 깊이 고민했다.
(이런 시간들을 통해서 나는 겸손함을 더욱 더 배웠던 시간이었다. 나보다 똑똑하고 훌륭한 사람들이 많았고, 그 분들을 통해서 사회를 볼 수 있었다. 그러나.. 내 안에 아직도 많은 식견이 부족해서 많이 다듬어야 할 것 같다. )

내가 과연 모든 사람이 만족할만한 완전한 거버넌스를 만들어낼 수 있을까? 라는 고민속에 초안을 작성하기 시작했다. 잘못된 방향과 설득으로 인해서 어중간하게 하다가 종료하는 것은 없도록 해야할 것 같았다. 감사하게도 조직장과 좋은 피드백을 통해서 좋은 초안이 만들어지기 시작했다.

오픈 소스 거버넌스의 사용 조건, 오픈 소스 거버넌스 결과에 대한 게시판, 오픈 소스 거버넌스 조직은 모든 오픈소스를 다 할 수 없기 때문에 오픈 소스를 사용하려고 하는 서비스 부서와의 협업 모델을 생각하며 그렸다. 다양한 시나리오를 따라 흘러가는 플로우 차트를 그리며 오픈 소스 거버넌스에 대한 내용을 작성해서 어른 조직장들의 회의를 통과했다.

내가 가장 바탕을 둔 것은 오픈 소스 라이브러리에 대한 '검증'과 서비스 부서와 거버넌스 조직간의 '신뢰'였다. 첫번째 오픈 소스 소프트웨어에 대한 '검증'은 철저히 하려고 했다. 비슷한 종류의 라이브러리를 비교하고 사용성 팩터들을 모두 나열하고 객관적인 검토와 테스트를 통해서 검증할 수 있게 했다. 그리고 승인받은 오픈 소스 라이브러는 버전 통제를 통해서 잘 쓸 수 있는 것과 못쓰는 것, 문제 있는 것을 구분하려고 했다.

두번째, 상호 조직간의 신뢰였다. 거버넌스 자체가 굉장히 딱딱해질 수 밖에 없고, 특히 칼로 자르듯 하는 부분 때문에 감정이 상하거나 신뢰 관계가 손상되지 않게 하려고 했다. 서로가 돕고 지식을 쌓고 신뢰를 증강시키는 부분을 많이 염두하려고 했다. 신뢰 관계가 손상되면 소문이 나게 되고 불신이 전체 조직으로 흘러갈 수 있는 부분을 줄이려 했다. 이 불신에 대한 방법론이 여러가지로 나타나게 하지 않는 것이 최선이라고 생각했다. 


(이미지 출처 : http://opiniontribunal.wordpress.com, 사진만 링크걸음)

초안을 작성하고 나고 공유하면서. 여러 담당자들로부터 들은 얘기는 다들 할 수 없다는 분위기였다. 그리고 코딩과 상관없는 일들을 하는 것에 대해서 많은 부담을 가지고 있었다. '코딩'의 즐거움이 없어서 부담스러웠을지 모르겠다. 하지만, 조직장의 업무 지시로 다들 잘해갔다. 그리고, 관계에 대한 신뢰 부분을 얘기할 때, 여러 개발자들의 설득이 가장 어려웠다. 관계의 중요성을 잘 얘기하려고 했지만, 이 부분은 잘 이해하는 사람은 많지 않았다. 개발자들에게는 관계의 중요성을 얘기하는 것은 가장 어렵다. (협업 잘하는 것과 관계를 잘 이끄는 것은 비슷하지만, 완전 다른 내용이다. 관계를 잘 이끄는 것은 상대에 대한 신뢰와 섬김과 양보가 필요하다. 때로는 적절한 관계를 두고 하는 것도 포함한다. 나중에 일과 관계에 대해서 써봐야겠다는 생각이 든다.)

모든 오픈 소스 라이브러리를 검증할 수 없기 때문에 중요한 것만 검증을 시도했다. 모든 라이브러리를 검증할 필요가 없었다. 중요한 것만, 장애로 검증한 것만 해도 사실은 충분했다.

시스템적으로 다양하게 변화했다. 위키보다는 Cafe 형태로 제공하는 방법으로 말씀드렸고, 그 부분은 잘 진행할 수 있었다. 또한 좀 더 진화해서 서버에 있는 라이브러리를 검증하여 서비스 개발자에게 이메일로 알려주도록 하는 시스템을 추가하였다. 이렇게 하여 안정성을 확보한 솔루션이 만들어졌다.

실제로 업무가 돌아가는 것을 보면서 기분이 좋았고, 사실 초안을 작성한 나도 확신이 없었다. 오픈 소스 라이브러리에 대한 믿음을 가지고 힘을 가지고 능력을 가진 조직장때문에 일이 잘 진행되었다. 다만, 아쉬운 점은 상호간의 협력 대신 점차 그 의미는 시들어지고, 껍데기인 프로세스만 남는다는 사실이 아쉬웠다. 아마도 많은 사람들이 나와 같은 고민을 했을까? 흐훗..


마치며...

오픈 소스 거버넌스에 대해서 간단하게 적은 글이다. 프로세스를 만들기전 어떤 고민을 했는지를 적었다. 거버넌스에 대한 고민에 대해서 누군가가 이런 일을 할 때 좋은 소스가 되지 않을까 해서 작성해본다.
좋은 오픈 소스 거버넌스는 방법론적 접근보다는 검증과 신뢰 관계가 중요하다는 관점에서 시작했고, 좋은 결과를 가질 수 있었던 것 같다.

Posted by 김용환 '김용환'

댓글을 달아 주세요


DBCP(http://commons.apache.org/dbcp/configuration.html) 페이지를 보면 설명이 나오는데. 이거 썩 설명이 많지 않다. 여기다가 사족을 붙이고자 한다. 


 initialSize 0 (디폴트 값)
The initial number of connections that are created when the pool is started.
Since: 1.2
초기화시 싱생되는 connection 수를 의미한다. 

maxActive 8 (디폴트값)
The maximum number of active
 connections that can be allocated from this pool at the same time, or negative for no limit.
동시에 동작 가능한 최대 connection 개수이며, 이 이상의 connection을 요구하면, 반환될때까지 lock이 걸린다. -값은 크기 제한이 없다 

maxIdle 8 (디폴트값)
The maximum number of connections that can remain idle in the pool, without extra ones being released, or negative for no limit.
풀에서 idle를 유지하는 최대 개수를 의미한다.  -값은 크기 제한이 없다 

minIdle 0 (디폴트값)
The minimum number of connections that can remain idle in the pool, without extra ones being created, or zero to create none.
풀에서 idle를 유지할 수 있는 최소의 connection를 의미한다. evictor가 쫓아내더라도 이 숫자는 보장한다.

maxWait (디폴트값)
indefinitely The maximum number of milliseconds that the pool will wait (when there are no available connections) for a connection to be returned before throwing an exception, or -1 to wait indefinitely. 
Exception나기 전까지 풀에서 커넥션을 얻을때까지 얼마나 기다릴지를 지정한다.
보통 3000 이 좋은 값인듯.


파라미터

디폴트값

설명

validationQuery

The SQL query that will be used to validate connections from this pool before returning them to the caller. If specified, this query MUST be an SQL SELECT statement that returns at least one row.

 

물려있는 connection validation 체크를 수행할 sql 문을 지정한다. 오라클의 경우는 select 1 from dual 이며, msql의 경우는 select 1 로 지정하는 경우가 많다.

testOnBorrow

true

The indication of whether objects will be validated before being borrowed from the pool. If the object fails to validate, it will be dropped from the pool, and we will attempt to borrow another.
validationQuery
 connection이 살았나 죽었나 확인해준다.

NOTE - for a
true value to have any effect, the validationQuery parameter must be set to a non-null string.

 

pool에서 connection을 가져와서(borrow) connectionvalidationQuery를 실행한다.

testOnReturn

false

The indication of whether objects will be validated before being returned to the pool.
NOTE - for a
true value to have any effect, the validationQuery parameter must be set to a non-null string.

 

pool에서 connection을 돌려줄 때(return) connectionvalidationQuery를 실행한다.

testWhileIdle

false

The indication of whether objects will be validated by the idle object evictor (if any). If an object fails to validate, it will be dropped from the pool. NOTE - for a true value to have any effect, the validationQuery parameter must be set to a non-null string.
왕 중요하다.

Evitor threadIdle connection을 확인하여 connectionvalidationQuery를 체크하여 문제가 발생하면 connection pool에서 제거한다.

timeBetweenEvictionRunsMillis

-1

The number of milliseconds to sleep between runs of the idle object evictor thread. When non-positive, no idle object evictor thread will be run. 
evictor thread
가 실행되는 주기를 의미한다. 단위는 millisecond이다. 그동안 6000으로 사용해왔다.

numTestsPerEvictionRun

3

The number of objects to examine during each run of the idle object evictor thread (if any).

Evitor thread 동작시 idle connection의 개수만큼 체크한다.

minEvictableIdleTimeMillis

1000 * 60 * 30

The minimum amount of time an object may sit idle in the pool before it is eligable for eviction by the idle object evictor (if any). 
디폴트는 30분이다. evictor요청이 오면 이 시간값을 기준으로 생성된지 오래된 idle connecion을 없앤다
DB
입장에서 보면, 한번 3개가 들어와 session에 대해서 soft parsing하는 쿼리문으로 보인다


if (evictor
요청시간 - idle 객체 생성된 시간
> minEvictableIdleTimeMillis) {
     connection
회수
()
}
Evictor
가 잘 일을 못한다고 해서 -1로 설정하고 있다


 

 

'general java' 카테고리의 다른 글

Notepad with class java file (Jad Usage Tip)  (0) 2009.04.17
java sources for ojdbc  (0) 2009.04.17
중요한 DBCP 설정 공부  (0) 2009.04.10
Jad 사용법  (0) 2009.04.10
JMX 개발중 POJO를 이용하기  (0) 2009.03.31
jdk 5,6 클래스를 jdk 3,4에서 실행하기  (0) 2009.03.30
Posted by 김용환 '김용환'
TAG 자바

댓글을 달아 주세요