Performance Optimization
Performance optimization is a systems engineering effort—it involves three dimensions: the JVM, the database, and the connection pool. Only by accurately pinpointing bottlenecks can we address the root causes.
1. What You'll Learn
- JVM Parameter Tuning: Heap Size / GC Algorithm Selection (G1GC / ZGC)
- HikariCP Connection Pool Parameter Optimization and Connection Leak Detection
- JPA Query Optimization: The N+1 Problem and Solutions
@EntityGraph/fetch join - Database Indexing Strategies and Slow Query Log Analysis
- Alice optimized the P99 latency of the OrderFlow order list API from 500 ms to 50 ms
2. A True Story of a Performance Engineer
(1) Pain Point: The API is as slow as a snail
Charlie reported that the P99 latency for the OrderFlow order list API was 500 ms, resulting in a very poor user experience. Alice's analysis revealed the following: 1) Frequent Full GC pauses in the JVM lasting 200 ms; 2) An N+1 problem occurring when JPA queries the order list (100 orders = 101 SQL queries); 3) The HikariCP connection pool size is insufficient, causing requests to time out while waiting for a connection.
(2) Solution Methods for System Optimization
Optimize Layer by Layer to Precisely Address Bottlenecks:
| Level | Bottleneck | Optimization Solution |
|---|---|---|
| JVM | Full GC Pause | G1GC + Heap Size Tuning |
| Connection Pool | Not Enough Connections | HikariCP Parameter Optimization |
| ORM | N+1 Queries | JOIN FETCH / EntityGraph |
| Database | Full Table Scan | Add Index |
(3) Revenue
After Alice optimized each component one by one, the P99 latency dropped from 500 ms to 50 ms, and throughput increased tenfold.
3. JVM Parameter Tuning
(1) Comparison of GC Algorithms
| GC Algorithm | Maximum Pause Time | Suitable Scenarios | JVM Parameters |
|---|---|---|---|
| G1GC | 10-200 ms | General (JDK 17 default) | -XX:+UseG1GC |
| ZGC | < 1 ms | Low Latency (JDK 17+) | -XX:+UseZGC |
| SerialGC | Long | Small Heap (< 200MB) | -XX:+UseSerialGC |
| ParallelGC | Medium | Throughput-First | -XX:+UseParallelGC |
graph LR
A["JVM Tuning"] --> B["Heap Size<br/>-Xms = -Xmx"]
A --> C["GC Algorithm<br/>G1 / ZGC"]
A --> D["GC Logging<br/>-Xlog:gc*"]
B --> B1["Container: MaxRAMPercentage"]
C --> C1["Default: G1GC"]
C --> C2["Low latency: ZGC"]
(1) ▶ Example: JVM Configuration in a Docker Environment
ENTRYPOINT ["java", \
"-XX:+UseG1GC", \
"-XX:MaxRAMPercentage=75.0", \
"-XX:InitialRAMPercentage=50.0", \
"-XX:+UseStringDeduplication", \
"-Xlog:gc*:file=/app/logs/gc.log:time,uptime,level,tags", \
"-jar", "app.jar"]
Output:
// Execution Successful
| Parameter | Meaning | Recommended Value |
|---|---|---|
MaxRAMPercentage |
Maximum heap as a percentage of container memory | 75.0 |
InitialRAMPercentage |
Percentage of container memory occupied by the initial heap | 50.0 |
+UseG1GC |
Use the G1 garbage collector | Enabled by default |
+UseStringDeduplication |
Removing Duplicates from Strings Saves Memory | Recommended |
MaxGCPauseMillis |
GC Target Pause Time | 200 (G1) / None (ZGC) |
(2) ▶ Example: ZGC Low-Latency Configuration
java -XX:+UseZGC \
-XX:MaxRAMPercentage=75.0 \
-XX:+ZGenerational \
-Xlog:gc*:file=gc.log \
-jar app.jar
Output:
# Command executed successfully
4. HikariCP Connection Pool Optimization
(1) Connection Pool Parameters
| Parameter | Default Value | Description | Recommended Value |
|---|---|---|---|
maximumPoolSize |
10 | Maximum number of connections | Number of CPU cores x 2 + number of disks |
minimumIdle |
= maxPool | Minimum idle connections | = maxPoolSize |
connectionTimeout |
30,000 ms | Connection timeout | 3,000 ms |
idleTimeout |
600,000 ms | Idle connection timeout | 600,000 ms |
maxLifetime |
1,800,000 ms | Maximum connection lifetime | 1,800,000 ms |
leakDetectionThreshold |
0 (Disabled) | Connection Leak Detection | 60,000 ms |
(1) ▶ Example: HikariCP Production Configuration
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 20
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000
pool-name: OrderFlowHikariCP
Output:
The configuration has taken effect.
(2) ▶ Example: Connection Leak Detection
# Enable leak detection (log warning if connection held > 60s)
spring:
datasource:
hikari:
leak-detection-threshold: 60000
WARN - Connection leak detection triggered for {conn-12345}
Stack trace of the call site that acquired the connection:
at com.orderflow.service.OrderService.getOrder(OrderService.java:45)
...
5. JPA Query Optimization
(1) The N+1 Problem
(1) ▶ Example: Demonstration of the N+1 Problem
// BAD: N+1 problem
// 1 SQL to fetch 100 orders + 100 SQLs to fetch items for each order = 101 SQLs!
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
order.getItems().size(); // Triggers lazy load for each order
}
Output:
// Execution Successful
(2) ▶ Example: Using JOIN FETCH to Solve the N+1 Problem
// GOOD: Single query with JOIN FETCH
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items i JOIN FETCH i.product WHERE o.status = :status")
List<Order> findByStatusWithItems(@Param("status") String status);
Output:
// Execution Successful
(3) ▶ Example: Declarative Loading with @EntityGraph
@Entity
@NamedEntityGraph(
name = "Order.withItemsAndProduct",
attributeNodes = {
@NamedAttributeNode("items"),
@NamedAttributeNode(value = "items", subgraph = "item-product")
},
subgraphs = {
@NamedSubgraph(name = "item-product", attributeNodes = @NamedAttributeNode("product"))
}
)
public class Order { /* ... */ }
// Usage in Repository
@EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
List<Order> findByStatus(String status);
Output:
// Execution Successful
| Solution | Number of SQL Statements | Applicable Scenarios | Maintainability |
|---|---|---|---|
| Default Lazy Loading | N+1 | Single-Object Query | High |
| JOIN FETCH | 1 | Specific Query | Medium |
| @EntityGraph | 1 | Reusable load plan | High |
| @BatchSize | N/batchSize | Batch Loading | High |
6. Database Index Optimization
(1) Indexing Strategy
(1) ▶ Example: Adding a Database Index
-- Index for order status queries
CREATE INDEX idx_order_status ON orders(status);
-- Composite index for common query pattern
CREATE INDEX idx_order_status_created ON orders(status, created_at DESC);
-- Index for product search
CREATE INDEX idx_product_name ON products(name);
-- Index for order item product lookup
CREATE INDEX idx_order_item_product ON order_items(product_id);
Output:
CREATE TABLE
| Index Type | Use Case | Example |
|---|---|---|
| Single-column index | Single-condition query | WHERE status = 'PENDING' |
| Composite Index | Multi-Condition Query | WHERE status = ? AND created_at > ? |
| Unique Index | Unique Constraint | UNIQUE(sku) |
| Covering Index | Index contains all query columns | SELECT status FROM orders WHERE id = ? |
(2) ▶ Example: Slow Query Analysis
-- MySQL slow query log configuration
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- Log queries > 1 second
SET GLOBAL log_queries_not_using_indexes = ON;
-- Analyze query execution plan
EXPLAIN SELECT o.*, i.*
FROM orders o
JOIN order_items i ON o.id = i.order_id
WHERE o.status = 'PENDING'
AND o.created_at > '2024-01-01'
ORDER BY o.created_at DESC
LIMIT 20;
Output:
CREATE TABLE
| EXPLAIN Field | Meaning | Key Points |
|---|---|---|
type |
Access Type | ALL (Full Table Scan) Needs Optimization |
key |
Index used | NULL indicates that no index was used |
rows |
Number of Scanned Lines | The lower, the better |
Extra |
Additional Information | Using filesort requires optimization |
7. Comprehensive Example: OrderFlow P99 Reduced from 500 ms to 50 ms
# application-prod.yml (optimized)
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 20
connection-timeout: 3000
leak-detection-threshold: 60000
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
default_batch_fetch_size: 100
jdbc:
batch_size: 50
order_inserts: true
order_updates: true
# JVM parameters (Docker ENTRYPOINT)
java -XX:+UseG1GC \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=50.0 \
-XX:+UseStringDeduplication \
-XX:MaxGCPauseMillis=100 \
-Xlog:gc*:file=/app/logs/gc.log:time,uptime,level,tags \
-jar app.jar
// Optimized OrderRepository with EntityGraph and batch fetching
public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
@Query("SELECT o FROM Order o WHERE o.status = :status ORDER BY o.createdAt DESC")
Page<Order> findByStatusPaged(@Param("status") String status, Pageable pageable);
@EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
Optional<Order> findById(Long id);
}
// Hibernate batch size configuration eliminates N+1 for collections
// application.yml: hibernate.default_batch_fetch_size=100
| Before Optimization | After Optimization | Improvement Factor |
|---|---|---|
| P99: 500 ms | P99: 50 ms | 10x |
| SQL/Request: 101 | SQL/Request: 1 | 100x |
| GC pause: 200 ms | GC pause: 50 ms | 4x |
| Connection wait: 100 ms | Connection wait: 2 ms | 50x |
❓ FAQ
batch_size do?hibernate.jdbc.batch_size=50 is set, INSERT/UPDATE statements are executed in batches (50 records per batch), reducing the number of round trips to the database. This works even better when used in conjunction with order_inserts=true./metrics to check HTTP request durations; 2) Analyze GC logs to identify GC pauses; 3) HikariCP metrics to check the connection pool status; 4) MySQL slow query log to identify slow queries; 5) APM tools (SkyWalking/Zipkin) for end-to-end tracing.📖 Summary
- JVM Tuning: G1GC is sufficient by default; ZGC is designed for ultra-low latency; MaxRAMPercentage controls container memory
- HikariCP: Connection pool size = number of CPU cores x 2 + number of disks; enable leakDetection to troubleshoot memory leaks
- The N+1 Problem: JOIN FETCH in a single SQL statement, declarative loading with @EntityGraph, and batch loading with @BatchSize
- Database indexes: Add indexes for frequently used query conditions; use EXPLAIN to analyze the execution plan
- Batch processing: hibernate.jdbc.batch_size + order_inserts reduces database round trips
- Systematic Optimization: Identify bottlenecks → Quantify baselines → Optimize layer by layer → Verify results
📝 Exercises
-
Basic Exercise (Difficulty: ⭐): Configure the JVM G1GC parameters and HikariCP connection pool parameters for OrderFlow, and enable GC logging and connection leak detection.
-
Advanced Problem (Difficulty ⭐⭐): Use
@EntityGraphandJOIN FETCHto solve the N+1 problem in the order list query, and compare the number of SQL statements and response times before and after optimization. -
Challenge (Difficulty: ⭐⭐⭐): Use JMeter to perform load testing on the OrderFlow order list API, establish a performance baseline, and gradually optimize it (JVM + connection pool + N+1 + indexes). Record the changes in P50 and P99 for each optimization step, and ultimately optimize P99 to below 50 ms.



