Java 程序的数据结构是非常丰富的。其中的内容,举一些例子:
1.静态成员变量
2.动态成员变量
3.区域变量
4.短小紧凑的对象声明
5.庞大复杂的内存申请
这么多不同的数据结构,到底是在什么地方存储的,它们之间又是怎么进行交互的呢?
JVM 内存区域划分如图所示,从图中我们可以看出:
JVM 堆中的数据是共享的,是占用内存最大的一块区域。
可以执行字节码的模块叫作执行引擎。
执行引擎在线程切换时怎么恢复?依靠的就是程序计数器。
JVM 的内存划分与多线程是息息相关的。像我们程序中运行时用到的栈,以及本地方法栈,它们的维度都是线程。
本地内存包含元数据区和一些直接内存。
一、线程私有(线程维度)
1.虚拟机栈
1.什么是虚拟机栈
Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,即生命周期和线程相同。Java虚拟机栈和线程同时创建,用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接 、 方法出口等信息。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
2.什么是栈帧
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数 栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
3.局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。 其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。
4.操作数栈
操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。
5.动态链接
1.指向运行时常量池的方法引用;
2.每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用;
3.包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking);
4.在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的;
5.动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用;
6.方法返回地址
方法返回地址存放调用该方法的PC寄存器的值。一个方法的结束,有两种方式:正常地执行完成,出现未处理的异常正常的退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者 的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过 异常表来确定,栈帧中一般不会保存这部分信息。
无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。
2.程序计数器
程序计数器(Program Counter Register):也叫PC寄存器,是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条 需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。
因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
3.本地方法栈
本地方法栈(Native Method Stacks) 与虚拟机栈所发挥的作用是非常相似的, 其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码) 服务, 而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
(1)本地方法栈是加载native的方法, native类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的。
(2)虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
(3)是线程私有的,它的生命周期与线程相同,每个线程都有一个。
在Java虚拟机规范中,对本地方法栈这块区域,与Java虚拟机栈一样,规定了两种类型的异常:
(1)StackOverFlowError :线程请求的栈深度>所允许的深度。
(2)OutOfMemoryError:本地方法栈扩展时无法申请到足够的内存。
二、共享区域
1.堆
1.1堆的概念
对于Java应用程序来说, Java堆(Java Heap) 是虚拟机所管理的内存中最大的一块。 Java堆是被所 有线程共享 的一块内存区域, 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例, Java 世界里“几乎”所有的对 象实例都在这里分配内存。“几乎”是指从实现角度来看, 随着Java语 言的发展, 现在已经能看到些许迹象表明日 后可能出现值类型的支持, 即使只考虑现在, 由于即时编译技术的进步, 尤其是逃逸分析技术的日渐强大, 栈上分配、 标量替换优化手段已经导致一些微妙的变化悄然发生, 所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。
1.2堆的特点
(1)是Java虚拟机所管理的内存中最大的一块。
(2)堆是jvm所有线程共享的。堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer (TLAB)
(3)在虚拟机启动的时候创建。
(4)唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。
(5)Java堆是垃圾收集器管理的主要区域。
(6)因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器 基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;
新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间。
(7)java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)。
(8)方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除。
(9)如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
1.3年轻代和老年代
1)年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分 成1个Eden Space和2个Suvivor Space(from 和to)。
2)年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍 然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。
配置新生代和老年代堆结构占比
默认 -XX:NewRatio=2 , 标识新生代占1 , 老年代占2 ,新生代占整个堆的1/3
修改占比 -XX:NewPatio=4 , 标识新生代占1 , 老年代占4 , 新生代占整个堆的1/5 Eden空间和另外两个Survivor空间占比分别为8:1:1
可以通过操作选项 -XX:SurvivorRatio 调整这个空间比例。 比如 -XX:SurvivorRatio=8 几乎所有的java对象都在Eden区创建, 但80%的对象生命周期都很短,创建出来就会被销毁.
1.4对象分配过程
JVM设计者不仅需要考虑到内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,因此还需要考虑GC执行完内存回收后是否存在空间中间产生内存碎片。
分配过程:
1.new的对象先放在伊甸园区。该区域有大小限制;
2.当伊甸园区域填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园预期进行垃圾回收(Minor GC),将伊甸园区域中不再被其他对象引用的对象进行销毁,再加载新的对象放到伊甸园区;
3.然后将伊甸园区中的剩余对象移动到幸存者0区;
4.如果再次触发垃圾回收,此时上次幸存下来的放在幸存者0区的,如果没有回收,就会放到幸存者1区;
5.如果再次经历垃圾回收,此时会重新返回幸存者0区,接着再去幸存者1区。
6.如果累计次数到达默认的15次,这会进入养老区。
可以通过设置参数,调整阈值 -XX:MaxTenuringThreshold=N
7.养老区内存不足是,会再次出发GC:Major GC 进行养老区的内存清理;
8.如果养老区执行了Major GC后仍然没有办法进行对象的保存,就会报OOM异常.
2.方法区
方法区(Method Area) 与Java堆一样, 是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
元空间、永久代是方法区具体的落地实现。方法区看作是一块独立于Java堆的内存空间,它主要是用来存储所加载的类信息的。
1.方法区的特点:
(1)方法区与堆一样是各个线程共享的内存区域;
(2)方法区在JVM启动的时候就会被创建并且它实例的物理内存空间和Java堆一样都可以不连续;
(3)方法区的大小跟堆空间一样可以选择固定大小或者动态变化;
(4)方法区的对象决定了系统可以保存多少个类,如果系统定义了太多的类导致方法区溢出虚拟机同样会跑出(OOM)异常(Java7之前是 PermGen Space (永久带) Java 8之后 是MetaSpace(元空间) );
(5)关闭JVM就会释放这个区域的内存;
2.方法区的结构
类加载器将Class文件加载到内存之后,将类的信息存储到方法区中。
方法区中存储的内容:
(1)类型信息(域信息、方法信息);
(2)运行时常量池;
类型信息:对每个加载的类型(类Class、接口 interface、枚举enum、注解 annotation),JVM必须在方法区中存储以下类型信息。
(1)这个类型的完整有效名称(全名 = 包名.类名);
(2)这个类型直接父类的完整有效名(对于 interface或是java.lang. Object,都没有父类);
(3)这个类型的修饰符( public, abstract,final的某个子集);
(4)这个类型直接接口的一个有序列表;
域信息:即为类的属性,成员变量。
(1)JVM必须在方法区中保存类所有的成员变量相关信息及声明顺序;
(2)域的相关信息包括:域名称、域类型、域修饰符(pυblic、private、protected、static、final、volatile、transient的某个子集);
方法信息:JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序。
(1)方法名称和返回类型(或void);
(2)方法参数的数量和类型(按顺序);
(3)方法的修饰符public、private、protected、static、final、synchronized、native、abstract的一个子集;
(4)方法的字节码bytecodes、操作数栈、局部变量表及大小( abstract和native方法除外);
(5)异常表( abstract和 native方法除外)。每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引;
3.运行时常量池
常量池:存放编译期间生成的各种字面量与符号引用; 字节码文件中 ,内部包含了常量池。 运行时常量池:常量池表在运行时的表现形式; 方法区中 ,内部包含了运行时常量池。
编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中。
理解为字节码中的常量池 Constant pool 只是文件信息,它想要执行就必须加载到内存中。而Java程序是靠 JVM,更具体的来说是JVM的执行引擎来解释执行的。执行引擎在运行时常量池中取数据。被加载的字节码常量池中的信息是放到了方法区的运行时常量池中。