Garbage Collection Detayları
C veya C++ yazdıysan, bellekle bizzat uğraştığını bilirsin: malloc ile alan ayırırsın, free ile geri verirsin. Bir gün free çağırmayı unutursun — memory leak. Bir gün aynı alanı iki kere free edersin — double free, program çöker. Bir gün free edilmiş alana erişirsin — dangling pointer, güvenlik açığı.
Java bunu senin için otomatik yapar. Garbage Collector (GC), artık kullanılmayan nesneleri tespit eder ve belleklerini geri kazanır. Sen sadece new ile nesne oluşturursun, gerisini JVM halleder.
GC'yi bir apartmanın çöp toplama servisi gibi düşün. Sen çöpünü kapının önüne bırakırsın, servis gelip alır. Ne zaman gelir, nasıl toplar, nereden başlar — bunlar servisin iç detayı. Ama eğer çöp servisi yavaşlarsa ya da kapasitesini aşarsa, apartmanın (uygulamanın) yaşam kalitesi düşer. Bu yüzden GC'nin nasıl çalıştığını bilmek, performans sorunlarını teşhis etmek için kritiktir.
Neden Garbage Collection?
Manuel bellek yönetiminin sorunlarını zaten saydık. Ama GC'nin otomatik olması "bedava" anlamına gelmez. GC çalışırken CPU zamanı harcar. Bazı GC algoritmaları uygulamayı tamamen durdurur (Stop-the-World pause). Bu duraklamalar milisaniyelerden saniyelere kadar çıkabilir.
Dolayısıyla GC, güvenlik ve kolaylık ile performans arasında bir trade-off'tur. Java bu trade-off'u senin lehine optimize etmek için yıllar içinde birçok GC algoritması geliştirdi. Hangisini seçeceğin, uygulamanın karakterine bağlı.
Heap Yapısı
Java'da nesneler heap adı verilen bellek alanında yaşar. Heap sabit bir bölge değil — JVM başlangıçta belirli bir boyutta ayırır, ihtiyaç oldukça büyütür (bir üst sınıra kadar). GC'yi anlamak için heap'in iç yapısını bilmek gerekir.
Generational Hypothesis
GC'nin temelinde bir gözlem yatar: çoğu nesne kısa ömürlüdür. Bir method içinde oluşturulan geçici nesneler, döngüdeki ara değerler, String birleştirmelerin ara ürünleri — bunların hepsi birkaç milisaniye içinde işlerini bitirir ve çöp olur.
Az sayıda nesne ise uzun yaşar: cache'teki veriler, singleton'lar, veritabanı bağlantı havuzları.
Bu gözlem şu tasarıma yol açar: Heap'i genç ve yaşlı bölgelere ayır. Genç nesneleri sık topla (çoğu zaten ölüdür, hızlı biter). Yaşlı nesneleri nadir topla (çoğu hâlâ yaşıyor, pahalı bir iş).
Heap Bölgeleri
┌─────────────────────────────────────────────────┐
│ HEAP │
│ ┌──────────────────────┐ ┌──────────────────┐ │
│ │ Young Gen │ │ Old Gen │ │
│ │ ┌──────┐ ┌────────┐ │ │ │ │
│ │ │ Eden │ │Survivor│ │ │ Uzun ömürlü │ │
│ │ │ │ │ S0│ S1 │ │ │ nesneler │ │
│ │ └──────┘ └────────┘ │ │ │ │
│ └──────────────────────┘ └──────────────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Metaspace (off-heap) │ │
│ │ Sınıf metadata, method bilgileri │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘Eden: Yeni nesneler burada doğar. new Object() dediğinde nesne Eden'a yerleşir. Eden dolduğunda Minor GC tetiklenir.
Survivor (S0, S1): Minor GC'den sağ kurtulan nesneler buraya taşınır. İki Survivor alanı vardır — her Minor GC'de canlı nesneler birinden diğerine kopyalanır. Bu "ping-pong" sistemi fragmentasyonu önler.
Old Generation (Tenured): Birçok Minor GC'den sağ kurtulan nesneler — yani kanıtlanmış uzun ömürlüler — Old Gen'e terfi (promote) eder. Old Gen dolduğunda Major GC (ya da Full GC) tetiklenir. Bu daha pahalı bir operasyondur.
Metaspace: Java 8 ile PermGen'in yerini aldı. Sınıf tanımları, method metadata'ları burada tutulur. Heap'in parçası değildir — native memory'de yaşar. Varsayılan olarak sınırsız büyüyebilir (-XX:MaxMetaspaceSize ile sınırlanır).
Minor GC vs Major GC
| Özellik | Minor GC | Major GC (Full GC) |
|---|---|---|
| Temizlediği alan | Young Gen | Tüm heap (Young + Old) |
| Sıklık | Sık (saniyeler) | Nadir (dakikalar) |
| Süre | Kısa (ms) | Uzun (yüzlerce ms - saniyeler) |
| Stop-the-World | Kısa | Uzun |
GC Algoritmaları
Java'da birden fazla GC algoritması var. Her biri farklı bir iş yükü için optimize edilmiş.
Serial GC (-XX:+UseSerialGC)
En basit GC. Tek thread kullanır. Stop-the-World süreleri uzundur. Küçük heap'lerde (<100MB) ve tek çekirdekli sistemlerde kullanılır. Bugün genellikle sadece çok küçük uygulamalar veya container'lar için tercih edilir.
java -XX:+UseSerialGC -jar myapp.jarParallel GC (-XX:+UseParallelGC)
Serial'in çok thread'li versiyonu. Birden fazla GC thread'i aynı anda çalışır — toplama süresi kısalır. Throughput (birim zamanda yapılan iş miktarı) optimize eder. Java 8'e kadar varsayılan GC buydu.
java -XX:+UseParallelGC -XX:ParallelGCThreads=4 -jar myapp.jarStop-the-World duraklamaları hâlâ var ama paralel çalışma sayesinde daha kısa sürer.
G1 GC (-XX:+UseG1GC) — Varsayılan
Java 9'dan itibaren varsayılan GC. Region tabanlı tasarımıyla hem throughput hem de düşük duraklama hedefler. İleriki bölümde detaylı inceleyeceğiz.
ZGC (-XX:+UseZGC)
Java 15'te production-ready oldu. Ultra düşük latency hedefler — duraklamalar genellikle 1ms altında. Terabaytlarca heap'i yönetebilir. Java 21 ile generational versiyonu da geldi.
Shenandoah (-XX:+UseShenandoahGC)
Red Hat'in geliştirdiği düşük latency GC. ZGC'ye benzer hedefleri var — çok kısa duraklamalar. OpenJDK'da mevcut, Oracle JDK'da yok.
| GC | Hedef | Duraklama | Heap Boyutu |
|---|---|---|---|
| Serial | Basitlik | Uzun | Küçük (<100MB) |
| Parallel | Throughput | Orta | Orta-büyük |
| G1 | Dengeli | Orta-kısa | Orta-büyük |
| ZGC | Düşük latency | <1ms | Büyük (TB'ler) |
| Shenandoah | Düşük latency | <10ms | Orta-büyük |
G1 GC Detaylı
G1 (Garbage-First), modern Java uygulamalarının varsayılan GC'sidir. Adı "çöp-önce" anlamına gelir — en çok çöp barındıran bölgeleri önce toplar.
Region Tabanlı Tasarım
Klasik GC'ler heap'i Young ve Old olarak iki büyük bloğa ayırır. G1 ise heap'i eşit boyutta region'lara böler (varsayılan: 1-32MB arası, heap boyutuna göre otomatik ayarlanır).
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ E │ E │ S │ O │ O │ E │ H │ - │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ O │ - │ E │ O │ O │ - │ H │ O │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┼─────┤
│ E │ O │ - │ S │ O │ E │ - │ O │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
E = Eden | S = Survivor | O = Old | H = Humongous | - = BoşHer region tek bir role atanır ama bu rol değişebilir. Bir region önce Eden olabilir, sonra boşaltılıp Old olabilir. Bu esneklik, G1'e büyük esneklik sağlar.
Humongous region: Bir nesne region boyutunun yarısından büyükse "humongous" (devasa) sayılır ve özel bölgelere yerleştirilir. Çok büyük array'ler veya byte buffer'lar bu kategoriye girer.
G1 Çalışma Döngüsü
G1 üç ana fazda çalışır:
1. Young-only Collection: Eden dolduğunda çalışır. Canlı nesneler Survivor veya Old region'lara taşınır. Eden region'ları boşaltılır.
2. Concurrent Marking: Old Gen'in doluluk oranı bir eşiği geçtiğinde (-XX:InitiatingHeapOccupancyPercent, varsayılan %45) concurrent marking başlar. Uygulama çalışırken arka planda hangi nesnelerin canlı olduğunu belirler.
3. Mixed Collection: Concurrent marking tamamlandıktan sonra, G1 hem Young hem de en çok çöp barındıran Old region'ları temizler. "Mixed" denmesinin sebebi budur — sadece Young değil, seçilmiş Old region'lar da temizlenir.
// G1'in davranışını gösteren basit bir senaryo
import java.util.ArrayList;
import java.util.List;
class Main {
public static void main(String[] args) {
List<byte[]> longLived = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
// Kısa ömürlü nesneler → Eden'da doğar, Minor GC'de temizlenir
byte[] temp = new byte[1024 * 100]; // 100KB
// Her 10 iterasyonda bir uzun ömürlü nesne
if (i % 10 == 0) {
longLived.add(new byte[1024 * 500]); // Old Gen'e promote olur
}
}
System.out.println("Long-lived objects: " + longLived.size());
}
}G1 Duraklama Hedefi
G1'in en güçlü özelliği duraklama hedefi koyabilmendir:
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar myapp.jarBu, G1'e "her GC duraklamasını 200ms altında tut" der. G1 buna uymaya çalışır (garanti değildir) — daha az region toplayarak süreyi kısaltır. Ama az toplama, daha sık GC anlamına gelir.
⚠️ Dikkat: MaxGCPauseMillis değerini çok düşük koymak geri tepebilir. G1 her seferinde çok az temizlerse, heap dolmaya devam eder ve sonunda uzun bir Full GC kaçınılmaz olur. Varsayılan 200ms genellikle iyi bir başlangıçtır.
ZGC: Düşük Latency, Büyük Heap
ZGC, Java'nın en modern GC'lerinden biri. Hedefi net: duraklama sürelerini 1ms altında tutmak — heap ne kadar büyük olursa olsun.
Nasıl Çalışır?
ZGC'nin sihri colored pointers ve load barriers tekniklerindedir. Kısaca: nesneleri taşırken uygulamayı durdurmak yerine, nesnelere erişildiği anda (load barrier ile) referansları günceller. Bu sayede GC'nin büyük bölümü uygulama ile eş zamanlı çalışır.
# ZGC kullanımı (Java 17+)
java -XX:+UseZGC -Xmx16g -jar myapp.jar
# Java 21+ ile generational ZGC (önerilen)
java -XX:+UseZGC -XX:+ZGenerational -Xmx16g -jar myapp.jarNe Zaman ZGC?
Heap boyutu çok büyük (8GB+, hatta TB'ler)
Düşük latency kritik (fintech, gaming, real-time sistemler)
Uzun GC duraklamaları kabul edilemez
Ne Zaman ZGC Değil?
Küçük heap'lerde (<4GB) G1 yeterli ve daha az overhead
Throughput en önemli metrikse, Parallel GC daha iyi olabilir
ZGC biraz daha fazla CPU kullanır — CPU'nun sınırlı olduğu ortamlarda dikkatli ol
GC Tuning Temelleri
GC tuning, uygulamandan önce ölçüm yapmak demektir. Premature optimization'dan kaçın — önce sorun olduğunu kanıtla, sonra tune et.
Temel JVM Parametreleri
# Minimum ve maksimum heap boyutu
java -Xms512m -Xmx2g -jar myapp.jar
# -Xms: Başlangıç heap boyutu. JVM bu kadar bellek ayırarak başlar.
# -Xmx: Maksimum heap boyutu. Heap bu sınırı aşamaz.Best practice: -Xms ve -Xmx'i aynı değer yap. Böylece JVM heap'i büyütmek/küçültmek için zaman harcamaz.
# Önerilen başlangıç konfigürasyonu
java -Xms2g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar myapp.jarGC Seçimi
| Senaryo | Önerilen GC |
|---|---|
| Genel amaçlı web uygulaması | G1 (varsayılan) |
| Batch processing, yüksek throughput | Parallel |
| Düşük latency, büyük heap | ZGC |
| Microservice, küçük container | Serial veya G1 |
GC Logları Okuma
GC'nin ne yaptığını anlamak için loglarını açmalısın.
Log Açma (Java 9+)
# Temel GC logları
java -Xlog:gc -jar myapp.jar
# Detaylı loglar
java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar myapp.jar
# Açıklama:
# gc* → GC ile ilgili tüm tag'ler
# file=gc.log → Dosyaya yaz
# time,uptime → Zaman bilgisi ekleLog Çıktısını Okuma
[0.234s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
12M->4M(256M) 3.456ms
[15.678s][info][gc] GC(12) Pause Young (Normal) (G1 Evacuation Pause)
128M->45M(256M) 8.234ms
[45.123s][info][gc] GC(25) Pause Full (G1 Compaction Pause)
230M->120M(256M) 456.789msBu satırları okuyalım:
GC(0): GC event numarasıPause Young (Normal): Minor GC — Young Gen temizliği12M->4M(256M): Heap kullanımı 12MB'den 4MB'ye düştü, toplam heap 256MB3.456ms: Duraklama süresi
Son satırdaki Pause Full dikkat çekici: 456ms'lik bir duraklama. Bu çok uzun — muhtemelen heap yetersiz kalıyor.
Alarm sinyalleri:
Full GC sıklığı artıyor → Heap yetersiz veya memory leak var
Full GC sonrası heap kullanımı düşmüyor → Memory leak güçlü şüpheli
Duraklama süreleri kabul edilemez uzunlukta → GC algoritmasını değiştir veya heap'i büyüt
GC Log Analiz Araçları
Ham logları elle okumak zor. Şu araçlar görselleştirme sağlar:
GCViewer: Açık kaynak, GC loglarını grafik olarak gösterir
GCEasy (gceasy.io): Online GC log analizi — dosyayı yükle, rapor al
JClarity Censum: Detaylı GC analizi
Memory Leak Tespiti
Java'da garbage collector var — ama memory leak yine olabilir. Nasıl mı? Bir nesneye hâlâ referans varsa GC onu toplayamaz. Kullanmayacağın nesnelere referans tutmaya devam edersen, heap yavaş yavaş dolar.
Yaygın Memory Leak Senaryoları
// 1. Static koleksiyona sürekli ekleme, hiç silmeme
public class EventLog {
private static final List<Event> events = new ArrayList<>();
public static void log(Event event) {
events.add(event); // Sonsuza kadar büyür!
}
}
// 2. Listener/callback kaydedip çıkarmayı unutmak
button.addActionListener(e -> handleClick());
// Butonu kullanmayı bıraktığında listener hâlâ referans tutar
// 3. Map'e koyup çıkarmayı unutmak (cache senaryosu)
Map<String, HeavyObject> cache = new HashMap<>();
cache.put(key, expensiveComputation());
// Expired entry'leri asla silmiyorsan → leakTespit Araçları
jps — Çalışan Java proseslerinin PID'lerini listeler:
jps -l
# 12345 com.example.MyApp
# 12346 jdk.jcmd/sun.tools.jps.Jpsjstat — GC istatistiklerini canlı izler:
# Her 1 saniyede GC istatistikleri
jstat -gcutil 12345 1000
# Çıktı:
# S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
# 0.00 45.23 67.89 78.45 96.12 92.34 42 0.543 3 1.234 8 0.123 1.900E: Eden doluluk oranı (%)
O: Old Gen doluluk oranı (%)
YGC/YGCT: Young GC sayısı / toplam süre
FGC/FGCT: Full GC sayısı / toplam süre
Eğer O sürekli artıyorsa ve Full GC sonrası düşmüyorsa → memory leak.
jmap — Heap dump alır:
# Heap dump oluştur
jmap -dump:format=b,file=heapdump.hprof 12345
# Canlı nesnelerin histogramı
jmap -histo 12345 | head -20VisualVM — Görsel izleme aracı:
# JDK ile gelmeyebilir, ayrı indirilebilir
visualvmVisualVM ile heap kullanımını grafiksel olarak izler, heap dump alır, hangi sınıfların en çok bellek kullandığını görürsün. "Monitor" sekmesinde heap'in sürekli arttığını görüyorsan, "Sampler" sekmesine geç ve hangi nesnelerin biriktiğini bul.
💡 İpucu: Production'da memory leak şüphesi varsa, heap dump almadan önce dikkat et — dump dosyası heap kadar büyük olabilir (2GB heap = 2GB dosya). Disk alanı kontrolü yap. Ve dump alma sırasında uygulama kısa süre durabilir.
Heap Dump Analizi
Heap dump'ı Eclipse MAT (Memory Analyzer Tool) ile açmak en yaygın yöntemdir:
MAT'ı aç → heap dump dosyasını yükle
Leak Suspects Report: Otomatik olarak şüpheli nesneleri tespit eder
Dominator Tree: Hangi nesne en çok bellek tutuyor
Histogram: Sınıf bazında nesne sayısı ve bellek kullanımı
// MAT Histogram örnek çıktı:
Class Name | Objects | Shallow Heap | Retained Heap
------------------------------------------------------------------
byte[] | 125,432 | 45,678,912 | 45,678,912
java.lang.String | 98,234 | 2,357,616 | 48,036,528
com.example.CacheEntry | 50,000 | 1,200,000 | 25,000,000 ← Şüpheli!
java.util.HashMap$Node | 50,123 | 1,603,936 | 26,603,93650.000 CacheEntry nesnesi ve 25MB retained heap — cache'in temizlenmediği açık.
Finalization ve Cleaner
Java'da finalize() method'u, bir nesne GC tarafından toplanmadan önce çağrılır. Fikir güzel görünüyor: kaynakları temizlemek için son bir şans. Ama pratikte felaket.
Neden finalize() Deprecated?
// YAPMA — finalize() Java 9'da deprecated, Java 18'de removal için işaretlendi
class ResourceHolder {
private FileInputStream stream;
@Override
protected void finalize() throws Throwable {
stream.close(); // "GC toplamadan önce dosyayı kapat"
super.finalize();
}
}Sorunlar:
1. Çağrılma garantisi yok. JVM kapanırken finalizer'ları çalıştırmayabilir. Kaynak asla temizlenmeyebilir.
2. Ne zaman çağrılacağı belirsiz. GC ne zaman çalışacağını bilmezsin. Dosya handle'ı veya veritabanı bağlantısı dakikalarca, hatta saatlerce açık kalabilir.
3. GC'yi yavaşlatır. Finalizer queue'su ayrı bir thread tarafından işlenir. Nesneler en az iki GC döngüsü yaşar — ilkinde finalizer kuyruğuna girer, ikincisinde toplanır.
4. Exception yutulur. Finalizer içinde exception fırlarsa sessizce yutulur. Haberin bile olmaz.
5. Güvenlik riski. Finalizer attack'lar mümkün — kötü niyetli bir alt sınıf finalizer'da nesneyi yeniden erişilebilir yapabilir.
Doğru Yol: try-with-resources
// DOĞRU — AutoCloseable + try-with-resources
class ResourceHolder implements AutoCloseable {
private final FileInputStream stream;
ResourceHolder(String path) throws IOException {
this.stream = new FileInputStream(path);
}
@Override
public void close() throws IOException {
stream.close();
}
}
// Kullanım
try (ResourceHolder holder = new ResourceHolder("data.txt")) {
// holder'ı kullan
} // Otomatik olarak close() çağrılır — garanti!Cleaner API (Java 9+)
Eğer gerçekten bir "son şans temizliği" lazımsa, java.lang.ref.Cleaner kullan:
import java.lang.ref.Cleaner;
class NativeResource implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
private final long nativePointer;
private final Cleaner.Cleanable cleanable;
NativeResource() {
this.nativePointer = allocateNative(); // Native bellek ayır
// Cleaner'a temizleme aksiyonunu kaydet
// DİKKAT: Lambda 'this'e referans TUTMAMALI!
long ptr = this.nativePointer;
this.cleanable = CLEANER.register(this, () -> freeNative(ptr));
}
@Override
public void close() {
cleanable.clean(); // Manuel temizlik — tercih edilen yol
}
private static native long allocateNative();
private static native void freeNative(long pointer);
}Cleaner, finalizer'ın sorunlarının çoğunu çözer: ayrı bir thread'de güvenle çalışır, exception'lar yutulmaz. Ama yine de try-with-resources birincil yolun olmalı — Cleaner sadece bir güvenlik ağıdır.
Pratik: GC Davranışını Gözlemleme
Aşağıdaki programı farklı GC'lerle çalıştırarak farkı gözlemleyebilirsin:
import java.util.ArrayList;
import java.util.List;
class Main {
public static void main(String[] args) {
List<byte[]> retained = new ArrayList<>();
int leakCount = 0;
for (int i = 0; i < 500; i++) {
// Kısa ömürlü nesneler — GC tarafından toplanmalı
for (int j = 0; j < 100; j++) {
byte[] temp = new byte[1024]; // 1KB
}
// Her 5 iterasyonda bir "leak" — retained listesine ekle
if (i % 5 == 0) {
retained.add(new byte[1024 * 100]); // 100KB
leakCount++;
}
}
System.out.println("Retained objects: " + leakCount);
System.out.println("Approximate retained size: " +
(leakCount * 100) + " KB");
}
}# G1 ile çalıştır ve GC loglarını gör
javac Main.java
java -Xms64m -Xmx64m -XX:+UseG1GC -Xlog:gc Main
# ZGC ile karşılaştır
java -Xms64m -Xmx64m -XX:+UseZGC -Xlog:gc Main
# Serial ile karşılaştır
java -Xms64m -Xmx64m -XX:+UseSerialGC -Xlog:gc MainLogları karşılaştırdığında GC event sayısını, duraklama sürelerini ve heap kullanım değişimlerini göreceksin. G1 daha sık ama kısa duraklamalar yapar. Serial daha nadir ama daha uzun. ZGC en kısa duraklamaları verir.
GC Tuning Checklist
Tuning yapmadan önce bu listeyi takip et:
1. Ölç. GC loglarını aç, mevcut durumu anla. Körlemesine parametre değiştirme.
2. Heap boyutunu doğru ayarla. jstat ile Old Gen doluluk oranını izle. Sürekli %80+ ise heap'i büyüt. Full GC çok sık ise de heap muhtemelen küçük.
3. Varsayılanlarla başla. G1 zaten çoğu senaryo için iyi ayarlanmış. İlk denemen her zaman varsayılan parametrelerle olsun.
4. Bir parametreyi değiştir, tekrar ölç. Birden fazla parametreyi aynı anda değiştirme — hangisinin etkili olduğunu bilemezsin.
5. Throughput mu latency mi? Neyi optimize ettiğini bil. İkisini aynı anda en üst düzeye çıkaramazsın — trade-off kaçınılmaz.
Özet
Garbage Collection, Java'nın otomatik bellek yönetim sistemidir —
newile oluşturursun, GC gerisini halleder.Heap Young Gen (Eden + Survivor) ve Old Gen olarak bölünür. Çoğu nesne Young Gen'de doğar ve ölür (generational hypothesis).
G1 GC varsayılan seçimdir — region tabanlı, duraklama hedefi koyulabilir. ZGC ultra düşük latency için, Parallel yüksek throughput için uygundur.
GC loglarını
-Xlog:gcile aç, Full GC sıklığı ve duraklama sürelerini izle. Sürekli artan Old Gen kullanımı memory leak işaretidir.Memory leak tespiti için jstat ile canlı izle, jmap ile heap dump al, Eclipse MAT ile analiz et.
finalize()kullanma — try-with-resources ve Cleaner API kullan. Kaynak temizliğini GC'ye bırakma, kendim yönet.
AI Asistan
Sorularını yanıtlamaya hazır