简单谈谈对GC垃圾回收的通俗理解
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
简单谈谈对GC垃圾回收的通俗理解
简单谈谈对GC垃圾回收的通俗理解
⽂章简介
《简单谈谈对GC垃圾回收的理解》是我的第⼀篇博客,了解并学习了JVM的垃圾回收机制后,把⾃⼰的⼀些理解记录下来,通过输出博客的⽅式来沉淀,我觉得是⼀个不错的⽅式!
垃圾回收是指什么
所谓的垃圾,顾名思义,就是指的在程序运⾏的过程中,有类的诞⽣、初始化、销毁,在这⼀系列的过程中,我们的程序⾃然会产⽣⼀些已经消亡的,不需要的类、实例等等。
⽽这些对于程序不需要的东西或者阻碍程序正常运⾏的东西就是垃圾。
⽽垃圾回收指的就是将这些垃圾从我们程序运⾏的环境中清除出去,⾄于这些垃圾会去向哪⾥,⾃然是不需要我们去关⼼的,毕竟你也从未关⼼过垃圾收集车 将垃圾运往何处!
为什么需要垃圾回收
我们都知道,⼩区的垃圾回收车 每天都会将垃圾集中运⾛处理,还⼩区⼀个⼲净舒适的环境。
那么同样,程序的运⾏也需要⼀个畅通⽆阻的环境。
如果垃圾过多,可能导致程序运⾏时间变长,就像我们的电脑C盘空间不⾜的时候,电脑不仅运⾏缓慢,还会出现死机、蓝屏等恶况!
程序中⽐较经典的因为垃圾过多导致的错误就有OOM(ng.OutOfMemoryError)。
OOM 内存不⾜
官⽅语⾔是这样描述的:当Java虚拟机由于内存不⾜⽽⽆法分配对象,并且垃圾回收器⽆法再提供更多内存时,抛出该异常。
通俗来说,就是内存满了,爆了。
⽽我们知道,java中错误(Error)和异常(Exception)是不同的。
异常还可以被捕获、抛出来通过程序代码来处理,但是错误已经严重到不⾜以被应⽤处理,会导致程序直接崩溃停⽌!在真实项⽬中这样的情况肯定是不允许发⽣的。
那么我们可以分析⼀下导致内存不⾜的原因!⽆外乎有以下两点:
分配到的内存太少。
程序使⽤的太多。
分配太少:我们知道,虚拟机可以使⽤的内存也是有个限度的,当被分配的内存⼤⼩完全不够⽀持我们程序正常运⾏就会OOM。
我们可以通过以下代码查看JVM初始化的总内存⼤⼩和JVM试图使⽤的最⼤内存。
long maxMemory = Runtime.getRuntime().maxMemory();//虚拟机试图使⽤的最⼤内存
long totalMemory = Runtime.getRuntime().totalMemory();//jvm初始化的总内存
System.out.println("maxMemory=" + maxMemory+"字节\t" + (maxMemory/(double)1024/1024) + "MB" );
System.out.println("totalMemory=" + totalMemory+"字节\t" + (totalMemory/(double)1024/1024) + "MB" );
在默认情况下,初始化的总内存约为我们电脑内存的1/64,试图使⽤的最⼤内存约为电脑内存的1/4。
这⾥的计算误差可以⾃⾏了解。
⽽这个分配给虚拟机的内存其实是可以⼿动调节的。
我们可以通过调节虚拟机参数来⾃定义分配内存⼤⼩。
因此我们可以尝试去增⼤分配内存,测试程序是否仍然OOM!如果还是OOM那可能就需要去检查我们的代码是不是出问题了。
-Xms4028m -Xmx4028m
使⽤的太多:意思就是本来分配的内存完全是⾜够程序正常运⾏的,但是由于某些错误的使⽤导致内存使⽤后没有及时释放,造成内存泄露或内存溢出。
内存泄露:(memory leak)指的是程序在申请内存后,⽆法释放已申请的内存空间,导致虚拟机⽆法将该块内存分配给其他程序使⽤。
内存溢出:( out of memory)指的是程序申请的内存超出了JVM能提供的内存⼤⼩。
在之前没有垃圾⾃动回收的⽇⼦⾥,⽐如C语⾔和C++语⾔,我们必须亲⾃负责内存的申请与释放操作,如果申请了内存,⽤完后⼜忘记了释放,⽐如C++中的new了但是没有delete,那么就可能造成内存泄露。
偶尔的内存泄露可能不会造成问题,⽽⼤量的内存泄露可能会导致内存溢出。
⽽在Java语⾔中,由于存在了垃圾⾃动回收机制,所以,我们⼀般不⽤去主动释放不⽤的对象所占的内存,也就是理论上来说,是不会存
在“内存泄露”的。
但是,如果编码不当,⽐如,将某个对象的引⽤放到了全局的Map中,虽然⽅法结束了,但是由于垃圾回收器会根据对象的引⽤情况来回收内存,导致该对象不能被及时的回收。
如果该种情况出现次数多了,就会导致内存溢出,⽐如系统中经常使⽤的缓存机制。
Java中的内存泄露,不同于C++中的忘了delete,往往是逻辑上的原因泄露。
在我们扩⼤了JVM内存后仍然OOM的话,就需要使⽤内存快照分析⼯具来快速定位内存泄漏。
使⽤JProfiler来分析Dump出的内存⽂件。
还是修改JVM的启动参数并导出内存⽂件,然后⽤JProfiler打开分析。
-Xms10m -Xmx80m -XX:+HeapDumpOnOutOfMemoryError
这样我们便能快速定位内存泄漏,并进⾏改进!
怎么进⾏垃圾回收
垃圾回收的地⽅
JVM的位置
垃圾回收的地⽅⾃然是在JVM中。
因此,⾸先我们需要了解JVM在哪⾥?下⾯通过⼀张图展⽰JVM的位置。
JVM与操作系统交互,⽽程序是在JVM上运⾏的。
这⾥的JVM其实就是JRE,⽽不是JDK!
jvm的体系结构
那我们知道了JVM的位置,但是垃圾回收也并不是存在于JVM的所有⾓落的。
因为JVM中也是划分为了好⼏块区域的,下⾯我们看看JVM中区域的划分。
我们从上到下来分析⼀下JVM的体系结构。
类加载器
顾名思义,类加载器就是⽤于加载⼀个类的。
我们都知道,我们写的代码刚开始是⼀个.java⽂件,经过idea编译之后变成.class⽂件,最后通过类加载器加载成为⼀个Class,我们可以通过这⼀个Class,new出很多个实例对象。
类加载器⼜分为以下:
1. 虚拟机⾃带的加载器。
2. 启动类加载器/根加载器 Bootstrap 位于\jdk_1.8\jre\lib\rt.jar下
3. 扩展程序加载器 ExtClassLoader 位于\jdk_1.8\jre\lib\ext下
4. 应⽤程序加载器/系统类加载器 AppClassLoader
既然有这么多ClassLoader,它们是从哪⾥加载class的,这个问题jdk源码中uncher已经给出回答:Bootstrap ClassLoder加载的是System.getProperty("sun.boot.class.path");、ExtClassLoader加载的是System.getProperty("java.ext.dirs")、AppClassLoader加载的是System.getProperty("java.class.path")。
另外不得不了解的是双亲委派机制:
说⽩了就是需要加载⼀个类的时候,⼦加载器都特别懒,都想依靠⽗加载器,只有⽗加载器加载不了这个类的时候,⼦加载器才去加载。
Java的类加载使⽤双亲委派模式,即⼀个类加载器在加载类时,先把这个请求委托给⾃⼰的⽗类加载器去执⾏,如果⽗类加载器还存在⽗类加载器,就继续向上委托,直到顶层的启动类加载器。
如果⽗类加载器能够完成类加载,就成功返回,如果⽗类加载器⽆法完成加载,那么⼦加载器才会尝试⾃⼰去加载。
那么谁是⽗,谁是⼦?通过以下代码即可查看。
car car1 = new car();
Class<? extends car> car1Class = car1.getClass();
System.out.println(car1Class.getClassLoader());//AppClassLoader
System.out.println(car1Class.getClassLoader().getParent());//ExtClassLoader
System.out.println(car1Class.getClassLoader().getParent().getParent());//null 1.不存在 2.java程序找不到
AppClassLoader -------->ExtClassLoader-------->Bootstrap 由⼦到⽗。
这种机制的好处:
避免类的重复加载。
防⽌java的核⼼API被篡改。
即使我们定义了与java核⼼api相同的类,双亲委派机制也会引导去加载核⼼api。
举个例⼦:
⾃定义类:ng.String
与核⼼api重名,根据双亲委派机制,引导类会加载核⼼api中的String,忽略掉⾃定义String类,如果不采⽤该机制,那么⾃定义的String将有可能被加载,那将会导致⾮常崩溃的情况,⽐如功能⽆法实现,项⽬信息泄露等等。
⾃定义类:ng.MyClass
包名的命名与核⼼api包重合,那么理论上就会由引导类加载器完成加载,可是经过检验发现是没有权限访问核⼼包路径的,那么就会拒绝加载,避免对引导类加载器本⾝与核⼼包造成威胁。
类加载器了解这么多就差不多了。
接下来看看运⾏时数据区。
运⾏时数据区
这块区域⾥⾯会发⽣很多事情。
变量的初始化、常量定义、对象实例化、⽅法调⽤等。
在这⼀系列过程中产⽣垃圾是不可避免的。
⽽我们⼀定要记住的是以下两块区域,有⼀块是不会产⽣垃圾的。
先说说不会产⽣垃圾这⼀块区域。
我们看到这⼀块区域有三部分组成:
java栈
栈是⼀种先进先出的数据结构。
主要存储8⼤基本类型、对象引⽤、实例的⽅法。
栈⾥⾯要使⽤的东西就会压栈,不使⽤的就会弹栈。
其实就是进出栈。
⽤完即弹出,所以不会产⽣垃圾。
这⾥常问到的问题是main⽅法为何先调⽤却最后结束?以及ng.StackOverflowError 栈溢出。
本地⽅法栈
这⾥主要⽤于登记带有native关键字修饰的本地⽅法。
我们都知道Java是⽤C++写的,这⾥的native修饰的⽅法也是C++的本地⽅法库,Java通过JNI(本地⽅法接⼝)去调⽤C++的库⽅法,来实现Java不容易实现的功能。
程序计数器
保存下⼀条将要执⾏的指令地址。
也称PC寄存器。
再说说有垃圾的这⼀块区域。
我们看到这⼀块区域有两部分组成:
⽅法区:
静态变量、常量、类信息(构造⽅法、接⼝定义)、运⾏时常量池存在⽅法区,但是实例变量存在堆内存中,和⽅法区⽆关。
⽅法区是被所有线程共享的。
堆:
类加载器读取了类⽂件之后,⼀般会把什么东西放到堆中?类、⽅法、常量、变量~,保存我们所有引⽤类型的真实对象。
堆内存⼜分为三个区:新⽣区、养⽼区、永久区。
之前所说的OOM就是指的堆内存不⾜。
所谓的JVM调优也主要指堆内存调优。
关于这⼀块区域暂时先了解到这⾥,后⾯会更详细的说明这⼀块区域的信息。
堆的三⼤区域
先看⼀张图:
如下,左边的元空间指永久区、中间的3⼩块指新⽣区,右边⽼年区也就是养⽼区。
接下来详细看看这⼏个区的特性。
新⽣区:
类诞⽣、成长、甚⾄死亡的地⽅。
伊甸园区。
所有的对象都是在伊甸园区new出来的。
伊甸园区满了就会触发⼀次轻GC。
幸存下来的会放到幸存者区。
幸存者区0、1。
(也称from、to区)伊甸园区和幸存区都满了就会触发⼀次重GC。
幸存下来的会放到⽼年区。
(轻重GC先不理会)真理:经过研究,99%的对象都是临时对象!所以很少OOM。
养⽼区:
⼤部分对象都是⽐较稳定,不容易消亡的对象。
元空间:
这个区域是常驻内存的。
⽤来存放⼀些jdk⾃⾝携带的class对象和接⼝元数据。
储存的是java运⾏时的⼀些环境和类信息,这个区域不存在垃圾回收,关闭VM虚拟就会释放这个区域的内存。
关于元空间和永久代的关系可以⾃⾏了解。
轻GC和重GC
分类
Minor GC
轻GC
清理年轻代。
Minor GC指新⽣代GC,即发⽣在新⽣代(包括Eden区和Survivor区)的垃圾回收操作,当新⽣代⽆法为新⽣对象分配内存空间
的时候,会触发Minor GC。
因为新⽣代中⼤多数对象的⽣命周期都很短,所以发⽣Minor GC的频率很⾼。
Major GC
轻GC
清理⽼年代。
Major GC清理Tenured区,⽤于回收⽼年代,出现Major GC通常会出现⾄少⼀次Minor GC。
Full GC
重GC
清理整个堆空间—包括年轻代、⽼年代、元空间。
Full GC是针对整个新⽣代、⽼⽣代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。
Full GC不等于
Major GC,也不等于Minor GC+Major GC,发⽣Full GC需要看使⽤了什么垃圾收集器组合,才能解释是什么样的垃圾回收。
分别触发的条件
Minor GC:
1. Eden区域满。
2. 新创建的对象⼤⼩ > Eden所剩空间。
Full GC:
1. 每次晋升到⽼年代的对象平均⼤⼩>⽼年代剩余空间。
2. MinorGC后存活的对象超过了⽼年代剩余空间。
3. 永久代空间不⾜。
4. ⼿动调⽤System.gc()
GC回收算法
在堆中存放着对象实例,GC回收器在对堆进⾏回收前,需要确定哪些对象需要被回收,即确定哪些对象还存活,哪些对象已经死去。
⽽GC 算法正是起这种作⽤的。
种类
标记清除法。
标记压缩。
复制算法。
引⽤计数器。
(已经不再使⽤)
引⽤计数器
给对象添加⼀个引⽤计数器,每当⼀个地⽅引⽤它时,计数器加1,每当引⽤失效时,计数器减少1.当计数器的数值为0时,也就是对象⽆法被引⽤时,表明对象不可在使⽤,这种⽅法实现简单,效率较⾼,⼤部分情况下不失为⼀个有效的⽅法。
但是主流的Java虚拟机如HotSpot 并没有选取引⽤计数法来回收内存,主要的原因难以解决对象之间的相互循环引⽤的问题。
标记清除法
缺点:两次扫描严重浪费时间,会产⽣内存碎⽚。
优点:不需要额外的空间。
标记压缩
复制算法
好处:没有内存碎⽚
坏处:浪费了内存空间。
多⼀半空间永远是空 to。
复制算法最佳使⽤场景:对象存活度较低的时候。
总结
内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法=标记压缩>标记清除算法
内存利⽤率:标记压缩算法=标记清除算法>复制算法
思考⼀个问题:难道没有最优算法吗?
没有。
没有最好的算法,只有最合适的。
GC也被称为分代收集算法。
年轻代:
存活率低 - 复制算法。
⽼年代:
区域⼤,存活率⾼ - 标记清除和标记压缩混合实现。
以上
感谢您花时间阅读我的博客,以上就是我对GC垃圾回收的⼀些理解,若有不对之处,还望指正,期待与您交流。
本篇博⽂系原创,仅⽤于个⼈学习,转载请注明出处。