Java代码需要运行在虚拟机(JVM)上,而JVM为了方便管理内存,会在Java程序运行过程中,把自己所管理的内存划分为若干个不同的数据区域,用作不同的用途,先看一下大致划分(JDK1.6)
堆
存放内容:
·大多数创建的对象(随着技术的发展,对象必须在堆上分配不是那么绝对了)
·数组值
GC情况:
GC工作的主要区域,回收不再被使用的对象
内存溢出:
当需要为对象分配内存时,堆的内存占用已达到了设置的最大值,则抛出OutOfMenoryError
参数设置:
-Xms最小堆内存,默认为物理内存的1/64,但小于1G
-Xmx最大堆内存,默认为物理内存的1/4,但小于1G
注意:
实际使用时,设置最大堆内存=最小堆内存,这样堆内存就不会频繁调整了
由于现代GC基本都采用分代收集算法,所以堆还可以细分为:新生代和老年代,再细致一点就是Eden空间、From Survivor空间、To Survivor空间等,关于这些请查看 JVM(三)垃圾回收
方法区
存放内容:
·已加载的类的信息(名称、修饰符等)
·静态变量(static)、常量(final)
·类中的方法信息
·运行时常量池(编译器生成的各自字面量和符号引用存储在class文件的常量池中,即所说的静态常量池,这些内容会在类加载之后进入运行时常量池)JDK1.6常量池在方法区,1.7在堆
GC情况:
·回收未被引用的类
·堆常量池的回收
·JVM规范不强制要求方法区必须实现垃圾回收
内存溢出:
当类加载器加载class文件到内存中时,提取的类信息要存放在方法区,而方法区的内存占用已达到了设置的最大值,则抛出OutOfMenoryError
参数设置:
-XX:permSize方法区最小值,默认为16M
-XX:MaxPermSize方法区最大值,默认为64M
虚拟机栈
存放内容:
·局部变量表:八大基本数据类型(并不会初始化值)、对象引用。局部变量表空间在编译期已经分配好,运行期不变
ps:关于局部变量表的存放内容我在学习时了解不是很透彻,与此同时还提出一个问题https://ask.csdn.net/questions/747770,对此不甚理解的小伙伴也可以关注一下,顺便有知道的小伙伴麻烦来释疑解惑~为此我还专门把字体改成了基佬紫
·操作数栈
运行状况:
每创建一个线程就会对应创建一个虚拟机栈,栈中有多个栈帧,每调用一个方法就往栈中创建一个栈帧,栈帧是用来存放方法数据和部分过程结果的数据结构,每一个方法从调用到最终返回结果的过程,就对应一个栈帧从入栈到出栈的过程
线程运行过程中,只有一个栈帧处于活跃状态,称为“当前活动栈帧”(对应方法为当前方法,类为当前类),当前活动栈帧始终是虚拟机栈的栈顶元素
GC情况:
栈的生命周期和线程同步,随着线程销毁或结束,它们占用的内存会自动释放,不需要GC
内存溢出/泄露:
1、Java程序启动一个新的线程,而没有足够的空间为该线程分配一个栈时,会抛出OutOfMenoryError
2、当线程调用一个方法时,JVM压入一个新的栈帧到这个线程对应的栈中,只要这个方法还没返回,这个栈帧就存在,如果方法嵌套的调用层数太多,如递归,随着栈帧的增多,最终导致这个线程栈中的栈帧总大小超出-Xss所设置大小,抛出StackOverFlowError
大小设置:
-Xss设置每个线程的堆栈大小,通常1M即可
本地方法栈
和虚拟机栈作用相似,不过虚拟机栈为Java方法提供服务,本地方法栈为Native方法提供服务
异常情况也和虚拟机栈一样
程序计数器(PC寄存器)
存放内容:
保存当前线程执行的虚拟机字节码指令的内存地址(线程执行native方法时为空)
GC情况:
唯一一个没有规定任何OutOfMenory情况的区域
内存结构的定义是由JVM所决定的,所以JVM产商不同,内存结构会稍有不同,不过大体上都受《Java虚拟机规范》的约束。此处我们提几个比较重要的点:
- 规范中定义的方法区,只是一种概念上的区域,只说明了它应该具有什么样的功能,但并没有规定这个区域应该处于何处,所以不同的虚拟机可能实现不同;
- 不同版本JDK的方法区所处位置不同,上面的图划分了逻辑区域,并不是物理区域,比如某些版本的JDK中,方法区是在堆中实现的;
- 运行时常量池用于存放编译期生成的各种字面量(数字)和符号应用,但Java并不要求常量只有在编译期才能产生,比如在运行期,String.intern也会把新的常量放入池中;
- Java程序运行期间除了以上描述的内存外,还有一块内存区域可供使用,即直接内存。《Java虚拟机规范》并没有定义它,因为它并不由JVM进行管理,它是由本地方法库直接在堆外申请的内存区域;
- 堆和栈的数据划分也不是绝对的,比如HotSpot的JIT会针对对象分配做相应的优化。
实例解析:
//例1
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = new Integer(40);
System.out.println("i1=i2 ?" + (i1==i2));//true 因为Java创建了[-128,127]的缓存数据,会直接引用该值在常量池的地址
System.out.println("i1=i3 ?" + (i1==i3));//false new出来的对象在堆中,而i1在常量池
Integer i4 = i2 + 0;
Integer i5 = new Integer(0);
Integer i6 = i3 + i5;
Integer i7 = i2 + i5;
System.out.println("i1=i4 ?" + (i1==i4));//true
System.out.println("i1=i6 ?" + (i1==i6));//true 因为Java数学计算都先进行拆箱,这里的i6就相当于Integer i6 = 40;
System.out.println("i1=i7 ?" + (i1==i7));//true 同上
//例2
int j0 = 400;
Integer j1 = 400;
Integer j2 = 400;
Integer j3 = new Integer(400);
System.out.println("j0=j1 ?" + (j0==j1));//true 因为Integer会自动拆箱成int
System.out.println("j1=j2 ?" + (j1==j2));//false 因为超过了127,所以会自动装箱成new Integer
System.out.println("j1=j3 ?" + (j1==j3));//false j1和j3都是new Integer
Integer j4 = 0;
Integer j5 = j2 + j4;
Integer j6 = new Integer(0);
Integer j7 = j3 + j6;
Integer j8 = j2 + j6;
System.out.println("j1=j5 ?" + (j1==j5));//false
System.out.println("j1=j7 ?" + (j1==j7));//false
System.out.println("j1=j8 ?" + (j1==j8));//false
//例3
String a = "我只是一个字符串比较长的字符串";
String b = "我只是一个字符串比较长的字符串";
String c = new String("我只是一个字符串,比较长的字符串");
System.out.println("a=b ?" + (a==b)); //true 直接存入常量池
System.out.println("a=c ?" + (a==c)); //false 堆与常量池的比较
String d = "我只是一个字符串";
String e = "比较长的字符串";
String f = d + e;
String g = "我只是一个字符串" + "比较长的字符串";
System.out.println("a=f ?" + (a==f)); //false
System.out.println("a=g ?" + (a==g)); //true
//关于最后两条的解释,可以看下底部参考文章中的《触摸java常量池》中的解释