İçeriğe geç
academia.sh

Ders 01 / 11

Sınıf Yükleme

Yükleyici hiyerarşisi yetki devriyle çalışır: bir yükleyiciye sorulan sınıfı çoğu zaman üstteki yükleyici tanımlar, kimlik yalnız `==` ile okunur. Dört tetikleyicinin üçü sınıf başlatmasını koşturur, biri (derleme zamanı sabiti okumak) hiç koşturmaz; aynı sınıfa aynı anda dokunan iki iş parçacığından yalnızca biri başlatma bloğunu çalıştırır, öbürü bitmesini bekler.

İçindekiler

Standart Kütüphane ve Akışlar kursu son dersini bir garantiyle kapattı: bir davranışın arkasında artık tek bir iş parçacığı durmuyordu, aynı nesneye aynı anda erişen birden çok iş parçacığı vardı. Bu kurs o garantinin tam olarak ne olduğunu soracak, ama önce ondan daha küçük ve yine de yalnız koda bakarak yanıtlanamayan bir soru var: bu satırı hangi iş parçacığı koşturuyor? Soru, yazdığımız ilk satırdan önce başlıyor. main çalışmaya başlamadan önce bile bir sınıfın çalışma zamanı biçimine girmesi gerekir — yüklenmesi ve başlatılması — ve bu işi de bir iş parçacığı yapar: sınıfa ilk dokunan iş parçacığı.

Bu ders iki soruyu ayırır. Birincisi: bir sınıf istendiğinde onu hangi yükleyici tanımlar? İkincisi: başlatma bloğunu hangi iş parçacığı koşturur, ve aynı sınıfa aynı anda dokunan ikinci bir iş parçacığına ne olur?

Yükleyici Hiyerarşisi ve Yetki Devri

Bir sınıf istendiğinde JVM onu tek bir yükleyiciye değil bir yükleyici zincirine sorar. Zincirdeki her yükleyicinin bir üstü (parent) vardır; zincirin en tepesinde JVM’in kök yükleyicisi durur ve bu yükleyici bir Java nesnesi olarak temsil edilmez — getClassLoader() çağrısı onun için null döner. Bir sınıfı isteyen yükleyici (initiating loader) kendi işini önce üstüne devreder: üstüne sorar, üst de kendi üstüne sorar. Zincirde bir yükleyici sınıfı gerçekten tanımlayabilirse (defineClass çağrısını yapabilirse) o sınıfın tanımlayan yükleyicisi (defining loader) odur. Bu yetki devri (delegation) ilkesidir ve ClassLoader.loadClass yönteminin varsayılan davranışıdır — hiçbir satır yazmadan, yalnız bir yükleyicinin üstünü belirterek devralınır.

Yetki devrinin sonucu isteyen ile tanımlayanı ayırabilir: bir yükleyiciye bir sınıf sorulur, ama sınıfı üstteki bulur ve tanımlarsa isteyen ile tanımlayan aynı nesne değildir. Bu fark çağrı yazımında görünmez — alt.loadClass("X") çağrısı her koşulda aynı satırdır; hangi yükleyicinin tanımladığı yalnız dönen Class nesnesinin getClassLoader()’ı == ile karşılaştırılarak görülür.

  • JV1 — Yükleyici kimliği yalnız == karşılaştırmasıyla okunur; yükleyicinin adı, kimlik karması ya da toString çıktısı hiçbir yerde basılmaz.
  • JV2 — Derleyici, standart kitaplığın araç sağlayıcı arayüzünden program içinden çağrılır; iki küçük sınıf iki ayrı dizine derlenir ve bu iki dizin dışında hiçbir kaynak ölçüme katılmaz.
  • JV3 — Üstteki yükleyicinin bu ölçümde kendi üstü yoktur; zincirin ne kadar yükseldiği değil, hangi katmanın tanımlayan olduğu ölçülür.
// Delegeli.java — yetki devriyle calisan yukleyici: findClass override edilir, loadClass devralinir
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

class Delegeli extends ClassLoader {
    private final Path dizin;

    Delegeli(Path dizin, ClassLoader ust) {
        super(ust);
        this.dizin = dizin;
    }

    protected Class<?> findClass(String ad) throws ClassNotFoundException {
        try {
            byte[] bayt = Files.readAllBytes(dizin.resolve(ad + ".class"));
            return defineClass(ad, bayt, 0, bayt.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(ad);
        }
    }
}

Delegeli yalnız findClass’ı yeniden tanımlar; loadClass’a hiç dokunulmaz. Paylasilan sınıfının bayt kodu yalnızca üstteki yükleyicinin baktığı dizinde durur, Ozelin bayt kodu yalnızca alttakinin baktığı dizinde. Böylece iki dizin, iki ayrı yükleyicinin görebildiği iki ayrı kaynağı temsil eder.

// Yukleyiciler.java — isteyen yukleyici ile tanimlayan yukleyici kimlik karsilastirmasiyla ayrilir
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.file.*;
import java.util.spi.ToolProvider;

public class Yukleyiciler {
    static void derle(String kaynak, String sinif, Path dizin) throws Exception {
        Path d = Files.createTempDirectory("kaynak");
        Path k = d.resolve(sinif + ".java");
        Files.writeString(k, kaynak);
        Files.createDirectories(dizin);
        PrintWriter bos = new PrintWriter(Writer.nullWriter());
        ToolProvider.findFirst("javac").orElseThrow()
                .run(bos, bos, "-d", dizin.toString(), k.toString());
    }

    public static void main(String[] args) throws Exception {
        Path ustDizin = Files.createTempDirectory("ust");
        Path altDizin = Files.createTempDirectory("alt");
        derle("class Paylasilan { static int deger() { return 1; } }", "Paylasilan", ustDizin);
        derle("class Ozel { static int deger() { return 2; } }", "Ozel", altDizin);

        ClassLoader ust = new Delegeli(ustDizin, null);
        ClassLoader alt = new Delegeli(altDizin, ust);

        Class<?> c1 = alt.loadClass("Paylasilan");
        Class<?> c2 = alt.loadClass("Ozel");

        System.out.println("Paylasilan sinifini tanimlayan : "
                + (c1.getClassLoader() == ust ? "ustteki" : c1.getClassLoader() == alt ? "kendisi" : "baska"));
        System.out.println("Ozel sinifini tanimlayan       : "
                + (c2.getClassLoader() == alt ? "kendisi" : c2.getClassLoader() == ust ? "ustteki" : "baska"));
        System.out.println("kok yukleyici temsili (null)   : " + (Object.class.getClassLoader() == null));
    }
}
Paylasilan sinifini tanimlayan : ustteki
Ozel sinifini tanimlayan       : kendisi
kok yukleyici temsili (null)   : true

Aynı alt.loadClass(...) çağrısı iki kez yapıldı ve iki ayrı yanıt geldi. Paylasilan alt yükleyiciye soruldu, ama bulan üstteki yükleyiciydi — yetki devri üstte durdu ve tanımlayan alt değil ust çıktı. Ozel üstteki yükleyicinin baktığı dizinde yoktu; devir başarısız olunca istek alta geri döndü ve tanımlayan bu kez alt’ın kendisi oldu. Aradaki tek fark, ikinci sınıfın bayt kodunun üstte hiç bulunmamasıydı — kodda hiçbir dallanma bunu söylemiyor. Object.class.getClassLoader() çağrısı null verdi: zincirin en üstünde, hiçbir Java nesnesiyle temsil edilmeyen kök yükleyici duruyor.

Sınıf Başlatmasını Hangi İş Parçacığı Koşturuyor

Yükleme (bir sınıf dosyasının bulunup çalışma zamanı temsilinin kurulması) ile başlatma (statik alanlara ilk değerlerin verilmesi ve başlatma bloğunun çalıştırılması) iki ayrı olaydır ve iki ayrı anda olabilir. JVM bir sınıfı istendiği anda değil, gerçekten kullanılacağı anda başlatır ve tam olarak bir kez başlatır. İki iş parçacığı aynı sınıfa aynı anda dokunursa biri başlatma bloğunu koşturur, öbürü bitene kadar bekler — sıra çizelgeleyiciye bırakılmaz, bu bir dil garantisidir ve JVM içinde örtük bir kilitle sağlanır.

Hangi eylemlerin bir sınıfı gerçekten başlattığı da çağrı yazımına bakarak görünmez: dört aday eylemden üçü başlatmayı tetikler, biri hiç tetiklemez.

  • JV4 — Bir sınıfın başlatılıp başlatılmadığı kendi kurduğumuz sayaçla izlenir; sayaç yalnız başlatma bloğunun gövdesi gerçekten çalıştığında artar, sınıfın yüklenmesiyle artmaz.
  • JV5 — Dört tetikleyici dört ayrı sınıf üzerinde denenir, çünkü bir sınıf en çok bir kez başlar; aynı sınıfı iki tetikleyiciyle sınamak ikinci denemeyi anlamsız kılar.
  • JV6 — İki iş parçacığının aynı sınıfa aynı anda dokunması iki CountDownLatch ile kurgulanır: biri ikisinin de hazır olduğunu, öbürü ikisinin de aynı anda serbest bırakıldığını garanti eder; aradaki sıra çizelgeleyiciye bırakılmaz.
  • JV7 — Yarışan iki iş parçacığı da aynı etiketle (“oluşturulan”) kayıtlıdır; hangisinin kazandığı bu ölçümün konusu değildir, kazananın kayıtta olup olmadığı konudur.
// Bas.java — sinif baslatmasini hangi is parcacigi kosturuyor: kendi kaydimizla izlenir
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

class Bas {
    static final AtomicInteger sayac = new AtomicInteger(0);
    static final List<String> gorulen = Collections.synchronizedList(new ArrayList<>());
    static final Map<Thread, String> etiketler = Collections.synchronizedMap(new IdentityHashMap<>());

    static void tanit(Thread t, String etiket) { etiketler.put(t, etiket); }

    static String kim() {
        String e = etiketler.get(Thread.currentThread());
        return e == null ? "bilinmeyen" : e;
    }

    static void gozle(String tetikleyici) {
        sayac.incrementAndGet();
        gorulen.add(tetikleyici);
        System.out.printf("%-30s baslatici kosuyor: %s%n", tetikleyici, kim());
    }
}

Dört tetikleyici dört ayrı sınıfa yazılır; her sınıfın başlatma bloğu yalnız Bas.gozle’yi çağırır.

// YeniOrnekTetigi.java — yeni ornek olusturma baslatmayi tetikler mi
class YeniOrnekTetigi { static { Bas.gozle("yeni ornek olusturma"); } }
// StatikYontemTetigi.java — statik yontem cagirma baslatmayi tetikler mi
class StatikYontemTetigi { static { Bas.gozle("statik yontem cagirma"); } static void yontem() { } }
// StatikAlanTetigi.java — final olmayan statik alan okuma baslatmayi tetikler mi
class StatikAlanTetigi { static { Bas.gozle("statik alan okuma"); } static int deger = 5; }
// SabitTetigi.java — derleme zamani sabiti okuma baslatmayi tetikler mi
class SabitTetigi { static { Bas.gozle("derleme zamani sabiti okuma"); } static final int SABIT = 7; }

Beşinci sınıf iki ölçümde birden kullanılır: önce yüklenip başlatılmadan durabildiğini, sonra iki iş parçacığının aynı anda dokunduğunda başlatmanın tam bir kez çalıştığını gösterir.

// GecBaslatilan.java — once yuklenip baslatilmadan durabilen, sonra iki iplik tarafindan es zamanli dokunulan sinif
class GecBaslatilan { static { Bas.gozle("gec baslatilan sinif"); } static int deger = 9; }
// Tetikleyiciler.java — dort tetikleyicinin kaci baslatmayi kosturuyor, yukleme baslatmadan ayri mi, es zamanli dokunma ne veriyor
import java.util.concurrent.CountDownLatch;

public class Tetikleyiciler {
    public static void main(String[] args) throws Exception {
        Bas.tanit(Thread.currentThread(), "cagiran");

        System.out.println("-- dort tetikleyici --");
        new YeniOrnekTetigi();
        StatikYontemTetigi.yontem();
        int a = StatikAlanTetigi.deger;
        int s = SabitTetigi.SABIT;
        System.out.println("okunan sabit deger                      : " + s);
        System.out.println("yeni ornek olusturma baslatti mi         : " + Bas.gorulen.contains("yeni ornek olusturma"));
        System.out.println("statik yontem cagirma baslatti mi        : " + Bas.gorulen.contains("statik yontem cagirma"));
        System.out.println("statik alan okuma baslatti mi            : " + Bas.gorulen.contains("statik alan okuma"));
        System.out.println("derleme zamani sabiti okuma baslatti mi  : " + Bas.gorulen.contains("derleme zamani sabiti okuma"));
        System.out.println("dort tetikleyicinin kosturdugu baslatici : " + Bas.sayac.get());

        System.out.println("-- yukle, baslatma; sonra iki iplik es zamanli dokunsun --");
        ClassLoader yukleyici = Tetikleyiciler.class.getClassLoader();
        Class<?> yuklenen = Class.forName("GecBaslatilan", false, yukleyici);
        System.out.println("yalnizca yuklendi, baslatici kosan sayisi: " + Bas.sayac.get());

        CountDownLatch hazir = new CountDownLatch(2);
        CountDownLatch baslasin = new CountDownLatch(1);
        Runnable govde = () -> {
            hazir.countDown();
            try { baslasin.await(); } catch (InterruptedException e) { }
            int x = GecBaslatilan.deger;
        };
        Thread ilk = new Thread(govde);
        Thread ikinci = new Thread(govde);
        Bas.tanit(ilk, "olusturulan");
        Bas.tanit(ikinci, "olusturulan");
        ilk.start();
        ikinci.start();
        hazir.await();
        baslasin.countDown();
        ilk.join();
        ikinci.join();
        System.out.println("iki iplik dokundukten sonra Class ayni mi: " + (yuklenen == GecBaslatilan.class));
        System.out.println("baslatici toplam kac kez kostu           : " + Bas.sayac.get());
    }
}
-- dort tetikleyici --
yeni ornek olusturma           baslatici kosuyor: cagiran
statik yontem cagirma          baslatici kosuyor: cagiran
statik alan okuma              baslatici kosuyor: cagiran
okunan sabit deger                      : 7
yeni ornek olusturma baslatti mi         : true
statik yontem cagirma baslatti mi        : true
statik alan okuma baslatti mi            : true
derleme zamani sabiti okuma baslatti mi  : false
dort tetikleyicinin kosturdugu baslatici : 3
-- yukle, baslatma; sonra iki iplik es zamanli dokunsun --
yalnizca yuklendi, baslatici kosan sayisi: 3
gec baslatilan sinif           baslatici kosuyor: olusturulan
iki iplik dokundukten sonra Class ayni mi: true
baslatici toplam kac kez kostu           : 4

Dört adaydan üçü başlatmayı gerçekten koşturdu: yeni örnek oluşturmak, statik yöntem çağırmak, final olmayan bir statik alanı okumak. Dördüncüsü — derleme zamanı sabitini okumak — hiçbir satır bırakmadı; SabitTetigi sınıfının başlatma bloğu hiç çalışmadı, buna karşılık okunan değer (7) doğru geldi. Bu satır aşağıda ayrı bir bölümde ele alınıyor.

İkinci blok yükleme ile başlatmayı ayırıyor. Class.forName("GecBaslatilan", false, ...) sınıfı yükledi — bir Class nesnesi döndü — ama sayaç 3’te kaldı: başlatma bloğu çalışmadı. Yükleme, başlatmadan önce ve ondan bağımsız olarak tamamlanabiliyor.

Üçüncü blok aynı sınıfa iki iş parçacığını aynı anda gönderiyor. hazir mandalı ikisinin de başlama çizgisinde olduğunu, baslasin mandalı ikisinin de aynı anda serbest bırakıldığını garanti ediyor; hangisinin GecBaslatilan.deger okumasına önce ulaşacağı ise çizelgeleyiciye bağlı ve bilinmiyor. Buna karşılık sonuç belirlenimci: başlatıcı toplamda bir kez koştu (sayaç 3’ten 4’e çıktı, ikiye değil) ve koşturan taraf kayıtlı iki iş parçacığından biriydi — bilinmeyen değil. İkinci iş parçacığı da GecBaslatilan.deger‘i okudu, ama kendi başlatma bloğunu çalıştırmadı; JVM’in örtük kilidine takılıp birincinin bitmesini bekledi. Hangi iş parçacığının kazandığı koşumdan koşuma değişebilir, ama kazananın kayıtta olduğu ve başlatmanın tam bir kez çalıştığı değişmiyor — bu yüzden ders iki iş parçacığını da aynı etiketle kaydetti (JV7).

Sınırlayıcı Ölçüm: Derleme Zamanı Sabiti Sınıfı Hiç Başlatmaz

Yukarıdaki tabloda dördüncü satır boştu: SabitTetigi sınıfının başlatma bloğu hiç çalışmadı ve Bas.gozle hiç çağrılmadı. Nedeni SABIT alanının bildirim biçiminde: static final int SABIT = 7; bir derleme zamanı sabitidir, ve derleyici böyle bir alanı okuyan her yeri sabitin değeriyle değiştirir — Nesneye Dayalı Java kursunun ölçtüğü değer gömme. int s = SabitTetigi.SABIT; satırı sınıf dosyasında SabitTetigi’ye ait hiçbir iz bırakmaz; 7 değeri doğrudan çağrı yerine gömülür. Sınıfa hiç dokunulmadığı için başlatılacak bir şey de yoktur.

Bu, ortak ölçüm çerçevesinin kendi kör noktasının bu derste yeniden kurulmuş hâlidir: boş bir satır “başlatma olmadı” demek değildir, “tetikleyici hiç oluşmadı” demektir. Bir ölçüm yalnız beklenen satırın yokluğuna bakarak “düzenek çalışmıyor” sonucuna varamaz; önce o satırı üretecek eylemin gerçekten gerçekleştiğini doğrulamak zorundadır. Burada eylem (bir alanı okumak) gerçekleşti, ama okunan şey sınıfa değil, derleyicinin sınıfın yerine koyduğu sayıya gitti. Gözlemin yokluğu, düzeneğin yokluğu değildir.

StatikAlanTetigi ile SabitTetigi arasındaki tek fark bir final sözcüğüdür; ikisi de int türünde bir alan tutar, ikisi de aynı tetikleyici satırla okunur. Buna karşılık biri sınıfını başlatırken öbürü hiç dokunmaz — kararı veren şey alanın türü ya da değeri değil, değerinin derleme zamanında bilinip bilinmediğidir. Bu ayrım, Java Temelleri kursunun ilk dersinde ölçülen sabit katlamayla aynı kaynaktan gelir: derleyici, sonucu önceden hesaplayabildiği her yerde çalışma zamanına hiçbir iz bırakmaz.

Özet

  • Yükleyici hiyerarşisi yetki devriyle çalışır: bir sınıfı isteyen yükleyici önce üstüne sorar, sınıfı üstteki bulursa tanımlayan üstteki olur; kimlik yalnız == ile okunur, ad ve sayı basılmaz.
  • Kök yükleyici bir Java nesnesi olarak temsil edilmez; getClassLoader() onun için null döner.
  • Yükleme ile başlatma iki ayrı olaydır: bir sınıf, başlatma bloğu hiç çalışmadan yüklenebilir.
  • Dört tetikleyicinin üçü (yeni örnek, statik yöntem çağrısı, final olmayan statik alan okuma) sınıf başlatmasını koşturur; derleme zamanı sabitini okumak koşturmaz, çünkü değer çağrı yerine gömülür ve sınıfa hiç dokunulmaz.
  • Aynı sınıfa aynı anda dokunan iki iş parçacığından yalnızca biri başlatma bloğunu çalıştırır; öbürü JVM’in örtük kilidine takılıp bitmesini bekler. Kazanan taraf koşumdan koşuma değişebilir, ama her zaman kayıtlı bir iş parçacığıdır ve başlatma toplamda bir kez koşar.
  • Bir ölçümün beklediği satır çıktıda yoksa, önce o satırı üretecek eylemin gerçekten tetiklenip tetiklenmediği doğrulanır; boş satır “olmadı” değil “tetiklenmedi” anlamına gelebilir.

Sonraki Adım

Bu 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ı — GecBaslatilan.deger okundu, ama kimin yazdığı değeri gördüğü hiç sınanmadı. Sıradaki ders bunu ölçer: yerel değişken, statik alan, nesne alanı ve iş parçacığına özel bir değer dört ayrı yerde durur, ve bunlardan yalnızca bir kısmı yazan iş parçacığının dışına görünür.

İ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