본문 바로가기
Development/Android

[Android] Android GC_CONCURRENT FREED 에러 해결 방법과 메모리 최적화

by 은스타 2020. 4. 8.
반응형
Android GC_CONCURRENT FREED 에러 해결 방법과 메모리 최적화

Android GC_CONCURRENT FREED 에러 해결 방법과 메모리 최적화

Android Studio에서 개발하다 보면 로그창에 GC_CONCURRENT FREED라는 메시지를 자주 보게 됩니다. 이 메시지를 보고 에러라고 생각하여 걱정하시는 초보 개발자분들이 많은데요, 사실 이것은 에러가 아니라 정상적인 메모리 관리 과정입니다. 이 글에서는 GC_CONCURRENT FREED 메시지의 정확한 의미, 가비지 컬렉션의 작동 원리, 그리고 메모리 최적화 방법까지 초보자도 쉽게 이해할 수 있도록 상세히 알아보겠습니다.

목차
1. GC_CONCURRENT FREED는 에러가 아닌 정상 메시지
2. 가비지 컬렉션과 메시지 상세 분석
3. 메모리 문제 디버깅과 최적화 방법
4. 실전 메모리 사용 최적화 코드
5. 자주 묻는 질문과 주의사항

#1. GC_CONCURRENT FREED는 에러가 아닌 정상 메시지
가장 먼저 알아두어야 할 점은 GC_CONCURRENT FREED는 에러가 아니라는 것입니다. 이것은 단순히 Android 시스템에서 메모리 관리를 위해 가비지 컬렉션(Garbage Collection)이 실행되었다는 정보성 메시지입니다.
1) GC_CONCURRENT FREED 메시지 예시
일반적인 GC_CONCURRENT FREED 메시지의 형태는 다음과 같습니다:
D/dalvikvm: GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms
. . . . .
2) 가비지 컬렉션이란?
가비지 컬렉션(Garbage Collection)은 프로그램이 동적으로 할당했던 메모리 영역 중에서 더 이상 사용하지 않는 영역을 자동으로 탐지하고 해제하는 메모리 관리 기법입니다.
(1) 가비지 컬렉션이 필요한 이유
Android 앱은 Java나 Kotlin으로 작성되며, 이러한 언어들은 개발자가 직접 메모리를 할당하고 해제하지 않습니다. 대신 가비지 컬렉터(Garbage Collector)가 주기적으로 실행되어 더 이상 사용되지 않는 객체들을 찾아 메모리에서 제거합니다.
(2) 가비지 컬렉션의 3단계 작동 방식
표시(Mark) - 사용 중인 객체를 식별하고 표시합니다
청소(Sweep) - 표시되지 않은 객체(더 이상 참조되지 않는 객체)를 제거합니다
압축(Compact) - 남은 객체들을 메모리 공간에 재배치하여 메모리 단편화를 줄입니다
. . . . .
3) Android의 가비지 컬렉션 유형
Android에서는 여러 유형의 가비지 컬렉션이 있으며, 로그 메시지로 확인할 수 있습니다:
가비지 컬렉션 유형 설명
GC_CONCURRENT 앱 실행과 동시에 발생하는 가비지 컬렉션으로, 앱의 실행을 최소한으로 방해합니다
GC_FOR_MALLOC 메모리 할당 요청을 처리하기 위해 발생하는 가비지 컬렉션입니다
GC_EXPLICIT System.gc()와 같은 명시적인 호출로 발생하는 가비지 컬렉션입니다
GC_EXTERNAL_ALLOC 외부 메모리 할당을 위한 가비지 컬렉션입니다 (Android 2.3 이전 버전)
GC_HPROF_DUMP_HEAP 힙 덤프를 생성하기 위한 가비지 컬렉션입니다

#2. 가비지 컬렉션과 메시지 상세 분석
GC_CONCURRENT FREED 메시지의 각 항목이 무엇을 의미하는지 자세히 분석해보겠습니다.
1) 메시지 구조 상세 분석
D/dalvikvm: GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms
(1) 각 항목의 의미
D/dalvikvm - Debug 로그 태그로, Dalvik 가상 머신(Android 런타임 환경)에서 발생한 로그임을 나타냅니다
GC_CONCURRENT - 가비지 컬렉션 유형을 나타냅니다. CONCURRENT는 앱 실행과 동시에 가비지 컬렉션이 수행되었음을 의미합니다
freed 2049K - 가비지 컬렉션을 통해 확보(해제)된 메모리 양이 2049KB임을 나타냅니다
65% free 3571K/9991K - 힙(Heap) 메모리의 사용 상태를 보여줍니다. 전체 9991KB 중 3571KB가 사용 가능한 상태(free)이며, 이는 전체의 65%에 해당합니다
external 4703K/5261K - 네이티브 힙에 할당된 메모리 상태를 나타냅니다. 전체 5261KB 중 4703KB가 사용 중입니다
paused 2ms+2ms - 가비지 컬렉션으로 인해 앱 실행이 일시 중지된 시간을 나타냅니다. 첫 번째 2ms는 표시(Mark) 단계, 두 번째 2ms는 청소(Sweep) 단계에서 소요된 시간입니다
. . . . .
2) 가비지 컬렉션이 자주 발생하는 경우
GC_CONCURRENT FREED 메시지 자체는 에러가 아니지만, 이러한 메시지가 매우 자주 발생한다면 메모리 사용에 문제가 있을 수 있습니다.
(1) 주요 원인 3가지
메모리 누수(Memory Leak) - 더 이상 사용하지 않는 객체에 대한 참조가 유지되어 가비지 컬렉터가 해당 객체를 수거하지 못하는 상황
과도한 객체 생성 - 짧은 시간에 많은 임시 객체를 생성하는 경우
큰 이미지나 데이터 처리 - 대용량 이미지나 데이터를 효율적으로 처리하지 않는 경우
. . . . .
3) 가비지 컬렉션의 영향과 성능
가비지 컬렉션은 메모리 관리에 중요하지만, 앱 성능에 영향을 줄 수 있습니다:
(1) 성능 영향 요소
앱 일시 중지 - 가비지 컬렉션이 실행될 때 앱이 잠시 멈출 수 있습니다. 특히 메모리 사용량이 많은 경우 이러한 일시 중지가 눈에 띄게 됩니다
배터리 소모 - 잦은 가비지 컬렉션은 CPU 사용량을 증가시켜 배터리 소모를 높입니다
UI 지연 - 메인 스레드에서 가비지 컬렉션이 발생하면 UI 렌더링이 지연될 수 있습니다
(2) 주의가 필요한 상황
매우 짧은 간격으로 계속해서 발생하는 경우
'paused' 시간이 10ms 이상으로 길어지는 경우
③ 앱이 느려지거나 프레임이 떨어지는 현상과 함께 발생하는 경우

#3. 메모리 문제 디버깅과 최적화 방법
Android 앱의 메모리 사용을 확인하고 문제를 해결하기 위한 구체적인 방법을 알아보겠습니다.
1) Android Studio Profiler 사용하기
Android Studio의 Profiler 도구는 앱의 메모리, CPU, 네트워크 사용량을 실시간으로 모니터링할 수 있게 해줍니다.
(1) 사용 방법
① Android Studio에서 앱을 실행합니다
② 하단의 'Profiler' 탭을 클릭합니다
③ 'MEMORY' 섹션을 선택하여 메모리 사용량을 모니터링합니다
④ 메모리 그래프에서 급격한 증가나 감소를 확인합니다
. . . . .
2) LeakCanary 라이브러리 사용하기
LeakCanary는 메모리 누수를 감지하고 알려주는 인기 있는 오픈 소스 라이브러리입니다.
// 앱의 build.gradle 파일에 추가
dependencies {
    // 디버그 빌드에만 LeakCanary 추가
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}
. . . . .
3) 덤프 힙(Dump Heap) 분석하기
메모리 사용량이 비정상적으로 높을 때 힙 덤프를 생성하고 분석할 수 있습니다:
(1) 힙 덤프 생성 방법
① Android Profiler에서 'Dump Java Heap' 버튼을 클릭합니다
② 생성된 힙 덤프 파일(.hprof)을 분석하여 어떤 객체가 많은 메모리를 차지하는지 확인합니다
③ MAT(Memory Analyzer Tool)를 사용하여 심화 분석을 수행합니다

#4. 실전 메모리 사용 최적화 코드
Adnroid 앱의 메모리 사용을 최적화하기 위한 실전 코드 예제를 소개합니다.
1) 컨텍스트 참조 관리하기
Activity나 Fragment 같은 컨텍스트를 강한 참조(Strong Reference)로 유지하면 메모리 누수가 발생할 수 있습니다.
(1) 잘못된 예시
// 잘못된 예시
public class MyManager {
    private Context context;

    public MyManager(Context context) {
        this.context = context; // Activity 컨텍스트를 강한 참조로 유지
    }
}
(2) 개선된 예시
// 개선된 예시 - WeakReference 사용
public class MyManager {
    private WeakReference<Context> contextRef;

    public MyManager(Context context) {
        this.contextRef = new WeakReference<>(context);
    }

    public void doSomething() {
        Context context = contextRef.get();
        if (context != null) {
            // 컨텍스트 사용
        }
    }
}
. . . . .
2) 이미지 효율적으로 로드하기
큰 이미지는 메모리를 많이 차지합니다. Glide나 Picasso 같은 이미지 로딩 라이브러리를 사용하면 이미지 로딩과 캐싱을 효율적으로 관리할 수 있습니다.
// build.gradle에 추가
implementation 'com.github.bumptech.glide:glide:4.12.0'

// Glide를 사용한 이미지 로딩
Glide.with(context)
    .load(imageUrl)
    .override(300, 300) // 적절한 크기로 리사이징
    .centerCrop()
    .into(imageView);
. . . . .
3) RecyclerView 뷰 재활용 최적화
RecyclerView에서 뷰 홀더 패턴을 제대로 구현하여 뷰 재활용을 최적화합니다.
// Kotlin으로 작성된 효율적인 RecyclerView.Adapter
class MyAdapter(private val items: List<Item>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // 새 뷰 생성은 필요할 때만
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_layout, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        // 기존 뷰 재활용
        holder.bind(items[position])
    }

    override fun getItemCount() = items.size

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(item: Item) {
            // 뷰 내용 업데이트
        }
    }
}
. . . . .
4) 불필요한 객체 생성 피하기
반복문 내에서 객체를 반복적으로 생성하는 것은 피해야 합니다.
(1) 잘못된 예시
// 잘못된 예시
for (int i = 0; i < 1000; i++) {
    String s = new String("객체 " + i); // 매 반복마다 새 객체 생성
    process(s);
}
(2) 개선된 예시
// 개선된 예시
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    builder.setLength(0); // 기존 객체 재사용
    builder.append("객체 ").append(i);
    process(builder.toString());
}
. . . . .
5) 메모리 캐시 관리하기
메모리 캐시는 성능을 향상시키지만, 크기를 적절히 관리해야 합니다.
// LruCache를 사용한 메모리 캐시 구현
int cacheSize = 4 * 1024 * 1024; // 4MB
LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
        // 비트맵의 실제 바이트 크기 반환
        return bitmap.getByteCount();
    }
};

#5. 자주 묻는 질문과 주의사항
1) 자주 묻는 질문(FAQ)
(1) GC_CONCURRENT FREED 메시지를 로그에서 숨길 수 있나요?
네, 로그캣(Logcat)에서 필터를 설정하여 특정 태그나 메시지를 제외할 수 있습니다. "dalvikvm" 태그를 필터링하거나, "GC_"로 시작하는 메시지를 제외하도록 설정할 수 있습니다.
(2) System.gc()를 호출해도 괜찮을까요?
일반적으로 System.gc()를 명시적으로 호출하는 것은 권장되지 않습니다. Java와 Android 런타임은 가비지 컬렉션을 효율적으로 관리하도록 설계되어 있으며, 명시적인 호출은 시스템의 최적화를 방해할 수 있습니다.
(3) Out of Memory 에러와 관련이 있나요?
Out of Memory(OOM) 에러는 가비지 컬렉션이 충분한 메모리를 확보하지 못할 때 발생할 수 있습니다. 이는 메모리 누수가 있거나, 앱이 사용 가능한 메모리보다 더 많은 메모리를 요구할 때 나타납니다. 메모리 프로파일링을 통해 원인을 파악하고 해결해야 합니다.
. . . . .
2) 주의가 필요한 상황
매우 짧은 간격(1초 이내)으로 GC 메시지가 계속 나타나는 경우
② paused 시간이 10ms 이상으로 길어지는 경우
③ 앱 사용 중 프레임 드롭이나 버벅임이 발생하는 경우
④ 메모리 사용량이 지속적으로 증가하는 경우

마무리
GC_CONCURRENT FREED 메시지는 에러가 아니라 Android 시스템이 정상적으로 메모리를 관리하고 있다는 신호입니다. 이 메시지 자체는 걱정할 필요가 없지만, 너무 자주 발생하거나 paused 시간이 길어지는 경우에는 메모리 사용 최적화가 필요합니다.
핵심 요약
GC_CONCURRENT FREED는 정상적인 메모리 관리 과정입니다
② 가비지 컬렉션은 더 이상 사용하지 않는 객체를 자동으로 제거합니다
Android Studio Profiler와 LeakCanary를 활용하여 메모리 문제를 디버깅하세요
컨텍스트 참조 관리, 이미지 최적화, 뷰 재활용으로 메모리를 효율적으로 사용하세요
⑤ 메시지가 매우 자주 발생하거나 paused 시간이 길어지면 최적화가 필요합니다
메모리 관리는 Android 앱 개발에서 중요한 부분이지만, 초보 개발자라면 처음부터 모든 것을 완벽하게 할 필요는 없습니다. 기본적인 원칙을 이해하고, 앱을 개발하면서 점진적으로 최적화해 나가는 것이 중요합니다. 이제 Android Studio 로그창에서 GC_CONCURRENT FREED 메시지를 봐도 당황하지 않고, 앱의 메모리 건강 상태를 확인하는 지표로 활용할 수 있을 것입니다!
긴 글 읽어주셔서 감사합니다.

끝.
반응형