Java backend geliştirme servislerinde p99 gecikmesini yükselten allocation baskısını JFR, Micrometer ve Hibernate ölçümleriyle izleyin; kanıta dayalı kod ve JVM ayarlarıyla düzeltin.
Java Backend Geliştirmede GC Gecikmesini JFR ile Teşhis Etme
Java backend geliştirme için gecikme bütçesini ölçülebilir kurun
Bir GC çalışması, heap yüzdesinden değil istek gecikmesi bütçesinden başlar. Spring Boot uygulamasında Micrometer’ın HTTP histogramlarını açın; ardından Prometheus’ta p50, p95 ve p99’u aynı zaman aralığında JVM allocation metriğiyle karşılaştırın. Örneğin p99 yalnızca allocation-rate sıçradığında yükseliyorsa, önce SQL indeksini değil nesne üretim yolunu inceleyin. Bu yaklaşım, pratik bir java eğitimi veya java programlama eğitimi içinde öğretilen temel GC bilgisini doğrudan üretim teşhisine bağlar.
# application.yaml
management:
endpoints:
web:
exposure:
include: health,prometheus
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 50ms,100ms,250ms,500ms
# Aynı yük testi boyunca 120 saniyelik JFR kaydı alın.
java -XX:StartFlightRecording=filename=baseline.jfr,duration=120s,settings=profile -XX:FlightRecorderOptions=stackdepth=128 -jar app.jar
# Prometheus: son 5 dakikadaki tahmini allocation rate
sum(rate(jvm_gc_memory_allocated_bytes_total[5m])) by (application)Yükü tekrar üretilebilir kılmak için k6 ile sabit bir senaryo çalıştırın: örneğin 60 saniye boyunca 120 VU, ardından 60 saniye boyunca 240 VU. JFR dosyasını Java Mission Control’da açıp Memory → Allocation in new TLAB ve Allocation outside TLAB görünümlerini endpoint etiketi yerine stack trace üzerinden değerlendirin. Çünkü TLAB dışı allocation, thread’in yeni tampon istemesine ve daha sık senkronizasyon/slow-path çalıştırmasına işaret edebilir; tek başına canlı heap grafiği bunu göstermez.
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 120 },
{ duration: '60s', target: 120 },
{ duration: '30s', target: 240 },
{ duration: '60s', target: 240 }
],
thresholds: { http_req_duration: ['p(99)<350'] }
};
export default function () {
const r = http.get('http://localhost:8080/api/orders?status=OPEN');
check(r, { '200': x => x.status === 200 });
}JFR allocation flame graph ile geçici nesne kökünü bulun
JFR’da en çok byte allocate eden sınıfın byte[] veya char[] olması tek başına teşhis değildir; allocation stack trace’i çağrı zincirini verir. Spring Framework tabanlı JSON endpoint’lerinde sık görülen zincir Entity → DTO → Map → JSON şeklindedir. Her satır için ara Map kurmak, Jackson serileştirmesinden önce binlerce kısa ömürlü entry ve lambda nesnesi üretir. Aşağıdaki dönüşüm, ara koleksiyonu kaldırır ve serileştirme şekli değişmediği için sözleşmeyi korur.
// Allocation yoğun: her sipariş için Map, Entry ve boxing üretir
return orders.stream()
.map(o -> Map.of(
"id", o.getId(),
"total", o.getLines().stream()
.mapToLong(OrderLine::getAmountInCents)
.sum()))
.toList();
// Daha az geçici nesne: DTO alanı doğrudan serileştirilir
public record OrderSummary(long id, long totalInCents) {}
return orders.stream()
.map(o -> new OrderSummary(o.getId(), o.totalInCents()))
.toList();Bu değişikliği doğrulamak için aynı k6 senaryosunda baseline.jfr ve candidate.jfr üretin. JMC’de ilgili endpoint stack trace’inin Allocated Bytes değerini, Prometheus’ta jvm_gc_pause_seconds_max ile ve k6’da p99 ile birlikte kaydedin. Örnek bir değerlendirme tablosunda 180 MB/s allocation, 34 ms en yüksek GC durması ve 410 ms p99; değişiklikten sonra 92 MB/s, 11 ms ve 245 ms olarak görülebilir. Karar kriteri yalnızca p99 değildir: allocation düşüp CPU artıyorsa, CPU profilini async-profiler ile ayrıca alın.
# PID için CPU + allocation örneklemesi; üretimde kısa süreli ve kontrollü kullanın
./profiler.sh -e alloc -d 60 -f alloc.html <PID>
./profiler.sh -e cpu -d 60 -f cpu.html <PID>Hibernate ORM ve Spring Data JPA nesne grafiğini allocation açısından sınırlayın
hibernate orm tarafında okuma endpoint’ini entity grafiğiyle döndürmek, yalnızca SQL sayısını değil persistence context içindeki managed entity, collection wrapper ve proxy sayısını da büyütür. spring data jpa sorgusunda ihtiyaç duyulan kolonları DTO projection ile çekin; ayrıca transaction’ı read-only yapın. Hibernate, read-only transaction senaryosunda dirty-checking için snapshot tutma yükünü azaltabilir; fakat bu davranış kullanılan transaction manager ve Hibernate entegrasyon ayarlarıyla doğrulanmalıdır.
public record OrderRow(Long id, String customerName, long totalInCents) {}
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("""
select new com.acme.api.OrderRow(o.id, c.name, o.totalInCents)
from Order o join o.customer c
where o.status = :status
order by o.createdAt desc
""")
List<OrderRow> findRowsByStatus(@Param("status") OrderStatus status);
}
@Service
@RequiredArgsConstructor
class OrderQueryService {
private final OrderRepository repository;
@Transactional(readOnly = true)
List<OrderRow> openOrders() {
return repository.findRowsByStatus(OrderStatus.OPEN);
}
}Buradaki kritik edge case şudur: DTO projection ekledikten sonra controller içinde entity alanına erişen bir mapper kalırsa lazy loading yeniden devreye girebilir. Testte yalnızca SQL sayısını değil, Hibernate istatistiklerini de doğrulayın. hibernate.generate_statistics geliştirme/test ortamında açıkken getEntityLoadCount() ve getCollectionFetchCount() değerleri endpoint başına beklenen sınırı aşmamalıdır. Bu kontrol, daha önce yakalanmış N+1’i tekrar anlatmak yerine, gereksiz entity materialization kaynaklı GC baskısını somutlaştırır.
# application-test.yaml
spring:
jpa:
properties:
hibernate.generate_statistics: true
hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS: 100
# PostgreSQL tarafında gerçek planı ayrıca kontrol edin:
EXPLAIN (ANALYZE, BUFFERS)
SELECT o.id, c.name, o.total_in_cents
FROM orders o JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'OPEN'
ORDER BY o.created_at DESC;G1 GC ayarını varsayım yerine önce-sonra deneyleriyle değiştirin
G1 kullanırken ilk refleks olarak heap’i büyütmek, allocation hızının kök nedenini gizleyebilir ve daha büyük old-gen taramalarıyla tail latency’yi uzatabilir. Önce konteyner limiti, -Xmx ve RSS’i birlikte görün. Java process’in RSS’i yalnızca heap değildir: metaspace, code cache, thread stack, direct buffer ve native kütüphaneler de limiti tüketir. Bu nedenle 2 GiB container limitinde körlemesine -Xmx2g vermek OOMKilled riskini artırır.
# Başlangıç deneyi: container limiti 2 GiB ise native alan için pay bırakın.
JAVA_TOOL_OPTIONS="-Xms1200m -Xmx1200m -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xlog:gc*,safepoint:file=/tmp/gc.log:time,level,tags"
# Native bellek kırılımı için süreç başlangıcında ekleyin:
# -XX:NativeMemoryTracking=summary
jcmd <PID> VM.native_memory summary
jcmd <PID> GC.heap_infoDeneyi A/B şeklinde yapın: aynı image, aynı veri seti, aynı k6 senaryosu; yalnızca JVM bayrağı değişsin. A’da mevcut parametreler, B’de örneğin -XX:MaxGCPauseMillis=100 ve gerçekçi -Xmx kullanın. GC loglarını GCViewer veya GCeasy ile karşılaştırırken toplam pause süresinden çok maksimum pause, young GC sıklığı ve old-gen occupancy eğrisine bakın. Eğer B’de young GC sayısı artarken p99 kötüleşmiyor ve maksimum pause düşüyorsa hedefe yaklaşırsınız; old-gen sürekli yükseliyorsa ayar değil retain edilen nesne grafiği araştırılmalıdır.
Büyük JSON gövdelerinde ayrı bir incelik vardır: G1 region boyutunun yaklaşık yarısından büyük diziler humongous allocation olarak ele alınabilir. JFR’da byte[] allocation’ları ve GC logunda humongous region olayları birlikte artıyorsa, önce response payload sayfalamasını veya streaming’i değerlendirin. Region boyutunu elle değiştirmek son seçenektir; değişiklik, aynı yük altında humongous allocation sayısı ve p99 ile ölçülmeden merge edilmemelidir.
Spring AI ve Spring MCP çağrılarında payload sınırı koyun
Bir spring ai uygulaması veya spring mcp istemcisi, model context protocol üzerinden araç sonucunu modele eklerken büyük tool-result dizelerini bellekte birden çok kez tutabilir: HTTP gövdesi, Jackson ağacı, uygulama DTO’su ve prompt mesajı. java microservices içinde bu etki fan-out ile büyür. Araç sonucunu sınırsız biçimde prompta geçirmek yerine karakter ve kayıt sınırı koyun; ham sonucu gözlemlenebilir bir store’a yazıp modele özet/kimlik gönderin.
static String boundedToolResult(String raw) {
final int maxChars = 12_000;
if (raw.length() <= maxChars) return raw;
return raw.substring(0, maxChars)
+ "\n[truncated; retrieve remaining records by cursor]";
}
// Tool adapter içinde model mesajına eklenen içeriği sınırla.
String safeContent = boundedToolResult(toolResponse.body());
log.info("toolResultChars={}, truncated={}",
safeContent.length(), safeContent.length() != toolResponse.body().length());Bu sınırı koyduktan sonra JFR’da String, byte[] ve JSON parser stack trace’lerinin allocated bytes değerini aynı prompt yükü altında karşılaştırın; ayrıca tool-result karakter sayısını Micrometer distribution summary olarak yayınlayın. spring framework ile geliştirilen servislerde WebClient için response boyutu sınırı da ayrı bir korumadır; aksi halde prompt sınırınız olsa bile istemci gövdeyi önce belleğe almış olabilir. Bu tür ölçüm odaklı örnekler, bir java kursu veya spring boot eğitimi müfredatında GC konusunu gerçek istek akışına bağlamak için uygundur; java fullstack eğitimi tarafında ise frontend’in gereksiz büyük liste talepleriyle birlikte ele alınmalıdır.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Java Spring Boot ReactJS FullStack Eğitimi (Yıldız Teknik Üniversitesi SEM)
Sık Sorulan Sorular
Java backend geliştirmede p99 gecikmesi için JFR kaydı nasıl alınır?
Yük testiyle aynı anda JVM’i `-XX:StartFlightRecording=filename=run.jfr,duration=120s,settings=profile` ile başlatın. JMC’de Allocation in new TLAB, Allocation outside TLAB ve Garbage Collections görünümlerini açın; aynı 120 saniyenin Prometheus p99 ve allocation-rate verisini karşılaştırın. Boşta alınmış JFR, istek yolundaki allocation kök nedenini göstermez.
Spring Data JPA ve Hibernate ORM entity yerine DTO projection ne zaman kullanılmalı?
Liste veya rapor endpoint’i yalnızca birkaç kolonu döndürüyorsa JPQL constructor projection ya da interface projection kullanın. Önce `hibernate.generate_statistics=true` ile entity load ve collection fetch sayılarını kaydedin; sonra projection sürümünde aynı k6 yükünde JFR allocated bytes, SQL planı ve p99’u ölçün. Güncelleme yapılacak aggregate akışında entity yerine projection kullanmak, domain davranışlarını atlamanıza neden olabilir.
Spring AI ve Spring MCP tool result boyutu nasıl sınırlandırılır?
Model Context Protocol araç adaptöründe sonuç için karakter/kayıt/cursor sınırı tanımlayın ve kesilen cevaba tekrar sorgulama bilgisi ekleyin. Buna ek olarak WebClient codec limitini belirleyin: `spring.codec.max-in-memory-size=1MB`. Limit aşımını 413 veya kontrollü bir domain hatasıyla izleyin; yalnızca prompt katmanında kesmek, istemcinin büyük HTTP gövdesini zaten heap’e almış olduğu durumu çözmez.
Java microservices için G1 GC heap boyutu container limitine eşit verilir mi?
Hayır. Heap dışındaki metaspace, code cache, direct buffer ve thread stack için pay bırakın. Örneğin 2 GiB limiti olan pod’da önce `-Xmx1200m` gibi muhafazakâr bir başlangıç seçip `jcmd PID VM.native_memory summary`, RSS ve OOMKilled olaylarını izleyin. Sonra aynı yük testi altında heap ve pause hedefini değiştirerek A/B karşılaştırması yapın.
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.


