JVM内存结构【原创】
JVM内存结构
imagejava虚拟机会在执行java程序的过程中会把它所管理的内存区域划分为若干个数据区域,这些区域都有各自的用途。其中有些区域随着虚拟机进程的启动而存在,而有些区域则依赖用户线程的启动和结束而建立和销毁。具体划分如下图
程序计数器
1.一块较小的内存空间
2.字节码解释器就是通过改变程序计数器的值来选取下一条需要运行的字节码指令
3.不存在内存溢出的区域
4.线程私有
JAVA虚拟机栈
1.线程私有
2.描述的是java方法执行的模型
3.方法执行的时候会创建一个栈帧,方法的执行过程实际也就是栈帧在虚拟机栈中的入栈出栈的过程
栈是一种先进后出的数据结构,JAVA虚拟机栈也不例外。不过这里入栈出栈的对象是方法,每个方法执行的同时都会都会创建一个栈帧,可以理解为一个容器,里面存储了局部变量表,操作数栈,动态链接,方法出口等信息。而一个方法的调用直至完成就是栈帧在虚拟机栈的入栈出栈过程。在方法中我们通常会这么定义变量
public static void main(String[] args) {
int a = 10;
}
实际上这个main方法是占内存空间的,并且其所占的内存大小在编译期间就分配好了。(局部变量表存放了编译期间可知的基本数据类型和对象引用,其中除了long和double占2个局部变量空间,其他的都是一个局部变量空间。)
在此块空间中JAVA虚拟机规范定义了两种异常:
1\. StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度就会抛出此异常。比如进行循环遍历操作的时候
2.OutOfMemoryError:Java虚拟机栈扩展时无法申请到足够的内存的时候容易出现,也就是OOM。
思考:栈既然是线程私有的,内存一般是有限的,假如线程数量过多,所需内存大于我们的物理内存,这时候该如何是好?栈内存是可以调节大小的,此时我们可以将栈内存调小来支持更多的线程数量。
本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的。不过JAVA虚拟机栈是为了我们自己写的JAVA方法服务的,而本地方法栈是为虚拟机使用的Native方法服务的。虚拟机对此部分并没有强制规定,在不同的虚拟机产品中有不同的实现。我们常用的HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一了。因此本地方法栈也存在上述的两个异常。
JAVA堆
- java堆是java虚拟机所管理的内存中最大的一块。
2. 所有线程共享的区域
3. 基本上所有的对象实例和数组都在此块区域分配空间
4. 垃圾收集器管理的主要区域
5. 可以是物理上不连续的空间但要求是逻辑上连续的空间
因为回收算法的因素,java堆还可以细分为新生代和老年代。如果堆中没有内存完成实例分配,并且无法再扩展的时候也会出现OutOfMemoryError异常。
思考:不是说我们存在垃圾回收机制吗?并且java和c,c++最大的区别就在于内存的管理。我们不可能所有的对象都一直在使用中,那么内存满了无法进行堆扩展会不会是因为垃圾回收器没工作?其实这里存在了两种情况:1.内存泄漏,2.内存溢出.具体下一章给出解释。
方法区
1.各个线程共享
2.存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码
在HotSpot虚拟机中,这部分可以被垃圾回收器所管理,因此又被称之为“永久代”,垃圾回收器在这块主要是对常量池的回收和堆类型的卸载,有意思的是因为在方法区实现常量池容易出现OOM,在jdk1.7中字符串常量池已经从方法区移到另一个地方去了。哪呢?答案是堆中。
运行时常量池(存在方法区中)
运行时常量池是方法区中的一部分。用于存放Class文件编译期间生成的各种字面量和符号引用。
字面量:int a = "java"; 此时 a就可以被称之为字面量 Integer a = new Integer(10);此时 a称之为符号引用
当常量池中无法申请到内存时也会抛出OutOfMemoryError异常。
在这有一个有意思的问题:
在上面我们说到了字符串常量池以前是在方法区的,也是属于运行时常量池中的一种,但是后面字符串常量池迁到了堆中,就带来了下面这个现象:
public static void main(String[] args) {
String str1 = new StringBuilder("ha").append("nd").toString();
System.out.println(str1.intern() == str1);
}
上述代码在jdk1.6中得到的结果是false但是在jdk1.7中却是true。原因是jdk1.6的时候intern()方法会把首次遇到的字符串复制到永久代中(方法区),返回的也是永久代中的引用,而StringBuilder创建的字符串实例在java堆上,所以为false。在jdk1.7中intern()不在把对象复制到永久代,而是在常量池中记录首次出现的实例引用,因此jdk1.7中返回的实际上和StringBuilder创建的实例返回到的引用是同一个。因此是true。
有了上面的intern()函数的知识,大家觉得下面这个是true还是false呢?
public static void main(String[] args) {
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
答案是false;是不是和预料中不一致呢
实际上是因为java这个字符串不是第一次出现了,它已经存在于字符串常量池中。
image我们再来看看这个:
image是不是有什么想法呢?
参考书籍:《深入理解java虚拟机》