原创 吴就业 125 0 2019-09-04
本文为博主原创文章,未经博主允许不得转载。
本文链接:https://wujiuye.com/article/08f14c1a33eb4bb8b30f22e19406f7a1
作者:吴就业
链接:https://wujiuye.com/article/08f14c1a33eb4bb8b30f22e19406f7a1
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。
一开始接触垃圾回收这个话题的时候,我最感兴趣的是,jvm是怎么判断一个对象是否被引用的?
最早的jvm版本,其实也是用了引用计数算法。
关于引用计数算法,很多框架也会用到。比如spring 的注解式事务,关于这个知识点,这里我不详细说明,就是每执行一个mapper方法都将引用数加1,这跟mybatis框架有关。只有引用数为0时,才会真正的去释放连接,当然,是否释放由连接池决定。也就是事务提交之后。
引用计数法为什么被Jvm弃用?
原因是无法解决循环引用问题,即对象A引用对象B,同时对象B又引用对象A,导致对象A和B都无法被JVM回收。
说到垃圾回收就不得不说GC Root可达性分析。关于GC Root可达性分析,需要理解两个概念:
垃圾回收器,现在常用的就CMS和G1两种。堆,就是一块jvm向系统申请的内存,并没有什么新生代老年代的化分。对于CMS垃圾收集器来说,CMS使用了分代回收算法,将整个堆空间分为年轻代和老年代,年轻代又划分为Eden区和两个幸存区。而G1垃圾收集器使用分区回收算法,将整个堆化分为一个个的Regin,但每个Regin还保留分代。
JDK1.8之后默认使用G1垃圾收集器,而1.8之前包括1.8则默认使用CMS垃圾收集器,至少我看的OpenJDK使用的是CMS。1.8还取消了永久代,使用元空间,当然,这跟堆没有关系。
关于元空间、永久代、方法区,不要搞混咯。《java虚拟机规范》中规定了方法区这个概念和它的作用,但并没有规定要如何去实现它。方法区主要用于存储类的信息、常量池、方法数据、方法代码等。
在HotSpot JVM中,JDK1.8之前,永久代就是方法区的实现,而1.8之后,废弃了永久代。元空间就是HotSpot JVM在1.8版本及之后方法区的实现。元空间与永久代最大的区别是,元空间并不在虚拟机中,而是使用直接内存。并且,字符串常量由永久代转移到堆中,jdk1.8字符串常量就已经是分配在堆中了。所以性能调优的配置一般不会考虑元空间,影响不大。
以CMS垃圾收集器为例。年轻代使用复制算法,而老年代则使用标志-整理算法。年轻代的三个区Eden区、from幸存区和to幸存区默认大小比例为8:1:1。年轻代执行的是Minor GC,年轻代的回收过程还是比较复杂的。
回收时,先将eden区存活对象复制到from区,然后清空eden区,当这个from区也存放满了时,则将eden区和from区存活对象复制到to区,然后清空eden区和这个from区,然后将to区和from区交换,保持to区为空。当to区不足以存放 eden区和from区的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC。
关于from区和to区,年轻代对象还有一个年龄的概念,每gc一次如果对象还活着,则将年龄加1,到一定年龄之后还活着,就将对象直接放入老年代,这个年龄是可以配置的,一般不去修改这个配置。
标志清除算法,即并发将需要回收的对象添加一个标志;清除,即stop the work,停止所有java线程,将被标志的对象回收。这个算法的缺点是,会产生内存碎片,这里空一点,那里空一点,如果有大一点的对象则会没有位置放得下。但大的对象都会直接进入年老代。
复制算法,需要两个区域,一个区域作为当前使用区域,另一个区域则是备用区域,当下一次进行垃圾回收时,将存活的对象复制到另一个区域去,再将当前区域一次性清理。
标志整理算法,与标志清除算法一样,都需要并发标志需要回收的对象,然后STW清除,但这里的清除不是简单的清楚,而是将所有存活的对象移动到连续的一块区域,会覆盖被标志的对象的位置,剩下的区域就是可用区域。清除算法关注垃圾对象,而整理算法关注存活对象。
不管是标志清除,还是标志整理,如何才能知道一个对象是否是垃圾对象,才是重点。
现在来回答GC Root可达性分析的两个问题。
第一个问题,哪些是GC Root节点?
a、java虚拟机栈,栈帧中的局部变量表和操作数栈中的引用的对象;
b、方法区中的类静态属性引用的对象;
c、方法区中的常量引用的对象;
d、本地方法栈中JNI本地方法的引用对象。
可能第一个和第四个都比较难理解,第一个涉及到java栈和栈帧的概念。静态字段引用的对象是存活整个进程的生命周期的,局部变量表和操作数栈中引用的对象一定是当前方法使用到的对象。这也就明白了,为什么局部变量都是方法结束后才会被回收。
从Java栈的概念来理解,一个线程对应一个Java栈,也叫Java虚拟机栈。一个方法则对应一个栈帧,方法调用对应着栈帧的入栈和出栈。栈帧的结构分为局部变量表,操作数栈,动态链接,方法出口。动态链接:一个方法调用其它方法,需要将方法的符号引用转为其在内存中的直接引用。方法出口:正常执行完成出口,抛出异常完成出口。说实话,动态链接与方法出口我也没明白透彻。
操作数栈是用来存放代码运行所需要的变量的。怎么理解简单呢?字节码可由JIT编译为机器码运行,汇编指令同样也是需要编译为机器码才能运行的。就以汇编指令来说明,比如执行一条add指令,需要将一个变量放到寄存器,再与内存中的一个变量累加,结果存放到寄存器。一条add指令就需要两个操作数才能完成。那么理解java操作数栈也是一样的,调用一个this.A方法,假设A方法需要三个参数,那么操作数栈至少需要4个u2单位的栈深度,第一个是this,其它三个是参数。这里不做更深入的解释。
本地变量表是一块连续空间,可以理解是u2类型的数组。u2是jvm定义的两个字节无符号类型。局部变量表存放着局部变量的引用。局部变量按出现的顺序存放于局部变量表,注意,像new A().say();这种代码,你看着是没有保存A对象的,但编译为字节码后,它是会被存放在局部变量表的,还有抛出异常的catch(Exception e) 异常e也会占用局部变量表的一个位置,当抛出异常时,会将异常存放于局部变量表,抛出则是athrows指令。我们写代码时候,声明Object c=new Object,其实c不过是给我们看的变量名罢了,编译成字节码后就没有c变量的概念存在了。
局部变量引用的对象除了方法结束后会被回收,也可显示将变量赋值为null,不再引用对象。但对象什么时候被回收,这还得看垃圾收集器什么时候执行GC。如果是大对象,会在年老代中,需要等下一次Full GC;如果对象还在年轻代中,下一次Minor GC就能将对象回收。当然,这里说的对象是在方法中new的对象,并且,是非线程共享的变量。
第二个问题,什么是可达性分析?
比如有一个对象A,A有name字段(字符串类型),而A是一个局部变量,垃圾收集器执行GC时,在第一阶段的并发标志,会寻找到这个局部变量作为GC Root节点,然后就是一颗二叉树的深度遍历,找出所有被引用的对象,其它未能从所有GC Root节点找到的对象,即没有任何引用的对象,就需要被标志回收。
注意,基本数据类型int,long,bool等,对方法而言,是直接存放在局部变量表的,也就是占用的是栈的空间,并非堆空间。看字节码你就理解了。
与编码相关的,还有引用类型这一知识点。Java有四种引用类型,分别是强引用、软引用、弱引用和虚引用。强引用,只要有引用存在,就不会被回收,一般的Object a=new Object(),就是a持有这个对象的强引用。而弱引用,WeakReference
声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。
笔者最近的一次重构项目选择用dubbo去实现服务间的调用,选择dubbo作为分布式的RPC远程服务调用框架,但笔者在使用的过程中遇到了很多疑难问题,网上搜不到一篇能解决我疑问的文章,无奈,只能选择自己从源码中寻找答案。
Java8提供的流式编程Stream,相信大家每天都在用。但是读过源码的,我猜也没有几个,包括我。只是最近使用上遇到些问题,不得不去深入了解,所以我花了点时间粗略看了一下,但关于并行流的逻辑我也没理解清楚。
第一次将分布式技术应用到实际项目中就遇到分布式事务的问题,好在不是那种严格要求双写一致性的事务问题。我了解的分布式事务解决方案有两种,分别是XA和TCC,今天要分享的是,我如何使用TCC处理项目中分布式事务问题。
老项目一直在使用AWS的ElastiCache的Redis集群服务,为什么突然要自己部署集群呢。理由只有一个,贵了。对的,使用AWS的Redis集群服务,每个月要300$以上的费用,这成本是高了些,并且现在这个平台的并发量不高,缓存的数据量也只有1G多,确实贵了。
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。