JAVA----JVM垃圾回收GC

转载:http://www.cnblogs.com/redcreen/tag/jvm/

https://cloud.tencent.com/developer/article/1336613

java内存组成介绍:堆(Heap)和非堆(Non-heap)内存

       按照官方的说法:“

Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”

“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。

可以看出JVM主要管理两种类型的内存:堆和非堆

简单来说

  1. 堆就是Java代码可及的内存,是留给开发人员使用的
  2. 非堆就是JVM留给 自己用的

组成图

  • 方法栈&本地方法栈:
    线程创建时产生,方法执行时生成栈帧
  • 方法区
    存储类的元数据信息 常量等

  • java代码中所有的new操作
  • native Memory(C heap)
    Direct Bytebuffer JNI Compile GC;

 

堆内存分配

JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指 定,默认是物理内存的1/4。

默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;

空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。

因此服务器一般设置-Xms、-Xmx相等以避免在每次GC 后调整堆的大小。

对象的堆内存由称为垃圾回收器的自动内存管理系统回收。

 

组成详解
Young Generation即图中的Eden + From Space + To Space

Eden

存放新生的对象

Survivor Space

有两个,存放每次垃圾回收后存活的对象
Old GenerationTenured Generation 即图中的Old Space 
主要存放应用程序中生命周期长的存活对象

     非堆内存分配

 
      JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;

                 由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。

组成详解
Permanent Generation保存虚拟机自己的静态(refective)数据
主要存放加载的Class类级别静态对象如class本身,method,field等等
permanent generation空间不足会引发full GC(详见HotSpot VM GC种类)
Code Cache用于编译和保存本地代码(native code)的内存
JVM内部处理或优化

      JVM内存限制(最大值)

      JVM内存的最大值跟操作系统有很大的关系。

简单的说就32位处理器虽然 可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统 下为2G-3G),

而64bit以上的处理器就不会有限制了。

 

所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法 的代码都在非堆内存中。

 

本地方法接口 ===》 系统级

本地方法栈 ===》 系统级

 

 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区域,这些区域都有各自的用途以及创建和销毁的时间。根据《Java虚拟机规范(第2版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域

 

 Java堆所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例

Java堆是垃圾收集器管理(GC: Garbage Collector)的主要区域。

由于现在收集器基本采用分代回收算法,按对象访问的频繁不频繁,把它分成几个区域。就是堆内存又被划分为:

 

 JVM堆配置参数

 

 

 新生代用来存放JVM刚分配的Java对象

 

 


 

 

对象存活判断    判断对象是否存活一般有两种方式: 

(1) 引用计数  (已废弃)--判断对象的引用次数

每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。

此方法简单,无法解决对象相互循环引用的问题。

(2) 可达性分析(Reachability Analysis)-->通过判断对象引用链条是否可达来决定对象是否被回收--->GC Root搜索算法

从GC Roots开始向下搜索,搜索所走过的路径称为引用链。

当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,是不可达对象。

在Java语言中,GC Roots包括:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性实体引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI引用的对象

 

3. 垃圾收集算法

(1) 标记 -清除算法===>Mark-Sweep===>算法分为"标记"和"清除"两个阶段:

首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的

它的主要缺点有两个:

  1. 一个是效率问题,标记和清除过程的效率都不高
  2. 另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,如果空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作


 

(2) 复制算法---->Coping==>如:新生代中Survivor区<===>幸存区

"复制"(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,

按顺序分配内存即可,实现简单,运行高效。

只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低


 

(3) 标记-压缩算法--使用于存活率比较高的场景-->老年代-->解决的内存碎片的问题

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法

根据老年代的特点,有人提出了另外一种"标记-压缩"(Mark-Compact)算法,标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存


 

(4) 分代收集算法--组合拳

  1. 新生代:复制算法
  2. 老生代:"标记-清理"或"标记-压缩"算法

GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

"分代收集"(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用"标记-清理"或"标记-压缩"算法来进行回收

Java堆分  新生代  和 老生代  和 持久代(方法区)

  1. 新生代:复制算法
  2. 老生代:"标记-清理"或"标记-压缩"算法

垃圾收集器分类---停顿时间VS吞吐量

  1. 串行回收【Serial Collector】----GC单线程内存回收 会暂停所有用户的线程-->Serial是串行的

  2. 并行回收【Paraller Collector】----收集是指多个GC线程并行工作,但此时用户线程是暂停的-->Paralle收集器是并行的,
  3. 并发回收【Concurrent Collector】----如:用户线程和GC线程同时执行【并不一定是并行,有可能是交替执行】适合用户响应时间有要求的场景Web等-->CMS收集器是并发的

并发回收是指用户线程与GC线程同时执行(不一定是并行,可能交替执行,但总体上是在同时执行的),不需要暂停用户线程(其实在CMS中用户线程还是需要停顿的,只是停顿时间非常短,GC线程在另一个CPU上执行)。


 

4. 垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现

 

 

(1) Serial收集器

串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿只使用一个线程去回收,新生代、老年代串行回收,新生代使用复制算法,老年代使用标记-压缩算法垃圾收集的过程中会Stop The World(服务暂停)

参数控制:-XX:+UseSerialGC 指定收集器为Serial收集器


 

(2) ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本新生代使用"复制"算法并行收集,老年代使用"标记-压缩"算法串行收集

参数控制: -XX:+UseParNewGC 指定收集器为ParNew收集器 -XX:ParallelGCThreads 限制线程数量


 

(3) Parallel Scavenge 收集器

Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;新生代使用"复制"算法,老年代使用"标记-压缩"

参数控制:-XX:+UseParallelGC 使用Parallel收集器+老年代串行


 

(4) Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和"标记压缩"算法。这个收集器是在JDK 1.6中才开始提供

参数控制: -XX:+UseParallelOldGC 使用Parallel收集器+老年代并行


(5) CMS收集器---互联网广泛使用

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

 

从名字(包含"Mark Sweep")上就可以看出CMS收集器是基于"标记-清除"算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些, 整个过程分为4个步骤,包括:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤仍然需要"Stop The World"。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)

优点:并发收集、低停顿 缺点:产生大量空间碎片、并发阶段会降低吞吐量

参数控制:

-XX:+UseConcMarkSweepGC 使用CMS收集器 -XX:+UseCMSCompactAtFullCollection "Full GC"后,进行一次碎片整理,整理过程是独占的,会引起停顿时间变长 -XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理 -XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)


 

(6) G1收集器

G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:

  1. 空间整合,G1收集器采用"标记-压缩"算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
  2. 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

收集步骤:

  1. 标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC,对应GC log:GC pause (young) (inital-mark)

  1. Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。
  2. Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

  1. Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。
  2. Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。

  1. 复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。

 

 


 

5. 常用的收集器组合


 

 

 

 

 

 

 

 

 

 

 

 


 

  1. 永久区(这里的内容一般就一直存在,如类对象、静态变量。垃圾回收不会去回收它们。)
  2. 老生代,缓存,引用老不被释放
  3. 新生代,新对象

能不能回收看它有没有被引用,有引用就不能回收,没引用就可以回收。

最开始创建的对象都在新生代区域。如果GC回收时,发现它有引用,就将它放到老生代。这样老生代回收的频率比新生代就低些。新生代发生的垃圾回收就成为miniGC。如果经过几次垃圾回收后,内存占用率还是很高,GC就考虑对老生代也进行垃圾回收。如果老生代发生了垃圾回收就叫fullGC。一旦发生fullGC,就会影响程序执行,程序假死,甚至jvm都暂停,只做垃圾回收的事情。

新生代又划分为三个区:伊甸园、幸存区1、幸存区2

伊甸园,最早的对象new完就在这里,它刚生下来。一旦GC回收伊甸园的对象幸存下来,就将其放到“幸存区”,幸存区分为两个,幸存区1和幸存区2。同一时刻必然有一个幸存区是空的。

如:new对象被放在伊甸园,当伊甸园满了,就会触发miniGC,假设对象没有被回收掉,有人引用它,它就会被放在幸存区中去。假设第二个新的对象放入伊甸园,当伊甸园也满了,miniGC开始回收。它就会将第二个对象放入幸存区2,同时将幸存区1中的所有对象也都放到幸存区2中。这样幸存区1和伊甸园就都空下来了。假设第三个对象又来了,伊甸园又满了,它就会将第三个对象和幸存区2中的对象都放入到幸存区1中。这是就把伊甸园和幸存区2空出来了。

交替来空闲。这样做主要为了效率,只做移动,无需比较无需判断。反正总有一个幸存区是空的,为了在不同的内存区移动对象效率更高些。把对象都放在一块内存,读取是不就快些。只扫描一部分内存区域,不用跳着去扫。

那什么时候对象会跑到老生代去呢?每次对象被GC都有个计数,比如第一次活下来了,第二次活下来了,GC收集了很多次,达到一定的寿命,比如15次。那就把这个对象移到老生代。也就是基于习惯,如果对象老被引用,那肯定不应该被回收掉。那把它扔到哪里呢?扔到老生代中。其实很好理解,老生代应该放的就是经常驻留在内存中的对象。比如说缓存,缓存中的数据,在java中就表示成一个map。你说这个map能让GC轻易回收掉吗?所以缓存对象经过几次GC后就会放在老生代中。

或者说老生代就是寿命比较长的数据,新生代就是那些寿命比较短的对象。寿命最短的对象都在伊甸园中。

 

 

 

 

 

 

 

 

JAVA 内存模型 

(1)、共享性、互斥性、原子性、可见性、有序性。
(2)、JMM内存模型——描述线程本地内存和主内存之间的抽象关系。线程A和线程B之间通讯,需要通过主内存。
JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

注意,线程本地内存只是一个抽象概念,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值