Java运行时数据区详解

1、运行时数据区概述

  • 内存和CPU一样是一个很重要的资源,我们所写出来的程序在运行的时候有变量有常量有方法,这些东西在运行的时候会被jvm放到不同的区域,这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。
    运行时数据区

2、方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,实际上《Java虚拟机规范》只是规定了有方法区这个概念和作用,并没有规定如何实现它,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。方法区是个比较神奇的东西,在jdk版本的迭代过程中方法区的实现也是一直在变的由至一开始的永久代到现在的元空间不一样的是永久代属于虚拟机的一部分空间在堆里面和新生代老年代地址是连续的,而元空间则是本地内存不属于虚拟机。方法区存放了类信息(构造方法/接口定义)、运行时常量池

2.1、 运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

2.2、字符串常量池

在JDK6.0之前的版本字符串常量池是放在方法区中的,在JDK之后的版本被移到堆中了。在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降。

3、堆

对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

  • 弱分代假说:绝大多数对象都是朝生夕灭的。
  • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
  • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

3.1、新生代

3.1.1 新生代的内存布局

按照上图所示大部分的对象都是出生在新生代的Eden区,正如这个区的名字伊甸对象可以无忧无虑的生活在这个区域同时这个区域的空间是比较大的JVM的默认配置(-XX:Survivor-Ratio=8)里面Eden:Survivor的值为8同时From和To的比例为1:1,这时新生代的内存布局为一块较大的Eden区和两块较小的Survivor区域,每次分配内存只使用Eden和其中一块Survivor另一块Survivor是空闲的。

3.1.2 新生代的垃圾回收方式

该区域发生垃圾回收时会将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间所以在默认的情况下内存是有10%是被浪费的,当Survivor空间不足以容纳一次Minor GC之后存货的对象时,就需要依赖其他区域做担保大多数是老年代。如果只有该区域发生了GC被称为Minor GC/Young GC。

3.2、老年代

3.2.1 进入老年代的方式

  • 正常情况下当对象出生在Eden区域之后每经历一次垃圾回收如果存活下来了那么对象的年龄也就增加一,当对象年龄增加到15之后证明这个对象经久不衰会被放到老年代该年龄的最大值为15具体原因可以参照对象头的MarkWork(埋一个坑),到达那个年龄会被放到老年代是可以由JVM参数-XX:MaxTenuringThreshold控制的例如-XX:MaxTenuringThreshold=7。
  • 在某些特殊情况下例如对象特别大时也会进入老年代(-XX:PretenureSizeThreshold可以调整这个值),这样做的好处是可以避免大对象在没有死去的时候频繁的在新生代复制来复制去,坏处是可能会触发Full GC,所以在平时编程的时候要尽量避免大对象。
  • 为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。

3.2.2 老年代的垃圾回收

老年代的垃圾回收方式主要为标记清除和标记整理,如果之后该区域发生GC那么该GC被称为Major GC/Old GC目前只有CMS会单独收集老年的,正常情况下都是Full GC 全部区域的回收,因为在回收老年带的垃圾的时候会触发STW所以要尽量避免该区域的垃圾回收。

4、虚拟机栈

请添加图片描述
Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:Java虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

4.1、局部变量表

局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译为class文件时。就在方法的code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。

Java虚拟机规范没有明确指出一个变量槽的大小,但是每个变量槽都应该能存放一个Boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存储。对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配俩个连续的变量槽空间,Java语言中明确64位的数据类型只有long和double两种。

Java虚拟机通过索引定位的方式使用局部变量表,索引值的范围时从0开始至局部变量表最大的变量槽数。如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽,如果访问的是64位数据类型的变量,则说明会同时使用第N和N+1俩个变量槽。

当一个方法被调用,Java虚拟机会使用局部变量表完成参数值到参数变量列表的传递过程,如果执行的是实例方法,那局部变量表中第0位索引的变量槽默认是实例引用(可通过this使用),其余参数按照参数表顺序排列。为了尽可能节省栈帧耗用的内存空间,局部变量表的变量槽是可以复用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的变量槽就可以交给其他变量来重用,这里需要知道的时候变量槽的复用不一定是好事,可能会让已经无用的对象无法回收.

public static void main(String[] args) {
    {
        byte[] placeholder = new byte[64 * 1024 * 124];
    }
    System.gc();
}
/**
[GC (System.gc())  13179K->8740K(251392K), 0.0041632 secs]
[Full GC (System.gc())  8740K->8526K(251392K), 0.0043666 secs]
*/
public static void main(String[] args) {
    {
        byte[] placeholder = new byte[64 * 1024 * 124];
    }
    int x = 1;
    System.gc();
}
/**
[GC (System.gc())  13179K->820K(251392K), 0.0019925 secs]
[Full GC (System.gc())  820K->593K(251392K), 0.0036329 secs]
*/

可以看到示例一中的64M没有被回收,由于现代JVM判断一个对象是否还活着的方式是可达性分析,从GC Root出发只要能到达那说明还活着,在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

在执行 System.gc()时虚拟机栈帧的槽位里还指向的是placeholder,所以代码一中placeholder在GC的时候placeholder被认定为还活着就无法回收,在示例二中槽位的共用原来指向placeholder的槽位执行int x了所以通过可达性分析判断placeholder已经死亡了所以示例二回收了placeholder。

4.2 操作数栈

操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。Javac编译器的数据流分析工作保证了在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器必须要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。再以上面的iadd指令为例,这个指令只能用于整型数的加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况。

4.3、动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号
引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。

4.4、方法返回地址

当一个方法开始执行后,只有两种方式退出这个方法。

  • 第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为“正常调用完成”(Normal Method Invocation Completion)。
  • 另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用完成(Abrupt Method Invocation Completion)”。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的。

无论采用何种退出方式,在方法退出之后,都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。一般来说,方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。

5、本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。

6、程序计数器

  • JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
  • PC寄存器的作用:PC寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。
  • 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
  • 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值