JVM内存管理与垃圾回收机制

一、内存溢出和内存泄漏

1.1、内存溢出(OOM  out of memory) ----内存不够用。

比如上厕所坑位不够

内存溢出是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;

1.2、内存泄漏 (Memory Leak)    -----内存空间浪费,内存用完未释放

比如站着茅坑不出来,而且是永久占着

内存泄漏是指程序在申请内存后,无法释放已申请的内存空间。一次内存泄露危害可以忽略,但内存泄露堆积的后果很严重,早晚会占光所有的可用内存。所以内存泄漏最终会导致内存溢出!

1.3、4类内存泄漏(按照发生方式分)

常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。 
偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。 
一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。 
隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。在实际中也比较难检测到,所以更麻烦。

1.4、内存溢出的一般原因及解决办法

1.4.1、内存中加载的数据量过于庞大,如一次从数据库取出过多数据;       -----》检查错误日志,是否其他异常原因引起。
1.4.2、集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;   ------ ↑
1.4.3、代码中存在死循环或循环产生过多重复的对象实体;                        ------ ↑
1.4.4、启动参数内存值设定的过小;   -----》修改JVM启动参数,增加内存。(-Xms,-Xmx参数一定不要忘记加)

1.5、内存泄漏的一般原因及解决方法

1.5.1、静态集合类引起内存泄露

像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们仍然一直被Vector等引用着。这样就造成了内存泄漏。

//循环申请50次 Object对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身
//(object=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。
//因此还必须从Vector中删除,例如将Vector对象设置为null。

Static Vector vector = new Vector(20);
for (int i = 1; i<50; i++){
    Object object = new Object();
    vector.add(object);
    object = null;
}

1.5.2、监听器 

在Java中,我们经常和监听器打交道,通常一个应用会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。所以最好在释放对象的时候显式删除这些监听器,避免内存泄漏。

1.5.3、各种连接 

比如数据库连接(dataSourse.getConnection()),网络连接(socket)和IO连接,除非其显式的调用了close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。

1.5.4、单例模式

如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露。不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被JVM正常回收,导致内存泄露。

class A{

    public A(){
        B.getInstance().setA(this);
    }
....
}

//B类采用式单例
//B持有对A的引用,则A将不会被GC回收,造成内存泄漏
class B{
    private A a;
    private static B instance=new B();
    public B(){}
    public static B getInstance(){
        return instance;
    }
    public void setA(A a){
        this.a=a;
    }
    //getter()
}

 

二、JVM内存划分(JDK1.7 & JDK1.8

* * * * * * * * * *  JDK1.7下JVM内存划分  * * * * * * * * * *

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间。有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。JVM内存模型可以分为两个部分,其中堆和方法区是所有线程共享的,而虚拟机栈,本地方法栈和程序计数器是线程私有的。如下图所示:

JVM内存管理与垃圾回收机制_第1张图片

2.1、程序计数器   ---  Program Counter Register

程序计数器是JVM中一块较小的内存区域,保存着当前线程执行的虚拟机字节码指令的内存地址(可以看作当前线程所执行的字节码的行号指示器)。如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址(可以理解为代码中的行号),如果正在执行的是native方法,这个计数器的值为undefined。

JVM的多线程是通过线程轮流切换并分配CPU执行时间片的方式来实现的,任何一个时刻,一个CPU都只会执行一条线程中的指令。为了保证线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程间的程序计数器独立存储,互不影响。

程序计数器是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域,因为程序计数器是由虚拟机内部维护的,不需要开发者进行操作。

2.2、虚拟机栈   ---   Virtual Machine Stacks

JVM内存管理与垃圾回收机制_第2张图片

虚拟机栈是线程隔离的,并不属于线程共享区域。每创建一个线程时就会对应创建一个Java栈,每个线程都有自己独立的虚拟机栈。这个栈中又会包含多个栈帧,每调用一个方法时就会向虚拟机栈创建并压入一个栈帧,栈帧存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用到最终返回结果的过程,就对应着一个栈帧从入栈到出栈的过程。虚拟机栈是一个后进先出的数据结构,线程运行过程中,只有处于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法,当前活动帧栈始终是虚拟机栈的栈顶元素。

虚拟机栈描述的是Java方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表。局部变量表存放了编译期可知的各种基本数据类型(byte、char、short、int、float、long、double、boolean)和对象引用类型(reference类型)。通常我们所说的“栈内存”指的就是局部变量表这一部分。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多少内存是固定的,运行期间不会改变局部变量表的大小。

64位的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。

在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

2.3、本地方法栈   ---   Native Method Stacks

本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点以及都能抛出StackOverflowError和OutOfMemoryError异常。不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。HotSpot虚拟机不区分虚拟机栈和本地方法栈,两者是一块的。

2.4、方法区   --- Method Area

方法区是所有线程共享的内存区域。保存着被加载过的每一个类的信息(虚拟机加载的类信息:类的版本、字段、方法、接口、常量;静态变量;即时编译器编译后的代码等数据);这些信息由类加载器在加载类的时候,从类的源文件中抽取出来;static变量信息也保存在方法区中。

可以看做是将类(Class)的元数据,保存在方法区里。方法区逻辑上属于堆的一部分,为了与堆进行区分,又叫“非堆”

HotSpot虚拟机使用永久代来实现方法区,使得HotSpot虚拟机的垃圾收集器可以像管理堆内存一样来管理这部分内存,能省去专门为方法区编写内存管理代码工作。所以开发者喜欢将方法区称为永久代,本质上两者并不等价,对于其他虚拟机来说不存在永久代的概念。

当有多个线程都用到一个类的时候,而这个类还未被加载,则应该只有一个线程去加载类,让其他线程等待,因为他是线性共享的。方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。JVM也可以允许用户和程序指定方法区的初始大小,最小和最大限制。方法区同样存在垃圾收集,因为通过用户定义的类加载器可以动态扩展Java程序,这样可能会导致一些类,不再被使用,变为垃圾。这时候需要进行垃圾清理。

2.5、堆区   ---   Heap

JVM管理的最大的一块内存区域,存放对象的实例,是线程共享区,在虚拟机启动时创建堆是垃圾收集器管理的主要区域,因此也被称为“GC堆”。

JVM内存管理与垃圾回收机制_第3张图片

堆内存又分为年轻代(又分为eden、S0、S1三部分)、老年代和永久代。但是永久代严格来说并不属于堆内存中的一部分,同时jdk1.8之后永久代已经被移除。

GC(垃圾回收器)对年轻代中的对象进行回收被称为Minor GC,用通俗一点的话说年轻代就是用来存放年轻的对象,年轻对象是什么意思呢?年轻对象可以简单的理解为没有经历过多次垃圾回收的对象,如果一个对象经历过了一定次数的Minor GC,JVM一般就会将这个对象放入到年老代,而JVM对年老代的对象的回收则称为Major GC。

大部分对象刚创建的时候,JVM会将其分布到Eden区域。当Eden区域中的对象达到一定的数目的时候,就会进行Minor GC,经历这次垃圾回收后所有存活的对象都会进入两个Suvivor Space中的一个。

同一时刻两个Survivor Space,即S0和S1中总有一个总是空的。年轻代中的对象经历过了多次的垃圾回收就会转移到年老代中。

当申请不到空间时会抛出 OutOfMemoryError。

* * * * * * * * * *  JDK1.8下JVM内存划分  * * * * * * * * * *

先看一张JDK1.7与JDK1.8内存结构对比图:

JVM内存管理与垃圾回收机制_第4张图片

原图地址:https://www.processon.com/view/link/5b61ea2ae4b0555b39cfa842

在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotSpot虚拟机对方法区的实现为永久代
在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotSpot中的永久代
在JDK1.8 hotSpot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace) 

元空间的大小仅受本地内存限制,可以通过以下参数来指定元空间大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集

-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

 

三、JVM垃圾回收机制

垃圾回收机制---GC,Java将内存管理交给垃圾回收机制,这是因为在面向对象编程中一个对象的生命周期往往无法预料,所以我们无法为每个对象指定回收时机。但是我们可以采用System.gc()Runtime.getRuntime().gc()进行请求垃圾回收,可以使用对象的finalize()对必要资源在垃圾回收之前进行处理。JVM垃圾回收机制使得Java程序员不需要考虑内存管理,Java中的对象不再有作用域的限制,只有对象的引用有作用域,可以有效的防止内存泄漏,更有效的使用可用内存。Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法都要做两件基本的事情。一是发现无用对象,二是回收无用对象占的内存空间,释放该空间。

3.1、如何判断对象已“死”?

Java堆中存放着几乎所有的对象实例,垃圾回收器在堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经“死去”。然后将“死去”的对象回收。

3.1.1、引用计数法

引用计数法描述的算法为:给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;每当引用失效时,计数器就-1;任何时刻计数器为0的对象就是再没有引用指向的对象,即对象已“死”。引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个比较好的算法。比如Python语言就是采用的引用计数法来进行内存管理的。但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因是引用计数法无法解决对象的循环引用问题(例如两个对象互相引用,则会出现引用计数器永远不为0的现象,对象永远不会“死”,也容易造成内存泄漏)。

3.1.2、可达性分析算法

Java采用可达性分析算法来判断对象是否存活。通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。

JVM内存管理与垃圾回收机制_第5张图片

对象Object5 - Object7之间虽然彼此还有联系,但是它们到 GC Roots 是不可达的,因此它们会被判定为可回收对象。

在Java语言中,可作为GC Roots的对象包含以下几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中(Native方法)引用的对象
在JDK1.2以前,一个对象只有被引用或者没有被引用两种状态。在JDK1.2之后,Java对引用的概念做了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,这四种引用的强度依次递减(强软弱虚)。

对象的自我救赎:

即使在可达性分析算法中不可达的对象,也并非"非死不可",这时候他们暂时处在"缓刑"阶段。要宣告一个对象的真正死亡,至少要经历两次标记过程: 如果对象在进行可达性分析之后发现①没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,虚拟机会将这两种情况都视为"没有必要执行",此时的对象才是真正"死"的对象。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它(这里所说的执行指的是虚拟机会触发finalize()方法)。②finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象在finalize()中成功拯救自己(只需要重新与引用链上的任何一个对象建立起关联关系即可),那在第二次标记时它将会被移除出"即将回收"的集合;如果对象这时候还是没有逃脱,那基本上它就是真的被回收了。

注意:任何一个对象的finalize()方法都只会被系统自动调用一次,如果相同的对象在逃脱一次后又面临一次回收,它的finalize()方法不会被再次执行,因此第二次自救行动会失败。

3.1.3、回收方法区

方法区(永久代)的垃圾回收主要收集两部分内容:废弃常量和无用类
回收废弃常量和回收Java堆中的对象十分类似。以常量池中字面量(直接量)的回收为例,假如一个字符串"abc"已经进入了常量池中,但是当前系统没有任何一个String对象引用常量池中的"abc"常量,也没有其他地方引用这个字面量,如果此时发生GC并且有必要的话,这个"abc"常量会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
判定一个类是否是"无用类"则相对复杂很多。类需要同时满足下面三个条件才会被算是"无用类":
1)该类的所有实例都已经被回收(即在Java堆中不存在任何该类的实例)
2)加载该类的ClassLoader已被回收
3)该类对应的Class对象没有任何其他地方被引用,无法在任何地方通过反射访问该类的方法

同时满足上面三个条件的无用类可能被回收,但不一定。

3.2、垃圾回收算法

3.2.1、标记-清除算法

标记-清除算法是最基础的收集算法。算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象(标记过程见上文)。后续的收集算法都是基于这种思路并对其不足加以改进而已。
“标记-清除”算法的不足主要有两个:

效率问题:标记和清除这两个过程的效率都不高

空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。标记-清除算法前后内存状态见图:

JVM内存管理与垃圾回收机制_第6张图片

3.2.2、复制算法---新生代回收算法

“复制”算法是为了解决“标记-清除”的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等的复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。算法的执行流程如下图:

JVM内存管理与垃圾回收机制_第7张图片

现在的商用虚拟机(包括HotSpot)都是采用这种收集算法来回收新生代

新生代中98%的对象都是"朝生夕死"的,所以并不需要按照1 : 1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保。
HotSpot默认Eden与Survivor的大小比例是8 : 1,也就是说Eden:Survivor From : Survivor To = 8:1:1。所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。

HotSpot实现的复制算法流程如下:

当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域,并将Eden区和From区清空。
当后续Eden区又发生Minor gc的时候,会对Eden区和To区进行垃圾回收,存活的对象复制到From区,并将Eden区和To区清空
部分对象会在From区域和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还存活,就存入老年代。

JVM内存管理与垃圾回收机制_第8张图片

3.3.3、标记-整理算法

复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法(因为老年代的对象都是存活较久的对象)。
针对老年代的特点,提出了一种称之为标记-整理算法。标记过程仍与标记-清除过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。流程图如下:
 

JVM内存管理与垃圾回收机制_第9张图片

3.3.4、分代收集算法

当前JVM垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。
一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。

关于上文提到的Minor GC和Major GC(Full GC)

Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。Full GC 又称为老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
 

 

参考博客:

https://blog.csdn.net/Hollake/article/details/92762180

https://blog.csdn.net/q5706503/article/details/84640762

https://blog.csdn.net/qq_37598011/article/details/88182781

https://blog.csdn.net/ztx114/article/details/79400789

 

以上!

 

你可能感兴趣的:(Java,Java虚拟机)