Java直接内存原理提到了SocketChannel#write的实现原理。
通过IOUtil#write将java堆内存拷贝到了直接内存,然后再把地址传给了I/O函数。
那么 BIO 是怎么实现往socket里面写数据的呢?
BIO
Socket#getOutputStream()获得SocketOutputStream
三个write方法最后都会调用native方法SocketOutputStream#socketWrite0
SocketOutputStream.c#Java_java_net_SocketOutputStream_socketWrite0方法
/*
* Class: java_net_SocketOutputStream
* Method: socketWrite
* 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;
/* 省略,入参校验*/
/*
* 尽可能使用堆栈分配缓冲区。
* 对于large sizes,我们从堆中分配一个中间缓冲区(达到最大值)。
* 如果堆不可用,我们只使用的堆栈缓冲区。
*/
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 (bufP == NULL) {
bufP = BUF;
buflen = MAX_BUFFER_LEN;
}
}
while(len > 0) {
int loff = 0;
int chunkLen = min(buflen, len);
int llen = chunkLen;
int retry = 0;
/*
* 这个方法复制了java数组到native堆中!!!
*/
(*env)->GetByteArrayRegion(env, data, off, chunkLen, (jbyte *)bufP);
/*
* 由于Windows套接字中的错误(在NT和Windows 2000上观察到),可能需要重试发送。
*/
while(llen > 0) {
/* 循环调用发送*/
int n = send(fd, bufP + loff, llen, 0);
if (n > 0) {
llen -= n;
loff += n;
continue;
}
/*
* 由于Windows套接字中的错误(在NT和Windows 2000上观察到),可能需要重试发送。
*/
if (WSAGetLastError() == WSAENOBUFS) {
/* 省略,失败重试机制*/
}
/*
* 发送失败 - 可能由关闭或写入错误引起。
*/
if (WSAGetLastError() == WSAENOTSOCK) {
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException", "Socket closed");
} else {
NET_ThrowCurrent(env, "socket write error");
}
/* 释放临时缓冲区内存*/
if (bufP != BUF) {
free(bufP);
}
return;
}
len -= chunkLen;
off += chunkLen;
}
/* 释放临时缓冲区内存*/
if (bufP != BUF) {
free(bufP);
}
}
jni.cpp宏定义
#define DEFINE_GETSCALARARRAYREGION(ElementTag,ElementType,Result, Tag) \
DT_VOID_RETURN_MARK_DECL(Get##Result##ArrayRegion);\
\
JNI_ENTRY(void, \
jni_Get##Result##ArrayRegion(JNIEnv *env, ElementType##Array array, jsize start, \
jsize len, ElementType *buf)) \
/* 省略,动态判断应该去调用byte、int等哪个方法;还有一些动态追踪的逻辑?*/
typeArrayOop src = typeArrayOop(JNIHandles::resolve_non_null(array)); \
if (start < 0 || len < 0 || ((unsigned int)start + (unsigned int)len > (unsigned int)src->length())) { \
THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException()); \
} else { \
if (len > 0) { \
int sc = TypeArrayKlass::cast(src->klass())->log2_element_size(); \
/* 内存拷贝*/
memcpy((u_char*) buf, \
(u_char*) src->Tag##_at_addr(start), \
len << sc); \
} \
} \
JNI_END
所以除了直接使用ByteBuffer#allocateDirect分配堆外内存之外,不管是BIO和NIO都需要将java堆内存拷贝到native堆(堆外内存)。
当然都不能避免从native堆拷贝到socket buffer(SO_RCVBUF和SO_SNDBUF)。