【Java基础随机】Java内存区域
Java内存区域
总体结构图
各部分区域
一、程序计数器(寄存器)
- 是一块较小的内存空间,可看作为当前线程所执行的字节码的行号指示器。由于JAVA虚拟机的多线程是通过线程的轮流切换并分配处理器执行的时间来实现的(一个时刻,一个处理器内核只会执行一条线程中的指令),所以需要线程切换回来后将它恢复到正确的执行位置,则每个线程都有个程序计数器,其是线程私有的,生命周期与线程相同。线程执行JAVA方法时,计数器记录正在执行的虚拟机字节码指令的地址,当执行Native方法时,计数器值为空(Undefined)。JAVA虚拟机规范中没有规定此区域的内存溢出“OutOfMemoryError”情况。
二、JAVA虚拟机栈
- 这块内存区域和程序计数器一样,是线程私有的,生命周期同线程一样。虚拟机栈描述JAVA方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 栈帧是用于虚拟机执行方法调用和方法执行时候的数据结构,可以简单理解为每一次函数调用设计的相关信息的记录单元。
- 局部标量表是一组变量值的存储空间,用于存放 方法参数 和 局部变量。在Class 文件的方法表的 Code 属性的 max_locals 指定了该方法所需局部变量表的最大容量。
- 操作数栈中的每一个元素可以是任意的Java数据类型,方法刚刚开始执行的时候,这个方法的操作数栈是空的,方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,如算术运算时通过操作数栈来进行。如整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将这两个Int值出栈并相加,然后将相加的结果入栈。其栈中元素数据类型与字节码指令的序列时严格匹配的,不可能出现long和float使用iadd命令相加的情况。
- 每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
- 方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能保存这个计数器值。异常处理器表确定返回地址时,栈帧一半不会保存这部分信息。
三、本地方法栈
- 本地方法栈和虚拟机栈所发挥的作用十分相似,它们之间的区别是虚拟机栈执行Java方法服务,本地方法栈为虚拟机使用到的本地方法(Native方法)服务。线程在调用本地方法时,来存储本地方法的局部变量表,本地方法的操作数栈等等信息。
- 通常,本地方法可能会和Java方法一起在某个线程中调用,一个线程可能在整个生命周期中都执行Java方法,操作他的Java栈;或者他可能毫无障碍地在Java栈和本地方法栈之间跳转。
- 如下图该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。图中的本地方法栈显示为 一个连续的内存空间。假设这是一个C语言栈,期间有两个C函数,他们都以包围在虚线中的灰色块表示。第一个C函数被第二个Java方法当做本地方法调用, 而这个C函数又调用了第二个C函数。之后第二个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过 本地方法接口回调了一个Java方法(第三个Java方法)。最终这个Java方法又调用了一个Java方法(他成为图中的当前方法)
四、Java堆
- 对大多数应用来说,Java堆是JVM管理的内存中最大的一块。它是所有线程共享的一块内存区域,在虚拟机启动时候创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配。(随着JIT编译器的发展、逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术会使得对象实例都在堆分配变得不再“绝对”)
- Java堆是垃圾收集器管理的主要区域,所有很多时候被称为“GC堆”Garbage Collected Heap 英文直译“垃圾收集堆”,从内存回收的角度看,现在收集器基本使用分代收集算法,所以Java堆中还可以分为新生代和老年代。
- 若要细分,可以在逻辑上分为下面几个区域。
- Eden Space字面意思是伊甸园,对象被创建的时候首先放到这个区域,进行垃圾回收后,不能被回收的对象被放入到空的survivor区域。
- Survivor Space幸存者区,用于保存在eden space内存区域中经过垃圾回收后没有被回收的对象。Survivor有两个,分别为To Survivor、 From Survivor,这个两个区域的空间大小是一样的。执行垃圾回收的时候Eden区域不能被回收的对象被放入到空的survivor(也就是To Survivor,同时Eden区域的内存会在垃圾回收的过程中全部释放),另一个survivor(即From Survivor)里不能被回收的对象也会被放入这个survivor(即To Survivor),然后To Survivor 和 From Survivor的标记会互换,始终保证一个survivor是空的。
- Eden Space和Survivor Space都属于新生代,新生代中执行的垃圾回收被称之为Minor GC(因为是对新生代进行垃圾回收,所以又被称为Young GC),每一次Young GC后留下来的对象age加1。(虚拟机给每个对象定义了一个对象年龄计数器,当从Eden成功存活至To Survivor时,age=1,在Survivor空间中每“熬过”一次Minor GC,年龄就增加一岁,当年龄达到设置的参数标准时[默认15岁],对象就会被晋升到老年代中)。
- Old Gen老年代,用于存放新生代中经过多次垃圾回收仍然存活的对象,也有可能是新生代分配不了内存的大对象会直接进入老年代。经过多次垃圾回收都没有被回收的对象,这些对象的年代已经足够old了,就会放入到老年代。当老年代被放满的之后,虚拟机会进行垃圾回收,称之为Major GC。由于Major GC除并发GC外均需对整个堆进行扫描和回收,因此又称为Full GC。
- Code Cache代码缓存区,它主要用于存放JIT所编译的代码。CodeCache代码缓冲区的大小在client模式下默认最大是32m,在server模式下默认是48m,这个值也是可以设置的,它所对应的JVM参数为ReservedCodeCacheSize 和 InitialCodeCacheSize,可以设置其值。
- Perm Gen全称是Permanent Generation space,是指内存的永久保存区域,因而称之为永久代。这个内存区域用于存放Class和Meta的信息,Class在被 Load的时候被放入这个区域。因为Perm里存储的东西永远不会被JVM垃圾回收的,所以如果你的应用程序LOAD很多CLASS的话,就很可能出现PermGen space错误。默认大小为物理内存的1/64。
-
这里,提一下Java堆的内存分配机制。
1)、对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配,当Eden区中没有足够空间进行分配时,虚拟机会发动一次Minor GC。
2)、大对象直接进入老年代
大对象指的是需要大量连续内存空间的Java对象,最典型的大对象就是很长的字符串以及数组(如大的byte[])。大对象对虚拟机的内存分配来说是一个不好的消息,经常导致内存还有不少空间的时候就提前出发垃圾收集以获取足够的连续空间来“安置”它们。虚拟机会提供一个可设置的参数,令大于设置值的对象直接在老年代分配。
3)、长期存活的对象将进入老年代
对应虚拟机分代收集的思想管理内存,在垃圾收集内存回收时就必须能识别哪些对象应该放在新生代,哪些对象应该放在老年代中。为做到这一点,虚拟机给对象定义了一个对象年龄计数器,当从Eden成功存活至To Survivor时,age=1,在Survivor空间中每“熬过”一次Minor GC,年龄就增加一岁,当年龄达到设置的参数标准时[默认15岁],对象就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数设置。
4)、动态对象年龄判定
虚拟机并不是永远的要求对象的年龄必须打到晋升老年代的年龄阈值才能晋升老年代,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到对象的年龄计数器达到晋升老年代的年龄阈值的要求。 -
根据Java虚拟机规范,Java堆可以处于在物理上不连续的内存空间,但是只要逻辑上连续就可以了。它既可以固定大小,也可以扩展,现在主流虚拟机都是按照可扩展来实现的,如果堆中没有内存完成实例分配,且无法扩展的时候,将会抛出内存溢出异常。
-
关于垃圾回收,可以查看具体关于垃圾回收的博客。
-
附Java虚拟机规范中的原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated.,即堆是为所有类实例和数组分配内存的运行时数据区域。
五、方法区
- 由英文Method Area直译而来,与Java堆一样是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它拥有一个别名叫做Non-Heap(非堆),目的可能是与Java堆做一个区分。根据Java虚拟机规范规定,当方法区无法满足内存分配需求时,将抛出内存溢出异常。
- 运行时常量池时方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译期产生的各种字面量和符号引用。
- 提个例子:
- 若你定义了一个 String s = new String(“xyz”); 你觉得在内存中分配了几个对象?
- 答案是1个或2个,如果常量池中原来没有 ”xyz”, 就是两个。如果原来的常量池中存在“xyz”时,就是一个。但是s指向的地址是堆中的对象。因为它是通过new String()来实例的。若你使用String s = “xyz”; 来定义s的话,首先在常量池中查找是否存在内容为"xyz"字符串对象,如果不存在则在常量池中创建"xyz",并让str引用该对象,如果存在则直接让str引用该对象。