援引官网的关于Java虚拟机架构图
图中囊括的信息甚广,我们慢慢来研究学习。
运行时区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:
程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间。
由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
作用:记住下一条jvm指令的执行地址。
特点:
- 线程私有
- 不会OutOfMemoryError(内存溢出)
Java 虚拟机栈
先回忆栈的特点:先进后出
虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚 拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。虚拟机栈是线程私有的。
- 我们可以把虚拟机栈看做每个线程运行时需要的内存空间;
- 一个栈内可以由多个栈帧(Frame)组成,对应每次方法调用时占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
栈内存大小参数:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
-Xsssize
Sets the thread stack size (in bytes). Append the letter
k
orK
to indicate KB,m
orM
to indicate MB,g
orG
to indicate GB. The default value depends on the platform:
- Linux/ARM (32-bit): 320 KB
- Linux/i386 (32-bit): 320 KB
- Linux/x64 (64-bit): 1024 KB
- OS X (64-bit): 1024 KB
- Oracle Solaris/i386 (32-bit): 320 KB
- Oracle Solaris/x64 (64-bit): 1024 KB
The following examples set the thread stack size to 1024 KB in different units:
-Xss1m -Xss1024k -Xss1048576
This option is equivalent to
-XX:ThreadStackSize
.
注意事项:
垃圾回收是否涉及栈内存?
栈内存不需要垃圾回收。栈帧在每次方法调用 结束就会弹出栈,会被自动回收掉。
栈内存分配越大越好吗?
栈的内存越大,会让线程的内存占用更大,让线程数目变少,栈内存变大不会加快程序的运行,只是能容纳更多的方法递归调用,所以不建议设置过大的栈内存。
- 方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。本地方法栈是线程私有的。
堆
Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。通过 new 关键字,创建对象都会使用堆内存,此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。
特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
堆空间参数
-Xmxsize
Specifies the maximum size (in bytes) of the memory allocation pool in bytes. This value must be a multiple of 1024 and greater than 2 MB. Append the letter
k
orK
to indicate kilobytes,m
orM
to indicate megabytes,g
orG
to indicate gigabytes. The default value is chosen at runtime based on system configuration. For server deployments,-Xms
and-Xmx
are often set to the same value. See the section "Ergonomics" in Java SE HotSpot Virtual Machine Garbage Collection Tuning Guide at`
http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html`.The following examples show how to set the maximum allowed size of allocated memory to 80 MB using various units:
-Xmx83886080 -Xmx81920k -Xmx80m
The
-Xmx
option is equivalent to-XX:MaxHeapSize
.
方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。他的特点也是线程共享。
在JDK 8以前方法区的一个实现我们叫它永久代,在JDK 8之后移除了永久代,在本地内存中实现的元空间(Metaspace)来代替永久代。在JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
官方的方法区解释:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the "text" segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.
The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量 等信息,这部分内容将在类加载后存放到方法区的运行时常量池中,并把里面的符号地址变为真实地址。
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中 定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
对象内存布局
对象在堆内存中的存储布局
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
对象头
HotSpot虚拟机对象的对象头部分包括两类信息。
- 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它为“Mark Word”
- 类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。此外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。
实例数据
实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。
对齐填充
起占位符的作用,不一定每个对象都有,用于保证任何对象的大小都必须是8字节的整数倍。