AIO寫文件的OutOfMemoryError

問題重現(xiàn)

AIO進行寫文件使用了AsynchronousFileChannel類來實現(xiàn),測試代碼如下:

public class AsynchronousFileChannelTest {
    private static final String outputPath = "output.txt";
    private static String data = "你好";

    public static void main(String[] args) throws IOException {
        Path path = Paths.get(outputPath);
        if (!Files.exists(path)) {
            Files.createFile(path);
        }
        AsynchronousFileChannel fileChannel =
                AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        long position = 0;

        buffer.put(data.getBytes());
        buffer.flip();

        for (int i = 0; i < 10000000; i++) {
            fileChannel.write(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {

                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("bytes written: " + result);
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("Write failed");
                    exc.printStackTrace();
                }
            });
            position += data.getBytes().length;
        }

    }
}

執(zhí)行結(jié)果如下:

java.lang.OutOfMemoryError: unable to create new native thread
    at java.lang.Thread.start0(Native Method)
    at java.lang.Thread.start(Thread.java:714)
    at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:950)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1368)
    at sun.nio.ch.Invoker.invokeIndirectly(Invoker.java:236)
    at sun.nio.ch.SimpleAsynchronousFileChannelImpl.implWrite(SimpleAsynchronousFileChannelImpl.java:359)
    at sun.nio.ch.AsynchronousFileChannelImpl.write(AsynchronousFileChannelImpl.java:251)
    at cn.ideabuffer.interview.test.io.AsynchronousFileChannelTest.main(AsynchronousFileChannelTest.java:34)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

可見,該問題是內(nèi)存溢出,不能創(chuàng)建新的線程。

查看原因

那么,為什么會創(chuàng)建這么多的線程呢?

我們先來看一下AsynchronousFileChannelImpl類的write方法:

public final <A> void write(ByteBuffer var1, long var2, A var4, CompletionHandler<Integer, ? super A> var5) {
    if(var5 == null) {
        throw new NullPointerException("\'handler\' is null");
    } else {
        this.implWrite(var1, var2, var4, var5);
    }
}

這里調(diào)用了implWrite方法,implWrite方法是在SimpleAsynchronousFileChannelImpl類中定義的,下面來看一下SimpleAsynchronousFileChannelImpl類的implWrite方法:注意:因為我是在Mac OS上進行測試,windows下是沒有SimpleAsynchronousFileChannelImpl類的

<A> Future<Integer> implWrite(final ByteBuffer var1, final long var2, final A var4, final CompletionHandler<Integer, ? super A> var5) {
    if(var2 < 0L) {
        throw new IllegalArgumentException("Negative position");
    } else if(!this.writing) {
        throw new NonWritableChannelException();
    } else if(this.isOpen() && var1.remaining() != 0) {
        final PendingFuture var8 = var5 == null?new PendingFuture(this):null;
        Runnable var7 = new Runnable() {
            public void run() {
                // 省略一些代碼
                ...

            }
        };
        this.executor.execute(var7);
        return var8;
    } else {
        ClosedChannelException var6 = this.isOpen()?null:new ClosedChannelException();
        if(var5 == null) {
            return CompletedFuture.withResult(Integer.valueOf(0), var6);
        } else {
            Invoker.invokeIndirectly(var5, var4, Integer.valueOf(0), var6, this.executor);
            return null;
        }
    }
}

看一下第15行和第22行,這里都使用了executor來執(zhí)行具體的寫操作,而executor是在哪里定義的呢?

由于創(chuàng)建AsynchronousFileChannel對象的時候是如下代碼:

AsynchronousFileChannel fileChannel =
                AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);

AsynchronousFileChannel的open方法定義如下:

public static AsynchronousFileChannel open(Path file, OpenOption... options)
        throws IOException
{
    Set<OpenOption> set = new HashSet<OpenOption>(options.length);
    Collections.addAll(set, options);
    return open(file, set, null, NO_ATTRIBUTES);
}

這里調(diào)用了重載的open方法,注意第三個參數(shù)為null,該參數(shù)的類型就是ExecutorService,查看該方法:

public static AsynchronousFileChannel open(Path file,
                                               Set<? extends OpenOption> options,
                                               ExecutorService executor,
                                               FileAttribute<?>... attrs)
        throws IOException
{
    FileSystemProvider provider = file.getFileSystem().provider();
    return provider.newAsynchronousFileChannel(file, options, executor, attrs);
}

這里的provider是UnixFileSystemProvider,查看該類的newAsynchronousFileChannel方法:

public AsynchronousFileChannel newAsynchronousFileChannel(Path var1, Set<? extends OpenOption> var2, ExecutorService var3, FileAttribute... var4) throws IOException {
    UnixPath var5 = this.checkPath(var1);
    int var6 = UnixFileModeAttribute.toUnixMode(438, var4);
    ThreadPool var7 = var3 == null?null:ThreadPool.wrap(var3, 0);

    try {
        return UnixChannelFactory.newAsynchronousFileChannel(var5, var2, var6, var7);
    } catch (UnixException var9) {
        var9.rethrowAsIOException(var5);
        return null;
    }
}

調(diào)用了UnixChannelFactory的newAsynchronousFileChannel方法,該方法代碼如下:

static AsynchronousFileChannel newAsynchronousFileChannel(UnixPath var0, Set<? extends OpenOption> var1, int var2, ThreadPool var3) throws UnixException {
    UnixChannelFactory.Flags var4 = UnixChannelFactory.Flags.toFlags(var1);
    if(!var4.read && !var4.write) {
        var4.read = true;
    }

    if(var4.append) {
        throw new UnsupportedOperationException("APPEND not allowed");
    } else {
        FileDescriptor var5 = open(-1, var0, (String)null, var4, var2);
        return SimpleAsynchronousFileChannelImpl.open(var5, var4.read, var4.write, var3);
    }
}

這里就用到了SimpleAsynchronousFileChannelImpl的open方法:

public static AsynchronousFileChannel open(FileDescriptor var0, boolean var1, boolean var2, ThreadPool var3) {
    ExecutorService var4 = var3 == null?SimpleAsynchronousFileChannelImpl.DefaultExecutorHolder.defaultExecutor:var3.executor();
    return new SimpleAsynchronousFileChannelImpl(var0, var1, var2, var4);
}

可以看到,這里的ExecutorService對象使用了DefaultExecutorHolder中的defaultExecutor:

private static class DefaultExecutorHolder {
    static final ExecutorService defaultExecutor = ThreadPool.createDefault().executor();

    private DefaultExecutorHolder() {
    }
}

再看一下ThreadPool的createDefault方法:

static ThreadPool createDefault() {
    int var0 = getDefaultThreadPoolInitialSize();
    if(var0 < 0) {
        var0 = Runtime.getRuntime().availableProcessors();
    }

    ThreadFactory var1 = getDefaultThreadPoolThreadFactory();
    if(var1 == null) {
        var1 = defaultThreadFactory();
    }

    // 創(chuàng)建executor
    ExecutorService var2 = Executors.newCachedThreadPool(var1);
    return new ThreadPool(var2, false, var0);
}

可以看到,這里默認創(chuàng)建了一個CachedThreadPool,在newCachedThreadPool方法中使用了SynchronousQueue作為任務隊列:

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}

這里注意第二個參數(shù),第二個參數(shù)是設置線程池最大的任務數(shù)量,有關線程池請參考之前的文章深入理解Java線程池:ThreadPoolExecutor

也就是說,這里的任務數(shù)量是沒有限制的,而SynchronousQueue這個隊列比較特殊,它是一個沒有數(shù)據(jù)緩沖的BlockingQueue(隊列只能存儲一個元素),生產(chǎn)者線程對其的插入操作put必須等待消費者的移除操作take,反過來也一樣,消費者移除數(shù)據(jù)操作必須等待生產(chǎn)者的插入。

不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue內(nèi)部并沒有數(shù)據(jù)緩存空間,你不能調(diào)用peek()方法來看隊列中是否有數(shù)據(jù)元素,因為數(shù)據(jù)元素只有當你試著取走的時候才可能存在,不取走而只想偷窺一下是不行的,當然遍歷這個隊列的操作也是不允許的。隊列頭元素是第一個排隊要插入數(shù)據(jù)的線程,而不是要交換的數(shù)據(jù)。數(shù)據(jù)是在配對的生產(chǎn)者和消費者線程之間直接傳遞的,并不會將數(shù)據(jù)緩沖數(shù)據(jù)到隊列中??梢赃@樣來理解:生產(chǎn)者和消費者互相等待對方,握手,然后一起離開。

根據(jù)我們的測試代碼來看,寫文件的時候會向executor中添加一個線程作為任務來執(zhí)行,而這時如果磁盤的寫速度太慢,而程序在不停地進行寫任務的添加,這會導致隊列中的對象越來越多,而隊列中的對象就是Runnable對象,也就是線程對象??梢栽趫箦e信息中看到,異常是在Invoker類中:

static <V, A> void invokeIndirectly(final CompletionHandler<V, ? super A> var0, final A var1, final V var2, final Throwable var3, Executor var4) {
    try {
        var4.execute(new Runnable() {
            public void run() {
                Invoker.invokeUnchecked(var0, var1, var2, var3);
            }
        });
    } catch (RejectedExecutionException var6) {
        throw new ShutdownChannelGroupException();
    }
}

這里執(zhí)行的時候會創(chuàng)建一個線程對象,在調(diào)用了execute方法之后,會調(diào)用線程池中的addWorker方法添加任務:

private boolean addWorker(Runnable firstTask, boolean core) {
    
    ...
    
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                ...            
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

在添加任務完成后,會調(diào)用start方法來啟動線程。

所以,在磁盤寫速度比較慢的時候,不停地向線程池中添加線程對象并啟動線程,而且隊列的大小沒有限制。

但這個異常并不是堆內(nèi)存的溢出,堆內(nèi)存的溢出如下:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

問題分析

那么,究竟為什么會報不能創(chuàng)建線程的異常呢?

我們先把內(nèi)存按區(qū)域進行以下分類:

  • MaxProcessMemory:指的是一個進程的最大內(nèi)存
  • JVMMemory:JVM內(nèi)存
  • ReservedOsMemory:保留的操作系統(tǒng)內(nèi)存
  • ThreadStackSize:線程棧的大小

在java語言里, 當你創(chuàng)建一個線程的時候,虛擬機會在JVM內(nèi)存創(chuàng)建一個Thread對象同時創(chuàng)建一個操作系統(tǒng)線程,而這個系統(tǒng)線程的內(nèi)存用的不是JVMMemory,而是系統(tǒng)中剩下的內(nèi)存(MaxProcessMemory - JVMMemory - ReservedOsMemory)。

具體計算公式如下:

(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads 

我們測一下如下代碼:

public class TestNativeOutOfMemoryError {

    public static void main(String[] args) {

        for (int i = 0;; i++) {
            System.out.println("i = " + i);
            new Thread(new HoldThread()).start();
        }
    }

}

class HoldThread extends Thread {
    CountDownLatch cdl = new CountDownLatch(1);

    public HoldThread() {
        this.setDaemon(true);
    }

    public void run() {
        try {
            cdl.await();
        } catch (InterruptedException e) {
        }
    }
}

該代碼不停地創(chuàng)建線程,看下結(jié)果:

i = 4072
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

最終停在了4072,也就是創(chuàng)建了4073個線程后報OOM。

查看一下系統(tǒng)的線程數(shù)量限制:

sangjiandeMBP:~ sangjian$ sysctl kern.num_taskthreads
kern.num_taskthreads: 4096

可見,系統(tǒng)的線程數(shù)量限制為4096,從這個數(shù)量來說,和我們運行的結(jié)果是一致的。

所以,第一個異常Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread并不一定代表是系統(tǒng)內(nèi)存不足導致的溢出,也可能是創(chuàng)建的線程數(shù)量達到了系統(tǒng)的限制。

解決問題

  1. 如果程序中有bug,導致創(chuàng)建大量不需要的線程或者線程沒有及時回收,那么必須解決這個bug,修改參數(shù)是不能解決問題的;

  2. 如果程序確實需要大量的線程,現(xiàn)有的設置不能達到要求,那么可以通過修改MaxProcessMemory,JVMMemory,ThreadStackSize這三個因素,來增加能創(chuàng)建的線程數(shù):

    • MaxProcessMemory 使用64位操作系統(tǒng)
    • JVMMemory 減少JVMMemory的分配
    • ThreadStackSize 減小單個線程的棧大小
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 30,264評論 8 265
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,658評論 19 139
  • 最全的iOS面試題及答案 iOS面試小貼士 ———————————————回答好下面的足夠了-----------...
    大羅Rnthking閱讀 1,052評論 0 2
  • 想當年把門貼成這樣還挨了一頓批
    蓬蓬蓬的毛毛熊閱讀 159評論 2 0
  • 從小到大都在說,命運掌握在自己手中,生命中的選擇都是自己做的。 是的,只有講這種意識付諸實踐,切實地落實到自己的行...
    Alice的夢幻之旅閱讀 185評論 0 0

友情鏈接更多精彩內(nèi)容