JVM(一)---- 总结与专题目录
JVM(二)----Java运行时数据区域
JVM(三)----垃圾收集算法及Safe Point介绍
JVM(四)----HotSpot的垃圾收集器与内存分配回收策略
JVM(五)----虚拟机类加载机制

本文结构如下:
1.运行时数据区域总览
2.每一部分介绍
3.Hotspot永久代在1.8中移除的总结

首先理解一个概念,就是虚拟机实例。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。

一、总览

Java虚拟机所管理的内存将会包括一下几个运行时数据区域:


Java虚拟机运行时数据区.png

注意:线程私有和共享的区域。

二、介绍每个部分

2.1程序计数器

  • 作用:
    记录当前线程所执行到的字节码的行号。字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • 意义:
    JVM的多线程是通过线程轮流切换并分配处理器来实现的,对于我们来说的并行事实上一个处理器也只会执行一条线程中的指令。所以,为了保证各线程指令的安全顺利执行,每条线程都有独立的私有的程序计数器。
  • 存储内容
    当线程中执行的是一个Java方法时,程序计数器中记录的是正在执行的线程的虚拟机字节码指令的地址。
    当线程中执行的是一个本地方法时,程序计数器中的值为空。
  • 可能出现异常
    此内存区域是唯一一个在JVM上不会发生内存溢出异常(OutOfMemoryError)的区域。

2.2Java虚拟机栈

  • 作用
    描述Java方法执行的内存模型,也是线程私有的,生命周期与线程相同。每个方法在执行的同时都会开辟一段内存区域用于存放方法运行时所需的数据,称为栈帧,一个栈帧包含如:局部变量表、操作数栈、动态链接、方法出口等信息。
  • 意义
    每个方法从调用到执行结束,就对应着一个栈帧在虚拟机栈中入栈和出栈的整个过程。
  • 存储内容
    局部变量表(编译期可知的各种基本数据类型、引用类型和指向一条字节码指令的returnAddress类型)、操作数栈、动态链接、方法出口等信息。
    值得注意的是:局部变量表所需的内存空间在编译期间完成分配。在方法运行的阶段是不会改变局部变量表的大小的。
  • 可能出现的异常
    如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
    如果在动态扩展内存的时候无法申请到足够的内存,就会抛出OutOfMemoryError异常。

2.3本地方法栈

  • 作用
    为JVM所调用到的Native方法(即本地方法)服务。
  • 可能出现的异常
    和虚拟机栈出现的异常很相像。也是StackOverflowError异常和OutOfMemoryError异常。

2.4 Java堆(Heap)

  • 作用
    所有线程共享一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例。
  • 意义
    1、存储对象实例,更好地分配内存。
    2、垃圾回收(Garbage Collection)。堆是垃圾收集器管理的主要区域。更好地回收内存。
  • 存储内容
    存放对象实例,几乎所有的对象实例都在这里进行分配。堆可以处于物理上不连续的内存空间,只要逻辑上是连续的就可以。
    值得注意的是:在JIT编译器等技术的发展下,所有对象都在堆上进行分配已变得不那么绝对。
  • 可能出现的异常
    实现堆可以是固定大小的,也可以通过设置配置文件设置该为可扩展的。
    如果堆上没有内存进行分配,并无法进行扩展时,将会抛出OutOfMemoryError异常。

2.5方法区

  • 作用
    用于存储运行时常量池、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 意义
    对运行时常量池、常量、静态变量等数据做出了规定。
  • 存储内容
    运行时常量池(具有动态性)、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 可能出现的异常
    当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

三、关于永久代在1.8中移除的总结

绝大部分 Java 程序员应该都见过 "java.lang.OutOfMemoryError: PermGen space "这个异常。这里的 “PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 的规范,而后者则是 JVM 规范的一种实现,可以这么理解:方法区和永久带的关系就像Java中接口和实现接口的类之间的关系一样。并且只有 HotSpot 才有 “PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。

在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。下面我们就来看看 Metaspace 与 PermGen space 的区别。

其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。我们可以通过一段程序来比较 JDK 1.6 与 JDK 1.7及 JDK 1.8 的区别,以字符串常量为例:

package com.paddx.test.memory;
 
import java.util.ArrayList;
import java.util.List;
 
public class StringOomMock {
    static String  base = "string";
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i=0;i< Integer.MAX_VALUE;i++){
            String str = base + base;
            base = str;
            list.add(str.intern());
        }
    }
}

这段程序不断的生成新的字符串,并且通过intern方法将字符串放到字符串常量池中,这样可以比较快速的消耗内存。我们通过 JDK 1.6、JDK 1.7 和 JDK 1.8 分别运行:

JDK 1.6 的运行结果:


image.png

JDK 1.7的运行结果:


image.png

JDK 1.8的运行结果:


image.png

从上述结果可以看出,JDK 1.6下,会出现“PermGen Space”的内存溢出,而在 JDK 1.7和 JDK 1.8 中,会出现堆内存溢出,并且 JDK 1.8中 PermSize 和 MaxPermGen 已经无效。因此,可以大致验证 JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中,并且 JDK 1.8 中已经不存在永久代的结论。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

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

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

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

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

最后总结一下为什么移除永久带而使用元空间的原因吧:

  • 字符串存在永久代中,容易出现性能问题和内存溢出。
  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  • Oracle 可能会将HotSpot 与 JRockit 合二为一。

参考资料:http://www.cnblogs.com/paddix/