System.arraycopy() looks fast but hides GC costs. Learn why bulk operations kill latency and real solutions.
JOptimize Team
You think System.arraycopy() is fast. It's not.
System.arraycopy() is native code, yes. But copying 1GB of arrays triggers full GC pauses that destroy latency.
This happened in production. A "batch operation" doing bulk array copies every 100ms caused 500ms GC pauses. Users saw nothing for half a second. Hundreds of times per day.
Code looks innocent:
public void processBatch(byte[] data) { byte[] copy = new byte[data.length]; System.arraycopy(data, 0, copy, 0, data.length); process(copy); }
Calls this 1000 times per second on 1MB arrays = 1GB/sec allocated.
Young generation fills instantly. GC runs every 10ms. Each GC touches old generation. After 1 minute, full GC kicks in. 2 second pause.
JVM allocates new array → copies data → old copy becomes garbage.
If you do this in bulk (batch processing, message handlers, stream processing):
Actual production bug:
@Component public class MessageHandler { @KafkaListener(topics = "events") public void handle(byte[] payload) { // Bad: Copy every message byte[] copy = new byte[payload.length]; System.arraycopy(payload, 0, copy, 0, payload.length); process(copy); } }
Kafka: 50,000 messages/sec, 10KB each = 500MB/sec allocation.
Result: GC pause every 1 second. P99 latency = 1500ms (SLA was 100ms).
@Component public class MessageHandler { @KafkaListener(topics = "events") public void handle(byte[] payload) { // Good: No copy, work directly process(payload); } } private void process(byte[] data) { // Don't modify original, just read int sum = 0; for (byte b : data) { sum += b; } }
No allocation = no GC.
If you MUST copy (thread-unsafe data, modification needed):
public class ByteArrayPool { private final Queue<byte[]> pool = new ConcurrentLinkedQueue<>(); private final int bufferSize; public ByteArrayPool(int bufferSize, int poolSize) { this.bufferSize = bufferSize; for (int i = 0; i < poolSize; i++) { pool.offer(new byte[bufferSize]); } } public byte[] acquire() { byte[] buf = pool.poll(); return buf != null ? buf : new byte[bufferSize]; } public void release(byte[] buf) { Arrays.fill(buf, (byte) 0); // Reset pool.offer(buf); } } // Usage private final ByteArrayPool pool = new ByteArrayPool(10_000, 50); public void handle(byte[] payload) { byte[] copy = pool.acquire(); try { System.arraycopy(payload, 0, copy, 0, payload.length); process(copy); } finally { pool.release(copy); } }
Result: Fixed 50 arrays allocated, reused infinitely. Zero GC for copies.
For large, long-lived copies:
import java.nio.ByteBuffer; public class OffHeapCopy { private ByteBuffer directBuf = ByteBuffer.allocateDirect(10_000_000); // 10MB off-heap public void handle(byte[] payload) { directBuf.clear(); directBuf.put(payload); process(directBuf); // Work with ByteBuffer } private void process(ByteBuffer buf) { buf.flip(); while (buf.hasRemaining()) { byte b = buf.get(); // Process } } }
Off-heap = GC never touches it. No pauses for this allocation.
Downside: manual cleanup required, slight performance overhead vs on-heap.
Don't copy until you know you need it:
public class LazyByteArrayCopy { private final byte[] original; private volatile byte[] copy = null; public LazyByteArrayCopy(byte[] data) { this.original = data; } public byte[] get() { return original; // Return original until modified } public void modify(int index, byte value) { if (copy == null) { synchronized (this) { if (copy == null) { copy = new byte[original.length]; System.arraycopy(original, 0, copy, 0, original.length); } } } copy[index] = value; } }
Only copy on first modification. Most reads = no allocation.
Throughput: 10,000 req/sec P50 latency: 5ms P99 latency: 500ms GC pause time: 1-3 seconds GC frequency: Every 1-2 seconds
Throughput: 50,000 req/sec P50 latency: 1ms P99 latency: 50ms GC pause time: <10ms (young gen only) GC frequency: Every 30 seconds
Array copy looks like a micro-optimization. It's not.
One bad pattern:
-XX:+PrintAllocationRateSystem.arraycopy() itself is fast. But bulk allocation on hot paths kills GC.
Work in-place when possible. Pool when you must copy. Monitor GC under real load.
One "innocent" array copy at scale = multi-second pause.
Optimize with JOptimize PRO. Use code LINKEDIN40 for 40% OFF.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.