从JDK1.8的JVM内存模型和GC剖析项目开发中常见的Java内存溢出及其解决办法

Java内存溢出是实际开发中比较常见的问题。想要合理的分配和利用内存资源,让应用程序更高效的运行,更好的利用系统资源,则需要对Java内存模型进行深入的理解。

首先,我们先回忆下

Java内存模型

    JVM中内存通常划分为两个部分,分别为堆内存与栈内存,栈内存主要用运行线程方法存放本地暂时变量与线程中方法运行时候须要的引用对象地址。堆内存中存放的是全部的对象信息。相比栈内存,堆内存能够所大的多,所以JVM主要通过对堆内存划分不同的功能区块实现对堆内存中对象管理。

堆内存划分

在JDK7以及其前期的JDK版本号中。堆内存通常被分为三块区域:新生代内存(young generation)、老年代内存(old generation)、永久内存(Permanent Generation for VM Matedata),一个对象被创建以后首先被放到Nursery中的Eden内存中,假设存活期超两个Survivor之后就会被转移到长时内存中(Old Generation)中;永久内存中存放着对象的方法、变量等元数据信息。通过假设永久内存不够。我们就会得到例如以下错误:

java.lang.OutOfMemoryError: PermGen

而在JDK8中情况发生了明显的变化,就是普通情况下你都不会得到这个错误,原因在于JDK8中把存放元数据中的永久内存从堆内存中移到了本地内存(native memory)中,这样永久内存就不再占用堆内存。它能够通过自己主动增长来避免JDK7以及前期版本号中常见的永久内存错误(java.lang.OutOfMemoryError: PermGen),或许这个就是你的JDK升级到JDK8的理由之中的一个吧。

当然JDK8也提供了一个新的设置Matespace内存大小的參数。通过这个參数能够设置Matespace内存大小,这样我们能够依据自己项目的实际情况,避免过度浪费本地内存,达到有效利用。

-XX:MaxMetaspaceSize=128m 设置最大的元内存空间128兆

 注意:假设不设置JVM将会依据一定的策略自己主动添加本地元内存空间。

假设你设置的元内存空间过小,你的应用程序可能得到下面错误:

java.lang.OutOfMemoryError: Metadata space


java1.8之前内存区域分为方法区、堆内存、虚拟机栈、本地方法栈、程序计数器。

    方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。很多人都更愿意把方法区称为“永久代”(Permanent Generation)。从jdk1.7已经开始准备“去永久代”的规划,jdk1.7的HotSpot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中。

    在jdk1.8中,永久代已经不存在,存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。

    元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小: 
  -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 
  -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。 
  除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性: 
  -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 
  -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集 

注意:如果不设置JVM将会根据一定的策略自动增加本地元内存空间。

如果你设置的元内存空间过小,你的应用程序可能得到以下错误:

java.lang.OutOfMemoryError: Metadata space

    在Java7之前,HotSpot虚拟机中将GC分代收集扩展到了方法区,使用永久代来实现了方法区。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。而在Java8中,已经彻底没有了永久代,将方法区直接放在一个与堆不相连的本地内存区域,这个区域被叫做元空间。  

    常量池里存储着字面量和符号引用。 符号引用包括:1.类的全限定名,2.字段名和属性,3.方法名和属性。

    字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。)。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。具有以下特点:
1.字符串池常量池在每个VM中只有一份,存放的是字符串常量的引用值,存放在堆中.
2.class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。
3.运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

堆内存不够最常见的错误就是OOM(OutOfMemoryError)
栈内存溢出最常见的错误就是StackOverflowError。程序有递归调用时候最容易发生

Java程序运行时的内存分配策略

一、静态分配

    对应JVM内存中的静态存储区(方法区),主要存放静态数据、全局static数据和常量。这块内存在编译时已分配好,并在程序的整个运行周期都存在。

二、栈式分配

    对应JVM内存中的栈区,当方法被执行时,方法体内的局部变量(包括基本数据类型、对象引用)都在栈上创建,并在方法执行结束是释放它们占用的内存。因为栈内存分配运算内置与处理器的指令集中,效率很高,但是同时内存容量也相对局限

三、堆式分配

    对应JVM内存中的堆区,又称动态内存分配,通常就是指程序运行时创建的对象占用的内存。这部分内存在不使用时将会由GC回收。

思考一下:堆与栈的区别

    在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。
当在一段方法块中定义一个变量时,Java就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。


    堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。
在堆中分配的内存,将由Java垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

    局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。

    成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。

java中的四种引用类型:

    强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;
如:User u=new User(); 
User u 存在于栈里面,new User() 存在于堆里面的,栈通过 = 号,将堆对象引用起来,叫强引用(当前这种形式称为显式的强引用(强可及对象))

    软引用(SoftReference):只有在内存空间不足时,才会被回的对象;

    弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;

    虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

    软引用和弱引用,这两个引用是可以随时被虚拟机回收的对象,我们将一些比较占内存但是又可能后面用的对象,比如Bitmap对象,可以声明为软引用或弱引用。但是注意一点,每次使用这个对象时候,需要显示判断一下是否为null,以免出错。

    JVM的垃圾回收机制中,判断一个对象是否死亡,并不是根据是否还有对象对其有引用,而是通过可达性分析。对象之间的引用可以抽象成树形结构,通过树根(GC Roots)作为起点,从这些树根往下搜索,搜索走过的链称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明这个对象是不可用的,该对象会被判定为可回收的对象。

那么哪些对象可作为GC Roots(GC 会自动回收的对象)呢?主要有以下几种:
    1.虚拟机栈(栈帧中的本地变量表)中引用的对象。
    2.方法区中类静态属性引用的对象。
    3.方法区中常量引用的对象
    4.本地方法栈中JNI(即一般说的Native方法)引用的对象。
    5.Thread —活着的线程

开发中常见的内存溢出

1、栈溢出

    java栈空间是线程私有的,是java方法执行的内存模型。每个方法执行时都会在java栈空间产生一个栈帧,存放方法的变量表,返回值等信息,方法的执行到结束就是一个栈帧入栈到出栈的过程。

    所以栈溢出的原因一般是循环调用方法导致栈帧不断增多,栈深度不断增加,最终没有内存可以分配,出现StackOverflowError

2、堆溢出

    Java堆是线程共有的区域,主要用来存放对象实例,几乎所有的java对象都在这里分配内存,也是JVM内存管理最大的区域。java堆内存分年轻代和年老代,堆内存溢出一般是年老代溢出。当程序不断地创建大量对象实例并且没有被GC回收时,就容易产生内存溢出。当一个对象产生时,主要过程是这样的:

JVM首先在年轻代的Eden区为它分配内存;
    若分配成功,则结束,否则JVM会触发一次Young GC,试图释放Eden区的不活跃对象;
    如果释放后还没有足够的内存空间,则将Eden区部分活跃对象转移到Suvivor区,Suvivor区长期存活的对象会被转移到老年代;
    当老年代空间不够,会触发Full GC,对年老代进行完全的垃圾回收;
    回收后如果Suvivor和老年代仍没有充足的空间接收从Eden复制过来的对象,使得Eden区无法为新产生的对象分配内存,即溢出。

3、PermGenspace永久区内存溢出

    永久代也是java堆内存的一部分,主要用来存放Class的相关信息,如类名,访问修饰符等等。一般永久代溢出的原因是动态加载大量的Class并且没有及时被GC回收。只能通过调整永久代内存参数的方式解决。

4、unable to create native thread 内存不足导致无法创建本地线程

    我们知道,操作系统对每个进程的内存都是有一定限制的,当堆内存和非堆内存分配过大时,剩余的内存不足以创建足够的线程栈,就会产生OutOfMemoryError。因此我们可以增大进程占用的总内存或减小堆内存等来解决问题。

内存泄漏与内存溢出的区别

内存泄漏 Memory Leak

    指程序申请了内存后(new),用完的内存没有释放(delete),一直被某个或某些- 实例所持有却不再被使用导致 GC 不能回收

    生活例子 : 电热水器洗完澡不关水,其他人用就没热水的情况

    内存泄漏是导致内存溢出的原因之一;内存泄漏累积起来就会造成内存溢出

    内存泄漏可以通过完善代码来避免

内存溢出 Out Of Memory

    指程序申请内存时,没有足够的内存空间使用

    生活例子 : 水杯满了还往里面加水

内存溢出可以通过调整配置来减少发生频率,无法彻底避免

常见内存溢出处理:

绝大部分的内存溢出原因是由于图片太大引起的,一般我们使用软引用/弱引用解决由于图片资源过大的内存溢出;

    但是API9以后,GC机制改变了,    建议使用LruCache缓冲图片资源。注意临时Bitmap对象的及时回收,先recycle(),后致空 。尽量避免Try catch某些大内存分配的操作。如加载Bitmap时:缩放比例、解码格式、局部加载(图片大于手机屏幕)

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值