Java虚拟机(一):Java内存区域

1 什么是Java内存区域

总所周知,现代计算机中的程序要想得到执行,得先将代码载入内存中,程序在运行时产生的数据也会放置在内存中。为了更好的管理内存中的数据,JVM规范规定了几个运行时数据区域,这些区域都有各自的用途以及创建和销毁时间,有的区域自虚拟机进程启动直到虚拟机进程销毁,有些区域则随着线程的启动而启动,线程的销毁而销毁。

在虚拟机的自动内存管理下,Java程序员不需要像C/C++程序员那样为每一个malloc/new操作去写与之配对的free/delete操作,不容易出现内存泄露和内存溢出问题。不过,这并不意味着Java程序就不会发生内存泄露或者内存溢出,在一些特殊情况下,仍然会发生,当问题发生的时候,有时我们不得不深入到JVM层面去解决问题,所以,理解JVM内存机制有利于我们排查问题、解决问题。

2 Java运行时内存区域划分

Java内存区域划分

上图就是Java虚拟机规范所描述的内存划分,主要是上面蓝框部分的五个区域,大致可以把他们分为两类:

  • 线程私有的
  • 线程共享的

2.1 线程私有部分

2.1.1 虚拟机栈

通常我们都把虚拟机栈简称为“栈”。它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型,每一个方法调用都是一个栈帧,栈帧里包含方法局部变量表,操作数栈,动态链接,方法出口等信息。方法从开始调用到结束调用就是在栈上入栈和出栈的过程。

栈帧所占用的空间是在编译器就确定的,编译器在编译期获取方法的局部变量的个数,类型,计算局部变量所占用的空间,其他的信息也可以在编译期获取并确定,在运行时不会改变占用空间大小。

在虚拟机规范中规定了关于栈的两个异常:

  • StackOverflowError
  • OutOfMemoryError

StackOverflowError即栈溢出,如果申请栈的深度大于虚拟机规定的栈深度,那么就会抛出该溢出。OutOfMemoryError表示没有足够的内存,我在做测试的时候,很难模拟出这个错误,即使把栈内存设置的很小,也仅仅抛出StackOverflowError。

2.1.2 本地方法栈

本地方法栈也是栈,结构和虚拟机栈一样,都是后进先出的结构。只是本地方法栈中存储的是Native方法的栈帧,在虚拟机规范中没有明确规定Native方法是由什么语言编写的,这取决于具体的虚拟机实现,在HotSpot中,Native方法是C++实现的方法。和虚拟机栈一样,本地方法栈也会抛出两个异常。

2.1.3 程序计数器

程序计数器是一块很小的内存(即使它在图中看着很大)。和CPU里的程序计数器(PC)不同,它存储的是当前线程执行字节码的位置,而CPU里的PC存储的是下一条指令所在的地址。我们知道,程序是顺序执行的,那么为什么需要程序计数器呢,好像没有程序计数器,程序也能按照顺序往下执行?这是因为在多线程的情况下,会发生频繁的线程切换,在切换之前要记录当前执行到哪了,这样方便切换回来的时候能从之前切换前的位置继续执行代码,从这里我们也能看出,这个程序计数器应该是线程私有的东西,如果程序计数器是线程之间共享的,就会发生线程安全问题了,可能JVM就需要付出更大的代价来解决问题。

如果正在执行的代码不是Java代码(例如Native方法),那么程序计数器的值为空。同时,此内存没有规定OutOfMemoryError异常,这是因为程序计数器里存储的值是一个可预计的值,其占用空间是固定长度的,不需要做动态扩展,也就不可能抛出OutOfMemoryError异常了。

2.2 线程共享部分

2.2.1 方法区

在Java8之前叫做方法区,在Java8之后被称作“元空间(meta space)”。用于存储类的类型信息,也就是类的元信息,常量,静态变量,JIT编译后的代码等,这些数据是常驻内存的数据,所以一般垃圾回收器很少会光顾这里。

在方法区中有一个比较特殊的区域叫做“运行时常量区”。什么是运行时常量?Java中常量可以在编译器确定(例如有final修饰的变量),也可以在运行时生成(例如"hello".length()),运行时常量区就是用来存储运行时产生的常量。那为什么要做这样一个划分呢,都统一存储在方法区不行吗?运行时常量是在运行时产生的,编译完成也无法确定,类的信息在编译器是可以确定的,一些在编译期可以确定的常量会和同一个类的其他元信息存在一起。如果该类在运行时产生了一个运行时常量,虚拟机一般会有两种方法,一是将该常量存在另一块区域,例如“运行时常量区”,二是将该常量插入到该类的的元信息所在内存区域,这种方法非常不好,插入内存可能会导致大量的数据移动,非常费时。

根据虚拟机规范的规定,当方法区的内存不足时,会抛出OutOfMemoryError异常。在一般的应用中,这种情况不常见,但是在一些使用到动态代理的应用中,就要注意这个问题了,无论是基于JDK接口的动态代理,还是基于字节码技术的动态代理,都会在运行时产生新的类,新的类的元信息会放到方法区里,如果动态代理使用不当,会造成大量的类产生,从而挤爆方法区,导致OutOfMemoryError

2.2.2 堆

终于说到堆了,这一块是虚拟机管理的内存中最大的一块了,Java使用new来创建的实例对象就存储在堆里,在以前,所有的对象都会存储在堆上,但随着技术的进步(栈上分配,标量分析等),这一点不再是那么绝对了,一些对象可能会被分配在栈上,而不是堆上。

堆是垃圾回收的主要目标,垃圾回收绝大部分时间都是在对堆上内存做“清理”。有些垃圾收集器会使用基于分代收集的算法,这使得堆又可以分为老年代和新生代,新生代又可以分为Eden区和from survivorto survivor,但无论是如何划分,都只是为了方便垃圾回收,存储的东西仍然是对象实例。

Java虚拟机规范中并没有规定堆必须是连续内存,也就是说堆可以是由多个不连续的内存组合形成的,只要逻辑上是连续的就可以,这有点像虚拟内存。

3 直接内存

直接内存不是Java运行时数据区的一部分,所以在图中没有标识出来。在JDK1.4新加入了NIO,NIO可以通过Native方法来申请堆外内存,例如unsafe.allocateMemory(int size) 方法。直接内存不会受堆大小的限制,但是会受到主机内存的限制,所以仍然可能抛出OutOfMemoryError

在网上经常能看到直接内存的读写效率会比堆内存的效率高,但是分配空间的效率比堆内存效率低。实际使用的时候好像确实是这样。至于为什么会这样,我也不太清楚具体机制,网上搜了也搜不到文章。

4 总结

为了更好的管理内存,减少程序员的负担。Java虚拟机将程序进程的内存分为了几个区域,每个区域的功能不同,大小不同。其大致又可以分为两个大类,一是线程私有的,包括程序计数器、虚拟机栈、本地方法栈,二是线程共享的,包括堆、方法区。特殊的还有直接内存,这块内存不属于JVM规范里规定的运行时数据区,只能通过Native方法才能申请,其申请分配的效率较低,但是读写效率较高,对于一些不频繁开辟,但是频繁使用的场合尤其适合(例如NIO)。

你可能感兴趣的:(Java虚拟机(一):Java内存区域)