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); }
。。。