Java内存区域

来源:互联网 发布:剑灵怎么导入数据图 编辑:程序博客网 时间:2024/05/12 10:31

JVM具有自动内存管理机制,Java不需要像c/c++一样,为每一个new操作写配对的delete/free代码,不容易出现内存泄露和溢出。JVM内存区域主要包括如下部分:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。

VM内存区域

程序计数器

程序计数器可以视为当前线程所执行的字节码行号指示器,如果当前执行的是Native方法,计数器的值为空(Undefined)。在JVM的概念模型中,字节码解释器通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程回复等都依赖程序计数器完成。

每条线程都有独立的计数器,保证线程切换恢复正确位置,因此程序计数器这一块内存区域是线程隔离的。该区域是唯一一个没有规定任何OutOfMemoryError的区域。

Java虚拟机栈

Java虚拟机栈也是线程隔离的,一个线程一个栈,并且生命周期与线程相同。

它内部由栈帧构成,一个栈帧代表一个调用的方法,线程在每次方法调用执行时创建一个栈帧然后压栈,栈帧用于存放局部变量、操作数、动态链接、返回地址等信息。方法执行完成后对应的栈帧出栈。我们平时说的栈内存就是指这个栈。

一个线程中的方法可能还会调用其他方法,这样就会构成方法调用链,而且这个链可能会很长,而且每个线程都有方法处于执行状态。对于执行引擎来说,只有活动线程栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),这个栈帧关联的方法称为当前方法(Current Method)。

栈帧结构]

虚拟机栈的2种异常:

  • StackOverFlowError:调用链过长,线程请求深度大于JVM所允许的深度。
  • OutOfMemoryError:虚拟机栈动态扩展时无法申请到足够内存。

本地方法栈

本地方法栈与虚拟机栈的所用很相似,是虚拟机线程调用Native方法执行时的栈。Java可以通过java本地接口JNI(Java Native Interface)来调用其它语言编写(如C)的程序,在Java里面用native修饰符来描述一个方法是本地方法。

虚拟机规范中没有对本地方法栈作强制规定,虚拟机可以自由实现,所以可以不是字节码。如果是以字节码实现的话,虚拟机栈本地方法栈就可以合二为一,事实上,OpenJDK和SunJDK所自带的HotSpot虚拟机就是直接将虚拟机栈和本地方法栈合二为一的。

Java堆

Java堆是JVM管理的最大一块内存,是线程共享的,在JVM启动时创建。堆存放所有对象实例以及数组。不过在JIT(Just-in-time)和逃逸情况下,栈上分配、标量替换有可能在栈上分配对象实例。

堆是java垃圾收集器管理的主要区域(所以很多时候会称它为GC堆)。从GC回收的角度看,由于现在GC基本都是采用的分代收集算法,所以堆内存结构还可以分块成:新生代和老年代、永久代;再细一点的有Eden空间、From Survivor空间、To Survivor空间等。值得注意的是,从JKD1.7开始,永久代Perm逐渐被移除,最新的JDK1.8中已经使用元空间(MetaSpace)代替永久代。

java堆内存

Java堆可以像磁盘空间一样,允许逻辑上连续而物理不连续。如果堆中没有内存完成实例分配并且无法扩展,将会抛出OutOfMemoryError异常。

方法区

Java虚拟机规范将方法区描述为堆的逻辑部分,但是却称为非堆(Non-Heap),在Sun的HotSpot虚拟机中,可以将方法区理解为堆内存块中的永久代(Permanent Generation)。它是线程共享的。

方法去用于存储在加载类文件时,用于存放加载过的类信息,常量,静态变量,及JIT编译后的代码(类方法)等数据。

类的基本信息

每个类的全限定名、每个类的直接超类的全限定名(可约束类型转换)、该类是类还是接口、该类型的访问修饰符、直接超接口的全限定名的有序列表。

运行时常量池

常量池(Constant Pool Table),用于存放编译期生成的各种字面量、符号引用,文字字符串、final变量值、类名和方法名常量,这部分内容将在类加载后存放到方法区的运行时常量池中。它们以数组形式访问,是调用方法、与类联系及类的对象化的桥梁。

运行时常量池除了存放编译期产生的Class文件的常量外,还可存放在程序运行期间生成的新常量,比较常见增加新常量方法有String类的internd()方法。String.intern()是一个Native方法,它的作用是:如果运行时常量池中已经包含一个等于此String对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此String内容相同的字符串,并返回常量池中创建的字符串的引用。不过JDK7的intern()方法的实现有所不同,当常量池中没有该字符串时,不再是在常量池中创建与此String内容相同的字符串,而改为在常量池中记录堆中首次出现的该字符串的引用,并返回该引用。

但是,JDK1.7之前运行时常量池是方法区的一部分,JDK1.7及之后版本已经将运行时常量池从方法区中移了出来,在堆(Heap)中开辟了一块区域存放运行时常量池。

字段信息

字段信息存放类中声明的每一个字段(实例变量)的信息,包括字段的名、类型、修饰符。如privateStringa=“”;则a为字段名,String为描述符,private为修饰符。

方法信息

类中声明的每一个方法的信息,包括方法名、返回值类型、参数类型、修饰符、异常、方法的字节码。(在编译的时候,就已经将方法的局部变量表、操作数栈大小等完全确定并存放在字节码中,在加载载的时候,随着类一起装入方法区。)在运行时,虚拟机线程调用方法时从常量池中获得符号引用,然后在运行时解析成方法的实际地址,最后通过常量池中的全限定名、方法和字段描述符,把当前类或接口中的代码与其它类或接口中的代码联系起来。

静态变量

即类变量,被类的所有实例对象共享。方法区的静态区专门存放静态变量和静态块。

到类ClassLoader的引用

到该类的类装载器的引用。

到类Class的引用

虚拟机为每一个被装载的类型创建一个Class实例,用来代表这个被装载的类。

Java虚拟机规范规定方法区可抛出OutOfMemoryError。

直接内存

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

本机直接内存的分配不会受到Java堆大小的限制,但是,还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制,从而导致动态扩展时出现OutOfMemoryError异常。

总结

类和对象在运行时的内存里是怎么样的?以及各类型变量、方法在运行时是怎么交互的?

  • 在程序运行时类是在方法区,实例对象本身在堆里面。
  • 方法字节码在方法区。
  • 线程调用方法执行时创建栈帧并压栈,方法的参数和局部变量在栈帧的局部变量表。
  • 对象的实例变量和对象一起在堆里,所以各个线程都可以共享访问对象的实例变量。
  • 静态变量在方法区,所有对象共享。字符串常量等常量在运行时常量池。
  • 各线程调用的方法,通过堆内的对象,方法区的静态数据,可以共享交互信息。

参考