java_基础04内存结构和gc垃圾回收

gc基础

JVM架构图分析

下图:参考网络+书籍,如有侵权请见谅

内存分区

java中的内存是由java虚拟机自己去管理的,java的内存分配分为两个部分,一个是数据堆,一个是
堆内存用来存放由new创建的对象和数组,在堆中分配的内存由java虚拟机的自动垃圾回收器来管理;
栈用来存放类的信息的,它和堆不同,运行期内GC不会释放空间,当超过变量的作用域后,java会自动释放掉为该变量所分配的内存空间:
1、如果程序声明了static的变量,就直接在栈中运行的,进程销毁了,不一定会销毁static变量。
2、在函数中定义的基本类型变量和对象的引用变量都在函数的栈内存中分配

内存主要被分为三块:新生代(Youn Generation)、旧生代(Old Generation)、持久代(Permanent Generation)。三代的特点不同,造就了他们使用的GC算法不同,新生代适合生命周期较短,快速创建和销毁的对象,旧生代适合生命周期较长的对象,持久代在Sun Hotpot虚拟机中就是指方法区(有些JVM根本就没有持久代这一说法)。

新生代(Youn Generation):大致分为Eden区和Survivor区,Survivor区又分为大小相同的两部分:FromSpace和ToSpace。新建的对象都是从新生代分配内存,Eden区不足的时候,会把存活的对象转移到Survivor区。当新生代进行垃圾回收时会出发Minor GC(也称作Youn GC)。
旧生代(Old Generation):旧生代用于存放新生代多次回收依然存活的对象,如缓存对象。当旧生代满了的时候就需要对旧生代进行回收,旧生代的垃圾回收称作Major GC(也称作Full GC)。
持久代(Permanent Generation):在Sun 的JVM中就是方法区的意思,尽管大多数JVM没有这一代。

IBM研究表明,新生代 98% 的对象都是朝生夕死的,所以并不需要 1:1 来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间

JVM GC地点(收堆区和方法区)

需要注意的是,JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。

对象(回收那些对象)

判断哪些Java对象不再被使用,需要被回收。通常有两种算法:引用计数算法、可达性分析算法。

引用计数算法(已被淘汰)

引用计数算法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

存在的问题:主流的Java虚拟机里面都没有选用引用计数算法来管理内存,因为单纯的引用计数很难解决对象之间 相互循环引用 (A引用B,B引用A)的问题。

可达性分析算法(主流的算法)

当前主流的商用程序语言(Java、C#,上至古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。
算法的基本思路:通过一系列称为 “GC Roots” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为 “引用链”(Reference Chain),如果某个对象到 GCRoots 间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

在Java语言中,可作为 GC Roots 的对象包括下面几种:

1
2
3
4
(1)虚拟机(JVM)栈中引用对象
(2)方法区中的类静态属性引用对象
(3)方法区中常量引用的对象(final 的常量值)
(4)本地方法栈JNI的引用对象

GC算法

常见的GC算法:复制、标记-清除和标记-压缩

复制(新生代Eden区)

复制算法采用的方式为从根集合进行扫描,将存活的对象移动到一块空闲的区域,如图所示:

当存活的对象较少时,复制算法会比较高效(新生代的Eden区就是采用这种算法),其带来的成本是需要一块额外的空闲空间和对象的移动。

标记-清除

该算法采用的方式是从根集合开始扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,并进行清除。标记和清除的过程如下:

上图中蓝色部分是有被引用的对象,褐色部分是没有被引用的对象。在Marking阶段,需要进行全盘扫描,这个过程是比较耗时的

清除阶段清理的是没有被引用的对象,存活的对象被保留。

标记-清除动作不需要移动对象,且仅对不存活的对象进行清理,在空间中存活对象较多的时候,效率较高,但由于只是清除,没有重新整理,因此会造成内存碎片

标记-压缩(适用于旧生代)

该算法与标记-清除算法类似,都是先对存活的对象进行标记,但是在清除后会把活的对象向左端空闲空间移动,然后再更新其引用对象的指针,如下图所示

由于进行了移动规整动作,该算法避免了标记-清除的碎片问题,但由于需要进行移动,因此成本也增加了。(该算法适用于旧生代)

分代收集(多回收方式结合)

针对对象的存活周期将内存划分为几块,一般为 新生代 和 老年代,根据不同快对象的存活特点,选择合适的收集算法。新生代的算法存活率比较低,可以选择 Copying老年代的对象存活率高,并且没有担保空间,可以选择 Mark-Compact算法

JVM GC时间(何时gc)

最简单的分代式GC策略,按HotSpot VM的serial GC的实现来看,触发条件是:
young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高
full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。

GC算法优劣标准

评价一个垃圾收集GC算法的两个标准
吞吐量(throughput)越高算法越好
暂停时间(pause times)越短算法越好

多种垃圾收集器

JVM中Serial收集器、ParNew收集器、Parallel收集器解析
Serial收集器 单线程方式(没有线程切换开销,如果受限物理机器单线程可采用)串行且采用stop the world在工作的时候程序会停止Serial和serial old
ParNew收集器:多线程(多CPU和多Core的环境中高效),生产环境对低延时要求高的话,就采用ParNew和CMS组合来进行server端的垃圾回收
Parallel 收集器:多线程,并行, 它可以控制JVM吞吐量的大小,吞吐量优先的收集器,一般设置1%,可设置程序暂停的时间,会通过把新生代空间变小,来完成回收,频繁的小规模垃圾回收,会影响程序吞吐量大小
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网或者B/S系统的服务端上。CMS收集器是基于“标记-清除”算法实现,CMS收集器的内存回收过程是与用户线程一起并发执行的。优点:并发收集、低停顿。

不同垃圾收集器

其他

STW

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

堆与栈里存什么

1)堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是在栈中,一个对象只对应了一个4btye的引用。
2)为什么不把基本类型放堆中呢?因为其占用的空间一般是1~8个字节——需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况——长度固定,因此栈中存储就够了,如果把他存在堆中是没有什么意义的。可以这么说,基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了,因为一个是栈中的数据一个是堆中的数据。最常见的一个问题就是,Java中参数传递时的问题。
3)Java中的参数传递时传值呢?还是传引用?程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象本身。

堆内存与栈内存的区别

申请和回收方式不同:栈上的空间是自动分配自动回收的,所以栈上的数据的生存周期只是在函数的运行过程中,运行后就释放掉,不可以再访问。而堆上的数据只要程序员不释放空间,就一直可以访问到,不过缺点是一旦忘记释放会造成内存泄露。
碎片问题:对于栈,不会产生不连续的内存块;但是对于堆来说,不断的new、delete势必会产生上面所述的内部碎片和外部碎片。
申请大小的限制:栈是向低地址扩展的数据结构,是一块连续的内存的区域。栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间,就会产生栈溢出;对于堆,是向高地址扩展的数据结构,是不连续的内存区域。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
申请效率的比较:栈由系统自动分配,速度较快。但程序员是无法控制的;堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

java中的引用

强引用(Strong Reference):
在代码中普遍存在的,类似”Object obj = new Object”这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象

软引用(Sofe Reference):
有用但并非必须的对象,可用SoftReference类来实现软引用,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存异常异常。

弱引用(Weak Reference):
被弱引用关联的对象只能生存到下一次垃圾收集发生之前,JDK提供了WeakReference类来实现弱引用。

虚引用(Phantom Reference):
也称为幽灵引用或幻影引用,是最弱的一种引用关系,JDK提供了PhantomReference类来实现虚引用。

软引用(Soft Reference):软引用和弱引用的区别在于,若一个对象是弱引用可达,无论当前内存是否充足它都会被回收,而软引用可达的对象在内存不充足时才会被回收,因此软引用要比弱引用“强”一些
虚引用(Phantom Reference):虚引用是Java中最弱的引用,那么它弱到什么程度呢?它是如此脆弱以至于我们通过虚引用甚至无法获取到被引用的对象,虚引用存在的唯一作用就是当它指向的对象被回收后,虚引用本身会被加入到引用队列中,用作记录它指向的对象已被销毁
3. How——如何使用弱引用?
拿上面介绍的场景举例,我们使用一个指向Product对象的弱引用对象来作为HashMap的key,只需这样定义这个弱引用对象:

productA = new Product(...);  
WeakReference<Product> weakProductA = new WeakReference<>(productA);  

现在,若引用对象weakProductA就指向了Product对象productA。那么我们怎么通过weakProduct获取它所指向的Product对象productA呢?很简单,只需要下面这句代码:

Product product = weakProductA.get();  

下面我们来简单地介绍下引用队列的概念。实际上,WeakReference类有两个构造函数:

WeakReference(T referent) //创建一个指向给定对象的弱引用  
WeakReference(T referent, ReferenceQueue<? super T> q) //创建一个指向给定对象并且登记到给定引用队列的弱引用  

我们可以看到第二个构造方法中提供了一个ReferenceQueue类型的参数,通过提供这个参数,我们便把创建的弱引用对象注册到了一个引用队列上,这样当它被垃圾回收器清除时,就会把它送入这个引用队列中,我们便可以对这些被清除的弱引用对象进行统一管理

2.软引用(SoftReference)
软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
4.虚引用(PhantomReference)
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收
要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动

三.如何利用软引用和弱引用解决OOM问题
前面讲了关于软引用和弱引用相关的基础知识,那么到底如何利用它们来优化程序性能,从而避免OOM的问题呢?
下面举个例子,假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。
设计思路是:用一个HashMap来保存图片的路径 和 相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。在Android开发中对于大量图片下载会经常用到。

JVM GC怎么判断对象可以被回收了?

对象没有引用
作用域发生未捕获异常
程序在作用域正常执行完毕
程序执行了System.exit()
程序发生意外终止(被杀线程等)

在Java程序中不能显式的分配和注销缓存,因为这些事情JVM都帮我们做了,那就是GC。
有些时候我们可以将相关的对象设置成null 来试图显示的清除缓存,但是并不是设置为null 就会一定被标记为可回收,有可能会发生逃逸。
将对象设置成null 至少没有什么坏处,但是使用System.gc() 便不可取了,使用System.gc() 时候并不是马上执行GC操作,而是会等待一段时间,甚至不执行,而且System.gc() 如果被执行,会触发Full GC ,这非常影响性能。

对象从年轻代进入老年代

启用分代 GC 的,在发生 Young GC,更准确地说是在 Survivor 区复制的时候,存活的对象的分代年龄会加1。
当分代年龄 = -XX:MaxTenuringThreshold 指定的大小时,对象进入老年代
动态晋升到老年代的机制,首先根据 -XX:TargetSurvivorRatio (默认 50,也就是 50%) 指定的比例,乘以 survivor 一个区的大小,得出目标晋升空间大小。然后将分代对象大小,按照分代年龄从小到大相加,直到大于目标晋升空间大小。之后,将得出的这个分代年龄以上的对象全部晋升。
对于一些的 GC 算法,还可能直接在老年代上面分配,例如 G1 GC 中的 humongous allocations(大对象分配),就是对象在超过 Region 一半大小的时候,直接在老年代的连续空间分配。

参考

JVM架构和GC垃圾回收机制(JVM面试不用愁):https://blog.csdn.net/aijiudu/article/details/72991993
GC Root 对象有哪些:https://blog.csdn.net/baidu_20608025/article/details/87936633
JVM如何判断Java对象是否存活,是否要被GC回收?:https://blog.csdn.net/hbtj_1216/article/details/104139941
深入理解JVM的内存结构及GC机制:https://blog.csdn.net/anjoyandroid/article/details/78609971
JVM GC:https://www.jianshu.com/p/e8ee087bcd4f
最详细的JVM&GC讲解高广超:https://www.jianshu.com/p/99772ad092d3
Java性能优化之JVM GC(垃圾回收机制):https://zhuanlan.zhihu.com/p/25539690
对象会从年轻代进入老年代:https://blog.csdn.net/zhxdick/article/details/112131452
Major GC和Full GC的区别是什么?触发条件呢?:https://www.zhihu.com/question/41922036
JVM常见的垃圾回收器:https://blog.csdn.net/weixin_43228814/article/details/88934939
JVM成神之路-Java垃圾回收:https://blog.csdn.net/w372426096/article/details/81360083
理解Java中的弱引用(Weak Reference):https://www.cnblogs.com/absfree/p/5555687.html
Java 如何有效地避免OOM:善于利用软引用和弱引用:https://www.cnblogs.com/dolphin0520/p/3784171.html

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×