堆、栈、方法区
简单描述:堆只存放具体对象,new出来的对象会存在于堆中,内存的管理与清理垃圾由JVM的GC来管理和清理。
栈:存放基础数据类型、变量名,变量名会指向其针对的堆中对象的地址值。
方法区:用于存放整个程序唯一的值,比如静态变量,class信息,方法区中有个常量池,用于存放类中常量
程序员不需要关心内存如何分配,以及如何释放内存,释放内存由JVM来操作。
JVM清理内存的步骤大体分为两个步骤:
1.将无用的对象标记出来
2.将被标记的对象删除
如何判断什么对象应该被回收
标记的方法有两种:
1.引用计数器
每个对象都会有一个对应的引用计数器,当对象被引用一次计数器将+1,如果没有引用计数器则归零,归零的对象则代表无用了,会被GC回收掉。
但是此方法会存在内存泄露,举个例子:
对象A和B被new出来,此时A和B各被引用一次,计数器各+1。
将A和B相互赋值,A和B的引用再+1,但是此时的+1时存在于堆空间中的引用
将A和B置空让他们=null
此时将变量表中的引用切断了,计数器为0,但是对空间的A和B其实计数器还是1,那么不符合回收条件,不予回收。
2。可达性分析法
大部分虚拟机使用的时可达性分析法。它有两个概念:GC roots 、可达性
GC roots:被栈中变量表所引用的对象、被方法区中静态变量或常态变量所引用的对象、被本地方法栈jni所引用的对象
可达性:可达性对象就是可以和GC roots构成链接的对象
如果对象有可以到达的GC roots,则代表该对象满足可达性。如果对象无可达的GC roots ,则不满足可达性,会被清除。(简单来说就是,对象是否有被变量表所引用,如果没有引用则被清除,如果变量表中有引用到,则还有用,不会被清除)
如何回收清理
标记了哪些对象可以清理后,接下来要考虑如何高效清理垃圾。
常用的清理方法有:
1.标记-清理 算法
2.复制 算法
3.标记-整理 算法
4.分代 算法
1.标记-清理 算法
最容易实现的算法,也就是直接将标记过的对象进行清除。
缺点:容易产生大量内存碎片,如果有对象需要分配大量内存的时候,会因为找不到足够的内存空间,所以提前触发新的垃圾回收。
2.复制 算法
为了解决(标记-清理)所产生的内存碎片问题,复制算法出现了。
复制算法会将内存分为两个区域A、B,先将对象放在A区域,当A区域存放满了,将A中存活的对象复制到B中,复制过去使用的内存空间都聚集在一起,所以不会有内存碎片,然后清理A中剩余的对象,如果B满了,做同样的动作往A中复制。
缺点:浪费的空间太大,一次只能使用一半的空间,这种算法一般只存在理论,一般使用的是它的升级版算法。
3.标记-整理 算法
为了解决内存碎片和浪费内存空间的问题,标记-整理出现了。
实现方法很简单,将要清除的对象标记,然后将未被标记的对象移动到内存空间的一端,然后删除其余的对象。这样既可以解决内存碎片问题,也可以解决浪费内存资源的问题。
缺点:效率上来说没有复制算法来的快
如果移动了对象的内存位置,如果该对象被应用,还要调整引用地址
在移动的过程中,需要停止用户程序操作。
4.分代 算法
可以理解成分代算法融合了前面几种,分代算法分为:新生代、老年代。频繁增删的小对象在新生代中处理,长时间存活的大对象在老年代中处理。
新生代:
新生代分为两个区域,Eden区、survivor区,而survivor区又分为from区和to区(这里和复制算法的A、B区很相似),还有一个新概念是对象年龄计数器(每当对象活过一次清理,年龄+1)。
1.新产生的对象会进入Eden区,如果Eden区存满了会进行清理,存活下来的
对象会进入survivor区的from区,并且年龄+1,然后将标记的对象删除
2.Eden区继续处理对象,满了再触发清理,此时将eden区和from区存活的对象都
复制到to区,然后将标记的对象删除。
随后就是反复重复以上动作,survivor区的from和to区就是左右复制(和刚才说的复制算法一样)。
老年代:
什么样的对象可以进入老年代:
1.大对象,比如很长的字符串、数组,此等需要很长的连续性内存的对象可以直接
老年代存放。
2.survivor区中存活时间过长的对象可以晋升至老年代。比如年龄大于15岁可以进入
3.当survivor区放不下的时候,老年代可以当作新生代的备用库进行存放。
老年代如何清理对象:
由于老年代存放的都是长时间无需清理的数据,所以可以使用标记-整理算法,就算是消耗时间比较长,但是频率低,所以影响不大。