JVM内存区域划分

数据区域划分

参考:关于JVM的常见问题。注意中间绿色的部分,该部分为JVM执行Java程序过程中把它所管理的运行时内存划分为若干个不同的数据区域:

线程私有的(灰色),线程创建而创建,线程退出而销毁:

  • 程序计数器:如果正在执行的是Java方法,记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是Native方法,这个计数器值为undefined

  • 虚拟机栈:每执行一个Java方法便加入一个栈帧,里面含有局部变量表、操作数栈、动态链接和方法返回地址等,大小可以受到几个因素影响,一个是jvm参数-XSS,默认值随着虚拟机版本以及操作系统影响,从Oracle官网上可以找到

    Java SE 6, the default on Sparc is 512k in the 32-bit VM, and 1024k in the 64-bit VM. On x86 Solaris/Linux it is 320k in the 32-bit VM and 1024k in the 64-bit VM.

    如果单个栈超过了这个大小,就会抛出StackOverflowError,一般来说递归调用是常见的原因。

    并发数即线程数受到的限制:

    • 操作系统,以ubuntu为例,/proc/sys/kernel/threads-max和/proc/sys/vm/max_map_count定义了总的最大线程数
    • JVM,理论上我们能分配给线程的内存除以单个线程占用的内存就是最大线程数。Java进程分配了堆、栈和静态方法区(或叫永久代,perm区),因此可以大致认为

    线程数 = (系统空闲内存-堆内存(-Xms, -Xmx)- perm区内存(-XX:MaxPermSize)) / 线程栈大小(-Xss)

    可以通过减少最大堆-Xmx和线程栈大小-Xss的容量来换取更多的线程数

    使用命令jstack <pid>可以列出当前pid对应jvm的所有线程栈描述。

  • 本地方法栈:与虚拟机方法栈相似,用于执行native方法

线程共享的(蓝色),虚拟机启动时创建:

  • 堆:用于存放对象实例,分为新生代(Young)和老年代(Old)。其中,新生代又细分为一个Eden区两个Survivor区。分代是为了优化JVM的性能,由于很多对象都是朝生夕死的,把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

    • 新生代与老年代的比例的值为1:2(该值可以通过参数–XX:NewRatio来设定)
    • Eden:from:to = 8:1:1(可以通过参数–XX:SurvivorRatio来设定)

    Eden区是新对象优先分配内存的地方(如果对象过大,比如大数组,将会直接放到老年代),由于堆是所有线程共享的,因此在堆上分配内存需要加锁。而HotSpot JVM为提升效率,会为每个新建的线程在Eden区分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。

    利用两个survivor达到新生代无碎片的目的

    1. 程序初始化,新生代的三个空间均为空
    2. Eden被分配的新对象占满,触发第一次Minor GC(Young GC),Eden中存活对象被复制到Survivor from中,剩余对象被回收(回收后,Eden为空,Survivor from无碎片地存放所有存活对象,Survivor to为空),每经过一次Minor GC,Survivor中的对象年龄增加1
    3. Eden再次被新对象占满,触发第二次Minor GC,此时Eden存活对象被复制到Survivor to中,Survivor from中存活的对象根据年龄决定去向,达到一定值(默认是15,通过-XX:MaxTenuringThreshold 来设定)的对象会被移到老年代,没有达到的会被复制到Survivor to中,剩余对象被回收(回收后,Eden为空,Survivor from为空,Survivor to无碎片地存放所有存活对象),交换from和to,然后重复3,直到to区被填满,to区被填满时,会将所有对象移到老年代
    4. 老年代容量满的时候,会触发一次Major GC(Full GC,Major GC的速度一般会比Minor GC慢10倍以上),回收年老代和新生代中不再被使用的对象资源。
  • 方法区:在HotSpot JVM中也叫永久代,存放了类的信息、常量、类静态变量、即时编译器编译后的代码等元素,含有一个运行时常量池(存放编译器生成的各种字面量和符号引用)。从JDK8开始,永久代才完全消失,转而使用元空间。 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

直接内存

直接内存(Direct Memory,通过-XX:MaxDirectMemorySize来设定)不是JVM运行时数据区域的一部分,但是这部分内存也被频繁使用。在JDK1.4中加入的NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,避免在Java堆和Native堆中来回复制数据,从而提供性能

对象的创建与访问方式

对象的创建

  1. 类加载检查:检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类的加载过程
  2. 为对象分配内存:对象所需内存的大小在类加载完成后便完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。由于堆被线程共享,因此此过程需要进行同步处理(分配在TLAB上不需要同步)。分配方式由堆是否规整(GC是否带有压缩整理功能)决定:
    • 指针碰撞(Bump the pointer):如果Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离
    • 空闲列表(Free List):如果Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
  3. 内存空间初始化:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  4. 对象设置:JVM对对象头进行必要的设置,保存一些对象的信息(指明是哪个类的实例,哈希码,GC年龄等)
  5. init:执行完上面的4个步骤后,对JVM来说对象已经创建完毕了,但对于Java程序来说,我们还需要对对象进行一些必要的初始化

对象的访问方式

Object obj = new Object(); 根据reference(obj)如何访问到堆中对象的具体位置,主流的访问方式有两种:

  1. 使用句柄,间接寻址。在堆中划分一块内存作为句柄池,reference中存储的是对象的句柄地址,句柄中包含对象实例数据和类型数据各自的实际地址信息。优点是:在对象被移动(GC时移动对象是非常普遍的行为)时只需要改变句柄中的实例数据指针,而reference本身不需要修改。
  2. 直接指针。reference中存储的直接就是对象地址,在对象的实例数据中包含一个指向对象类型数据的指针。优点是:访问速度快,节省了一次指针定位的时间开销,Sun HotSpot采用本方式