前言:该文章为转载,觉得作者写的非常好,就想整理一下转发,想留给自己方便日后复习浏览
1、Java栈
Java在函数中定义的基本类型(int,long,short,byte,float,double,boolean,char)的变量(局部变量和函数的形参)的引用和数据,以及对象的引用都放在栈中存储。
1、栈的特点
1、存取速度快。仅次于CPU中的寄存器。
2、每个线程都会有一个栈空间,不同栈之间不能直接访问,所以线程之间不能共享栈中的数据。
3、存在栈中的数据是可以共享的。
比如我们定义int a=3;int b=3;a=4;
编译器先处理int a=3;首先会在栈中创建一个变量a的引用,然后在栈中查找有没有字面值为3的地址,如果有则将a指向这个地址;如果没有则开辟一块内存放字面值3,然后将a引用指向这个地址。
接着处理int b=3;,在栈中创建b的引用变量,由于在栈中字面值为3的地址已经存在,所以直接将b指向这个地址。这样a和b同时指向了字面值为3的这个地址。
接着处理a=4;,会在栈中查找字面值为4的地址,如没有就开辟内存存放字面值4,让a指向这个地址,如有就将a直接指向这个地址。
此时b依然等于3,不会等于4。这点和对象的引用不同,需要注意。
4、如果栈内存中没有足够的空间可以使用,JVM会抛出java.lang.StackOverFlowError异常。
5、栈中定义的变量,在超出变量的作用域后,Java会自动释放为变量所分配的内存空间,该内存空间可以立即被另作他用。
2、堆
堆中主要用于存放new出来的对象和数组。下面举例看下对象的实例化过程。
Person a=new Person ("123");
Person b=new Person ("123");
编译器会先执行Person a=new Person ("123")
;,会在堆中开辟内存存放创建的对象Person ("123")
,在栈中创建变量a,将a指向对象的内存首地址,a就是该对象的引用。
接着执行Person b=new Person ("123")
;,虽然前面已经创建了对象Person ("123")``,但是只要使用关键字new
,就会在堆中开辟一块新的内存存放创建的对象,在栈中创建变量b,将b指向新对象的首地址。所以a和b指向并不是同一个对象。
如果定义Person c = a;a.setName("234")
那么String name=c.getName()
就等于“234”
。
执行Person c = a
;则变量a和c都会指向同一个对象,所以使用a变量修改对象中的内容时,c指向的对象的内容也会改变。
2.1、特点
1、存取速度比栈中的慢。
2、一个JVM只有一个堆内存,所以线程间是可以共享堆内存中的数据的。
3、如果堆中内存不足,则会抛出java.lang.OutOfMemoryError异常。
4、定义在栈中的变量a指向堆中的对象Person的首地址,在超出变量的作用域后,Java会自动释放a所分配的内存空间,此时就没有引用指向对象了,但是对象并不会马上被回收,需要等某个时间通过垃圾回收来回收内存。
3、常量池
常量池分为两种:静态常量池
和运行时常量池
。
静态常量池
静态常量池指的是在编译期确定,保存在class文件中的一些数据。常量池主要用于存放两大类常量:字面量(Literal)和符号引用量,字面量相当于Java语言层面常量的概念,如文本字符串、声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
1、类和接口的全限定名;2、字段的名称和描述符;3、方法的名称和描述符。
运行时常量池
在运行时常量池是方法区的一部分,在JDK1.7之后运行时常量池从方法区中移出,放在堆中。常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
3.1、字符串常量池
对于字符串,其对象的引用都是存储在栈中的,如果是编译期已经创建好
(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)
才能确定的就存储在堆中。对于equals
相等的字符串,在常量池中永远只有一份,在堆中有多份。
String s1 = "china";
String s2 = "china";
String s3 = "china";
String ss1 = new String("china");
String ss2 = new String("china");
String ss3 = new String("china");
s1==s2==s3; //true
ss1!=ss2!=ss3!=s1!=s2!=ss3; //true
ss1.eqauls(ss2);//true
ss1.eqauls(ss3);//true
ss1.eqauls(s1);//true
ss1.eqauls(s2);//true
ss1.eqauls(s3);//true
可以看出s1、s2、s3在编译期就被创建,并存入到了常量池中。编译器在执行
String s1 = "china";
时会先在常量池中查找是否存在字符串常量“china”,如果不存在就在常量池中new一个china字符串,存在就不new,然后让栈中的变量指向这个china字符串。因此常量池中只有一个china字符串对象,然后在执行
String s2 = "china";String s3 = "china";
时,会在常量池中找到china字符串,并让s2、s3指向它。
对于
String ss1 = new String("china");
在编译时并不会创建,在运行时,通过new产生一个字符串(假设为“china”)时,会先去常量池中查找是否已经存在字符串“china”,如果不存在则在常量池中创建一个“china”字符串对象,然后在堆中再创建一个常量池中的“china”对象的拷贝对象;如果常量池中存在,就直接在堆中创建一个常量池中此“china”对象的拷贝对象。
3.2、包装类实现了常量池技术
对于8种基本数据类型大部分都有自己的包装类,其中Byte,Short,Integer,Long,Character,Boolean
都实现了常量池技术,;而Byte,Short,Integer,Long类型
在装箱时会缓存了范围[-128,127]
的数据到数组中,Character
会缓存[0,127]
范围的数据到数组中进行缓存。
1、对于Integer
来说,范围是[-128,127]
的数在自动装箱时全部被自动加入到了常量池
里面,具体可查看Integer.valueof(int i)
方法。Integer.valueOf()
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
//IntegerCache类
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
实例
public class test {
public static void main(String[] args) {
Integer i1=10;
Integer i2=10;
System.out.println(i1==i2);
System.out.println(i1.equals(i2));
}
}
输出:
true
true
2、使用new关键字创建Integer
时,即使数据在范围[-128,127]
,也不会去缓存中查找,直接在堆中创建一个新的Integer对象
。并不会像new String("123")
可能需要在堆
中或常量池
中各创建一个对象。
Integer i1=new Integer(10);
Integer i2=new Integer(10);
Integer i3=10;
System.out.println(i1==i2); //false
System.out.println(i1.equals(i2)); //true
System.out.println(i1==i3); //false
System.out.println(i1.equals(i3)); //true
3、当整数不在[-128,127]
范围内时,就会在堆
中创建对象。
看下面例子在内存中的分配。
public void test() {
int a1 = 9; //自动拆箱 Integer.intValue()
int b1 = 9; //自动拆箱 Integer.intValue()
final int A2 = 9;
final int B2 = 9;
Integer a3 = new Integer(9);
Integer b3 = new Integer(9);
Integer a4 = 9; //自动装箱 调用Integer.valueOf(int)
Integer b4 = 9; //自动装箱 调用Integer.valueOf(int)
}
4、方法区
方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息
、常量
、静态变量
、即编译器编译后的代码等数据
。静态变量
、常量在方法区
,所有方法,包括静态
和非静态
的,也在方法区。
5、成员变量和局部变量在内存中的分配
局部变量
的数据存在于栈
内存中。栈内存中的局部变量随着方法的消失而消失。 成员变量
存储在堆
中的对象里面,由垃圾回收器负责回收。
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;
Test test = new Test();
test.change(date);
BirthDate d1 = new BirthDate(7, 7, 1970);
}
public void change(int i) {
i = 1234;
}
}
对于以上这段代码,date
为局部变量
,i,d,m,y
都是形参为局部变量
,day,month,year
为成员变量
。下面分析一下代码执行时候的变化:
main方法开始执行:int date = 9
; date局部变量
,基础类型
,引用和值
都存在栈
中。Test test = new Test();test
为对象引用,存在栈
中,对象(new Test()
)存在堆
中。test.change(date)
; i为局部变量
,引用和值存在栈
中。当方法change
执行完成后,i就会从栈
中消失。BirthDate d1= new BirthDate(7,7,1970)
; d1为对象引用,存在栈
中,对象(new BirthDate()
)存在堆
中,其中d,m,y为局部变量存储在栈
中,且它们的类型为基础类型
,因此它们的数据也存储在栈
中。day,month,year为成员变量
,它们存储在堆
中(new BirthDate()
里面)。当BirthDate
构造方法执行完之后,d,m,y
将从栈
中消失。main方法
执行完之后,date变量,test,d1引用
将从栈
中消失,new Test(),
new BirthDate()
将等待垃圾回收.