Java虚拟机GC总结

澳门新葡亰网站注册 1

关于 JVM
内存模型以及垃圾回收的文章网上很多,自己以前也看过很多,但是却从来也没有系统的去了解学习过,这次正巧看到一本讲解
JVM 的好书 – 周志明老师的《深入理解 Java
虚拟机》,然后就花了点时间,认真系统的学习了一遍,尽管还没有看完,但是已经爱耐不住,觉得要写点东西出来,写的过程是一个思考融汇的过程,也是一个知识升华的过程。

1. Java内存区域与内存溢出异常

这篇主要简单分享一下关于 JVM
内存模型、内存溢出、内存分代、以及垃圾回收算法的相关知识。当然在原书中,这几部分作者都花了不少篇幅去讲解。如果这篇文章让你对相关知识产生了兴趣而意犹未尽,推荐去阅读原书。

Java内存区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分成方法区、堆、本地方法栈、Java虚拟机栈、程序计数器共五个数据区域,下面做下简单介绍:

1)方法区:主要存放常量、静态变量、已被虚拟机加载的类信息。

2)堆:存放对象实例。

3)Java虚拟机栈:描述Java方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,栈帧主要存放局部变量表、操作栈、动态链接、方法出口等信息。

4)本地方法栈:与Java虚拟机栈的功能类似,主要是为Java虚拟机的Native方法提供服务。

5)程序计数器:当前线程所执行的字节码的行号指示器。

其中方法区、堆是线程共享的,本地方法区、Java虚拟机栈、程序计数器是线程独享的

JVM 内存区域

都知道 JVM 的内存区域分为5个部分,如果有疑惑,可以参看之前的一篇文章
– JVM
内存区域介绍。

这里也简单罗列一下 JVM 的五部分

  • 程序计数器这是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器,线程私有。
  • Java 虚拟机栈它是
    Java方法执行的内存模型,每一个方法被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程,线程私有。
  • 本地方法栈跟虚拟机栈类似,不过本地方法栈用于执行本地方法,线程私有。
  • Java
    堆该区域存在的唯一目的就是存放对象,几乎应用中所有的对象实例都在这里分配内存,所有线程共享。
  • 方法区它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,所有线程共享。

内存溢出异常-OutOfMemoryError

内存溢出包括Java堆内存溢出、Java虚拟机栈溢出和本地方法栈溢出,下面做下简单介绍:

1)Java堆内存溢出:对象分配的内存空间超过最大堆的容量限制。
例子:

while(true){
new Object();
}
  1. 栈内存溢出:虚拟机在扩展栈时无法申请到足够的内存空间。

3)栈溢出-StackOverflowError:线程请求的栈深度大于虚拟机所允许的最大栈深度。
例子:

public vod test(){
  test();
}

有关 OOM

都知道,任何一个应用在启动后,操作系统分配给它的内存一定是有限的,所以如何合理有效的管理内存,就变得尤为重要。

而从上节可知,我们一般讨论的对象内存分配均发生在 Java
堆上。所以这里说的内存管理大部分情况下即指对 Java
堆内存。而程序计数器、虚拟机栈他们随着线程生而生,亡而亡,所以他们内存相对比较好管理,出现的问题也比较少。

一个应用启动后,不停运行,不停的执行命令,创建对象,而这些对象,大都存放在堆内存区域。这部分区域的大小是有限的,而需要生成的对象是无限的,当某一次创建对象时发现堆内存实在没有空间可用来创建对象的时候,JVM
就会爆出 OutOfMemoryError 异常(后文统称 OOM),程序就会挂掉。

上面只是说明了一下表象。其实 OOM 远不是上面说的那么简单。如果要理解
OOM,这里还有一些其他知识需要说明。

  • OOM 发生前其实 JVM 会进行内存的垃圾回收(GC)。
  • 垃圾回收有多种不同的实现算法。
  • 澳门新葡亰网站注册,为了更好的管理内存,堆内存进行了分代。
  • 堆内存的新生代和老年代的垃圾回收算法不一致。

其实,这里的知识需要综合理解,你才会对 OOM 有一个全面的认识。

2. 垃圾回收

垃圾回收一般会涉及到如下三个问题:
1)那些内存需要回收?
2)什么时候进行回收?
3)怎么进行回收?
我们常说的GC一般是发生在Java堆区,下面先了解下Java堆区的结构划分。Java堆通常被划分成两个不同的区域:新生代
( Young )、老年代 ( Old ),新生代 ( Young )
又被划分为三个区域:Eden、From Survivor、To
Survivor,由此得到Java堆结构图如下。

澳门新葡亰网站注册 1

Java堆结构图

从上图可以看出: 堆大小 = 新生代 + 老年代。
默认情况下,

新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 
Eden : from : to = 8 : 1 : 1 

虚拟机每次只会使用 Eden 和其中的一块 Survivor
区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
因此,新生代实际可用的内存空间为90%的新生代空间。
回到上面的第一个问题,可知Java堆里面的新生代、老年代内存需要回收,元空间占有的内存需要回收,但是什么时候进行回收呢,接下来我们会探讨这个问题。

内存分代

一个应用启动,操作系统会给他分配一个初始的内存大小,由上可知,这部分内存大部分应该属于堆内存,JVM
为了更好地利用管理这部分内存,对该区域做了划分。一部分成为新生代,另一部分称为老年代。

一开始对象的创建都发生在新生代,随着对象的不断创建,如果新生代没有空间创建新对象,将会发生
GC ,这时的 GC 称之为 Minor GC,位于新生代的对象每经过一次 Minor GC
后,如果这个对象没有被回收,则为自己的标记数加1,这个标记数用于标识这个对象经历了多少次的
Minor GC,对于 Sun 的 Hotspot 虚拟机,如果这个次数超过 15
,该对象才会被移动到老年代。

随着时间的推移,如果老年代也没有足够的空间容纳对象,老年代也会试着发起
GC,这时的 GC 被称为 Full GC。

相比 Minor GC,Full GC 发生的次数比较少,但是每发生一次 Full
GC,整个堆内存区域都需要执行一次垃圾回收,这对程序性能造成的影响比 Minor
GC 大很多。所以我们应该尽量避免或者减少 Full GC 的发生。

同时,在堆内存区域,发生最多的 GC 情形就是新生代的 Minor GC
了,因为所有的对象会优先去新生代开辟空间,所以这块的内存变化会很快,只有内存不够用,就会发生
GC,但是一般的 Minor GC 执行比 Full GC
快很多。为什么呢?因为新生代和老年代的垃圾回收算法不一样。

Minor GC && Full GC

GC一般分为Minor GC、Full GC,Minor GC是发生在新生代,Full
GC是针对整个堆,同时会对元空间(JDK1.8用元空间取代永久代)进行垃圾回收。

Minor GC触发条件:
对象首先在Eden中进行分配,当Eden的空间不足时,虚拟机会触发一次Minor
GC,Minor GC发生的次数很频繁,其速度也很快。

Minor GC的过程:
当对象在 Eden中进行分配后,经过一次 Minor GC
后,如果对象还存活,并且能够被另外一块 Survivor
区域所容纳,则使用复制算法将该对象复制到另外一块 Survivor 区域
,然后清理所使用过的 Eden 以及 Survivor
区域,并且将这些对象的年龄设置为1,以后对象在 Survivor 区每经过一次
Minor GC,就将对象的年龄 +
1,当对象的年龄达到某个值时,这些对象将会变成老年代。

Full GC触发条件:
1)老年代空间不足。
2)元空间不足。
3)Minor GC晋升到老年代所需的空间大于老年代剩余的空间。

上面讲了什么时候进行回收,最后我们讲讲如何进行回收。Java虚拟机对不可用的对象进行回收,哪些对象是可用的,哪些对象是不可用的?
Java并不是采用引用计数算法来判定对象是否可用,而是采用根搜索算法(GC
Root Tracing)
,当一个对象到GC
Roots没有任何引用相连接,用图论的来说就是从GC
Roots
到这个对象不可达,则证明此对象是不可用的,说明此对象可以被GC。

到底哪些对象可以被当成GC Roots,在Java语言中,一般下面对象可以被当成GC
Roots:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象。
2)方法区类静态属性引用的对象。
3)方法区常量引用的对象。
4)本地方法栈JNI引用的对象。

在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,则使用复制算法,新生代内存被分为一个较大的Eden区和两个较小的Survivor区,每次只使用Eden区和一个Survivor区,当回收时将Eden区和Survivor还存活着的对象一次性的拷贝到另一个Survivor区上,最后清理掉Eden区和刚才使用过的Survivor区。
老年代中对象存活率高,没有额外的空间对它进行分配担保,必须使标记-清理标记-整理算法。

垃圾回收算法