Back to blog

Tối Ưu Hiệu Suất & Profiling trong Spring Boot

javaspring-bootperformanceprofilingbackend
Tối Ưu Hiệu Suất & Profiling trong Spring Boot

Giới Thiệu

Ứng dụng Spring Boot của bạn hoạt động tốt. Tests pass. Người dùng đang đăng ký. Rồi một ngày, thời gian phản hồi tăng vọt từ 200ms lên 3 giây, connection pool database cạn kiệt, và dashboard giám sát chuyển sang màu đỏ.

Vấn đề hiệu suất không tự báo trước trong quá trình phát triển — chúng xuất hiện trong production dưới tải thực tế. Sự khác biệt giữa ứng dụng chậm và nhanh không phải là may mắn — mà là đo lường, profiling, và tối ưu có mục tiêu.

Hướng dẫn này dạy bạn tìm và sửa bottleneck hiệu suất một cách có hệ thống. Không đoán mò. Không tối ưu sớm. Mọi thay đổi đều được hỗ trợ bằng dữ liệu.

Bạn Sẽ Học Được Gì

✅ Xác định bottleneck hiệu suất bằng phương pháp đo lường trước
✅ Profile CPU, memory và threads với Java Flight Recorder và VisualVM
✅ Giám sát runtime metrics với Spring Boot Actuator và Micrometer
✅ Tối ưu database queries, connection pools và batch operations
✅ Tăng tốc HTTP responses với compression, caching và async handling
✅ Tinh chỉnh JVM memory và garbage collection cho production workloads
✅ Tăng tốc khởi động ứng dụng với lazy initialization và AOT compilation
✅ Thiết lập performance baselines và phát hiện regression với JMH

Điều Kiện Tiên Quyết


1. Hiểu Về Bottleneck Hiệu Suất

Trước khi tối ưu bất cứ thứ gì, bạn cần hiểu thời gian được dành ở đâu. Tối ưu sớm lãng phí công sức và thường làm code khó bảo trì hơn.

Quy Tắc Vàng: Đo Lường Trước

"Chúng ta nên quên đi những hiệu quả nhỏ, khoảng 97% thời gian: tối ưu sớm là gốc rễ của mọi điều xấu." — Donald Knuth

Bốn Loại Bottleneck

LoạiTriệu ChứngNguyên Nhân Thường GặpCông Cụ
CPUCPU usage cao, tính toán chậmThuật toán kém hiệu quả, logging quá nhiều, serializationJFR, VisualVM, top
MemoryOutOfMemoryError, GC pauses thường xuyênMemory leaks, object graphs lớn, heap size saiHeap dumps, MAT, JFR
I/O (Database)Queries chậm, connection pool cạn kiệtN+1 queries, thiếu indexes, không có cachingSlow query log, EXPLAIN ANALYZE
NetworkLatency cao, timeout errorsResponses không nén, không có HTTP caching, blocking callsWireshark, Actuator metrics

Thời Gian Thường Được Dành Ở Đâu

Trong hầu hết ứng dụng Spring Boot, đây là phân bổ:

Database queries chiếm phần lớn. Điều này có nghĩa là tối ưu query và caching mang lại lợi ích lớn nhất — đó là lý do chúng ta đã đề cập Redis caching và JPA optimization trước đó trong series này.


2. JVM Profiling với VisualVM & Java Flight Recorder

Profiling cho thấy chính xác ứng dụng của bạn dành thời gian và memory ở đâu. Hai công cụ bao phủ hầu hết nhu cầu.

Java Flight Recorder (JFR)

JFR được tích hợp sẵn trong JDK (11+) với overhead gần bằng không — an toàn cho production.

Bật JFR khi khởi động ứng dụng:

java -XX:StartFlightRecording=duration=60s,filename=recording.jfr \
     -jar target/myapp.jar

Bật JFR với continuous recording (production):

java -XX:StartFlightRecording=disk=true,maxsize=500m,maxage=1d \
     -XX:FlightRecorderOptions=stackdepth=256 \
     -jar target/myapp.jar

Kích hoạt recording trên ứng dụng đang chạy với jcmd:

# Tìm PID
jps -l
 
# Bắt đầu recording
jcmd <PID> JFR.start name=profile duration=120s filename=profile.jfr
 
# Dừng recording thủ công
jcmd <PID> JFR.stop name=profile
 
# Dump recording hiện tại
jcmd <PID> JFR.dump name=profile filename=dump.jfr

Phân Tích JFR Recordings

Mở file .jfr với JDK Mission Control (JMC) — công cụ GUI miễn phí đi kèm Oracle JDK hoặc tải riêng:

# Khởi chạy JMC (nếu đã cài)
jmc
 
# Hoặc dùng jfr command-line tool (JDK 17+)
jfr print --events jdk.CPULoad recording.jfr
jfr summary recording.jfr

Các events quan trọng cần tìm:

JFR EventCho Thấy GìHành Động
jdk.CPULoadCPU usage của process và hệ thốngXác định code CPU-bound
jdk.GCPausePhaseThời lượng và tần suất GC pauseTinh chỉnh cài đặt GC
jdk.ObjectAllocationSampleNơi objects được cấp phátGiảm allocations
jdk.JavaMonitorWaitThread contentionSửa lock contention
jdk.FileRead / jdk.FileWriteFile I/O operationsTối ưu file access
jdk.SocketRead / jdk.SocketWriteNetwork I/OXác định external calls chậm

VisualVM cho Development Profiling

VisualVM cung cấp giao diện trực quan để profiling trong quá trình phát triển:

# Cài VisualVM (macOS)
brew install --cask visualvm
 
# Khởi chạy
visualvm

Quy trình CPU Profiling:

  1. Khởi động ứng dụng Spring Boot
  2. Mở VisualVM → attach vào JVM đang chạy
  3. Vào Sampler → click CPU
  4. Kích hoạt operation chậm (ví dụ: gọi API endpoint chậm)
  5. Click Snapshot để capture kết quả
  6. Sắp xếp theo Self Time để tìm hotspots

Tạo Profiling Endpoint (Chỉ Development)

Thêm diagnostic endpoint để dễ dàng profiling trong quá trình phát triển:

@RestController
@RequestMapping("/api/debug")
@Profile("dev") // Chỉ available trong dev profile
public class ProfilingController {
 
    @Autowired
    private MeterRegistry meterRegistry;
 
    @GetMapping("/jfr/start")
    public ResponseEntity<String> startRecording()
            throws Exception {
        ProcessBuilder pb = new ProcessBuilder(
            "jcmd",
            String.valueOf(ProcessHandle.current().pid()),
            "JFR.start",
            "name=debug",
            "duration=60s",
            "filename=debug-recording.jfr"
        );
        pb.redirectErrorStream(true);
        Process process = pb.start();
        String output = new String(
            process.getInputStream().readAllBytes());
        return ResponseEntity.ok(
            "Recording started: " + output);
    }
 
    @GetMapping("/memory")
    public Map<String, Object> memoryInfo() {
        Runtime runtime = Runtime.getRuntime();
        return Map.of(
            "maxMemory", formatBytes(runtime.maxMemory()),
            "totalMemory",
                formatBytes(runtime.totalMemory()),
            "freeMemory",
                formatBytes(runtime.freeMemory()),
            "usedMemory", formatBytes(
                runtime.totalMemory()
                    - runtime.freeMemory())
        );
    }
 
    private String formatBytes(long bytes) {
        return String.format("%.2f MB",
            bytes / (1024.0 * 1024.0));
    }
}

3. Spring Boot Actuator cho Runtime Metrics

Spring Boot Actuator cung cấp monitoring endpoints sẵn sàng cho production. Kết hợp với Micrometer, nó cung cấp nền tảng metrics cho việc giám sát hiệu suất.

Thiết Lập Actuator

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
 
<!-- Micrometer Prometheus registry (cho Prometheus/Grafana) -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus,env
  endpoint:
    health:
      show-details: when-authorized
  metrics:
    tags:
      application: ${spring.application.name}
    distribution:
      percentiles-histogram:
        http.server.requests: true
      sla:
        http.server.requests: 50ms,100ms,200ms,500ms,1s

Các Metrics Quan Trọng Cần Giám Sát

HTTP request metrics:

# Tất cả HTTP request metrics
curl localhost:8080/actuator/metrics/http.server.requests
 
# Lọc theo endpoint
curl "localhost:8080/actuator/metrics/http.server.requests?tag=uri:/api/users"
 
# Lọc theo status
curl "localhost:8080/actuator/metrics/http.server.requests?tag=status:200"

JVM metrics:

# Memory usage
curl localhost:8080/actuator/metrics/jvm.memory.used
 
# GC pause time
curl localhost:8080/actuator/metrics/jvm.gc.pause
 
# Thread count
curl localhost:8080/actuator/metrics/jvm.threads.live
 
# CPU usage
curl localhost:8080/actuator/metrics/process.cpu.usage

Database metrics:

# HikariCP active connections
curl localhost:8080/actuator/metrics/hikaricp.connections.active
 
# Connection wait time
curl localhost:8080/actuator/metrics/hikaricp.connections.acquire
 
# Connection pool usage
curl localhost:8080/actuator/metrics/hikaricp.connections.usage

Custom Metrics với Micrometer

Theo dõi metrics hiệu suất cụ thể cho business:

@Service
public class OrderService {
 
    private final Counter orderCounter;
    private final Timer orderProcessingTimer;
    private final DistributionSummary orderValueSummary;
 
    public OrderService(MeterRegistry meterRegistry) {
        this.orderCounter = Counter.builder("orders.created")
            .description("Total orders created")
            .tag("type", "all")
            .register(meterRegistry);
 
        this.orderProcessingTimer = Timer
            .builder("orders.processing.time")
            .description("Order processing duration")
            .publishPercentiles(0.5, 0.95, 0.99)
            .register(meterRegistry);
 
        this.orderValueSummary = DistributionSummary
            .builder("orders.value")
            .description("Order values distribution")
            .baseUnit("dollars")
            .publishPercentiles(0.5, 0.95)
            .register(meterRegistry);
    }
 
    public Order createOrder(OrderRequest request) {
        return orderProcessingTimer.record(() -> {
            Order order = processOrder(request);
            orderCounter.increment();
            orderValueSummary.record(
                order.getTotalAmount().doubleValue());
            return order;
        });
    }
 
    private Order processOrder(OrderRequest request) {
        // Business logic
        return new Order(/* ... */);
    }
}

Gauge để theo dõi trạng thái active:

@Component
public class QueueMetrics {
 
    private final BlockingQueue<Task> taskQueue;
 
    public QueueMetrics(MeterRegistry registry,
            BlockingQueue<Task> taskQueue) {
        this.taskQueue = taskQueue;
 
        Gauge.builder("task.queue.size", taskQueue,
                BlockingQueue::size)
            .description("Current task queue size")
            .register(registry);
 
        Gauge.builder("task.queue.remaining",
                taskQueue,
                BlockingQueue::remainingCapacity)
            .description("Remaining queue capacity")
            .register(registry);
    }
}

Prometheus Endpoint

Với Prometheus registry, tất cả metrics có sẵn tại /actuator/prometheus ở định dạng Prometheus:

curl localhost:8080/actuator/prometheus

Output:

# HELP http_server_requests_seconds
# TYPE http_server_requests_seconds histogram
http_server_requests_seconds_bucket{method="GET",uri="/api/users",status="200",le="0.05"} 142
http_server_requests_seconds_bucket{method="GET",uri="/api/users",status="200",le="0.1"} 198
http_server_requests_seconds_bucket{method="GET",uri="/api/users",status="200",le="0.2"} 210
http_server_requests_seconds_count{method="GET",uri="/api/users",status="200"} 215
http_server_requests_seconds_sum{method="GET",uri="/api/users",status="200"} 18.432

4. Tối Ưu Database Query

Database access là bottleneck #1 trong hầu hết ứng dụng Spring Boot. Phần này tập trung vào tối ưu runtime ngoài những gì đã đề cập trong Advanced JPA Optimization.

Bật Slow Query Logging

PostgreSQL:

# application.yml
spring:
  jpa:
    properties:
      hibernate:
        generate_statistics: true
        session.events.log.LOG_QUERIES_SLOWER_THAN_MS: 100
  datasource:
    hikari:
      leak-detection-threshold: 30000  # 30 giây

Hibernate statistics logging:

logging:
  level:
    org.hibernate.stat: DEBUG
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE

Output hiển thị số query mỗi session — cách nhanh để phát hiện N+1:

Session Metrics {
    726313 nanoseconds spent acquiring 1 JDBC connections;
    326040 nanoseconds spent releasing 1 JDBC connections;
    3524968 nanoseconds spent preparing 12 JDBC statements;  ← 12 queries!
    42803543 nanoseconds spent executing 12 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
}

Tinh Chỉnh HikariCP Connection Pool

HikariCP là connection pool mặc định của Spring Boot. Pool cấu hình sai là kẻ giết hiệu suất thầm lặng.

spring:
  datasource:
    hikari:
      # Kích thước pool
      maximum-pool-size: 20       # Max connections
      minimum-idle: 5             # Min idle connections
 
      # Timeouts
      connection-timeout: 10000   # 10s để lấy connection
      idle-timeout: 300000        # 5 phút idle trước khi loại bỏ
      max-lifetime: 1800000       # 30 phút tuổi thọ tối đa
 
      # Validation
      validation-timeout: 5000    # 5s cho validation query
 
      # Phát hiện leak
      leak-detection-threshold: 60000  # Cảnh báo nếu giữ > 60s
 
      # Metrics
      register-mbeans: true

Công thức tính kích thước pool:

optimal_pool_size = (core_count * 2) + effective_spindle_count
 
Ví dụ: Server 4-core với SSD
optimal_pool_size = (4 * 2) + 1 = 9

Quan trọng: Nhiều connections hơn ≠ hiệu suất tốt hơn. Quá nhiều connections gây overhead chuyển đổi context trên database server. Bắt đầu nhỏ (10-20) và chỉ tăng nếu metrics cho thấy thời gian chờ connection.

Giám sát health connection pool:

@Scheduled(fixedRate = 30000) // Mỗi 30 giây
public void logPoolMetrics() {
    HikariPoolMXBean poolProxy = hikariDataSource
        .getHikariPoolMXBean();
 
    log.info("HikariCP - Active: {}, Idle: {}, " +
             "Waiting: {}, Total: {}",
        poolProxy.getActiveConnections(),
        poolProxy.getIdleConnections(),
        poolProxy.getThreadsAwaitingConnection(),
        poolProxy.getTotalConnections());
}

Batch Operations

Các câu lệnh INSERT/UPDATE riêng lẻ chậm. Batch operations giảm đáng kể round trips.

spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 50
          batch_versioned_data: true
        order_inserts: true
        order_updates: true
@Service
@Transactional
public class ProductImportService {
 
    @PersistenceContext
    private EntityManager entityManager;
 
    public int importProducts(
            List<ProductDto> products) {
        int batchSize = 50;
 
        for (int i = 0; i < products.size(); i++) {
            Product product = mapToEntity(products.get(i));
            entityManager.persist(product);
 
            // Flush và clear mỗi batch
            if (i > 0 && i % batchSize == 0) {
                entityManager.flush();
                entityManager.clear();
            }
        }
        entityManager.flush();
        entityManager.clear();
 
        return products.size();
    }
 
    private Product mapToEntity(ProductDto dto) {
        Product product = new Product();
        product.setName(dto.getName());
        product.setPrice(dto.getPrice());
        product.setCategory(dto.getCategory());
        return product;
    }
}

So sánh hiệu suất:

Phương Pháp10,000 RecordsRound Trips
Insert riêng lẻ~45 giây10,000
Batch size 50~3 giây200
Batch size 100~2.5 giây100
JDBC executeBatch()~1.5 giây100

Tối Ưu Read-Only Query

Đánh dấu read-only transactions để tránh overhead dirty checking:

@Service
public class ReportService {
 
    @Transactional(readOnly = true)
    public List<SalesReport> generateMonthlyReport(
            YearMonth month) {
        // Hibernate bỏ qua dirty checking cho read-only
        // transactions, tiết kiệm CPU time
        return reportRepository
            .findByMonth(month.getMonthValue(),
                         month.getYear());
    }
}
// Repository với query hints
public interface ProductRepository
        extends JpaRepository<Product, Long> {
 
    @QueryHints({
        @QueryHint(
            name = "org.hibernate.readOnly",
            value = "true"),
        @QueryHint(
            name = "org.hibernate.fetchSize",
            value = "50")
    })
    @Query("SELECT p FROM Product p WHERE p.active = true")
    List<Product> findAllActive();
}

5. Hiệu Suất HTTP & REST API

Tối ưu chu trình request-response giữa API và clients.

Nén Response (GZIP)

Bật nén GZIP để giảm kích thước payload response 60-80%:

server:
  compression:
    enabled: true
    min-response-size: 1024      # Nén responses > 1KB
    mime-types:
      - application/json
      - application/xml
      - text/html
      - text/xml
      - text/plain
      - application/javascript
      - text/css

Xác minh compression hoạt động:

# Không có compression
curl -s -o /dev/null -w "%{size_download}" \
  http://localhost:8080/api/products
# Output: 45230
 
# Có compression
curl -s -o /dev/null -w "%{size_download}" \
  -H "Accept-Encoding: gzip" \
  http://localhost:8080/api/products
# Output: 8124 (nhỏ hơn 82%)

HTTP Caching với ETags

ETags cho phép clients bỏ qua việc tải responses không thay đổi:

@Configuration
public class WebConfig implements WebMvcConfigurer {
 
    @Bean
    public FilterRegistrationBean<ShallowEtagHeaderFilter>
            shallowEtagHeaderFilter() {
        FilterRegistrationBean<ShallowEtagHeaderFilter>
            filterBean = new FilterRegistrationBean<>(
                new ShallowEtagHeaderFilter());
        filterBean.addUrlPatterns("/api/*");
        filterBean.setName("etagFilter");
        return filterBean;
    }
}

Custom Cache-Control headers cho endpoints cụ thể:

@RestController
@RequestMapping("/api/products")
public class ProductController {
 
    @GetMapping("/{id}")
    public ResponseEntity<Product> getProduct(
            @PathVariable Long id) {
        Product product = productService.findById(id);
 
        return ResponseEntity.ok()
            .cacheControl(CacheControl
                .maxAge(Duration.ofMinutes(10))
                .mustRevalidate())
            .eTag(String.valueOf(
                product.getUpdatedAt().hashCode()))
            .body(product);
    }
 
    @GetMapping("/categories")
    public ResponseEntity<List<String>> getCategories() {
        List<String> categories =
            productService.getAllCategories();
 
        // Categories hiếm khi thay đổi — cache 1 giờ
        return ResponseEntity.ok()
            .cacheControl(CacheControl
                .maxAge(Duration.ofHours(1))
                .cachePublic())
            .body(categories);
    }
}

Async Request Handling

Cho các operations I/O-bound, async handling giải phóng Tomcat threads:

@RestController
@RequestMapping("/api/reports")
public class ReportController {
 
    @Autowired
    private ReportService reportService;
 
    // Synchronous — block Tomcat thread
    @GetMapping("/sync/{id}")
    public Report getReportSync(@PathVariable Long id) {
        return reportService.generateReport(id);
    }
 
    // Async với CompletableFuture — giải phóng Tomcat thread
    @GetMapping("/async/{id}")
    public CompletableFuture<Report> getReportAsync(
            @PathVariable Long id) {
        return CompletableFuture.supplyAsync(
            () -> reportService.generateReport(id));
    }
 
    // Streaming large responses
    @GetMapping(value = "/stream",
        produces = MediaType.APPLICATION_NDJSON_VALUE)
    public Flux<Report> streamReports() {
        return reportService.streamAllReports();
    }
}

Cấu hình Tomcat thread pool:

server:
  tomcat:
    threads:
      max: 200          # Max worker threads
      min-spare: 20     # Min idle threads
    max-connections: 8192
    accept-count: 100   # Queue size khi tất cả threads bận
    connection-timeout: 20000

Tối Ưu JSON Serialization

Jackson mạnh mẽ nhưng có thể chậm với large objects. Tối ưu nó:

spring:
  jackson:
    serialization:
      WRITE_DATES_AS_TIMESTAMPS: false
    deserialization:
      FAIL_ON_UNKNOWN_PROPERTIES: false
    default-property-inclusion: non_null  # Bỏ qua null fields
@Configuration
public class JacksonConfig {
 
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer
            jacksonCustomizer() {
        return builder -> builder
            .featuresToDisable(
                SerializationFeature
                    .WRITE_DATES_AS_TIMESTAMPS,
                DeserializationFeature
                    .FAIL_ON_UNKNOWN_PROPERTIES)
            .featuresToEnable(
                DeserializationFeature
                    .READ_UNKNOWN_ENUM_VALUES_AS_NULL)
            .serializationInclusion(
                JsonInclude.Include.NON_NULL);
    }
}

Dùng @JsonView để chỉ trả về fields cần thiết:

public class Views {
    public static class Summary {}
    public static class Detail extends Summary {}
}
 
@Entity
public class Product {
 
    @JsonView(Views.Summary.class)
    private Long id;
 
    @JsonView(Views.Summary.class)
    private String name;
 
    @JsonView(Views.Summary.class)
    private BigDecimal price;
 
    @JsonView(Views.Detail.class)
    private String fullDescription;
 
    @JsonView(Views.Detail.class)
    private List<Review> reviews;
}
 
@RestController
@RequestMapping("/api/products")
public class ProductController {
 
    // Trả về chỉ id, name, price
    @GetMapping
    @JsonView(Views.Summary.class)
    public List<Product> listProducts() {
        return productService.findAll();
    }
 
    // Trả về tất cả fields bao gồm description, reviews
    @GetMapping("/{id}")
    @JsonView(Views.Detail.class)
    public Product getProduct(@PathVariable Long id) {
        return productService.findById(id);
    }
}

6. Tối Ưu Memory & GC Tuning

Vấn đề memory gây ra những lỗi nghiêm trọng nhất — OutOfMemoryError làm crash toàn bộ ứng dụng.

Phân Tích Heap Usage

Tạo heap dump:

# Từ ứng dụng đang chạy
jcmd <PID> GC.heap_dump /tmp/heapdump.hprof
 
# Tự động khi OutOfMemoryError (thêm vào JVM args)
java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/log/app/ \
     -jar myapp.jar

Phân tích với Eclipse MAT (Memory Analyzer Tool):

  1. Mở file .hprof trong MAT
  2. Chạy Leak Suspects Report — MAT tự động xác định leaks có thể
  3. Kiểm tra Dominator Tree — hiển thị objects nào giữ nhiều memory nhất
  4. Xem Histogram — số lượng mỗi loại object

Memory Leaks Thường Gặp trong Spring Boot

1. Unbounded caches:

// ❌ Memory leak — tăng mãi
private final Map<String, Object> cache =
    new HashMap<>();
 
public Object getData(String key) {
    return cache.computeIfAbsent(key,
        k -> expensiveQuery(k));
}
 
// ✅ Cache có giới hạn với eviction
private final Map<String, Object> cache =
    Collections.synchronizedMap(
        new LinkedHashMap<>(100, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(
                    Map.Entry<String, Object> eldest) {
                return size() > 1000;
            }
        }
    );
 
// ✅ Tốt hơn: Dùng Spring Cache với TTL (xem bài Redis)
@Cacheable(value = "data", key = "#key")
public Object getData(String key) {
    return expensiveQuery(key);
}

2. Resources không đóng:

// ❌ Stream không đóng — database connection bị leak
public List<User> getActiveUsers() {
    return userRepository.streamAll()
        .filter(User::isActive)
        .collect(Collectors.toList());
}
 
// ✅ Try-with-resources đóng stream
@Transactional(readOnly = true)
public List<User> getActiveUsers() {
    try (Stream<User> stream =
             userRepository.streamAll()) {
        return stream
            .filter(User::isActive)
            .collect(Collectors.toList());
    }
}

3. Event listener tích tụ:

// ❌ Listener đăng ký nhưng không bao giờ gỡ
@Component
public class NotificationListener {
 
    @Autowired
    private ApplicationEventPublisher publisher;
 
    @PostConstruct
    public void init() {
        // Mỗi hot-reload trong dev thêm một listener!
        publisher.publishEvent(
            new ListenerRegisteredEvent(this));
    }
}
 
// ✅ Dùng @EventListener (Spring quản lý lifecycle)
@Component
public class NotificationListener {
 
    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        // Spring xử lý registration/cleanup
        sendNotification(event.getOrder());
    }
}

Chọn GC Algorithm

GC AlgorithmTốt Nhất ChoFlagPause Target
G1GCMục đích chung (mặc định JDK 17)-XX:+UseG1GC200ms
ZGCUltra-low latency (<10ms pauses)-XX:+UseZGC<10ms
ShenandoahLow latency (OpenJDK)-XX:+UseShenandoahGC<10ms
Parallel GCMaximum throughput (batch processing)-XX:+UseParallelGCN/A

JVM flags khuyến nghị cho Spring Boot API điển hình:

java \
  -Xms512m \                        # Heap ban đầu
  -Xmx2g \                          # Heap tối đa
  -XX:+UseG1GC \                    # G1 garbage collector
  -XX:MaxGCPauseMillis=200 \        # GC pause mục tiêu
  -XX:+HeapDumpOnOutOfMemoryError \ # Dump khi OOM
  -XX:HeapDumpPath=/var/log/app/ \  # Vị trí heap dump
  -XX:+ExitOnOutOfMemoryError \     # Thoát khi OOM (để container restart)
  -Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=10m \
  -jar myapp.jar

Cho APIs low-latency (ZGC):

java \
  -Xms1g -Xmx4g \
  -XX:+UseZGC \
  -XX:+ZGenerational \           # Generational ZGC (JDK 21+)
  -Xlog:gc*:file=gc.log:time \
  -jar myapp.jar

Giám Sát GC qua Actuator

# GC pause time
curl localhost:8080/actuator/metrics/jvm.gc.pause
 
# Memory pool usage
curl localhost:8080/actuator/metrics/jvm.memory.used?tag=area:heap
 
# GC count
curl localhost:8080/actuator/metrics/jvm.gc.pause \
  | jq '.measurements[] | select(.statistic=="COUNT")'

Chỉ số health GC:

MetricHealthyWarningCritical
GC pause p99< 200ms200-500ms> 500ms
GC overhead< 5%5-15%> 15%
Heap sau GC< 70%70-85%> 85%

7. Tối Ưu Khởi Động Ứng Dụng

Khởi động chậm tăng thời gian deployment, feedback loops CI/CD, và cold-start latency trong môi trường serverless/container.

Đo Thời Gian Khởi Động

# application.yml
spring:
  main:
    log-startup-info: true
 
logging:
  level:
    org.springframework.boot.autoconfigure: DEBUG

Spring Boot 3.2+ startup report:

java -jar myapp.jar --spring.main.startup-report=true

Output hiển thị mỗi auto-configuration class và thời gian khởi tạo:

Spring Boot Startup Report
============================
Startup Time: 4.2 seconds (process: 5.1 seconds)
 
Auto-configuration (top 10 by duration):
  DataSourceAutoConfiguration: 892ms
  HibernateJpaAutoConfiguration: 1240ms
  WebMvcAutoConfiguration: 312ms
  SecurityAutoConfiguration: 289ms
  ...

Lazy Initialization

Chỉ khởi tạo beans khi sử dụng lần đầu — không phải khi startup:

spring:
  main:
    lazy-initialization: true  # Global lazy init

Lazy initialization có chọn lọc (khuyến nghị cho production):

// Eager (mặc định) — loaded khi startup
@Service
public class PaymentService {
    // Cần ngay cho health checks
}
 
// Lazy — loaded khi sử dụng lần đầu
@Service
@Lazy
public class ReportGenerationService {
    // Chỉ cần khi ai đó request report
}

Đánh đổi: Lazy init giảm thời gian startup nhưng chuyển chi phí khởi tạo sang request đầu tiên. Với APIs, điều này có nghĩa request đầu tiên tới lazy bean sẽ chậm hơn.

Loại Bỏ Auto-Configuration Không Cần Thiết

@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class,       // Nếu không dùng DB
    MongoAutoConfiguration.class,            // Nếu không dùng Mongo
    RedisAutoConfiguration.class,            // Nếu không dùng Redis
    MailSenderAutoConfiguration.class,       // Nếu không gửi email
    SecurityAutoConfiguration.class,         // Nếu không dùng security
    FlywayAutoConfiguration.class            // Nếu không dùng Flyway
})
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

Tìm auto-configurations nào đang active:

java -jar myapp.jar --debug 2>&1 | grep "matched"

Spring AOT & GraalVM Native Images

Để khởi động nhanh nhất có thể, compile sang native image:

<!-- pom.xml — Spring Boot 3.x -->
<plugin>
    <groupId>
        org.graalvm.buildtools
    </groupId>
    <artifactId>
        native-maven-plugin
    </artifactId>
</plugin>
# Build native image
mvn -Pnative native:compile
 
# Chạy — khởi động trong mili giây
./target/myapp

So sánh startup:

Chế ĐộThời Gian StartupMemoryTốt Nhất Cho
JVM (mặc định)3-8 giây200-500 MBLong-running services
JVM + lazy init1-4 giây200-500 MBLong-running services
Native image50-200 ms50-100 MBServerless, CLI tools

Đánh đổi: Native images khởi động nhanh hơn nhưng peak throughput chậm hơn JIT-compiled JVM. Cho các servers long-running xử lý sustained load, JVM thường thắng.

Tối Ưu Component Scanning

// ❌ Scan mọi thứ dưới com.example
@SpringBootApplication
@ComponentScan("com.example")
public class MyApplication {}
 
// ✅ Chỉ scan những gì cần
@SpringBootApplication
@ComponentScan(basePackages = {
    "com.example.myapp.controller",
    "com.example.myapp.service",
    "com.example.myapp.repository",
    "com.example.myapp.config"
})
public class MyApplication {}

8. Load Testing & Benchmarking

Tối ưu hiệu suất không có đo lường là đoán mò. Phần này đề cập micro-benchmarking với JMH và thiết lập baselines.

JMH (Java Microbenchmark Harness)

JMH là công cụ tiêu chuẩn để đo hiệu suất code nhỏ. Nó xử lý JVM warm-up, dead-code elimination, và constant folding — các vấn đề làm benchmarks thông thường không đáng tin.

<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.37</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.37</version>
    <scope>test</scope>
</dependency>

Ví dụ benchmark — so sánh các phương pháp serialization:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
public class SerializationBenchmark {
 
    private ObjectMapper objectMapper;
    private Product product;
 
    @Setup
    public void setup() {
        objectMapper = new ObjectMapper();
        objectMapper.registerModule(
            new JavaTimeModule());
 
        product = new Product();
        product.setId(1L);
        product.setName("Wireless Keyboard");
        product.setPrice(new BigDecimal("79.99"));
        product.setDescription(
            "Ergonomic wireless keyboard with " +
            "Bluetooth connectivity");
        product.setCategories(
            List.of("Electronics", "Accessories"));
        product.setCreatedAt(Instant.now());
    }
 
    @Benchmark
    public String jacksonSerialization()
            throws Exception {
        return objectMapper.writeValueAsString(product);
    }
 
    @Benchmark
    public byte[] jacksonSerializationBytes()
            throws Exception {
        return objectMapper.writeValueAsBytes(product);
    }
 
    @Benchmark
    public String gsonSerialization() {
        return new Gson().toJson(product);
    }
 
    public static void main(String[] args)
            throws Exception {
        Options opt = new OptionsBuilder()
            .include(
                SerializationBenchmark.class
                    .getSimpleName())
            .resultFormat(ResultFormatType.JSON)
            .result("benchmark-results.json")
            .build();
        new Runner(opt).run();
    }
}

Output:

Benchmark                                   Mode  Cnt   Score   Error  Units
SerializationBenchmark.jacksonBytes         avgt   10   1.245 ± 0.034  us/op
SerializationBenchmark.jacksonString        avgt   10   1.512 ± 0.041  us/op
SerializationBenchmark.gsonSerialization    avgt   10   3.847 ± 0.112  us/op

Thiết Lập Performance Baselines

Tạo baselines cho API endpoints quan trọng. Lưu chúng cùng code:

@SpringBootTest(
    webEnvironment = SpringBootTest.WebEnvironment
        .RANDOM_PORT)
public class PerformanceBaselineTest {
 
    @Autowired
    private TestRestTemplate restTemplate;
 
    @Test
    void getUser_shouldRespondWithin200ms() {
        long start = System.nanoTime();
 
        ResponseEntity<User> response = restTemplate
            .getForEntity("/api/users/1", User.class);
 
        long duration = (System.nanoTime() - start)
            / 1_000_000;
 
        assertThat(response.getStatusCode())
            .isEqualTo(HttpStatus.OK);
        assertThat(duration)
            .as("GET /api/users/1 should complete " +
                "within 200ms, took %dms", duration)
            .isLessThan(200);
    }
 
    @Test
    void listProducts_shouldRespondWithin500ms() {
        long start = System.nanoTime();
 
        ResponseEntity<List> response = restTemplate
            .getForEntity("/api/products?page=0&size=20",
                List.class);
 
        long duration = (System.nanoTime() - start)
            / 1_000_000;
 
        assertThat(response.getStatusCode())
            .isEqualTo(HttpStatus.OK);
        assertThat(duration)
            .as("GET /api/products should complete " +
                "within 500ms, took %dms", duration)
            .isLessThan(500);
    }
}

Tích Hợp với Gatling

Để load testing đầy đủ, dùng Gatling như đã đề cập trong Advanced Testing. Quy trình kết hợp:

  1. JMH tìm methods và algorithms chậm
  2. Tối ưu bottleneck đã xác định
  3. Gatling xác nhận fix dưới tải thực tế
  4. JFR đào sâu hơn nếu Gatling results vẫn không đạt SLA

9. Production Performance Checklist

Dùng checklist này khi deploy ứng dụng Spring Boot lên production.

JVM Configuration

# Template JVM flags cho production
JAVA_OPTS="\
  -server \
  -Xms${HEAP_SIZE} \
  -Xmx${HEAP_SIZE} \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/var/log/app/heapdump.hprof \
  -XX:+ExitOnOutOfMemoryError \
  -Xlog:gc*:file=/var/log/app/gc.log:time:filecount=5,filesize=10m \
  -Djava.security.egd=file:/dev/./urandom \
  -Dspring.profiles.active=prod \
"
 
java $JAVA_OPTS -jar myapp.jar

Dockerfile với Performance Flags

FROM eclipse-temurin:21-jre-alpine
 
WORKDIR /app
COPY target/*.jar app.jar
 
ENV JAVA_OPTS="\
  -server \
  -Xms512m -Xmx512m \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/tmp \
  -XX:+ExitOnOutOfMemoryError"
 
EXPOSE 8080
 
ENTRYPOINT ["sh", "-c", \
  "java $JAVA_OPTS -jar app.jar"]

Tham Chiếu Nhanh: Cài Đặt Hiệu Suất

Cài ĐặtDevelopmentProduction
Heap size-Xmx512m-Xms2g -Xmx2g
GCDefault (G1)G1 hoặc ZGC
Hibernate SQL loggingDEBUGWARN
Hibernate statstruefalse
HikariCP pool510-20
Tomcat threads200200-400
GZIP compressionTùy chọnBật
Spring lazy inittrueCó chọn lọc
Actuator exposureAllHealth + Prometheus
Log levelDEBUGINFO

Chiến Lược Giám Sát Hiệu Suất

Các alerts quan trọng cần cấu hình:

MetricWarningCritical
p99 response time> 500ms> 2s
Error rate (5xx)> 1%> 5%
CPU usage> 70%> 90%
Heap usage> 70%> 85%
HikariCP wait time> 1s> 5s
GC pause p99> 200ms> 500ms

Tóm Tắt và Điểm Chính

✅ Đo lường trước khi tối ưu — dùng JFR, VisualVM và Actuator để xác định bottlenecks thực sự, không phải đoán
✅ Database queries chiếm phần lớn thời gian phản hồi — tối ưu queries, tinh chỉnh HikariCP pool size và dùng batch operations
✅ Spring Boot Actuator + Micrometer cung cấp metrics sẵn sàng cho production — expose Prometheus endpoint và xây Grafana dashboards
✅ GZIP compression giảm payload 60-80% — bật cho JSON APIs với chi phí CPU tối thiểu
✅ HTTP caching với ETags và Cache-Control ngăn chuyển dữ liệu không cần thiết giữa client và server
✅ G1GC là lựa chọn an toàn mặc định, ZGC cho ultra-low latency — đặt -Xms bằng -Xmx để tránh heap resizing
✅ Lazy initialization tăng tốc startup nhưng chuyển chi phí sang request đầu tiên — dùng có chọn lọc trong production
✅ GraalVM native images khởi động trong mili giây nhưng đánh đổi peak throughput — lý tưởng cho serverless và CLI tools
✅ JMH benchmarks ngăn optimization regressions — theo dõi serialization, algorithm và query performance theo thời gian
✅ Luôn dump heap khi OOM (-XX:+HeapDumpOnOutOfMemoryError) — bạn không thể debug crash không tái tạo được


Tiếp Theo Là Gì?

Bây giờ bạn đã biết cách profile, đo lường và tối ưu hiệu suất, tiếp tục xây dựng ứng dụng production-ready:

Tiếp Tục Series Spring Boot

  • GraphQL với Spring for GraphQL: Xây dựng query APIs linh hoạt cho phép clients request đúng data cần — giảm over-fetching
  • Docker & Kubernetes Deployment: Containerize ứng dụng đã tối ưu và cấu hình resource limits, health probes và horizontal pod autoscaling
  • Monitoring với Actuator, Prometheus & Grafana: Xây dashboards real-time cho metrics bạn đã instrument trong hướng dẫn này

Bài Viết Spring Boot Liên Quan

Bài Viết Nền Tảng


Thuộc series Spring Boot Learning Roadmap

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.