JVM基本结构
首先我们看一张图片,通过图片来理解JVM结构。
从图片中可以看到,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 来开启。