一.概念理解
1.1堆与栈
栈是运行时单位,解决程序的运行问题,即程序如何执行,或说如何处理数据
堆是数据的存储单位,堆是jvm中管理内存中最大一块。它是被共享,存放对象实例。也称为“gc堆”。
垃圾回收的主要管理区域
在java中一个线程就会有一个线程栈与之对应,不同的线程执行逻辑不同,因此需要一个独立的线程栈。而堆是所有线程共性的。栈因为是运行单位,因此里面存储的信息都是跟当前线程相关信息的。包括局部变量,程序运行状态,方法返回值等等;而堆只负责存储信息。
1.2为什么需要堆和栈
第一.从设计角度,栈代表了处理逻辑,而堆代表了数据。这样分开,使得逻辑更为清晰。分而治之的思想。这种隔离。模块化的思想在设计的方方面面都有体现。
第二.堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益很多,一方面这种共享提供了一种有效的数据交互方式,另一方面,堆中的共享变量和缓存可以被所有栈访问,节省空间
第三.栈因为运行时需要,比如保存形系统运行的上下文,需要进行地段的划分。由于栈只能向上增长,因此就会限制栈储存内容的能力。而堆不同,堆中的对象可以根据需要动态增长的。因此栈和堆拆分,使得动态增长成为可能,相应栈中只需要记录堆中的一个地址即可。
第四.面向对象就是堆与栈的完美结合。我们把对象开分开,对象的属性其实就是数据,存放在堆中;而对象的行为(运行逻辑),放在栈中。我们在编写对象的时候,其实是编写了数据结构,也编写的数据处理的逻辑。在java中,Main函数是栈的起始点,也是程序的起始点
1.3存储数据
堆中存的是对象
栈中存储的是基本数据类型和堆中对象的引用
一个对象的大小是不可估计的,或者说是动态变化的,但是栈中,一个对象只对应了一个4byte的引用
为什么不把基本类型放在堆中呢?
**因为其占用的空间一般是1~8个字节,需要空间比较少,因为是基本类型,不会出现动态增长的的情况(长度固定),因此在栈中存储就足够了。**如果把它存在在堆中是没有意义的(还会浪费空间)。可以这么说,基本类型和对象的引用都是存在在栈中,但是基本类型,对象引用和对象本身就有区别了,因为一个是栈中的数据,一个是堆中的数据,会产生java中参数传递的问题
三.参数传递
1.java中只有值传递(基本数据类型的值,引用数据类型的值)
2.java中没有指针的概念
3.程序运行永远是在栈中进行的,因此参数传递时,只存在传递基本类型和对象引用的问题。不会传对象本身。堆和栈中,栈是程序运行最根本的东西。程序运行可以没有堆,但是不能没有栈。而堆是为栈进行数据存储服务,说白了堆就是一块共享的内存。正因为堆和栈分离的思想,才使得java的垃圾回收称为可能。
java中,栈的大小通过-Xss来设置,当栈中存储数据比较多时,需要适当调大这个值,否则会出现java.lang.StackOverflowError异常。常见的出现这个异常是无法返回的递归,因为栈中保存的信息都是方法返回的记录点。
四.对象的大小
在stack里面,对象的大小:4byte的引用
基本数据的类型的大小是固定的。对于非基本数据类型的java对象的大小,就值得商榷。
在Java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任何属性的对象的大小,看下面语句:
Object ob = new Object();
这样在程序中完成了一个java对象的生命,但是它所占的空间为:4byte+8byte是上面部分所说的java栈中保存引用的所需的空间。那8byte则是java堆中对象的信息。因为所有的java非基本类型都需要默认集成Object类,因此不论什么样的java对象,其大小必须大于8byte。
五.引用类型(不会回收强引用,其他引用会优先回收)
对象的引用分为强引用,弱引用,软引用,虚引用。
强引用:一般生成对象时java虚拟机生成的引用,强引用条件下,垃圾回收需要严格判断当前对象引用是否是强引用,如果是,则不会被垃圾回收。
软引用:一般作为缓存来使用,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。
弱引用:弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾回收时,是一定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。
六.回收算法(堆)
6.1 标记-清除(Mark-Sweep):
1.回收算法的意思是怎么移除在堆内存中不被使用的对象
2.遍历堆里面的所有对象,标记要回收的对象(不可达的对象)
3.珊瑚堆里面标记的对象
此算法执行分两阶段:
第一阶段从引用根节点开始标记所有被引用的对象
第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
6.2.复制(Copying):
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
6.3标记-整理(Mark-Compact):
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
七.按分区对待
7.1增量收集(Incremental Collecting)
实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。
把堆都扫描一般
7.2分代收集(Incremental Collecting)
解决了回收的数量和范围问题
将堆里面的内存都遍历一般,可以达到目的,但是性能很差
有的对象:存活时间较长(IOC)
有的对象:存活时间短(局部变量)
基于对对象生命周期分析后得出的垃圾回收算法**。把堆分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的**
8.按系统线程分
8.1串行收集:
串行收集使用单线程处理所有垃圾回收工作, 因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。
总结:单核 ,或者多核,数据量少
8.2并行收集
并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。
总结:在多核上的发挥较大。多核堆的数据 大
大型的应用
不管你是并行还是串行,都需要停止整个应用。15ms
8.3并发收集
也需要停止应用,但是能减少时间!10
相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。
在运行时,也能回收垃圾。
九.何为垃圾?
9.1引用计数
上面说到的“引用计数”法,通过统计控制生成对象和删除对象时的引用数来判断。垃圾回收程序收集计数为0的对象即可。但是这种方法无法解决循环引用
**** 碎片问题
由于不同Java对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间,以及程序运行效率降低。所以,在上面提到的基本垃圾回收算法中,“复制”方式和“标记-整理”方式,都可以解决碎片的问题
十.对象引用树遍历
从程序运行的根节点出发,遍历整个对象引用,查找存活的对象。那么在这种方式的实现中,垃圾回收从哪儿开始的呢?即,从哪儿开始查找哪些对象是正在被当前系统使用的。上面分析的堆和栈的区别,其中栈是真正进行程序执行地方,所以要获取哪些对象正在被使用,则需要从Java栈开始。同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。
同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。这样,以栈或寄存器中的引用为起点,我们可以找到堆中的对象,又从这些对象找到对堆中其他对象的引用,这种引用逐步扩展,最终以null引用或者基本类型结束,这样就形成了一颗以Java栈中引用所对应的对象为根节点的一颗对象树,如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收。而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。
因此,垃圾回收的起点是一些根对象(java栈, 静态变量, 寄存器…)。而最简单的Java栈就是Java程序执行的main函数。这种回收方式,也是上面提到的“标记-清除”的回收方式
堆变大->回收的时间变长->应用的停止时间长(采用并发收集)
但是这种方式有一个很明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增大,对应应用暂停的时间也会相应的增大。一些对相应时间要求很高的应用,比如最大暂停时间要求是几百毫秒,那么当堆空间大于几个G时,就很有可能超过这个限制,在这种情况下,垃圾回收将会成为系统运行的一个瓶颈。为解决这种矛盾,有了并发垃圾回收算法,使用这种算法,垃圾回收线程与程序运行线程同时运行。在这种方式下,解决了暂停的问题,但是因为需要在新生成对象的同时又要回收对象,算法复杂性会大大增加,系统的处理能力也会相应降低,同时,“碎片”问题将会比较难解决。
分代回收(重点)
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
试想,在不进行对象分代的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。
持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大;
年轻代和年老代的划分是对垃圾收集影响比较大的;
年青代:
生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期
短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生
成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满
时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个
Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。
需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来
对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对
象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的
(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
年老代:
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存
放的都是一些生命周期较长的对象。
持久带:
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态
生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这
些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
堆里面分代(重点)
1 年轻代
2 老年代
3 持久代 jdk8 里面没有持久化代了使用MeteSpance
年轻代:
Eedn:伊甸区
So:存活区
S1:存活区
老年代满:(Full GC)因为该时间较长
开始回收
7.1 Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
需要:速度快,效率高
7.2 Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
年老代(Tenured)被写满
持久代(Perm)被写满
System.gc()被显示调用
年轻代->老年代(OK)
当老年代满->持久化转化(错误的)
持久化转化: 项目启动就确定下了
就是类加载器加载到的东西 + 常量池 + jvm 内部的数据
永久:可能类特别多,但是永久代空间较少。可能发生满