Hemen hemen her programcının Matrix filmini seyrettiğini düşünüyorum. Star Wars gibi Matrix filmi de biz yazılımcılar için bir kült. Biraz abartı da olsa fikir olarak çok enteresan, en azından bir yazılımcı için. Matrix’de kullanılan yazılım sistemi dikkat çekiyor. En çok ilgimi çeken dejavü olarak isimlendirilen yazılım hataları (bug) ve Neo’nun bir tren istasyonunda hapis kalması ve trene binmesine rağmen tekrar tekrar aynı istasyona geri dönmesi, yani bir nevi for döngüsü olmuştur. Bir for döngüsünün bu kadar güzel görselleştirilmesi beni çok etkilemişti. Böyle bir sistemin entegrasyon testleri nasıl yapılıyor acaba?
Gelelim gerçek hayattaki Matrix’e. Java ile program yazan programcılar da yazdıkları programlar gibi bir Matrix içinde yaşarlar. Bu dünyanın ismi JVM – Java Virtual Machine yani Java Sanal İşlemcisi‘dir. JVM C++ dilinde yazılmış bir programdır. Bir Java programı javac.exe ile derlendikten sonra byte code ismi verilen bir ara sürüm oluşur. Byte code, ana işlem biriminin (CPU – Cental Processing Unit) anlayacağı cinsten komutlar ihtiva etmez, yani klasik Assembler değildir. Java byte code sadece JVM bünyesinde çalışır. JVM, derlenen Java programı için ana işlemci birimi olma görevini üstlenir. Bu özelliginden dolayı Java programlarını değişik platformlar üzerinde çalıştırmak mümkündür. Her platform için bir JVM sürümü Java programlarını koşturmak için yeterli olacaktır. Bu sebepten dolayı Java “write once, run anywhere – bir kere yaz, her yerde koştur” ünvanına sahiptir.
Java programları java.exe komutu kullanılarak koşturulur. java.exe işletim sistemi bünyesinde bir JVM meydana getirir yani Matrix’i oluşturur. Bu Matrix Java programının yaşaması için gerekli ortamı ihtiva eder. Sıra dışı olmayan Java programları bu Matrix içinde dış dünya ile ilişkileri kesik bir şekilde yaşayıp giderler. Onların ihtiyaç duyduğu herşeyi Matrix onlara sunar. Java programları hangi işletim sistemi ya da hangi donanım üzerinde koştuklarını bile zorda kalmadıkça bilmezler. Onlar için her donanım üzerinde bir integer 4 byte yani 32 bittir. Bu sebepten dolayı Java’da C/C++ dan tanıdığımız unsigned int bulunmaz. İnteger hangi donanım olursa olsun 32 bittir, işletim sistemi ya da donanıma göre değişmez. Bu Java dilini tasarlayan mühendislerin Java programcılarının hayatını kolaylaştirmak için aldıkları bir tasarım kararıdır. Bir Java programının gördüğü alt yapı her zaman aynıdır. Java programcıları bunu bildikleri için Matrix haricinde olup bitenlere pek önem vermeden kodlarını yazarlar ve Matrix içinde yaşamaya devam ederler, taki Morpheus gelip programcıya mavi ve kırmızı hapları taktim edene kadar.
Klasik kurumsal Java projelerinde çalışan Java programcısı Matrix’in dışında olup bitenlerle ilgilenmez. JVM ona ihtiyaç duyduğu herşeyi sağlar. O yüzden programcının tercihi mavi hap ve Matrix içinde yaşamaya devam etmek olur.
Java sadece kurumsal projeler için kullanılmaz. Sıra dışı projelerde de Java’ya rastlamak mümkündür. Şu an çalıştığım proje bunun en iyi örneklerinden birisidir. Navteq/Nokia firmasının GeoCoder (http://maps.nokia.com) isminde, harita üzerinde lokasyon arama yapan bir servisi bulunmaktadır. Bu servis aynı zamanda Bing ve Yahoo tarafından kullanılmaktadır. Kısa bir zaman sonra Facebook tarafından kullanılma planları yapılmaktadır. Ben lokasyon arama işlemlerinin programlandığı ekipte çalışıyorum. Bu gibi projelerde Morpheus’un verdiği kırmızı hapı yutup, Matrix’in dışına çıkmak gerekiyor, aksi taktirde Matrix, yani JVM içinde kalarak uygulamanın tabiyatındaki sıra dışılığı anlamak ve kodlamak mümkün değildir. Bu sıra dışılık programcıyı çok daha değişik kategorilerde düşünmeye ve değişik disiplinlerde çalışmaya zorlamaktadır.
Nokia’nin lokasyon arama servisi için dünya çapında altı değişik hosting lokasyonunda 300 den fazla VM (virtual Machine – sanal sunucu) kullanılıyor. Aşağıdaki resimde de görüldüğü gibi her JVM işletim sistemi bünyesinde 40 GB’den daha fazla yer kaplıyor. Neden JVM için bu kadar büyük bir hafıza alanının kullanıldığını anlamak için, uygulamanın tabiyatını anlamak gerekiyor.
Yukarıda yer alan resimde işletim sistemi (RedHat Linux) JVM için yeni bir işlem (process) oluşturmuştur. Bu işlemin işletim sistemi bünyesindeki hafıza büyüklüğü toplamda (VIRT – Virtual – sanal) 44GB’dir. JVM için kullanılan hafıza (Heap size) 5 GB’dir. Heap ayarı -Xmx5g ile yapılmaktadır. Peki geriye kalan 39 GB neyin nesidir? Bunu anlamak için Matrix’in dışına çıkmamız gerekiyor, çünkü geriye kalan 39 GB Matrix’in dışında olup bitenleri temsil etmektedir.
Lokasyon arama servisi için klasik veri tabanı sistemi kullanılmamaktadır. Klasik bir veri tabanı sisteminin kullanılması ve lokasyon arama işlemlerinin bu veri tabanı üzerinden yapılması arama süresini dakikalara çıkarabilir. Arama sonuçlarının 100 ms (100 milisecond bir saniyenin onda biridir) gibi bir zaman diliminde oluşturulması şartı bu projede klasik veri tabanlarının aksine bir tercihi zorunlu kılmıştır. Veri tabanından çekilen verilerle dosya tabanlı yeni bir veri tabanı oluşturulmakta ve bu dosyalar onlarca GB büyüklükte olabilmektedir. Bu veri tabanında bulunan verilere index dosyaları üzerinden erişilmektedir. Örneğin kullanıcı İstanbul Kadıköy lokasyonunu aradığında, arama işlemi önce index dosyasında yapılmaktadır. İndex dosyasında İstabul Kadıköy için bir veri bulunduğunda, bu veri adres nesnesinin dosya veri tabanındaki gerçek adresini (storage id) ihtiva etmektedir. Bu şekilde index üzerinden dosya veri tabanındaki adres nesnesine ulaşmak mümkün olmaktadır. Arama işlemlerinin hızlı yapılabilmesi için bu index ve diğer veri tabanı dosyalarının topluca hazıfaya yüklenmesi gerekmektedir. Aksi taktirde arama işlemleri çok uzun sürebilmektedir, çünkü arama esnasında hafızada yüklü olmayan bir adres nesnesi bulundu ise, bu nesnenin disk üzerinde bulunan veri tabanı dosyalarından yüklenmesi gerekmektedir. Bu gibi IO (Input/Output) işlemleri zaman aldığı için, genel olarak arama işlemi bu gibi durumlarda uzamaktadır. Bunun önüne geçmek için tüm dosyaların hafızaya yüklenmesi gerekmektedir. Bu işlemi yapmak için de Java Memory Mapped Files yapıları kullanılmaktadır.
-bash-3.2$ pmap PID komutunu girdiğimizde 39 GB alanın ne için kullanıldığını görebiliyoruz.
Yukarıda yer alan resimde görüldüğü gibi uygulama bünyesinde kullanılan tüm dosyalar işletim sistemi tarafından oluşturulan işleme (process) dahil edişmiştir. Bu dosyalara Java terminolojisinde Memory Mapped Files (hazıfada yüklü dosyalar) ismi verilmektedir. Yüksek performansın önemli olduğu durumlarda bu dosyaların %100 hafızaya yüklenmiş olmaları büyük önem taşımaktadır. Aksi taktirde dosyaların kullanılmak istendiğinde hafızada olmamaları performansı kötü etkilemektedir. Kullanılan hafıza alanının arkasında bir dosya yoksa yani bir memory mapped file kullanılmıyorsa, bu alanlar [ anon ] (anonim) olarak listede gözükmektedir.
Bizim örneğimizde ihtiyaç duyulan tüm dosyaların hepsinin %100 hafızaya yüklenmediğini görüyoruz. İlk resimde yer alan RES (resident – aktüel işgal edilen hazıfa alanı anlamnda) kolonuna göre JVM’in aslında 36 GB hafıza alanı işgal etmektedir. İşletim sistemi biz zorlamadıkça kullanılan tüm dosyaları %100 hafızaya yüklemez. Bu şekilde örneğin kullanılmayan fonsiyon kütüphanelerinin hafızada boş yere yer işgal etmesi engellenmiş olur. Bunun yanısıra işletim sistemi her program tarafından kullanılan ortak fonksiyon kütüphanelerini sadece bir kez hazıya yükleyerek, bu dosyaların değişik işlemler (process) tarafından ortaklaşa kullanılmasını sağlar. Birinci resimde SHR (shared – ortaklaşa kullanılan hafıza alanı anlamında) kolonuna baktığımızda bu değerin 12 GB oldugunu görmekteyiz, yani başka programlarla paylaştığımız 12 GB büyüklügünde dosyalar işlemci hafızamıza (Java Process Heap) yüklenmış durumdadır.
Belirttigim gibi bu dosyalar 5 GB’lik Java Heap içinde yer almamaktadırlar. Bu dosyalar daha önce bahsettiğim 39 GB’lik hafaza alanında yer almaktadir. Bu hafıza alanına Java Process Heap, normal Java nesnelerinin yer aldığı alana ise Java Heap ismi verilmektedir. Java Heap ve Java Process Heap bir araya geldiğinde 44 GB’lik, ilk resimde gördüğümüz Java işleminin tüm hafıza alanı ortaya çıkmaktadır. Java Process Heap alanı işletim sisteminden malloc() sistem fonksiyonu kullanılarak oluşturulan hafıza alanıdır. Java’da bu hafiza alanına genel olarak native memory ismi verilir. JNI (Java Native Interface) kullanılarak Java uygulamaları için native memory alanı oluşturmak ve kullanmak mümkündür. Java içinde ise hafıza alanı new operatörünü kullanarak tedarik edilir. Bu hafıza alanı oluşturduğumuz nesne için direk Java Heap bünyesinden gelir ve tamamen JVM tarafından, daha doğrusu Garbage Collector (kullanılmayan nesnelere garbage ismi verilir) tarafından yönetilir.
Yukarda yer alan resime baktığımızda JVM tarafından yönetilen hafıza alanının toplamda 5 GB olduğunu, bunun 1 GB’lik gibi bir kısmının EDEN Heap space, 150 MB’sinin iki SURVIVOR Heap Space ve 4 GB’sinin OLD Heap space tarafından kullanıldığını görmekteyiz. Toplamda JVM’in yönettiği ve içinde Java nesneleri oluşturabileceğimiz alan 5 GB büyüklüktedir.
Bahsettiğim index ya da veri tabanı dosyalarının normal heap içinde bulunmaları JVM’e fazladan yük getirmekte ve Garbage Collection işlemini uzatmaktadır. Bu tür dosyaları normal Heap içinden Java Process Heap’e (native memory alanına) taşımak için ByteBuffer sınıfı kullanılmaktadır. ByteBuffer.allocateDirect() metodu ile en fazla 2 GB büyüklüğünge, Java Heap dışında hafıza alanı rezerve etmek mümkündür. Bu hafıza alanı Java Process Heap içinde yer alacaktır. allocateDirect() metoduna baktğımızda native hafıza rezervasyonu için sun.misc.Unsafe sınıfının allocateMemory() metodunun kullanıldığını görmekteyiz. Bu native olarak tanımlanmış ve C++ dilinde kodlanmış bir metotdur. Buradan da anlaşıldığı gibi Java bünyesinde bu sınıfı kullanmadan native memory rezervasyonu mümkün değildir.
JVM bünyesinde tüm hafıza otomatik olarak JVM ve Garbage Collector tarafindan yönetilir. Programcının bu konuda yapması gereken fazla birşey yoktur. Java Heap dışında işlem yapmak zorunda kalındığında durum farklıdır. Native hafıza alanını JVM bünyesindeki Garbage Collector yönetmez. Programcının C/C++ dillerinde olduğu gibi native hafıza alanını kendisi yönetmesi gerekir. Bu yüzden Morpheus’un verdiği kırmızı hapı yutup, Matrix’in dışına çıkması gerekir. Sadece Matrix’in dışında olan programcılar gerçekte olup bitenlerden haberdardırlar.
EOF (End Of Fun)
Özcan Acar
Geri izleme: Matrix’de Yaşayan Programcılar : Özcan Acar
Başarılı makale, teşekkürler.
Geri izleme: JVM (Java Virtual Machine) Nedir? - Kurumsal Java Yazılımı - Özcan Acar
Güzel bir anlatım ve dolu dolu bir yazı olmuş.
Teşekkürler.
in-memory database neden kullanmadınız acaba?
Veriye erisimin hizli olmasi icin binary storage kullanildi.
Javacılar olarak Java İmparatorluğunda(Matrix) yaşıyoruz. :) Teşekkürler.
Ama JVM çalışma mantığı biraz karışık geldi. Birkaç defa okumam gerekecek :)
Elinize sağlık, yazınızı beğendim. Morheus kısmı heyecanlandırdı :) İlmi derinliği olan yazılarınızı ilgiyle takip ediyorum. Özcan hocam, acaba bahsi geçen projedeki binary storage ın nasıl implemente edildiğine dair bilgi verebilirmisiniz? Örnek kod paylaşabilir misiniz? C++ ile mi yazılıp okunuyor?
Bahsettigim binary storage tamamen Java ortaminda gelistirildi. Verilerin hepsi byte olarak Buffer(nio) ve türevlerinde tutuluyor. Örnegin 123 seklinde bir long degerim var ise, bu deger LongBuffer icinde tutuluyor. Binary storage icinde yer alan veriler Java nesneleri. Nesnenin sahip oldugu her degisken icin disk üzerinde bir dosya var. Bu dosya degiskenin tipine göre verinin byte seklindeki halini tutuyor. Bu dosyalar daha sonra Java Memory Mapped Files kullanilarak hafizaya yükleniyor. Bir nesneyi sahip oldugu degiskenleri ihtiva eden buffer dosyalarini okuyarak tekrar olusturmak mümkün, bu sekilde bir nevi new Object() yapilmis oluyor.
Geri izleme: Bir Mahrumiyetin Sonu | Mikrodevre.com