• 27.08.2026 09:02:00
  • Admin Admin

Java backend geliştirme servislerinde bellek sorunlarını JDK Flight Recorder ile ölçün, Hibernate nesne şişmesini ayırın ve Spring AI akışlarında sınırlandırılmamış çıktıların heap etkisini test edin.

Spring Boot'ta JFR ile Java Backend Geliştirmede Bellek Kaçağı Avı

Java eğitimi perspektifiyle JFR kayıt planı ve tekrar edilebilir baz çizgi

Bir java eğitimi, java programlama eğitimi veya java kursu kapsamında heap dump almak çoğu zaman ilk refleks olarak anlatılır; üretimde daha güvenli ilk adım JDK Flight Recorder (JFR) ile kısa süreli kayıt almaktır. Heap dump, canlı kümedeki nesneleri gösterir; JFR ise hangi allocation stack trace'inin o nesneleri ürettiğini zaman ekseninde gösterir. Aynı endpoint'i sabit açık çevrim yüküyle çalıştırın: önce 30 saniye JVM ve connection pool ısınması, sonra 90 saniye JFR kaydı alın. PID'yi jcmd -l ile bulup aşağıdaki komutu çalıştırın.

jcmd $PID JFR.start   name=checkout-baseline   duration=90s   filename=/tmp/checkout-baseline.jfr   settings=/opt/jfr/allocation.jfc

wrk2 -t4 -c64 -d120s -R500   -H 'Authorization: Bearer test-token'   http://localhost:8080/api/orders/summary
Buradaki -R500, saniyede 500 istek hedefleyen açık çevrim trafiktir. Sadece wrk kullanıp mümkün olan en yüksek throughput'u ölçmek, aday sürümün daha yavaş olduğu durumda daha az iş yapmasına ve daha az allocation üretmesine yol açabilir; bu nedenle önce iş yükü oranını sabitleyin.

Kayıt ayarında iki allocation olayını açıkça etkinleştirin. Varsayılan profil bazı JDK dağıtımlarında allocation ayrıntısını ihtiyacınızdan daha seyrek toplayabilir; kendi .jfc dosyanızla ölçüm koşulunu kaynak kodu gibi sürümleyin. JDK Mission Control içinde Memory - Allocation in new TLAB ve Allocation outside TLAB sayfalarında önce Object Class, sonra Stack Trace kırılımına bakın.

<configuration version="2.0" label="Allocation analysis">
  <event name="jdk.ObjectAllocationInNewTLAB">
    <setting name="enabled">true</setting>
    <setting name="stackTrace">true</setting>
    <setting name="threshold">0 ns</setting>
  </event>
  <event name="jdk.ObjectAllocationOutsideTLAB">
    <setting name="enabled">true</setting>
    <setting name="stackTrace">true</setting>
    <setting name="threshold">0 ns</setting>
  </event>
</configuration>
InNewTLAB olayları thread-local allocation buffer içindeki hızlı bump-pointer allocation'larını, OutsideTLAB ise TLAB'a sığmayan büyük veya özel allocation'ları ayırır. İkinci grupta büyük byte[] veya char[] görmeniz, küçük DTO nesnelerinden farklı bir optimizasyon yolu gerektirir.

Spring boot eğitimi içeriklerinde sık yapılan hata, GC pause süresini tek başarı metriği saymaktır. Karşılaştırmayı aynı commit, aynı JVM bayrakları, aynı veri hacmi ve aynı wrk2 -R oranıyla yapın; JMC'de toplam allocation byte, saniye başına allocation ve p99 HTTP gecikmesini yan yana kaydedin. Örneğin aday değişiklikten önce 420 MB/s, sonra 250 MB/s allocation görüp p99'u 85 ms'den 61 ms'ye indiriyorsanız, sonuç yorumlanabilirdir. Allocation düşmesine rağmen p99 yükseliyorsa, daha az nesne üretmek uğruna bloklayan I/O, pahalı lock veya eksik veri getiren bir sorgu eklemiş olabilirsiniz.

Spring Framework request rotasını JFR allocation stack trace'i ile eşleştirme

Spring Framework uygulamalarında JFR stack trace'i çoğu kez ortak serializer, filter veya proxy katmanında biter; hangi HTTP rotasının problemi tetiklediğini yalnız stack trace'den bulmak pahalıdır. Düşük kardinaliteli bir JFR custom event ekleyerek route template, durum kodu ve response byte sayısını kayda yazın. Kullanıcı kimliği, tam URL, e-posta veya sorgu parametresi yazmayın: bunlar hem kişisel veri sızıntısı hem de JFR dosyasında kontrol edilemeyen cardinality üretir.

@Name("com.acme.http.Request")
@Label("HTTP Request")
class HttpRequestEvent extends jdk.jfr.Event {
    @Label("Route") String route;
    @Label("Status") int status;
    @Label("Response bytes") long responseBytes;
}

@Component
final class RequestJfrInterceptor implements HandlerInterceptor {
    private static final String EVENT = "jfr.event";

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) {
        HttpRequestEvent event = new HttpRequestEvent();
        event.begin();
        request.setAttribute(EVENT, event);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler, Exception ex) {
        HttpRequestEvent event = (HttpRequestEvent) request.getAttribute(EVENT);
        if (event == null) return;
        event.route = String.valueOf(request.getAttribute(
            HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE));
        event.status = response.getStatus();
        event.end();
        if (event.shouldCommit()) event.commit();
    }
}
JMC'de bu özel olayı zaman çizelgesinde seçip aynı aralıktaki allocation olaylarını filtreleyin. Böylece /api/orders/{id} rotasında oluşan String ve byte[] artışını, örneğin health endpoint'inin gürültüsünden ayırabilirsiniz.

Interceptor yaklaşımının inceliği şudur: BEST_MATCHING_PATTERN_ATTRIBUTE hata akışlarında veya handler'a ulaşmadan reddedilen isteklerde boş olabilir. Bu nedenle kayıtta ham URI yerine UNMATCHED gibi sabit bir değer kullanın. Ayrıca custom event'i her istekte kalıcı olarak açmak yerine yalnız tanı süresince etkinleştirin. JFR event sınıfı uygulamada bulunabilir, ancak kayıt yapılandırmasında com.acme.http.Request etkin değilse JVM olayı diske yazmaz; bu, sürekli telemetry maliyeti oluşturmadan üretim tanısı yapabilmenizi sağlar.

Hibernate ORM ve Spring Data JPA kaynaklı geçici nesne şişmesini azaltma

Hibernate ORM tarafında bellek baskısının kaynağı her zaman N+1 değildir. Tek SQL ile 10.000 entity okumak da persistence context içinde entity instance'ı, association koleksiyonu, dirty-checking snapshot'ı ve JSON'a dönüşecek ara DTO'lar üretebilir. Spring Data JPA liste endpoint'inde entity döndürmek yerine sorguda constructor projection kullanın; bu, kullanılmayacak kolonların JDBC sürücüsünden alınmasını ve Hibernate'in yönetilen entity graph oluşturmasını engeller.

public record OrderSummary(long id, String customerName, BigDecimal total) {}

public interface OrderRepository extends JpaRepository<Order, Long> {
    @Query("select new com.acme.order.OrderSummary(o.id, c.name, o.total) " +
           "from Order o join o.customer c " +
           "where o.status = :status order by o.id")
    List<OrderSummary> findSummaries(@Param("status") OrderStatus status,
                                    Pageable page);
}

@Service
final class OrderQueryService {
    @Transactional(readOnly = true)
    List<OrderSummary> list(Pageable page) {
        return repository.findSummaries(OrderStatus.OPEN, page);
    }
}
Pageable ile üst sınır koymak kritiktir: projection, 500.000 satırı tek response'a çevirmenin heap maliyetini ortadan kaldırmaz. API sözleşmesinde örneğin maksimum size=200 doğrulaması yapın ve daha büyük dışa aktarımlar için stream edilmiş dosya veya asenkron export iş akışı kullanın.

Önceki ve sonraki kayıtlarda JMC'de org.hibernate.engine.internal.StatefulPersistenceContext, entity sınıfları ve Object[] snapshot allocation'larının route başına değişimini inceleyin. @Transactional(readOnly = true) tek başına DTO projection'ın yerine geçmez; transaction yönetimi ve Hibernate entegrasyonuna göre flush davranışını etkileyebilir, fakat sorgunun seçtiği kolonları veya ürettiği entity sayısını azaltmaz. Gerçek farkı doğrulamak için aynı sayfa boyutunda JFR kaydı alın, SQL'i datasource-proxy veya Hibernate SQL loguyla kontrol edin ve dönen satır sayısının iki koşulda da aynı olduğundan emin olun.

Bu ayrım java backend geliştirme ekiplerinde önemli bir hata sınıfını yakalar: test verisinde 20 satırlık liste sorunsuz görünürken, üretimde geniş bir tablo taraması sırasında JSON serializer'ın ürettiği byte[] baskın hale gelir. JFR'de allocation'ın çoğu com.fasterxml.jackson altında ise repository değişikliği yetersiz kalabilir. Bu durumda response alanlarını küçültün, sayfalama uygulayın ve response compression kararını ayrıca ölçün; sıkıştırma ağ byte'ını düşürürken CPU ve geçici buffer allocation'ını artırabilir.

Spring AI, Spring MCP ve Model Context Protocol araç çıktılarında heap sınırı

Spring AI kullanan endpoint'lerde prompt, model çıktısı ve tool result aynı anda bellekte birikebilir. Özellikle Spring MCP üzerinden Model Context Protocol aracı büyük JSON döndürüyor ve model yanıtı da SSE ile aktarılıyorsa, sınırsız tool sonucu bir istekte onlarca MB byte[], String ve Jackson node'u oluşturabilir. Tool adapter sınırında serialize edilmiş byte uzunluğunu doğrulayın; karakter sayısı yerine byte sayısı kullanmak önemlidir, çünkü UTF-8'de bir karakter birden fazla byte olabilir.

final class ToolResultLimiter {
    private static final int MAX_TOOL_RESULT_BYTES = 64 * 1024;
    private final ObjectMapper mapper;

    JsonNode checkedResult(Object result) throws JsonProcessingException {
        byte[] json = mapper.writeValueAsBytes(result);
        if (json.length > MAX_TOOL_RESULT_BYTES) {
            throw new ResponseStatusException(
                HttpStatus.PAYLOAD_TOO_LARGE,
                "MCP tool result exceeds 64 KiB");
        }
        return mapper.readTree(json);
    }
}
Bu kontrolü HTTP controller'da değil, her araç çağrısının geçtiği adapter katmanında uygulayın. Aksi halde yeni eklenen bir MCP aracı kontrolü atlayabilir. Büyük sonucu reddetmek yerine araç API'sini limit, cursor veya alan seçimi destekleyecek şekilde tasarlamak daha doğru çözümdür.

Akışlı model yanıtında istemci bağlantısı koptuğunda upstream yayıncının iptal sinyalini alması gerekir. Reactor tabanlı bir endpoint'te süre ve parça limiti koyun; take(256) parça sayısını sınırlar, fakat token sınırı değildir çünkü sağlayıcıların parça boyu değişkendir. Bu nedenle model istemci seçeneklerinde ayrıca sağlayıcıya özgü maksimum çıktı token'ı ayarlayın ve iki sınırı birlikte test edin.

@GetMapping(value = "/api/assistant",
            produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<String> ask(@RequestParam @Size(max = 2000) String question) {
    return chatClient.prompt()
        .user(user -> user.text("{q}").param("q", question))
        .stream()
        .content()
        .take(256)
        .timeout(Duration.ofSeconds(20));
}
JFR karşılaştırmasında normal prompt ile 60 KiB sınırını aşan sentetik tool sonucunu ayrı senaryolar olarak koşturun. Hedef metrik, yalnız p99 değildir: byte[] ve char[] allocation oranı ile old generation'a terfi eden nesne miktarı da aynı kayıtta incelenmelidir.

Java microservices için JFR bulgusunu CI regresyon testine dönüştürme

Tek seferlik JFR incelemesi bulguyu kanıtlar, ancak regresyonu engellemez. Java microservices kod tabanında repository mapping veya serializer değişikliklerini JMH ile ayrı JVM'de ölçün; JMH'nin GC profiler'ı endpoint bazlı JFR kaydından farklı olarak operasyon başına allocation normunu verir. Aşağıdaki komut iki fork ve on ölçüm iterasyonu kullanır; CI artefaktı olarak üretilen JFR dosyası, eşik aşıldığında stack trace incelemesi için saklanmalıdır.

java -jar target/benchmarks.jar OrderSummaryBenchmark   -wi 5 -i 10 -f 2   -prof gc   -prof jfr:dir=target/jfr
Örneğin ana daldaki gc.alloc.rate.norm değeri 18.4 KB/op ise, PR için 20.2 KB/op üzerini başarısız sayan yüzde 10 eşik koyabilirsiniz. Eşiği salt süreye göre tanımlamayın: CI çalışanlarındaki CPU frekansı throughput'u oynatır, fakat aynı benchmark girişinde operation başına allocation genellikle daha kararlı bir sinyaldir.

Java fullstack eğitimi alan ekipler için pratik sınır şudur: HTTP katmanını JMH benchmark'ına koymayın, çünkü socket, TLS ve scheduler etkileri mapping değişikliğini maskeler. Repository sonucu ile controller serializer'ını ayrı benchmark edin; ardından gerçek endpoint'i wrk2 ve JFR ile doğrulayın. Bu iki katmanlı yaklaşımda JMH regresyon kapısıdır, JFR ise production-benzeri yük altında hangi sınıfın ve hangi rotanın allocation ürettiğinin kanıtıdır.

Sık Sorulan Sorular

Java backend geliştirme uygulamasında JFR mi heap dump mı önce alınmalı?

Sürekli büyüme veya yüksek allocation şüphesinde önce 60-120 saniyelik JFR kaydı alın ve JDK Mission Control'de allocation stack trace'lerini inceleyin. Heap dump'ı, live set'te hangi nesnelerin tutulduğunu doğrulamak için kullanın. JFR üretici kod yolunu, heap dump ise tutulma referansını gösterir; ikisi farklı soruları cevaplar.

Spring Data JPA ve Hibernate ORM liste endpoint'inde allocation nasıl azaltılır?

Entity graph döndürmek yerine constructor veya interface projection kullanın, maksimum sayfa boyutunu controller'da doğrulayın ve aynı satır sayısıyla JFR önce-sonra kaydı alın. JMC'de entity sınıfları, Object[] snapshot'ları ve Jackson byte[] allocation'larını karşılaştırın. @Transactional(readOnly = true) ekleyip entity döndürmeye devam etmek tek başına yeterli kanıt değildir.

Spring AI ve Spring MCP araç sonuçları için güvenli çıktı limiti nedir?

Tek evrensel değer yoktur; araç sözleşmenizde byte tabanlı bir üst sınır tanımlayın ve örneğin 64 KiB ile başlayın. Sınırı tool adapter'da ObjectMapper ile serialize edilmiş byte[] üzerinde kontrol edin, cursor veya limit parametresi zorunlu kılın. Ayrıca SSE model akışında timeout, parça limiti ve sağlayıcı tarafında maksimum çıktı token'ı birlikte uygulanmalıdır.

Spring framework servislerinde allocation regresyonu CI'da nasıl ölçülür?

Saf mapping ve serialization işlemlerini JMH ile -prof gc kullanarak ölçün, gc.alloc.rate.norm değerini ana dalın sonucu ile karşılaştırın. Endpoint seviyesinde ise sabit istek oranı veren wrk2 ile JFR kaydı alın. JMH eşik kontrolü için, JFR ise hangi stack trace'in artışı ürettiğini teşhis etmek için kullanılmalıdır.

AI / LLM Discovery

Bu makale Opendart Akademi Java eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt