상세 컨텐츠

본문 제목

자바 애플리케이션의 Windows 메모리 사용 관리 방법

JAVA

by AlrepondTech 2020. 9. 17. 09:10

본문

반응형

 

 

=================================

=================================

=================================

 

 

출처: http://levin01.tistory.com/412

 

자바 런타임이 대규모의 메모리 관리를 핸들하더라도 프로그램의 메모리 사용에 대해 관심을 기울이면 머신 퍼포먼스를 최적화하고 메모리 유출을 감지하는데 큰 도움이 된다. Windows에서 메모리 사용을 감시하는데 사용할 수 있는 많은 툴이 있다. 사용 관점에 따라 장단점을 갖고 있다. 필자는 메모리 사용에 대한 몇 가지 잘못된 개념들을 바로잡고 툴 사용법을 설명하겠다.

자바의 잘 알려진 장점들 중 하나는, C 프로그래머와는 달리, 자바 프로그래머가 필요한 메모리를 할당하고 비우는 등의 성가신 책임을 질 필요가 없다는 점이다. 자바 런타임이 이 일을 관리하기 때문이다. 메모리는 각 인스턴스화된 객체를 위한 힙 영역에 자동으로 할당되고 가비지 컬렉터는 더 이상 필요하지 않는 객체로 채워진 메모리를 주기적으로 비운다. 하지만 그렇다고 해서 완전히 의무에서 벗어난 것은 아니다. 프로그램의 메모리 사용을 감시해야 하는데, 그 이유는 자바 프로세스의 메모리는 힙에서 부유하는 객체 이상의 것들로 구성되기 때문이다. 이것은 또한 프로그램용 바이트코드(런타임 시 JVM이 변환하는 명령어), JIT 코드(목표 프로세서를 위해 이미 컴파일된 코드), 원시 코드, JVM이 사용하는 메타데이터(테이블, 라인 넘버 테이블 등 제외) 등으로 구성되어 있다. 원시 라이브러리 같은 특정 유형의 메모리는 프로세스 간 공유될 수 있기 때문에 자바 애플리케이션의 실제 풋프린트를 결정하는 일이 어려워질 수 있다.

Windows에서 메모리 사용을 감시할 툴들은 많이 있지만 안타까운 것은 이중 어떤 것도 우리가 필요로 하는 정보를 주는 것은 없다. 게다가 다양한 툴들 자체도 공통의 어휘를 공유하지 않는다. 이 글에서 무료로 사용할 수 있는 유용한 툴들을 소개하고 사용 방법을 설명하겠다.

Windows 메모리 연구

이 글에서 설명할 툴을 이해하기에 앞서 Windows가 메모리를 관리하는 방법을 이해해야 한다. Windows는demand-paged virtual memory시스템을 사용한다.

 

가상 주소 공간

가상 메모리 개념은 실제 메모리에 한번에 맞지 않는 프로그램을 다루는 복잡한 문제에 대한 솔루션으로서 1950년대에 생겨났다. 가상 메모리 시스템에서 프로그램들은 물리적으로 사용할 수 있는 것보다 큰 주소 세트에 접근할 수 있고 메모리 매니저가 이들 논리적 주소들을 실제 위치로 매핑하면서 디스크상의 임시 스토리지에 오버플로우를 저장한다.

Windows가 사용하는 가상 메모리 구현에서 가상 스토리지는 페이지(page)라고 하는 동일한 크기의 단위로 구성된다. 각 운영 시스템 프로세스는 각자의가상 주소 공간을 할당 받는다. 이 가상 메모리 페이지들은 읽기와 쓰기가 가능하다. 각 페이지는 세 가지 상태 중 하나가 될 수 있다:

  • Free: 이 프로세스는 주소 공간의 해당 영역을 사용하고 있지 않다. 읽기 또는 쓰기를 목적으로 이 영역에 접근을 시도하면 특정 유형의 런타임 오류가 발생한다. Windows 다이얼로그 박스는 팝업창을 띄워 접근 위반이 발생했다고 알린다. (자바 프로그램은 이러한 종류의 실수를 할 수 없다; 포인터를 지원하는 언어로 작성된 프로그램만이 할 수 있는 일이다.)

  • Reserved: 프로세스가 앞으로 사용하도록 보유된 것이지만 Committed 되기 전까지는 접근할 수 없다. 많은 자바 힙이 Reserved로 시작한다.

  • Committed: 프로그램에 의해 접근할 수 있고 완전히 지원되는 메모리. 페이징 파일에서 메모리에 페이지 프레임이 할당된다. Committed 페이지들은 프로세스가 첫 번째로 이들을 참조할 때만 주 메모리로 로딩된다. 따라서on-demand paging이다.

그림 1은 프로세스의 주소 공간에 있는 가상 페이지들이 메모리의 물리적 페이지 프레임으로 매핑되는 방법을 보여주고 있다.


그림 1. 프로세스의 주소 공간의 가상 페이지들을 물리적 페이지 프레임으로 매핑하기

 

32 비트 머신(주로, Intel 프로세서)에서 구동하고 있다면 프로세스를 위한 전체 가상 주소 공간은 4GB이다. 4GB는 32 비트로 처리할 수 있는 가장 큰 용량이기 때문이다. Windows에서는 여러분이 주소 공간의 모든 메모리에 접근할 수 없다; 프로세스는 개인적으로 사용할 때 반 이하를 갖게 되고 Windows는 그 나머지를 사용한다. 2GB 개인 영역에는 프로그램을 실행하기위해 JVM이 필요로 하는 대부분의 메모리가 포함되어 있다: 자바 힙, JVM을 위한 C 힙, 프로그램 쓰레드용 스택, 바이트코드와 JIT 메소드를 보유하기 위한 메모리, 원시 메소드가 할당하는 메모리 등.

크고 인접한 영역의 메모리를 할당하기는 하지만 이 모든 것을 즉시 필요로 하지 않는 프로그램들은 Reserved와 Committed 메모리의 조합을 사용한다. JVM은 자바 힙을 이러한 방식으로 할당한다. JVM의-mx매개변수는 힙의 최대 사이즈에 대해 지시하지만 JVM은 시작할 때 모든 메모리를 할당하지는 않는다.-mx에 의해 지정된 양을 보유하면서 커밋 될 수 있는 것으로 전체 범위의 주소를 표시한다. 그런 다음 메모리의 일부만 커밋하는데 이것은 메모리 매니저가 실제 메모리와 페이징 파일에서 페이지를 할당하여 백업하는데 필요한 부분을 위한 것이다. 나중에 활성 데이터의 양이 늘어나고 힙이 확장되어야 한다면 JVM은 현재 커밋된 영역에 인접하여 약간 더 커밋할 수 있다. 이러한 방식으로 JVM은 하나의 인접한 힙을 관리하면서 필요할 때 늘릴 수 있다. (참고자료)

실제 메모리

물리적 스토리지는 일반적으로 알려져 있는 동일한 크기 단위인페이지 프레임(page frame)으로 구성된다.페이지 테이블(page table)이라고 하는 운영 체계 데이터 구조는 애플리케이션이 접근한 가상 페이지들을 주 메모리의 실제 페이지로 매핑한다. 맞을 수 없는 페이지들은 디스크상의 임시 페이징 파일에 저장된다. 페이지가 메모리상에 없는 페이지로 접근을 시도할 때,페이지 오류(page fault)는 메모리 매니저로 하여금 페이징 파일에서 이를 검색하도록 하여 이를 다시 주 메모리에 놓는다. 이 작업을페이징(paging)이라고 한다. 어떤 페이지가 교체되어야 하는지를 결정하는데 사용되는 이 세밀한 알고리즘은 Windows 버전에 따라 다르다. Windows에서는 페이지 프레임이 여러 애플리케이션들이 한번에 사용하는 프로세스들간(예를 들어, DLL) 공유되도록 한다. Windows는 다른 주소 공간의 많은 가상 페이지들을 같은 물리적 위치로 매핑하여 이를 수행한다.

애플리케이션은 이 모든 액티비티를 감쪽같이 모르고 있다. 알고 있는 것이라곤 자신의 가상 주소 공간이다. 하지만상주 세트(resident set)로 알려진 주 메모리의 페이지 세트가작업 세트(working set)로 알려진 실제 사용에 필요한 페이지 세트 보다 작을 경우 퍼포먼스가 떨어지게 된다.

 

 
 

 

Task Manager와 PerfMon

이제 가장 일반적인 두 개의 툴인 Task Manager와 PerfMon을 살펴보도록 하겠다. Windows에 번들되어 있다.

Task Manager

Task Manager는 매우 단순한 Windows 프로세스 모니터이다. Ctrl-Alt-Delete 키 조합이나 Taskbar를 오른쪽 클릭하여 액세스 할 수 있다. Processes 탭에서 상세한 정보를 보여주고 있다.(그림 2)

 


그림 2. Task Manager Processes 탭

 

그림 2의 컬럼들은View > Select Columns를 선택하여 커스터마이징 되었다. 어떤 컬럼 헤딩 이름이 모호하지만 Task Manager 도움말에 각각의 정의가 나와있다. 프로세스의 메모리 사용에 대한 가장 연관이 깊은 것은 다음과 같다:

  • Mem Usage: 온라인 도움말은 이를 프로세스의 작업 세트(working set)로 칭하고 있다.(많은 사람들은 이를 상주 세트(resident set)라고 부른다.) 이 페이지 세트는 주 메모리에 있다. 하지만 다른 프로세스들에 의해 공유될 수 있는 페이지들을 포함하고 있기 때문에 이중 카운트가 되지 않도록 주의해야 한다. DLL을 사용하고 있는 두 개의 프로세스들의 메모리 사용을 알려면 단순히 Mem Usage 값을 추가할 수 없다.

  • Peak Mem Usage: 프로세스가 시작된 후 가장 높은 값의 Mem Usage

  • Page Faults: 주 메모리에 없었던 곳에 페이지가 액세스를 시작한 이후 총 시간 수

  • VM Size: 온라인 도움말은 이것을 '프로세스에 의해 할당된 전체 프라이빗 가상 메모리'로 칭한다. 이것은 프로세스에 의해 커밋된 프라이빗 메모리이다. 프로세스가 메모리를 보유하고는 있지만 커밋하지 않는 다면 전체 주소 공간의 크기와는 매우 다를 수 있다.

비록 Windows 문서가 Mem Usage를 작업 세트로 언급한다 하더라도 이 글에서는 상주 세트로 통칭하도록 하겠다. Memory Management Reference's glossary에서 이 용어에 대한 자세한 정의를 참고하기 바란다. (참고자료)작업 세트(Working set)는 프로세스가 어떤 페이징이라도 피하기 위해 그 지점에서 메모리에 갖고 있어야 할 페이지들의 논리적 개념을 의미한다.

PerfMon

또 다른 툴로는 PerfMon이 있다. 이것은 프린트 큐부터 전화 기술 까지 광범위한 카운터를 감시한다. PerfMon은 일반적으로 시스템 경로에 있기 때문에 명령행에서perfmon을 입력하여 시작할 수 있다. 이 툴의 장점은 카운터를 그래픽으로 디스플레이 한다는 것이다. 따라서 시간의 흐름에 따라 변하는 방법을 쉽게 볼 수 있다.

PerfMon 스크린의 상단에 있는 툴바에서+버튼을 클릭한다. 대화창이 나오고 검사 할 카운터를 선택할 수 있다. (그림 3a) 카운터는퍼포먼스 객체들로 알려진 범주들로 그룹핑된다. 메모리 사용과 관련된 두 가지는MemoryProcess이다. 카운터 정의는Explain버튼을 클릭한다. 개별 창에 설명이 나온다.(그림 3b)


그림 3a. PerfMon 카운터 윈도우



그림 3b. 설명

 

(Ctrl을 사용하여 여러 열들을 하이라이트 하여) 관심 있는 카운터를 선택하고 모니터 하려는 인스턴스를 선택한다. 그런다음Add를 클릭한다. 이 툴은 즉시 선택한 모든 카운터들의 값을 디스플레이 하기 시작한다. 리포트, 시간 별 그래프, 히스토그램으로 나타낼 수 있다. 그림 4는 히스토그램 디스플레이이다.


그림 4. PerfMon 히스토그램

 

그래프 보기를 원하지 않으면 그래프 영역에서 오른쪽 클릭을 하여Properties를 선택하여 스케일을 변경해야 할 것이다. 그런다음 Graph 탭으로 움직인다. 특정 카운터의 스케일을 변경하려면 Data 탭으로 간다.

카운터
불행히도 PerfMon은 Task Manager와 다른 용어를 사용한다. 표 1은 가장 유용한 카운터를 요약한 것이다:


표 1. 유용한 PerfMon 메모리 카운터

카운터 이름 범주 설명 Task Manager equivalent
Working Set Process Resident set - 실제 메모리에 얼마나 많은 페이지들이 있는가? Mem Usage
Private Bytes Process 메모리를 나타내는 할당된 총 프라이빗 가상 메모리 VM Size
Virtual Bytes Process 공유된 페이지를 포함하여 가상 주소 공간의 총 크기. Reserved 메모리를 포함하기 때문에 이전 두 개의 값들 보다 훨씬 더 클 수 있다. --
Page Faults / sec Process 초당 발생했던 페이지 오류의 평균 수 Page Faults로 링크됨
Committed Bytes Memory "committed" 상태에서 가상 바이트의 총 수 --

예제

C로 작성한 작은 프로그램을 다운로드 및 실행하면 Task Manager와 PerfMon에서 이러한 양(quantity)이 어떻게 나타나는지 알 수 있다. (Download섹션 참조) 이 프로그램은 WindowsVirtualAlloc호출을 사용하여 우선 메모리를 보유(reserve)한 다음 커밋한다. 마지막으로 메모리 일부를 관여하기 시작하고 매 4,096 바이트마다 값을 작성하면서 페이지를 작업 세트로 가져간다. 프로그램을 실행하고 Task Manager 또는 PerfMon을 사용하여 이를 본다면 값 변경 사항을 볼 수 있다.

 

 



웹에서 유용한 툴

애플리케이션이 얼마나 많은 메모리를 사용하고 있는지 알았다면 이제는 실제 메모리 내용을 상세하게 볼 차례이다. 이 섹션에서는 좀더 세련된 툴을 소개하도록 하겠다. 적절한 사용 시점과 아웃풋을 인터프리팅하는 방법을 설명하겠다.

PrcView

우선 PrcView 툴을 설명하겠다. (참고자료) 많은 작업에 이 툴이 사용된다. 속성을 설정하고 프로세스를 죽일 수 있고, 머신 상에 프로세스의 속성을 나열하는 유용한 명령행 버전으로 존재한다. 풋프린트에서 보기위해 이를 어떻게 사용하는지 설명하겠다.

PrcView를 시작하면 Task Manager 같이 생긴 프로세스가 시스템에 나타난다. 스크롤을 이용하면 자바 프로세스가 하이라이트 되면 스크린은 그림 5 처럼 보인다.


그림 5. 초기 PrcView 스크린

 

자바 프로세스에서 오른쪽을 클릭하여 팝업 메뉴를 띄우거나 상단 메뉴 바에서Process를 선택하면 프로세스에 관한 몇 가지 사항들을 검사할 수 있다. 프로세스가 갖고 있는 쓰레드와 로딩한 DLL 등이 나타난다. 그리고 이것을 죽이거나 속성을 설정할 수도 있다. 우리가 관심 있는 옵션은 메모리 검사이다. 그림 6을 참조하라.


그림 6. 프로세스 메모리 검사

 

PrcView가 디스플레이하는 주소-공간 지도가 나온다. 첫 번째 줄은 주소 0 부터 이고 길이는 65,536 (64K)이며 메모리는 Free 이다. 어떤 것도 할당된 것은 없고 주소도 사용 될 수 없다. 그 다음에 바로 시작한 두 번째 라인은 주소는 0x00010000이고 길이는 커밋 된 메모리의 8,192-byte(두 개의 4K 페이지) 이다. 메모리는 어드레스 될 수 있고 페이징 파일에서 페이징 프레임의 지원을 받는다. 또 다른 free 확장 그런다음 커밋 된 확장 등으로 이어진다.

이 주소 공간의 영역 중 어떤 것도 당신에겐 의미가 없다. Windows에 의해 사용되기 때문이다. Windows 주소 공간을 설명하고 있는 Microsoft 문서는 이들은 MS-DOS 호환성을 위해 보유된 다양한 영역들이고 사용자 데이터와 코드용 영역은 4MB 에서 시작함을 언급하고 있다. (참고자료)

스크롤을 아래로 내려보면 확실히 인식할 수 있는 주소 공간에서 무엇인가에 다다르게 된다. (그림 7)


그림 7. 자바 힙의 값

 

그림 7에서, 강조된 라인과 그 바로 밑에 것은 자바 힙에 상응한다. 여기에서 시작했던 자바 프로세스는 1000MB 힙 (-mx1000m사용)이 주어졌다. 이는 해당 프로그램에 있어 엄청나게 큰 것으로서 PrcView 맵에서 확실히 나타나있다. 강조된 라인은 힙의 커밋된 부분을 단 4MB로 보여주고 있으며 주소 0x10180000에서 시작한다. 강조된 라인 바로 밑에는 큰 reserved 확장인데 이것은 아직 커밋되지 않은 나머지 힙이다. 시작 시 JVM은 초기에 총 1000MB를 보유하고 있다.(0x10180000 에서 0x4e980000 까지의 주소범위를 사용 불가로 만든다.) 그리고 시작하는데 필요한 것을 커밋한다. 이 경우 4MB 이다. 이 값이 현재 힙 사이즈에 상응한다는 것을 확인하려면-verbosegcJVM 옵션으로 자바 프로그램을 호출한다. 그러면 가비지 컬렉터에서 자세한 정보가 프린트 된다.-verbosegc아웃풋에 있는 두 번째 라인의 두 번째 GC 에서 현재 힙 사이즈가 대략 4GB라는 것을 볼 수 있다:

 

>
java - mx1000m - verbosegc Hello[JVMST080: verbosegc is enabled][JVMST082: -verbose: gc
        output will be written to stderr
    ] < GC[0]: Expanded System Heap by 65536 bytes <
    GC(1): GC cycle started Wed Sep 29 16: 55: 44 2004 < GC(1): freed 417928 bytes,
    72 % free(3057160 / 4192768), in 104 ms > < GC(1): mark: 2 ms, sweep: 0 ms, compact: 102 ms >
    <
    GC(1): refs: soft 0(age >= 32), weak 0, final 2, phantom 0 > < GC(1): moved 8205 objects,
    642080 bytes, reason = 4 >

 

-verbosegc아웃풋 포맷은 우리가 사용하는 JVM 포맷에 의존한다. (참고자료)

활성 데이터의 양이 늘어나고 JVM이 4GB 이상으로 힙을 확장해야 하는 경우 RESERVED 영역 그 이상으로 커밋한다. 새로운 영역은 0x10580000에서 시작한다는 것을 의미하며 이미 커밋 된 힙 메모리와 연결된다는 것을 의미한다.

그림 7의 PrcView 스크린의 마지막 세 줄은 프로세스의 총 커밋 메모리를 보여주고 있다. 7 번째 칼럼(Type)에서 총합이 계산된다:

  • Private: 페이징 파일에서 지원되는 커밋된 메모리
  • Mapped: 페이징 파일에서 지원되는 커밋된 메모리
  • Image: 실행 파일 코드(시작 실행 파일과 DLL)에 속해있는 커밋된 메모리

지금 까지, 크기에 기반하여 주소 공간 안에 힙을 배치했다. 메모리의 다른 영역이 어떤 것이 있는지를 이해하려면 메모리 안을 자세히 들여다 보는 것이 필요하다.

TopToBottom

TopToBottom은 smidgeonsoft.com에서 무료로 사용할 수 있다. (참고자료) 해당 문서는 없지만 현재 실행하는 프로세스를 포괄적으로 볼 수 있다. 이름과 프로세스 ID 뿐만 아니라 시작 시간 별로 프로세스를 소팅할 수 있다. 때문에 프로그램이 시작한 순서를 이해할 때 유용하다.

그림 8은 생성 시간에 따라 소팅한 프로세스 리스트로 TopToBottom을 보여주고 있다. (View > Sort > Creation Time)


그림 8. 생성 시간 별로 소팅한 TopToBottom 프로세스

 

StartUp 탭은 자바 프로세스를 생성했던 프로세스, 이것이 시작한 시간과 날짜, 이를 호출 할 때 사용한 실제 명령행, 실행 파일과 현재 디렉토리로의 전체 경로를 디스플레이하고 있다. Environment 탭을 클릭하면 시작 시 프로세스로 전달되었던 모든 환경 변수의 값을 디스플레이한다. Modules 탭은 자바 프로세스가 사용중인 DLL을 보여주고 있다.(그림 9)


그림 9. TopToBottom Modules 탭

 

다시, 다양한 방법으로 이 리스트를 소팅할 수 있다. 그림 9에서 초기화 순서 별로 소팅되고 있다. 열을 더블 클릭하면 DLL에 대한 자세한 정보를 볼 수 있다: 주소와 크기, 작성 날짜와 시간, 의존하고 있는 다른 DLL 리스트, DLL을 로딩했던 모든 실행 프로세스 리스트. 리스트를 검토하면 NTDLL.DLL 같은 DLL을 볼 수 있는데 이는 모든 실행 프로세스에 필요한 것이다; JVM.DLL 같은 DLL은 모든 자바 프로세스들간 공유된다; 다른 것들은 하나의 프로세스에 의해 사용된다.

개별 DLL의 크기를 추가하여 프로세스가 사용하고 있는 DLL의 전체 크기를 알아낼 수 있다. 하지만 결과 숫자는 프로세스가 그 모든 프로세스를 소비하고 잇다는 것을 의미하는 것이 아니기 때문에 오도될 수 있다. 진짜 크기는 프로세스가 실제로 사용하고 있는 DLL의 일부에 의존한다. 그러한 부분은 프로세스의 작업 세트에 의존한다. 이는 분명해 보이지만 DLL은 읽기 전용이고 공유 된다. 많은 프로세스들이 모두 주어진 DLL을 사용한다면 단 한 세트의 실제 메모리 페이지만이 어떤 한 시간에 DLL을 보유하게 된다. Task Manager 같은 툴들은 작업 세트를 공유된 페이지들과 공유되지 않은 페이지들의 총합으로 보고하여 풋프린트에서 DLL이 사용의 실제 효과를 결정하기 힘들다. 모듈 정보는 DLL 때문에 기인한 풋프린트의 "최악의 경우"를 볼 수 있는 유용한 방법이다. 이것은 필요할 경우 다른 툴들을 사용하여 자세한 분석이 이루어져 다듬어진다.

메모리 풋프린트에 관심이 있으면 Memory 탭을 클릭한다. 그림 10은 자바 프로그램이 사용하고 있는 모든 메모리의 하위 세트이다.


그림 10. TopToBottom Memory 탭

 

PrcView와 비슷하다. 하지만 가상 주소 공간의 커밋된 메모리만 보여주고 있다. 보유된 메모리는 없다. 하지만 두 개의 장점이 있다. 하나는, 페이지들을 좀더 자세하게 특징지을 수 있다. 예를 들어 그림 10에서 Thread 3760 스택 영역을 그저 읽기/쓰기 데이터가 아닌 구체적으로 구분했다. 이것이 인식하는 추가 데이터 영역들에는 Environment, Process Parameters, Process Heap, Thread Stack, Thread Environment Block(TEB)이 포함된다. 두 번째, TopToBottom에서 직접 메모리를 검색할 수 있다. 텍스트 스트링을 검색하거나 16 바이트까지 시퀀스의 16진수 검색을 할 수 있다. 16진수 검색을 특정 정렬까지 제한할 수 있는데 이는 주소에 대한 레퍼런스를 검색할 때 유용하다.

TopToBottom은 또한 스냅샷 장치가 있어서 프로세스에 대한 모든 정보를 클립보드에 덤핑할 수 있다.

VADump

VADump는 편리한 명령행 툴로서 패키지의 일부이다. (참고자료) 가상 주소 공간 개요와 특정 프로세스의 상주 세트를 덤핑하는 용도이다. VADump를 사용하는 가장 간단한 방법은 명령행에 다음 명령어를 입력한다:

 

vadumpprocess_id

 

process_id는 당신이 관심을 갖고 있는 프로세스의 수이다. 인자 없이 VADump를 호출하여 전체 사용 정보를 디스플레이 할 수 있다. 아웃풋을 파일에 파이프(pipe) 할 것을 권장한다. (예를 들어,vadump 1234 > output.txt)VADump는 스크린이 감당하기엔 너무 많은 정보를 만들기 때문이다.

아웃풋은 인덱스를 프로세스를 위한 가상 주소 공간으로 보여주는 것으로 시작한다:

 

>
vadump - p 3904 Address: 00000000 Size: 00010000
State FreeAddress: 00010000 Size: 00002000
State Committed Protect Read / Write Type PrivateAddress: 00012000 Size: 0000E000
State FreeAddress: 00020000 Size: 00001000 State Committed Protect Read / Write
Type PrivateAddress: 00021000 Size: 0000 F000
State FreeAddress: 00030000 Size: 00010000
State Committed Protect Read / Write Type PrivateAddress: 00040000
Size: 0003 B000 RegionSize: 40000 State Reserved
Type Private................................

 

 

(읽기 쉽도록 아웃풋 내용을 잘랐다.)

각 블록 마다 다음 정보를 볼 수 있다:

  • 주소(Address): 16진법 포맷. 프로세스의 가상 주소 공간의 시작과 관련
  • 크기(Size): 바이트 단위. 16 진법
  • 상태(State): Free, reserved, or committed
  • 보호 상태(Protection status): 읽기 전용 또는 읽기/쓰기
  • 유형(Type): private (다른 프로세스가 접근할 수 없음), mapped (파일 시스템에서 직접), image (실행 파일 코드)

그런 다음 사이즈 포함하여 프로세스에 의해 사용중인 모든 DLL 리스트를 보여주고 그 다음 작업 세트와 페이지 파일 사용에 대한 정보 요약이 나온다.

지금 까지, 이 정보는 다른 툴로도 사용할 수 있다. VADump의-o옵션을 사용하면 더 많은 아웃풋을 만들 수 있다. 현재 작업 세트의 스냅샷을 만들어 낸다. (이 페이지는 실제로 정해진 시간에 주어진 지점에 주 메모리에 있는 페이지이다.) 이 옵션의 문서화는 형편없지만 상주 세트에서 가장 중요한 컴포넌트가 무엇인지 파악하는데 매우 유용하다. 따라서 메모리 최적화의 강력한 후보이다. 메모리 유출을 확인할 때도 사용할 수 있다. 규칙적인 간격으로 스냅샷을 찍는다. 이 모드에서 아웃풋은 가상 주소 공간에서 커밋 페이지의 자세한 덤프로 시작한다. 현재 주 메모리에 있는지의 여부는 상관없다:

 

>
vadump - o - p 39040 x00010000(0) PRIVATE Base 0x000100000 x00011000(0)
PRIVATE Base 0x000100000 x00020000(0) PRIVATE Base 0x000200000 x00030000(0)
PRIVATE Base 0x000300000 x00031000(0) Private Heap 20 x00032000(0)
Private Heap 20 x00033000(0) Private Heap 20 x00034000(0)
Private Heap 20 x00035000(0) Private Heap 20 x00036000(0)
Private Heap 20 x00037000(0) Private Heap 20 x00038000(0)
Private Heap 20 x00039000(0) Private Heap 20 x0003A000(0)
Private Heap 20 x0003B000(0) Private Heap 20 x0003C000(0)
Private Heap 20 x0003D000(0) Private Heap 20 x0003E000(0)
Private Heap 20 x0003F000(0) Private Heap 20 x0007C000(0)
Stack
for ThreadID 00000 F640x0007D000(0) Stack
for ThreadID 00000 F640x0007E000(0)
Stack
for ThreadID 00000 F640x0007F000(0) Stack
for ThreadID 00000 F640x00080000(7)
UNKNOWN_MAPPED Base 0x000800000 x00090000(0) PRIVATE Base 0x000900000 x00091000(0)
Process Heap0x00092000(0) Process Heap0x00093000(0)
Process Heap...........................

 

긴 목록의 끝까지 스크롤을 내리면 좀더 자세한 정보가 나온다: 주 메모리에 현재 상주하고 있는 프로세스의 페이지용 페이지-테이블 매핑 리스트이다:

 

0xC0000000 > (0x00000000 : 0x003FFFFF)   132 Resident Pages             
(0x00280000 : 0x00286000) > jsig.dll             
(0x00290000 : 0x00297000) > xhpi.dll             
(0x002A0000 : 0x002AF000) > hpi.dll             
(0x003C0000 : 0x003D8000) > java.dll             
(0x003E0000 : 0x003F7000) > core.dll             
(0x00090000 : 0x00190000) > Process Heap segment 0             
(0x00190000 : 0x001A0000) > Private Heap 0 segment 0             
(0x001A0000 : 0x001B0000) > UNKNOWN Heap 1 segment 0             
(0x00380000 : 0x00390000) > Process Heap segment 0             
(0x00030000 : 0x00040000) > Private Heap 2 segment 0             
(0x00390000 : 0x003A0000) > Private Heap 3 segment 0             
(0x00040000 : 0x00080000) > Stack for thread 00xC0001000 > (0x00400000 : 0x007FFFFF)   
13 Resident Pages             
(0x00400000 : 0x00409000) > java.exe.........................
........................................

 

 

이들 매핑 각각은 페이지 테이블의 싱글 엔트리에 상응한다. 따라서 추가 4KB를 이 프로세스용 작업 세트에 기여한다. 이 매핑으로 애플리케이션의 어떤 부분이 가장 많은 메모리를 사용하고 있는지 파악하기 어렵지만 다음 아웃풋에 유용한 요약이 있다:

 

Category                   Total       Private  Shareable  Shared                       
Pages   KBytes   KBytes   KBytes    KBytes  Page Table Pages        
20       80       80       0       0  Other System            
10       40       40       0       0  Code/StaticData       
1539     6156     3988    1200     968  Heap                  
 732     2928     2928       0       0  Stack                   
 9       36       36       0       0  Teb                      
5       20       20       0       0  Mapped Data             
30      120        0       0     120  Other Data            
1314     5256     5252       4       0  Total Modules         
1539     6156     3988    1200     968  Total Dynamic Data    
2090     8360     8236       4     120  Total System            
30      120      120       0       0Grand Total Working Set 3659    
14636    12344    1204    1088

 

 

두 개의 가장 흥미로운 값들은 일반적으로 Heap이다. 이것은 Windows 프로세스 힙과 Other Data이다. Windows API 호출을 통해 직접 할당된 메모리는 프로세스 힙의 일부를 형성하고 Other Data에는 자바 힙이 포함된다. Grand Total Working Set는 Task Manager의 Mem Usage와 Tab 필드와 코릴레이션 한다. 이것은 내부 Windows 구조인 프로세스의 Thread Environment Block에 필요한 메모리이다.

마지막으로VADump -o아웃풋 밑에는 DLL, 힙, 쓰레드 스택에서 작업 세트로의 기여를 요약된다:

 

Module Working Set Contributions in pages    Total   Private Shareable    
Shared Module        9         2         7         0 
java.exe       85         5         0        80 
ntdll.dll       43         2         0        41 
kernel32.dll       15         2         0        13 
ADVAPI32.dll       11         2         0         9 
RPCRT4.dll       53         6         0        47 
MSVCRT.dll      253        31       222         0 
jvm.dll        6         3         3         0 
jsig.dll        7         4         3         0 
xhpi.dll       15        12         3         0 
hpi.dll       12         2         0        10 
WINMM.dll       21         2         0        19 
USER32.dll       14         2         0        12 
GDI32.dll        6         2         0         4 
LPK.DLL       10         3         0         7 
USP10.dll       24        18         6         0 
java.dll       22        16         6         0 
core.dll       18        14         4         0 
zip.dll      915       869        46         0 
jitc.dllHeap Working Set Contributions   6 
pages from Process Heap (class 0x00000000) 0x00090000 - 0x00190000 6 pages   
2 pages from Private Heap 0 (class 0x00001000) 0x00190000 - 0x001A0000 2 pages   
0 pages from UNKNOWN Heap 1 (class 0x00008000) 0x001A0000 - 0x001B0000 0 pages   1 
pages from Process Heap (class 0x00000000) 0x00380000 - 0x00390000 1 pages 715 
pages from Private Heap 2 (class 0x00001000) 0x00030000 - 0x00040000 15 
pages 0x008A0000 - 0x009A0000 241 pages 0x04A60000 - 0x04C60000 450 
pages 0x054E0000 - 0x058E0000 9 pages   1 pages from Private Heap 3 (class 0x00001000) 
0x00390000 - 0x003A0000 1 pages   7 pages from Private Heap 4 
(class 0x00001000) 0x051A0000 - 0x051B0000 7 pagesStack Working Set Contributions   4 pages 
from stack for thread 00000F64   1 pages from stack for thread 00000F68   1 pages from 
stack for thread 00000F78   1 pages from stack for thread 00000F7C   2 pages from stack for thread 00000EB0

 

 

이 모드에서 VADump를 사용하여 두 개 이상의 자바 프로세스의 결합 풋프린트에 대한 정확한 뷰를 얻을 수 있다. (섹션 참조)

Sysinternals Process Explorer

그러나 메모리 분석을 위한 좀더 유용한 툴들은 Sysinternals에서 제공한다. (참고자료) 이중 하나는 그래픽 프로세스 익스플로러(그림 11)이다. Task Manager의 훌륭한 대체품이다.


그림 11. Process Explorer 프로세스 트리

 

 

Process Explorer는 Task Manager와 기능이 모두 같다. 예를 들어, 전체 시스템 퍼포먼스에 대한 동적 그래프를 볼 수 있다. (View > System Information...)그리고 비슷한 방식으로 주 프로세스 뷰에 있는 칼럼들을 설정할 수 있다.Process > Properties...이하에서, Process Explorer는 이 프로세스에 대한 자세한 정보를 제공한다. 전체 경로와 명령행, 쓰레드, CPU 사용에 대한 동적 그래프, 프라이빗 바이트 등이 그것이다. 사용자 인터페이스는 그림 11에서 보듯 우수하다. DLL 상의 정보를 검색할 수 있고 프로세스를 위해 한들 할 수 있다.Options > Replace Task Manager를 사용하여 Task Manager 대신 Process Explorer를 실행할 수 있다.

Sysinternals ListDLLs

Sysinternals 명령행 유틸리티인 ListDLLs와 Handle을 다운로드 할 수 있다. 메모리 모니터링 형식을 스크립트나 프로그램으로 결합할 때 특히 유용하다.

ListDLLs로 DLLs를 볼 수 있다. 이것은 메모리 풋프린트에 중대한 기여를 한다. 이것을 사용하려면 경로에 이것을 추가하여 help 옵션으로 이것을 호출하여 사용 정보를 얻는다. 프로세스 ID와 이름으로 호출할 수도 있다. 다음은 자바 프로그램이 사용하는 DLL 리스트이다:

 

>
listdlls - r 3904 ListDLLs V2 .23 - DLL lister
for Win9x / NTCopyright(C) 1997 - 2000
Mark Russinovichhttp: //www.sysinternals.com------------------------------------
    -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - java.exe pid: 3904 Command line: java - mx1000m - verbosegc
HelloBase Size Version Path0x00400000 0x9000 141.2003 .0005 .0022
C: \WINDOWS\ system32\ java.exe0x77f50000 0xa7000 5.01 .2600 .1217
C: \WINDOWS\ System32\ ntdll.dll0x77e60000 0xe6000 5.01 .2600 .1106
C: \WINDOWS\ system32\ kernel32.dll0x77dd0000 0x8d000 5.01 .2600 .1106
C: \WINDOWS\ system32\ ADVAPI32.dll0x78000000 0x87000 5.01 .2600 .1361
C: \WINDOWS\ system32\ RPCRT4.dll0x77c10000 0x53000 7.00 .2600 .1106
C: \WINDOWS\ system32\ MSVCRT.dll0x10000000 0x178000 141.2004 .0003 .0001
C: \Java141\ jre\ bin\ jvm.dll # # # Relocated from base of 0x10000000: 0x00280000 0x6000
141.2004 .0003 .0001 C: \Java141\ jre\ bin\ jsig.dll # # # Relocated from base of
    0x10000000: 0x002900000 x7000 141.2004 .0003 .0001 C: \Java141\ jre\ bin\ xhpi.dll # # #
Relocated from base of 0x10000000: 0x002a0000 0xf000 141.2004 .0003 .0001
C: \Java141\ jre\ bin\ hpi.dll0x76b40000 0x2c000 5.01 .2600 .1106 C: \WINDOWS\ system32\ WINMM.dll0x77d40000
0x8c000 5.01 .2600 .1255 C: \WINDOWS\ system32\ USER32.dll0x7e090000 0x41000 5.01 .2600 .1346
C: \WINDOWS\ system32\ GDI32.dll0x629c0000 0x8000 5.01 .2600 .1126 C: \WINDOWS\ system32\ LPK.DLL0x72fa0000
0x5a000 1.409 .2600 .1106 C: \WINDOWS\ system32\ USP10.dll
# # # Relocated from base of 0x10000000: 0x003c0000 0x18000
141.2004 .0003 .0001 C: \Java141\ jre\ bin\ java.dll # # # Relocated from base of 0x10000000: 0x003e0000 0x17000
141.2004 .0003 .0001 C: \Java141\ jre\ bin\ core.dll # # # Relocated from base of 0x10000000: 0x04a40000 0x12000
141.2004 .0003 .0001 C: \Java141\ jre\ bin\ zip.dll # # # Relocated from base of 0x10000000: 0x04df0000 0x3a1000
141.2004 .0003 .0001 C: \Java141\ jre\ bin\ jitc.dll

 

 

또는listdlls -r java명령어를 사용하면 실행중인 모든 자바 프로세스와 이들이 사용하고 있는 DLL을 보여준다.

Sysinternals Handle

Handle은 프로세스가 사용하고 있는 모든 (파일과 소켓에 대한) Handle을 보여준다. Handle 다운로드 파일의 압축을 풀어 이를 경로에 추가한다. 자바 프로그램상에서 실행하면 다음과 같은 아웃풋을 만들어 낸다:

 

>
handle - p 3904 Handle v2 .2 Copyright(C) 1997 - 2004 Mark RussinovichSysinternals -
    www.sysinternals.com-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    -- -- -- --java.exe pid: 3904 99 VXW67\ cem c: File
C: \wsappdev51\ workspace\ Scratch 4 c: File
C: \wsappdev51\ workspace\ Scratch\ verbosegc.out 50: File
C: \wsappdev51\ workspace\ Scratch\ verbosegc.out 728: File
C: \WebSphere MQ\ Java\ lib\ com.ibm.mq.jar 72 c: File
C: \WebSphere MQ\ Java\ lib\ fscontext.jar 730: File
C: \WebSphere MQ\ Java\ lib\ connector.jar 734: File
C: \WebSphere MQ\ Java\ lib\ jms.jar 738: File
C: \WebSphere MQ\ Java\ lib\ jndi.jar 73 c: File
C: \WebSphere MQ\ Java\ lib\ jta.jar 740: File
C: \WebSphere MQ\ Java\ lib\ ldap.jar 744: File
C: \WebSphere MQ\ Java\ lib\ com.ibm.mqjms.jar 748: File
C: \WebSphere MQ\ Java\ lib\ providerutil.jar 74 c: File
C: \Java141\ jre\ lib\ ext\ oldcertpath.jar 750: File
C: \Java141\ jre\ lib\ ext\ ldapsec.jar 754: File
C: \Java141\ jre\ lib\ ext\ JawBridge.jar 758: File
C: \Java141\ jre\ lib\ ext\ jaccess.jar 75 c: File
C: \Java141\ jre\ lib\ ext\ indicim.jar 760: File
C: \Java141\ jre\ lib\ ext\ ibmjceprovider.jar 764: File
C: \Java141\ jre\ lib\ ext\ ibmjcefips.jar 768: File
C: \Java141\ jre\ lib\ ext\ gskikm.jar 794: File
C: \Java141\ jre\ lib\ charsets.jar 798: File
C: \Java141\ jre\ lib\ xml.jar 79 c: File
C: \Java141\ jre\ lib\ server.jar 7 a0: File
C: \Java141\ jre\ lib\ ibmjssefips.jar 7 a4: File
C: \Java141\ jre\ lib\ security.jar 7 a8: File
C: \Java141\ jre\ lib\ graphics.jar 7 ac: File
C: \Java141\ jre\ lib\ core.jar

 

이 프로세스가 클래스 경로상의 디렉토리와 여러 JAR 파일에 대한 handle을 갖고 있다는 것을 알 수 있다. 사실 프로세스는 훨씬 더 많은 handle을 갖고 있다. 하지만 기본적으로 이 유틸리티는 파일을 참조하는 handle만 보여준다.-a매개변수를 사용하여 다른 것들도 디스플레이 할 수 있다zzzzz:

 

>
handle - a - p 3904 Handle v2 .2 Copyright(C) 1997 - 2004 Mark RussinovichSysinternals -
    www.sysinternals.com-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
    -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -
    java.exe pid: 3904 99 VXW67\ cem c: File
C: \wsappdev51\ workspace\ Scratch 4 c: File
C: \wsappdev51\ workspace\ Scratch\ verbosegc.out 50: File
C: \wsappdev51\ workspace\ Scratch\ verbosegc.out 71 c: Semaphore 720: Thread
java.exe(3904): 3760 724: Event 728: File
C: \WebSphere MQ\ Java\ lib\ com.ibm.mq.jar 72 c: File
C: \WebSphere MQ\ Java\ lib\ fscontext.jar 730: File
C: \WebSphere MQ\ Java\ lib\ connector.jar 734: File
C: \WebSphere MQ\ Java\ lib\ jms.jar 738: File
C: \WebSphere MQ\ Java\ lib\ jndi.jar 73 c: File
C: \WebSphere MQ\ Java\ lib\ jta.jar 740: File
C: \WebSphere MQ\ Java\ lib\ ldap.jar 744: File
C: \WebSphere MQ\ Java\ lib\ com.ibm.mqjms.jar 748: File
C: \WebSphere MQ\ Java\ lib\ providerutil.jar 74 c: File
C: \Java141\ jre\ lib\ ext\ oldcertpath.jar 750: File
C: \Java141\ jre\ lib\ ext\ ldapsec.jar 754: File
C: \Java141\ jre\ lib\ ext\ JawBridge.jar 758: File
C: \Java141\ jre\ lib\ ext\ jaccess.jar 75 c: File
C: \Java141\ jre\ lib\ ext\ indicim.jar 760: File
C: \Java141\ jre\ lib\ ext\ ibmjceprovider.jar 764: File
C: \Java141\ jre\ lib\ ext\ ibmjcefips.jar 768: File
C: \Java141\ jre\ lib\ ext\ gskikm.jar 76 c: Key
HKCU 770: Semaphore 774: Thread
java.exe(3904): 3964 778: Event 77 c: Semaphore 780: Semaphore 784: Thread
java.exe(3904): 3960 788: Event 78 c: Thread
java.exe(3904): 3944 790: Event 794: File
C: \Java141\ jre\ lib\ charsets.jar 798: File
C: \Java141\ jre\ lib\ xml.jar 79 c: File
C: \Java141\ jre\ lib\ server.jar 7 a0: File
C: \Java141\ jre\ lib\ ibmjssefips.jar 7 a4: File
C: \Java141\ jre\ lib\ security.jar 7 a8: File
C: \Java141\ jre\ lib\ graphics.jar 7 ac: File
C: \Java141\ jre\ lib\ core.jar 7 b0: Event 7 b4: Thread java.exe(3904): 3940
7 b8: Event 7 bc: Semaphore 7 c0: Directory\ BaseNamedObjects 7 c4: Key
HKLM\ SOFTWARE\ Windows NT\ Drivers32 7 c8: Semaphore 7 cc: Semaphore 7 d0:
    Event 7 d4: Desktop\ Default
7 d8: WindowStation\ Windows\ WindowStations\ WinSta0 7 dc:
    Event 7e0: WindowStation\ Windows\ WindowStations\ WinSta0
7e4: Event 7e8: Section 7 ec:
    Port 7 f0: Directory\ Windows 7 f4: Key
HKLM 7 f8: Directory\ KnownDlls
7 fc: KeyedEvent\ KernelObjects\ CritSecOutOfMemoryEvent

 

메모리에 관심을 갖고 있다면 각 handle이 특정 공간을 소비하기 때문에 handle은 중요하다. 정확한 양은 운영 체계 버전과 handle의 유형에 따라 다르다. 일반적으로 handle은 풋프린트에 중요한 영향을 끼치지 않는다. 이 유틸리티가 만들어내는 라인의 수를 세기만 해도 handle의 수가 중요하거나 늘어나고 있다는 것을 빠르게 볼 수 있다.

 

 

여기에서 제시한 모든 툴을 다루는데 자신이 생겼으리라 믿고 여기에 메모리 모니터링을 향상시킬 유용한 방법을 소개하겠다.

프로세스 ID 찾기

애플리케이션의 프로세스 ID를 찾아서 VADump 같은 명령행 툴에서 사용하려면 Task Manager의 Applications 탭을 열어 관심 있는 프로세스를 더블 클릭한다.Go To Process를 선택한다. Processes 탭에서 ID를 볼 수 있다.

자바 프로세스 구분하기

java 또는 javaw 이름이 붙은 모든 프로세스 리스트가 헷갈린다면 어떤 것을 검사하고 싶은지 파악하려고 하는가? 자바 프로세스가 IDE 또는 스크립트에서 시작되었다면 어떤 JVM이 사용 중이고 어떤 명령행 매개변수가 자바 프로세스로 보내지는지 결정하기 힘들다. 이 정보는 TopToBottom Startup 탭에서 쉽게 사용할 수 있다. JVM을 호출하는데 사용되는 전체 명령행과 프로세스가 시작된 시간을 볼 수 있다.

handle hog 구분하기

다른 프로세스에 의해 사용 중이라는 정보를 얻도록 파일을 저장해 본 적이 있는가? 해당 프로그램을 닫을 때 에러 메시지가 여전히 뜨는 것을 경험했는가? SysInternals Process Explorer 툴의 Handle Search 장치를 사용하여 문제를 찾을 수 있다. Search 대화창을 열어 파일의 이름을 입력한다. ProcExp는 모든 개방 handle을 검색하고 프로세스를 구분한다. 종종 사용자 인터페이스를 닫은 후에 에디터나 웹 브라우저에 남아 실행되는 스텁 프로세스로 드러난다.

얼마나 많은 메모리가 공유되고 있는지 조사하기

VADump의-o옵션을 사용하여 프로세스의 현재 작업 세트에 무엇이 있고 얼마나 많이 공유되는지 파악할 수 있다. 시스템 상에서 실행중인 하나의 자바 프로그램 덤프를 가져다가 또 다른 것을 시작하고 다시 덤프를 갖는다. 각 요약에서 Code/StaticData 값을 비교하면 "Shareable" 바이트가 "Shared" 가 되어있음을 발견한다.

상주 세트 다듬기

Windows는 사용하지 않는 것으로 드러난 프로세스의 상주 세트를 다듬는(trimming)" 정책을 구현한다. Task Manager의 Processes 탭을 열어 검사하고 있는 애플리케이션의 프로세스를 볼 수 있다. 그런 다음 애플리케이션 윈도우를 최소화한다. Mem Usage 필드에 어떤 일이 발생하는지 관찰하라!

애플리케이션에 필요한 최소 메모리의 양 결정하기

Windows Server 2003과 Windows NT의 경우, Microsoft는 ClearMem이라고 하는 재미있는 유틸리티를 제공한다. Windows에서 앞으로 애플리케이션이 메모리를 더 사용할지 검사하고자 할 때 유용하다. (참고자료) 이 툴은 실제 메모리의 크기를 결정하고 소비할 충분한 메모리를 할당하여 할당된 메모리를 최대한 빨리 파악하여 릴리스한다. 이것은 다른 애플리케이션의 메모리 소비에 압력을 주고 여러 번 ClearMem을 실행할 때의 효과는 애플리케이션이 사용하고 있는 메모리의 양을 최소한의 값으로 떨어뜨린다.

 

 

결론

이 글에서 Windows가 메모리를 어떻게 관리하는지를 요약했고 자바 프로그램의 메모리 사용을 감시하는데 사용할 수 있는 무료 툴들을 소개했다. 웹이나 상용 오퍼링에서 무료로 다운로드 할 수 있다. 하지만 충분한 지식으로 무장하기 바란다. 무엇이 측정되고 있는지에 대해 정확히 알려면 꾸준힌 실험을 하는 것 뿐이다.

물론, 이러한 툴들은 문제를 확인할 때만 도움이 된다. 이를 해결하는 것은 여러분의 몫이다. 자바 힙이 메모리의 가장 큰 부분을 차지하고 있다는 것을 알게 될 것이다. 코드를 자세히 검토하여 객체 레퍼런스가 필요한 것 이상을 보유하고 잇는지 확인해야 한다. 많은 유용한 툴들과 자료들을참고자료섹션에서 참조하기 바란다.

 

 

다운로드 하십시오

설명이름크기다운로드 방식

A C program to demonstrate how Windows uses memory experiment.c 3KB  FTP|HTTP|Download Director
 
  다운로드 방식에 대한 정보     Get Adobe® Reader®

 

 

참고자료

 

=================================

=================================

=================================

 

 

반응형


관련글 더보기

댓글 영역