Her zamanki gibi, “makine çalışmıyor” başlığına sahip bir e-mail ile bildirilmiş bir sorunu inceliyordum. Müşteriyle görüştükten sonra makineye bağlandım ve kayıt dosyalarına bir göz attım. Server uygulaması OutOfMemoryError nedeniyle çökmüştü, yani program kendisine ayrılan hafıza alanından daha fazlasını kullanmaya kalkmıştı. İyi haber bu hata ile beraber çökme anındaki tüm hafıza bilgilerini de kaydediyor olmamızdı. Bu dosyayı kaydedildiği yerden alıp bağlantıyı kestim. Artık masamdaki bilgisayarda sorunu rahat rahat inceleyebilirdim.
Dosyayı açtıktan sonra en çok yer tutan nesneleri büyükten küçüğe doğru dizdiğimde aşağıdaki gibi bir tablo çıktı.

Bu tabloda dikkatimi çeken şey ilk sütundaydı. Nesneler String tipindeydi ve içerikleri sadece “2”‘den ibaretti. Buna rağmen hafızada neredeyse 1.5 megabyte yer tutuyorlardı. Bu bilmecenin çözümü de o kadar zor değildi. Aynı listeyi bu nesnelerden çıkan referanslarla gösterdiğimde bir sonraki tabloyu elde ettim.

Burada görüldüğü gibi Strin nesnesinin “value” nesnesi gerçekten de 1.5 MB’lik bir XML içeriğe sahipti. Bu XML mesajı ve oradan elde edilen “2” değerini programda arayınca şüpheliyi buldum. Java 6 (openjdk) versiyonunda kullandığımız metod şu şekilde programlanmıştı:
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > count) { throw new StringIndexOutOfBoundsException(endIndex); } if (beginIndex > endIndex) { throw new StringIndexOutOfBoundsException(endIndex - beginIndex); } return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); }
Bu metodda bakmam gereken tek yer şu satırdı:
new String(offset + beginIndex, endIndex – beginIndex, value);
O koda da baktığımda şunu gördüm:
// Package private constructor which shares value array for speed. String(int offset, int count, char value[]) { this.value = value; this.offset = offset; this.count = count; }
Evet, açıklama satırında da görüldüğü üzere yeni yaratılan String nesnesi optimizasyon amacıyla orjinal nesnenin içeriğini hafızada tutmaya devam ediyordu. Bu yüzden tek bir rakamdan oluşan içerik bile 1.5 MB yer kaplıyordu. Bu davranış openjdk 7u40-b43 versiyonundan sonra değiştirildi. Öncelikle yukarıdaki constructor deprecated olarak işaretlendi ve yerine substring metodunda şu constructor çağrılmaya başlandı:
public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count < 0) { throw new StringIndexOutOfBoundsException(count); } if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } this.value = Arrays.copyOfRange(value, offset, offset+count); }
Görüldüğü gibi artık orijinal nesnenin içeriğinin bir kopyası yapılmakta. Bunun üzerine programda openjdk 7u40-b43’ten sonraki versiyonları kullanmaya başladık. Böylece programdaki String nesneleri artık sadece “2” gibi bir değer için 1.5 MB’lık bir yük taşımak zorunda kalmadı.
Bir kere daha gördüm ki, kullanılan kütüphanenin nasıl programlandığı dağlar kadar fark yaratabiliyor ve programcı arada sırada bu detaylarla uğraşmak zorunda kalıyor.