JVM(一)内存结构

关于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的算法会影响分配策略.



关于内存分配策略和回收策略,以及内存模型,后面再写文章讨论.






评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值