Java内存模型是深入理解java虚拟机原理的基础,学会内存分析,才能对虚拟机进行性能调优。
这块知识还是比较抽象的,为了便于大家理解,我们以一段实际可执行的样例代码来分析。
public class Math{
public static int initData = 666;
public static User user = new User();
public int compute(){
int a = 1;
int b = 2;
int c = (a+b)*10;
return c;
}
public static void main(String[] args){
Math math = new Math();
math.compute();
}
}
虚拟机装载运行这段代码后,其内存空间情况如下图。
类加载子系统
Java虚拟机提供了一套类加载器,基于class文件结构规范,对编译之后的class文件进行解析加载到内存区域。加载运行之后的内存空间可以划分为线程私有的程序计数器、栈和本地方法栈,以及线程共享的堆和方法区。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5oedvlwZ-1594134692119)(./images/class文件结构.png)]
程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
栈
栈(Stack)是一种先进后出的数据结构,操作速度仅次于寄存器,优于堆。栈帧(Stack Frame)是用来支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的基本结构。简单的理解,一个函数对应一个栈帧。栈帧包含局部变量、操作数栈和动态链接和方法出口。
本地方法栈
我们知道java语言是改良于C++的,java出现时还是C语言家族独领风骚的年代。本地方法则提供了java利用其它语言优势的接口,为这些本地方法(函数)提供的栈则称为本地方法栈。
堆
《深入理解java虚拟机》是什么描述java堆的
- Java堆(Java Heap)是java虚拟机所管理的内存中最大的一块
- java堆被所有线程共享的一块内存区域
- 虚拟机启动时创建java堆
- java堆的唯一目的就是存放对象实例。
- java堆是垃圾收集器管理的主要区域。
- 从内存回收的角度来看, 由于现在收集器基本都采用分代收集算法, 所以Java堆可以细分为:新生代(Young)和老年代(Old)。 新生代又被划分为三个区域Eden、From Survivor, To Survivor等。无论怎么划分,最终存储的都是实例对象, 进一步划分的目的是为了更好的回收内存, 或者更快的分配内存。
- java堆的大小是可扩展的, 通过-Xmx和-Xms控制。
- 如果堆内存不够分配实例对象, 并且对也无法在扩展时, 将会抛出outOfMemoryError异常。
方法区
是各个线程共享的内存区域,用于存储class二进制文件,包含了虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
方法区中一块重要类型就是常量池。
Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
- 类和接口的全限定名
- 字段名称和描述符
- 方法名称和描述符
而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是**String类的intern( )**方法。
String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。
常量池的好处
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,等于号只用判断引用是否相等,也就可以判断实际值是否相等。