我们关注的垃圾回收是指哪部分内存?
在Java虚拟机运行时数据区中有5部分:方法区、堆、虚拟机栈、本地方法栈、程序计数器
- 虚拟机栈、本地方法栈、程序计数器这三部分是线程私有的,随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出有条不紊的执行者出栈和入栈操作,每一个栈帧中分配多少内存基本上是在类结构确定下来时就已经知道的,因此这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,方法结束或者线程结束时,内存自然就跟着回收了。
- Java堆和方法区则不一样,一个接口(interface)的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序运行期间才知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾回收所关注的内存正是这部分区域。
如何判断堆中的对象已死
- 引用计数算法,给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器值就减1;任何时刻计数器的值为0时表示这个对象是没有被使用的。
缺点:很难解决对象之间相互循环引用的问题。 - 可达性分析算法
先看个概念,什么是“GC Roots”?在Java中,可以作为GC Roots的对象有下面几种:
a.虚拟机栈中引用的对象
b.方法区中类的静态属性引用的对象
c.方法区中常量引用的对象
d.本地方法栈中JNI引用的对象
从“GC Roots”作为起始点开始往下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明对象时不可用的。
方法区的回收
方法区也称“永久代”,在方法区中进行垃圾回收的“性价比”一般比较低。方法区主要回收两部分内容:废弃常量、无用的类
- 回收废弃常量:假如一个字符串“abc“已经进入了常量池中,但是当前系统没有任何一个String对象是叫做”abc“的,换句话说就是没有任何String对象引用常量池中的”abc“常量,如果这时发生内存回收,这个”abc“常量就会被清理出常量池。
- 回收无用的类:必须同时满足三个条件才会被判定为”无用的类“
a.该类所有的实例都已经被回收,也就是说Java堆中不存在该类的任何实例。
b.加载该类的ClassLoader已经被回收。
c.该类对应的jaav.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
设置VM参数
- 参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
作用:限制Java堆的大小为20M,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设置为了一样,避免堆自动扩展),-XX:+HeapDumpOnOutOfMemoryError表示让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析。 - 参数:-Xss128k(对于HotSpot来说不区分虚拟机栈和本地方法栈)
作用:设置栈的大小为128k - 参数:-XX:PermSize=10M -XX:MaxPermSize=10M
作用:设置方法区的内存大小,并且不可扩展 - 参数: -Xms20M -XX:MaxDirectMemorySize-10M
作用:设置堆内存大小为20M,设置直接内存大小为10M
垃圾回收算法
-
标记-清除算法
image.png
这个算法氛围”标记“和”清除“两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
缺点:”标记“和”清除“这两个过程的效率都不高,并且在标记清除之后会产生大量不连续的内存碎片,空间碎片太多导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不再次触发垃圾回收。 -
复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象赋值到另外一块上面,然后再把已使用过的内存空间一次清理掉
缺点:将内存缩小为了原来的一半,未免太高了一点
现在的商业虚拟机一般是将这块内存分为一块较大的Eden空间和两块较小的Survivor空间,比例是8:1:1,每次使用Eden和其中一块Survivor,当回收时,将Eden和Survivor中还存活着的对象一次性的复制到另外一块Survivor空间上,最后清理掉Eden和刚刚用过的Survivor空间。 -
标记-整理算法
image.png
-
分代收集算法
主流算法,当前主流商业虚拟机普遍使用的垃圾收集算法,一般是根据对象存活周期的不同将内存划分为新生代和老年代,在新生代中采用复制算法,在老年代中使用”标记-清除“或者”标记-整理“算法
垃圾收集器

如果两个收集器之间存在连线,说明这两个收集器可以搭配使用,没有最好的收集器,只有最合适的收集器(不同的应用场景)
STW(stop the world)
在利用GC Roots做可达性分析时,这项分析工作必须在一个能确保一致性的快照中进行(一致性的意思是指在整个分析期间,整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点无法满足的话分析结果的准确性就无法得到保证),所以在GC进行时必须停顿所有Java执行线程(这就是STW)。
GC日志

- 33.125和100.667代表了GC发生的时间,是从Java虚拟机启动以来经过的秒数。
- [GC和[Full GC表示这次垃圾收集的停顿类型,如果有”Full“,说明这次GC发生了STW
- [DefNew、[Tenured、[Perm表示GC发生的区域,这里显示的区域的名称是与使用的GC收集器密切相关的,DefNew是”Default New Generation“的缩写,是在Serial收集器中定义的。如果是ParNew收集器,新时代的名称会变为[ParNew,意为”Parallel New Generation“,如果是Parallel Scavenge收集器,它的新时代的名词是”PSYoungGen“
- 方括号之内的3324k->152k(3712k)含义是”GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)“
- 方括号之外的3324k->152k(11904k)含义是”GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)“
- 0.0025925secs表示该内存区域GC所占用的事件,单位是秒,有的收集器会给出更具体的时间数据,如”[Times: user=0.01 sys=0.00 , real=0.02secs“,分别代表用户态消耗的CPU时间、内核态消耗的CPU时间、操作从开始到结束所经过的墙钟时间,墙钟时间包括各种非运算的等待耗时,例如磁盘I/O、线程阻塞,而CPU时间不包括这些。
内存分配与回收策略
对象的内存分配往大方向上讲,就是在堆上分配,对象主要分配在新时代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配,少数情况下可能会直接分配在老年代中,分配的规则不是百分比固定的,取决于当前使用的是哪种垃圾收集器组合,还有虚拟机中与内存相关的参数的配置。
-
对象优先在Eden区分配
(新时代GC也叫Minor GC,老年代GC也叫Major GC、Full GC,Major GC的时间一般比Minor GC慢10倍以上)
大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
image.png
虚拟机参数:限制Java堆大小为20MB,不可扩展,其中10MB分配给新时代,剩下的10MB分配给老年代,-XX:SurvivorRatio=8表示新时代Eden区与Survivor区的比例是8:1,分别为8MB、1MB、1MB
- 大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组,虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个值的对象直接在老年代分配,这样做的目的是避免在Eden区以及两个Survivor区之间发生大量的内存复制(新时代采用复制算法收集内存)

3145728代表的是1024x1024x3=3M,表示大于3M的对象直接分配在老年代。
-
长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能够被Survivor容纳的话,将被移动到Survivor空间中,并且对象的年龄设定为1,对象在Survivor区中每”熬过“一次Minor GC,年龄就增加1岁,当它的年龄增加到一定的程度(默认是15),就会被晋升到老年代中,对象晋升老年代的年龄阈值可以通过参数-XX:MaxTenuringThreshold设置
image.png
网友评论