问题描述
我的代码在运行以下行时抛出 OutOfMemoryError:
int numBytes = socketChannel.write(_send_buffer);
其中 socketChannel
是 java.nio.channels.SocketChannel 的实例
和 _send_buffer
是 java.nio.ByteBuffer
代码通过非阻塞选择器写操作到达这一点,并在 _send_buffer
的容量很大时在第一次尝试写时抛出这个。当 _send_buffer
小于 20Mb 时,我对代码没有任何问题,但是当尝试使用更大的缓冲区(例如 > 100Mb)进行测试时,它会失败。
根据 java.nio.channels.SocketChannel.write() 的文档:
尝试将最多 r 个字节写入通道,其中 r 是缓冲区中剩余的字节数,即 src.remaining(),此时调用此方法。 假设写入一个长度为 n 的字节序列,其中 0 除非另有说明,写操作只有在写完所有 r 个请求的字节后才会返回。某些类型的通道,取决于它们的状态,可能只写入一些字节或可能根本不写入。例如,处于非阻塞模式的套接字通道不能写入比套接字输出缓冲区中可用的字节多的字节。
我的通道应该设置为非阻塞,所以我认为写操作应该只尝试写到套接字输出缓冲区的容量。由于我之前没有指定这一点,我尝试通过带有 setOption 选项的 SO_SNDBUF 方法将其设置为 1024 字节。即:
socketChannel.setOption(SO_SNDBUF,1024);
虽然我仍然收到 OutOfMemoryError。这是完整的错误消息:
2021-04-22 11:52:44.260 11591-11733/jp.oist.abcvlib.serverLearning I/.serverLearnin: Clamp target GC heap from 195MB to 192MB
2021-04-22 11:52:44.260 11591-11733/jp.oist.abcvlib.serverLearning I/.serverLearnin: Alloc concurrent copying GC freed 2508(64KB) AllocSpace objects,0(0B) LOS objects,10% free,171MB/192MB,paused 27us total 12.714ms
2021-04-22 11:52:44.261 11591-11733/jp.oist.abcvlib.serverLearning W/.serverLearnin: Throwing OutOfMemoryError "Failed to allocate a 49915610 byte allocation with 21279560 free bytes and 20MB until OOM,target footprint 201326592,growth limit 201326592" (VmSize 5585608 kB)
2021-04-22 11:52:44.261 11591-11733/jp.oist.abcvlib.serverLearning I/.serverLearnin: Starting a blocking GC Alloc
2021-04-22 11:52:44.261 11591-11733/jp.oist.abcvlib.serverLearning I/.serverLearnin: Starting a blocking GC Alloc
现在我可以内联调试并在写入行停止并且没有任何崩溃,所以我相信处理 _send_buffer
本身的内存要求没有问题,但是在尝试写入时,后台正在创建一些东西另一个难以处理的分配。
也许我在考虑这个错误,需要将我的 _send_buffer
大小限制为更小,但我认为应该有一种方法来限制 write 命令进行的分配 no?或者至少以某种方式为我的应用程序分配更多的 Android 内存。我使用的是 Pixel 3a,根据 specs 它应该有 4GB 的内存。现在我意识到必须与系统的其余部分共享,但这是一个简单的测试设备(没有安装游戏、个人应用程序等)所以我认为我应该可以访问相当大的一块那4GB。当我以 201,326,592 的增长限制(根据上面的 logcat)崩溃时,我在 0.2 / 4.0 = 规范内存的 5% 时崩溃对我来说似乎很奇怪。
任何有关我方法中基本缺陷的正确方向的提示,或避免 OutOfMemoryError 的建议,将不胜感激!
编辑 1:
根据评论的要求添加一些代码上下文。请注意,这不是一个可运行的示例,因为代码库非常大,而且由于公司政策,我不允许将其全部共享。请注意,_send_buffer
与 socketChannel 本身的发送缓冲区无关(即 getSendBufferSize 引用的内容,它只是一个 ByteBuffer,我用来在通过通道。由于我无法分享与生成 _send_buffer
的内容相关的所有代码,请注意它是一个 ByteBuffer,可能非常大(> 100Mb)。如果这从根本上是一个问题,那么请指出这一点以及为什么。
所以考虑到以上几点,下面贴出NIO相关的代码。请注意,这是非常原型的 alpha 代码,因此对于评论和日志语句的过载,我深表歉意。
SocketConnectionManager.java
(本质上是一个负责 Selector 的 Runnable)
请注意,sendMsgToServer
方法已被覆盖(未经修改)并从主 Android 活动(未显示)调用。 byte[] episode
参数被包装到 ByteBuffer
(下一节)中的 SocketMessage.java
中,然后被放入 _send_buffer
方法中的 write
实例中{1}}。
SocketMessage.java
SocketMessage.java
这受到了给定 here 的示例 Python 代码的极大启发,特别是 package jp.oist.abcvlib.util;
import android.util.Log;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketOption;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.IllegalBlockingModeException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Set;
import static java.net.StandardSocketOptions.SO_SNDBUF;
public class SocketConnectionManager implements Runnable{
private SocketChannel sc;
private Selector selector;
private SocketListener socketListener;
private final String TAG = "SocketConnectionManager";
private SocketMessage socketMessage;
private final String serverIp;
private final int serverPort;
public SocketConnectionManager(SocketListener socketListener,String serverIp,int serverPort){
this.socketListener = socketListener;
this.serverIp = serverIp;
this.serverPort = serverPort;
}
@Override
public void run() {
try {
selector = Selector.open();
start_connection(serverIp,serverPort);
do {
int eventCount = selector.select(0);
Set<SelectionKey> events = selector.selectedKeys(); // events is int representing how many keys have changed state
if (eventCount != 0){
Set<SelectionKey> selectedKeys = selector.selectedKeys();
for (SelectionKey selectedKey : selectedKeys){
try{
SocketMessage socketMessage = (SocketMessage) selectedKey.attachment();
socketMessage.process_events(selectedKey);
}catch (ClassCastException e){
Log.e(TAG,"Error",e);
Log.e(TAG,"selectedKey attachment not a SocketMessage type");
}
}
}
} while (selector.isOpen()); //todo remember to close the selector somewhere
} catch (IOException e) {
Log.e(TAG,e);
}
}
private void start_connection(String serverIp,int serverPort){
try {
InetSocketAddress inetSocketAddress = new InetSocketAddress(serverIp,serverPort);
sc = SocketChannel.open();
sc.configureBlocking(false);
sc.setOption(SO_SNDBUF,1024);
socketMessage = new SocketMessage(socketListener,sc,selector);
Log.v(TAG,"registering with selector to connect");
int ops = SelectionKey.OP_CONNECT;
sc.register(selector,ops,socketMessage);
Log.d(TAG,"Initializing connection with " + inetSocketAddress);
boolean connected = sc.connect(inetSocketAddress);
Log.v(TAG,"socketChannel.isConnected ? : " + sc.isConnected());
} catch (IOException | ClosedSelectorException | IllegalBlockingModeException
| CancelledKeyException | IllegalArgumentException e) {
Log.e(TAG,"Initial socket connect and registration:",e);
}
}
public void sendMsgToServer(byte[] episode){
boolean writeSuccess = socketMessage.addEpisodeToWriteBuffer(episode);
}
/**
* Should be called prior to exiting app to ensure zombie threads don't remain in memory.
*/
public void close(){
try {
Log.v(TAG,"Closing connection: " + sc.getRemoteAddress());
selector.close();
sc.close();
} catch (IOException e) {
Log.e(TAG,e);
}
}
}
和 libclient.py
。这是因为服务器正在运行 python 代码而客户端正在运行 Java。因此,如果您想了解事情为什么会如此,请参考 RealPython 套接字教程。我基本上使用 app-server.py 作为我的代码模板,并为客户端翻译(经过修改)为 Java。
app-client.py
解决方法
偶然发现了 Android 文档中的 this,它回答了为什么我会遇到 OutOfMemoryError 的问题。
为了维护功能强大的多任务环境,Android 对每个应用的堆大小设置了硬性限制。确切的堆大小限制因设备的总体可用 RAM 量而异。如果您的应用已达到堆容量并尝试分配更多内存,则可能会收到 OutOfMemoryError。
在某些情况下,您可能希望查询系统以确定当前设备上有多少可用的堆空间,例如,确定可以安全地保存在缓存中的数据量。您可以通过调用 getMemoryClass() 向系统查询此图。此方法返回一个整数,指示可用于应用堆的兆字节数。
运行 ActivityManager.getMemoryClass 方法后,我发现我的 Pixel 3a 有 192 MB 的硬限制。当我试图分配超过 200 MB 的空间时,我达到了这个限制。
我还检查了 ActivityManager.getLargeMemoryClass,发现我有 512 MB 的硬限制。因此,我可以将我的应用设置为具有“largeHeap”,但尽管有 4GB 的 RAM,但我需要解决 512 MB 的硬限制。
除非其他人对此有所了解,否则我将不得不编写一些逻辑来将 episode
分段写入文件(如果它超过某个点),并稍后通过通道分段发送。我猜这会减慢速度,所以如果有人有可以避免这种情况的答案,或者告诉我为什么如果做得好这不会减慢速度,那么我很乐意为您提供答案。只是将其作为答案发布,因为它确实回答了我原来的问题,但并不令人满意。