Java虚拟机(三)-运行时数据区

By prince No comments

前言:

内存分代概念

对于垃圾收集算法来说,分代回收是高级算法之一。对象按照生成时间进行分代,刚刚生成不久的年轻对象划为新生代(Young gen-eration),而存活了较长时间的对象划为老生代(Old generation)。根据具体实现方式的不同,可能还会划分更多的代。比如有的把永久代也算做一个代。

内存划分

java memory主要分heap memory 和 non-heap memory,其计算公式如下:

Max memory = [-Xmx] + [-XX:MaxPermSize] + number_of_threads * [-Xss]

  • heap结构

按分代,分young-eden,young-survivor,old
用-Xmn,-Xms,-Xmx来指定

  • non-heap结构

包括metaspace,thread stacks,compiled native code,memory allocated by native code

-XX:PermSize或-XX:MetaspceSize,-Xss或-XX:ThreadStackSize

 

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域有其各自的用途,以及创建销毁的时间,其中有些会一直存在,即随着虚拟机的启动而创建、随着虚拟机的退出而销毁;另外一些则是与虚拟机中的单个线程一一对应,这些与线程相对应的数据区域会随着线程的开始而创建、随着线程的结束即销毁。

这些区域如下图所示:

首先,整个运行时数据区指的是Java(JVM)运行时的数据区域划分。Java虚拟机是可以多线程并发执行的。对于一个CPU任意时刻,只能执行JVM中的一条线程。这就意味着,JVM要想实现多线程,则每条线程必须有独立的程序计数器用来标记线程执行的指令的位置(线程切换后恢复到正确的执行位置)。所以——程序计数器是线程私有的,私有意味着各条线程之间的计数器互不影响。如上图所示,绿色区域的部分都是线程私有(线程隔离)的,而蓝色部分则是所有线程可以共享的数据区域。

现在我们来具体看一下每个区域的具体信息:

1.程序计数器

程序计数器(Program Counter Register),是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型中字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖于此计数器。JVM中的程序计数器也是在Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域。在任意时刻一条JVM线程只能执行一个方法的代码,方法可以是Java方法,或者是native方法。

此处还有3点需注意:

1.Java虚拟机中的程序计数器仅仅是虚拟机中的,存在于内存之上的“虚拟”计数器,而不是电脑中的实体程序计数器。

2.JVM线程中执行的方法有2种类型:普通Java方法和由其他语言实现的native方法。如果当前执行的是普通Java方法,则程序计数器记录的是虚拟机字节码指令的地址。如果当前执行的是native方法,则计数器的值为空(null)。

3.Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器

(native方法多由C和C++语言实现,譬如java.lang.Object类中的hashCode()方法就是native方法,其底层是通过C++实现的。)

public native int hashCode();

2.Java虚拟机栈

和程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,即生命周期和线程相同。Java虚拟机栈和线程同时创建,用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame),并入栈用于存储局部变量表操作数栈动态链接方法出口等信息,一旦完成调用,则出栈。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。所有的的栈帧都出栈后,线程也就完成了使命。

Java虚拟机规范中Java虚拟机栈内存的大小既可以被实现成固定大小,也可以根据计算动态拓展或收缩,当前大部分的JVM实现是支持动态拓展的。Java虚拟机栈可能发生的异常:

1.线程请求分配的栈容量>Java虚拟机最大栈容量,则JVM会抛出StackOverFlowError异常。

2.如果Java虚拟机可动态拓展,则如果在拓展的过程中无法申请到足够的内存,就会抛出OutOfMemoryError异常。

3.本地方法栈

本地方法栈(Native Method Stack)和Java虚拟机栈类似区别在于Java虚拟机栈是为了Java方法服务的,而本地方法栈是为了native方法服务的。在虚拟机规范中并没有对本地方法实现所采用的编程语言与数据结构采取强制规定,因此不同的JVM虚拟机可以自己实现自己的native方法。此处需要说明:Sun HotSpot虚拟机就直接将本地方法栈和Java虚拟机栈合二为一了。

Java不是完美的,Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java程序的功能,被native关键字修饰的方法可以用C++语言重写。

4.Java堆

前面所说的程序计数器、Java虚拟机栈、本地方法栈通常只占很小一部分的内存空间,对与大多数应用来说,Java堆(Java Heap)才是JVM管理的内存空间中最大的一块。此区域存在的唯一目的就是存放对象实例,几乎所有的对象实例都会在这被分配内存,而且Java堆是被所有线程共享的一块内存区域。

Java堆是Java中垃圾收集器管理的主要区域,因此也被称为GC堆—Garbage Collected Heap.其唯一的用途就是存放对象实例:几乎所有的对象实例及数组都在对上进行分配。1.7后,字符串常量池从永久代中剥离出来,存放在堆中。

  • 从内存回收角度来讲,Java堆可以可以分为新生代老年代,再细分有Eden空间From Survivor空间To Survivor空间等。
  • 从内存分配角度来看,Java堆可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)

5.方法区(永久代或元数据区)

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

Java虚拟机规范对方法区的限制十分宽松,除了和Java堆一样不需要连续的内存空间分配和可选择固定大小或可拓展内存以外,方法区也可以被垃圾回收器管理或不受其管理。

首先,要明确一个「概念」——方法区,是一个概念,是Java虚拟机规范中定义的概念,一个「非堆」的运行时数据区域,用于存放被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,运行时常量池也是存放于方法区中。逻辑上的「非堆」表示和Java堆独立,那物理上呢?Java虚拟机规范中定义了方法区这个概念,但是并没有规定此区域的是否需要垃圾收集。

在Java7以前,HotSpot虚拟机中,方法区也被称为“永久代”,因为在物理上,方法区使用的是由JVM开辟的堆内存,由于和Java堆共享内存且内存空间由垃圾收集器统一分配和管理,自然的垃圾收集也拓展到了方法区上。此时,Java堆中分区为青年代Young Generation和老年代Old Generation,而方法区自然地被称为永久代Permanent Generation 。

(JVM虚拟机有不同的实现,比较主流的是sun公司的HotSpot虚拟机,在此才有“永久代的概念”,其他虚拟机不存在“永久代”这个概念)

在Java8中,HotSpot虚拟机改变了原有方法区的物理实现,将原本由JVM管理内存的方法区的内存移到了虚拟机以外的计算机本地内存,并将其称为元空间(Metaspace)。这样一来,现在的方法区实际存储在于元空间,再也不用和Java堆共享内存了,“永久代”也就永久地被撤销了。

尽管永久代撤销了,方法区这个逻辑上的空间一直是存在的,所以在java8以后,方法区的垃圾回收在物理上就是对元空间的垃圾回收。由于元空间用的是计算机本地内存,所以理论上来说只要内存足够大,方法区就能有多大,实际上Metaspace的大小是可以通过参数设定的,如果Metaspace的空间占用达到了设定的最大值,那么就会触发GC来收集死亡对象和类的加载器。常用的G1和CMS垃圾收集器都能很好地回收Metaspace区。

6.运行时常量池

运行时常量池(Runtime Constant Pool)是.class文件中每一个类或接口的常量池表(constant pool table)的运行时表示形式,属于方法区的一部分。每一个运行时常量池都在Java虚拟机的方法区中分配,在加载类和接口道虚拟机后,就创建对应的运行时常量池。常量池的作用是:

存放编译器生成的各种字面量和符号引用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时解析、翻译到具体的内存地址之中。

字面量(Literal),通俗理解就是Java中的常量,如文本字符串、声明为final的常量值等。

符号引用(Symbolic References)则是属于编译原理中的概念,包括了下面三类常量:

1.类和接口的全限定名

2.字段的名称和描述符

3.方法的名称和描述符

7.永久代和元数据区区别

字符串常量池的变化

  • 在java1.6的时候将字符串常量池在永久代
  • 在java1.7的时候将字符串常量池则移到java heap

所有的被intern的String被存储在PermGen区.PermGen区使用-XX:MaxPermSize=N来设置最大大小,但是由于应用程序string.intern通常是不可预测和不可控的,因此不好设置这个大小。设置不好的话,常常会引起

java.lang.OutOfMemoryErrorPermGen space
  • java1.7,1.8的字符串常量池在堆中实现

字符串常量池被限制在整个应用的堆内存中,在运行时调用String.intern()增加字符串常量不会使永久代OOM了。

方法区的变化

  • java8的时候去除PermGen,将其中的方法区移到non-heap中的Metaspace

move name and fields of the class, methods of a class with the bytecode
of the methods, constant pool, JIT optimizations etc to metaspace

  • Metaspace属于non-heap

Metaspace与PermGen之间最大的区别在于:Metaspace并不在虚拟机中,而是使用本地内存。

如果没有使用-XX:MaxMetaspaceSize来设置类的元数据的大小,其最大可利用空间是整个系统内存的可用空间。JVM也可以增加本地内存空间来满足类元数据信息的存储。
但是如果没有设置最大值,则可能存在bug导致Metaspace的空间在不停的扩展,会导致机器的内存不足;进而可能出现swap内存被耗尽;最终导致进程直接被系统直接kill掉。

  • OOM异常

如果类元数据的空间占用达到MaxMetaspaceSize设置的值,将会触发对象和类加载器的垃圾回收。

java.lang.OutOfMemoryError: Metaspace space

JVM从Metaspace在捕获一个一个内存分配失败后抛出。

Metaspace相关参数

  • -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  • -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
  • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

发表评论

 

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据