Az önce bir röportaj yaptım ve Java ile bir bellek sızıntısı yaratmam istendi .
Söylemeye gerek yok, bir tane oluşturmaya nasıl başlayacağına dair hiçbir fikri olmayan oldukça aptal hissettim.
Bir örnek ne olurdu?
Az önce bir röportaj yaptım ve Java ile bir bellek sızıntısı yaratmam istendi .
Söylemeye gerek yok, bir tane oluşturmaya nasıl başlayacağına dair hiçbir fikri olmayan oldukça aptal hissettim.
Bir örnek ne olurdu?
Yanıtlar:
Saf Java'da gerçek bir bellek sızıntısı (kod çalıştırarak erişilemeyen, ancak yine de bellekte depolanan nesneler) oluşturmanın iyi bir yolu:
ClassLoader
.new byte[1000000]
), statik bir alanda güçlü bir referans depolar ve ardından a ThreadLocal
. Ek belleği ayırmak isteğe bağlıdır (sınıf örneğini sızdırmak yeterlidir), ancak sızıntının çok daha hızlı çalışmasını sağlayacaktır.ClassLoader
yüklendiği tüm başvuruları temizler .ThreadLocal
Oracle'ın JDK'sında uygulanma şekli nedeniyle , bu bir bellek sızıntısı oluşturur:
Thread
birinin threadLocals
iş parçacığı-yerel değerlerini saklayan özel bir alanı vardır .ThreadLocal
nesneye zayıf bir referanstır , bu nedenle bu ThreadLocal
nesne çöp toplandıktan sonra girişi haritadan kaldırılır.ThreadLocal
onun nesnenin anahtar , bu nesne de olacaktır çöp toplama uzun iplik hayatları kadar ne harita çıkarıldı.Bu örnekte, güçlü referanslar zinciri şöyle görünür:
Thread
nesne → threadLocals
harita → örnek sınıf örneği → örnek sınıf → statik ThreadLocal
alan → ThreadLocal
nesne.
( ClassLoader
Sızıntıyı yaratmada gerçekten bir rol oynamaz, bu ek referans zinciri nedeniyle sızıntıyı daha da kötüleştirir: örnek sınıf → ClassLoader
→ yüklediği tüm sınıflar. Özellikle JVM uygulamalarında, özellikle daha önce Java 7, çünkü sınıflar ve ClassLoader
s doğrudan doğruca tahliye edildi ve asla çöp toplanmadı.)
Bu örüntünün bir varyasyonu, uygulama kaplarının (Tomcat gibi), bir ThreadLocal
şekilde kendilerini işaret eden uygulamaları sık sık yeniden uygularsanız, elek gibi bellek sızmasına neden olabilir . Bu, bazı ince nedenlerden dolayı olabilir ve hata ayıklamak ve / veya düzeltmek genellikle zordur.
Güncelleme : Birçok kişi bunu sormaya devam ettiğinden, işte bu davranışı gösteren bazı örnek kodlar .
Statik alan tutma nesnesi referansı [esp final field]
class MemorableClass {
static final ArrayList list = new ArrayList(100);
}
String.intern()
Uzun String Çağrısı
String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();
(Kapatılmamış) açık akışlar (dosya, ağ vb.)
try {
BufferedReader br = new BufferedReader(new FileReader(inputFile));
...
...
} catch (Exception e) {
e.printStacktrace();
}
Kapatılmamış bağlantılar
try {
Connection conn = ConnectionFactory.getConnection();
...
...
} catch (Exception e) {
e.printStacktrace();
}
Yerel yöntemlerle ayrılmış bellek gibi JVM'nin çöp toplayıcısından erişilemeyen alanlar
Web uygulamalarında, bazı nesneler, uygulama açıkça durduruluncaya veya kaldırılana kadar uygulama kapsamında saklanır.
getServletContext().setAttribute("SOME_MAP", map);
noclassgc
IBM JDK'deki kullanılmayan sınıf çöpü toplamasını önleyen seçenek gibi yanlış veya uygunsuz JVM seçenekleri
Bkz. IBM jdk ayarları .
close()
genellikle sonlandırıcıda çağrılmaz) çünkü engelleme işlemi olabilir). Kapatmamak kötü bir uygulamadır, ancak bir sızıntıya neden olmaz. Kapatılmamış java.sql.Connection aynı.
intern
hashtable içerikleri üzerinde yalnızca zayıf bir referansı varmış gibi görünüyor . Böyle olunca edilir çöp sızıntı düzgün toplanmış olup. (ancak IANAJP) mindprod.com/jgloss/interned.html#GC
Yapılacak basit bir şey, yanlış (veya var olmayan) bir HashSet kullanmak hashCode()
veya equals()
daha sonra "kopya" eklemeye devam etmektir . Yinelenenleri olması gerektiği gibi görmezden gelmek yerine, küme yalnızca büyüyecek ve bunları kaldıramayacaksınız.
Bu kötü anahtarların / öğelerin takılmasını istiyorsanız, aşağıdaki gibi statik bir alan kullanabilirsiniz:
class BadKey {
// no hashCode or equals();
public final String key;
public BadKey(String key) { this.key = key; }
}
Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.
Aşağıda, standart unutulmuş dinleyiciler, statik referanslar, hashmaps'ta sahte / değiştirilebilir anahtarların yanı sıra yaşam döngüsünü sona erdirme şansı olmadan sıkışmış iş parçacıklarının yanı sıra Java'nın sızdığı açık olmayan bir durum olacaktır.
File.deleteOnExit()
- her zaman ipi sızdırıyor, char[]
, bu nedenle daha sonra geçerli değildir ; @ Daniel, oylamaya gerek yok.En çok yönetilmeyen ipliklerin tehlikesini göstermek için ipliklere konsantre olacağım, salıncaka dokunmak bile istemiyorum.
Runtime.addShutdownHook
Kaldırmayın ... ve sonra removeShutdownHook ile bile başlatılmamış iş parçacıkları ile ilgili ThreadGroup sınıfındaki bir hata nedeniyle, ThreadGroup etkin bir şekilde sızıntı. JGroup'un GossipRouter'da kaçağı var.
A oluşturmak, ancak başlamak değil, Thread
yukarıdakiyle aynı kategoriye girer.
Bir iplik devralır oluşturma ContextClassLoader
ve AccessControlContext
artı ThreadGroup
ve herhangi InheritedThreadLocal
onca referanslar sınıf yükleyicisi ve tüm statik referanslar ve ja-ja tarafından yüklenen tüm sınıfları ile birlikte, potansiyel bir sızıntı vardır. Etki özellikle süper basit bir ThreadFactory
arayüze sahip olan tüm jucExecutor çerçevesi ile görülebilir , ancak çoğu geliştiricinin gizlenen tehlike hakkında hiçbir fikri yoktur. Ayrıca birçok kütüphane istek üzerine iş parçacığı başlatır (çok fazla sektör popüler kütüphanesi).
ThreadLocal
önbelleğe; bunlar birçok durumda kötüdür. Eminim herkes ThreadLocal, kötü haber dayalı biraz basit önbellekleri gördü: iplik ClassLoader bağlamında beklenenden daha fazla devam ederse, bu saf güzel küçük bir sızıntı. Gerçekten gerekmedikçe ThreadLocal önbellekleri kullanmayın.
Arayan ThreadGroup.destroy()
ThreadGroup hiçbir konuları kendisi vardır, ama yine de çocuk ThreadGroups tutar zaman. ThreadGroup'un üst öğesinden kaldırılmasını engelleyen kötü bir sızıntı, ancak tüm çocuklar numaralandırılamaz hale geliyor.
WeakHashMap ve değeri (in) kullanılması doğrudan anahtara başvurur. Bu bir yığın dökümü olmadan bulmak zor bir. Bu Weak/SoftReference
, korunan nesneye sert bir referans tutabilecek tüm genişletilmiş durumlar için geçerlidir .
Kullanma java.net.URL
HTTP (S) protokolü ile ve gelen kaynağı yüklenirken (!). Bu özeldir, KeepAliveCache
ThreadGroup sisteminde geçerli iş parçacığının bağlam sınıf yükleyicisine sızan yeni bir iş parçacığı oluşturur. Canlı bir iş parçacığı olmadığında iş parçacığı ilk istek üzerine oluşturulur, böylece ya şanslı olabilirsiniz ya da sızıntı olabilir. Sızıntı Java 7'de zaten düzeltilmiştir ve iş parçacığı oluşturan kod, bağlam sınıfı yükleyiciyi düzgün bir şekilde kaldırır. Birkaç vaka daha var (ImageFetcher gibi, ayrıca sabit ) benzer iş parçacıkları oluşturma.
Yapıcıdan InflaterInputStream
geçirmeyi new java.util.zip.Inflater()
( PNGImageDecoder
örneğin) kullanma ve end()
şişiriciyi çağırmama . Eğer yapıcıyı sadece new
şansınız olmadan geçirirseniz ... Ve evet, close()
akış yapıldığında manuel olarak yapıcı parametresi olarak aktarılırsa, şişiriciyi kapatmaz. Sonlandırıcı tarafından serbest bırakılacağı için bu gerçek bir sızıntı değil ... gerekli gördüğü zaman. O ana kadar yerel belleği o kadar kötü yiyor ki, Linux oom_killer'in süreci cezasız bir şekilde öldürmesine neden olabilir. Ana sorun, Java'da sonlandırmanın çok güvenilmez olması ve G1'in 7.0.2'ye kadar daha kötü hale getirmesidir. Hikayenin ahlaki: mümkün olan en kısa sürede yerel kaynakları serbest bırakın; sonlandırıcı çok zayıf.
İle aynı durum java.util.zip.Deflater
. Deflater Java'da belleğe aç olduğu için bu çok daha kötüdür, yani her zaman birkaç bit KB yerel bellek ayırmak için 15 bit (maks.) Ve 8 bellek seviyesi (9 maks.) Kullanır. Neyse ki, Deflater
yaygın olarak kullanılmaz ve bence JDK yanlış kullanım içermez. end()
Manuel olarak bir Deflater
veya oluşturursanız her zaman arayın Inflater
. Son ikisinin en iyi kısmı: mevcut normal profil oluşturma araçlarıyla bulamazsınız.
(İstek üzerine karşılaştığım zaman kaybını biraz daha ekleyebilirim.)
İyi şanslar selametle; sızıntılar kötülük!
Creating but not starting a Thread...
Yikes, bundan birkaç yüzyıl önce kötü bir şekilde ısırıldım! (Java 1.3)
unstarted
sayıyı artırmaz, ancak iplik grubunun yok edilmesini önler (daha az kötülük ama yine de bir sızıntı)
ThreadGroup.destroy()
ThreadGroup kendi iş parçacığı yokken çağırmak ..." inanılmaz ince bir hata; Bunu saatlerdir kovaladım, yoldan çıktım, çünkü kontrol GUI'mdeki ipliği numaralandırmak hiçbir şey göstermedi, ancak iplik grubu ve muhtemelen en az bir çocuk grubu gitmeyecekti.
Buradaki örneklerin çoğu "çok karmaşık" tır. Bunlar uç davalardır. Bu örneklerle, programcı bir hata yaptı (eşittir / hashcode'u tanımlamayın gibi) veya JVM / JAVA'nın (statik ile sınıf yükü) bir köşe durumu tarafından ısırıldı. Bence bu, bir görüşmecinin istediği türden bir örnek ya da en yaygın durum değil.
Ancak bellek sızıntıları için gerçekten daha basit durumlar var. Çöp toplayıcı yalnızca artık referansta bulunulmayanları boşaltır. Java geliştiricileri olarak hafızayı önemsemiyoruz. Gerektiğinde tahsis ediyoruz ve otomatik olarak serbest bırakılıyoruz. İnce.
Ancak, uzun ömürlü herhangi bir başvurunun devlet paylaşma eğilimi vardır. Herhangi bir şey, statik, tek ton olabilir ... Genellikle önemsiz olmayan uygulamalar karmaşık nesneler grafik yapma eğilimindedir. Null değerine bir referans ayarlamayı unutmak veya daha sık olarak bir nesneyi bir koleksiyondan kaldırmayı unutmak bellek sızıntısı yapmak için yeterlidir.
Elbette her türlü dinleyici (UI dinleyicisi gibi), önbellek veya uzun ömürlü paylaşılan bir durum, doğru şekilde ele alınmadığında bellek sızıntısı üretme eğilimindedir. Anlaşılması gereken, bunun bir Java köşe durumu veya çöp toplayıcıyla ilgili bir sorun olmadığıdır. Bu bir tasarım problemidir. Uzun ömürlü bir nesneye bir dinleyici eklediğimizi düşünüyoruz, ancak artık gerekmediğinde dinleyiciyi kaldırmıyoruz. Nesneleri önbelleğe alıyoruz, ancak bunları önbellekten kaldırma stratejimiz yok.
Belki bir hesaplama için gerekli olan önceki durumu saklayan karmaşık bir grafiğimiz var. Fakat önceki devletin kendisi, önceki ve benzeri devletle bağlantılıdır.
SQL bağlantılarını veya dosyalarını kapatmamız gerektiği gibi. Null değerine uygun referanslar ayarlamamız ve öğeleri koleksiyondan kaldırmamız gerekiyor. Uygun önbellekleme stratejilerine sahip olmalıyız (maksimum bellek boyutu, öğe sayısı veya zamanlayıcılar). Bir dinleyicinin bilgilendirilmesine izin veren tüm nesneler hem addListener hem de removeListener yöntemi sağlamalıdır. Ve bu bildirimler artık kullanılmadığında, dinleyici listelerini temizlemelidir.
Bir bellek sızıntısı gerçekten de mümkündür ve mükemmel bir şekilde tahmin edilebilir. Özel dil özelliklerine veya köşe kasalarına gerek yoktur. Bellek sızıntıları ya bir şeyin eksik olduğunu, hatta tasarım problemlerinin bir göstergesidir.
WeakReference
) bir nesnenin başka bir nesnenin varlığını önemsemesi mümkündür. birinden diğerine. Bir nesne referansının yedek bir biti varsa, "hedefe önem veren" bir göstergeye sahip olmak yararlı olabilir ...
PhantomReference
bir nesnenin ilgilenen kimseye sahip olmadığı tespit edildiğinde, sistemin (benzer yollarla ) bildirim sağlamasını sağlayın . WeakReference
biraz yakın gelir, ancak kullanılmadan önce güçlü bir referansa dönüştürülmesi gerekir; güçlü referans varken bir GC döngüsü meydana gelirse, hedefin yararlı olduğu varsayılır.
Cevap tamamen görüşmecinin ne istediğini düşündüğüne bağlıdır.
Uygulamada Java sızıntısı yapmak mümkün müdür? Tabii ki, ve diğer cevaplarda çok sayıda örnek var.
Ama sorulmuş olabilecek birden fazla meta soru var mı?
Meta sorunuzu "Bu röportaj durumunda kullanabileceğim bir cevap nedir" olarak okuyorum. Ve bu yüzden, Java yerine röportaj becerilerine odaklanacağım. Bir röportajda bir sorunun cevabını bilmeme durumunu, Java sızıntısını nasıl yapacağınızı bilmeniz gerektiğinden daha fazla tekrarlayacağınıza inanıyorum. Umarım, bu yardımcı olacaktır.
Görüşme için geliştirebileceğiniz en önemli becerilerden biri, soruları aktif olarak dinlemeyi öğrenmek ve niyetlerini çıkarmak için görüşmeci ile çalışmaktır. Bu sadece sorularına istedikleri gibi cevap vermenizi sağlamakla kalmaz, aynı zamanda bazı önemli iletişim becerilerinizin olduğunu gösterir. Eşit yetenekli birçok geliştirici arasında bir seçim söz konusu olduğunda, her seferinde cevap vermeden önce dinleyen, düşünen ve anlayan birini işe alacağım.
Anlamadığınız aşağıdakiler, bir çok anlamsız örnektir JDBC . Veya JDBC yakın bir geliştirici beklediğini nasıl en az Connection
, Statement
ve ResultSet
onları iptal edebilir veya bunlara referansları kaybederek yerine uygulanması güvenmeden önce örneklerini finalize
.
void doWork()
{
try
{
Connection conn = ConnectionFactory.getConnection();
PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
ResultSet rs = stmt.executeQuery();
while(rs.hasNext())
{
... process the result set
}
}
catch(SQLException sqlEx)
{
log(sqlEx);
}
}
Yukarıdakilerle ilgili problem, Connection
nesnenin kapalı olmaması ve dolayısıyla çöp toplayıcının gelip ulaşılamadığını görene kadar fiziksel bağlantının açık kalacağıdır. GC finalize
yöntemi çağırır , ancak finalize
en azından uygulandığı şekilde uygulamayan JDBC sürücüleri vardır Connection.close
. Sonuçta ortaya çıkan davranış, toplanamayan nesneler nedeniyle bellek geri kazanılırken, Connection
nesne ile ilişkili kaynakların (bellek dahil) geri alınamayacağıdır.
Connection
'S finalize
yönteminin her şeyi temizlemediği böyle bir durumda , veritabanı sunucusu sonunda bağlantının canlı olmadığını anlayana kadar, veritabanı sunucusuna yapılan fiziksel bağlantının birkaç çöp toplama döngüsü sürdüğü görülebilir ) ve kapatılmalıdır.
JDBC sürücüsü uygulanacak olsa bile, finalize
sonlandırma sırasında istisnaların atılması mümkündür. Sonuçta ortaya çıkan davranış, şimdi "hareketsiz" nesneyle ilişkili herhangi bir belleğin, finalize
yalnızca bir kez çağrılması garanti edildiği gibi geri alınmayacağıdır.
Yukarıdaki nesne sonlandırma sırasında istisnalarla karşılaşma senaryosu, bellek sızıntısı - nesne dirilişine yol açabilecek başka bir senaryo ile ilgilidir. Nesne dirilişi genellikle kasıtlı olarak başka bir nesneden sonlandırılmadan nesneye güçlü bir referans yaratarak yapılır. Nesnenin yeniden canlandırılması kötüye kullanıldığında, diğer bellek sızıntıları kaynaklarıyla birlikte bellek sızıntısına neden olur.
Tasarlayabileceğiniz daha birçok örnek var -
List
Yalnızca listeye eklediğiniz ve listeden silmediğiniz bir örneği yönetme (artık ihtiyacınız olmayan öğelerden kurtulmanız gerekir) veyaSocket
S veya File
s'yi açma , ancak artık gerekmediklerinde kapatma ( Connection
sınıfla ilgili yukarıdaki örneğe benzer ).Connection.close
Tüm SQL çağrılarımın nihayet bloğuna girene kadar SQL veritabanlarının bağlantı sınırlarına çok fazla vurdum . Ekstra eğlence için veritabanına çok fazla çağrı önlemek için Java tarafında kilitler gerektiren bazı uzun süredir çalışan Oracle saklı yordamlar çağırdı.
Muhtemelen olası bir bellek sızıntısının en basit örneklerinden biri ve bundan nasıl kaçınılacağı, ArrayList.remove (int) 'nin uygulanmasıdır:
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index + 1, elementData, index,
numMoved);
elementData[--size] = null; // (!) Let gc do its work
return oldValue;
}
Bunu kendiniz uyguluyorsanız, artık kullanılmayan dizi öğesini ( elementData[--size] = null
) temizlemeyi düşünür müsünüz ? Bu referans büyük bir nesneyi canlı tutabilir ...
Artık ihtiyaç duymadığınız nesnelere göndermeler yaptığınızda bellek sızıntısı olur. Bellek sızıntılarının kendilerini Java'da nasıl gösterdiğine ve bu konuda neler yapabileceğinize ilişkin örnekler için Java programlarında bellek sızıntılarını işleme bölümüne bakın .
...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language.
Bu sonucu nasıl çizdiğinizi anlamıyorum. Herhangi bir tanımla Java'da bellek sızıntısı oluşturmanın daha az yolu vardır . Kesinlikle geçerli bir soru.
İle bellek sızıntısı yapabilirsiniz Sun.misc.Unsafe sınıfıyla . Aslında bu hizmet sınıfı farklı standart sınıflarda (örneğin java.nio sınıflarında) kullanılır. Bu sınıfın örneğini doğrudan oluşturamazsınız , ancak bunu yapmak için yansımayı kullanabilirsiniz .
Eclipse IDE'de kod derlenmiyor - komutu kullanarak derleyin javac
derlenmiyor - (derleme sırasında uyarı alırsınız)
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;
public class TestUnsafe {
public static void main(String[] args) throws Exception{
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field f = unsafeClass.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
System.out.print("4..3..2..1...");
try
{
for(;;)
unsafe.allocateMemory(1024*1024);
} catch(Error e) {
System.out.println("Boom :)");
e.printStackTrace();
}
}
}
Cevabımı buradan kopyalayabilirim: Java'da bellek sızıntısına neden olmanın en kolay yolu?
"Bilgisayar bilimlerinde bellek sızıntısı (veya bu bağlamda sızıntı), bir bilgisayar programı bellek tükettiğinde ancak işletim sistemine geri bırakamadığında oluşur." (Vikipedi)
Kolay cevap: Yapamazsınız. Java otomatik bellek yönetimi yapar ve sizin için gerekli olmayan kaynakları serbest bırakır. Bunun olmasını engelleyemezsin. Her zaman kaynakları serbest bırakabilecektir. Manuel bellek yönetimi olan programlarda bu farklıdır. Malloc () kullanarak C'de biraz bellek alamazsınız. Belleği boşaltmak için, malloc'un döndürdüğü işaretçiyi ve üzerinde free () öğesini çağırmanız gerekir. Ancak artık işaretçiye sahip değilseniz (üzerine yazılmış veya kullanım ömrü aşılmışsa), bu maalesef bu belleği boşaltmaktan acizsiniz ve dolayısıyla bir bellek sızıntısına sahip olursunuz.
Şimdiye kadar olan diğer tüm cevaplar benim tanımımda gerçekten bellek sızıntısı değil. Hepsi hafızayı anlamsız şeylerle çok hızlı doldurmayı amaçlıyor. Ama herhangi bir zamanda hala yarattığınız nesneleri dereference ve böylece bellek serbest -> NO LEAK. Acconrad'ın cevabı oldukça yakın geliyor ama itiraf etmeliyim çünkü çözümü etkili bir şekilde çöp toplayıcıyı sonsuz bir döngüde zorlayarak "çökertmek".
Uzun cevap şudur: JNI'yi kullanarak manuel bellek yönetimi ve dolayısıyla bellek sızıntıları olan bir Java kütüphanesi yazarak bellek sızıntısı elde edebilirsiniz. Bu kütüphaneyi çağırırsanız, java işleminiz bellek sızdırır. Veya JVM'de hatalar olabilir, böylece JVM belleği kaybeder. JVM'de muhtemelen hatalar var, çöp toplama o kadar da önemsiz olduğu için bazı bilinenler bile olabilir, ancak yine de bir hata. Tasarım gereği bu mümkün değildir. Böyle bir hatadan etkilenen bazı java kodlarını isteyebilirsiniz. Maalesef birini bilmiyorum ve zaten bir sonraki Java sürümünde artık bir hata olmayabilir.
İşte http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29 aracılığıyla basit / uğursuz bir örnek .
public class StringLeaker
{
private final String muchSmallerString;
public StringLeaker()
{
// Imagine the whole Declaration of Independence here
String veryLongString = "We hold these truths to be self-evident...";
// The substring here maintains a reference to the internal char[]
// representation of the original string.
this.muchSmallerString = veryLongString.substring(0, 1);
}
}
Alt dize orijinalin çok daha uzun bir dizesinin iç temsilini ifade ettiğinden, orijinal bellekte kalır. Bu nedenle, oyunda bir StringLeaker'ınız olduğu sürece, sadece tek karakterli bir dize tuttuğunuzu düşünebiliyor olsanız bile, orijinal dize de sahip olursunuz.
Orijinal dizeye istenmeyen bir başvuru depolamaktan kaçınmanın yolu şöyle bir şey yapmaktır:
...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...
Ek kötülük .intern()
için alt dizeyi de kullanabilirsiniz:
...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...
Bunu yaptığınızda, StringLeaker örneği atıldıktan sonra bile hem orijinal uzun dize hem de türetilen alt dize bellekte kalır.
muchSmallerString
bırakıldığında ( StringLeaker
nesne yok edildiğinden), uzun dize de serbest bırakılır. Bellek sızıntısı dediğim şey, bu JVM örneğinde asla serbest bırakılamayan bellektir. Ancak, hafızayı boşaltmak için nasıl kendini göstermiştir: this.muchSmallerString=new String(this.muchSmallerString)
. Gerçek bir bellek sızıntısı ile yapabileceğiniz hiçbir şey yoktur.
intern
vaka bir daha bir "hafıza sürpriz" olabilir "bellek sızıntısı." .intern()
Bununla birlikte, alt dizenin kullanılması, kesinlikle daha uzun dizgeye yapılan başvurunun korunduğu ve serbest bırakılamadığı bir durum oluşturur.
GUI kodunda bunun yaygın bir örneği, bir widget / bileşen oluşturma ve bazı statik / uygulama kapsamındaki nesnelere dinleyici ekleme ve ardından widget yok edildiğinde dinleyiciyi kaldırma işlemidir. Sadece bir bellek sızıntısı değil, aynı zamanda olayları ne dinlerseniz dinleyin, tüm eski dinleyicileriniz de çağrılır.
Herhangi bir sunucu uygulamasında (Tomcat, Jetty, Glassfish, ne olursa olsun) çalışan herhangi bir web uygulamasını alın. Uygulamayı arka arkaya 10 veya 20 kez yeniden konuşlandırın (sunucunun otomatik devreye alma dizinindeki WAR öğesine dokunmanız yeterli olabilir.
Kimse bunu gerçekten test etmedikçe, birkaç yeniden dağıtımdan sonra bir OutOfMemoryError alma olasılığınız yüksektir, çünkü uygulama kendi kendine temizlemeye özen göstermedi. Bu testle sunucunuzda bir hata bile bulabilirsiniz.
Sorun şu ki, kabın ömrü uygulamanızın ömründen daha uzun. Kapsayıcıya uygulamanızın nesnelerine veya sınıflarına ilişkin tüm referansların çöp toplanabileceğinden emin olmalısınız.
Web uygulamanızın kaldırılmasından sonra sadece bir referans varsa, karşılık gelen sınıf yükleyici ve sonuç olarak web uygulamanızın tüm sınıfları çöp toplanamaz.
Uygulamanız tarafından başlatılan iş parçacıkları, ThreadLocal değişkenleri, günlük ekleyicileri, sınıf yükleyici sızıntılarına neden olan olağan şüphelilerden bazılarıdır.
Belki JNI aracılığıyla harici yerel kod kullanarak?
Saf Java ile neredeyse imkansız.
Ancak bu, artık belleğe erişemediğinizde "standart" bir bellek sızıntısıyla ilgilidir, ancak yine de uygulamaya aittir. Bunun yerine kullanılmayan nesnelere başvuruları saklayabilir veya akışları daha sonra kapatmadan açabilirsiniz.
Bir kez PermGen ve XML ayrıştırma ile ilgili güzel bir "bellek sızıntısı" oldu. Daha hızlı karşılaştırma yapmak için kullandığımız XML ayrıştırıcısı (hangisinin olduğunu hatırlayamıyorum) etiket adlarında bir String.intern () yaptı. Müşterilerimizden biri, veri değerlerini XML özelliklerinde veya metinde değil, tagnames olarak depolamak için harika bir fikre sahipti, bu yüzden şöyle bir belgemiz vardı:
<data>
<1>bla</1>
<2>foo</>
...
</data>
Aslında, benzersiz değil ve günde 10-15 milyon oranında gelen rakamlar değil daha uzun metin kimlikleri (yaklaşık 20 karakter) kullandılar. Bu günde 200 MB çöp yapar, bir daha asla ihtiyaç duyulmaz ve asla GCed olmaz (PermGen'de olduğu için). 512 MB'ye ayarlanmış bir permenimiz vardı, bu yüzden bellek yetersiz özel durumun (OOME) gelmesi yaklaşık iki gün sürdü ...
Bellek sızıntısı nedir:
Tipik örnek:
Nesnelerin önbelleği, işleri karıştırmak için iyi bir başlangıç noktasıdır.
private static final Map<String, Info> myCache = new HashMap<>();
public void getInfo(String key)
{
// uses cache
Info info = myCache.get(key);
if (info != null) return info;
// if it's not in cache, then fetch it from the database
info = Database.fetch(key);
if (info == null) return null;
// and store it in the cache
myCache.put(key, info);
return info;
}
Önbelleğiniz büyür ve büyür. Ve çok geçmeden tüm veritabanı belleğe çekilir. Daha iyi bir tasarım LRUMap kullanır (Sadece son kullanılan nesneleri önbellekte tutar).
Elbette, işleri çok daha karmaşık hale getirebilirsiniz:
Sık sık ne olur:
Bu Info nesnesinin başka nesnelere başvuruları varsa, yine başka nesnelere başvuruları vardır. Bir şekilde, bunun bir tür bellek sızıntısı olduğunu da düşünebilirsiniz (kötü tasarımın neden olduğu).
Kimsenin iç sınıf örneklerini kullanmamasının ilginç olduğunu düşündüm. Dahili bir sınıfınız varsa; doğal olarak içeren sınıfa bir referans tutar. Tabii ki teknik olarak bir bellek sızıntısı değildir çünkü Java sonunda temizleyecektir; ancak bu, sınıfların beklenenden daha uzun süre asılı kalmasına neden olabilir.
public class Example1 {
public Example2 getNewExample2() {
return this.new Example2();
}
public class Example2 {
public Example2() {}
}
}
Şimdi Örnek1'i çağırır ve Örnek1'i atarak bir Örnek2 alırsanız, kendiliğinden bir Örnek1 nesnesine bağlantınız olur.
public class Referencer {
public static Example2 GetAnExample2() {
Example1 ex = new Example1();
return ex.getNewExample2();
}
public static void main(String[] args) {
Example2 ex = Referencer.GetAnExample2();
// As long as ex is reachable; Example1 will always remain in memory.
}
}
Ayrıca, belirli bir süreden daha uzun bir süredir var olan bir değişkeniniz varsa; Java her zaman var olacağını ve artık kodda ulaşılamazsa asla temizlemeye çalışmadığını varsayar. Ancak bu tamamen doğrulanmamıştır.
Son zamanlarda log4j neden bir bellek sızıntı durumu ile karşılaştı.
Log4j, araya eklenmiş günlük çıktısını farklı kaynaklardan ayıran bir araç olan İç İçe Diyagnostik Bağlam (NDC) olarak adlandırılan bu mekanizmaya sahiptir . NDC'nin çalıştığı ayrıntı düzeyi iş parçacıklarıdır, bu nedenle günlük çıktılarını farklı iş parçacıklarından ayrı olarak ayırır.
İş parçacığına özgü etiketleri saklamak için, log4j'nin NDC sınıfı, Thread nesnesinin kendisi tarafından (thread id ifadesinin aksine) anahtarlanan bir Hashtable kullanır ve böylece NDC etiketi, thread'da kalan tüm nesneleri hafızada kalana kadar nesne de hafızada kalır. Web uygulamamızda, günlükleri tek bir istekten ayrı olarak ayırmak için bir istek kimliğine sahip çıkışları etiketlemek için NDC'yi kullanırız. NDC etiketini bir iş parçacığıyla ilişkilendiren kapsayıcı, bir istekten yanıtı döndürürken etiketi de kaldırır. Sorun, bir isteğin işlenmesi sırasında, aşağıdaki kod gibi bir alt iş parçacığının doğması sırasında ortaya çıktı:
pubclic class RequestProcessor {
private static final Logger logger = Logger.getLogger(RequestProcessor.class);
public void doSomething() {
....
final List<String> hugeList = new ArrayList<String>(10000);
new Thread() {
public void run() {
logger.info("Child thread spawned")
for(String s:hugeList) {
....
}
}
}.start();
}
}
Böylece bir NDC bağlamı ortaya çıkan satır içi iş parçacığıyla ilişkilendirildi. Bu NDC bağlamının anahtarı olan iş parçacığı nesnesi, büyükList nesnesinin asılı olduğu satır içi iş parçacığıdır. Böylece, iş parçacığı yaptığı işi bitirdikten sonra bile, kocaman listeye yapılan başvuru Hastable NDC bağlamı tarafından canlı tutuldu ve böylece bellek sızıntısına neden oldu.
Görüşmeci muhtemelen aşağıdaki kod gibi dairesel bir referans arıyordu. Ancak bu oldukça belirsiz bir soru, bu yüzden JVM bellek yönetimi anlayışınızı göstermek için birincil bir fırsat.
class A {
B bRef;
}
class B {
A aRef;
}
public class Main {
public static void main(String args[]) {
A myA = new A();
B myB = new B();
myA.bRef = myB;
myB.aRef = myA;
myA=null;
myB=null;
/* at this point, there is no access to the myA and myB objects, */
/* even though both objects still have active references. */
} /* main */
}
Daha sonra referans sayımıyla yukarıdaki kodun bellek sızdırdığını açıklayabilirsiniz. Ancak modern JVM'lerin çoğunda artık referans sayımı kullanılmıyor, çoğu bu hafızayı toplayacak bir süpürme çöp toplayıcısı kullanıyor.
Daha sonra, aşağıdaki gibi temel kaynağı olan bir Nesne oluşturmayı açıklayabilirsiniz:
public class Main {
public static void main(String args[]) {
Socket s = new Socket(InetAddress.getByName("google.com"),80);
s=null;
/* at this point, because you didn't close the socket properly, */
/* you have a leak of a native descriptor, which uses memory. */
}
}
Daha sonra bunun teknik olarak bir bellek sızıntısı olduğunu açıklayabilirsiniz, ancak gerçekten de sızıntı, JVM'deki Java kodunuz tarafından serbest bırakılmayan yerel kaynakları tahsis eden yerel koddan kaynaklanır.
Günün sonunda, modern bir JVM ile, yerel bir kaynağı JVM'nin farkındalığının normal kapsamı dışında ayıran bazı Java kodları yazmanız gerekir.
Herkes her zaman yerel kod yolunu unutur. İşte bir sızıntı için basit bir formül:
malloc
. Arama free
.Yerel koddaki bellek ayırmalarının JVM yığınından geldiğini unutmayın.
Statik bir Harita oluşturun ve buna sert referanslar eklemeye devam edin. Bunlar asla GC'd olmayacak.
public class Leaker {
private static final Map<String, Object> CACHE = new HashMap<String, Object>();
// Keep adding until failure.
public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}
Sınıfın sonlandırma yönteminde bir sınıfın yeni bir örneğini oluşturarak hareketli bir bellek sızıntısı oluşturabilirsiniz. Sonlandırıcı birden fazla örnek oluşturuyorsa bonus puanlar. Aşağıda, yığın boyutunuza bağlı olarak tüm yığını birkaç saniye ile birkaç dakika arasında sızdıran basit bir program var:
class Leakee {
public void check() {
if (depth > 2) {
Leaker.done();
}
}
private int depth;
public Leakee(int d) {
depth = d;
}
protected void finalize() {
new Leakee(depth + 1).check();
new Leakee(depth + 1).check();
}
}
public class Leaker {
private static boolean makeMore = true;
public static void done() {
makeMore = false;
}
public static void main(String[] args) throws InterruptedException {
// make a bunch of them until the garbage collector gets active
while (makeMore) {
new Leakee(0).check();
}
// sit back and watch the finalizers chew through memory
while (true) {
Thread.sleep(1000);
System.out.println("memory=" +
Runtime.getRuntime().freeMemory() + " / " +
Runtime.getRuntime().totalMemory());
}
}
}
Kimsenin bunu henüz söylediğini sanmıyorum: finalize () yöntemini geçersiz kılarak bir nesneyi diriltebilirsiniz, böylece finalize () bunun bir referansını saklar. Çöp toplayıcı nesnede yalnızca bir kez çağrılır, bundan sonra nesne asla yok olmaz.
finalize()
çağrılmaz, ancak daha fazla referans olmayacak şekilde nesne toplanır. Çöp toplayıcı da 'çağrılmaz'.
finalize()
yöntem JVM tarafından yalnızca bir kez çağrılabilir, ancak bu, nesne yeniden dirilir ve sonra tekrar başvurulursa yeniden toplanamayacağı anlamına gelmez. finalize()
Yöntemde kaynak kapatma kodu varsa, bu kod yeniden çalıştırılmaz, bu bellek sızıntısına neden olabilir.
Son zamanlarda daha ince bir kaynak sızıntısıyla karşılaştım. Sınıf yükleyicisinin getResourceAsStream aracılığıyla kaynakları açıyoruz ve giriş akışı tanıtıcıları kapatılmadı.
Uhm, diyebilirsin, ne salakça.
Bunu ilginç kılan şey: Bu şekilde, JVM'nin yığınından ziyade altta yatan sürecin yığın hafızasını sızdırabilirsiniz.
İhtiyacınız olan tek şey, içinde Java kodundan başvurulacak bir dosya bulunan bir kavanoz dosyasıdır. Kavanoz dosyası büyüdükçe, daha hızlı bellek ayrılır.
Aşağıdaki sınıfla kolayca böyle bir kavanoz oluşturabilirsiniz:
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class BigJarCreator {
public static void main(String[] args) throws IOException {
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
zos.putNextEntry(new ZipEntry("resource.txt"));
zos.write("not too much in here".getBytes());
zos.closeEntry();
zos.putNextEntry(new ZipEntry("largeFile.out"));
for (int i=0 ; i<10000000 ; i++) {
zos.write((int) (Math.round(Math.random()*100)+20));
}
zos.closeEntry();
zos.close();
}
}
Sadece BigJarCreator.java adlı bir dosyaya yapıştırın, derleyin ve komut satırından çalıştırın:
javac BigJarCreator.java
java -cp . BigJarCreator
Et voilà: geçerli çalışma dizininizde içinde iki dosya bulunan bir kavanoz arşivi bulacaksınız.
İkinci bir sınıf oluşturalım:
public class MemLeak {
public static void main(String[] args) throws InterruptedException {
int ITERATIONS=100000;
for (int i=0 ; i<ITERATIONS ; i++) {
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
}
System.out.println("finished creation of streams, now waiting to be killed");
Thread.sleep(Long.MAX_VALUE);
}
}
Bu sınıf temelde hiçbir şey yapmaz, ancak referanssız InputStream nesneleri oluşturur. Bu nesneler hemen çöp toplanacak ve bu nedenle yığın boyutuna katkıda bulunmayacaktır. Örneğimiz için mevcut bir kaynağı bir jar dosyasından yüklemek önemlidir ve burada boyut önemlidir.
Şüpheniz varsa, yukarıdaki sınıfı derlemeye ve başlatmaya çalışın, ancak iyi bir yığın boyutu (2 MB) seçtiğinizden emin olun:
javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak
Burada bir OOM hatasıyla karşılaşmayacaksınız, hiçbir referans tutulmadığından, yukarıdaki örnekte ne kadar büyük ITERATIONS seçmiş olursanız olun uygulama çalışmaya devam edecektir. Uygulama wait komutuna ulaşmadıkça, işleminizin bellek tüketimi (üstte (RES / RSS) veya işlem gezgininde görünür) artar. Yukarıdaki kurulumda, yaklaşık 150 MB bellek ayıracaktır.
Uygulamanın güvenli oynatılmasını istiyorsanız, giriş akışını oluşturulduğu yerden kapatın:
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();
ve işleminiz yineleme sayısından bağımsız olarak 35 MB'ı aşmayacaktır.
Oldukça basit ve şaşırtıcı.
Bir çok insanın önerdiği gibi, Kaynak Kaçaklarına neden olması oldukça kolaydır - JDBC örnekleri gibi. Gerçek Bellek sızıntıları biraz daha zordur - özellikle de sizin için JVM'nin kırık parçalarına güvenmiyorsanız ...
Çok büyük bir alan kaplayan ve daha sonra bunlara erişemeyen nesneler oluşturma fikirleri de gerçek bellek sızıntıları değildir. Hiçbir şey ona erişemezse, o zaman çöp toplanır ve bir şey erişebilirse o zaman bir sızıntı değildir ...
Eskiden çalışmanın bir yolu - ve hala işe yarayıp yaramadığını bilmiyorum - üç derin dairesel bir zincire sahip olmak. A nesnesindeki gibi B nesnesine bir referansı vardır, B nesnesinin C nesnesine bir referansı vardır ve C nesnesinin A nesnesine bir referansı vardır. GC, iki derin zincirin - A <--> B - A ve B'ye başka bir şey tarafından erişilemiyorsa, ancak üç yönlü zinciri kaldıramazsa, güvenli bir şekilde toplanabilir ...
Potansiyel olarak büyük bellek sızıntıları oluşturmanın başka bir yolu Map.Entry<K,V>
, a TreeMap
.
Bunun neden sadece TreeMap
s için geçerli olduğunu değerlendirmek zordur , ancak uygulamaya bakarak bunun nedeni şu olabilir: bir TreeMap.Entry
mağaza kardeşlerine atıfta bulunur, bu nedenle a TreeMap
toplanmaya hazırsa, ancak diğer bazı sınıflar onun Map.Entry
ardından, tüm Harita hafızada tutulur.
Gerçek hayat senaryosu:
Büyük bir TreeMap
veri yapısı döndüren bir db sorgusu olduğunu düşünün . İnsanlar genellikle TreeMap
öğe ekleme sırası korunurken s kullanırlar .
public static Map<String, Integer> pseudoQueryDatabase();
Sorgu birçok kez çağrıldıysa ve her sorgu için (her biri Map
geri döndüğünde) bir Entry
yere kaydederseniz, bellek sürekli olarak büyümeye devam eder.
Aşağıdaki sarmalayıcı sınıfını düşünün:
class EntryHolder {
Map.Entry<String, Integer> entry;
EntryHolder(Map.Entry<String, Integer> entry) {
this.entry = entry;
}
}
Uygulama:
public class LeakTest {
private final List<EntryHolder> holdersCache = new ArrayList<>();
private static final int MAP_SIZE = 100_000;
public void run() {
// create 500 entries each holding a reference to an Entry of a TreeMap
IntStream.range(0, 500).forEach(value -> {
// create map
final Map<String, Integer> map = pseudoQueryDatabase();
final int index = new Random().nextInt(MAP_SIZE);
// get random entry from map
for (Map.Entry<String, Integer> entry : map.entrySet()) {
if (entry.getValue().equals(index)) {
holdersCache.add(new EntryHolder(entry));
break;
}
}
// to observe behavior in visualvm
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
public static Map<String, Integer> pseudoQueryDatabase() {
final Map<String, Integer> map = new TreeMap<>();
IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
return map;
}
public static void main(String[] args) throws Exception {
new LeakTest().run();
}
}
Her pseudoQueryDatabase()
çağrıdan sonra , map
örnekler toplanmaya hazır olmalıdır, ancak en azından biri Entry
başka bir yerde saklandığından gerçekleşmez.
jvm
Ayarlarınıza bağlı olarak , uygulama nedeniyle erken aşamada çökebilir OutOfMemoryError
.
Bu visualvm
grafikten belleğin nasıl büyüdüğünü görebilirsiniz.
Karma veri yapısı ( HashMap
) için de aynı şey olmaz .
Bu, a HashMap
.
Çözüm? Kaydetmek yerine anahtarı / değeri (muhtemelen zaten yaptığınız gibi) doğrudan kaydedin Map.Entry
.
Burada daha kapsamlı bir karşılaştırma ölçütü yazdım .
Konular sonlanıncaya kadar toplanmaz. Çöp toplamanın kökleri olarak hizmet ederler . Bunlar, sadece onları unutarak veya onlara atıfta bulunarak geri kazanılmayacak birkaç nesneden biridir.
Dikkate alın: bir iş parçacığı sonlandırmak için temel desen iş parçacığı tarafından görülen bazı koşul değişkeni ayarlamaktır. İş parçacığı, değişkeni periyodik olarak kontrol edebilir ve sonlandırmak için bir sinyal olarak kullanabilir. Değişken bildirilmezse volatile
, değişkende yapılan değişiklik iş parçacığı tarafından görülmeyebilir, bu nedenle sonlandırmayı bilemez. Ya da bazı iş parçacıklarının paylaşılan bir nesneyi güncellemek isteyip istemediğinizi, ancak kilitlemeye çalışırken kilitlenip kilitlenmediğini düşünün.
Sadece bir avuç iş parçacığınız varsa, programınız düzgün çalışmayı durduracağı için bu hatalar muhtemelen açıktır. Gerektiğinde daha fazla iş parçacığı oluşturan bir iş parçacığı havuzunuz varsa, eski / sıkışmış iş parçacıkları fark edilmeyebilir ve süresiz olarak birikerek bellek sızıntısına neden olur. İş parçacıklarının uygulamanızda başka veriler kullanması muhtemeldir, bu nedenle doğrudan başvurdukları her şeyin toplanmasını da önler.
Oyuncak örneği olarak:
static void leakMe(final Object object) {
new Thread() {
public void run() {
Object o = object;
for (;;) {
try {
sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {}
}
}
}.start();
}
Çağrı System.gc()
sizin gibi tüm, ama geçirilen nesne leakMe
asla ölmeyecek.
(* Düzenlenmiş *)
Geçerli bir örnek iş parçacığı bir ortamda ThreadLocal değişkenleri kullanarak olabileceğini düşünüyorum.
Örneğin, diğer web bileşenleri ile iletişim kurmak, iş parçacığı tarafından oluşturulan iş parçacıklarına sahip olmak ve bir havuzda boşta kalanları korumak için Servlet'lerde ThreadLocal değişkenleri kullanmak. ThreadLocal değişkenleri, doğru şekilde temizlenmezse, muhtemelen aynı web bileşeni değerlerinin üzerine yazılıncaya kadar orada kalır.
Tabii ki, bir kez tanımlandığında, sorun kolayca çözülebilir.
Görüşmeci dairesel bir referans çözümü arıyor olabilir:
public static void main(String[] args) {
while (true) {
Element first = new Element();
first.next = new Element();
first.next.next = first;
}
}
Çöp toplayıcıları saymakla ilgili klasik bir sorundur. O zaman kibarca JVM'lerin bu sınırlamaya sahip olmayan çok daha karmaşık bir algoritma kullandıklarını kibarca açıklarsınız.
-Wes Tarle
first
yararlı değildir ve çöp toplanmalıdır. İçinde referans sayma (kendi başına), bunun üzerinde aktif bir referans yoktur, çünkü çöp toplayıcı, nesne freeed olmaz. Sonsuz döngü sızıntıyı azaltmak için burada: programı çalıştırdığınızda bellek süresiz olarak artacak.