关于java堆栈的讨论,老是在群里遇到.百度出来的,大部分都是千篇一律,都是转载的,看的也是云里雾里.这几天经过自己查资料和询问别人,将内容理了一下.记录下来.(错误之处请指正).
还是用我惯用的方式来写. 首先我们提出问题.
1.jvm运行时,内存里面都分了哪些区?
2.这些区里面都装了什么内容?
3.这些区会内存溢出吗,什么情况下会出现?报错信息是什么?怎么设置每个区的大小?
4.这些区跟多线程有没有什么关系?
5.内存结构跟内存模型有没有什么关系?
OK.下面来回答问题.
1.jvm运行时,内存里面都分了哪些区?
这其实就是JVM运行时数据区结构图,下面这个图参考《深入理解Java虚拟机》一书.
这个图说明了内存结构是什么样子的,线程共享和私有的区我也标出来了. 图下方的 执行引擎,本地库接口,本地方法库,不属于内存结构里的.不讨论.
2.这些区里面都装了什么内容?
我们一个一个来说:
2.1程序计数器(Program Counter Register)程序计数器是线程私有的,生命周期和线程一致.
程序在执行的时候,可能会中断(cpu切换到别的任务去了),当再恢复的时候,需要知道从哪开始继续执行.需要做个标记, 程序计数器,就是用来保存这个标记的.
因为程序会有多个线程同时运行,所以计数器肯定是要线程隔离,才能保证不会乱套.另外如果正在执行本地方法的话,这里面就是空的.
2.2 虚拟机栈(VM Stack)虚拟机栈是线程私有的,生命周期和线程一致.
平时很多人说的引用存栈上,对象存堆上. 其中的栈,就是这里说的虚拟机栈.
虚拟机栈描述的是java方法执行的内存模型,即每个方法执行的时候,都会同时创建一个栈帧(Stack Frame,一种数据结构),用于存放局部变量表,操作数栈,方法出口等信息.
一个方法被调用到执行完成,其实就是在 虚拟机栈中 入栈到出栈的一个过程.
局部变量表里面存的类型只能是基本类型和引用类型,
2.3 本地方法栈(Native Method Stack)本地方法栈是线程私有的,生命周期和线程一致.
本地方法栈和虚拟机栈作用类似,只不过这里存的是native的信息.因为有些jvm直接就把 本地方法栈和虚拟机栈合二为一,所以有些书都没有讲这些.
2.4 堆(Heap)堆是线程共享的,生命周期和Jvm一致.(随jvm启动而创建,一个jvm实例只有一个堆)
我们平常说的堆,就是这个堆了.(图上看堆的圆圈很小,其实它是占用内存最大的...嘿嘿)
堆就是用来存放对象实例的.Java虚拟机规范中描述到:所有的对象实例以及数组都要在堆上分配.(《深入理解Java虚拟机》一书提到,随着JIT的发展和逃逸分析技术的城市,栈上分配,标量替换等优化技术会导致一些微妙的变化发生,所有的对象都分配在堆上渐渐变得不是那么绝对了.. 这玩意太高深了.我就当没有这回事算了).
堆是垃圾回收器管理的主要区域,所以这里面又会因为垃圾回收器的算法原因而细分为一些小分区. 例如老年代,新生代等等.这个以后再讨论.
2.5方法区(Method Area) 方法区是线程共享的,生命周期和jvm一致.
方法区是用来存放被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据.
java虚拟机规范中,把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap,目的应该是与堆 区分开来.
有些人习惯把方法区称为永久代(Permanent Generation),但是本质上两者并不等价.因为只是HotSpot虚拟机是用永久代来实现方法区的,别的虚拟机就不是.而且HotSpot也有放弃永久代的规划.
2.6运行时常量池(Runtime Constant Pool) 运行时常量池是方法区的一部分(所以在图上没有画出来)
常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References).
字面量就是我们平常说的常量,像文本字符串,final修饰的常量等. 符号引用则是指 类和接口的全限定名,字段的名称和描述符,方法的名称和描述符 这些.(符号引用术语编译原理方面的概念.我的理解就是指 类,方法,字段的名称,很片面不过容易理解.)
2.7 直接内存(Direct Memory) 直接内存不是上图中的一部分,在虚拟机规范中也没有定义.
直接内存不归jvm管理,之所以每个讲内存的地方都要讲这一块,应该是因为NIO. NIO是直接操作堆外的内存,避免在java堆和native堆来回复制数据.
直接内存不归jvm管理,所以大小自然也不受jvm控制. 但是还是受到电脑硬件和操作系统的限制.
3.这些区会内存溢出吗,什么情况下会出现?报错信息是什么?怎么设置每个区的大小?
关于各个分区内存溢出的情况,在《深入理解Java虚拟机》一书中有详细描述,这里大部分都是摘抄.还是一个一个来说.
3.1 程序计数器
程序计数器占用内存很小,而且是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域.所以不存在内存溢出情况.
3.2 虚拟机栈 通过-Xss 设置每个线程的栈大小
虚拟机栈会内存溢出,且分为两种情况:
如果线程请求的栈深度大于虚拟机所允许的最大深度,抛出 StackOverflowError 异常.(java.langStackOverflowError)
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError 异常.(java.lang.OutOfMemoryError: unable to create new native thread)
这两种情况,貌似有重叠的地方,好理解,但是不太容易区分.书中对这2种情况分别进行了测试. 第一种直接无线递归就会出现. 第二种则必须在多线程下不断启用新线程调用方法才会出现.
3.3 本地方法栈 通过-Xoss设置本地方法栈大小(HotSpot没用,因为不区分虚拟机栈和本地方法栈)
和虚拟机栈一样.
3.4 堆 通过-Xms和-Xmx 限制大小
堆里面存放的是对象实例,当对象实例过多,装不下时,自然就溢出了.这个报错,应该很常见. (java.lang.OutOfMemoryError: Java heap space)
3.5方法区 通过-XX:PermSize和-XX:MaxPermSize 限制大小
方法区用来存放类信息,当类信息过多过大,装不下时,自然也会溢出.这个报错,也很常见. (java.lang.OutOfMemoryError: PermGen space)
3.6运行时常量池 只能通过方法区大小设置
常量池是方法区的一部分,当存放常量多多时,也就溢出了. 报错和方法区是一样的. (java.lang.OutOfMemoryError: PermGen space)
3.7直接内存 通过-XX:MaxDirectMemorySize 限制大小,默认和Java堆的最大值一样
报错信息为 (java.lang.OutOfMemoryError)
4.这些区跟多线程有没有什么关系?
其实上面每个分区是线程私有,还是线程共享已经都标识出来了,但是感觉还是比较笼统.至少我是理解了好半天.
我们来看段代码.好说明.
import java.util.ArrayList;
import java.util.List;
class TestA{
public TestB b;
}
class TestB{
public int age;
}
public class Test {
private List<TestA> list = new ArrayList<TestA>();
private void addA(TestA a){
list.add(a);
}
private List<TestA> getListA(){
return list;
}
public static void run(){
TestA a1 = new TestA();
<span style="white-space:pre"> </span>TestA a2 = new TestA();
<span style="white-space:pre"> </span>Test t1 = new Test();
t1.addA(a1);
System.out.println(t1.getListA().get(0).b.<span style="font-family: Arial, Helvetica, sans-serif;">age</span>);
}
public static void main(String[] args) {
run();
}
}
在这段代码中,定义了三个类, Test,TestA,TestB. 运行时,他们的类信息都会放在方法区中.然后我们从run方法看起.(main方法是个入口,比较特殊)
run方法第一行,TestA a1 = new TestA(); 会牵扯到三个区.
1). a1 会作为引用类型,存在栈上.
2). new TestA() 会作为实例数据存在堆上
3).TestA的对象类型数据(如父类,实现的接口,方法等) 都会存在方法区中.
OK.现在我们知道了他们存储的地方, 然后结合最上面的图, 我们能得到, a1 是线程安全的, new TestA()以及TestA的对象类型数据,是线程共享的,也就是说可能是线程不安全的.
这里我们先不考虑方法区,因为这里面信息一般应该只是读. 我们来着重看下 a1 和 new TestA();
先看a1, a1是个局部变量,局部变量是线程安全的,这句话,大家应该听过.因为每次方法执行时,都是在栈上创建一个栈帧,自然每次的a1,都是新的,而且方法结束后,a1所在的栈帧就没了.a1自然也没了.
再来看 new TestA() .其实每次方法执行的时候,都会在堆中创建一个新的实例,就像a1一样.只是创建的地方不一样. 我们知道,方法调用结束后,a1就消失了,那么 new TestA() 是否会消失呢?
这里牵扯到java的垃圾回收机制.后面详细讨论. 简单讲, 还有没有地方用到这个new TestA()呢. 如果有,就不会消失. 如果没有,就处于等待消失的状态(即等待GC).
对于我们的例子来说, a1 对应的 new TestA(), 因为后面有 t1.addA(a1) 这句话,所以说,它还被引用,不会消失. 而 a2对应的 new TestA(). 因为后面没有操作,当方法执行完成,它就处于等待消失的状态.
2个new TestA()都在堆里面, 都是线程共享的. a1对应的 new TestA()被加入到t1.list里面去了. 我们可以想象一下.如果是多线程操作,别的线程,就可以拿到a1对应的 new TestA(),然后进行修改操作,
这样其他线程就会受到影响, 这就是线程不安全的. 回过头来看a2 对应的 new TestA(), 因为方法结束后,就没有引用了. 所以别的线程没办法得到 a2对应的 实例, 也就没办法修改.(也不能说绝对没办法,如果TestA重写finalize方法,并且在方法里又把自己挂上一个引用,那么也会有问题.) 总之, 堆里面的是线程不安全的.
这里还要说下我原来的一个疑问, 即引用存栈上,实例存堆上. 那么a1 中的 TestB b , 是怎么存? 也是存在栈上? 然后TestB的实例存在堆上? TestB里面属性age是int基本类型,然后再存在栈上? 这样交叉来交叉去吗?
答案是否定的.基本类型会存在栈上,但是不一定非得在栈上,主要看声明变量的位置.
关于变量存放位置的问题,在网上看了一个例子,很能说明问题.搬到这里看看.
class BirthDate {
private int day;
private int month;
private int year;
public BirthDate(int d, int m, int y) {
day = d;
month = m;
year = y;
}
// 省略get,set方法………
}
public class Test{
public static void main(String args[]){
int date = 9;
<span style="white-space:pre"> </span>Test test = new Test();
test.change1(date);
BirthDate d1= new BirthDate(7,7,1970);
}
public void change1(int i){
i = 1234;
}
}
下面是对上面代码的分析.
1. main方法开始执行:int date = 9;
date局部变量,基础类型,引用和值都存在栈中。
2. Test test = new Test();
test为对象引用,存在栈中,对象(new Test())存在堆中。
3. test.change1(date);
i为局部变量,引用和值存在栈中。当方法change1执行完成后,i就会从栈中消失。
4. BirthDate d1= new BirthDate(7,7,1970);
d1 为对象引用,存在栈中,对象(new BirthDate())存在堆中,其中d,m,y为局部变量存储在栈中,且它们的类型为基础类型,因此它们的数据也存储在栈中。day,month,year为成员变量,它们存储在堆中(new BirthDate()里面)。当BirthDate构造方法执行完之后,d,m,y将从栈中消失。
5.main方法执行完之后,date变量,test,d1引用将从栈中消失,new Test(),new BirthDate()将等待垃圾回收。
5.内存结构跟内存模型有没有什么关系?
jvm内存结构:顾名思义,就是内存是以什么结构存的.
java内存模型(Java Memory Model,JMM):主要目标是用来定义程序中各个变量的访问规则,来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果.
jvm内存分配策略:分配策略主要是跟回收策略对应的. GC的算法会影响分配策略.
关于内存分配策略和回收策略,以及内存模型,后面再写文章讨论.