如果是一个类里面的静态成员变量和静态成员方法,它是存储在方法区的,静态成员变量是在方法区的静态域里面,而静态成员方法是在方法区的class二进制信息里面(.class文件和方法区里面的二进制信息不一样,读取.class文件按照虚拟机需要的格式存储在方法区。这种格式包括数据结构方面),静态成员和静态成员方法使用时不用创建对象,即类加载初始化后就可以使用,并且是线程共享的。
通过图中分析,很多问题也能够迎刃而解,比如不同线程调用方法为什么是线程安全的。局部变量存储在哪儿里(栈中),成员变量存储在哪儿里(静态成员变量存储在方法区,非静态成员变量存储在堆区),为什么局部变量不能够static修饰等(局部变量存储在栈区,在方法调用时不能够自动初始化必须由程序员手动初始化,否则会报错,归根结底是由于static变量和局部变量存储的位置不一样。)。
Java内存空间个人理解
堆:堆主要存放Java在运行过程中new出来的对象,凡是通过new生成的对象都存放在堆中,对于堆中的对象生命周期的管理由Java虚拟机的垃圾回收机制GC进行回收和统一管理。类的非静态成员变量也放在堆区,其中基本数据类型是直接保存值,而复杂类型是保存指向对象的引用,非静态成员变量在类的实例化时开辟空间并且初始化。所以你要知道类的几个时机,加载-连接-初始化-实例化。
栈:栈主要存放在运行期间用到的一些局部变量(基本数据类型的变量)或者是指向其他对象的一些引用,因为方法执行时,被分配的内存就在栈中,所以当然存储的局部变量就在栈中咯。当一段代码或者一个方法调用完毕后,栈中为这段代码所提供的基本数据类型或者对象的引用立即被释放;
常量池:常量池是方法区的一部分内存。常量池在编译期间就将一部分数据存放于该区域,包含基本数据类型如int、long等以final声明的常量值,和String字符串、特别注意的是对于方法运行期位于栈中的局部变量String常量的值可以通过 String.intern()方法将该值置入到常量池中。
静态域:位于方法区的一块内存。存放类中以static声明的静态成员变量
方法区:是各个线程共享的内存区域,它用于存储class二进制文件,包含了虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。它有个名字叫做Non-Heap(非堆),目的是与Java堆区分开。
需要特别注意的是:
方法区是线程安全的。由于所有的线程都共享方法区,所以,方法区里的数据访问必须被设计成线程安全的。例如,假如同时有两个线程都企图访问方法区中的同一个类,而这个类还没有被装入JVM,那么只允许一个线程去装载它,而其它线程必须等待 !
最后总结起来就是:
栈:为即时调用的方法开辟空间,存储局部变量值(基本数据类型),局部变量引用。注意:局部变量必须手动初始化。
堆:存放引用类型的对象,即new出来的对象、数组值、类的非静态成员变量值(基本数据类型)、非静态成员变量引用。其中非静态成员变量在实例化时开辟空间初始化值。更具体点,个人感觉非静态成员变量是放在堆的对象中。
方法区:存放class二进制文件。包含类信息、静态变量,常量池(String字符串和final修饰的常量值等),类的版本号等基本信息。因为是共享的区域,所以如果静态成员变量的值或者常量值(String类型的值能够非修改,具体请查看博客)被修改了直接就会反应到其它类的对象中。
成员变量与局部变量总结:
一:在方法中声明的变,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈,其对应在该方法中声明的变量随着栈的销毁而结束,这就局部变量只能在方法中有效的原因<1>在方法中生明的变量可以是基本类型的变量,也可以是引用类型的变量,(1)当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在方法栈中(2)当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在方法的栈中,该变量所指向的对象是放在堆类存中的》》》二:在类中声明的变量是成员变量,也叫全局变量,放在堆中的,<1>同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量(1)当声明的是基本类型的变量其变量名及其只时放在堆类存中的,(2)引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象
下面给大家看一个Java代码例子:
声明一个类:
public class A {
public final String tempString="world";//这里可以把final去掉,结果等同!!
public final char[] charArray="Hello".toCharArray();
public char[] getCharArray() {
return charArray;
}
public String getTempString() {
return tempString;
}
}
创建测试类:
public class TestA {
public static void main(String[] args) {
A a1=new A();
A a2=new A();
System.out.println(a1.charArray==a2.charArray);
System.out.println(a1.tempString==a2.tempString);
}
}
输出结果:
false
true
要想明白上面字符串对比为什么输出为true你必须知道:
该图片截自《深入理解Java虚拟机》
一个Class字节码文件的Class字节码文件对象只有一个常量池,常量池被所有线程共享。
在常量池中,字符串被存储为一个字符序列,每个字符序列都对应一个String对象,该对象保存在堆中。所以也就是说为什么String
temp=“xxx”;能够当成一个对象使用!!
如果多个线程去访问A类中的String字符串,每次都会到常量区中去找该字符序列的引用。
所以访问A类被创建的两个A类型对象的String字符串对比会输出true。
如果对字符串输出为true还是不懂,可以参考这篇博客:字符串被存储的原理过程
那么为什么final类型的字符数组就不为true了呢??
申明(不管是通过new还是通过直接写一个数组)一个数组其实在Java中就等同创建了一个对象,即每次创建类的对象都会自动创建一个新的数组空间。
其中要注意的是:常量池中存储字符数组只是存储的是每个字符或者字符串。
为了证明每次获取的final数组地址不一样,并且数组中的字符都会存储在常量池中,我们需要参考另外一个代码:
public class A {
public String tempString="world";
public final String tempStringArray[]={"Fire","Lang"};
public final char[] charArray={'h','e','l','l','o'};
public Character charx='l';
public char[] getCharArray() {
return charArray;
}
public String getTempString() {
return tempString;
}
public String[] getTempStringArray() {
return tempStringArray;
}
public Character getCharx() {
return charx;
}
}
测试代码:
public class TestA {
public static void main(String[] args) {
A a1=new A();
A a2=new A();
System.out.println(a1.tempString==a2.tempString);
System.out.println(a1.tempStringArray==a2.tempStringArray);//看这里
System.out.println("#####################");//看这里
System.out.println(a1.tempStringArray[0]==a2.tempStringArray[0]);
System.out.println(a1.tempStringArray[0]=="Fire");
System.out.println("#####################");
System.out.println(a1.charArray==a2.charArray);
System.out.println(a1.charx==a2.charx);
}
}
输出:
true
false
#####################
true
true
#####################
false
true
可以看到每次输出的final数组地址都不一样,最重要的是String类型的数组地址也都不一样!!但是String类型数组中的每个字符串都存储在常量池中。
所以可以肯定的是字符串和其它能够确定值的final字面量值是存储在常量池的!!并且在方法区内存中只有一份!!与所有线程共享访问!!
常量池存储的项目类型: