Skip to main content
Java remains one of the most widely deployed backend languages in the industry, backed by a mature ecosystem and a battle-tested JVM. This page distills the core knowledge you need for building production-grade Java services: Spring Boot’s auto-configuration model, JVM memory regions and class loading, multithreading primitives, Java 8+ language features, and data-access patterns with MyBatis-Plus.

Spring Boot Essentials

Spring Boot eliminates the boilerplate of traditional Spring applications. It follows a “convention over configuration” philosophy: a handful of starter dependencies gives you an embedded Tomcat server, JSON serialization, database connectivity, and more—all wired together automatically.

Auto-configuration and starters

Adding spring-boot-starter-web to your pom.xml pulls in Spring MVC, an embedded Tomcat, and Jackson without any XML configuration. Spring Boot scans your classpath and conditionally activates beans based on what it finds.
Enable hot reload during development with the devtools starter:
Then configure it in application.properties:

Dependency injection

Spring Boot manages beans through its IoC container. Annotate classes with @Service, @Repository, or @Component to register them, and use @Autowired (or constructor injection) to receive dependencies:

REST controllers

Use @RestController for APIs that return data (JSON by default). @Controller is for server-rendered pages. In a front-end/back-end separated architecture you will almost always use @RestController.
Spring Boot also supports request mapping shortcuts: @GetMapping, @PostMapping, @PutMapping, @DeleteMapping. Route paths should contain only nouns, not verbs (/users, not /getUser).

Interceptors and filters

Interceptors handle Spring-managed resources; filters run before the Spring context and intercept every request including static files. Execution order: Filter → Interceptor → Controller.
Register the interceptor in a WebMvcConfigurer:

JVM Internals

The JVM manages memory in several distinct regions. Understanding them helps you tune heap sizes, diagnose OutOfMemoryError, and reason about object lifecycles.

Memory regions

The heap is the largest region and the primary GC target. You control its bounds with -Xms (initial size) and -Xmx (maximum size).

Object lifecycle

When the JVM executes new Foo():
  1. Class load check — verifies that Foo is already loaded, linked, and initialized; triggers class loading if not.
  2. Memory allocation — carves out a region of the heap. Uses CAS-based retry or per-thread TLAB (Thread-Local Allocation Buffer) to keep allocations thread-safe.
  3. Zero initialization — sets all instance fields to their zero values so code can read fields before explicitly assigning them.
  4. Object header setup — records the class pointer, identity hash code, GC generation age, and lock state in the header.
  5. Constructor — runs <init>() to set user-defined initial values.

Class loading

Class loading proceeds through five stages:
  1. Loading — reads the .class bytecode and creates a Class object in the method area.
  2. Verification — confirms the bytecode conforms to the JVM specification (no memory corruption, no type violations).
  3. Preparation — allocates memory for static variables and sets them to zero (not their declared values yet).
  4. Resolution — replaces symbolic references in the constant pool with direct memory pointers.
  5. Initialization — executes the <clinit>() method, running static initializer blocks and assigning declared static values.
A class is unloaded only when its Class object is GC’d, which requires all instances to be collected and the class loader itself to be collected. JVM built-in class loaders never unload their classes.

JIT compilation

The JVM interprets bytecode initially. The JIT compiler identifies hot methods and compiles them to native machine code at runtime. This means long-running Java processes typically outperform freshly started ones because the JIT has had time to optimize hot paths.

Multithreading

Thread and Runnable

There are two basic ways to define work for a thread. Extending Thread is simpler but wastes Java’s single-inheritance slot:
Implementing Runnable is preferred because it keeps your class free to extend another class and makes it easy to share one Runnable across multiple threads:

ExecutorService and thread pools

Creating and destroying threads on demand is expensive. Use ExecutorService to maintain a pool of worker threads:
Thread pool parameters to know:
  • corePoolSize — threads kept alive even when idle.
  • maximumPoolSize — upper bound on thread count.
  • keepAliveTime — how long idle threads above corePoolSize survive before being terminated.

synchronized vs ReentrantLock

synchronized is an implicit lock tied to an object monitor. It releases automatically when the block exits, even on exception:
ReentrantLock is explicit and offers more control: try-lock, timed lock, and fairness settings. Always release in a finally block:
Priority ordering for choosing a lock: ReentrantLock > synchronized block > synchronized method.

CompletableFuture

CompletableFuture enables non-blocking async pipelines without explicit thread management:

Producer-consumer with wait / notifyAll

The classic bounded-buffer pattern uses wait and notifyAll on a shared monitor to coordinate producers and consumers:

Java 8+ Features

Optional

Optional<T> makes the possibility of a missing value explicit in the type system, eliminating entire classes of NullPointerException:

Stream API

Streams provide a declarative, functional-style pipeline for processing collections:

Functional interfaces and lambdas

Any interface with exactly one abstract method is a functional interface and can be expressed as a lambda:
Lambda expressions eliminate the need for anonymous inner classes and integrate naturally with the Stream API.

LocalDateTime

java.time.LocalDateTime replaces the problematic Date and Calendar APIs. It is immutable and thread-safe:

MyBatis-Plus

MyBatis-Plus enhances MyBatis with generic CRUD operations, a fluent query wrapper, and pagination—without requiring you to write SQL for common cases.

Setup

Add the dependencies in pom.xml:
Configure your datasource in application.properties:
Add @MapperScan to your main class:

Basic CRUD

Extend BaseMapper<T> to inherit full CRUD without writing SQL:
For custom queries, use annotations directly on the interface:

Conditional queries

The QueryWrapper lets you build type-safe WHERE clauses without string concatenation:

Pagination

Register the pagination interceptor once:
Then call selectPage:

Dynamic SQL with XML mappers

For complex queries, pair an XML mapper with the Java interface. Place the XML file in the same package as the mapper with the same name: