Article directory
1. Overview of direct memory
The following is the description of Java direct memory in section 2.2.7 of "In-depth Understanding of Java Virtual Machine, Third Edition".
Direct memory (Direct Memory) is not part of the virtual machine runtime data area, nor is it a memory area defined in the "Java Virtual Machine Specification" . But this part of memory is also frequently used, and may also cause
OutOfMemoryError
exceptions, so we put it here to explain it together. A newNIO(New Input/Output)
class , and an I/O method based on channels (Channel) and buffers (Buffer) was introduced. It can use the Native function library to directly allocate off-heap memory, and then store it in the Java heap through a TheDirectByteBuffer
object operates as a reference to this memory. This can significantly improve performance in some scenarios, because it avoids copying data back and forth between the Java heap and the Native heap. Obviously, the direct memory allocation of the machine will not be limited by the size of the Java heap, but since it is memory, it will definitely be limited by the total memory of the machine (including physical memory, SWAP partition or paging file) and the addressing space of the processor. Generally, when server administrators configure virtual machine parameters, they will-Xmx
set , but often ignore direct memory, so that the sum of each memory area is greater than the physical memory limit (including physical and operating system-level limits). , resulting inOutOfMemoryError
an exception .
Direct memory is often used for NIO operations, for data buffers.
However, the cost of direct memory allocation and recovery is high, but the read and write performance is high.
Finally, it is not subject to JVM memory recovery management
2. Direct memory usage
The following example uses two methods to copy files to another place
- Traditional Java buffer
- direct memory
static final String FROM = "D:\\BaiduNetdiskDownload\\《MYSQL内核:INNODB存储引擎 卷1》.zip";
static final String TO = "D:\\BaiduNetdiskDownload\\《MYSQL内核:INNODB存储引擎 卷1》(1).zip";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io();
directBuffer();
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
2.1 Java buffer
Java itself does not have the ability to read and write the disk. If you want to use the ability to read and write the disk, you must call the function provided by the operating system.
That is, the local method will be called internally, and the state of the CPU will be switched from user mode to kernel mode at the same time.
At the same time, the memory will also change accordingly. When switching to the kernel state, the disk file will be read into the system buffer first (read in batches). Java cannot use the system buffer, and Java will create a file in the heap memory. Java buffer ( byte[] buf = new byte[_1Mb]
), if Java wants to read the system buffer, it needs to indirectly read the data in the system buffer into the Java buffer, and Java can operate on the Java buffer.
The reason why using traditional IO is relatively inefficient is that the disk file needs to be read into the system buffer first, and then the system buffer is read into the Java buffer before Java can process the disk file, which causes unnecessary data copying. Efficiency is thus lower.
2.2 Direct memory
When executed ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
, the operating system will allocate a 1MB memory.
This memory Java code can be directly accessed, and the operating system can also be directly accessed. It is equivalent to a piece of memory shared by Java and the operating system.
At this time, the disk file can be read into the direct memory, and then the Java code can operate on the direct memory, that is, one less copy operation than traditional IO, so the efficiency is higher.
3. Release of direct memory
The following code allocates a block of 1G direct memory
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
System.in.read();
}
}
After the allocation is successful, you can see that the memory of the Java program is more than 1G in the task manager
Then byteBuffer
set to NULL
, start garbage collection
The memory of the Java program directly drops by about 1G, indicating that the direct memory has been released.
3.1 The principle of direct memory release
Didn't it say that direct memory is not managed by JVM memory recycling? Why is direct memory released after garbage collection?
Don't worry, let's introduce the release principle of direct memory first.
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);
System.in.read();
// 释放内存
unsafe.freeMemory(base);
System.in.read();
}
public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
Unsafe
It is the class used by the bottom layer of Java to allocate direct memory and release direct memory. However, it is generally not recommended to use this class.
Allocation of direct memory is achieved by the method Unsafe
of the class , and its return value is the address of the allocated memoryallocateMemory
The release of memory is achieved by the method Unsafe
of the class . freeMemory
This method needs to pass in the address of the memory that needs to be freed.
In other words, if you want to release direct memory, you need to actively call freeMemory
the method
Next, view ByteBuffer.allocateDirect(_1Gb)
the source code of
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
Its interior is new DirectByteBuffer(capacity)
, then look at this construction method
DirectByteBuffer(int cap) {
// package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
//使用Unsafe类分配直接内存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
You can see that the constructor is used unsafe.allocateMemory(size)
to allocate direct memory.
So when is the method called to release direct memory?
Need to pay attention to this line of code
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
where Deallocator
is a callback task object
private static class Deallocator
implements Runnable
{
private static Unsafe unsafe = Unsafe.getUnsafe();
private long address;
private long size;
private int capacity;
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}
public void run() {
if (address == 0) {
// Paranoia
return;
}
//释放直接内存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
Viewing its source code, you can find that it has been implemented Runnable
, and its run
method calls the method of releasing direct memoryunsafe.freeMemory(address);
After reading this, that is to say, if you want to release direct memory, you must call the method Deallocator
inrun
Then continue to say Clear
, Clear
in the Java class library is a special type, called the virtual reference type
When the object associated with the virtual reference is recycled, clean
the method of the virtual reference object will be triggered
private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}
public static Cleaner create(Object var0, Runnable var1) {
return var1 == null ? null : add(new Cleaner(var0, var1));
}
public void clean() {
if (remove(this)) {
try {
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
}
}
Looking at clean
the source code, you can find that Cleaner.create
when the method is executed, it will be new Deallocator(base, size, cap)
passed as a parameter Runnable var1
, and when create
the method is called new Cleaner(var0, var1)
, it will this.thunk
be assigned Runnable var1
.
That is to say, what is called in clean
the method is the method in the method. Thus freeing direct memory.this.thunk.run();
Deallocator
run
That is, ByteBuffer
inside the implementation class, (virtual reference) is used Cleaner
to monitor ByteBuffer
the object . Once ByteBuffer
the object is garbage collected, ReferenceHandler
the thread release the direct memory through the methodCleaner
call ofclean
freeMemory
4. Effect of disabling explicit recycling on direct memory
Explicit recycling can be disabled with the following parameter
-XX:+DisableExplicitGC 显式的
What is explicit recycling? The following is an example of explicit recycling
System.gc(); // 显式的垃圾回收,Full GC
After disabling explicit recycling, this line of code becomes invalid
This line of code is invalid and may directly affect the release of direct memory
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
System.in.read();
}
}
Also, as far as this example is concerned, System.gc();
garbage collection will not be triggered when the execution is reached.
Therefore, byteBuffer
although the object is NULL
, it will not be recycled, so the 1GB of direct memory previously requested will not be released.
This byteBuffer
can only be reclaimed when the real garbage collection is triggered, and the direct memory is also released.
The consequence of this is that the direct memory usage is relatively large.
The corresponding solution is to manually Unsafe
release the direct memory.