1.jvm体系结构概览
栈内存(Stack):每个线程私有的
堆内存(Heap):所有线程公用的
方法区(Method Area):有点像以前常说的“进程代码段”,这里面存放了每个加载类的反射信息、类函数的代码、编译时常量等信息
本地方法栈(Native Method Stack):主要用于JNI中的原生代码,平时很少涉及
2.垃圾回收对象
垃圾回收一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能
作用区域包括方法区和堆内存区
JVM的垃圾回收主要指的是回收JVM内存堆内的数据对象
里边存储的是所有新建的对象
3.判断对象是否存活
3.1引用计数算法
这种算法实现比较简单
但是每次对对象进行复制操作都需要维护引用计数器,其本身就有一定的性能消耗
此外比较难处理循环引用的情况
对扫描到的每个对象都要判断下该对象引用计数器是否为0,若为0就会释放该对象的内存空间。
3.2可达性分析算法
可达性分析算法是为了解决引用计数法的问题而提出
它使用了根集的概念
所谓的“根集”,就是正在运行的线程中,可以访问的引用变量 的 集合
比如所有线程当前函数的参数和局部变量、当前类的成员变量等等
垃圾回收线程先找出被根集直接引用的所有对象(不妨叫集合1)
然后再找出被集合 1直接引用的所有对象(不妨叫集合2)
然后再找出被集合2直接引用的所有对象......
如此循环往复,直到把能遍历到的对象都遍历完
3.3 引用类型
(1)强引用
强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object()’'这类的引用
只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
(2)软引用
软引用用来描述一些还有用但并非必须的对象
对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收
如果这次还没有足够的内存,才会抛出内存溢出异常。
(3)弱引用
弱引用也是用来描述非必须对象
但是它的强度比软引用更弱一些
被弱引用关联的对象只能生存到下一次垃圾收集发生之前
当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
(4)虚引用
虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响
也无法通过虚引用来取得一个对象实例
为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
4. 垃圾回收算法
4.1 标记-清除算法
这是最简单的一种垃圾回收策略
耗时少
但是会产生碎片空间
4.2 标记-整理算法
标记整理算法比起标记清除算法的优点是不会产生碎片内存空间
但是会消耗更多的时间
4.3复制算法
jvm年轻代的内容管理就采用的此算法
优点是不会产生碎片内存空间,效率相对标记整理要高
缺点就是会浪费一倍的内容空间
不过jvm根据实际情况采用的另外一种机制
最大程度的利用了内容空间
5. 分代收集管理
统计分析发现:发现大部分对象在很短的时间内就会失效,也即是说大部分对象的存活时间并不长。
98%的对象都是临时对象,会在一次垃圾回收周期内被清除
Stop the World 事件:垃圾的回收过程属于一种叫 "Stop the World" 的事件
在这种事件发生时,所有的程序线程都要暂停,直到事件完成为止
jvm中,将堆内存划分为几个区域,分别是:新生代、老年代和永生代
5.1 年轻代(Young Gen)
该内存区域主要用于存放新创建的对象
该区域的内存容量相对会较小,垃圾回收也比较频繁,是垃圾回收的主战场
年轻代又可进一步分成1个Eden Space和2个Suvivor Space(编号为0和1)
默认将会按照 8:1:1 划分成 Eden 与两块 Survivor空间
新生代每次 GC 之后都可以回收大批量对象,所以比较适合复制算法,只需要付出少量复制存活对象的成本
每次使用 Eden 与一块Survivor空间,这样我们只是闲置 10% 内存空间
当对象在堆中被创建时,将进入年轻代的Eden Space
随着程序创建对象的增加,Eden空间会慢慢被填满
当 Eden 空间填满时,会触发轻微的垃圾收集
此时JVM会扫描Eden Space和Suvivor Space 1
对于仍然存活的对象,则复制到Suvivor Space 0中
同时幸存对象的年龄增加,Eden区和S1被清空
当 Eden 空间再次被填满时,会继续触发轻微的垃圾收集
此时Suvivor Space的0和1的角色互换
此时JVM会扫描Eden Space和Suvivor Space 0
对于仍然存活的对象,则复制到Suvivor Space 1中
同时幸存对象的年龄再次增加
注意,此时Survivor区中有了不同年龄的对象,Eden区和S0被清空
当 Eden 空间再次被填满时,会继续触发轻微的垃圾收集
会继续重复同样的操作
这一次Survivor区仍然会交换
被引用的对象移动到S0
幸存对象的年龄继续增加,Eden区和S1被清空
如果其中一个Suvivor Space已满,或者在多次扫描Suvivor Space
某个对象仍然存活(某个对象的年龄超过年龄阈值)
将其移动到Old Gen
5.2 老年代(Tenured Gen)
该内存区域主要存放JVM中生命周期较长的对象(经过几次的Young Gen的垃圾回收后仍然存在)
该区域内存相对较大,垃圾回收也相对不频繁(譬如可能几个小时一次)
老年代主要采用压缩的方式来避免内存碎片(将存活对象移动到内存片的一边,也就是内存整理)
老年代的垃圾回收也会触发STW(Stop the World)
而且老年代回收速度会慢很多
所以,对于响应性的应用程序,应该尽量避免对老年代的垃圾回收
5.3永生代(Perm Gen)
该内存区域主要存放类定义、字节码和常量等很少会变更的信息
该内存区域是由JVM在运行时根据应用程序使用的类来填充的
此外,Java SE类库和方法也存储在这里
如果JVM发现某些类不再需要,并且其他类可能需要空间,则这些类可能会被回收。
6. 思考
6.1 System.gc()是否可以主动通知JVM立刻进行垃圾回收?
java中手动调用 System.gc();不能立刻让程序立刻就回收内存
这个调用相当于“建议”执行垃圾回收,但是什么时候调用是不能确定的
通过以下代码可以让jvm立刻进行垃圾回收
System.gc();runtime.runFinalizationSync();System.gc();
但是建议不到万不得已不要调用,因为jvm有自己的gc策略,根本不需要我们来手动
6.2 jvm的方法区,在什么时候会进行垃圾收集?
方法区也是线程共享的内存区域
用于存储已被虚拟机加载的类信息、即时编译后的代码、静态变量和常量等数据
方法区的垃圾回收主要有两种,分别是对废弃常量的回收和对无用类的回收
当一个常量对象不再任何地方被引用的时候,则被标记为废弃常量,这个常量可以被回收
方法区中的类需要同时满足以下三个条件才能被标记为无用的类:
1.Java堆中不存在该类的任何实例对象;
2.加载该类的类加载器已经被回收;
3.该类对应的java.lang.Class对象不在任何地方被引用,且无法在任何地方通过反射访问该类的方法。
当满足上述三个条件的类才可能被回收
但是并不是一定会被回收,需要参数进行控制
HotSpot虚拟机提供了-Xnoclassgc参数进行控制是否回收
6.3 如何降低GC的影响?
尽量减少堆内存的使用
由于GC是针对存储在堆内存的对象进行的
如果在程序中减少引用对象的分配(也就相应降低堆内存分配),那对于提高GC的性能
是很有帮助的
比如大量String操作的时候,使用StringBuilder替代
文/戴先生@2020年6月23日