JVM之周阳面试小合集
1 JVM体系结构
1.1 概览
jvm运行时内存图:
java gc 主要回收的是 方法区 和 堆中的内容:
1.2 类加载器(待补充)
- 类加载器是什么
- 双亲委派机制
- Java类加载的沙箱安全机制
1.3 常见的垃圾回收算法
-
引用计数
在双端循环,互相引用的时候,容易报错,目前很少使用这种方式了 -
标记复制
复制算法在年轻代的时候,进行使用,复制时候有交换。
优点:没有产生内存碎片
3. 标记清除
先标记,后清除,缺点是会产生内存碎片,用于老年代多一些
4. 标记清除整理
但是需要付出代价,因为移动对象需要成本
2 什么是GC Roots?有什么用?
2.1 GC Roots
-
一句话理解GC Roots
假设我们现在有三个实体,分别是 人,狗,毛衣。然后他们之间的关系是:人 牵着 狗,狗穿着毛衣,他们之间是强连接的关系。有一天人消失了,只剩下狗狗 和 毛衣,这个时候,把人想象成 GC Roots,因为 人 和 狗之间失去了绳子连接。那么狗可能被回收,也就是被警察抓起来,被送到流浪狗寄养所。假设狗和人有强连接的时候,狗狗就不会被当成是流浪狗。
-
那些对象可以当做GC Roots
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中的引用对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中的JNI(Native方法)的引用对象
代码说明:
public class GCRootDemo {
// 方法区中的类静态属性引用的对象
// private static GCRootDemo2 t2;
// 方法区中的常量引用,GC Roots 也会以这个为起点,进行遍历
// private static final GCRootDemo3 t3 = new GCRootDemo3(8);
public static void m1() {
// 第一种,虚拟机栈中的引用对象
GCRootDemo t1 = new GCRootDemo();
System.gc();
System.out.println("第一次GC完成");
}
public static void main(String[] args) {
m1();
}
}
2.2 JVM垃圾回收的时候如何确定垃圾?
什么是垃圾?
简单来说就是内存中已经不再被使用的空间就是垃圾
如何判断一个对象是否可以被回收?
-
引用计数法
Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法就是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加1,每当有一个引用失效,计数器值减1。任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。那么为什么主流的Java虚拟机里面都没有选用这个方法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。
该算法存在但目前无人用了,解决不了循环引用的问题,了解即可。 -
枚举根节点做可达性分析
为了解决引用计数法的循环引用个问题,Java使用了可达性分析的方法:
所谓 GC Roots 或者说 Tracing Roots的“根集合” 就是一组必须活跃的引用。基本思路就是通过一系列名为 GC Roots的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,没有被遍历到的对象就被判定为死亡。
必须从GC Roots对象开始,这个类似于linux的 / 也就是根目录蓝色部分是从GC Roots出发,能够循环可达
而白色部分,从GC Roots出发,无法到达
3 JVM参数调优
JVM如何调优和配置参数?请问如何盘点查看JVM系统默认值?
使用jps和jinfo进行查看:
- Xms:初始堆空间
- Xmx:堆最大值
- Xss:栈空间
-Xms 和 -Xmx最好调整一致,防止JVM频繁进行收集和回收
3.1 JVM参数类型
- 标配参数(从JDK1.0 - Java12都在,很稳定)
- -version
- -help
- -showversion
- X参数(了解)
- -Xint:解释执行
- -Xcomp:第一次使用就编译成本地代码
- -Xmixed:混合模式
- XX参数(重点)
- Boolean类型
- 公式:-XX:+ 或者-某个属性 + 表示开启,-表示关闭
- Case:-XX:-PrintGCDetails:表示关闭了GC详情输出
- key-value类型
- 公式:-XX:属性key=属性value
- 不满意初始值,可以通过下列命令调整
- case:如何:-XX:MetaspaceSize=21807104:查看Java元空间的值
- Boolean类型
3.2 查看运行的Java程序,JVM参数是否开启,具体值为多少?
首先我们运行一个HelloGC的java程序:
public class HelloGC {
public static void main(String[] args) throws Exception {
System.out.println("hello GC");
Thread.sleep(Integer.MAX_VALUE);
}
}
然后使用下列命令查看它的默认参数:
jps:查看java的后台进程
jinfo:查看正在运行的java程序
具体使用:
jps -l得到进程号
D:\Project\leetcode>jps -l
21312 Java面试周阳.JVM.HelloGC
15076 sun.tools.jps.Jps
12472
18072
24632 org.jetbrains.jps.cmdline.Launcher
8968 Java面试周阳.死锁.DeadLockDemo01
查看到HelloGC的进程号为:21312
我们使用jinfo -flag 然后查看是否开启PrintGCDetails这个参数:
jinfo -flag PrintGCDetails 21312
得到的内容为:
D:\Project\leetcode>jinfo -flag PrintGCDetails 21312
-XX:-PrintGCDetails
上面提到了,-号表示关闭,即没有开启PrintGCDetails这个参数。
下面我们需要在启动HelloGC的时候,增加 PrintGCDetails这个参数,需要在运行程序的时候配置JVM参数:
然后在VM Options中加入下面的代码,现在+号表示开启:
-XX:+PrintGCDetails
然后在使用jinfo查看我们的配置:
jps -l
jinfo -flag PrintGCDetails 20618
得到的结果为:
-XX:+PrintGCDetails
我们看到原来的-号变成了+号,说明我们通过 VM Options配置的JVM参数已经生效了。
使用下列命令,会把jvm的全部默认参数输出:
jinfo -flags ***
3.3 -Xms 和 -Xmx
两个经典参数:-Xms 和 -Xmx,配置JVM参数一般都为XX开头,这两个参数 如何解释?
- -Xms 等价于 -XX:InitialHeapSize :初始化堆内存(默认只会用最大物理内存的64分1)
- -Xmx 等价于 -XX:MaxHeapSize :最大堆内存(默认只会用最大物理内存的4分1)
3.4 查看JVM默认参数
-
-XX:+PrintFlagsInitial
- 主要是查看初始默认值
- 公式
- java -XX:+PrintFlagsInitial -version
- java -XX:+PrintFlagsInitial(重要参数)
-
-XX:+PrintFlagsFinal:表示修改以后,最终的值
如果有 := 表示修改过的, = 表示没有修改过的。
3.5 代码查看堆内存
查看JVM的初始化堆内存 -Xms 和最大堆内存 Xmx:
代码:
public class HelloGC2 {
public static void main(String[] args) {
// 返回Java虚拟机中内存的总量
long totalMemory = Runtime.getRuntime().totalMemory();
// 返回Java虚拟机中试图使用的最大内存量
long maxMemory = Runtime.getRuntime().maxMemory();
System.out.println("TOTAL_MEMORY(-Xms) = " + totalMemory + "(字节)," + (totalMemory/(double)1024/1024) + "MB");
System.out.println("MAX-MEMORY(-Xmx) = " + maxMemory + "(字节)," + (maxMemory/(double)1024/1024) + "MB");
}
}
输出结果:
-Xms 初始堆内存为:物理内存的1/64 -Xmx 最大堆内存为:系统物理内存的 1/4。
3.6 打印JVM默认参数
使用 java -XX:+PrintCommandLineFlags 打印出JVM的默认的简单初始化参数:
3.7 常用调优参数
- -Xms:初始化堆内存,默认为物理内存的1/64,等价于-XX:initialHeapSize
- -Xmx:最大堆内存,默认为物理内存的1/4,等价于-XX:MaxHeapSize
- -Xss:设计单个线程栈的大小,一般默认为512K~1024K,等价于 -XX:ThreadStackSize
- 使用 jinfo -flag ThreadStackSize 会发现 -XX:ThreadStackSize = 0
- 这个值的大小是取决于平台的
- Linux/x64:1024KB
- OS X:1024KB
- Oracle Solaris:1024KB
- Windows:取决于虚拟内存的大小
- -Xmn:设置年轻代大小
- -XX:MetaspaceSize:设置元空间大小
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,因此,默认情况下,元空间的大小仅受本地内存限制。
- -Xms10m -Xmx10m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal
- 但是默认的元空间大小:只有20多M
- 为了防止在频繁的实例化对象的时候,让元空间出现OOM,因此可以把元空间设置的大一些
- -XX:PrintGCDetails:输出详细GC收集日志信息
- GC
- Full GC
GC日志收集流程图:
我们使用一段代码,制造出垃圾回收的过程:
代码:
public class HelloGC3 {
public static void main(String[] args) {
byte[] byteArray = new byte[50*1024*1024];
System.gc();
}
}
再设置一下程序的启动配置: 设置初始堆内存为10M,最大堆内存为10M:
-Xms10m -Xmx10m -XX:+PrintGCDetails
运行结果:
问题发生的原因:
因为们通过 -Xms10m 和 -Xmx10m 只给Java堆栈设置了10M的空间,但是创建了50M的对象,因此就会出现空间不足,而导致出错
同时在垃圾收集的时候,我们看到有两个对象:GC 和 Full GC。
GC垃圾收集
GC在新生区
[GC (Allocation Failure) [PSYoungGen: 1946K->488K(2560K)] 1946K->740K(9728K), 0.0060382 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
参数对应的图为:
Full GC垃圾回收:
Full GC大部分发生在老年代
[Full GC (Allocation Failure) [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 300K->637K(7168K)] 796K->637K(9728K), [Metaspace: 3458K->3458K(1056768K)], 0.0048417 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
规律:
[名称: GC前内存占用 -> GC后内存占用 (该区内存总大小)]
-XX:SurvivorRatio
调节新生代中 eden 和 S0、S1的空间比例,默认为 -XX:SuriviorRatio=8,Eden:S0:S1 = 8:1:1
加入设置成 -XX:SurvivorRatio=4,则为 Eden:S0:S1 = 4:1:1
SurvivorRatio值就是设置eden区的比例占多少,S0和S1相同
Java堆从GC的角度还可以细分为:新生代(Eden区,From Survivor区合To Survivor区)和老年代
-
eden、SurvivorFrom复制到SurvivorTo,年龄 + 1
首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom去,当Eden区再次触发GC的时候会扫描Eden区合From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果对象的年龄已经到达老年的标准,则赋值到老年代区),通知把这些对象的年龄 + 1 -
清空eden、SurvivorFrom
然后,清空eden,SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to -
SurvivorTo和SurvivorFrom互换
最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区,部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认为15),最终如果还是存活,就存入老年代
-XX:NewRatio(了解)配置年轻代new 和老年代old 在堆结构的占比
默认: -XX:NewRatio=2 新生代占1,老年代2,年轻代占整个堆的1/3
-XX:NewRatio=4:新生代占1,老年代占4,年轻代占整个堆的1/5,NewRadio值就是设置老年代的占比,剩下的1个新生代
新生代特别小,会造成频繁的进行GC收集
-XX:MaxTenuringThreshold
设置垃圾最大年龄,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区,部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认为15),最终如果还是存活,就存入老年代
这里就是调整这个次数的,默认是15,并且设置的值 在 0~15之间
查看默认进入老年代年龄:jinfo -flag MaxTenuringThreshold 17344
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻对象不经过Survivor区,直接进入老年代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大的值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概念。
4 强引用、软引用、弱引用、虚引用
在原来的时候,我们谈到一个类的实例化:
Person p = new Person()
在等号的左边,就是一个对象的引用,存储在栈中;而等号右边,就是实例化的对象,存储在堆中;其实这样的一个引用关系,就被称为强引用。
4.1 强引用
当内存不足的时候,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,打死也不回收~!
强引用是我们最常见的普通对象引用,只要还有一个强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收,因此强引用是造成Java内存泄漏的主要原因之一。
对于一个普通的对象,如果没有其它的引用关系,只要超过了引用的作用于或者显示地将相应(强)引用赋值为null,一般可以认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾回收策略)。
强引用小例子:
public class StrongReferenceDemo {
public static void main(String[] args) {
// 这样定义的默认就是强应用
Object obj1 = new Object();
// 使用第二个引用,指向刚刚创建的Object对象
Object obj2 = obj1;
// 置空
obj1 = null;
// 垃圾回收
System.gc();
System.out.println(obj1);
System.out.println(obj2);
}
}
输出结果:
输出结果我们能够发现,即使 obj1 被设置成了null,然后调用gc进行回收,但是也没有回收实例出来的对象,obj2还是能够指向该地址,也就是说垃圾回收器,并没有将该对象进行垃圾回收。
4.2 软引用
软引用是一种相对弱化了一些的引用,需要用Java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集,对于只有软引用的对象来讲:
- 当系统内存充足时,它不会被回收
- 当系统内存不足时,它会被回收
软引用通常在对内存敏感的程序中,比如高速缓存就用到了软引用,内存够用 的时候就保留,不够用就回收。
具体使用:
public class SoftReferenceDemo {
public static void main(String[] args) {
softRefMemoryEnough();
}
private static void softRefMemoryEnough() {
// 创建一个强引用
Object o1 = new Object();
// 创建一个软引用
SoftReference<Object> softReferenceDemo = new SoftReference(o1);
System.out.println(o1);
System.out.println(softReferenceDemo.get());
o1 = null;
// 手动GC
System.gc();
System.out.println(o1);
System.out.println(softReferenceDemo.get());
}
}
输出结果:
我们首先查看内存够用的时候,首先输出的是 o1 和 软引用的 softReference,我们都能够看到值。然后我们把o1设置为null,执行手动GC后,我们发现softReference的值还存在,说明内存充足的时候,软引用的对象不会被回收
下面我们看当内存不够的时候,
代码:
public static void softRefMemoryNotEnough() {
System.out.println("==========================");
// 创建一个强应用
Object o1 = new Object();
// 创建一个软引用
SoftReference<Object> softReference = new SoftReference(o1);
System.out.println(o1);
System.out.println(softReference.get());
o1 = null;
// 模拟oom自动GC
try {
// 创建30M大对象
byte[] bytes = new byte[3000*1024*1024];
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(01);
System.out.println(softReference.get());
}
}
我们使用了JVM启动参数配置,给初始化堆内存为5M:
-Xms5m -Xmx5m -XX:+PrintGCDetails
输出结果:
4.3 弱引用
不管内存是否够,只要有GC操作就会进行回收,弱引用需要用 java.lang.ref.WeakReference 类来实现,它比软引用生存期更短。对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的空间。
代码演示:
public class WeakReferenceDemo {
public static void main(String[] args) {
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());
}
}
输出结果:
我们看结果,能够发现,我们并没有制造出OOM内存溢出,而只是调用了一下GC操作,垃圾回收就把它给收集了。
软引用和弱引用的使用场景:
场景:假如有一个应用需要读取大量的本地图片
- 如果每次读取图片都从硬盘读取则会严重影响性能
- 如果一次性全部加载到内存中,又可能造成内存溢出。
此时使用软引用可以解决这个问题
设计思路:使用HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占的空间,从而有效地避免了OOM的问题。
Map<String, SoftReference<String>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
WeakHashMap是什么?
比如一些常常和底层打交道的,mybatis等,底层都应用到了WeakHashMap。
WeakHashMap和HashMap类似,只不过它的Key是使用了弱引用的,也就是说,当执行GC的时候,WeakHashMap中的key会进行回收,下面我们使用例子来测试一下。
我们使用了两个方法,一个是普通的HashMap方法,我们输入一个Key-Value键值对,然后让它的key置空,然后在查看结果。
代码:
public class WeakHashMapDemo {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
Integer key = new Integer(1);
String value = "HashMap";
map.put(key, value);
System.out.println(map);
key = null;
System.gc();
System.out.println(map);
}
}
输出结果:
第二个是使用了WeakHashMap,完整代码如下:
public class WeakHashMapDemo2 {
public static void main(String[] args) {
Map<Integer, String> map = new WeakHashMap<>();
Integer key = new Integer(1);
String value = "WeakHashMap";
map.put(key, value);
System.out.println(map);
key = null;
System.gc();
System.out.println(map);
}
}
输出结果:
从这里我们看到,对于普通的HashMap来说,key置空并不会影响,HashMap的键值对,因为这个属于强引用,不会被垃圾回收。
但是WeakHashMap,在进行GC操作后,弱引用的就会被回收.
4.4 虚引用
虚引用又称为幽灵引用,需要java.lang.ref.PhantomReference 类来实现。
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
如果一个对象持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列ReferenceQueue联合使用。
虚引用的主要作用和跟踪对象被垃圾回收的状态,仅仅是提供一种确保对象被finalize以后,做某些事情的机制。
PhantomReference的get方法总是返回null,因此无法访问对象的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。
换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候,收到一个系统通知或者后续添加进一步的处理,Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前,做必要的清理工作。
这个就相当于Spring AOP里面的后置通知。
**使用场景:**一般用于在回收时候做通知相关操作
引用队列 ReferenceQueue
软引用,弱引用,虚引用在回收之前,需要在引用队列保存一下。我们在初始化的弱引用或者虚引用的时候,可以传入一个引用队列:
Object o1 = new Object();
// 创建引用队列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// 创建一个弱引用
WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);
那么在进行GC回收的时候,弱引用和虚引用的对象都会被回收,但是在回收之前,它会被送至引用队列中。
代码:
public class PhantomReferenceDemo {
public static void main(String[] args) {
Object o1 = new Object();
// 创建引用队列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// 创建一个弱引用
WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);
// 创建一个虚引用
PhantomReference<Object> phantomReference = new PhantomReference<>(o1, referenceQueue);
System.out.println(o1);
System.out.println(weakReference.get());
// 取队列中的内容
System.out.println(referenceQueue.poll());
o1 = null;
System.gc();
System.out.println("执行GC操作");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(o1);
System.out.println(phantomReference.get());
// 取队列中的内容
System.out.println(referenceQueue.poll());
}
}
5 GCRoots和四大引用小总结
- 红色部分在垃圾回收之外,也就是强引用的
- 蓝色部分:属于软引用,在内存不够的时候,才回收
- 虚引用和弱引用:每次垃圾回收的时候,都会被干掉,但是它在干掉之前还会存在引用队列中,我们可以通过引用队列进行一些通知机制
6 Java内存溢出OOM
JVM中常见的两个错误:
- StackoverFlowError :栈溢出
- OutofMemoryError: java heap space:堆溢出
除此之外,还有以下的错误:
- java.lang.StackOverflowError
- java.lang.OutOfMemoryError:java heap space
- java.lang.OutOfMemoryError:GC overhead limit exceeeded
- java.lang.OutOfMemoryError:Direct buffer memory
- java.lang.OutOfMemoryError:unable to create new native thread
- java.lang.OutOfMemoryError:Metaspace
注意:
OutOfMemoryError和StackOverflowError是属于Error,不是Exception。
6.1 StackoverFlowError
堆栈溢出,我们有最简单的一个递归调用,就会造成堆栈溢出,也就是深度的方法调用,栈一般是512K,不断的深度调用,直到栈被撑破。
代码:
public class StackOverflowErrorDemo {
public static void main(String[] args) {
stackOverflowError();
}
/**
* 栈一般是512K,不断的深度调用,直到栈被撑破
*/
private static void stackOverflowError() {
stackOverflowError();
}
}
运行结果:
6.2 OutOfMemoryError
6.2.1 java heap space
我们模拟创建很多对象,导致堆空间不够存储:
public class JavaHeapSpaceDemo {
public static void main(String[] args) {
// 堆空间的大小 -Xms10m -Xmx10m
// 创建一个 80M的字节数组
byte[] bytes = new byte[80*1024*1024];
}
}
输出结果:
6.2.2 GC overhead limit exceeded
GC回收时间过长时会抛出OutOfMemoryError,过长的定义是,超过了98%的时间用来做GC,并且回收了不到2%的堆内存。连续多次GC都只回收了不到2%的极端情况下,才会抛出。假设不抛出GC overhead limit 错误会造成什么情况呢?那就是GC清理的这点内存很快会再次被填满,迫使GC再次执行,这样就形成了恶性循环,CPU的使用率一直都是100%,而GC却没有任何成果。
代码演示:
为了更快的达到效果,我们首先需要设置JVM启动参数
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
public class OverheadlLmitExceeded {
public static void main(String[] args) {
int i = 0;
List<String> list = new ArrayList<>();
try {
while (true) {
list.add(String.valueOf(++i).intern());
}
} catch (Exception e) {
System.out.println("***************i:" + i);
e.printStackTrace();
throw e;
}
}
}
输出结果:
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7091K->7091K(7168K)] 9139K->9139K(9728K), [Metaspace: 3503K->3503K(1056768K)], 0.0336242 secs] [Times: user=0.16 sys=0.00, real=0.03 secs]
[Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7116K->650K(7168K)] 9164K->650K(9728K), [Metaspace: 3535K->3535K(1056768K)], 0.0064666 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:401)
at java.lang.String.valueOf(String.java:3099)
at Java面试周阳.JVM.OverheadlLmitExceeded.main(OverheadlLmitExceeded.java:12)
Heap
PSYoungGen total 2560K, used 93K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd17750,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 650K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 9% used [0x00000000ff600000,0x00000000ff6a2978,0x00000000ffd00000)
Metaspace used 3553K, capacity 4540K, committed 4864K, reserved 1056768K
class space used 395K, capacity 428K, committed 512K, reserved 1048576K
Process finished with exit code 1
这个异常出现的步骤就是,我们不断的像list中插入String对象,直到启动GC回收。
6.2.3 Direct buffer memory
Netty底层使用NIO,在netty中会出现这种错误:这是由于NIO引起的。
写NIO程序的时候经常会使用ByteBuffer来读取或写入数据,这是一种基于通道(Channel) 与 缓冲区(Buffer)的I/O方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
ByteBuffer.allocate(capability): 第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。
ByteBuffer.allocteDirect(capability): 第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存的拷贝,所以速度相对较快。
但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象就不会被回收,这时候怼内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那么程序就崩溃了。
一句话总结:本地内存不足,但是堆内存充足的时候,就会出现这个问题
代码演示:
public class DirectBufferMemoryError {
public static void main(String[] args) {
// 使用 -XX:MaxDirectMemorySize=5m 配置能使用的堆外物理内存为5M
// -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
// 只设置了5M的物理内存使用,但是却分配 6M的空间
ByteBuffer bb = ByteBuffer.allocateDirect(6*1024*1024);
}
}
输出结果:
[GC (System.gc()) [PSYoungGen: 1920K->488K(2560K)] 1920K->708K(9728K), 0.0010284 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 488K->0K(2560K)] [ParOldGen: 220K->634K(7168K)] 708K->634K(9728K), [Metaspace: 3396K->3396K(1056768K)], 0.0047790 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:693)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at Java面试周阳.JVM.DirectBufferMemoryError.main(DirectBufferMemoryError.java:10)
Heap
PSYoungGen total 2560K, used 152K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 7% used [0x00000000ffd00000,0x00000000ffd26290,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 634K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 8% used [0x00000000ff600000,0x00000000ff69ead0,0x00000000ffd00000)
Metaspace used 3524K, capacity 4502K, committed 4864K, reserved 1056768K
class space used 391K, capacity 394K, committed 512K, reserved 1048576K
6.2.4 unable to create new native thread
不能够创建更多的新的线程了,也就是说创建线程的上限达到了,继续创建就会报此错误。在高并发场景的时候,有可能出现此问题,高并发请求服务器时,经常会出现如下异常java.lang.OutOfMemoryError:unable to create new native thread,准确说该native thread异常与对应的平台有关。
导致原因:
- 应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
- 服务器并不允许你的应用程序创建这么多线程,linux系统默认运行单个进程可以创建的线程为1024个,如果应用创建超过这个数量,就会报 java.lang.OutOfMemoryError:unable to create new native thread
解决方法:
- 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
- 对于有的应用,确实需要创建很多线程,远超过linux系统默认1024个线程限制,可以通过修改linux服务器配置,扩大linux默认限制
代码:
public class UnableCreateNewThreadDemo {
public static void main(String[] args) {
for (int i = 0; ; i++) {
System.out.println("************** i = " + i);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
这个时候,就会出现下列的错误,线程数大概在 900多个:
Exception in thread "main" java.lang.OutOfMemoryError: unable to cerate new native thread
如何查看线程数:
ulimit -u
6.2.5 Metaspace
元空间内存不足,Matespace元空间应用的是本地内存。-XX:MetaspaceSize 的处理化大小为20M
元空间是什么?
元空间就是我们的方法区,存放的是类模板,类信息,常量池等。
Metaspace是方法区HotSpot中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟内存中,而是使用本地内存,也即在java8中,class metadata(the virtual machines internal presentation of Java class),被存储在叫做Matespace的native memory。
永久代(java8后背元空间Metaspace取代了)存放了以下信息:
- 虚拟机加载的类信息
- 常量池
- 静态变量
- 即时编译后的代码
模拟Metaspace空间溢出,我们不断生成类 往元空间里灌输,类占据的空间总会超过Metaspace指定的空间大小。
在模拟异常生成时候,因为初始化的元空间为20M,因此我们使用JVM参数调整元空间的大小,为了更好的效果:
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
代码:
public class MetaspaceOutOfMemoryDemo {
// 静态类
static class OOMTest {
}
public static void main(final String[] args) {
// 模拟计数多少次以后发生异常
int i =0;
try {
while (true) {
i++;
// 使用Spring的动态字节码技术
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMTest.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, args);
}
});
}
} catch (Exception e) {
System.out.println("发生异常的次数:" + i);
e.printStackTrace();
} finally {
}
}
}
运行结果:
发生异常的次数: 201
java.lang.OutOfMemoryError:Metaspace