OOM内存溢出实战不得不看的经典
各种OOM的溢出实战及对象、布局、访问、对存活判断及引用一、虚拟机中的对象我们JAVA编程中无时无刻都在操作创建对象,那么我们虚拟机在遇到new关键词创建对象的执行过程是怎样的?1、例如我们有一个User类首先检查是否存在这个User类,然后看是否有没有被加载过,如果没有加载JVM会先进行User加载。2、加载完成后我们会在堆中分配特定大小的内存进行分配。(1)...
各种OOM的溢出实战及对象、布局、访问、对存活判断及引用
一、虚拟机中的对象
我们JAVA编程中无时无刻都在操作创建对象,那么我们虚拟机在遇到new关键词创建对象的执行过程是怎样的?
1、例如我们有一个User类首先检查是否存在这个User类,然后看是否有没有被加载过,如果没有加载JVM会先进行User加载。
2、加载完成后我们会在堆中分配特定大小的内存进行分配。
(1)、划分内存的方法:指针碰撞:例如一块方格的堆内存区域,假如前面两个已经被分配了,要分第三个内存块给User这个类,那么只要指针移动一个位置即可,这种方式即为指针碰撞的划分内存方法。
(2)空闲列表:例如一块方格的堆内存区域,假如已经分配的内存不规整,犬牙交错的已经分配了内存,那这个时候要分内存块给User这个类,显然用指针碰撞的方式已经不适用,这个时候我们就可以弄一个列表来维护可分配的内存块,哪些用过的打勾,哪些用过的打叉这种方式就叫空闲列表的划分内存方法。
采用哪种划分内存的方法,由我们堆的内存是不是规整来决定,堆的内存是否规整由我们的垃圾回收器是否有这种压缩整理来决定。
除了划分内存方法不同的问题,还有并发安全的问题
(1)、拿指针碰撞的方式来讲,如果一个线程A对象要分配一个内存,假如指针还没移动,线程B对象也要分配内存,这时候会产生,A对象和B对象都会使用这个区域。怎么解决这个问题
(2)、用CAS配上失败重试:CAS(比较并替换) 并发编程中讲的很详细,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
3、内存分配完成后,进行内存空间初始化,赋初值指的是同一给integer赋值0,String赋值空白字符串。
4、接下来就是设置:设置是指表明这个对象是哪个类的实例,通过这个对象我们怎么拿到这个类的相关的元数据信息,还有常说的对象哈希码,对象的GC分代年龄,这些信息是放在对象的头里面的。
5、到了对象初始化,我们才这正来执行我们所写的Employee的构造方法这些动作。
二、对象的布局
总体的布局可以分为三个区
1、对象头:包含两个部分
(1)、对象自身的运行时数据:常用的哈希码、锁的状态、并发编程里面的偏向锁、时间戳等等。
(2)、类型指针:指向的是这个类的元数据,JVM通过这个类型指针来找个这个对象是哪个类的实例。
2、实例数据:程序代码中所定义的各种类型的字段内容,就是我们Use中定义的id和name的值。
3、对齐填充:就是由JVM来做的,就是一个占位,HotSpot要求对象的起始地址必须是八个字节的整数倍也就是对象的大小一定是八个字节的整数倍,所以说如果对象头加实例数据不是八个字节的整数倍时就需要对齐填充。
三、对象的访问
访问对象有两种方式:
1、句柄:
什么叫使用句柄?可以理解为一个门牌号码(指示牌),例子在内存中划分一块区域来作为一个句柄池,由句柄池来持有这个对象在哪个位置。例如:如果java栈要访问对象时,先由对象的引用先指向句柄池,不直接指向对象,再由句柄池来拿到对象的实例数据和对象的类型数据。
2、直接指针
就是java栈要拿某个对象的实例数据时,就直接指向了这个对象的实例数据地址,不需要通过句柄池来转一遍。
两种访问方式再虚拟机中都存在使用句柄的好处,因为指向的是句柄池,不是一个固定的值,如果当对象发生移动的时候 移动是非常普遍的,因为会有垃圾收集。移动后我只需要改变句柄池的指向地址即可。使用直接指针的好处明显是少了句柄的转发,访问对象速度明显提升。Hotspot虚拟机中用的是直接指针的访问方式。
四、实战演练各种OOM溢出
1、堆溢出(代码示例)
import java.util.LinkedList;
import java.util.List;
/**
* @ClassName MyOOMTest$
* @Description TODO
* QQ号:48793696
* @date 2019/5/14 16:58
* @Version 1.0
**/
public class MyOOMTest {
/***
*
* VM参数:-Xms5m -Xmx5m -XX:+PrintGC
*/
public static void main(String[] args){
//String[] strings = new String[100000000];
List<Object> list = new LinkedList<Object>();
int temp = 0;
while(true){
temp++;
if(temp%10000 == 0)System.out.println("temp = "+temp);
list.add(temp);
}
}
}
执行结果抛出了OutOfMemory:java.lang.OutOfMemoryError: GC overhead limit exceeded,JVM发现内存不足的时候GC进行回收,但是回收的条件非常苛刻,导致没有回收的速度没有跟上新建对象的速度,导致了堆溢出。
import java.util.LinkedList;
import java.util.List;
/**
* @ClassName MyOOMTest$
* @Description TODO
* @Author QQ号:48793696
* @date 2019/5/14 16:58
* @Version 1.0
**/
public class MyOOMTest {
/***
*
* VM参数:-Xms5m -Xmx5m -XX:+PrintGC
*/
public static void main(String[] args){
String[] strings = new String[100000000];
}
}
执行结果也会抛出异常,但是我们看到的现象是GC的次数明显减少、抛出的是java.lang.OutOfMemoryError: Java heap space。
前面的代码因为一直在new Object(),一直累积,到最后只有一点点空间不够进行分配,然后GC试着进行回收,一直不停尝试,直到达到超过回收的最大次数,GC也发现臣妾做不到,认命之后抛出GC overhead limit exceeded。
String[] strings = new String[100000000];这个相当于分配一个巨型数组,类似于超过5M的东西硬要塞到5M的内存中去,这时候GC还是意思下的去回收下,然后发现还是塞不下,就干净利落的抛出java.lang.OutOfMemoryError: Java heap space
我们可以根据这两个异常来确定我们写代码的时候出现什么问题?
如果出现GC overhead limit exceeded,很有可能你在代码中出现了某种循环里面在不停的分配对象,虽然不大,但是分配的太多,把堆撑爆了。如果出现java.lang.OutOfMemoryError: Java heap space,在分配的时候,有巨型对象在分配,以致于超出了堆的大小。
1、方法区溢出
代码示例
import java.util.LinkedList;
import java.util.List;
/**
* @ClassName MyMetaSpaceTest$
* @Description TODO
* @Author QQ号:48793696
* @date 2019/5/14 17:46
* @Version 1.0
**/
public class MyMetaSpaceTest {
/***
*
* VM参数:-XX:MaxMetaspaceSize=3M
*/
public static void main(String[] args){
List<Object> list = new LinkedList<Object>();
int temp = 0;
while(true){
temp++;
if(temp%10000 == 0)System.out.println("temp = "+temp);
list.add(temp);
}
}
}
执行的结果为:
这是因为我们在分配方法区的内存太小了,甚至不能保证我们JVM本身的运行。
- 虚拟机栈溢出
代码示例:
public class MyStackOOMTest {
/***
* VM参数:-Xss256k
*/
private int stactSize = 1;
//private void recursive(int a ,String b){
private void recursive(){
stactSize++;
//recursive(a,b);
recursive();
}
public static void main(String[] args){
MyStackOOMTest myStackOOMTest = new MyStackOOMTest();
try {
//myStackOOMTest.recursive(10,"Allen");
myStackOOMTest.recursive();
}catch (Throwable e){
System.out.println("stactSize ========"+myStackOOMTest.stactSize);
e.printStackTrace();
}
}
}
执行结果为:
如果把递归的方法加上两个参数(int a,String b)执行的结果为
这样的结果比之前栈深度小了,栈大小我们也没有改变,我们只增加了递归方法的参数,意味着什么? 这代表我们的栈帧变大了,栈帧在之前讲过包含局部变量、操作数栈、帧数据区,局部变量包含这个方法的入参。这也就是说栈大小不变的情况下,如果参数越多,栈越浅,因为栈帧越大。
如果出现java.lang.StackOverflowError这种异常,要检查代码中是否有无限递归存在。很多出去面试会被问到二叉树的算法,问了递归还要问非递归,绝对是有原因的。有同学知道为什么吗?递归:如果用递归来实现,每次方法都会被打包成一个栈帧来执行,而每个方法打包成一个栈帧的过程要做的事情比较多,时间比较久,尤其是当方法的参数更多的时候耗费的时间就更长。非递归:非递归一般都是采用循环来实现树的遍历,循环来实现就取消了每次都要来打包成栈帧,出栈入栈的过程,所以说非递归算法或在树的遍历上天生就要比递归的方式速度要快。当树很浅的时候当然可以用递归,主要就是要控制递归的使用。
1、直接内存溢出
代码示例
import java.nio.ByteBuffer;
/**
* @ClassName MyDirectMemoryTest$
* @Description TODO
* @Author QQ号:48793696
* @date 2019/5/15 13:53
* @Version 1.0
**/
public class MyDirectMemoryTest {
/***
*VM参数:-Xmx10M -XX:MaxDirectMemorySize=10M
* */
public static void main(String[] args){
ByteBuffer.allocateDirect(1024 * 1024 * 14);
}
}
执行结果:
java.lang.OutOfMemoryError: Direct buffer memory如果大家在代码中看到这类异常,通常发生在我们的NIO通讯中,在我们使用了NIO,不管你用的是dubbo还是netty框架,假如抛出了这类异常,如果检查发现运行时设置的内存又发现很小,我们就要考虑是不是发生了本机内存溢出了。
怎么判断对象的存活?
1、引用计数算法
举个例子: 对象A(引用)——》对象B,对象C(引用)——》对象B ,会有一张表记录对象B被引用了两次1+1,对象A和对象B被引用0次。
优点:快、方便、实现简单,引用计数很方便,GC来决定哪个对象需要回收时,只要来判断这个计数是不是为0,表示没有引用的对象就可以为回收。
缺点:对象A(引用)——》对象B,对象B(引用)——》对象A,在表中记录对象A、 对象B都引用1次,这两个对象互相引用对方,导致他们的计数都不为0,于是导致GC永远不会回收。
适用场景:因为引用计数这个致命的问题导致Hotspot没有采用引用计数算法,但是早期的微软公司的COM技术、使用ActionScript 3的FlashPlayer、Python、都使用了引用计数算法来进行内存管理。
2、可达性分析
jvm中用的就是可达性分析。
算法基本思想:
通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索。搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(也就是说从GC Roots)到这个对象不可达)时,则证明此对象是不可用的。如图所示,对象Object5、Object6、Object7虽然互相有关联,但它们到GC Roots是不可达的,所以他们将会被判定为是可回收的对象。
GC Roots对象:
在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象 例如:static User u = new User();
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
所谓“GC roots”,或者说tracing GC的“根集合”,就是一组必须活跃的引用。 例如说,这些引用可能包括:所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
Tracing GC的根本思路就是:给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,其余对象(也就是没有被遍历到的)就自然被判定为死亡。注意再注意:tracing GC的本质是通过找出所有活对象来把其余空间认定为“无用”,而不是找出所有死掉的对象并回收它们占用的空间。
对象的引用
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与"引用有关"。
JDK中分为四种引用类型:
1、强引用
强引用就是指在我们程序代码之中普遍存在的,类似"Object obj = new Object()"这类引用。
2、软引用(SoftReference)
软引用用来描述一些还有用但非必须的对象,对于软引用关联的对象,在系统将要发生OOM(内存溢出异常)之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
3、弱引用(WeakReference)
弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联对象只能生存到下一次垃圾收集之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
4、虚引用(PhantomReference)
虚引用也成为幽灵引用或者幻影引用,它是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。举个例子:Object obj 定义成虚引用,obj.是拿不到实例的,这个基本用不着。
代码示例
软引用:
import java.lang.ref.SoftReference;
import java.util.LinkedList;
import java.util.List;
/**
* @ClassName MyReferenceTest$
* @Description TODO
* @Author QQ号:48793696
* @date 2019/5/15 19:03
* @Version 1.0
**/
public class MySoftReferenceTest {
public static class Employee {
public int salary = 0;
public String name = "";
public Employee(int salary, String name) {
super();
this.salary = salary;
this.name = name;
}
@Override
public String toString() {
return "Empolyee [name = " + name + ",salary = " + salary + "]";
}
}
//VM参数:-Xms10m -Xmx10m -XX:+PrintGC
public static void main(String[] args) {
Employee e = new Employee(100, "allen");
//软引用包装
SoftReference<Employee> eSoft = new SoftReference<Employee>(e);
e = null;
System.out.println("=======beforeGC=============" + eSoft.get());
System.gc();//演示如果内存足够,是不会回收SoftReference对象
System.out.println("=======AfterGC=============" + eSoft.get());
//模拟一个内存溢出的场景
List<byte[]> list = new LinkedList<byte[]>();
try {
for (int i = 0; i < 100; i++) {
System.out.println("**********************" + eSoft.get());
list.add(new byte[1024*1024*1]);
}
} catch (Throwable e1) {
System.out.println("======throwable========"+eSoft.get());
}
}
}
执行结果为;
我们看到软引用的对象,在系统将要发生OOM(内存溢出异常)之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。我们看到了两次垃圾回收。
弱引用:
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
/**
* @ClassName MyWeakReferenceTest$
* @Description TODO
* @Author QQ号:48793696
* @date 2019/5/17 18:08
* @Version 1.0
**/
public class MyWeakReferenceTest {
public static class Employee {
public int salary = 0;
public String name = "";
public Employee(int salary, String name) {
super();
this.salary = salary;
this.name = name;
}
@Override
public String toString() {
return "Empolyee [name = " + name + ",salary = " + salary + "]";
}
}
public static void main(String[] args) {
Employee e = new Employee(100, "allen");
//弱引用包装
WeakReference<Employee> eSoft = new WeakReference<Employee>(e);
e = null;
System.out.println("=======beforeGC=============" + eSoft.get());
System.gc();//演示如果内存足够,是不会回收WeakReference对象
System.out.println("=======AfterGC=============" + eSoft.get());
}
}
执行结果为:
我们可以看到垃圾发生时,不管内存够不够都对弱引用进行回收。
软引用、弱引用使用场景:用在内存资源的情况下,缓存可以用强引用,二级缓存可以用弱引用。为什么一般使用强引用?因为资源一般都会够,软引用、弱引用时JDK2才出现的。
更多推荐
所有评论(0)