Java栈与堆
Java程序运行时,主要涉及到的内存区域有四部分(还有其他部分不做详解):栈内存、堆内存、方法区以及常量池。
1、栈内存:栈中主要保存的信息为:基本数据类型变量的值(short int long float double char boolean byte)、对象的引用(对象保存在堆中)。
栈有如下几个特点:
- 栈内存中所保存的变量有两个特点,生存期确定和占用内存大小确定。(生存期结束后会立马释放所占用的内存空间)
- 栈内存的存取速度非常快,仅次于寄存器。
- 栈内存中保存的变量是共享的。(int a=3,b=3 a==b返回true)
2、堆内存:堆中主要保存的信息为:通过new关键字生成的对象,以及数组。
堆有如下几个特点:
- 堆内存中保存的变量大小不固定,生存期也不固定。(由JVM GC garbage collector进行垃圾回收)
- 堆内存的存取速度不如栈内存。
3、常量池:常量池是指在编译器确定,并被保存在已编译的.class文件中的一些数据,除了包含在代码中的基本数据类型变量还包含对象的常量值(final)。
常量池的特点:
- 常量池中的变量不是保存在堆内存中,则是保存在方法区中。
- 常量池中的变量是共享的
下图以String字符串来讲解栈、堆、常量池三者之间的关系
String a = "xd";
String b = "xd";
String aa = new String("xd");
String bb = new String("xd");
System.out.println(a==b);//true
System.out.println(aa==bb);//false
对于变量a和b,在对代码进行编译期间,会将变量的值"xd"存入常量池中(常量池中仅保存一个变量),因此a和b的地址(对象的引用存入栈内存中)是一致的,都是指向常量池中的"xd",而aa和bb变量的地址分别指向new String("xd")产生的两个对象,因此aa和bb的地址不是一致的。他会先去常量池中查看是否存在"xd"这个常量,如果有则直接将它进行复制,然后存入堆内存中。
关于常量池的讲解,借鉴了下面这个博客:
https://www.cnblogs.com/SaraMoring/p/5687466.html
总之,常量池中存放的都是在编译期间就能确定的变量。如通过双引号String a = "xx";产生的字符串,final关键字修饰的变量,以及8中基本数据类型变量。(有一个技巧,就是通过反编译工具将.class文件进行反编译,会发现很多变量,直接显示的是值,而非变量本身,如final 关键字修饰的变量,这种变量就是存在常量池中的变量)
下图中主要讲解的是Java中8中基本类型和它们对应的封装类型(对象类型)(Integer、Short、Long、Float、Double、Char、Boolean、Byte),下面将主要讲解装箱和拆箱操作。
由如下代码:
int a = 3;
Integer b = 3;
Integer c = 3;
Integer d = new Integer(3);
Integer e = new Integer(3);
System.out.println(a == b);//true
System.out.println(a == d);//true
System.out.println(b == c);//true
System.out.println(b == d);//false
System.out.println(d == e);//false
1、int与Integer类型的数据进行比较的时候,Integer都会先将变量转化为int类型再进行比较。因此为true
2、同理1
3、Integer类型数据存在一个常量池(-128到127的数据保存在里面),而常量池中只会保存一个对象,因此,所有指向常量池中这个对象的地址都是相同的,故b=3。
4、由上面可知道,凡是new出来的对象,都是存放在堆内存当中,故该对象与常量池中的对象不是同一个对象,因此b==d为false。
5、凡是new出来的对象,都是一个新对象存放在堆内存中的,因此d==e为false。
JVM 垃圾回收机制(GC)
首先,要知道一点就是,对于栈中保存的变量,由于它的生存期跟大小都是确定的,因此,当该变量生存期到了之后,会自动释放掉所占用的内存空间。一般我们讲到的GC或者Java 垃圾回收机制都是针对于堆内存而言的,只有堆内存中的变量生存期和大小是不确定的,需要JVM 进行垃圾回收。
堆内存保存变量的区域按照变量的状态可以划分为三个区域:
- 新域:存储所有新生对象。(包含三个空间,Eden区、Survivor1区、Survicor2区),新域叫做Young Generation 简称YG,发生在新域的垃圾回收叫做Minor GC。
- 旧域:对新域中的对象进行一定次数的GC循环后仍然无法回收则转移至旧域。旧域叫做Older Generation 简称OG,发生在旧域的垃圾回收称为Major GC。
- 永久域:存储类和方法对象。这个区域是独立的,默认为4M,不包括在JVM堆内。
Minor GC:JVM最新产生的对象存放在Eden区,当Eden区被占满了。(对象太多了),此时Minor GC开始工作,这个时候应用程序将停止运行,进行垃圾回收(找不到的对象),将所有可以找到的对象,移至Survivor1区,当Survivor1区占满了后,这个时候Minor GC又开始工作,将Survivor1区内可以找到的对象移至Survivor2区,未找到的对象进行回收。同理,当Survivor2区占满了后,Minor GC又将Survivor2区找到的对象移至Survivor1区。像这样反复GC几次(次数由JVM决定)之后,如果仍然没有被回收掉的对象会被移至Older 区(旧域OG),进行Major GC。
Major GC:对于旧域,采用的是tracing算法的一种,称为标记-清除-压缩收 集器,注意,这有一个压缩,这是个开销挺大的操作。(不一定要OG满,OG满了后触发Full GC)
注:
- Minor GC和Major GC分别对于YG和OG的垃圾回收,互不干涉。
- GC区域包括:[Older Generation][Young Generation]。[Young Generation]=[Eden][Survivor1][Survivor2]
- 当Eden或Survivor1或Survivor2区域满了后,触发Minor GC,经过几次GC后无法清理掉的对象移动至OG。
- 当Older Generation 满了后,触发Full GC,Full GC很消耗内存,他会吧OG、YG中的大部分垃圾回收掉。这个时候用户线程会被block。
总结:
- JVM堆的大小决定了GC的运行时间,如果JVM堆的大小超过一定限度,那么GC的时间会很长。
- 对象生存的时间越长,GC需要的回收时间也越长,影响了回收速度。
- 大多数对象都是短命的,所以如果能够让这些对象的生存期在GC的一次运行周期内,最好。
- 应用程序中,建立与释放对象的速度决定了垃圾回收的频率。
- 如果GC一次运行的周期超过了3-5秒,这会很影响应用程序的运行,如果可以应该减小堆的大小。
- 前辈经验之谈,通常情况下 JVM堆的大小应该为物理内存的80%。