Java虚拟机OOM问题和四大引用问题简述
一.请你谈谈实际的项目中在Java虚拟机会抛出哪些异常,每个异常都是怎么产生的?
1.java.lang.StackOverflowError 栈空间满了
public static void stackOverFlow(){ // 递归调用之后,把栈空间塞满了,当程序出现递归调用没有终止的时候,就会出现此类错误 // Exception in thread "main" java.lang.StackOverflowError stackOverFlow(); }
运行结果:
2.java.lang.OutOfMemoryError: Java heap space 堆空间满了
public static void outOfMemoryHeap() { // 通过不停的生成新的string对象,把堆空间塞满,堆中没有在存放新的对象,此时会出现该错误 // Exception in thread "main" java.lang.OutOfMemoryError: Java heap space String str = "itheima"; while (true){ str += str + new Random().nextInt(111111)+new Random().nextInt(22222222); str.intern(); } }
运行结果:
3.java.lang.OutOfMemoryError: GC overhead limit exceeded GC回收时间过长
public static void outOfMemoryGC() { /* * GC回收时间过长时候会抛出OutOfMemoryError,过长的定义是指,超过98%的时间用来做GC并且回收了不到 * 2%的堆内存,连续多次GC都回收不到2%的极端情况下才会抛出.假如不抛出GC overflow limit会发生什么 * 呢?那就是GC清理的内存很快就会被再次填满,迫使GC再次执行,导致CPU的使用率一直在100%,但是GC却没有 * 任何成果. * 配置JVM参数 -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails * * java.lang.OutOfMemoryError: GC overhead limit exceeded * */ int i = 0; List<String> list = new ArrayList<>(); try { while (true){ list.add(String.valueOf(++i).intern()); } }catch (Throwable e){ e.printStackTrace(); System.out.println("i = "+i); throw e; } }
运作结果:
4.java.lang.OutOfMemoryError: Direct buffer memory 外部内存满了
public static void outOfMemoryDbm() { /* 写NIO的程序进场使用ByteBuffer来读取或写入数据,这是一种基于通道和缓冲区的I/O方式,它可以使用 Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块的引用 进行操作.这样可以在一些场景中显著提高性能,因为避免了Java堆和Native堆中来回复制内存 ByteBuffer.allocate(capability) 第一种方式是分配JVM堆内存,属于GC的范围,由于需要拷贝,所有 速度相对较慢 ByteBuffer.allocateDirect(capability)第一种方式是分配OS本地内存,不属于GC管辖的范围,所以 由于不需要进行内存拷贝所以速度相对较快 但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GCDirectByteBuffer对象们就不会被回收 这时候堆内存充足,但是本地内存已经使用光,再次尝试使用本地内存就会抛出OOM错误,JVM崩溃 JVM配置 -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails * java.lang.OutOfMemoryError: Direct buffer memory nio程序经常出现 */ System.out.println("JVM最大可用内存: "+ (sun.misc.VM.maxDirectMemory() / (double)1024 / 1024 )+"MB"); try { TimeUnit.MICROSECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6 * 1024 *1024); }
运行结果:
5.java.lang.OutOfMemoryError: unable to create new native thread 无法创建新的本地的线程
public static void outOfMemoryUcnt(){ /* * 高并发请求服务器时候,会出现如下的错误,准确的来说该异常与对应的平台有关 * 导致原因: * 1.应用创建了太多线程,一个应用程序创建了多个线程,超过了系统的承载 * 2.服务器并不允许应用程序创建过多的线程,Linux默认允许单个进程可以创建的线程数为1024个 * 应用程序创建的线程超过1024,就会抛出java.lang.OutOfMemoryError: unable to create new native thread * 解决办法: * 1.想办法降低应用程序创建的线程数量,分析应用程序是否真的需要创建这么多线程,如果不是,修改代码降低线程数 * 2.对于有的应用,确实需要创建很多线程,远超过Linux系统默认的1024个上限,可以通过修改Linux服务器配置 * 扩大Linux的默认限制 root用户没有限制 * ulimit -u 查看当前的用户下可以允许创建的线程数 * vim /etc/security/limits.d/90-nproc.conf 文件 * */ for (int i = 1; ; i++) { System.out.println("i = "+i); new Thread(()->{ try { Thread.sleep(Integer.MAX_VALUE); } catch (InterruptedException e) { e.printStackTrace(); } },String.valueOf(i)).start(); } }
运行结果:Windows没有测出来
6.java.lang.OutOfMemoryError: Metaspace 元空间溢出
static class OOMTest{}; 内部类 public static void outOfMemoryMetaSpace(){ /* * MetaSpace是方法区在HotSpot中的实现,它和持久区最大的区别在于:MetaSpace并不在虚拟机内存而使用本地内存 * 在JDK1.8中,class Metadata 被存储在MetaSpace的native memory * 永久代(元空间)存放了以下信息 * 1. 虚拟机加载的类信息 * 2. 常量池 * 3. 静态变量 * 4. 即时编译后的代码 * 配置JVM: -XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=9m * * */ int i = 0; try { while (true){ i++; Enhancer e = new Enhancer(); e.setSuperClass(OOMTest); e.setUseCache(false); e.setCallBack(new MethodInterceptor(){ @Override public Object intercept(Object o,Method method,Object[] objects,MethodProxy methodProxy) throws Throwable{ return methodProxy.invokeSuper(o,args); } }); e.create(); } }catch (Throwable e){ System.out.println("第" + i+"次元空间溢出"); e.printStackTrace(); } }
运行结果:Windows没有出现!!!
二.请你能谈一谈Java的有哪些引用?JVM分别对不同的引用时怎样进行回收的?有什么作用?
1.强引用
当内存不足时,JVM会进行垃圾回收,对于强引用对象,就算出现OOM错误也不会对该对象进行垃圾回收,强引用是最常见的普通对象的引用,
只要还有强引用指向一个对象,就说明这个对象还活着,垃圾回收机制就不会回收这个对象,在Java中,最常见的引用就是强引用,把一个对象赋值
给一个引用变量,这个引用变量也是强引用当一个变量是强引用时,它处于可达的状态,不被垃圾回收机制回收的.即使该对象以后永远也用不到,JVM
也不会对其进行回收,因此强引用也是引发Java内存泄漏的主要原因之一.
对于一个普通的对象,如果没有其它得我引用关系,只要超过了引用的作用域,或者显式的把强引用赋值为null,一般就可以被垃圾回收机制回收.
// 强引用 public static void show01(){ Object o1 = new Object(); // new 出来的对象一般默认是强引用 Object o2 = o1; // o2 引用赋值 o2也是强引用 o1 = null; // 置空 System.gc(); // 手动GC垃圾回收 System.out.println(o1); System.out.println(o2); }
运行结果: o2没有被垃圾回收机制清除
2.软引用
软引用是相对于强引用弱化了一些的引用,需要使用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾回收,
对于软引用的对象来说,当系统的内存充足时,不会被垃圾回收机制回收,当系统的内存不足时,会被垃圾回收机制回收.
软引用通常用在一些对内存敏感的系统中,比如高速缓存就有用到软引用
//软引用 public static void show02(boolean flag){ if(flag){ // 内存充足 Object o1 = new Object(); SoftReference<Object> softReference = new SoftReference<>(o1); System.out.println(o1); System.out.println(softReference.get()); o1 = null; System.gc(); System.out.println(o1); System.out.println(softReference.get()); }else{ // 内存不充足设置JVM -Xms5m -Xmx5m -XX:+PrintGCDetails Object o1 = new Object(); SoftReference<Object> softReference = new SoftReference<>(o1); System.out.println(o1); System.out.println(softReference.get()); o1 = null; System.gc(); try { // 造成OOM,故意制造大对象 byte[] bytes = new byte[30 * 1024 * 1024]; }catch (Throwable e){ e.printStackTrace(); }finally { System.out.println(o1); System.out.println(softReference.get()); } } }
运行结果:
1.内存充足,o1被置空,但o2没有被回收
2.内存不足 o1,o2都被置为空
3.弱引用
需要使用java.lang.ref.WeakReference类来实现,它比软引用的生命周期更短
对于只有软引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象所占的内存
用处:假如有一个应用,需要读取大量的本地图片,如果每次都从硬盘中读取,严重影响速度,如果全部加载到内存,又可能引发OOM
使用软引用/弱引用可以解决此类问题
使用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,
JVM会自动回收这些缓存图片所占用的空间,从而有效避免OOM问题
Map<String,SoftReference<BitMap>> imageCache = new HashMap<String,SoftReference<BitMap>>();
public static void show03(){ Object o1 = new Object(); WeakReference<Object> weakReference = new WeakReference<>(o1); System.out.println(o1); System.out.println(weakReference.get()); o1 = null; System.gc(); System.out.println(o1); System.out.println(weakReference.get()); }
运行结果: 只要一运行垃圾回收,内存就会被释放
4.虚引用
虚引用主要是通过java.lang.ref.PhantomReference类来实现的,与其他的引用类型不同,虚引用并不会决定对象的生命周期
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都有可能被垃圾回收机制回收,它不能单独使用也不能
通过它访问对象,虚引用必须和引用队列来联合使用虚引用的主要作用是跟踪对象被垃圾回收的状态,就是这个对象被收集器回收
的时候收到一个系统的通知或者后续添加做进一步的处理
public static void show04(){ Object o1 = new Object(); ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); PhantomReference<Object> phantomReference = new PhantomReference<>(o1,referenceQueue); System.out.println(o1); System.out.println(referenceQueue.poll()); System.out.println(phantomReference.get()); System.out.println("---------------------"); o1 = null; System.gc(); System.out.println(o1); System.out.println(referenceQueue.poll()); System.out.println(phantomReference.get()); }
运行结果:
1.虚引用的get()方法永远只返回null
2.在进行垃圾回收之后,在引用队列中可以看到此对象,主要就是在对象销毁前做出一些通知,类似于Spring的后置AOP