Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog
in java

java advanced performance techniques

Tung Bui Thanh (TungDaDev)46 mins read

Java advance perf.webp

Bài này gom các kỹ thuật tối ưu cấp hệ thống mà senior engineer và người viết framework hay dùng. Không phải thứ ngày nào cũng cần đến, cứ để dành cho lúc profiler chỉ ra đúng bottleneck.

Premature optimization is the root of all evil — Donald Knuth

Nhưng khi bạn đã đo, đã chứng minh được vấn đề và cần thêm tốc độ, thì đây là cách làm.

# branchless programming: khi cpu đoán sai

# tại sao branch prediction quan trọng

CPU hiện đại chạy như dây chuyền nhà máy (pipeline) với khoảng 14-20 stage. Để dây chuyền lúc nào cũng có việc, CPU dùng Branch Predictor đoán trước kết quả if/else rồi chạy trước (speculative execution). Đoán đúng thì gần như không tốn gì. Đoán sai (branch misprediction) thì CPU phải xả hết những gì đã chạy trước trong pipeline, mất cỡ 12-25 clock cycles, tức là hàng chục lệnh bị bỏ đi.

Với data có quy luật rõ (đã sort, hoặc hầu như luôn true/false), predictor đoán đúng trên 95%. Còn với data ngẫu nhiên (giá trị hash, network packet, game state) mà điều kiện chia đôi kiểu 50/50 thì tỉ lệ đoán sai có thể lên tới 30-50%.

# branchless trong java: thực tế

Java không cho viết inline assembly, nhưng JIT compiler có thể biến một số pattern thành conditional move (CMOV), loại instruction chọn giá trị mà không rẽ nhánh.

// Branch-heavy: mỗi iteration = 1 branch prediction
// Với random data: ~50% misprediction
public static int conditionalSum(int[] data) {
   int sum = 0;
   for (int val : data) {
       if (val >= 128) {  // Branch: predicted or mispredicted
           sum += val;
       }
   }
   return sum;
}
 
// ✅ Branchless: arithmetic thay thế branch
// JIT compiler thường convert sang CMOV instruction
public static int branchlessSum(int[] data) {
   int sum = 0;
   for (int val : data) {
       // (val - 128) >> 31 = 0 khi val >= 128, = -1 (all 1s) khi val < 128
       // ~((val - 128) >> 31) = -1 khi val >= 128, = 0 khi val < 128
       // val & mask = val khi mask = -1, = 0 khi mask = 0
       int mask = ~((val - 128) >> 31);
       sum += val & mask;
   }
   return sum;
}

# math-based branchless patterns

// Branching min/max
public static int branchingMax(int a, int b) {
   return a > b ? a : b;  // Branch — nhưng JIT thường optimize
}
 
// ✅ Branchless max (khi JIT KHÔNG optimize, hoặc trong interpreted mode)
public static int branchlessMax(int a, int b) {
   // Khi a > b: a - b > 0, sign bit = 0, >> 31 = 0
   // Khi a < b: a - b < 0, sign bit = 1, >> 31 = -1 (all 1s)
   int diff = a - b;
   int sign = diff >> 31;          // 0 if a >= b, -1 if a < b
   return a - (diff & sign);       // a - 0 = a, hoặc a - (a-b) = b
   // CẢNH BÁO: overflow khi |a - b| > Integer.MAX_VALUE
}
 
// ✅ Branchless absolute value
public static int branchlessAbs(int n) {
   int mask = n >> 31;        // 0 nếu positive, -1 nếu negative
   return (n ^ mask) - mask;  // flip bits + add 1 cho negative (two's complement)
}
 
// ✅ Branchless clamp (giới hạn value trong [min, max])
public static int branchlessClamp(int val, int min, int max) {
   // Math.max(min, Math.min(val, max)) — nhưng branchless
   int clamped = val - max;
   clamped &= clamped >> 31;   // 0 nếu val > max, negative value nếu val <= max
   clamped += max;              // clamp trên
 
   int lower = clamped - min;
   lower &= ~(lower >> 31);    // 0 nếu clamped < min
   return lower + min;          // clamp dưới
}

# branchless lookup (boolean to int conversion)

// Java boolean comparison → thường tạo branch
// ✅ Convert boolean condition thành 0/1 integer arithmetic
 
// Tính phí giao dịch: VIP = 0%, Normal = 2%
// Don't
double fee = isVip ? 0.0 : amount * 0.02;
 
// Do - Branchless (hữu ích khi gọi triệu lần với random isVip)
// Boolean.compare(isVip, false) = 1 khi true, 0 khi false
int notVip = 1 - Boolean.compare(isVip, false);  // 0 khi VIP, 1 khi không
double fee = amount * 0.02 * notVip;

# khi nào branchless thực sự giúp ích trong java?

ScenarioBranch OK?Branchless?
Sorted/predictable data✅ Branch predictor đúng >95%Không cần
Random data, hot loop (>1M iterations)❌ Misprediction penalty cao✅ Xem xét
Business logic (if/else rõ ràng)✅ Readability quan trọng hơnKhông bao giờ
Inner loop của game engine/signal processing❌ Mỗi nanosecond count✅ Benchmark
JIT-compiled code (production, warm JVM)✅ JIT thường tự optimizeProfile trước

Lưu ý: JIT của HotSpot khá thông minh. Math.min/max là intrinsic, còn ternary và if/else đơn giản thường được C2 đổi sang CMOV khi nó thấy đáng (C2 dựa vào profile nhánh để quyết định, nhánh lệch hẳn về một phía thì nó giữ branch). Trước khi viết branchless bằng tay, benchmark với JMH để chắc rằng JIT chưa làm hộ bạn.

# autoboxing: thuế ẩn trong java enterprise

# chi phí thực sự của autoboxing

Mỗi lần Java tự đổi int → Integer (autoboxing) qua Integer.valueOf, một object mới được tạo trên heap (trừ dải -128..127 đã được cache sẵn). Ở hệ thống throughput cao, đây là khoản "thuế vô hình" gặm dần performance: thêm allocation, thêm GC, thêm một lần dereference mỗi khi đọc giá trị. JIT đôi khi xoá được boxing nhờ escape analysis, nhưng không nên trông vào đó.

// Memory layout comparison:
// (HotSpot 64-bit, compressed oops/class pointers bật mặc định khi heap < 32GB)
// int:     4 bytes (stack hoặc inline trong array)
// Integer: 12 bytes object header + 4 bytes value = 16 bytes (heap)
// Long:    12 bytes header + 8 bytes value + 4 bytes padding = 24 bytes
// → cộng thêm 4 bytes reference trỏ tới object, Integer tốn ~5x so với int
// (JDK 27 bật compact object headers mặc định, JEP 534: header còn 8 bytes,
//  Integer/Long đều còn 16 bytes, nhưng vẫn gấp nhiều lần primitive)
 
// Autoboxing trap #1: Map với primitive keys
Map<Long, String> cache = new HashMap<>();
for (long id = 0; id < 1_000_000; id++) {
   cache.put(id, "value");  // id auto-boxed → 1M Long objects → ~24MB wasted
}
 
// Autoboxing trap #2: Generic methods
public static <T extends Comparable<T>> T max(T a, T b) {
   return a.compareTo(b) >= 0 ? a : b;
}
int result = max(x, y);  // x, y boxed → Integer, result unboxed → int
// 2 boxing + 1 unboxing PER CALL
 
// Autoboxing trap #3: Stream operations
long sum = longList.stream()
   .filter(x -> x > 100)     // x unboxed for comparison
   .map(x -> x * 2)          // x unboxed, result RE-BOXED
   .reduce(0L, Long::sum);   // unbox both, sum, RE-BOX accumulator
// MỖI stage map = unbox + rebox
 
// Fix: dùng primitive streams
long sum = longList.stream()
   .mapToLong(Long::longValue)  // unbox ONCE
   .filter(x -> x > 100)        // primitive operations from here
   .map(x -> x * 2)
   .sum();                       // primitive sum, no boxing

# phát hiện autoboxing trong production

// JFR (Java Flight Recorder) — monitor allocations
// jcmd <pid> JFR.start name=boxing duration=60s filename=boxing.jfr
 
// JMH benchmark with -prof gc
// @Benchmark reports gc.alloc.rate — nếu cao bất thường → boxing
 
// IntelliJ inspection: "Auto-boxing" → highlight tất cả boxing sites
// IDE settings: Inspections → Java → Performance → "Auto-boxing"

# giải pháp cấp kiến trúc

// ✅ Strategy 1: Primitive-specialized collections
// Eclipse Collections
import org.eclipse.collections.impl.map.mutable.primitive.LongObjectHashMap;
LongObjectHashMap<String> cache = new LongObjectHashMap<>();
cache.put(42L, "value");  // NO boxing — stores raw long
 
// ✅ Strategy 2: Object ngắn hạn + escape analysis của JIT
// Object (record hay class thường) không "thoát" ra khỏi method sau khi inline
// có thể được C2 scalar-replace → không cấp phát trên heap.
// Record KHÔNG có ưu đãi riêng; chuyện này cũng không đảm bảo, phải đo.
record PricePoint(long timestamp, double price) {}
 
// ✅ Strategy 3: Redesign API to accept primitives
// ❌
public void processOrder(Long orderId, Integer quantity, Double price) { }
// ✅
public void processOrder(long orderId, int quantity, double price) { }
// Caller không bị boxing penalty
 
// ✅ Strategy 4: Primitive OptionalLong/OptionalInt thay vì Optional<Long>
// ❌
Optional<Long> findPrice(String product);  // Long boxing
// ✅
OptionalLong findPrice(String product);  // No boxing

# enumset & bit masking: power tool cho feature flags

# enumset internals: array of bits

EnumSet là bitmask dựng sẵn của Java cho enum. Enum có từ 64 hằng trở xuống thì bên trong nó dùng RegularEnumSet, dữ liệu chỉ là 1 long (64 bit). Các thao tác add, remove, contains, union, intersection đều là phép bitwise, O(1). Enum trên 64 hằng thì chuyển sang JumboEnumSet dùng mảng long[].

public enum Permission {
   VIEW_DASHBOARD,    // bit 0
   EDIT_FORM,         // bit 1
   DELETE_FORM,       // bit 2
   MANAGE_USERS,      // bit 3
   DEPLOY_PROCESS,    // bit 4
   VIEW_AUDIT_LOG,    // bit 5
   EXPORT_DATA,       // bit 6
   MANAGE_ROLES,      // bit 7
   SYSTEM_CONFIG,     // bit 8
   VIEW_REPORTS       // bit 9
}
 
// Role definitions (lưu ý: EnumSet là mutable, đừng để code khác sửa trực tiếp các hằng này;
// cần trả ra ngoài thì dùng EnumSet.copyOf(...) hoặc Collections.unmodifiableSet(...))
public static final EnumSet<Permission> VIEWER = EnumSet.of(
   Permission.VIEW_DASHBOARD, Permission.VIEW_REPORTS
);  // Internal: 0000_0010_0000_0001
 
public static final EnumSet<Permission> EDITOR = EnumSet.of(
   Permission.VIEW_DASHBOARD, Permission.EDIT_FORM,
   Permission.VIEW_REPORTS, Permission.EXPORT_DATA
);  // Internal: 0000_0010_0100_0011
 
public static final EnumSet<Permission> ADMIN = EnumSet.allOf(Permission.class);
// Internal: 0000_0011_1111_1111
 
// Check permission — O(1) bitwise AND
public boolean hasPermission(EnumSet<Permission> userPerms, Permission required) {
   return userPerms.contains(required);  // Internally: (bits & (1L << ordinal)) != 0
}
 
// Check ALL required permissions — O(1) containsAll
public boolean hasAllPermissions(EnumSet<Permission> userPerms, EnumSet<Permission> required) {
   return userPerms.containsAll(required);  // Internally: (bits & required) == required
}
 
// Combine permissions — O(1) OR
public EnumSet<Permission> mergeRoles(EnumSet<Permission> role1, EnumSet<Permission> role2) {
   EnumSet<Permission> combined = EnumSet.copyOf(role1);
   combined.addAll(role2);  // Internally: bits |= other.bits
   return combined;
}

# persist enumset as bitmask in database

// Store permissions as BIGINT in PostgreSQL (single column, no join table)
@Entity
public class UserRole {
   @Id
   private UUID id;
   private String name;
 
   // Store as long bitmask
   @Column(name = "permissions_mask")
   private long permissionsMask;
 
   // Convert to/from EnumSet
   @Transient
   public EnumSet<Permission> getPermissions() {
       EnumSet<Permission> set = EnumSet.noneOf(Permission.class);
       for (Permission p : Permission.values()) {
           if ((permissionsMask & (1L << p.ordinal())) != 0) {
               set.add(p);
           }
       }
       return set;
   }
 
   public void setPermissions(EnumSet<Permission> permissions) {
       this.permissionsMask = 0L;
       for (Permission p : permissions) {
           this.permissionsMask |= (1L << p.ordinal());
       }
   }
}
 
// Query: find roles with DEPLOY_PROCESS permission
// SQL: WHERE (permissions_mask & 16) <> 0  (bit 4 = 2^4 = 16)
// JPQL không có toán tử bitwise &, nên dùng native query
@Query(value = "SELECT * FROM user_role WHERE (permissions_mask & :mask) = :mask",
       nativeQuery = true)
List<UserRole> findByPermission(@Param("mask") long mask);

# so sánh performance vs alternatives

ApproachMemory/UserContains checkAdd/RemoveDB storage
EnumSet<Permission> (10 perms)8 bytes data (object ~32 bytes)O(1) bitwiseO(1) bitwise1 BIGINT column
Set<String> (10 perms)~400 bytes (HashSet + String objects)O(1) hash + equalsO(1) amortizedJOIN table hoặc JSON
List<Permission> (10 perms)~200 bytes (ArrayList + enum refs)O(n) linear scanO(n) shiftJOIN table
Boolean fields (10 perms)10 bytes (padded)O(1) field accessO(1) field set10 columns

EnumSet thắng ở cả bộ nhớ, tốc độ lẫn độ gọn khi lưu DB. Cái giá là dữ liệu phụ thuộc vào ordinal(): chèn thêm một hằng vào giữa enum, hay đổi thứ tự, là các bit đã lưu trong DB bị lệch nghĩa hết. Muốn an toàn hơn thì gán số bit cố định cho từng hằng (field bit trong enum) thay vì dựa vào ordinal, và chỉ thêm hằng mới vào cuối.

# virtual threads: giải phóng throughput i/o-bound (java 21+)

# vấn đề với platform threads

Mỗi platform thread là 1 OS thread, mặc định reserve khoảng 1MB stack trên Linux 64-bit (phần lớn là virtual memory, RAM thật được commit dần khi stack lớn lên). Thứ đắt hơn là chi phí tạo thread, context switch và giới hạn số thread của OS. Khi thread chờ I/O (gọi DB, gọi HTTP), OS thread đó nằm im mà không ai dùng được.

Hệ quả: Tomcat mặc định có tối đa 200 thread, nên server chỉ xử lý được 200 request đồng thời. Request thứ 201 phải xếp hàng.

# virtual threads: lightweight, millions possible

Virtual thread (chính thức từ Java 21, JEP 444) là thread chế độ user do JVM quản lý. Khi virtual thread chờ I/O, JVM unmount nó khỏi carrier thread (một platform thread) rồi mount virtual thread khác lên. Nhờ vậy carrier thread hầu như không phải ngồi chờ, trừ khi virtual thread bị pin (xem phần dưới).

// Platform thread: 1 request = 1 OS thread (giới hạn ~200-500 threads)
// Virtual thread: 1 request = 1 virtual thread (có thể 1M+ concurrent)
 
// ✅ Spring Boot 3.2+ — bật virtual threads (1 dòng config)
// application.yml:
// spring:
//   threads:
//     virtual:
//       enabled: true
// → Tomcat dùng virtual threads cho mỗi request
// → @Async tasks chạy trên virtual threads
// → Scheduler tasks dùng virtual threads
 
// ✅ Tự tạo virtual thread executor
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
 
// Xử lý 10,000 concurrent HTTP calls — KHÔNG cần reactive/WebFlux
List<Future<String>> futures = new ArrayList<>();
for (String url : urls) {  // 10,000 URLs
   futures.add(executor.submit(() -> {
       // Mỗi call block 200ms trung bình
       // Platform threads: 200 threads / 200ms = tối đa 1000 req/s
       // Virtual threads: 10,000 concurrent / 200ms = tối đa 50,000 req/s
       // (trần lý thuyết theo Little's Law; thực tế còn bị giới hạn bởi
       //  downstream, connection pool, CPU...)
       return httpClient.send(
           HttpRequest.newBuilder().uri(URI.create(url)).build(),
           HttpResponse.BodyHandlers.ofString()
       ).body();
   }));
}
 
// Structured Concurrency (Java 21 preview, cần --enable-preview)
// Lưu ý: API này vẫn đang preview và đã đổi nhiều. Từ JDK 25 (JEP 505),
// ShutdownOnFailure bị bỏ, thay bằng StructuredTaskScope.open(Joiner...).
// Code dưới đây viết theo API của JDK 21.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
   Subtask<User> userTask = scope.fork(() -> userService.getUser(id));
   Subtask<List<Order>> ordersTask = scope.fork(() -> orderService.getOrders(id));
   Subtask<Double> balanceTask = scope.fork(() -> accountService.getBalance(id));
 
   scope.join();           // Wait for all
   scope.throwIfFailed();  // Propagate first failure
 
   // All completed successfully — combine results
   return new UserProfile(userTask.get(), ordersTask.get(), balanceTask.get());
   // 3 I/O calls chạy song song, tổng latency = max(3 calls) thay vì sum(3 calls)
}

# virtual threads: khi nào dùng, khi nào không

// ✅ DÙNG cho I/O-bound workloads:
// - REST API calls to downstream services
// - Database queries (JDBC)
// - File I/O
// - Message queue operations (RabbitMQ consume/produce)
// - Anything that spends most time WAITING
 
// ❌ KHÔNG dùng cho CPU-bound workloads:
// - Heavy computation (encryption, compression, ML inference)
// - Image/video processing
// - Complex mathematical calculations
// Lý do: Virtual threads không tăng CPU throughput — vẫn limited by cores
// Dùng ForkJoinPool hoặc parallel streams cho CPU-bound
 
// ❌ JDK 21-23: tránh synchronized bao quanh I/O (pinning problem)
// synchronized block PIN virtual thread vào carrier, carrier bị block theo
// (Từ JDK 24, JEP 491 đã gỡ pinning cho synchronized; vẫn còn pin khi
//  gọi native code, hoặc khi block trong class initializer)
synchronized (lock) {
   database.query(...);  // ❌ Virtual thread PINNED → carrier thread blocked
}
 
// ✅ Trên JDK 21-23: thay bằng ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
   database.query(...);  // ✅ Virtual thread can unmount during I/O
} finally {
   lock.unlock();
}

# đo lường hiệu quả virtual threads

// JFR events cho virtual threads:
// jdk.VirtualThreadStart, jdk.VirtualThreadEnd
// jdk.VirtualThreadPinned — QUAN TRỌNG: detect pinning issues
 
// System property để detect pinning at runtime (chỉ JDK 21-23):
// -Djdk.tracePinnedThreads=short
// Output warning khi virtual thread bị pinned
// JDK 24+ đã bỏ property này (JEP 491), dùng event JFR ở trên
 
// Ước tính lý thuyết theo Little's Law, KHÔNG phải benchmark đo thật
// (REST API, 1000 concurrent users, 100ms DB latency):
// Platform threads (200 pool): throughput = 2,000 req/s, p99 = 500ms (queuing)
// Virtual threads: throughput = 9,500 req/s, p99 = 110ms (no queuing)

# hibernate/jpa performance traps: thuế "tiện lợi"

# n+1 query problem: kẻ giết performance #1

// ❌ N+1: Load 100 orders → triggers 100 separate queries for items
@Entity
public class Order {
   @OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
   private List<OrderItem> items;  // LAZY = good default, BUT...
}
 
// Khi access items trong loop:
List<Order> orders = orderRepository.findAll();  // 1 query: SELECT * FROM orders
for (Order order : orders) {
   order.getItems().size();  // 100 queries: SELECT * FROM order_items WHERE order_id = ?
}
// Tổng: 101 queries thay vì 1
 
// ✅ Fix 1: JOIN FETCH (eager load in JPQL)
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
List<Order> findByStatusWithItems(@Param("status") OrderStatus status);
// 1 query: SELECT ... FROM orders JOIN order_items ...
 
// ✅ Fix 2: @EntityGraph (declarative fetch plan)
@EntityGraph(attributePaths = {"items", "items.product"})
List<Order> findByStatus(OrderStatus status);
// 1 query with LEFT JOIN
 
// ✅ Fix 3: @BatchSize (batch lazy loading — best for collections)
@Entity
public class Order {
   @OneToMany(mappedBy = "order")
   @BatchSize(size = 50)  // Load items for 50 orders at once
   private List<OrderItem> items;
}
// 100 orders → 1 query orders + 2 query items (mỗi batch 50) = 3 queries thay vì 101

# dirty checking overhead: hibernate luôn theo dõi

// Hibernate giữ snapshot của mọi entity managed khi load → so sánh khi flush
// 10,000 entities loaded = 10,000 snapshots in memory + O(n) comparison at flush
 
// ❌ Load nhiều entity chỉ để đọc
@Transactional
public List<ProductDTO> getAllProducts() {
   List<Product> products = productRepository.findAll();  // 10K entities tracked
   return products.stream().map(this::toDTO).toList();
   // Flush time: Hibernate so sánh 10K snapshots (dù không thay đổi gì)
}
 
// ✅ Fix 1: Read-only transaction (skip dirty checking)
@Transactional(readOnly = true)
public List<ProductDTO> getAllProducts() {
   // Spring set FlushMode.MANUAL + session read-only:
   // entity load ra không giữ snapshot, không dirty check
   List<Product> products = productRepository.findAll();
   return products.stream().map(this::toDTO).toList();
}
 
// ✅ Fix 2: Projection/DTO query (bypass entity tracking entirely)
@Query("SELECT new com.tungdadev.csp.product.dto.ProductSummaryDTO(" +
      "p.id, p.name, p.price) FROM Product p WHERE p.status = 'ACTIVE'")
List<ProductSummaryDTO> findActiveSummaries();
// KHÔNG tạo entity, KHÔNG snapshot, KHÔNG dirty check — pure DTO
 
// ✅ Fix 3: StatelessSession cho bulk reads
Session session = entityManager.unwrap(Session.class);
try (StatelessSession stateless = session.getSessionFactory().openStatelessSession()) {
   // ... query / scroll trên stateless ...
}
// StatelessSession: no first-level cache, no dirty checking, no cascades
// Nhớ đóng session (try-with-resources) để trả connection
// Perfect for batch processing millions of rows

# open session in view: anti-pattern ẩn

// spring.jpa.open-in-view=true (DEFAULT in Spring Boot!)
// → Hibernate Session mở suốt HTTP request lifecycle
// → Lazy loading works in Controller/View layer (tiện)
// → Database connection held cho TOÀN BỘ request duration (NGUY HIỂM)
 
// Vấn đề: Request mất 2s (1.9s render JSON, 0.1s DB query)
// → DB connection bị giữ 2s thay vì 0.1s
// → Connection pool exhausted dưới load
 
// ✅ Fix: Tắt OSIV, eager fetch những gì cần
// application.yml:
// spring:
//   jpa:
//     open-in-view: false
 
// Và fetch đầy đủ data trong Service layer (trước khi TX đóng)
@Service
@Transactional(readOnly = true)
public class OrderService {
   public OrderDetailDTO getOrderDetail(UUID id) {
       Order order = orderRepository.findByIdWithItemsAndCustomer(id);
       // JOIN FETCH tất cả cần thiết → không lazy load ngoài TX
       return mapper.toDetailDTO(order);
   }
}

# json serialization: thuế serialization

# jackson performance tuning

Mỗi REST request trong Spring Boot đều đi qua hai bước: JSON → Object (deserialize) lúc nhận và Object → JSON (serialize) lúc trả. Ở mức hàng nghìn request mỗi giây, Jackson có thể thành một CPU hotspot đáng kể.

// ❌ Default ObjectMapper — reflection-based, lần đầu chậm
// Jackson dùng reflection để inspect fields → tạo serializer/deserializer
// Lần serialize đầu cho mỗi type tốn hơn hẳn (reflection + build cache),
// các lần sau dùng serializer đã cache nên nhanh hơn nhiều
 
// ✅ Strategy 1: Reuse ObjectMapper (Spring Boot mặc định làm đúng)
// KHÔNG BAO GIỜ tạo new ObjectMapper() trong method — nó thread-safe, share được
@Configuration
public class JacksonConfig {
   @Bean
   public ObjectMapper objectMapper() {
       return JsonMapper.builder()
           .addModule(new JavaTimeModule())
           .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
           .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
           // Afterburner: bytecode generation thay vì reflection.
           // Module cũ, dựa vào internal API (Unsafe/defineClass) nên dễ
           // warning hoặc lỗi trên JDK mới;
           // Jackson khuyên dùng Blackbird (Strategy 2) cho JDK hiện đại
           .addModule(new AfterburnerModule())
           .build();
   }
}
// Dependency: com.fasterxml.jackson.module:jackson-module-afterburner
 
// ✅ Strategy 2: Blackbird (thay thế Afterburner, từ Jackson 2.12, nên dùng cho JDK 11+)
// Dùng java.lang.invoke.LambdaMetafactory thay vì bytecode generation
.addModule(new BlackbirdModule())  // Tương đương Afterburner, cleaner cho modern JVMs
// Dependency: com.fasterxml.jackson.module:jackson-module-blackbird
// (Jackson 3.x, mặc định trong Spring Boot 4, đổi groupId sang tools.jackson.*;
//  JavaTimeModule đã nằm sẵn trong databind)
 
// ✅ Strategy 3: Avoid serializing unnecessary fields
@JsonInclude(JsonInclude.Include.NON_NULL)  // Skip null fields → smaller JSON, less work
public class ProductDTO {
   private UUID id;
   private String name;
   private BigDecimal price;
 
   @JsonIgnore  // Never serialize — internal field
   private String internalCode;
 
   @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)  // Deserialize only
   private String secretToken;
}
 
// ✅ Strategy 4: Custom serializer cho hot DTOs
// Khi 1 DTO được serialize hàng triệu lần, custom serializer > reflection
public class ProductDTOSerializer extends JsonSerializer<ProductDTO> {
   @Override
   public void serialize(ProductDTO dto, JsonGenerator gen,
                         SerializerProvider provider) throws IOException {
       gen.writeStartObject();
       gen.writeStringField("id", dto.getId().toString());
       gen.writeStringField("name", dto.getName());
       gen.writeNumberField("price", dto.getPrice());  // giữ BigDecimal, đừng đổi sang double (mất chính xác)
       gen.writeEndObject();
       // Direct field writes — no reflection, no introspection
   }
}

# streaming json cho large responses

// ❌ Build entire list in memory → serialize all at once
@GetMapping("/products/export")
public List<ProductDTO> exportAll() {
   return productService.findAll();  // 100K objects in memory simultaneously
   // Jackson serializes toàn bộ List → massive heap usage → GC pressure
}
 
// ✅ Stream + Jackson Streaming API — memory gần như không đổi
// streamAll(): repository method tự khai báo, trả về Stream<Product>
// (nên kèm @QueryHints fetch size để JDBC driver không kéo hết về một lượt)
@GetMapping("/products/export")
public ResponseEntity<StreamingResponseBody> exportAll() {
   return ResponseEntity.ok()
       .contentType(MediaType.APPLICATION_JSON)
       .body(outputStream -> {
           // StreamingResponseBody chạy trên thread khác, ngoài transaction của request.
           // Spring Data JPA bắt buộc có transaction khi dùng Stream → tự mở bằng
           // TransactionTemplate (cấu hình readOnly = true)
           readOnlyTx.executeWithoutResult(status -> {
               try (JsonGenerator gen = objectMapper.getFactory()
                       .createGenerator(outputStream, JsonEncoding.UTF8);
                    Stream<Product> products = productRepository.streamAll()) {  // Stream phải được close
                   gen.writeStartArray();
                   int count = 0;
                   for (Product product : (Iterable<Product>) products::iterator) {
                       gen.writeObject(mapper.toDTO(product));
                       entityManager.detach(product);  // không để persistence context phình ra
                       if (++count % 1000 == 0) {
                           gen.flush();  // Flush định kỳ, không flush từng phần tử
                       }
                   }
                   gen.writeEndArray();
               } catch (IOException e) {
                   throw new UncheckedIOException(e);
               }
           });
       });
   // Memory: gần O(1) — mỗi lúc chỉ giữ một ít product (đã detach sau khi ghi)
}

# alternative: protocol buffers cho internal service communication

// Jackson JSON: human-readable, lặp lại tên field trong mỗi object
// Protobuf: binary, field đánh số thay vì tên, schema định nghĩa trước
// → thường nhanh hơn vài lần và payload nhỏ hơn đáng kể (tuỳ dữ liệu, phải tự benchmark)
 
// Khi CSP services gọi nhau (console → bpm-runtime → repo):
// REST + JSON: đơn giản, debug-friendly, standard
// gRPC + Protobuf: throughput cao hơn rõ cho high-frequency internal calls (HTTP/2, binary)
 
// Trade-off decision:
// External APIs (client-facing): JSON (compatibility, readability)
// Internal hot paths (service-to-service): consider Protobuf/gRPC

# graalvm native image: khởi động trong milliseconds

# vấn đề: jvm startup time

Một Spring Boot app chạy trên JVM thường mất vài giây tới hơn chục giây để khởi động (class loading, annotation scanning, tạo bean), rồi còn thêm thời gian JIT warmup mới đạt tốc độ tối đa. Trong môi trường serverless hay container scale liên tục, mỗi lần cold start là một khoảng trễ người dùng cảm nhận được.

# native image giải quyết gì

GraalVM Native Image compile Java bytecode thành native binary ngay lúc build (Ahead-of-Time compilation). Nó phân tích tĩnh để tìm mọi code có thể chạy tới (closed-world assumption), nên những gì chỉ lộ ra lúc runtime như reflection, proxy, resource phải được khai báo trước. Kết quả thường thấy:

  • Startup: cỡ vài chục tới vài trăm ms (thay vì vài giây)
  • Memory: thấp hơn đáng kể, thường chỉ bằng một phần so với chạy trên JVM
  • Không cần JIT warmup: request đầu đã chạy ở tốc độ ổn định, dù tốc độ đỉnh có thể thấp hơn JVM đã warm
<!-- pom.xml: Spring Boot 3.x native support -->
<plugin>
   <groupId>org.graalvm.buildtools</groupId>
   <artifactId>native-maven-plugin</artifactId>
</plugin>
 
<!-- Build native image -->
<!-- mvn -Pnative native:compile -->
<!-- Output: target/my-service (single executable binary, ~80MB) -->

# trade-offs và limitations

// ✅ Hoạt động tốt với Native Image:
// - Standard Spring Boot controllers, services, repositories
// - Spring Data JPA (với proper hints)
// - Spring Security (basic configurations)
// - RestTemplate, WebClient
 
// ❌ Không hoạt động (hoặc cần config thủ công):
// - Runtime reflection (phải declare trước trong reflect-config.json)
// - Dynamic proxies (JDK proxy cần declare, CGLIB không support)
// - Resource loading từ classpath (cần resource-config.json)
// - Serialization (cần serialization-config.json)
 
// Spring Boot 3.x generates hints automatically cho most cases
// Nhưng third-party libraries có thể cần manual hints
// (từ GraalVM for JDK 23, các file *-config.json được gộp vào một file
//  reachability-metadata.json; định dạng cũ vẫn được chấp nhận nhưng đã deprecated)
 
// reflect-config.json (định dạng cũ)
[
 {
   "name": "com.tungdadev.csp.dto.ProductDTO",
   "allDeclaredConstructors": true,
   "allDeclaredFields": true,
   "allDeclaredMethods": true
 }
]
 
// ❌ KHÔNG phù hợp cho:
// - Applications dùng heavy reflection (Activiti engine, complex Hibernate mappings)
// - Long-running services hưởng lợi từ JIT optimization (JIT > AOT cho peak throughput)
// - Services cần dynamic class loading
 
// ✅ PHÙ HỢP cho:
// - Lightweight microservices (config-service, discovery, proxy)
// - Serverless functions (AWS Lambda, Azure Functions)
// - CLI tools
// - Services cần fast startup (auto-scaling, spot instances)

# so sánh thực tế

MetricJVM (Hotspot)Native Image
Startup timeVài giâyVài chục-trăm ms
Memory (idle)200-500MB50-80MB
Peak throughputHigher (JIT optimized)Thường thấp hơn (PGO thu hẹp khoảng cách)
Build time10-30s3-10 minutes
Debug-abilityFull (JFR, JMX, profilers)Limited
ReflectionFull supportRequires hints

# caffeine cache: local cache "không rác"

# tại sao không chỉ dùng redis?

Gọi Redis là một network call, round-trip thường cỡ 0.5-2ms. Với data bị đọc hàng nghìn lần mỗi giây, chi phí mạng lớn hơn cả chi phí tính toán. Caffeine là cache nằm ngay trong process, đọc mất cỡ vài chục nanosecond, không qua mạng. Đổi lại, mỗi instance giữ một bản riêng, nên dữ liệu giữa các instance có thể lệch nhau trong khoảng TTL.

// Architecture: Request → Caffeine (L1, in-process) → Redis (L2, distributed) → DB (L3)
// Cache hit L1: ~50ns
// Cache hit L2: ~1-2ms (network)
// Cache miss (DB): ~5-50ms
 
// ✅ Spring Boot + Caffeine configuration
// Dependency: com.github.ben-manes.caffeine:caffeine
 
@Configuration
@EnableCaching
public class CacheConfig {
 
   @Bean
   public CacheManager cacheManager() {
       CaffeineCacheManager manager = new CaffeineCacheManager();
       manager.setCaffeine(Caffeine.newBuilder()
           .maximumSize(10_000)              // Max 10K entries
           .expireAfterWrite(Duration.ofMinutes(5))  // TTL 5 minutes
           .recordStats()                    // Enable metrics
       );
       return manager;
   }
 
   // Hoặc per-cache configuration (CHỌN MỘT trong hai bean,
   // không khai báo cả hai CacheManager cùng lúc)
   @Bean
   public CacheManager perCacheManager() {
       SimpleCacheManager manager = new SimpleCacheManager();
       manager.setCaches(List.of(
           buildCache("products", 5000, Duration.ofMinutes(10)),
           buildCache("users", 1000, Duration.ofMinutes(30)),
           buildCache("permissions", 2000, Duration.ofMinutes(5))
       ));
       return manager;
   }
 
   private CaffeineCache buildCache(String name, int maxSize, Duration ttl) {
       return new CaffeineCache(name, Caffeine.newBuilder()
           .maximumSize(maxSize)
           .expireAfterWrite(ttl)
           .recordStats()
           .build());
   }
}

# sử dụng với spring @cacheable

@Service
@RequiredArgsConstructor
public class ProductService {
 
   private final ProductRepository productRepository;
 
   // ✅ Cache kết quả — subsequent calls skip DB entirely
   @Cacheable(value = "products", key = "#id")
   public ProductDTO getProduct(UUID id) {
       return productRepository.findById(id)
           .map(this::toDTO)
           .orElseThrow(() -> new EntityNotFoundException("Product not found"));
       // First call: DB query + cache result
       // Subsequent calls: return from Caffeine (~50ns)
   }
 
   // ✅ Invalidate khi data thay đổi
   @CacheEvict(value = "products", key = "#id")
   public ProductDTO updateProduct(UUID id, UpdateProductDTO dto) {
       Product product = productRepository.findById(id).orElseThrow();
       product.setName(dto.getName());
       product.setPrice(dto.getPrice());
       return toDTO(productRepository.save(product));
   }
 
   // ✅ Invalidate toàn bộ cache
   @CacheEvict(value = "products", allEntries = true)
   public void refreshCache() {
       log.info("Products cache invalidated");
   }
 
   // ✅ Conditional caching — chỉ cache khi result != null
   @Cacheable(value = "products", key = "#code", unless = "#result == null")
   public ProductDTO findByCode(String code) {
       return productRepository.findByCode(code)
           .map(this::toDTO)
           .orElse(null);
   }
}

# caffeine advanced: async loading + multi-level cache

// ✅ AsyncLoadingCache — non-blocking cache population
@Component
public class ProductCacheService {
 
   private final AsyncLoadingCache<UUID, ProductDTO> cache;
 
   public ProductCacheService(ProductRepository repo, ProductMapper mapper) {
       this.cache = Caffeine.newBuilder()
           .maximumSize(10_000)
           .expireAfterWrite(Duration.ofMinutes(5))
           .refreshAfterWrite(Duration.ofMinutes(1))  // Background refresh
           .buildAsync((key, executor) -> CompletableFuture.supplyAsync(() ->
               repo.findById(key).map(mapper::toDTO).orElse(null), executor
           ));
       // refreshAfterWrite: sau 1 phút, lần access tiếp → return stale value
       // + trigger async refresh in background → next access gets fresh value
       // → Key đang "nóng" không bị latency spike khi refresh. Lần load đầu tiên
       //   và key đã hết hạn (expireAfterWrite 5 phút không ai đọc) vẫn là cache miss
   }
 
   public CompletableFuture<ProductDTO> getProduct(UUID id) {
       return cache.get(id);  // Non-blocking, returns CompletableFuture
   }
}
 
// ✅ Two-level cache: Caffeine (L1) + Redis (L2)
@Service
@RequiredArgsConstructor
public class TwoLevelCacheService {
 
   private final Cache<String, ProductDTO> localCache = Caffeine.newBuilder()
       .maximumSize(5000)
       .expireAfterWrite(Duration.ofSeconds(30))  // Short TTL for consistency
       .build();
 
   private final RedisTemplate<String, ProductDTO> redisTemplate;
   private final ProductRepository repository;
 
   public ProductDTO getProduct(String id) {
       // L1: Local cache (nanoseconds)
       ProductDTO cached = localCache.getIfPresent(id);
       if (cached != null) return cached;
 
       // L2: Redis (milliseconds)
       cached = redisTemplate.opsForValue().get("product:" + id);
       if (cached != null) {
           localCache.put(id, cached);  // Populate L1
           return cached;
       }
 
       // L3: Database (tens of milliseconds)
       ProductDTO fresh = repository.findById(UUID.fromString(id))
           .map(this::toDTO)
           .orElseThrow();
 
       // Populate both levels
       redisTemplate.opsForValue().set("product:" + id, fresh, Duration.ofMinutes(10));
       localCache.put(id, fresh);
       return fresh;
   }
}

# caffeine eviction strategies

StrategyConfigUse Case
Size-based.maximumSize(N)Bounded memory, W-TinyLFU eviction
Weight-based.maximumWeight(N).weigher(...)Heterogeneous entry sizes
Time-based (write).expireAfterWrite(duration)Data có known TTL
Time-based (access).expireAfterAccess(duration)Session-like data
Reference-based.weakValues() / .softValues()GC-friendly, memory-pressure eviction

# data locality trong java: SoA pattern trong thực tế

# vấn đề: java object layout

Trên HotSpot 64-bit với cấu hình mặc định (compressed oops, heap < 32GB), mỗi object có 12 bytes header (mark word 8 bytes + compressed class pointer 4 bytes). Một Point { double x; double y; } = 12 header + 4 bytes đệm để double căn theo 8 + 8 + 8 = 32 bytes. Mảng 1M Point = 1M reference (4 bytes mỗi cái) + 1M object (32 bytes mỗi cái) = ~36MB. Các object này lúc mới tạo liền nhau thì thường nằm gần nhau (cấp phát tuần tự trong TLAB), nhưng sau vài vòng GC, hay khi tạo xen kẽ với object khác, chúng có thể nằm rải rác. (Từ JDK 27, compact object headers bật mặc định theo JEP 534, header còn 8 bytes và Point còn 24 bytes, nhưng vấn đề pointer chase vẫn y nguyên.)

Cache line của CPU thường là 64 bytes. Khi duyệt mảng object, mỗi points[i].x là một lần pointer chase: đọc reference trước, rồi mới nhảy tới object. Nếu object không nằm trong cache thì phải xuống RAM, mất cỡ vài trăm clock cycles (~60-100ns), trong khi đọc trúng L1 chỉ mất khoảng 4 cycles.

# struct of arrays (SoA): cache-friendly alternative

// ❌ Array of Structs (AoS) — default Java style
// Memory layout: [ptr0][ptr1][ptr2]... → [hdr|x0|y0|vx0|vy0] [hdr|x1|y1|vx1|vy1]...
// Khi chỉ cần x,y,vx,vy → vẫn kéo cả header, color, active vào cache line = phí bandwidth
class Particle {
   double x, y;    // position
   double vx, vy;  // velocity
   int color;      // rendering only
   boolean active; // logic only
}
Particle[] particles = new Particle[1_000_000];  // 1M pointers + 1M scattered objects
 
// Tính toán physics (chỉ cần x, y, vx, vy — không cần color, active):
for (Particle p : particles) {
   p.x += p.vx * dt;  // Cache miss: load toàn bộ object (~56 bytes) cho 4 fields
   p.y += p.vy * dt;
}
 
// ✅ Struct of Arrays (SoA) — group by access pattern
// Memory layout: [x0|x1|x2|x3|...] [y0|y1|y2|y3|...] [vx0|vx1|...] [vy0|vy1|...]
// Khi cần x → CPU prefetcher load sequential doubles → cache hit mọi access
class ParticleSystem {
   int count;
   double[] xs;     // All X positions contiguous
   double[] ys;     // All Y positions contiguous
   double[] vxs;    // All X velocities contiguous
   double[] vys;    // All Y velocities contiguous
   int[] colors;    // Separate — not loaded during physics
   boolean[] active; // Separate — not loaded during physics
 
   public ParticleSystem(int capacity) {
       this.count = 0;
       this.xs = new double[capacity];
       this.ys = new double[capacity];
       this.vxs = new double[capacity];
       this.vys = new double[capacity];
       this.colors = new int[capacity];
       this.active = new boolean[capacity];
   }
 
   // Physics update: CPU prefetcher LOVES this pattern
   // Sequential access → every cache line fully utilized
   public void updatePositions(double dt) {
       for (int i = 0; i < count; i++) {
           xs[i] += vxs[i] * dt;   // xs[] contiguous → prefetch works perfectly
           ys[i] += vys[i] * dt;   // ys[] contiguous → same
       }
       // JIT compiler có thể AUTO-VECTORIZE loop này (SIMD)
       // → Process 4 doubles per instruction (AVX2)
   }
 
   // Rendering: chỉ load positions + colors (skip velocities)
   // (Graphics ở đây là API vẽ giả định, không phải java.awt.Graphics)
   public void render(Graphics g) {
       for (int i = 0; i < count; i++) {
           g.setColor(colors[i]);
           g.drawCircle(xs[i], ys[i], 3);
           // colors[], xs[], ys[] contiguous — 3 cache-friendly arrays
           // vxs[], vys[] KHÔNG bị load vào cache → more room for useful data
       }
   }
}

# SoA trong spring boot: batch processing thực tế

// Scenario: Tính discount cho 100K products mỗi đêm
// Input: product prices + categories + stock levels
// Logic: discount phụ thuộc price + category (KHÔNG cần name, description, images...)
 
// ❌ AoS: Load full Product entities
@Scheduled(cron = "0 0 2 * * *")
public void calculateNightlyDiscounts() {
   List<Product> products = productRepository.findAll();  // 100K full objects
   // Mỗi Product: id, name, description, price, category, stock, images, metadata...
   // Memory: ~500 bytes/product × 100K = 50MB (chỉ cần ~20 bytes/product)
   for (Product p : products) {
       double discount = computeDiscount(p.getPrice(), p.getCategory(), p.getStock());
       p.setDiscount(discount);
   }
   productRepository.saveAll(products);
}
 
// ✅ SoA: Query chỉ columns cần thiết, process in parallel arrays
@Scheduled(cron = "0 0 2 * * *")
public void calculateNightlyDiscounts() {
   // Projection query — chỉ lấy 4 fields
   List<Object[]> rows = entityManager.createNativeQuery(
       "SELECT id, price, category, stock_level FROM products WHERE status = 'ACTIVE'"
   ).getResultList();
 
   int size = rows.size();
   UUID[] ids = new UUID[size];
   double[] prices = new double[size];
   int[] categories = new int[size];
   int[] stocks = new int[size];
 
   // Unpack into parallel arrays (SoA layout)
   for (int i = 0; i < size; i++) {
       Object[] row = rows.get(i);
       ids[i] = (UUID) row[0];
       prices[i] = ((Number) row[1]).doubleValue();
       categories[i] = ((Number) row[2]).intValue();
       stocks[i] = ((Number) row[3]).intValue();
   }
 
   // Compute discounts — cache-friendly sequential access
   double[] discounts = new double[size];
   for (int i = 0; i < size; i++) {
       discounts[i] = computeDiscount(prices[i], categories[i], stocks[i]);
   }
 
   // Batch update
   jdbcTemplate.batchUpdate(
       "UPDATE products SET discount = ? WHERE id = ?",
       new BatchPreparedStatementSetter() { /* ... */ }
   );
}

# lookup tables (LUTs): precomputation strategy

# nguyên lý: đánh đổi memory lấy cpu cycles

Thay vì tính toán runtime, tính trước mọi kết quả và lưu trong array. Truy xuất = array[index] = O(1), 1 memory access.

// ❌ Runtime computation: sin() mỗi frame (floating point = expensive)
// Giả sử game loop 60fps, 10K particles mỗi frame = 600K sin() calls/second
public double getAngle(int degreeTenths) {
   return Math.sin(Math.toRadians(degreeTenths / 10.0));
}
 
// ✅ Precomputed sine table — 1 memory read thay vì floating point computation
public class SineLUT {
   // 3600 entries = 0.0° to 359.9° (precision 0.1°)
   private static final double[] SIN_TABLE = new double[3600];
   private static final double[] COS_TABLE = new double[3600];
 
   static {
       for (int i = 0; i < 3600; i++) {
           double radians = Math.toRadians(i / 10.0);
           SIN_TABLE[i] = Math.sin(radians);
           COS_TABLE[i] = Math.cos(radians);
       }
   }
 
   // O(1) lookup — ~4 clock cycles (cache hit) vs ~20-50 cycles (Math.sin)
   public static double sin(int degreeTenths) {
       return SIN_TABLE[((degreeTenths % 3600) + 3600) % 3600];
   }
 
   public static double cos(int degreeTenths) {
       return COS_TABLE[((degreeTenths % 3600) + 3600) % 3600];
   }
}

# LUT cho business logic: status/fee calculation

// Scenario: Tính phí giao dịch dựa trên (loại giao dịch × hạng khách hàng × kênh)
// 5 loại × 4 hạng × 3 kênh = 60 combinations
// ❌ Nested if/else hoặc switch: O(branches), hard to maintain, error-prone
 
// ✅ 3D Lookup Table — O(1) access, dễ cập nhật
public class FeeCalculator {
   // Dimensions: [transactionType][customerTier][channel]
   // volatile + thay nguyên bảng khi reload → thread đọc luôn thấy một bảng hoàn chỉnh
   private static volatile double[][][] feeTable = new double[5][4][3];
 
   static {
       // Load from config/database at startup
       // Hoặc hardcode:
       // feeTable[TRANSFER][VIP][MOBILE] = 0.001;
       // feeTable[TRANSFER][NORMAL][COUNTER] = 0.005;
       initFromConfig();
   }
 
   // O(1) fee lookup — không rẽ nhánh theo business rule, không tính toán
   public static double getFeeRate(TransactionType type, CustomerTier tier, Channel channel) {
       return feeTable[type.ordinal()][tier.ordinal()][channel.ordinal()];
   }
 
   // Hot-reload từ database (runtime config change)
   // (class này cần là Spring bean và inject feeConfigRepository thì @Scheduled mới chạy)
   @Scheduled(fixedRate = 60_000)  // Refresh mỗi phút
   public void refreshFeeTable() {
       double[][][] fresh = new double[5][4][3];  // build bảng mới, không sửa bảng đang được đọc
       for (FeeConfig config : feeConfigRepository.findAll()) {
           fresh[config.getType().ordinal()]
                [config.getTier().ordinal()]
                [config.getChannel().ordinal()] = config.getRate();
       }
       feeTable = fresh;  // publish an toàn qua volatile write
   }
}

# LUT cho validation: character classification

// ❌ String-based validation (tạo strings, regex, multiple comparisons)
public boolean isValidIdentifier(String s) {
   return s.matches("[a-zA-Z_][a-zA-Z0-9_]*");  // Regex = expensive per call
}
 
// ✅ Lookup table: O(1) per character check
public class CharClassifier {
   // 128 ASCII entries — boolean array indexed by char value
   private static final boolean[] IS_IDENTIFIER_START = new boolean[128];
   private static final boolean[] IS_IDENTIFIER_PART = new boolean[128];
 
   static {
       for (char c = 'a'; c <= 'z'; c++) { IS_IDENTIFIER_START[c] = true; IS_IDENTIFIER_PART[c] = true; }
       for (char c = 'A'; c <= 'Z'; c++) { IS_IDENTIFIER_START[c] = true; IS_IDENTIFIER_PART[c] = true; }
       for (char c = '0'; c <= '9'; c++) { IS_IDENTIFIER_PART[c] = true; }
       IS_IDENTIFIER_START['_'] = true;
       IS_IDENTIFIER_PART['_'] = true;
   }
 
   public static boolean isValidIdentifier(String s) {
       if (s == null || s.isEmpty()) return false;
       char first = s.charAt(0);
       if (first >= 128 || !IS_IDENTIFIER_START[first]) return false;
       for (int i = 1; i < s.length(); i++) {
           char c = s.charAt(i);
           if (c >= 128 || !IS_IDENTIFIER_PART[c]) return false;
       }
       return true;
       // O(n) scan nhưng mỗi check = 1 array access (vs regex backtracking)
   }
}

# memory pooling nâng cao: arena allocation (java 22+)

# vấn đề sâu hơn object pooling

Object pooling (đã nói ở bài java high performance techniques) giải quyết chuyện tái sử dụng object. Còn với off-heap memory (native memory, direct ByteBuffer) thì rắc rối hơn:

  • ByteBuffer.allocateDirect() đắt hơn cấp phát trên heap nhiều: phải xin native memory, zero vùng nhớ, kiểm tra hạn mức -XX:MaxDirectMemorySize (khi gần chạm hạn mức còn có thể kích hoạt System.gc()).
  • Direct buffer chỉ được giải phóng khi object ByteBuffer bị GC thu hồi (qua Cleaner). Heap còn rộng thì GC chưa chạy, native memory cứ thế phình lên, nhìn giống leak.
  • Không có cách chính thống để giải phóng ngay một direct buffer, nên vòng đời rất khó kiểm soát.

# MemorySegment arena (java 22+ foreign function & memory api)

FFM API chính thức từ Java 22 (JEP 454). Arena quản lý vòng đời của một nhóm vùng nhớ: đóng arena là giải phóng hết, ngay lập tức, không chờ GC. Đoạn code dưới viết theo API Java 22 trở lên (ở Java 21 bản preview, tên method có khác, vd allocateArray).

// ✅ Arena-based allocation — deterministic deallocation, no GC pressure
import java.lang.foreign.*;
 
// Confined arena: memory freed khi close(), scoped to 1 thread
try (Arena arena = Arena.ofConfined()) {
   // Allocate 1MB off-heap buffer — no GC involvement
   MemorySegment buffer = arena.allocate(1024 * 1024);
 
   // Allocate structured data (like C struct)
   MemorySegment point = arena.allocate(ValueLayout.JAVA_DOUBLE, 2);
   point.setAtIndex(ValueLayout.JAVA_DOUBLE, 0, 3.14);  // x
   point.setAtIndex(ValueLayout.JAVA_DOUBLE, 1, 2.71);  // y
 
   // Allocate array of 10,000 doubles (contiguous, off-heap)
   MemorySegment prices = arena.allocate(ValueLayout.JAVA_DOUBLE, 10_000);
   for (int i = 0; i < 10_000; i++) {
       prices.setAtIndex(ValueLayout.JAVA_DOUBLE, i, computePrice(i));
   }
   // Process prices — data is contiguous, cache-friendly
   processContiguous(prices, 10_000);
 
}  // ALL memory freed instantly here — no GC, no finalizers, deterministic
 
// Shared arena: accessible from multiple threads
Arena shared = Arena.ofShared();
MemorySegment sharedBuffer = shared.allocate(4096);
// ... pass to multiple threads ...
shared.close();  // Free when done — thread-safe close
 
// Auto arena: memory freed by GC (like DirectByteBuffer, but safer API)
Arena auto = Arena.ofAuto();
MemorySegment managed = auto.allocate(1024);
// Freed khi GC thấy arena và các segment của nó không còn ai tham chiếu
// Không close() được (gọi sẽ ném UnsupportedOperationException)

# practical use: high-throughput network buffer pool

// Scenario: Server xử lý 100K connections, mỗi connection cần read/write buffer
// Allocate/free ByteBuffer liên tục = expensive, fragmented
 
public class DirectBufferPool {
   private final int bufferSize;
   private final Queue<MemorySegment> pool;
   private final Arena arena;  // Long-lived arena for pool buffers
 
   public DirectBufferPool(int bufferSize, int poolSize) {
       this.bufferSize = bufferSize;
       this.pool = new ConcurrentLinkedQueue<>();
       this.arena = Arena.ofShared();
 
       // Pre-allocate all buffers in one large allocation (contiguous)
       MemorySegment bulk = arena.allocate((long) bufferSize * poolSize);
       for (int i = 0; i < poolSize; i++) {
           pool.offer(bulk.asSlice((long) i * bufferSize, bufferSize));
       }
   }
 
   public MemorySegment acquire() {
       MemorySegment seg = pool.poll();
       if (seg != null) return seg;
       // Pool exhausted — allocate new (should be rare if pool sized correctly)
       // Lưu ý: segment này sống tới khi shutdown() và sẽ vào pool khi release,
       // nên pool có thể lớn dần nếu thường xuyên cạn
       return arena.allocate(bufferSize);
   }
 
   public void release(MemorySegment seg) {
       // Zero-out for security, then return to pool
       seg.fill((byte) 0);
       pool.offer(seg);
   }
 
   public void shutdown() {
       arena.close();  // Free ALL pooled memory at once
   }
}

# lock-free nâng cao: beyond AtomicInteger

# compare-and-swap (cas): cơ chế phần cứng

CAS (compare-and-swap) là một lệnh CPU nguyên tử (x86: LOCK CMPXCHG; ARMv8.1+ có lệnh CAS, ARM đời cũ dùng cặp LL/SC). Trong một bước không thể chen ngang, nó đọc giá trị, so với giá trị kỳ vọng, nếu bằng thì ghi giá trị mới. Không cần lock của OS, không có context switch. Có tranh chấp thì thread thua chỉ việc thử lại.

// CAS pseudo-code (thực tế là 1 CPU instruction):
// boolean CAS(address, expectedValue, newValue) {
//     if (*address == expectedValue) {
//         *address = newValue;
//         return true;   // Success
//     }
//     return false;      // Someone else changed it — retry
// }
 
// ✅ CAS-based stack (Treiber stack, lock-free)
// (Trong Java, GC giúp tránh bài toán ABA ở đây vì node cũ không bị tái sử dụng khi còn tham chiếu)
public class LockFreeStack<E> {
   private final AtomicReference<Node<E>> top = new AtomicReference<>(null);
 
   private static class Node<E> {
       final E item;
       Node<E> next;
       Node(E item, Node<E> next) { this.item = item; this.next = next; }
   }
 
   public void push(E item) {
       Node<E> newNode = new Node<>(item, null);
       Node<E> currentTop;
       do {
           currentTop = top.get();
           newNode.next = currentTop;
       } while (!top.compareAndSet(currentTop, newNode));
       // CAS loop: retry nếu another thread modified top giữa get() và compareAndSet()
       // Trong practice: retry rất ít lần (1-3) trừ extreme contention
   }
 
   public E pop() {
       Node<E> currentTop;
       Node<E> newTop;
       do {
           currentTop = top.get();
           if (currentTop == null) return null;  // Empty stack
           newTop = currentTop.next;
       } while (!top.compareAndSet(currentTop, newTop));
       return currentTop.item;
   }
}

# varhandle: modern cas api (java 9+)

// VarHandle: type-safe, performance-equivalent to Unsafe, legal API
// Thay thế sun.misc.Unsafe cho CAS operations
 
public class LockFreeCounter {
   private volatile long value;
 
   private static final VarHandle VALUE_HANDLE;
   static {
       try {
           VALUE_HANDLE = MethodHandles.lookup()
               .findVarHandle(LockFreeCounter.class, "value", long.class);
       } catch (Exception e) {
           throw new ExceptionInInitializerError(e);
       }
   }
 
   public long incrementAndGet() {
       long current;
       long next;
       do {
           current = (long) VALUE_HANDLE.getVolatile(this);
           next = current + 1;
       } while (!VALUE_HANDLE.compareAndSet(this, current, next));
       return next;
   }
 
   // Memory ordering options (từ weak → strong):
   // getOpaque()    — no ordering guarantees (fastest)
   // getAcquire()   — acquire semantics (reads after this see updates)
   // getVolatile()  — full volatile (sequential consistency)
 
   // setOpaque()    — no ordering
   // setRelease()   — release semantics (writes before this visible)
   // setVolatile()  — full volatile
 
   // weakCompareAndSet() — may spuriously fail (dùng trong loops anyway)
   // compareAndSet()     — strong, never spuriously fails
}

# false sharing: cache line contention ẩn

// Vấn đề: 2 threads write vào 2 biến khác nhau
// NHƯNG 2 biến nằm trên CÙNG cache line (64 bytes)
// → CPU phải invalidate cache line liên tục giữa cores = performance collapse
 
// ❌ False sharing: counters cạnh nhau trong memory
public class Counters {
   volatile long counter1;  // Core 0 writes
   volatile long counter2;  // Core 1 writes
   // Cả 2 fit trong 1 cache line (16 bytes << 64 bytes)
   // → Core 0 write invalidates Core 1's cache line (and vice versa)
   // → Có thể chậm đi nhiều lần so với khi 2 biến nằm ở 2 cache line riêng
}
 
// ✅ @Contended: JVM chèn padding để tách cache lines
// Annotation nằm trong jdk.internal → code ngoài JDK cần:
//   compile: --add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED
//   runtime: -XX:-RestrictContended (không có flag này JVM bỏ qua annotation)
public class ContendedCounters {
   @jdk.internal.vm.annotation.Contended volatile long counter1;  // đặt trên từng field
   @jdk.internal.vm.annotation.Contended volatile long counter2;  // mỗi field được padding riêng
}
 
// Hoặc manual padding (không cần flag nào):
public class PaddedCounters {
   volatile long counter1;
   // Manual padding: 7 longs = 56 bytes → đẩy counter2 sang cache line khác
   long p1, p2, p3, p4, p5, p6, p7;
   volatile long counter2;
   // Cẩn thận: JVM được phép sắp xếp lại field trong cùng class.
   // Cách chắc hơn là padding qua kế thừa (field cha nằm trước field con),
   // kiểu các thư viện như JCTools/Disruptor đang làm. Kiểm tra bằng JOL.
}
 
// LongAdder đã solve false sharing internally — dùng nó thay vì tự padding
// Internally: Cell[] array với @Contended annotation trên mỗi Cell

# SIMD vectorization: khi JIT tự tối ưu

# auto-vectorization trong hotspot jit

C2 (JIT compiler của HotSpot) có thể tự biến các vòng lặp đơn giản thành lệnh SIMD (SSE/AVX trên x86, NEON/SVE trên ARM) qua cơ chế SuperWord. Với AVX2 (thanh ghi 256 bit), một lệnh xử lý 4 double hoặc 8 float cùng lúc.

// ✅ Loop mà JIT CÓ THỂ auto-vectorize:
// - Simple array operations
// - No method calls trong loop body
// - No branches (hoặc branch rất đơn giản)
// - Predictable loop bounds
 
// ✅ Vectorizable: element-wise addition
public static void addArrays(double[] a, double[] b, double[] result) {
   for (int i = 0; i < a.length; i++) {
       result[i] = a[i] + b[i];  // JIT: 4 additions per AVX2 instruction
   }
}
 
// ✅ Vectorizable: scalar multiplication
public static void scale(double[] arr, double factor) {
   for (int i = 0; i < arr.length; i++) {
       arr[i] *= factor;  // JIT: 4 multiplications per instruction
   }
}
 
// ❌ NOT vectorizable: method call in loop
public static void transform(double[] arr) {
   for (int i = 0; i < arr.length; i++) {
       arr[i] = customTransform(arr[i]);  // Method call blocks vectorization (trừ khi JIT inline được nó)
   }
}
 
// ❌ NOT vectorizable: data-dependent branch
public static void conditional(double[] arr) {
   for (int i = 0; i < arr.length; i++) {
       if (arr[i] > 0) arr[i] = Math.sqrt(arr[i]);  // Branch thường chặn SIMD
       else arr[i] = 0;
   }
}

# vector api (incubator): explicit SIMD control

Vector API (jdk.incubator.vector) incubate từ Java 16 và tới JDK 27 vẫn là incubator (JEP 537, lần thứ 12), vì đang chờ Project Valhalla. Muốn dùng phải thêm --add-modules jdk.incubator.vector khi compile và chạy. Đoạn code dưới đã chạy được trên JDK 21.

// Khi auto-vectorization không đủ: Vector API cho explicit SIMD programming
import jdk.incubator.vector.*;
 
public class VectorMath {
   // Process 4 doubles at once (AVX2: 256-bit = 4 × 64-bit double)
   private static final VectorSpecies<Double> SPECIES = DoubleVector.SPECIES_256;
 
   // ✅ Explicit vectorized dot product
   public static double dotProduct(double[] a, double[] b) {
       int upperBound = SPECIES.loopBound(a.length);
       DoubleVector vsum = DoubleVector.zero(SPECIES);
 
       // Main loop: process 4 elements per iteration
       int i = 0;
       for (; i < upperBound; i += SPECIES.length()) {
           DoubleVector va = DoubleVector.fromArray(SPECIES, a, i);
           DoubleVector vb = DoubleVector.fromArray(SPECIES, b, i);
           vsum = va.fma(vb, vsum);  // Fused Multiply-Add: vsum += va * vb
           // 1 instruction = 4 multiplications + 4 additions
       }
 
       double result = vsum.reduceLanes(VectorOperators.ADD);
 
       // Tail loop: remaining elements (< 4)
       for (; i < a.length; i++) {
           result += a[i] * b[i];
       }
       return result;
   }
 
   // Speedup lý thuyết tối đa: ~4x vs scalar loop (4 lanes)
   // Với AVX-512 (SPECIES_512): tối đa ~8x (8 doubles per instruction)
   // Thực tế thấp hơn khi bị giới hạn bởi memory bandwidth; nên dùng
   // DoubleVector.SPECIES_PREFERRED để tự chọn độ rộng hợp với CPU
   // Lưu ý: cộng FP theo lane đổi thứ tự cộng → kết quả có thể lệch rất nhỏ so với loop thường
}

# khi nào simd matters trong enterprise java?

Use CaseFrequencySIMD benefit
JSON parsing (character scanning)Every requestModerate (JIT auto-vectorizes)
Batch price calculationsNightly batchHigh (explicit Vector API)
Image/PDF processingOn-demandVery high
Encryption/hashingEvery requestBuilt-in (JDK uses intrinsics)
String operations (equals, indexOf)FrequentAlready optimized by JDK
ML inferenceRare in enterpriseExtreme (use native libs)

Trong enterprise Java, bạn hiếm khi cần SIMD viết tay. JIT và các intrinsic của JDK đã lo phần lớn. Vector API đáng cân nhắc khi làm xử lý tín hiệu, tính toán khoa học, hoặc khi đã kiểm tra mà JIT không vectorize được vòng lặp nóng của bạn.

# decision matrix

TechniqueComplexityRiskRewardDùng khi
Tránh AutoboxingLowNoneMediumLuôn luôn (habit tốt)
EnumSet bitmaskLowNoneMediumPermission/flag systems
Virtual ThreadsLowLowHighI/O-bound services (Java 21+)
Caffeine L1 cacheMediumLowHighHot data, >1000 reads/sec
Hibernate tuning (OSIV, projections)MediumLowHighMọi JPA project
Jackson optimizationMediumNoneMediumHigh-throughput APIs
Branchless programmingHighMediumMediumInner loops + random data (profile first!)
SoA data layoutHighMediumHighBatch processing >100K items
Lookup TablesLowNoneHighRepeated computation, bounded input
Lock-free (CAS)Very HighHighHighExtreme contention, framework-level
Arena allocationHighMediumHighOff-heap, native interop
GraalVM NativeHighHighHighServerless, startup-critical
Explicit SIMD (Vector API)Very HighMediumVery HighScientific/batch computation

# vài nguyên tắc cần nhớ

  1. Profile trước: Dùng JFR/async-profiler tìm bottleneck thật. Phần lớn trường hợp, nó không nằm ở chỗ bạn đoán.

  2. Hái quả thấp trước: Autoboxing, Hibernate N+1, thiếu cache, chọn sai collection. Mấy thứ này sửa nhanh mà nhiều khi lợi gấp bội.

  3. Mặc định ưu tiên dễ đọc: Clean code + thuật toán đúng quan trọng hơn micro-optimization. Team còn phải đọc và bảo trì code này nhiều năm.

  4. Đo, đừng đoán: JMH cho micro-benchmark, Gatling/k6 cho load test. Số liệu thật đáng tin hơn cảm giác.

  5. JIT thông minh hơn bạn nghĩ: HotSpot tự inline, phân tích escape, vectorize, bỏ dead code. Đừng "tối ưu" thứ JIT đã làm rồi.

  6. Tối ưu đúng tầng: Sửa ở kiến trúc (thêm cache, xử lý async, gom batch) thường lợi hơn hẳn, có khi tới hàng chục, hàng trăm lần. Micro-optimization (branchless, SIMD) may lắm được vài lần, và chỉ ở đúng đoạn code nóng.

Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.