Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途以及创建和销毁的时间。有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁,这些区域被称之为运行时数据区域,其划分大致如下图所示:
returnAddress
,其中 64 位长度的long
和double
类型的数据会占用 2 个局部变量空间(Slot
),其余的数据类型只占用 1 个。-Xms
和-Xmx
来控制,其中-Xms
为设置最小堆内存,-Xmx
为设置最大堆内存。-XX:PermSize
和-XX:MaxPermSize
来控制方法区的大小,其中-XX:PermSize
为设置方法区最小可用内存,-XX:MaxPermSize
为设置方法区最大可用内存。实际上,除了上述的运行时数据区域之外,还有一个内存区域值得我们了解,即
OutOfMemoryError
异常的出现。在 JDK 1.4 中新加入了NIO
类,引入了一种基于通道与缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配对外内存,然后通过一个存储在 Java 堆中的DirectByteBuffer
对象作为这块内存的引用进行操作。这样能在一些场景汇总显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是既然是内存,肯定还是会收到本机总内存大小以及处理器寻址空间的限制。因此,如果我们在分配内存的时候,忽略了直接内存,很可能使得各个内存区域总和大于物理内存限制,从而导致动态扩展的时候出现OOM
异常。在虚拟机中创建对象,大致经过以下这些步骤,分别为:
new
指令时,首先将去检查这个指令的参数是否能在常量池汇总定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。在上面的工作都完成后,从虚拟机的角度来看,一个新的对象已经产生了,但是从 Java 的角度来看,对象的创建才刚刚开始,<init>
方法还没有执行,所有的字段都还未零。所以,一般来说(由字节码中是否跟随着invokespecial
指令所决定),执行new
指令之后会接着执行<init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。
在 HotSpot 虚拟机中,对象的内存中存储的布局可以分为 3 块区域:对象头、实例数据和对其填充。
创建对象是为了使用对象,我们的 Java 程序需要通过栈上的reference
数据来操作堆上的具体对象。由于虚拟机规范仅规定了reference
是一个引用对象,具体如何实现、如何定位、如何访问并没有做限制,因此对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用“句柄”和“直接指针”两种。
reference
中存储的就是对象的句柄地址,而句柄中包括了对象实例数据与数据类型各自的具体地址信息。使用句柄来访问的最大好处就是reference
中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普通的行为)时只会改变句柄中的实例数据指针,而reference
本身不需要修改。reference
中存储的直接就是对象地址。使用直接指针访问的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在 Java 中非常频繁,因此这类开销极少成多以后也是一项非常可观的执行成本。HopSpot VM 就是使用直接指针的方式进行对象访问的。