Java 中的堆也是 GC 收集垃圾的主要区域。
GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。
Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。
新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。
当一个对象被判定为 “死亡” 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳
( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。
但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。
Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-整理算法。
现实的生活中,老年代的人通常会比新生代的人 “早死”。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。
另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。
程序计数器
线程私有,控制着字节码所执行的行数.调用本地方法(native)时为空
当前线程所执行的字节码的行号指示器,字节码解释器就是在工作时通过改变计数器的行号来获获取要执行的字节码指令.调用本地方法(native)时计数器为空
判断对象是否已“死”
引用计数
:简单但是很慢:每个对象都有一个引用计数器,当有引用对象连接时,计数器+1,引用离开时,引用计数-1,垃圾回收器会在含有所有对象的表上遍历,当发现某个对象的引用计数为0时,释放空间,缺陷:对象之间存在循环引用,可能会出现“对象应该被回收,但引用计数却不为0的情况”,定位这些交互自引用的对象组所需的工作量很大,这种方法一般只是阐述原理,不运用
根搜索方法(GC Roots)
对象不可达,对象可以回收;
不可达的对象并不是非死不可,要经过两次标记,引用不可达第一次标记,然后经过筛选,是否调用 finalize() 方法,任何对象的 finalize() 方法只会被系统调用一次,finalize() 运行代价高昂,不确定性强,无法保证对象的调用顺序,finalize() 方法能做的工作,都可以使用try-finally 可以做的更好、更及时
引用:强引用、软引用、弱引用、需引用
在一些更快的模式中,依据的思想:对任何“活的对象”,一定能追溯到其存活在堆栈或静态存储区的引用,这个引用链条可能会穿过数个对象层次,由此从堆栈和静态存储区开始,遍历所有的引用,就能够找到所有“活”的对象,这样就解决了“交互自引用的数组”。
如何找到存活对象:
停止-复制
:先暂停程序运行,然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全是垃圾,这种所谓的“复制式回收器”效率会降低,在两个堆之间来回倒腾,需要维护多一倍的空间
标记-清扫
:从堆栈存和静态存储区出发,遍历所有的引用,进而找出所有的存活对象,每当找到一个存活对象,就会给对象设一个标记,这个过程不会回收任何对象,只有全部标记完成之后,才会开始清扫,没有被标记的对象,不会发生任何复制动作
块
:内存分配单位,垃圾回收的时候就可以往废弃的块里拷贝对象。每个块都有相应的代数,记录是否还存活,如果块在某处被引用,代数会增加,垃圾回收器会对上次回收动作之后新分配的块进行整理
自适应垃圾回收技术
:java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就会切换到标记-清扫
方式,同样,java虚拟机会跟踪标记-清扫
的效果,要是堆空间中出现很多碎片,就会切换回停止-复制
,这就是自适应
技术
垃圾收集算法
标记-清除算法(Mark-Sweep)
分标记和清除,首先标记所有需要回收的对象,在标记完后统一回收所有被标记的对象,最基础的收集算法;不足
:效率问题,标记和清除都效率不高,空间问题标记清除后会产生大量不连续的空间碎片,碎片太多会导致程序在运行过程中需要分配较大的对象,无法找到足够连续内存,而不得不提前触发一次垃圾回收操作
复制算法(Coping)
将可用内存按容量分为大小相等的两块,每次只是用其中一块,这一块内存用完了,就将存活的对象复制到另一块上面,然后再把使用过的内存空间清理掉,这样每次都是对整个半区进行回收,不用考虑内存碎片等复杂情况,实现简单,运行高效,但是可用内存只有一半,代价太大。一般用于回收新生代,因为新生代对象98%都会很快被回收,所以不用1:1划分,而是分为一块较大的Eden空间和2块较小的Survior空间,默认分配大小是8:1:1,每次使用Eden和一块Survior,当回收时,将存活对象复制到另外一块Survior上,这样只有10%被浪费。复制收集算法在对对象存活率较高时就要进行较多的复制操作,效率将会降低,在老年代一般不能直接选用这种算法
默认Eden:S0:S1=8:1:1,因此,新生代中可以使用的内存空间大小占用新生代的9/10,那么有人就会问,为什么不直接分成两个区,一个区占9/10,另一个区占1/10,这样做的原因大概有以下几种
1.S0与S1的区间明显较小,有效新生代空间为Eden+S0/S1,因此有效空间就大,增加了内存使用率
2.有利于对象代的计算,当一个对象在S0/S1中达到设置的XX:MaxTenuringThreshold值后,会将其分到老年代中,设想一下,如果没有S0/S1,直接分成两个区,该如何计算对象经过了多少次GC还没被释放,你可能会说,在对象里加一个计数器记录经过的GC次数,或者存在一张映射表记录对象和GC次数的关系,是的,可以,但是这样的话,会扫描整个新生代中的对象, 有了S0/S1我们就可以只扫描S0/S1区了.
标记-整理算法(Mark-Compact)
标记过程中仍然和标记-清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存,这样就不会出现大量的空间碎片,适用于老年代
分代收集算法(Generational Collection)
当前商业虚拟机的垃圾收集器都采用分代收集算法,只是根据存活周期不同将内存分为几块。一般是把java堆分成新生代和老年代,这样根据各个年代的特点采用最适当的收集算法,在新生代中,每次垃圾收集都发现有大量的对象死去,只有少量存活就用复制算法,只需要付出少量的代价就能完成垃圾回收;而老年代中因为对象存活率较高、没有额外空间进行分配担保,就必须使用标记-清理或者标记-整理算法来进行回收
常用的配置选项
-Xms | 初始堆大小。如:-Xms256m |
---|---|
-Xmx | 最大堆大小。如:-Xmx512m |
-Xmn | 新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90% |
-Xss | JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。 |
-XX:NewRatio | 新生代与老年代的比例,如-XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3 |
-XX:SurvivorRatio | 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10 |
-XX:PerSize | 永久代(方法区)的初始大小 |
-XX:MaxPerSize | 永久代(方法区)的最大值 |
-XX:PrintGCDetails | 打印GC信息 |
垃圾回收器:
1、Serial(新生代-串行-收集器)
- 策略:标记-复制-清除;
- 优点:简单高效,适合 Client 模式的桌面应用(Eclipse)
- 缺点:多核环境下,无法充分利用资源
2、ParNew(新生代-并行-收集器) - 策略:标记-复制-清除
- 优点:多线程,独占式,多核环境下提高 CPU 利用率
- 缺点:单核环境下比 Serial 效率低
3、Parallel Scanvenge(新生代-并行-收集器) - 策略:标记-复制-清除
- 优点:精准控制吞吐量、GC 时间。吞吐量=执行用户代码时间/(执行用户代码时间 + 内存回收时间) 关注停顿时间和吞吐量的代销,吞吐量优先收集器
- 配置参数(可通过参数精准调控)
4、Serial old(老年代-串行-收集器) - 策略:标记-清除-整理
- 优点:简单高效
- 缺点:在多核环境下,无法充分利用资源
5、Parall Old(老年代-并行-收集器) - 策略:标记-清除-整理
- 优点:在多核环境下,提高 CPU 利用率
- 缺点:单核环境下,比 Serial Old 更低
6、CMS(Concurrent Mark Sweep 老年代-并发-收集器) - 策略:标记-清除
- 优点:停顿时间短,获取最短回收停顿时间为目标
- 缺点:无法处理浮动垃圾(由于CMS并发清理阶段用户线程还在运行着,伴随着程序的运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法再本次收集中处理掉他们,只好等待下一次 GC 时再将其清理掉)、存在大量的空间碎片
- 适用场景:互联网 Web 应用的 Server 端,涉及用户交互、响应速度快
7、G1(新生代&老年代-并行&并发-服务端收集器) - 策略:G1 将内存划分为 Region,避免内存碎片,标记-整理
- 优点:Eden、Survivor、Tenured 不再固定,内存使用率过高;可控的 STW 时间,根据预期的停顿时间,只回收部分 Region;在基本补习生吞吐量的前提下完成低停顿的内存回收
- 适用场景:多核 CPU,JVM 占用内存比较大的情况
JVM内存运行时数据区
#####java是否存在内存泄露
内存泄露:指一个不再被程序使用的对象或变量一直被占据在内存中。Java 使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引用,只要它们和根进程不可达(有向图两个顶点不连通)的,那么GC也是可以回收
内存泄露的场景
1.程序员创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,这个对象无用但是无法被垃圾回收器回收
2.一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类被长期引用了,即使那个外部类实例不再被使用,由于内部类持久外部类的实例对象,这个外部类对象将不会被垃圾回收