ÖNEMLİ : Kendim için aldığım notlar. Umarım size de bir faydası olur. Kullanılan her bir makale referans olarak eklenmiştir.

Java'da Hafıza Modeli Serisi


  1. Java’da Hafıza Modeli 1 - İlkel Veri Tipleri(Primitive Types)
  2. Java’da Hafıza Modeli 2 - Nesneler
  3. Java’da Hafıza Modeli 3 - Nesneler
  4. Java’da Hafıza Modeli 4 - Kapsam(Scope)
  5. Java’da Hafıza Modeli 5 - Pass By Value / Pass By Reference
  6. Java’da Hafıza Modeli 6 - Java’da Statik ve Statik Olmayan Değişken ve Metotların Hafıza Yönetimi
  7. Java’da Hafıza Modeli 7 - String Interning Nedir, String Pool Nedir, == operatörü ve equals metodu Arasındaki Fark
  8. Java’da Hafıza Modeli 8 - Java’da Static Initializer nedir? - Java statik ilklendirici
  9. Java’da Hafıza Modeli 9 - Java’da Instance Initializer Nedir? - Java örnek ilklendirici
  10. Java’da Hafıza Modeli 10 - Java’da Neden BAZI durumlarda metot kullanmak yerine initializer’ı tercih ederiz?

Konu ile alakalı bir bilgilendirme


Arkadaşlar, bu konu her ne kadar spesifik bir konu gibi gözükse de bir bağlam içerisinde konuyu ele almak çok daha önemlidir. Yani, statik nedir? sorusunu daha geniş bağlamda düşünmemiz gerekiyor. Bunun için statik olmayan değişken ve metotlar nedir? Statik ve statik olmayan değişkenler hafızada nasıl saklanır? gibi farklı soruları da beraberinde düşünmemiz elzemdir. Bunun için aşağıdaki içeriklerime sırasıyla bakmanızı öneriyorum.

Değişken Geçirme(Passing a Variable)

“Değişken geçirme” terimi, bir metot daha önce tanımladığınız bir değişkenle çağrıldığında kullanılır.

1
2
int a = 2;
calTotal(a);

a değişkeni calTotal yönteminde kullanılmak üzere, bu yönteme geçirilmiştir. Bu yöntemin de zaten bir int değeri alacak şekilde önceden tanımlandığını anlıyoruz.

Programlama dilleri metotlara parametre aktarılırken 2 farklı yaklaşım kullanır. Pass by value(değere göre geçirme) ve pass by reference(referansa göre geçirme) yaklaşımları değişkenlerin metotlara nasıl aktarıldığını tanımlamak için kullanılan 2 farklı tekniktir. Kısaca izah edecek olursak, değere göre geçişte, metoda gerçek değerin geçirildiği anlamına gelir. Referansla geçişte, değerin nerede saklandığını tanımlayan bir işaretçinin (bu, geçirilen değişkenin hafızadaki adresi olarak düşünülebilir) geçirildiği anlamına gelir.

Önceki derslerde ilkel ve ilkel olmayan tiplerin hafızada nerelerde ve nasıl saklandığını zaten bildiğinizi varsayarak, hibrit bir hafıza şekli ile bu durumu anlatmak istiyorum. Buradaki amaç heap ve stack gibi hafıza birimlerine etraflıca girmeden konuyu daha genel bir çerçeveden ele almaktır.


How memory management works in java (java'da hafıza yönetimi nasıl çalışır)

Basitleştirmek için, hafızayı yan yana sıralanmış bloklar olarak düşünebilirsiniz. Ve her bir bloğun da veri saklayan bir alanı temsil ettiğini hayal edin. Gri rakamlar herbir bloğun hafızadaki adresini, mavi ve kırmızı rakamlar ise bu hafıza bloklarında saklanan gerçek değerleri temsil etmektedir.

Pass by Value Nedir? (C ve C++ dillerinde)

“Pass by Value” yaklaşımı uygulandığında, metotun içine aldığı parametrenin değeri, belleğin başka bir yerine kopyalanır. Şayet metodun değişkenine erişmek veyahut bu değişkeni değiştirmek isterseniz, yalnızca kopyaya erişilir/değiştirilir, orijinal değere dokunulmaz. Aşağıdaki örnekte myAge değişkeninin orijinal değeri 106 nolu blokta saklanmaktadır ve bu değer 14‘tür. Bu değerin calBirthYear metoduna geçen kopyasının değeri ise 152 nolu blokta tutulmaktadır.

1
2
int myAge = 14;
calBirthYear(myAge);


Java pass by value (java'da değer geçirme)

Şayet bir işlem yapılmak istendiğinde gerekli işlem orijinal değere değil, kopyalanan değere uygulanır. Aslında bunun izahını bir önceki bölüm olan scope(kapsam) konusunda farklı bir şekilde ele almıştık.

Yukarıdaki örneği daha açık hale getirmek için, metot içindeki değişken bu örnekte age olarak adlandırılmıştır. Yani 152 nolu blokta yer alan age değişkeni aslında myAge değişkeninden klonlanan değerinin ta kendisidir. Bütün işlemler age üzerinde gerçekleşecektir. myAge değişkeni bu işlemlerden etkilenmez çünkü myAge calBirthYear metodunun kapsamı(scope) dışındadır. Aşağıdaki c++ programlama dilinde hazırlanmıştır. Kodun bütün halini şu linkte görebilirsiniz.

1
2
3
4
int calBirthYear(int age) {
	int birthYear = CURRENT_YEAR  age;
	return birthYear;
}

Peki, “pass by value” yaklaşımı uygulandığında, metot kapsamı dışındaki değişkeni nasıl değiştiririz? Bir değişkeni değere göre iletirken, kaynak değişkeni güncellemenin tek yolu metodun dönen değerini kullanmaktır. Bu, metot dışında yalnızca bir değerin değiştirilebileceği anlamına gelir.

1
2
3
4
5
6
int myAge = 14;
myAge = increaseAge(myAge);

int increaseAge(int age) {
	return age + 1;
}


Java pass by value (java'da değer geçirme)

Görüleceği üzere myAge değeri ancak bu şekilde değişir. 15 olarak bu değer güncellenir.

Pass by Reference Nedir? (C ve C++ dillerinde)

Referans ile geçirme, değişkenin hafıza adresinin ilgili metoda iletildiği anlamına gelir. Yani hafızada ilgili değişkenin değerini saklayan bloğun adresi, metoda geçirilir.

C++ programlama dili ile,

1
2
3
4
5
6
7
8
int increaseAgeByRef(int &age) {
    age = age + 1;
  }
int main() {
  int myAge = 14;
  increaseAgeByRef(myAge);
  return 0;
}

C programlama dili ile,

1
2
3
4
5
6
7
8
int increaseAgeByRef(int *age) {
    *age = *age + 1;
  }
int main() {
  int myAge = 14;
  increaseAgeByRef(&myAge);
  return 0;
}

Yukarıdaki örnek hem C hem de C++ ile gösterilmiştir. Konuyu java ile ele almadan önce farklı dillerde bu yaklaşımların nasıl gerçekleştiğini anlamak önemlidir.

Önceki örneklerden de bildiğimiz üzere myAge değişkeni hafızada 106 nolu blokta tutulmaktadır. Yani bu değişkenin hafızadaki adresi 106‘dır. Bu örneklerde de, bu değişkenin tuttuğu değer yerine, hafızadaki adresi olan 106 numarasının increaseAgeByRef metoduna geçirilmesi sağlanıyor. Anlaşılacağı üzere increaseAgeByRef metodu int tipinde, age isminde bir argüman aldığını deklare ediyor. C++ ve C gibi dillerde, değişkenin önünde & sembolü geldiğinde, girilen parametrenin hafızadaki adresine işaret ettiğini anlaşılır. Bu örnekte myAge değişkeninin adresinin 106 nolu blok olduğunu biliyoruz. Bu yüzden bu adresin işaret ettiği bloğun değeri üzerinde işlem yapılacağını anlayabiliriz.

Yalnız C programlama dilinde ekstra bir işaretçiye daha ihtiyacımız vardır. Dikkat ederseniz C ile yazdığımız kodda, metot tanımı içinde, age değişkeninin hemen önünde bir ( * ) yıldız sembolü bulunmaktadır. Bu sembolün aslında bir anlamı vardır. & Sembolü bir değişkenin adresini gösterirken, ( * ) sembolü ise bu adreste depolanan değeri döndürür. Yani bir nevi & sembolünü tersine çevirir. Özetle adres yerine, adresin işaret ettiği bloktaki değeri verir.

Bu sayede myAge değişkeninin orijinal değeri olan 14 doğrudan değiştirilmiş olur. Referansa göre geçirme sayesinde doğrudan orijinal değer üzerinde istediğimiz manipülasyonu yapabiliriz. Burada age ve myAge değişkenleri aynı adrese işaret ettiği için, age değişkeninin değerini değiştirmemiz myAge değişkenini de etkileyecektir. Burada 14 değerine 1 ekleniyor. Yani 106 nolu blokta bulunan orijinal değeri 15 olarak güncelliyoruz.

Sonuç olarak pass by value‘da olduğu gibi değer, ayrı bir bloğa kopyalanmadı. Doğrudan orijinal değer üzerinde gerekli işlemler gerçekleşmiş oldu.


Java pass by reference (java'da referans geçirme)

Not:


Referans ile geçirme yaklaşımı ile, metot parametresinde verilen birden fazla değişkeni değiştirmek mümkündür.

Bu gibi durumlarda fonksiyon dönüş türünü true veya false şeklinde yaparak genel durumun başarılı/başarısız olduğunu belirten bir şey veyahut önemli bir değişken döndürülebilir.

Örneğin;

C programlama dili ile,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdbool.h>
#include <stdio.h>

bool calculate(int *c1, int *c2, int *c3) {
    *c1 = *c1 + 1;
    *c2 = *c2 + 1;
    *c3 = *c3 + 1;
    return true;
}

int main() {
  int c1 = 1;
  int c2 = 2;
  int c3 = 3;
  calculate(&c1,&c2,&c3);
  printf("C1 is %d, C2 is %d and C3 is %d\n", c1, c2, c3);
  return 0;
}

Kodu bu linkte çalıştırabilirsiniz.

C++ programlama dili ile,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

bool calculate(int &c1, int &c2, int &c3) {
    c1 = c1 + 1;
    c2 = c2 + 1;
    c3 = c3 + 1;
    return true;
}

int main() {
  int c1 = 1;
  int c2 = 2;
  int c3 = 3;
  calculate(c1,c2,c3);
  printf("C1 is %d, C2 is %d and C3 is %d\n", c1, c2, c3);
  return 0;
}

Kodu bu linkte çalıştırabilirsiniz.

Basit Kural:


Pass by reference veya pass by value‘dan birini kullanmaya karar vermek için iki basit genel kural vardır:

  • Bir işlev tek bir değer döndürüyorsa: pass by value kullanılabilir,
  • Bir işlev iki veya daha fazla farklı değer döndürüyorsa: pass by reference kullanmak daha isabetli olabilir. Son verdiğimiz iki örnek bu madde için uygundur. Dönüş türü boolean olan bir metot içinde birden fazla referans değeri değiştirilmektedir.

Not: Referans yoluyla geçirme genellikle diziler veya obje gibi yapılar kullanılarak önlenebilir.

Pass by Value/Reference?? - JAVA

Java üst düzey bir programlama dilidir. Bu, normal şartlar altında, bellekte ne olduğu konusunda endişelenmeniz gerekmediği anlamına gelir. Çünkü java hafıza yönetimini arka planda kendi halleder.

Java’da da ilkel veri tipleri (int, double vb.) her zaman değere göre iletilir, yani bütün işlem aslında metoda geçirilen değişkenin değerin bir kopyası üzerinden gerçekleşir. C ve C++ üzerinden konuyu anlatırken oluşturduğumuz illüstrasyon burada da geçerlidir. Bu sebepten ötürü tekrardan resmetmek istemedim.

Peki ilkel olmayanlar türler için(yani objeler için) bu durum sizce nasıl gerçekleşir? Java’da primitive türler için pass by value yaklaşımı olsa da, sezgisel olarak tüm nesneler için pass-by-reference yaklaşımı birçok farklı kaynağa göre daha doğrudur. (Makalenin sonunda bu tanımlama için bir önemli not bulacaksınız. Orayı okumadan geçmemenizi öneririm. Çünkü oracle referans türleri için de pass-by-value ifadesini kullanıyor)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class JSample {
    public static void main(String[] args) {
      SomeObject someObject = new SomeObject("object 1");
      System.out.println(someObject);
      testMethod(someObject);
      System.out.println(someObject);
    }
    public static void testMethod(SomeObject someObjectX) {
        someObjectX = new SomeObject("object 2");
    }
}
 class SomeObject {
    public SomeObject(String x){
    }
}

Kodu online bir editör olan pythontutor’da çalıştırmak isterseniz, bu linki ziyaret edebilirsiniz.

Not:


Bir yönteme parametre olarak bir değer (veyahut nesneyi tutan bir referans) geçirirseniz, yöntemin dışında, atanan bu değeri tutan değişkenin değeri veyahut geçirilen bu şey bir nesneyi tutan referans ise, bu referansın tuttuğu obje aynı kalır(Çünkü referansın işaret ettiği objenin id’sinin bir kopyası bu metoda geçirilmiştir). Fakat referansın işaret ettiği objenin state’i(yani objenin instance variable’ları) istenilirse metot içinden değiştirilebilir..

Görüleceği üzere someObject referansı testMethod yöntemine geçiriliyor. Dikkat ederseniz bu referans heap alanında bir objeyi işaret ediyor. Bu kodu çalıştırdığımızda, referansın testMethod yöntemi içinde çeşitli işlemlere maruz kaldığını göreceksiniz. Yalnız yöntemden çıktıktan sonra, bu referansın yönteme girmeden önce sahip olduğu değeri(burada primitive olmayan türlerde yani kompleks türlerde referansın tuttuğu değer aslında objenin id’sidir.) muhafaza ettiği de anlaşılıyor. Peki değişen tam olarak nedir ve burada pass by value ve pass by reference yaklaşımlarından hangisi uygulanıyor?

Aslında yöntemin içine girildiğinde someObject referansının bir kopyası bu yönteme geçer. Bu kopya-referans da, orijinal referans gibi hafızada bir yer kaplar ve tıpkı orijinal referans gibi heap alanında aynı objeye işaret eder. Haliyle bir kopya referans da olsa, kopya referans üzerinde yapılan işlemler, referansın heap alanında işaret ettiği nesneyi de etkileyecektir. Ama referansın sahip olduğu değeri, yani heap alanında işaret ettiği nesnenin adresini(aslında bu başka dillerde adres olarak nitelendiği için adres ifadesini kullandım. Java tarafında bu adresten ziyade, objenin id’sidir.) etkilemeyecektir. Verilen örnekte kopya referansın(someObjectX) tuttuğu obje metota ilk geçirildiğinde orijinal referans ile aynı değeri tutmasına rağmen metot içinde yeni bir obje atanarak değişmiştir. Tabii ki bu değişim orijinal referansı etkilemez. Sanırım orijinal referans ve kopya referans‘ın stack’de tuttuğu id’ler değişmediği için, java’da objeler(yani primitive olmayan türler) için pass by value yaklaşımının olduğu sanılır.(Makele sonundaki nota bakmanınız öneririm) Ama java’da objeler için pass-by-reference yaklaşımı daha doğrudur.

Örneğin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class JSample {
    public static void main(String[] args) {
      SomeObject someObject = new SomeObject("object 1");
      System.out.println(someObject);
      testMethod(someObject);
      System.out.println(someObject);
    }
    public static void testMethod(SomeObject someObject) {
        someObject.setName("o1");
        someObject = new SomeObject("object 2");
        someObject.setName("o2");
    }
}
 class SomeObject {
   private String name;
   public SomeObject(String x){

   }

   public void setName(String name){
     this.name = name;
   }
}

Kodu online bir editör olan pythontutor’da çalıştırmak isterseniz, bu linki ziyaret edebilirsiniz.

buradaki setName işlevi, someObject referansının değerini(biliyorsunuz tuttuğu değer objenin id’siydi) değil, referansın heap’te işaret ettiği nesnenin özelliğini/state’ini (property/field) değiştirir. Yani özellikten kasıt, ilgili nesnenin örnek değişkeni(instance variable) olan name değişkenidir. Bu arada someObject referansının değeri aynı kalır. Çünkü metoda geçirilen değişken aslında someObject değişkeninin bir kopyasıdır.

Aşağıdaki kod bloğu az önce vermiş olduğumuz örneğin hemen hemen aynısıdır. Sadece özellikle belirtmek için metot içine aldığımız parametreyi farklı göstermek için ismini someObjectX olarak değiştirdim. Yukarıdaki örnekte de bu değişikliği dilerseniz yapabilirsiniz.

1
2
3
4
5
6
7
8
SomeObject someObject = new SomeObject("object 1");
testMethod(someObject);

public static void testMethod(SomeObject someObjectX) {
    someObjectX.setName("o1");
    someObjectX = new SomeObject("object 2");
    someObjectX.setName("o2");
}

Farz edelim ki referansın heap alanıdaki adresi 121 rakamı olsun.


Java object Variable(java nesne değişkeni), Java pass by value (java'da değer geçirme)

  • 1.satırda: heap alanında new anahtar kelimesi yardımıyla bir someObject objesi yaratılır. Bu objeyi stack’da someObject referansı temsil etmektedir. Hayali verdiğimiz 121 rakamı(yani objenin heap alanıdaki adresi) bu referansa değer olarak geçirilir. (Bir üstteki şekil)


Java object Variable(java nesne değişkeni), Java pass by value (java'da değer geçirme)

  • 2.satırda: ise someObject referansı testMethod yöntemine geçer. Yani aslında bu referansın bir kopyası testMethod yöntemine geçecektir. (Bir üstteki şekil)

  • 4.satırda: burada someObjectX isminde bir kopya-referans oluşturulur. Bu referans/değişken someObject referansında olduğu gibi 121 değerine sahiptir. Her ne kadar kopya-referans olsa da heap alanında yine aynı objeyi işaret edeceğini unutmayın. (Bir üstteki şekil)


Java object Variable(java nesne değişkeni), Java pass by value (java'da değer geçirme)

  • 5.satırda: ise someObjectX referansının heap alanında işaret ettiği nesnenin name özelliği(yani nesnenin üye değişkeni(instance variable)) o1 olarak güncelleniyor. someObject referansı da heap’teki aynı nesneyi işaret ettiği için haliyle bu güncellemeden dolaylı yoldan etkilenmiş olur ama sahip olduğu değerde(yani 121‘de) bir değişiklik olmaz. (Bir üstteki şekil)


Java object Variable(java nesne değişkeni), Java pass by value (java'da değer geçirme)

  • 6.satırda: bu satırda yeni birsomeObject objesi yaratılır ve kopya-referansımız olan someObjectX referansı/değişkeni artık yeni yaratılan bu nesneyi işaret etmeye başlar. Burada bir başka değişen şey ise someObjectX referansının değeridir. Bu referans yeni objenin adresi olan 119 rakamını saklamaya başlar. Buna karşın someObject referansı ise hâlen 121 adresini(aslında java’da bunun adres değil id old. belirtmiştim.) muhafaza etmektedir. (Bir üstteki şekil)


Java object Variable(java nesne değişkeni), Java pass by value (java'da değer geçirme)

  • 7.satırda: 7.satırda ise someObjectX referansının işaret ettiği nesnenin name özelliği(yani nesnenin üye değişkeni) o2 olarak güncellenmektedir. (Bir üstteki şekil)


Java object Variable(java nesne değişkeni), Java pass by value (java'da değer geçirme)

  • 3.satırda: Son olarak bir ek bilgi daha verecek olursak, testMethod yönteminden çıktıktan sonra, yani bu yöntemin kapsamı dışına çıktıktan sonra, someObjectX referansı yok olacak, bu referansın işaret ettiği nesne ise sahipsiz(yani referanssız) kaldığı için garbage collector’un inisiyatifine kalacaktır(garbage collector bu nesneyi gerekirse hemen de siler, ya da silmek için hazır bekletir). Sadece someObject referansı ve bu referansın işaret ettiği nesne kalacaktır. (Gerekli nesne temizleme işlemleri garbage collector tarafından gerekirse gerçekleştirilir.)

Birtakım Notlar:


(Yalnız şunu belirtmek istiyorum. someObject objesi oluştururken constructor’a geçirdiğim “object 1” ve metot içindeki objede geçirilen “object 2” string değerlerini constructor içinde obje instance variable’larını ilklendirmek için kullanabilirdim. Sanırım başta öyle düşünüp sonrasında constructor içinde ilklendirmeyi unuttuğumdan öyle kalmış.

Sadece belirtmek istedim. constructor içinde ilklendirmiş olsaydım, obje ilk oluştuğunda name instance variable’a karşılık gelen yer boş olmaz, bu değerlerle dolu olurdu. Sonrasını zaten biliyorsunuz. Bu boş olan yerlere o1 ve o2 değerleri geliyor.)

Anlaşılacağı üzere, someObject referansı nesnenin kendisini tutmuyor, referansın değeri sadece nesneyi tanımlayan bir işaretçidir(yani nesnenin heap’deki adresidir). 121 sayısı aslında yönteme geçirilir

ÖNEMLİ NOT : Aslında oracle’ın kendi dökümantasyonunda, referans türleri için de pass-by-value yaklaşımının olduğu yazılmaktadır. Oracle, metoda geçirilen referans türleri için pass-by-value tanımını şu şekilde izah etmektedir.

Reference data type parameters, such as objects, are also passed into methods by value. This means that when the method returns, the passed-in reference still references the same object as before. However, the values of the object’s fields can be changed in the method, if they have the proper access level.

“Metot dönüş yaptığında, metota geçirilen referansın hâlen eski objeyi işaret etmesi pass-by-value kullandığını göstermektedir”, şeklinde bir ifadeyi göreceksiniz. Yani metoda geçirilen aslında kopya bir referans olduğu için bu söylenmektedir. Zaten yukarıda bunu izah etmiştim. Kopya referans, metot içerisinde başka bir objeye atansa bile, orijinal referans metot öncesindeki objeye işaret etmeye devam edecektir. Aslında birçok kaynak buna sezgisel olarak pass-by-referans olarak ifade eder. Bana kalırsa da bu tanım daha doğrudur. Tercihi yine de size bırakıyorum.

Referanslar