JAVA垃圾回收简单了解一下

2018-09-21  本文已影响0人  极客123

Java虚拟机(Java Virtual Machine , JVM)

Java程序运行的平台,是用于计算机设备的一种规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。


JVM是为具体的操作系统和架构服务的,是 操作系统 - 和 已编译的程序或者应用程序 之间的中间层,在JVM的帮助下,你编写的应用程序和底层代码无关,Java程序通过javac被编译为字节码的形式,JVM将字节码解释为最适合当前操作系统的具体指令


接下来就开始层层剥茧吧,一步步来认识这个鬼


JAVA清道夫

此前先来了解下内存是如何分配的

java内存.jpg
运行时数据区划分如图
RuntimeDataSpace.jpg
如图 : 
        基本上Java堆是虚拟机管理的内存中最大的一块。
        堆是被所有线程共享的一块区域
        在虚拟机启动时创建,通过参数“-Xmx和-Xms”控制
        堆呢,也是主要的内存池,整个应用都可以访问
        程序里new(可以说new是开辟疆土的意思)关键字在java堆(heap)中分配内存
        垃圾回收呢,一种机制的存在,为了保证程序的正常运行
        JVM会尝试通过垃圾回收机制回收一些内存
        如果无法获得足够运用的内存
        JVM就会抛出OutOfMemoryError异常并退出

----在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象;而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。JVM的一个系统级线程会自动释放该内存块。垃圾回收意味着程序不再需要的对象是"无用信息",这些信息将被丢弃。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。事实上,除了释放没用的对象,垃圾回收也可以清除内存记录碎片。由于创建对象和垃圾回收器释放丢弃对象所占的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。碎片整理将所占用的堆内存移到堆的一端,JVM将整理出的内存分配给新的对象。
----垃圾回收能自动释放内存空间,减轻编程的负担。这使Java 虚拟机具有一些优点。首先,它能使编程效率提高。在没有垃圾回收机制的时候,可能要花许多时间来解决一个难懂的存储器问题。在用Java语言编程的时候,靠垃圾回收机制可大大缩短时间。其次是它保护程序的完整性, 垃圾回收是Java语言安全性策略的一个重要部份。
----垃圾回收的一个潜在的缺点是它的开销影响程序性能。Java虚拟机必须追踪运行程序中有用的对象,而且最终释放没用的对象。这一个过程需要花费处理器的时间。其次垃圾回收算法的不完备性,早先采用的某些垃圾回收算法就不能保证100%收集到所有的废弃内存。当然随着垃圾回收算法的不断改进以及软硬件运行效率的不断提升,这些问题都可以迎刃而解。

Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。


关于GC的算法

标记清除算法(标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象)
复制算法(“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低)
存活的对象多的时候,使用复制算法就会执行较多的复制操作,效率会降低
标记压缩算法(标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存)
分代算法(GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。分代算法对此,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。)


堆内存分布图

堆内存1.png
年轻代(Young Generation)Eden
1.所有新生成的对象首先都是放在年轻代的
  年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。
一个Eden区,两个 Survivor区(一般而言)。
大部分对象在Eden区中生成。
回收时先将eden区存活对象复制到一个survivor0区(复制算法)
然后清空eden区,当这个survivor0区也存放满了时
则将eden区和survivor0区存活对象复制到另一个survivor1区
然后清空eden和这个survivor0区
此时survivor0区是空的
然后将survivor0区和survivor1区交换
即保持survivor1区为空, 如此往复。
当eden区和survivor中某个使用的区中存活的对象大于用来存放回收时需要保存的存活对象的时候就会触发分配担保机制,见文章尾部补充。

3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。
若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收

4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)
年老代(Old Generation)
1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。
  因此,可以认为年老代中存放的都是一些生命周期较长的对象。

2.内存比新生代也大很多(大概比例是1:2)
  当老年代内存满时触发Major GC即Full GC
  Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
持久代(Permanent Generation)
用于存放静态文件,如Java类、方法等。
持久代对垃圾回收没有显著影响
但是有些应用可能动态生成或者调用一些class
例如Hibernate 等
在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

jdk8移除了PermGen,取而代之的是MetaSpace
元空间(Metaspace);这与Oracle JRockit 和IBM JVM’s很相似。

更新原因
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

具体新的结构如图

584866-20170426154633834-741444326.jpg 584866-20170426175411428-34722603.png

参考链接地址
https://www.cnblogs.com/dennyzhangdd/p/6770188.html


什么是垃圾回收?通过上面的散碎的知识点,总结一波:


垃圾回收,重新回收之前分配过的内存机制,回收回来的内存可以重新分配给新的数据使用,在java中,通过new构建一个对象的时候,就会和JVM索要内存,开辟新的疆土,JVM会为对象补给足够多的内存。
当这个对象不需要的时候,JVM就会收回来。
尽管有一些不同的实现垃圾回收的算法,但是这些算法都有唯一的目标(四字真言):-标-记-回-收-
java传动的使用GC算法是标记-清理算法,没有过多引用的对象会被设置为可回收,在这个过程中,为了能够成功的回收内存,JVM中的所有线程都会被暂停,这种方式成为“停止一切的回收 - stop-the-word- ”
当然,垃圾回收器试图让这种回收的数目降到最低
垃圾回收过程中还会执行一些其他操作,例如对象在不同的代之间的晋升,以及将一些经常访问的对象通过内存移动的方式组织在一起,从而维持尽可能多的可用空间。这种组织在一起的操作叫做压缩,内存压缩发生在jvm处于stop-the-word期间, 因为活动对象,有可能被移动到不同的物理内存位置。


补充: 内存担保机制

在JVM的内存分配时,当在新生代无法分配内存的时候,就会把新生代的对象转移到老生代,然后把新对象放入腾空的新生代。

内存分配是在JVM在内存分配的时候,新生代内存不足时,把新生代的存活的对象搬到老生代,然后新生代腾出来的空间用于为分配给最新的对象。这里老生代是担保人。在不同的GC机制下,也就是不同垃圾回收器组合下,担保机制也略有不同。在Serial+Serial Old的情况下,发现放不下就直接启动担保机制;在Parallel Scavenge+Serial Old的情况下,却是先要去判断一下要分配的内存是不是>=Eden区大小的一半,如果是那么直接把该对象放入老生代,否则才会启动担保机制。

在发生minor gc之前,虚拟机会检测 : 老年代最大可用的连续空间>新生代all对象总空间?

          1、满足,minor gc是安全的,可以进行minor gc。

          2、不满足,虚拟机查看HandlePromotionFailure参数(一个阈值):

                   (1)为true,允许担保失败,会继续检测老年代最大可用的
                            连续空间>历次晋升到老年代对象的平均大小。若大于,
                            将尝试进行一次minor gc,若失败,则重新进行一次full
                            gc。

                    (2)为false,则不允许冒险,要进行full gc(对老年代进行
                            gc)。

在JDK 6 Update 24之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察OpenJDK中的源码变化(见代码清单),虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

代码清单 HotSpot中空间分配检查的代码片段

bool TenuredGeneration::promotion_attempt_is_safe(size_t
max_promotion_in_bytes) const {
   // 老年代最大可用的连续空间
   size_t available = max_contiguous_available();  
   // 每次晋升到老年代的平均大小
   size_t av_promo  = (size_t)gc_stats()->avg_promoted()->padded_average();
   // 老年代可用空间是否大于平均晋升大小,或者老年代可用空间是否大于当此GC时新生代所有对象容量
   bool   res = (available >= av_promo) || (available >=
max_promotion_in_bytes);
  return res;
}
```了,
上一篇下一篇

猜你喜欢

热点阅读