codeworking.org
Search
Course Syllabus (Lesson 03 of 03)
Java Software Engineering • Module 01 25 min

Primitive Types, Stack/Heap Memory Models & Value Semantics

The Silicon Layer of the JVM: While Java is celebrated as a high-level, object-oriented language, its ultimate execution relies on the physical silicon architecture of the CPU and RAM. To write high-throughput, low-latency Java applications, an engineer must understand how data types are represented in raw binary bits and how the JVM partitions memory between the Thread Execution Stack and the Garbage-Collected Heap.


1. The 8 Java Primitive Types

Java is a strongly-typed language with exactly 8 built-in primitive data types. Unlike objects, primitives store their raw binary values directly without object headers, garbage collection overhead, or pointer dereferences.

Primitive Type Size in Bits Size in BytesRange of Values Default ValueInternal Representation
byte 8 bits 1 byte-128 to 127 08-bit Two’s Complement
short 16 bits 2 bytes-32,768 to 32,767 016-bit Two’s Complement
int 32 bits 4 bytes-2,147,483,648 to 2,147,483,647 (-2³¹ to 2³¹-1) 032-bit Two’s Complement
long 64 bits 8 bytes-2⁶³ to 2⁶³-1 (Append L suffix: 42L) 0L64-bit Two’s Complement
float 32 bits 4 bytes~1.4E-45 to ~3.4E+38 (Append f suffix: 3.14f) 0.0fIEEE-754 Single-Precision
double 64 bits 8 bytes~4.9E-324 to ~1.7E+308 (Default decimal) 0.0dIEEE-754 Double-Precision
char 16 bits 2 bytes\u0000 (0) to \uffff (65,535) \u000016-bit Unicode UTF-16 Code Unit
boolean JVM-dependent 1 byte (array)true or false falseInteger 1 or 0 in JVM opcodes
// Literal declarations and type casting
byte age = 28;
short port = 8080;
int userCount = 1_000_000;       // Numeric underscores improve readability
long transactionId = 9876543210L; // Required 'L' suffix for 64-bit literals
float interestRate = 0.0575f;    // Required 'f' suffix for 32-bit float
double accountBalance = 12500.50; // Standard 64-bit floating point
char grade = 'A';                // Single quotes for 16-bit char
boolean isActive = true;

2. Integer Representation: Two’s Complement Storage

All signed integer primitives in Java (byte, short, int, long) are stored in hardware memory using Two’s Complement binary representation:

32-Bit Integer (4 Bytes):
┌──┬──┬──┬──┬──┬──┬──┬──┬─────────────────────────────┬──┐
│S │B │B │B │B │B │B │B │  ... 24 Intermediate Bits ... │B │
└──┴──┴──┴──┴──┴──┴──┴──┴─────────────────────────────┴──┘
Bit 31 (Sign Bit: 0 = Positive, 1 = Negative)        Bit 0 (LSB)

The Two’s Complement Algorithm:

To compute the binary representation of a negative number:

  1. Start with the positive binary representation.
  2. Invert all bits (0 ➔ 1, 1 ➔ 0).
  3. Add 1 to the lowest bit.
Example: Representing -5 as an 8-bit byte:
1. Positive +5:  0000 0101
2. Invert bits:  1111 1010  (One's complement)
3. Add 1:        1111 1011  (Two's complement = -5)

Value Calculation: (-128) + 64 + 32 + 16 + 8 + 0 + 2 + 1 = -5

Arithmetic Overflow Behavior:

When an integer exceeds its maximum capacity, Java does not throw an exception; it silently wraps around using modular binary arithmetic:

int max = Integer.MAX_VALUE; //  2,147,483,647 (01111111 11111111 11111111 11111111)
int overflow = max + 1;      // -2,147,483,648 (10000000 00000000 00000000 00000000)

// Safe arithmetic with Math.addExact() in production:
int safe = Math.addExact(max, 1); // Throws ArithmeticException: integer overflow

3. The JVM Memory Architecture: Stack vs Heap

The Java Virtual Machine divides runtime memory into two primary structural regions: the Call Stack and the Garbage-Collected Heap.

The Thread Stack (L1 Fast Memory)
• Dedicated to a single OS thread (private to that thread).
• Allocates Stack Frames per function invocation.
• Stores primitive local variables directly inside the frame.
• Automatic deallocation when the function returns (instant, 0 GC overhead).
The Managed Heap (Shared Object Space)
• Shared across all application threads.
• Stores all new Object() instances and arrays.
• Variables on the stack hold 64-bit object reference pointers into the heap.
• Managed asynchronously by the JVM Garbage Collector.
JVM RUNTIME MEMORY TOPOLOGY:

THREAD EXECUTION STACK (Fast Frame Memory)       SHARED GARBAGE-COLLECTED HEAP
┌─────────────────────────────────────────┐     ┌───────────────────────────────────┐
│ Stack Frame: processPayment()           │     │ Heap Object: Order                │
│ ├── int orderId = 1042                  │     │ ├── Class Metadata Pointer (8B)   │
│ ├── double amount = 99.50               │     │ ├── Mark Word Header (8B)         │
│ └── Order orderRef ────────────(Pointer)─────►│ ├── int id = 1042                 │
│                                         │     │ └── String status = "PAID"        │
├─────────────────────────────────────────┤     └───────────────────────────────────┘
│ Stack Frame: main()                     │
└─────────────────────────────────────────┘

4. Primitive Types vs Object Wrappers (The Memory Penalty)

Java provides boxed wrapper classes (Byte, Short, Integer, Long, Float, Double, Character, Boolean) for object-oriented collections (like List<Integer>).

However, boxing carries a severe memory and cache latency penalty:

// Primitive: Stored directly on the Thread Stack
int primitiveVal = 42; // Takes exactly 4 Bytes of memory

// Boxed Wrapper: Allocated on the Heap with Object Header
Integer boxedVal = Integer.valueOf(42); 

Memory Footprint Breakdown on a 64-bit JVM:

  • Raw Primitive int: Exactly 4 bytes.
  • Boxed Integer Object in Heap:
    • Mark Word Header: 8 bytes (locking, identity hash, GC age).
    • Klass Word Pointer: 4 or 8 bytes (pointer to java.lang.Integer class metadata).
    • Primitive Payload (int value): 4 bytes.
    • 8-Byte Alignment Padding: 4 bytes.
    • Stack Reference Pointer: 8 bytes.
    • Total Footprint: 24 to 32 bytes (up to 6x to 8x more memory per integer!).
Array of 10,000,000 Primitives (int[]):
[ 4B ][ 4B ][ 4B ][ 4B ] ... ➔ ~40 MB in RAM (Contiguous, perfect L1 cache streaming)

Array of 10,000,000 Boxed Integers (Integer[]):
[ Ptr ] ➔ Heap Object (24B)  ➔ ~280 MB in RAM (Scattered pointers, massive cache misses)

5. Java’s Strict Pass-By-Value Semantics

A frequent point of confusion among developers is parameter passing in Java.

⚠️ The Golden Rule: Java is STRICTLY Pass-By-Value. Java never passes by reference.

1. Passing Primitives (Copying the Value)

When you pass a primitive variable to a method, Java creates an independent copy of the value on the new method’s stack frame. Modifying it inside the method has zero effect on the caller:

public class PassByValueDemo {
    public static void modifyPrimitive(int x) {
        x = 999; // Modifies the local stack frame copy only
    }

    public static void main(String[] args) {
        int original = 10;
        modifyPrimitive(original);
        System.out.println(original); // Outputs 10 (unchanged)
    }
}

2. Passing Object References (Copying the Pointer Address)

When you pass an object to a method, Java copies the memory address pointer by value. Both the caller and the callee hold copies of the pointer pointing to the exact same heap memory location:

public class ObjectReferenceDemo {
    static class Account {
        double balance = 100.0;
    }

    public static void updateBalance(Account acc) {
        acc.balance = 500.0; // Mutates the shared heap object!
    }

    public static void reassignReference(Account acc) {
        acc = new Account(); // Reassigns the local pointer copy only!
        acc.balance = 9999.0;
    }

    public static void main(String[] args) {
        Account myAccount = new Account();
        
        updateBalance(myAccount);
        System.out.println(myAccount.balance); // Outputs 500.0 (heap state was mutated)
        
        reassignReference(myAccount);
        System.out.println(myAccount.balance); // Still outputs 500.0 (original pointer untouched)
    }
}

6. Operators & Bitwise Manipulation

Java supports standard arithmetic, relational, and logical operators, along with high-performance low-level bitwise operators:

int a = 0b0000_1100; // 12 in binary
int b = 0b0000_1010; // 10 in binary

// Bitwise AND (&): 1 only if both bits are 1
int andResult = a & b; // 0b0000_1000 = 8

// Bitwise OR (|): 1 if either bit is 1
int orResult = a | b;  // 0b0000_1110 = 14

// Bitwise XOR (^): 1 if bits differ
int xorResult = a ^ b; // 0b0000_0110 = 6

// Bitwise NOT (~): Inverts all bits
int notResult = ~a;    // -13 in two's complement

// Bit Shift Left (<<): Multiplies by powers of 2
int shiftLeft = a << 2; // 12 * 4 = 48

// Signed Bit Shift Right (>>): Preserves sign bit
int shiftRight = a >> 1; // 12 / 2 = 6

// Unsigned Bit Shift Right (>>>): Shifts zeroes into MSB (ignores sign)
int unsignedShift = (-12) >>> 1; // 2,147,483,642

7. Key Takeaways & Architectural Summary

💡 Core Java Memory Principles
  • 8 Primitives: Stored directly without object overhead. Prefer primitives over boxed wrappers in performance-critical loops and arrays.
  • Two's Complement: Governs integer storage and modular arithmetic overflow. Use Math.addExact() for overflow protection.
  • Stack vs Heap: Thread execution stack handles fast, frame-scoped primitive values; shared Heap stores all managed object instances.
  • Strictly Pass-By-Value: Java always copies values. For primitives, it copies the raw data; for objects, it copies the 64-bit reference address.
S

Computer Science educator, Software Engineer, Cloud Computing & Cloud Native Architect, and AI/ML Engineer. Founder & Owner of unus.one, softwork.ing, and codeworking.org.

Comments & Discussion