Java运行时数据区域

1、Java运行时数据区域

Java虚拟机在执行Java程序的过程中,会把它所管理的内存划分为若干个不同的数据区域。

  1. 程序计数器
  2. Java虚拟机栈
  3. 本地方法栈
  4. Java堆
  5. 方法区
  6. 运行时常量池
  7. 直接内存
    Java运行时数据区域_第1张图片

1.1、程序计数器

程序计数器(Program Counter Register)是一块较小的内存区域,字节码解释器工作时,就是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等都是依托程序计数器完成的。也就是说处理器在时间片切换时,为了线程切换后能恢复到(找到)正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间互不影响,独立存储,线程私有的内存

1.2 Java虚拟机栈

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,生命周期与线程相同;

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法在执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表,操作数帧,动态链接,方法出口等信息。

一个方法的调用到执行结束,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。

  • 局部变量表存放的是编译期可知的各种Java虚拟机基本类型(boolean、byte、char、short、int、float、long、double)、对象的引用(reference类型,指的是对象地址的引用或句柄或其他于此对象相关的地址)、returnAddress(指向了一条字节码指令的地址)。
  • 这个数据类型在局部变量表中的存储空间以局部变量槽(Slot)表示,64位的长度(long,double)占用2个变量槽,其余都是1个。
  • 局部变量表所需的内存空间,在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部面量表的大小(大小指的是变量槽的数量)
  • 虚拟机一个变量槽占用的比特(例如32比特,64比特或者更大),是由具体的虚拟机决定的

《Java虚拟机规范》中这个内存区域规定了两类异常状况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError
    • 每次方法调用都会有一个栈帧压入虚拟机栈。操作系统给JVM分配的内存是有限的,JVM分配给“虚拟机栈”的内存是有限的。 如果方法调用过多,导致虚拟机栈满了就会溢出。 这里栈深度就是指栈帧的数量
  • 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存空间会抛出OutOfMemoryError
  • HotSpot虚拟机栈容量不支持动态扩展,所以只要线程申请到栈空间就不会出现OOM,如果申请失败则会出现OOM异常

1.3、本地方法栈

本地方法栈(Native Method Stack):本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。

  • Native Method就是一个java调用非java代码的接口。该方法的实现由非java语言实现,比如C。
    • 例如Object类中
      Java运行时数据区域_第2张图片

1.4、Java堆

Java堆(Java Heap)是虚拟机所管理内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,用于存放对象实例。Java堆是垃圾回收器管理的内存区域也称为GC堆。

Java堆可以处于物理上不连续的内存区域,但在逻辑上应该被视为连续的,但对于大对象(典型的如数组对象),多数虚拟机实现处于简单实现,存储高效的考虑,很可能会要求连续的内存空间

Java虚拟机的堆内存分为新生代、老年代、永久代、Eden、Survivor,在当前的HotSpot中上述提法就有很多需要商榷的地方。Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存

Java堆既可以被实现成固定大小,也可以是可扩展的,当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 设定),如果Java堆中没有内存完成实例分配,并且堆也无法再扩展,Java虚拟机将会抛出OutOfMemoryError异常

1.5、方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据

JDK 8 HotSpot完全放弃了永久代的概念,改用本地内存中实现的元空间(Meta-space)来代替

《Java虚拟机规范》对方法区的约束是非常宽松的,除了和Java堆一样不需要连续的内存和可以选择固定大小或可扩展外,甚至可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域的确比较少出现,,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标最主要是针对常量池的回收和对类型的卸载,但是回收效果比较难令人满意,尤其是类型的卸载。

方法区无法满足新的内存分配需求时将会抛出OutOfMemoryError异常

1.6、运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table)用于存放编译期生成的各种字面量与符号引用,在类加载后存放到方法区运行常量池中还会把由符号引用翻译出来的直接引用也存储在运行时常量池中

  • 字面量:等号右边的八种基本类型的值、字符串值、声明为final的常量的值

    //a为常量,10为字面量
    final int a = 10; 
    // b 为变量,hello world!为字面量
    string b = "hello world!"; 
    
  • 符号引用:可以是任意类型的字面量。只要能无歧义的定位到目标。在编译期间由于暂时不知道类的直接引用,因此先使用符号引用代替。最终还是会转换为直接引用访问目标。符号引用就是某个变量,在编译的时候,无法确定其内存地址。

    String str = "Hello World!"
    // str在编译的时候就会编译为符号引用。
    System.out.println(str);
    
  • 直接引用:程序运行时可以定位到引用的东西(类, 对象, 变量或者方法等)的地址

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

  • intren方法:通俗的讲,是将字符串放入常量池中。

    /**
    * 表达式右边是纯字符串常量,则存放在常量池中
    * 表达式右边存在字符串引用,则存放在堆中
    */
    public class test {
        public static void main(String[] args) {
            String s1="aaa";
            String s2="bbb";
            String s3="aaabbb";
            String s4=s1+s2;
            String s5="aaa"+"bbb";
            String s6=new String("aaabbb");
            // false
            System.out.println(s3==s4);
            // true
            System.out.println(s3==s4.intern());
            // true
            System.out.println(s3==s5);
            // false
            System.out.println(s3==s6);
            // true
            System.out.println(s3==s6.intern());
        }
    }
    

    说明:s1,s2,s3,s5均存放在常量池中,s4,s6存放在堆中。

当常量池无法再申请到内存时会抛出OutOfMemoryError异常

1.7、直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域,但是这部分内存也被频繁的使用,也可能导致OutOfMemoryError异常

JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理 上的和操作系统 级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

参考《深入理解Java虚拟机》-周志明

你可能感兴趣的:(JVM,Java虚拟机,JVM,运行时数据区域)