Java’da Bellek Yönetimi
Hepimiz hemen her gün onlarca, hatta yüzlerce satır kod yazıyoruz ama çoğu kez işin arka tarafında mutfakta bu işlerin nasıl yürütüldüğünü merak ederiz. Bende bu yazıda Java Memory Management kursundan aldığım notları sizlerle paylaşacağım.
Java’da yeni bir nesne oluşturmak için new anahtar kelimesini kullanırız. Oluşturduğumuz nesneler hafızada tiplerine göre Stack’te veya Heap’te tutulur.
Value type (değer tipli) nesnener Stack’te tutulur. Java’da Primitive tipler dediğimiz byte, char, int, long, double, boolean… gibi tiplere karşılık gelmektedir.
Reference type (referans tipli) nesnelerin değerleri Heap’te referansları ise Stack’te tutulur. Java’da Wrapper tipler dediğimiz Integer, Long, Double, Boolean, Object, Person (kendi özel tipimiz) tiplerine karşılık gelmektedir.
Kısaca Stack
- LIFO (Last in First out) son giren ilk çıkar mantığında çalışır.
- Veri depolama alanı çok geniş olmadığından kullanımı kolay ve hızlıdır.
- Veriler Big and Little Endian (artan ya da azalan) adres mantığında tutulur.
- Derleme zamanında oluşturulur.
- Life time (yaşam süresi) kısa olan değişkenler tutulur. Ör; local variables (yerel değişkenler).
- Static allocation (Kullanılacak depolama alanının boyutu biliniyorsa stack işe yarayacaktır.)
- Bir Java uygulamasında sadece tek bir stack yoktur. Her bir thread’in kendi stack’i vardır.
- Bir stack üzerindeki veriye kendi thread’inden başka bir thread erişemez.
- Doğru kullanılmadığında java.lang.StackOverFlowError hatası alınır.
Kısaca Heap
- Heap stack’e göre daha büyük boyuta sahiptir.
- Stack’e göre daha fazla alana sahip olduğundan stack’e göre daha yavaştır.
- Heap’teki veriler karışık şekilde sıralanır.
- Çalışma zamanında oluşturulur.
- Dynamic allocation (Kullanılacak depolama alanının boyutu bilinmiyorsa ya da sürekli değişken olacak ise heap kullanmak doğru olacaktır.)
- Bir Java uygulamasında tüm thread’ler için sadece bir tane heap bulunmaktadır.
- Doğru kullanılmadığında java.lang.OutOfMemoryError hatası alınır.
Autoboxing and Unboxing
Java 5 ile gelen bu özellik sayesinde java compiler primitive tip ile wrapper tip arasındaki dönüşümü yapabilmektedir.
Autobaxing primitive tiplere karşılık gelen referans tipler arasındaki dönüşümdür.
Örneğin : int → Integer , double → Double
Unboxing Yukarıdaki işlemin tersi yönünde bir dönüşüm işlemine denir.
Örneğin : Integer → int , Double → double
List<Long> numberList = new ArrayList<Long>();long number = 65;numberList.add(number); // autoboxinglong firstElement= numberList.get(0); // unboxing
Java’da Parametre Geçirme
pass by value
Metoda gönderilen parametrenin bir kopyasının tutularak gönderildiği parametre aktarım şeklidir. Yani metot içinde parametrenin değeri değiştiğinde ilk değeri değişmez. Değişiklik sadece metot scope içinde görülür.
pass by reference
Metoda gönderilen parametrenin referansının gönderildiği parametre aktarım şeklidir. Metot içinde parametrenin değeri değiştiğinde ilk değeride değişir.
Java pass-by value yaklaşımını kullanmaktadır.
Görüldüğü gibi number ve person nesnesinin değeri metot içerisinde değiştirildi.
Metot dışına çıkıldığında main metodu içerisinde yazdırıldığında ilk değerlerini koruduğu görülüyor.
Bu nedenle Java kesinlikle pass-by-value yaklaşımını kullanmaktadır.
Final kullanımı
Final anahtar kelimesi programın daha doğru, hatalara dayanıklı ve performanslı çalışmasını sağlar.
Final anahtar kelimesi sınıf değişkenleri, metot parametreleri, metotlar ve sınıflar için kullanılmaktadır.
Sınıf değişkenleri: Değişkenler için final anahtar kelimesi kullanmak demek değeri sonradan değiştirilemeyen değişkenler oluşturmak demektir. Sadece bir kere değer ataması yapılabilir ve bu atama tanımlandığı yerde veya sınıfın constructor’ında (yapılandırıcısında) gerçekleşebilir.
Metot parametresi: Metot’lara geçilen parametre final olarak tanımlanırsa metot içerisinde o parametrenin değeri değiştirilemez. Metot parametrelerinin tamamen final olarak tanımlamış olmalarında büyük fayda vardır. Bu şekilde parametrenin metot bünyesinde değişikliğe uğrama tehlikesi kaldırılmış olur.
Metot: Final olarak tanımlanan bir metot alt sınıflar tarafından override edilemez.
Sınıf: Final olan bir sınıf extend edilemez (genişletilerek bir alt sınıf oluşturulamaz).
Static kullanımı
Static anahtar kelimesi Java’da değişken’ler, blok’lar, metot’lar ve inner-class’lar için kullanılabilir. Static tanımlanan bir değişken veya metot nesnelerden bağımsız olarak var olurlar. Yani static anahtar kelimesi, nesnelere değil sınıflara aittir. Static değişken veya metot, nesne oluşturmadan sınıf ismi ile erişilebilir. Static ifadeler nesneler tarafından paylaşılırlar.
Static olmayan değişkenler nesnenin oluşması ile birlikte oluşur ve bellekte nesneye ait alanda yer alırlar. Static değişkenler ise sınıfın oluşturulması ile birlikte bellekte yerlerini alırlar. Böylece her nesne oluşmasında tekrar tekrar bellekte bu değişkenin yer tutması için belleğe başvurulmaz. Çünkü static değişkenin yeri sınıf oluşturulurken açılmıştır.
Static anahtar kelimesi blok’lar içinde kullanılır. Java’da static initialization block ve instance initialization block vardır. Bir sınıf yüklendiğinde ilk olarak ve sadece bir kez static initialization blok çalışır. Yeni bir instance oluşturulduğunda her defasinda instance initialization block çalışır.
Sınıf seviyesinde static kullanımı sadece inner-class’lar için kullanılabilir. Stataic-inner-class’tan bir instance oluşturmak için outer-class’ın referansına ihtiyaç yoktur. Stataic-inner-class, outer-class’ın sadece static üyelerine erişebilir. Non-static bir üyeye erişmek istediğinde derleme hatası alır.
Garbage Collector
Garbace Collector Java’nın en büyük nimetlerinden birisidir. Orta seviyeli dillerde bir nesneye artık ihtiyacınız olmadığında onu bellekten silmek zahmetli bir iştir. Bu dillerin en büyük sorunlarından birisi dinamik bellek yönetimidir. Java geliştiricileri bu konuda oldukça rahattır. Çünkü JVM tarafından GC ile referansı olmayan nesneler tespit edilip hafızadan silinir. GC’nin amacı hafızada bulunan ve ulaşılamayan (referansı olmayan) nesneleri hafızadan silmek ve hafızada yeni nesneler için boş alan açmaktır.
GC’nin çalışması için istekte bulunma
GC, JVM’nin kontrolü altındadır. GC’nin çalışması için JVM’e istekte bulunuruz. Fakat isteğimizin kabul edilip edilmeyeceğinin bir garantisi yoktur. İstekte bulunmak için System.gc() veya Runtime.getRuntime().gc() fonksiyonlarını çağırırız. GC çalıştığında finalize() methodu tetiklenir. Burada yapılacak son işlemler var ise bu metot içinde yapılır.
Şimdi GC çalıma mantığına bakalım.
- Uygulama çalıştığında heap boş olur.
- Dolduğunu varsayalım ve GC gelip kullanılmayan nesneleri temizler. Bu ilk temizlemede, sağ kalan nesneler Generation #0 olarak tanımlanır.
- Tekrardan dolduğu ve GC’nin çalıştığını varsayalım. Generation #0'dan sağ kalanlar Generation #1 olarak tanımlanır ve yenilerden sağ kalanlarda Generation #0 olarak tanımlanır.
- Tekrardan dolduğu ve GC’nin çalıştığını varsayalım. Generation #1'den sağ kalanlar Generation #2 olarak tanımlanır ve Generation #0'dan sağ kalanlar Generation #1 olarak tanımlanır. En yenilerden sağ kalanları Generation #0 olarak tanımlanır.
Tuning JVM (JVM’yi ayarlama)
Java geliştiricileri java.lang.OutOfMemoryError hatasıyla karşılaşmak istemezler. Eğer bellek yönetimi doğru yapılamaz ise bu hatayla karşılaşmak muhtemeldir.
Ör; uygulamanın ayağa kalkaması için minimum bellek boyutu doğru ayarlanmamış ise bu hata ile karşılaşılır.
# Xms heap alanının başlangıç miktarını belirtir
# Xmx heap alanının maximum miktarını belirtir
JVM argümanlarının tüm listesine buradan ulaşabilirsiniz.
Xms512m -Xmx1024m
JVM başlangıçta bellek miktarını 512 MB olarak ayarlayacaktır. Bu rakamı aşan bir ihtiyaç olduğunda ise 1024 MB’a yükseltecektir.