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
image.png
可以看出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)
}
如下图
image.png
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;
}
}
image
对于以上这段代码,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()将等待垃圾回收.
网友评论