OutOfMemoryError 到底能不能被捕获?


感觉中,OutOfMemeryError 是jvm抛出的异常,是不能被捕获的。

直到工作中真的遇到OOM异常,而且tomcat服务还一直对外提供服务。


那么问题来了:
  1. OOM 到底能不能被捕获?
  2. jvm抛出OOM后,是否就会立即停止运行呢?
  3. jvm什么时候会抛出OOM异常?


先来个例子:
本例子将会一一体现如上问题:(最好设置jvm最大内存如: -Xmx2m -Xms2m)

public class OOMCatchTest {

    /**
     * 可以看作是一个消息队列, 作为 Producer 与 Consumer 的通信桥梁 <br />
     *      其实此处存在并发写的问题,不过不是本文的重点,暂且忽略
     */
    private static volatile List<UserObj> userWaitingList = new ArrayList<>();

    private AtomicInteger uidCenter = new AtomicInteger(0);

    // 随机数生成源
    private final String rndSource = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";

    public static void main(String[] args) throws IOException {
        OOMCatchTest oomCatchTest = new OOMCatchTest();
        Thread producer = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(System.currentTimeMillis() + ": start producer.");
                oomCatchTest.productUserObj();
                System.out.println(System.currentTimeMillis() + ": end producer.");
            }
        });
        producer.setName("producer-1");
        producer.start();

        Thread consumer = new Thread(() -> {
                System.out.println(System.currentTimeMillis() + ": start consumer.");
                try {
                    oomCatchTest.consumeUserObj();
                }
                catch (Throwable e) {
                    System.out.println("consumer caught exception: " + e.getMessage());
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() + ": end consumer.");
            });
        consumer.setName("consumer-1");
        consumer.start();

        System.out.println("over the main");
    }

    // 生产者
    public void productUserObj() {
        Random rnd = new Random();
        OOMCatchTest oomTest = new OOMCatchTest();
        // 可作开关
        boolean startProduce = true;
        try {
            while (startProduce) {
                UserObj userTemp = new UserObj();
                userTemp.setAddress(oomTest.generateRandomStr(20));
                userTemp.setAge(rnd.nextInt(100));
                userTemp.setUid(oomTest.generateUid());
                userTemp.setName(oomTest.generateRandomStr(10));
                // 此处展示 ArrayList 导致的抛出OOM类型
                userWaitingList.add(userTemp);
                System.out.println("produce:" + userTemp);
            }
        }
        // 此处可捕获 OOM
        catch (Throwable e) {
            // 模拟一个服务提供者,做死循环
            System.out.println("An Exception: "
                    + e.getClass().getCanonicalName() + " " + e.getMessage()
                    + " occour..., cur uid:" + oomTest.uidCenter);
            int j = 0;
            // 此处运行代表 OOM 并未终止jvm
            while (j++ < 1000) {
                try {
                    Thread.sleep(1000);
                    System.out.println("producer oom, wait: " + j);
                }
                catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
            }
            // 如果打印栈桢,只会更增加内存消耗,从而导致线程异常退出
//            e.printStackTrace();
        }
    }

    // 消费者
    public void consumeUserObj() {
        // 可做开关
        boolean startConsume = true;
        while(startConsume) {
            // 做空等等
            while (userWaitingList.isEmpty()) {
                Thread.currentThread().yield();
            }
            while (userWaitingList.iterator().hasNext()) {
                try {
                    // do sth
                    Thread.sleep(50);
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
                UserObj obj = userWaitingList.iterator().next();

                System.out.println("consume user:" + obj);
                userWaitingList.remove(obj);

            }
        }
    }

    public String generateRandomStr(int digit) {
        StringBuilder sb = new StringBuilder();
        int len = rndSource.length();
        Random random = new Random();
        for(int i = 0; i < digit; i++) {
            sb.append(rndSource.charAt(random.nextInt(len)));
        }
        return sb.toString();
    }

    public Integer generateUid() {
        return uidCenter.incrementAndGet();
    }

    static class UserObj {
        private Integer uid;
        private String name;
        private Integer age;
        private String address;

        public Integer getUid() {
            return uid;
        }

        public void setUid(Integer uid) {
            this.uid = uid;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Integer getAge() {
            return age;
        }

        public void setAge(Integer age) {
            this.age = age;
        }

        public String getAddress() {
            return address;
        }

        public void setAddress(String address) {
            this.address = address;
        }

        @Override
        public String toString() {
            return "UserObj{" +
                    "uid=" + uid +
                    ", name='" + name + '\'' +
                    ", age=" + age +
                    ", address='" + address + '\'' +
                    '}';
        }
    }
}

如上例子,可能抛出如下异常:

produce:UserObj{uid=2135, name='7QSy8X251t', age=44, address='2wwye8WEfR6dHJEQrIHk'}
An Exception: GC overhead limit exceeded occour..., cur uid:2136
java.lang.OutOfMemoryError: GC overhead limit exceeded
consume user:UserObj{uid=1, name='nBf1Ck3T2G', age=20, address='7ubqHrfiHf5WEdPtbJak'}
    at java.util.Arrays.copyOfRange(Arrays.java:3664)
    at java.lang.String.<init>(String.java:207)
    at java.lang.StringBuilder.toString(StringBuilder.java:407)
    at com.xxx.tester.OOMCatchTest.generateRandomStr(OOMCatchTest.java:98)
    at com.xxx.tester.OOMCatchTest.productUserObj(OOMCatchTest.java:58)
1541324629155: end producer.
    at com.xxx.tester.OOMCatchTest$1.run(OOMCatchTest.java:26)
    at java.lang.Thread.run(Thread.java:745)
Exception in thread "consumer-1" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.util.Arrays.copyOfRange(Arrays.java:3664)
    at java.lang.String.<init>(String.java:207)
    at java.lang.StringBuilder.toString(StringBuilder.java:407)
    at com.xxx.tester.OOMCatchTest$UserObj.toString(OOMCatchTest.java:145)
    at java.lang.String.valueOf(String.java:2994)
    at java.lang.StringBuilder.append(StringBuilder.java:131)

    
Exception in thread "consumer-1" java.lang.OutOfMemoryError: GC overhead limit exceeded
java.lang.OutOfMemoryError: GC overhead limit exceeded
An Exception: GC overhead limit exceeded occour..., cur uid:11743
    at java.util.Arrays.copyOf(Arrays.java:3332)
Exception in thread "producer-1" java.lang.OutOfMemoryError: GC overhead limit exceeded

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "consumer-1"

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "producer-1"

produce:UserObj{uid=3751, name='dtXmpNGMs1', age=41, address='1Bujt5TzHv04cptNEyUb'}
An Exception: java.lang.OutOfMemoryError GC overhead limit exceeded occour..., cur uid:3752
producer oom, wait: 1
producer oom, wait: 2
producer oom, wait: 3

抛出OOM异常有几种情况:

  1. java中直接throw 抛出 OOM;(后面详细列举)

  2. 使用new int[MAX] 等基本类型方式时抛出 OOM,这种异常隐式抛出;

  3. 当收到外部特殊信号时抛出,如:常用的威胁信号 kill -3 <pid>;

而通常,前两个OOM都是可能被捕获的! 且抛出的OOM只会影响当前线程(和其他异常一样)。不过 OOM 一般会具有普遍性,即一个线程OOM时,通常其他线程也跑不掉!

下面来看几个JAVA中主动抛出 OOM 的样例吧:


// java.util.ArrayList.add(E e), 进行扩容的时候,就可能抛出oom, 也即代码异常,可以捕获

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
    
    
// java.nio.ByteBuffer.allocateDirect(int capacity); //分配直接内存时,可能抛出oom

    /**
     * Allocates a new direct byte buffer.
     *
     * <p> The new buffer's position will be zero, its limit will be its
     * capacity, its mark will be undefined, and each of its elements will be
     * initialized to zero.  Whether or not it has a
     * {@link #hasArray backing array} is unspecified.
     *
     * @param  capacity
     *         The new buffer's capacity, in bytes
     *
     * @return  The new byte buffer
     *
     * @throws  IllegalArgumentException
     *          If the <tt>capacity</tt> is a negative integer
     */
    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
    // java.nio.DirectByteBuffer
    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));
        // 此处先抛出oom
        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.create(this, new Deallocator(base, size, cap));
        att = null;

    }

    // which a process may access.  All sizes are specified in bytes.
    static void reserveMemory(long size, int cap) {

        if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }

        // optimist!
        if (tryReserveMemory(size, cap)) {
            return;
        }

        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();

        // retry while helping enqueue pending Reference objects
        // which includes executing pending Cleaner(s) which includes
        // Cleaner(s) that free direct buffer memory
        while (jlra.tryHandlePendingReference()) {
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }

        // trigger VM's Reference processing
        System.gc();

        // a retry loop with exponential back-off delays
        // (this gives VM some time to do it's job)
        boolean interrupted = false;
        try {
            long sleepTime = 1;
            int sleeps = 0;
            while (true) {
                if (tryReserveMemory(size, cap)) {
                    return;
                }
                if (sleeps >= MAX_SLEEPS) {
                    break;
                }
                if (!jlra.tryHandlePendingReference()) {
                    try {
                        Thread.sleep(sleepTime);
                        sleepTime <<= 1;
                        sleeps++;
                    } catch (InterruptedException e) {
                        interrupted = true;
                    }
                }
            }

            // no luck
            throw new OutOfMemoryError("Direct buffer memory");

        } finally {
            if (interrupted) {
                // don't swallow interrupts
                Thread.currentThread().interrupt();
            }
        }
    }

    
// java.util.concurrentHashMap.toArray();  // 

        public final Object[] toArray() {
            long sz = map.mappingCount();
            if (sz > MAX_ARRAY_SIZE)
                // Required array size too large
                throw new OutOfMemoryError(oomeMsg);
            int n = (int)sz;
            Object[] r = new Object[n];
            int i = 0;
            for (E e : this) {
                if (i == n) {
                    if (n >= MAX_ARRAY_SIZE)
                        throw new OutOfMemoryError(oomeMsg);
                    if (n >= MAX_ARRAY_SIZE - (MAX_ARRAY_SIZE >>> 1) - 1)
                        n = MAX_ARRAY_SIZE;
                    else
                        n += (n >>> 1) + 1;
                    r = Arrays.copyOf(r, n);
                }
                r[i++] = e;
            }
            return (i == n) ? r : Arrays.copyOf(r, i);
        }


// java.nio.file.Files.readAllBytes(Path path)

    public static byte[] readAllBytes(Path path) throws IOException {
        try (SeekableByteChannel sbc = Files.newByteChannel(path);
             InputStream in = Channels.newInputStream(sbc)) {
            long size = sbc.size();
            if (size > (long)MAX_BUFFER_SIZE)
                throw new OutOfMemoryError("Required array size too large");

            return read(in, (int)size);
        }
    }

    /**
     * Reads all the bytes from an input stream. Uses {@code initialSize} as a hint
     * about how many bytes the stream will have.
     *
     * @param   source
     *          the input stream to read from
     * @param   initialSize
     *          the initial size of the byte array to allocate
     *
     * @return  a byte array containing the bytes read from the file
     *
     * @throws  IOException
     *          if an I/O error occurs reading from the stream
     * @throws  OutOfMemoryError
     *          if an array of the required size cannot be allocated
     */
    private static byte[] read(InputStream source, int initialSize) throws IOException {
        int capacity = initialSize;
        byte[] buf = new byte[capacity];
        int nread = 0;
        int n;
        for (;;) {
            // read to EOF which may read more or less than initialSize (eg: file
            // is truncated while we are reading)
            while ((n = source.read(buf, nread, capacity - nread)) > 0)
                nread += n;

            // if last call to source.read() returned -1, we are done
            // otherwise, try to read one more byte; if that failed we're done too
            if (n < 0 || (n = source.read()) < 0)
                break;

            // one more byte was read; need to allocate a larger buffer
            if (capacity <= MAX_BUFFER_SIZE - capacity) {
                capacity = Math.max(capacity << 1, BUFFER_SIZE);
            } else {
                if (capacity == MAX_BUFFER_SIZE)
                    throw new OutOfMemoryError("Required array size too large");
                capacity = MAX_BUFFER_SIZE;
            }
            buf = Arrays.copyOf(buf, capacity);
            buf[nread++] = (byte)n;
        }
        return (capacity == nread) ? buf : Arrays.copyOf(buf, nread);
    }

// java.io.BufferedInputStream.read()/skip()/
// java.io.BufferedInputStream.fill()

    private void fill() throws IOException {
        byte[] buffer = getBufIfOpen();
        if (markpos < 0)
            pos = 0;            /* no mark: throw away the buffer */
        else if (pos >= buffer.length)  /* no room left in buffer */
            if (markpos > 0) {  /* can throw away early part of the buffer */
                int sz = pos - markpos;
                System.arraycopy(buffer, markpos, buffer, 0, sz);
                pos = sz;
                markpos = 0;
            } else if (buffer.length >= marklimit) {
                markpos = -1;   /* buffer got too big, invalidate mark */
                pos = 0;        /* drop buffer contents */
            } else if (buffer.length >= MAX_BUFFER_SIZE) {
                throw new OutOfMemoryError("Required array size too large");
            } else {            /* grow buffer */
                int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
                        pos * 2 : MAX_BUFFER_SIZE;
                if (nsz > marklimit)
                    nsz = marklimit;
                byte nbuf[] = new byte[nsz];
                System.arraycopy(buffer, 0, nbuf, 0, pos);
                if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
                    // Can't replace buf if there was an async close.
                    // Note: This would need to be changed if fill()
                    // is ever made accessible to multiple threads.
                    // But for now, the only way CAS can fail is via close.
                    // assert buf == null;
                    throw new IOException("Stream closed");
                }
                buffer = nbuf;
            }
        count = pos;
        int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
        if (n > 0)
            count = n + pos;
    }


// java.lang.AbstractStringBuilder.expandCapacity(int minimumCapacity)
// java.lang.AbstractStringBuilder.ensureCapacityInternal(int minimumCapacity)
// java.lang.AbstractStringBuilder.append(String str)

    /**
     * This implements the expansion semantics of ensureCapacity with no
     * size check or synchronization.
     */
    void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }

     其实,oom只是被建议为不要捕获的异常,你如果实在要捕获,why not ?

。。。

猜你喜欢

转载自www.cnblogs.com/yougewe/p/9905754.html