İçeriğe geç
academia.sh

Ders 02 / 11

Bellek Alanları

Aynı iş parçacığının yazdığı altı değerden üçü başka bir iş parçacığına hiç görünmez, üçü görünür — ayrımı belirleyen değerin türü değil durduğu yerdir: yerel değişken ve parametre çağrı çerçevesinde, nesne alanı ve dizi ögesi öbekte, statik alan sınıfa ait alanda durur. Paylaşılan olmak görülecek olmak değildir: bir sıralama aracı olmadan aynı okuma tanımsızdır.

İçindekiler

Önceki derste sınıfın çalışma zamanına kimin elinde girdiği ölçüldü: hangi yükleyicinin tanımladığı, hangi iş parçacığının başlattığı. Ama başlatılan sınıfın alanları nerede durduğu, ve bir iş parçacığının yazdığı değeri başka bir iş parçacığının görüp göremediği hiç sorulmadı. Bu ders o soruyu sorar: bir değeri kaç iş parçacığı görebiliyor?

Yanıt değerin türüne bakılarak verilemez — int de olsa aynı int, bir yerde yazan iş parçacığının dışına hiç çıkmaz, başka bir yerde her iş parçacığına açık durur. Ayrımı yapan şey değerin durduğu yerdir.

Bu ayrımın önemi, iki iş parçacığının aynı yöntemi aynı anda çağırdığı sıradan bir durumda görülür. İki iş parçacığı parametreYaz(5, ...) yöntemini tam olarak aynı anda çağırsa bile, ikisi de kendi p parametresini kendi çerçevesinde tutar; biri ötekinin p’sini hiçbir şekilde okuyamaz, çünkü ikisinin çerçevesi ayrı adres uzayında değil ama ayrı çerçeve zincirinde durur. Bu, programcının bir şey yapmasıyla kurulan bir izolasyon değildir — dilin kendisi tarafından, hiçbir kilide ve hiçbir anahtar sözcüğe gerek kalmadan sağlanır. Öbek paylaşımı ise tam tersi yönde çalışır: bir nesnenin öbekte paylaşılması, o nesnenin referansının başka bir iş parçacığına verilmiş olmasına bağlıdır; referans hiç paylaşılmazsa öbekteki bir nesne de yalnız tek bir iş parçacığına görünür kalabilir. Çağrı çerçevesi izolasyonu yapısaldır, öbek paylaşımı ise bir tasarım kararıdır.

Dört Yer: Çağrı Çerçevesi, Öbek ve Sınıfa Ait Alan

Bu konunun kataloğundaki alt başlık bellek alanlarını “yığın, yığıt ve meta alan” diye anar. Bu platformda yığın bir veri yapısının (öncelik ağacının) adıdır ve bellek alanı için kullanılmaz; düzyazı burada ve bundan sonra hep öbek yazar. Üç alanın üçü de JVM belirtiminin kendi çalışma zamanı veri alanları arasında adı geçen alanlardır: her iş parçacığının kendi yığıtı, bütün iş parçacıklarının paylaştığı tek bir öbek, ve yine bütün iş parçacıkları arasında paylaşılan tek bir yöntem alanı. Belirtim bu üç alanın nasıl gerçekleştirileceğini söylemez — yalnız hangi verinin hangi alana ait olduğunu söyler; bu ders de yalnız o ayrıma bakar, gerçekleştirime bakmaz.

Çalışma zamanı bir değeri üç ayrı alandan birine koyar. Bir yöntem çağrıldığında, o iş parçacığının kendi çağrı çerçevesi açılır ve yöntemin yerel değişkenleri ile parametreleri oraya yazılır; her iş parçacığının kendi çerçeve zinciri vardır, biri ötekininkine erişemez. Çerçeve, çağrı bittiğinde de kapanır: bir yöntem özyinelemeli çağrılırsa aynı iş parçacığında aynı yöntemin birden çok çerçevesi üst üste durur, ama hiçbiri bir başka iş parçacığının çerçeve zincirine karışmaz. Bir nesne new ile üretildiğinde alanları ve bir dizi üretildiğinde ögeleri öbeğe yazılır; öbek bütün iş parçacıklarının paylaştığı tek bir alandır ve bir nesneye giden referans hangi iş parçacığına aktarılırsa o iş parçacığı da aynı alanlara ulaşır. Bir sınıfın kendi statik alanları ise örneklerden ayrı, sınıfın kendisine ait bir alanda durur — JVM belirtiminin yöntem alanı, güncel adıyla meta alan; bu alan da bütün iş parçacıkları arasında tektir ve paylaşılır. Statik bir alanın var olması için sınıftan tek bir örnek bile üretilmiş olması gerekmez — meta alandaki yer sınıf başlatıldığı an açılır, örnek sayısından bağımsızdır.

ThreadLocal dördüncü bir örüntüdür: tuttuğu değer fiziksel olarak öbekte durur, ama her iş parçacığı kendi kaydına erişir — erişim yolu iş parçacığının kendisinden geçer. Bu yüzden sınıf olarak öbekte olsa bile davranış olarak çağrı çerçevesindeki bir değere benzer. Ama ömürleri aynı değildir: yerel değişken kendi çerçevesi kapanınca yok olur, bir sonraki çağrının hiçbir izini taşımaz. ThreadLocal kaydı ise iş parçacığı sürdüğü sürece kalır — aynı iş parçacığı aynı yöntemi ikinci kez çağırdığında kaydı hâlâ oradadır, yeniden yazılmadıkça bir önceki çağrıdan kalan değeri okur. “Yalnız” sütununa ikisi de girer, çünkü ikisi de başka bir iş parçacığına görünmez; ama biri çağrıyla doğup çağrıyla ölür, öbürü iş parçacığıyla birlikte yaşar.

Dizi ögesinin öbekte durması da aynı kuraldan gelir: Java Temelleri kursunun kurduğu gibi dizi kendisi bir nesnedir, bu yüzden bir dizinin ögeleri de tıpkı bir nesnenin alanları gibi öbekte durur ve aynı referans hangi iş parçacığına ulaşırsa o iş parçacığı da aynı ögelere erişir.

  • JV8 — Bir değerin “paylaşılan” sayılması, yazan iş parçacığının bitmesi beklendikten sonra okunan bir değerle belirlenir (join ile); bitmeden okunan bir değer bu ölçüme girmez.
  • JV9 — Yerel değişken ve yöntem parametresi için “yalnız” sonucu bir gözlemden değil bir imkânsızlıktan gelir: başka bir iş parçacığının bu iki değeri okuyacağı hiçbir adres yoktur, dolayısıyla denenecek bir çağrı bile yazılamaz.
  • JV10 — Ölçülen altı değer aynı yazan iş parçacığı tarafından, aynı koşumda, aynı sırayla yazılır; hiçbiri ayrı bir koşumdan gelmez.
  • JV11 — Ortama bağlı hiçbir veri okunmaz: yığıt derinliği, öbek boyutu, adres yazılmaz; kimlik yalnız değer eşitliğiyle (== 5) okunur.

Altı Değerin Üçü Yalnız, Üçü Paylaşılan

Ölçüm bir nesne yazar (alan ve dizi öbekte), bir sınıf statik alanı yazar (meta alanda), bir ThreadLocal yazar, ve aynı iş parçacığının yerel bir değişkeni ile bir yöntem parametresini yazar. Hepsi tek bir yazan iş parçacığında olur; okuma o iş parçacığı bittikten sonra, ana iş parçacığından yapılır.

// GorunurlukKutusu.java — bir nesne alani ve bir dizi ogesi tasir
class GorunurlukKutusu {
    int alan = 0;
    final int[] dizi = { 0 };
}
// GorunenDegerler.java — dort yerdeki alti deger: kaci yazan is parciginin disina goruyor
import java.util.concurrent.CountDownLatch;

public class GorunenDegerler {
    static int statikAlan = 0;
    static final ThreadLocal<Integer> ozel = ThreadLocal.withInitial(() -> 0);
    static int yalniz = 0, paylasilan = 0;

    static void parametreYaz(int p, int[] hedef) { hedef[0] = p; }

    static void goren(String etiket, boolean paylasildi) {
        if (paylasildi) paylasilan++; else yalniz++;
        System.out.printf("%-26s %s%n", etiket, paylasildi ? "paylasilan" : "yalniz");
    }

    public static void main(String[] args) throws InterruptedException {
        GorunurlukKutusu kutu = new GorunurlukKutusu();
        CountDownLatch yazildi = new CountDownLatch(1);
        int[] parametreGorulen = { -1 };

        Thread yazan = new Thread(() -> {
            int yerel = 5;
            ozel.set(5);
            statikAlan = 5;
            kutu.alan = 5;
            kutu.dizi[0] = 5;
            parametreYaz(5, parametreGorulen);
            if (yerel != 5) throw new IllegalStateException();
            yazildi.countDown();
        });
        yazan.start();
        yazildi.await();
        yazan.join();

        // yerel degisken ve parametre: baska bir is parciginin bunlari okuyacagi hicbir
        // adres yok, dolayisiyla denenecek bir cagri bile yazilamaz.
        goren("yerel degisken", false);
        goren("yontem parametresi", false);
        goren("is parcacigina ozel deger", ozel.get() == 5);
        goren("nesne alani", kutu.alan == 5);
        goren("dizi ogesi", kutu.dizi[0] == 5);
        goren("statik alan", statikAlan == 5);
        System.out.printf("gorunurluk: %d yalniz, %d paylasilan%n", yalniz, paylasilan);
    }
}
yerel degisken             yalniz
yontem parametresi         yalniz
is parcacigina ozel deger  yalniz
nesne alani                paylasilan
dizi ogesi                 paylasilan
statik alan                paylasilan
gorunurluk: 3 yalniz, 3 paylasilan

Altı değerin üçü yazan iş parçacığının dışına hiç çıkmadı, üçü çıktı. İlk iki satır hiç koşum gerektirmedi: yerel ve parametreYaz’ın parametresi yazan iş parçacığının kendi çerçevesinde yaşar ve öldüğünde çerçeveyle birlikte yok olur; ana iş parçacığının bunları okuyacağı bir değişken adı, bir alan, bir yöntem — hiçbiri yoktur. Koddaki if (yerel != 5) throw satırı bunu doğrular: yerelin kendisi yalnız kendi çerçevesi içinde, bir doğrulama olarak kullanılabilir; çerçeve dışına hiçbir biçimde aktarılmaz. parametreYaz çağrısı ise parametre değerini parametreGorulen adlı paylaşılan bir diziye kopyalayarak ana iş parçacığına taşıdı — ama taşınan şey parametrenin kendisi değil, kopyasıdır; ölçüm bu kopyayı hiç okumadı, çünkü sorulan soru “değer başka bir yere kopyalanabilir mi” değil “parametrenin durduğu yer başka bir iş parçacığına görünür mü” sorusudur, ve yanıtı hayırdır.

Üçüncü satır koşum gerektirdi ve sonucu da aynı yöndeydi: ozel.get() ana iş parçacığında çağrıldığında kendi ThreadLocal kaydını okur, yazan iş parçacığının kaydını değil; iki iş parçacığının aynı adı taşıyan iki ayrı değeri vardır ve 5 == 0 yanlış çıkar. Son üç satır ise kutu.alan, kutu.dizi[0] ve statikAlan’ın hepsinde true verdi: nesne öbekte tek bir yerde durur, iki iş parçacığı da aynı referansı taşır; statik alan sınıfın kendi alanında tek bir yerde durur, iki iş parçacığı da aynı sınıfa bakar.

Nesneye Ait Alan ile Sınıfa Ait Alan Arasındaki Fark

Bir önceki ölçüm tek bir GorunurlukKutusu nesnesi kullandı; “öbekte tek bir yerde durmak” ile “sınıfa ait alanda tek bir yerde durmak” aynı şey gibi görünebilir. Aradaki fark iki nesne üretilince ortaya çıkar.

// IkiOrnek.java — nesneye ait alan her ornekte ayri, sinifa ait alan tek bir yerde
public class IkiOrnek {
    public static void main(String[] args) {
        GorunurlukKutusu birinci = new GorunurlukKutusu();
        GorunurlukKutusu ikinci = new GorunurlukKutusu();
        birinci.alan = 1;
        ikinci.alan = 2;
        GorunenDegerler.statikAlan = 99;

        System.out.println("birinci.alan                : " + birinci.alan);
        System.out.println("ikinci.alan                 : " + ikinci.alan);
        System.out.println("iki nesnenin alani ayni mi  : " + (birinci.alan == ikinci.alan));
        System.out.println("iki nesne var, statikAlan yine de tek: " + GorunenDegerler.statikAlan);
    }
}
birinci.alan                : 1
ikinci.alan                 : 2
iki nesnenin alani ayni mi  : false
iki nesne var, statikAlan yine de tek: 99

İki GorunurlukKutusu nesnesi üretildiğinde alan alanından da iki ayrı kopya çıktı; biri 1, öbürü 2 tuttu ve ikisi eşit değil. alan, öbekte nesne başına bir yer kaplar — kaç nesne üretilirse o kadar kopyası olur. statikAlan ise GorunenDegerler sınıfının kendi alanıdır ve nesne sayısından bağımsızdır: sıfır nesne üretilse de var olurdu, iki nesne üretilince de tek kalır. Önceki ölçümde “paylaşılan” sayılan üç satırın ikisi (nesne alanı, dizi ögesi) paylaşılmayı aynı nesneye referansı taşımaktan alıyordu; üçüncüsü (statik alan) paylaşılmayı sınıfın kendisinden alıyordu. İkisi de öbek ya da meta alanda tek bir yerde durduğu için paylaşılıyor, ama “tek yer” ikisinde de farklı bir şeye bağlı: birinde nesnenin kimliğine, öbüründe sınıfın kimliğine.

Bu fark yeni bir sınıf tasarlarken doğrudan bir sonuç doğurur: bir alanı bilerek paylaşılan yapmak isteyen kod onu static yazar — o zaman paylaşım nesne sayısına bakmaksızın kurulmuş olur; bir alanı yalnız kendi nesnesine bağlı tutmak isteyen kod ise onu örnek alanı olarak bırakır ve nesneyi hiçbir zaman ikinci bir iş parçacığına aktarmaz. İki durumda da JVM’in kendisi bir seçim yapmaz; alanın static olup olmadığı ve referansın paylaşılıp paylaşılmadığı, ikisi de kaynak kodda yazılı bir karardır.

Sınırlayıcı Ölçüm: Paylaşılan Olmak Görülecek Olmak Değildir

Yukarıdaki üç paylasilan satırı bir koşullu okumayla elde edildi: yazan.join() çağrısı, yazma ile okuma arasına bir sıra koydu ve okuma yalnız yazma kesin bitince yapıldı. Bu sıra olmasaydı — örneğin ana iş parçacığı yazan.start()’tan hemen sonra, join çağırmadan statikAlan‘ı okusaydı — aynı üç satırın ne vereceği tanımsızdır: yazan iş parçacığının henüz yazmadığı eski değer (0) de görülebilir, yeni değer (5) de görülebilir. Tek bir int yazması Java’da atomiktir, yani okunan değer yarı yazılmış bir çöp olmaz; asıl sorun atomiklik değil sıralamadır — derleyicinin ve işlemcinin, gözlemlenebilir tek iş parçacıklı davranışı bozmadığı sürece komutları yeniden sıralamasına izin verilir, ve bu yeniden sıralama iki iş parçacığı arasında hangi yazmanın hangi okumadan önce sayılacağını belirsiz bırakabilir. Bu, ölçülüp raporlanacak bir sayı değil, dilin tanımsız bıraktığı bir durumdur; bu yüzden ders böyle bir okumayı çalıştırmaz, yalnız var olduğunu adlandırır — bu kursun kuralı bir yarış koşulunu koşturmak değil, modellemektir.

Statik alan “paylaşılan” olarak sayıldı çünkü öbekte tek bir yerde duruyor ve iki iş parçacığı da aynı adrese bakıyor — ama tek bir yerde durmak, o yerdeki en güncel değerin görüleceğinin garantisi değildir. “Aynı yerde durmak” bir konum olgusudur ve derleme zamanında bellidir; “görünür olmak” bir sıralama olgusudur ve yalnız çalışma zamanında, aradaki sıralama aracına bağlı olarak kurulur. Görünürlüğün hangi kurala bağlı olduğu — hangi yazmanın hangi okumadan önce sayılacağı, volatile ve kilit gibi araçların bu sırayı nasıl garantilediği — Eşzamanlılık konusunun Java Bellek Modeli dersinin konusudur; bu ders yalnız görünürlüğün sıfırdan kurulması gerektiğini, join gibi bir sıralama aracı olmadan sıfır olduğunu gösterir.

Yığıt derinliği ve öbek boyutu bu derste ölçülmez: ikisi de çalıştığı makineye göre değişen ortama bağlı verilerdir. Ölçülen tek şey bir değerin kaç iş parçacığına göründüğüdür — konum bilgisi (dört yerden hangisi) derleme zamanında bellidir ve koşumdan koşuma değişmez; görünürlük bilgisi (o konumdaki en güncel değerin okunup okunamayacağı) ise ancak doğru bir sıralama aracıyla birlikte anlamlıdır ve bu ders onu yalnız aracın var olduğu durumda ölçer.

Özet

  • Bir değer dört yerden birinde durur: yerel değişken ve parametre çağrı çerçevesinde, nesne alanı ve dizi ögesi öbekte, statik alan sınıfa ait alanda (meta alanda); ThreadLocal öbekte durur ama erişimi iş parçacığının kendisinden geçer.
  • Bu depoda yığın bir veri yapısının adıdır, bellek alanı için öbek kullanılır.
  • Altı ölçülen değerin üçü (yerel değişken, parametre, iş parçacığına özel değer) yazan iş parçacığının dışına hiç çıkmaz, üçü (nesne alanı, dizi ögesi, statik alan) çıkar.
  • Yerel değişken ve parametre için “yalnız” sonucu bir gözlem değil bir imkânsızlıktır: başka bir iş parçacığının onları okuyacağı bir adres yoktur.
  • Paylaşılan olmak görülecek olmak değildir: join gibi bir sıralama aracı olmadan aynı okumanın sonucu tanımsızdır; bu yüzden ders sırasız bir okumayı hiç çalıştırmaz.
  • Yığıt derinliği ve öbek boyutu ortama bağlıdır ve ölçülmez.

Sonraki Adım

Bu derste öbekte duran değerlerin hâlâ erişilebilir olduğu varsayıldı — kutu değişkeni hep elde tutuldu, hiçbir referans bırakılmadı, birinci ve ikinci de mainin kendi çerçevesinde son satıra kadar canlı kaldı. Ama bir nesneye giden son referans de kaybolabilir; program bir nesneyi bırakır, o nesneye artık hiçbir çerçeveden, hiçbir statik alandan ulaşılamaz hâle gelir, ve öbekte artık kimsenin okuyamadığı bir alan kalır. Sıradaki ders bu durumu ölçer: erişilebilirliğini yitiren bir nesnenin temizliğini hangi iş parçacığı üstleniyor — ne çağıran, ne programın oluşturduğu bir iş parçacığı, kayıtta hiç olmayan bir taraf.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat