JVM印象加深——JVM结构理解以及优化

JVM基本结构

首先我们看一张图片,通过图片来理解JVM结构。

center

从图片中可以看到,JVM运行时数据区可以分为5部分。

  • 方法区
  • 程序计算器
  • 本地方法区

一般我们会看主要的三大部分(方法区、堆、栈)。

线程独有区域

  • :栈内存,每个线程在创建的时候,都会创建私有的虚拟机栈,虚拟机栈存储着栈帧,栈帧可以用来存储局部变量,部分方法返回值

  • 程序计数器:程序计数器是用来指示当前线程正在执行的JVM指令,因此程序计数器是线程独有的。一个JVM支持多个线程,所以线程都有自己独有的线程计数器。如果线程正在执行的是java方法,则线程计数器保存的正是当前线程执行的JVM指令,如果正在执行的方法是Native方法,则保存为空(undefined)。

  • 本地方法区:本地方法区存储着native方法的调用状态,一般会随着线程创建而针对每一个线程分配。

线程共享区域

  • :存储着所有的类实例和数组对象。
  • 方法区:方法区为内存共享区域,一般存储着类的信息,如名称、修饰符等、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息等。当我们通过Class对象使用getName等方法时,其实就是从方法区中获取数据。
  • 运行时常量池:运行时常量池是方法区的一部分,存放着类中固定的常量信息、方法、和field的引用信息。JVM在加载类的时候会为每一个Class分配一个独立的常量池。

优化

           堆内存 由 新生代 ( Young )、老年代 ( Old )

                   新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。

对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

     Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

  • -Xms     堆的初始化内存大小,例如-Xms400m
  • -Xmx     堆的最大的内存大小,例如-Xms400m
  • -Xmn     新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90%  
  • –XX:NewSize=n     默认新生代 ( Young )的大小,譬如–XX:NewSize=400M 
  • -XX:MaxNewSize=n         新生代 ( Young )的最大大小,譬如–XX:MaxNewSize=400M
  • –XX:NewRatio=n(默认server模式是8,client模式是2) 新生代 ( Young ) 与老年代 ( Old )的比例
  • –XX:SurvivorRatio=n(默认是32)  Eden Space与Survivor Space的比例,譬如–XX:SurvivorRatio=7,则每个Survivor Space占整个Young Gen的1/9(注意,有两个Survivor区)

从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
本人使用的是 JDK1.6,以下涉及的 JVM 默认值均以该版本为准。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。
老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

  • 方法区(永久代)

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

方法区又称为永久代(在1.8中改为metespace,不用考虑该部分的调优),如果在程序的运行中,经常发现出现永久代的OOM异常,那么久可以考虑增大永久代的大小,使用的参数为

  • –XX:PermSize     永久代(方法区)的初始大小
  • –XX:MaxPermSize     永久代(方法区)的最大值

在引入大量的第三方类库的时候,可能会加载很多框架依赖的类,使用过程中可能就会产生因为Perm Gen不足产生的OOM。这时候可以适当调大Perm Gen的大小。

这里可以设置的最大值大于初始值,因为该方法区进行垃圾回收,使得内存波动较小,这样每次垃圾回收就很少触动动态改变方法区内存大小的机制,就不会影响JVM的性能。

  • GC 堆

Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。

  • Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。当一个对象被判定为 "死亡" 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。
  • Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。现实的生活中,老年代的人通常会比新生代的人 "早死"。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 "死掉" 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。

垃圾回收算法主要分为三类,他们都是基于标记-清除(复制)算法实现的。

Serial算法 
并行算法 
串行算法

垃圾收集器如下图所示:

è¿éåå¾çæè¿°

1) Serial: 串行收集器,新生代收集器,使用的复制算法,在单CPU情况下,性能最好。

2) ParNew: 并行收集器,新生代收集器,是Serial的多线程版本,使用多线程进行垃圾回收,单处理器(对于多核处理器是指单核)的情况下,性能没有Serial好,在多个cpu的情况下,性能高于Serial。

3) Parallel Scavenge: 并行多线程收集器,新生代收集器,使用的算法是复制算法。他关注的是如何提高JVM的吞吐量,而其他收集器目标是如何提高JVM每次垃圾回收时候用户线程的停顿时间。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。

4) Serial Old: 串行收集器,是Serial收集器的老年代版本,使用单线程进行收集,使用的算法是”标记-整理”算法。主要使用在虚拟机的Client模式下。

5) Parallel Old: 并行收集器,是Pralllel Scavenge收集器的老年代版本,使用的算法是”标记-整理算法”。

6) CMS: 并发收集器,老年代收集器,其基于”标记-清除”算法来实现的,主要包括下边的几个阶段: 1 初始标记 2 并发标记 3 重新标记 4 并发清除 其中需要停止用户线程的阶段是初始标记和重新标记阶段。CMS的详细介绍以后再说,这次不做重点。

7) G1收集器:并发收集器,产生于jdk1.7,可用于新生代和老年代收集器,在老年代使用的“标记-整理算法”, 也就是说不会产生内存碎片。

垃圾回收算法对应到的就是不同的垃圾收集器,具体到在 JVM 中的配置,是使用 -XX:+UseParallelOldGC 或者 -XX:+UseConcMarkSweepGC等 这种不同的收集器来达到选择算法的目的

一般常用到ConcMarkSweepGC, 也称之为 CMS,在使用CMS 进行老年代回收时,新生代默认使用了单线程回收算法,此时可以通过配置 -XX:+UseParNewGC来使用 新生代并行回收。

在JDK1.7 的时候引入了 G1 收集器,可以通过配置-XX:+UseG1GC 来开启。

猜你喜欢

转载自blog.csdn.net/weixin_40663800/article/details/88143208