JVM基础知识点

澳门新浦京8455com 1

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

澳门新浦京8455com ,1. Java内存区域与内存溢出异常

1. 内存模型以及分区,需要详细到每个区放什么(共分为5个)。

  1. JVM内存模型及分区
  2. jvm内存模型和内存分配
  • 程序计数器
    • 程序计数器是每个线程私有的。
    • 为了使每个线程在CPU线程切换后恢复到之前的程序执行位置。
  • Java栈区
    • 线程私有,生命周期与线程相同。
    • 用于存放局部变量表、操作栈、动态链接和方法出口等。
    • 每一个方法从被调用到被执行完,对应着一个栈帧在虚拟机中从入栈到出栈的过程。
    • 会有SOF和OOM
  • 本地方法栈
    • 与java栈的作用和原理相似。
    • Java栈为java方法服务,本地栈为执行本地方法服务。
  • 堆区
    • 被所有线程共享,用来存放对象实例。
    • GC的主要区域
    • 会有OOM
  • 方法区
    • 被所有线程共享。
    • 用于存放已被JVM加载的类信息、常量、静态变量等数据。

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

Java内存区域

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

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

2)堆:存放对象实例。

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

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

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

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

1.1. GC机制

  1. JAVA GC
    机制详解
  2. Java系列笔记(3) – Java
    内存区域和GC机制
  • GC主要管理的是堆区,堆区主要分为新生代和老年代
    • 新生代,分为一个Eden和两个Survivor区,新new的对象都在这里,很快消亡。
    • 老年代:新new的大对象直接丢到这里(为了避免在新生代区发生内存拷贝),其余是在新生代区多次回收没被干掉的变成老家伙的对象。
  • 如何回收?
    • 新生代:停止-复制算法
      • 分为一个Eden区、两个Survivor区
      • 先把Eden存活对象复制到Survivor0区,清空Eden区。
      • 当Survivor0区满了以后,把Eden和Survivor0区的存活对象复制到Survivor1区,清空Eden区和Survivor0区
      • 之后交换Survivor0和Survivor1区,保持Survivor1区是空的,如此往复。
    • 老年代:标记清理、标记整理算法。
      • 老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效
      • 标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。再对其他标记不用的内存对象进行清理。

JVM 内存区域

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

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

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

内存溢出异常-OutOfMemoryError

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

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

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

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

public vod test(){
  test();
}

5. GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?

成为Java
GC专家(3)—如何优化Java垃圾回收机制

  1. 将转移到老年代的对象数量降到最少,可以调整新生代空间大小。
  2. 减少Full GC时间,将老年代空间设定为一个合适的值。

有关 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堆结构图如下。

澳门新浦京8455com 1

Java堆结构图

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

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

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

7. Minor GC与Full GC分别在什么时候发生?

GC详解及Minor GC和Full
GC触发条件总结

  • Minor GC触发条件
    • 当Eden区满时,触发Minor GC
  • Full GC触发条件
    • 调用System.gc时,系统建议执行Full GC,但是不必然执行
    • 老年代空间不足
    • 方法区空间不足
    • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存

内存分代

一个应用启动,操作系统会给他分配一个初始的内存大小,由上可知,这部分内存大部分应该属于堆内存,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区。
老年代中对象存活率高,没有额外的空间对它进行分配担保,必须使标记-清理标记-整理算法。

9. 类加载的五个过程:加载、验证、准备、解析、初始化。

  • 加载
    • 通过一个类的权限定名来获取定义此类的二进制字节流
    • 将这个字节流所代表的静态存储结构转化为方法去的运行时数据结构
    • 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法去这些数据的访问入口。
  • 验证
    • 确保Class对象的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全
  • 准备
    • 正式为类变量分配内存并设置类变量的初始值的阶段,这些内存都将在方法区中进行分配。
  • 解析
    • 虚拟机将常量池内的符号引用替换为直接引用的过程
  • 初始化
    • 执行类构造器<clinit>()方法的过程,这个阶段才真正开始执行类中定义的java程序代码

垃圾回收算法

标记-清除算法(Mark-Sweep)

这是最基础的收集算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:

首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法(Copying)

为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。

但是这种算法的效率相当高,所以,现在的商业虚拟机都采用这种收集算法来回收新生代。为什么新生代可以使用复制算法呢?

IBM 有专门研究表明,新生代中的对象 98%
都是朝生夕死,所以就不需要按照1:1的比例来划分内存空间。这里鉴于此,新生代采用了如下的划分策略。

现在把新生代再划分为三部分,一块较大的 Eden(伊甸园) 和两块较小的
Survivor(幸存者) 区域。

当回收时,将 Eden 和 Survivor
中还存活着的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。HotSpot
虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存是会被“浪费”的。

这样清理完成后,原来的 Survivor 就空了,并一直保持为空,直到下次 Minor
GC 时,它再作为存活对象的盛放地。两个 Survivor 就这样轮流当做 GC
过程中新生代存活对象的中转站。

但是,如果使用复制算法的内存区域有大量的存活对象时,复制算法就会变得捉襟见肘,这时需要更大的
Survivor 区用于盛放那些存活对象,甚至可能需要
1:1的比例。所以针对堆内存区域的老年代,就有了下面的算法。

标记-整理算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。这种方法避免了碎片的产生,同时也不需要一块额外的内存空间,对于老年代会比较合适。

但是相比复制算法,虽然该算法占用的内存空间少,但是耗费的垃圾回收时间会比复制算法久,所以上面也说了

我们应该尽量避免或者减少 Full GC 的发生。

这两种算法用精炼的语言描述就是

  • 复制算法:用空间换时间
  • 标记-整理算法:用时间换空间

一句话 鱼与熊掌不可兼得,但是针对新生代和老年代,他们都是最佳的选择。

总结

简单梳理一下文中讲到的一些知识点

  • 为了更好的管理堆内存,该区域分为新生代和老年代。
  • 新生代发生垃圾回收要比老年代频繁。
  • 新生代发生的垃圾回收成为 Minor GC;老年代发生的 GC 成为 Full GC。
  • 新生代使用复制算法进行垃圾回收;老年代使用标记-整理算法
  • 为了更高效管理新生代的内存,按照复制算法,结合 IBM
    的研究论证,新生代分为三块,一块比较大的 Eden 区和两块比较小的
    Survivor 区,比例为 8:1:1

参考

《深入理解 Java 虚拟机》- 周志明老师

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图