Ранее мы подробно обсуждалиАнализ Java NIO (8): подробное объяснение селектора ядра с высокой степенью параллелизмаа такжеАнализ Java NIO (9): от сокета BSD к SocketChannel, Они являются диспетчером событий NIO и неблокирующим процессором соответственно.
поддерживатьChannel
двунаправленное чтение и запись иScatter/Gather
операция, нам также нужноBuffer
, сохраните данные ввода-вывода для резервного копирования. Обычные буферы — это буферы в куче JVM, которые легче понять.
Далее поговорим о перипетиях использования JVM памяти вне кучи и о том, почему она была разработанаDirectBuffer
.
Прежде всего, мы должны начать с эпохи до JDK1.4 без NIO, В то время все использовалиSocketInputStream
а такжеSocketOutputStream
.
1. Как традиционный сокет Java отправляет и получает данные?
кSocketOutputStream
Например, диаграмма классов наследования выглядит следующим образом:
На самом деле, все должны быть более знакомы с ним.java.io
Сумка самая фундаментальная абстракцияInputStream
а такжеOutputStream
, остальная часть абстракции и реализации класса представляет собой кучудекоратор.
Итак, что мы собираемся отслеживать, этоwrite方法
// java.io.SocketOutputStream
public void write(byte b[], int off, int len) throws IOException {
// 委托给本类的socketWrite方法
socketWrite(b, off, len);
}
/**
* Writes to the socket with appropriate locking of the
* FileDescriptor.
* @param b the data to be written
* @param off the start offset in the data
* @param len the number of bytes that are written
* @exception IOException If an I/O error has occurred.
*/
private void socketWrite(byte b[], int off, int len) throws IOException {
...省略非关键代码
FileDescriptor fd = impl.acquireFD();
try {
// 上面就是做了一些参数判断,然后委托给socketWrite0
socketWrite0(fd, b, off, len);
} catch (SocketException se) {
...
} finally {
impl.releaseFD();
}
}
/**
* Writes to the socket.
* @param fd the FileDescriptor
* @param b the data to be written
* @param off the start offset in the data
* @param len the number of bytes that are written
* @exception IOException If an I/O error has occurred.
*/
private native void socketWrite0(FileDescriptor fd, byte[] b, int off,
int len) throws IOException;
Последний вызов является нативным методомsocketWrite0
,Открытьjdk/src/solaris/native/java/net/SocketOutputStream.c
/*
* Class: java_net_SocketOutputStream
* Method: socketWrite0
* Signature: (Ljava/io/FileDescriptor;[BII)V
*/
JNIEXPORT void JNICALL
Java_java_net_SocketOutputStream_socketWrite0(JNIEnv *env, jobject this,
jobject fdObj,
jbyteArray data,
jint off, jint len) {
char *bufP;
char BUF[MAX_BUFFER_LEN];
int buflen;
int fd;
if (IS_NULL(fdObj)) {
JNU_ThrowByName(env, "java/net/SocketException", "Socket closed");
return;
} else {
fd = (*env)->GetIntField(env, fdObj, IO_fd_fdID);
/* Bug 4086704 - If the Socket associated with this file descriptor
* was closed (sysCloseFD), the the file descriptor is set to -1.
*/
if (fd == -1) {
JNU_ThrowByName(env, "java/net/SocketException", "Socket closed");
return;
}
}
if (len <= MAX_BUFFER_LEN) {
bufP = BUF;
buflen = MAX_BUFFER_LEN;
} else {
buflen = min(MAX_HEAP_BUFFER_LEN, len);
// 初始化一块直接内存
bufP = (char *)malloc((size_t)buflen);
/* if heap exhausted resort to stack buffer */
if (bufP == NULL) {
bufP = BUF;
buflen = MAX_BUFFER_LEN;
}
}
while(len > 0) {
int loff = 0;
int chunkLen = min(buflen, len);
int llen = chunkLen;
// 将堆内的data复制到刚才初始化的内存bufP里
(*env)->GetByteArrayRegion(env, data, off, chunkLen, (jbyte *)bufP);
while(llen > 0) {
// 发送数据
int n = NET_Send(fd, bufP + loff, llen, 0);
if (n > 0) {
llen -= n;
loff += n;
continue;
}
if (n == JVM_IO_INTR) {
JNU_ThrowByName(env, "java/io/InterruptedIOException", 0);
} else {
if (errno == ECONNRESET) {
JNU_ThrowByName(env, "sun/net/ConnectionResetException",
"Connection reset");
} else {
NET_ThrowByNameWithLastError(env, "java/net/SocketException",
"Write failed");
}
}
if (bufP != BUF) {
free(bufP);
}
return;
}
len -= chunkLen;
off += chunkLen;
}
if (bufP != BUF) {
free(bufP);
}
}
Этот раздел относительно короткий, поэтому код не пропущен.
Грубо разделить на 3 шага:
- Подать заявку на кусок прямой памяти bufP, длина len
- Скопируйте данные из кучи в только что инициализированную память bufP.
- Вызов функции Net_send для отправки данных
Net_send
макрос, определенный вnet_util_md.h
внутри
// net_util_md.h
#define NET_Send JVM_Send
// jvm.cpp
JVM_LEAF(jint, JVM_Send(jint fd, char *buf, jint nBytes, jint flags))
JVMWrapper2("JVM_Send (0x%x)", fd);
//%note jvm_r6
return os::send(fd, buf, (size_t)nBytes, (uint)flags);
JVM_END
// os_linux.inline.hpp
inline int os::send(int fd, char* buf, size_t nBytes, uint flags) {
RESTARTABLE_RETURN_INT(::send(fd, buf, nBytes, flags));
}
Этот макрос заменяетJVM_Send
, это метод инкапсуляции cpp, который, наконец, вызоветos::send
, это вos_linux.inline.hpp
, вы можете видеть, что последний вызов является глобальной функциейsend
.
send
Это наш старый друг, он соответствует стандарту POSIX.Socket API
, если вы работаете в Unix-подобной системе, либо черезman send
Давайте посмотрим на его документацию. Его сигнатура функции выглядит следующим образом:
#include <sys/socket.h>
ssize_t
send(int socket, const void *buffer, size_t length, int flags);
Эта функция используется для отправки данных, первый адрес которых — буфер, а длина — длина доsocket fd
.
Следовательно, каждый раз, когда традиционное программирование Java Socket отправляет данные, оно будет обращаться к части прямой памяти (вне кучи), затем копировать ее из кучи за пределы кучи и, наконец, вызывать send для ее отправки.
Зачем копировать данные из кучи в кучу? потому чтоАдрес объекта в куче изменится с помощью gc, и при отправке произойдет сбой..
В сценариях с высоким уровнем параллелизма это очень интенсивно использует память. Если данные, отправляемые по каждой ссылке, составляют 1 КБ, то 1 КБ данных находится в куче, и 1 КБ данных необходимо применять за пределами кучи. Требуется 2 ТБ памяти ( экстремальные сценарии), что, несомненно, является одним из узких мест.
2. Решение NIO: DirectBuffer
Поскольку Socket Api должен использовать память за пределами кучи, одним из решений являетсяповторно использовать эту память, так чтоНет необходимости каждый раз запрашивать новую память, что снижает потери системных вызовов..
Компьютерный мир — это решение одной проблемы и создание новых., если эта память повторно используется, то как ее утилизировать, когда она не используется, является проблемой, потому что эта памятьПамять находится вне кучи, и сборщик мусора JVM не может ею управлять, оставьте его в покое и рано или поздно запустите LinuxOOM Killer
.
NIO после jdk1.4 предлагает решение:DirectBuffer
2.1 DirectBuffer
Объекты в куче JVM связаны с gc, затем связывают объекты в куче JVM с кучей и запускают операцию по освобождению памяти вне кучи, когда объекты в куче освобождаются, и эта проблема может быть решена.
Распространенной реализацией этой дизайнерской идеи являетсяDirectByteBuffer
, откройте его код
// Primary constructor
//
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));
// 主要是记录jdk已经使用的直接内存的数量,当分配直接内存时,需要进行增加,当释放时,需要减少
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 分配直接内存
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 = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
private ByteBuffer putShort(long a, short x) {
if (unaligned) {
short y = (x);
unsafe.putShort(a, (nativeByteOrder ? y : Bits.swap(y)));
} else {
Bits.putShort(a, x, bigEndian);
}
return this;
}
В конструкторе будет применена прямая память, а размерByteBuffer.allocateDirect
указано время.
вызовDirectByteBuffer
изPutXXX
Когда приходит время, оно записывается в эту прямую память, не нужно идти вmalloc
.
такSocketChannel
использоватьDirectBuffer
При чтении и записи производительность намного лучше, чемSocketOutputStream
высокий, не требует столько памяти, даSocketChannel
Если вы сомневаетесь, пожалуйста, обратитесь к предыдущему анализуАнализ Java NIO (9): от сокета BSD к SocketChannel
2.2 Когда восстанавливается прямая память?
Последняя строка приведенного выше конструктора должна использоватьCleaner
объект, этот класс является虚引用
класс, унаследованный отPhantomReference
.
// 创建Cleaner对象
cleaner = Cleaner.create(this,
new Deallocator(base, size, cap));
создать ссылку на первый параметр наблюдается, второй параметр ссылка операция срабатывает восстанавливается.
В JVM фантомные ссылки используются для реализацииСредства более тонкого управления памятью, вы должны передать виртуальную ссылку при создании виртуальной ссылкиЭталонная очередь (ReferenceQueue), после вызова функции финализации объекта будет добавлена виртуальная ссылка на объектэталонная очередь, Вы можете узнать, будет ли объект переработан, проверив очередь.
sun.misc.Cleaner
является автономнымReferenceQueue
класс вcreate
будетDirectBuffer
Ссылка добавляется к наблюдению.После повторного использования ссылки JVM уведомит Cleaner о выполнении операции повторного использования.
public class Cleaner
extends PhantomReference<Object>
{
// 引用队列
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
...
private Cleaner(Object referent, Runnable thunk) {
super(referent, dummyQueue);
this.thunk = thunk;
}
...
}
2.3 Как восстановить прямую память?
существуетDirectBuffer
Существует внутренний класс вDeallocator
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释放内存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
Вызовите его метод запуска при перезапуске, вы можете видеть, что он вызываетсяsun.misc.unsafe
освободить. Как упоминалось здесь, все небезопасные классы — это статические собственные методы, которые на самом деле представляют собой кучу различных часто используемых методов.free
,malloc
Инкапсуляция таких функций манипулирования памятью, потому что java не может манипулировать прямой памятью.
One more thing
структураDirectBuffer
, есть линияBits.reserveMemory
, Функция в основном состоит в том, чтобы записывать объем прямой памяти, которую использовал jdk, но она будет судить о том, достаточно ли использования прямой памяти, если памяти недостаточно, она запускает gc..
// These methods should be called whenever direct memory is allocated or
// freed. They allow the user to control the amount of direct memory
// which a process may access. All sizes are specified in bytes.
static void reserveMemory(long size, int cap) {
synchronized (Bits.class) {
// 判断内存够不够, 够的话直接增加内存引用的计数
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// -XX:MaxDirectMemorySize limits the total capacity rather than the
// actual memory usage, which will differ when buffers are page
// aligned.
if (cap <= maxMemory - totalCapacity) {
reservedMemory += size;
totalCapacity += cap;
count++;
return;
}
}
// 内存不够就去建议JVM gc了。。
System.gc();
try {
// 等待垃圾回收
Thread.sleep(100);
} catch (InterruptedException x) {
// Restore interrupt status
Thread.currentThread().interrupt();
}
synchronized (Bits.class) {
// 增加直接内存的计数
if (totalCapacity + cap > maxMemory)
throw new OutOfMemoryError("Direct buffer memory");
reservedMemory += size;
totalCapacity += cap;
count++;
}
}
3. Резюме
Эта статья изSocketStream
В начале я проанализировал дефекты памяти и узкие места производительности Socket в предковом BIO:
- Требуется для каждой отправки
malloc
Новый кусочек прямой памяти, - Чтобы скопировать данные из кучи в кучу, а затем отправить.
Затем вводятся улучшения NIO для памяти вне кучи, а именноПовторное использование памяти вне кучи, а также проблему утилизации памяти вне кучи, вызванную повторным использованием памяти вне кучи, и интерпретирует способы ее создания и повторного использования на уровне исходного кода. Улучшение NIO есть только для 1, а для 2 улучшения нет. Может быть, это одна из причин, почему java ест память.
DirectBuffer
наш обычный冰山对象
, Будьте осторожны при его использовании.Если объект находится в молодом поколении, его легко скомпилировать, и прямая память может быть освобождена при ее восстановлении. Однако, пережив несколько сборщиков мусора, он перемещается в старое поколение, и сборщик мусора не так прост. Связанная с ним прямая память также не освобождается.
Это тоже частая причина утечек памяти.Смотрите сколько памяти не используется в куче JVM, а на машине осталось мало памяти, частый gc и частые срабатыванияOOM Killer
, я надеюсь, что прочитав эту статью, вы сможете понять, в чем проблема :)