a)基本数据类型
java的基本数据类型共有8种,即int,short,long,byte,float,double,boolean,char(注意,并没有String的基本类型 )。8中基本数据类型存在于栈中。
另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。比如:
我们同时定义:
int a=3;
int b=3;
编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b这个引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。
定义完a与b的值后,再令a = 4;那么,b不会等于4,还是等于3。在编译器内部,遇到时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。
b)对象
在java中,创建一个对象包括对象的声明和实例化两步,下面用一个例题来说明对象的内存模型。假设有类Person定义如下:
public class Person {
String name;
int age;
public Person(){
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
(1)声明对象时的内存模型
用Person p1;声明一个对象 p1 时,将在栈内存为对象的引用变量 p1 分配内存空间,但 Person 的值为空,称 p1 是一个空对象。空对象不能使用,因为它还没有引用任何”实体”。
(2)对象实例化时的内存模型
当执行 p1=new Person("w",5);时,会做两件事:在堆内存中为类的成员变量 name,age分配内存,并将其初始化为各数据类型的默认值;接着进行显式初始化(类定义时的初始化值);最后调用构造方法,为成员变量赋值。返回堆内存中对象的引用(相当于首地址)给引用变量 p1 ,以后就可以通过 p1 来引用堆内存中的对象了。
一个类通过使用new运算符可以创建多个不同的对象实例,这些对象实例将在堆中被分配不同的内存空间,改变其中一个对象的状态不会影响其他对象的状态。例如:
Person p1 = new Person();
Person p2 = new Person();
此时,将在堆内存中分别为两个对象的成员变量 name 、 age 分配内存空间,两个对象在堆内存中占据的空间是互不相同的。如果有:
Person p1 = new Person();
Person p2 = p1;
则在堆内存中只创建了一个对象实例,在栈内存中创建了两个对象引用,两个对象引用同时指向一个对象实例。
c)包装类
基本类型都有对应的包装类:如int对应Integer类,double对应Double类等,基本类型的定义都是直接在栈中,如果用包装类来创建对象,就和普通对象一样了。例如:int i=0;i直接存储在栈中。Integer i(i此时是对象)= new Integer(5);这样,i对象数据存储在堆中,i的引用存储在栈中,通过栈中的引用来操作对象。
此外 Integer 包装类还存在越界的问题,当值在-128~127,Integer会先判断缓存中是否已经创建该数据,如果存在则拿来用就可以了,如下图中的 c 和 d 就指向同一个地址值,不存在则会创建。当创建的数据超过-128~127范围时,会新开辟一个缓存区,导致 e 和 f 的引用并不是指向同一个内存地址。
int a = 100;
Integer b = 100; //进行了自动装箱
System.out.println(a == b);//true
Integer c = 100;
Integer d = 100;
System.out.println(c == d); //true
c = 200;
d = 200;
System.out.println(c == d); // false
Integer e = new Integer(300);
Integer f = new Integer(300);
System.out.println(e==f); //false
d)String
String是一个特殊的包装类数据。可以用以下两种方式创建:
1.String str = “abc”;
2.String str = new String(“abc”);
字符串的分配和其他的对象分配一样,需要耗费高昂的时间和空间为代价,如果需要大量频繁的创建字符串,会极大程度地影响程序的性能,因此 JVM 为了提高性能和减少内存开销引入了字符串常量池(Constant Pool Table)的概念。
字符串常量池相当于给字符串开辟一个常量池空间类似于缓存区,对于直接赋值的字符串(String s="xxx"方式1创建)来说,在每次创建字符串时优先使用已经存在字符串常量池的字符串,如果字符串常量池没有相关的字符串,会先在字符串常量池中创建该字符串,然后将引用地址返回变量,如下图所示:
而使用方式 2 创建 也就是说 new String 的方式会首先去判断字符串常量池,如果没有就会新建字符串那么就会创建 2 个对象,即现在常量池中创建一个,然后在堆中在创建一个并指向常量池,如果已经存在就只会在堆中创建一个对象指向字符串常量池中的字符串。
下面我们先来看一道题:
String a = "abc";
String b = "abc";
String b1 = "ab"+"c";
String m = "a";
String n = "bc";
String mn = m+n;//会在堆上创建,有StringBuild执行append()
System.out.println(a==mn); // false
String c = new String("abc");
String d = new String("abc");
String e = new String("abc").intern();
System.out.println(a==b); //true
System.out.println(b==b1);//true
System.out.println(b==c); //false
System.out.println(c==d); //false
System.out.println(a==e); //true
System.out.println(d==e); // false
其中 intern() 方法会直接得到常量池中的引用地址值返回给了 e ,所以 a == e 指向的是同一个地址。
方法传递 string 变量也是一样的操作,如下所示
public class demo01 {
// 字符串,传递的是引用,形参与实参指向同一个常量池字符“dahua”,
// 形参修改之后会在常量池新建一个常量池字符 “test success”,形参指向它,不影响实参
String str = new String("dahua");
char[] ch = {'a','b','c'};
public static void main(String[] args) {
demo01 d = new demo01();
d.change(d.str,d.ch);
System.out.println(d.str+"and");// 输出“dahua”
System.out.println(d.ch); //"gbc"
}
public void change(String str,char ch[]){
str = "test success";
ch[0] = 'g';
}
}
字符串,传递给方法时仍然是引用,此时,形参与实参指向同一个常量池字符“dahua”, 形参修改之后会在常量池新建一个常量池字符 “test success”,形参指向它,而不影响实参
e)数组
当定义一个数组,int x[];或int[] x;时,在栈内存中创建一个数组引用,通过该引用(即数组名)来引用数组。x=new int[3];将在堆内存中分配3个保存 int型数据的空间,堆内存的首地址放到栈内存中,每个数组元素被初始化为0。
f)静态变量
用static的修饰的变量会存放在方法区中
那静态变量与方法是在什么时候初始化的呢?对于两种不同的类属性,static属性与instance属性,初始化的时机是不同的。instance属性在创建实例的时候初始化,static属性在类加载,也就是第一次用到这个类的时候初始化,对于后来的实例的创建,不再次进行初始化。