读过之前半文章的小哥哥们肯定都知道,宝宝我最近入坑了Golang,当然这不是自愿的也说不上胁迫,反正就入坑了,没想到21世纪的C语言也有了堆和栈,随之而来的战利品是逃逸分析,自然这只是其中之一,其他的离题比较远,写文章最主要的是什么?不被催稿(奋斗状)!这个我有生之年是做不到了
还是先说说对象的分配吧,毕竟起个名字不容易,再离题了多尴尬~
在众多图当中发现一个比较喜欢的,分享一下,当然最最感谢的是作者:爱谁 谁,开个玩笑,斯武、风晴大哥
当然这个图也没涵盖所有点,比如这个逃逸分析是需要开启的-XX:+DoEscapeAnalysis,好像1.8是默认开启的,开了也是有些代价的,这需要权衡,开与不开——大佬您决定(下面会写);这个tlab也是一个点Thread Local Allocation Buffer线程本地分配缓存,他在伊甸园内 默认开启、默认设定为Eden Space的1%,见名猜意他是线程私有、独占的,因为独占所以不存在锁的问题、(自己跟自己抢锁玩~是不是闲的),自然也少了锁的开销,也不需要线程间同步,是不是优秀如大佬;思来想去、具体的过程还是说一下,因为顶重要的
- 逃逸分析确定在栈还是堆上,如是堆则
- 如tlab_top+size <= tlab_end则在TLAB上分配其增加tlab_top的值
- 如>则重新申请tlab,尝试存放
- 放不下则Eden加锁且eden_top+size<=eden_end则放Eden,增eden_top
- 如放不下则YoungGC,此时eden还是不足,么办法分代担保——老年代上
上面的过程涉及到jvm的选择策略,默认利用refill_waste值进行比较,大于则堆上分配否则废弃当前TLAB新建tlab分配新对象
堆上对象分配可利用
- 指针碰撞:serial、parNew等compact的收集器,内存是规整的,使用的和闲的之间有指针分界,分配内存时移动指针进行分配,bump the pointer
- 空闲列表:CMS基于mark-sweep的收集器,内存不规整肿么办?空闲列表帮忙您!谁来维护这个列表?当然时虚拟机老大哥了,大哥会记录哪些可用,分配时从列表中找足够大的进行分配就ok了
这当中隐晦地设计到大对象这个问题,那几个比较不就是嘛,这个大对象可指定 收集器的情况也不是很一样,情况吧稍微复杂些,但是搜或者查都不难找到,所以我就不写了
官方回答逃逸分析:一种确定指针动态范围的方法,就是在程序的哪些地方可访问到这些指针,具体涉及到指针分析和形状分析,是不是不好理解,在次要特别鸣谢公司让贫下中农的我可以”科学上网“;再直白一些:逃逸分析确定某个指针可以存储的所有地方,以及确定能否保证指针的生命周期只在当前进程或在其它线程中。
以好理解的角度说:一个函数分配了一个对象并返回该对象的指针,那么这个对象是不是就可能被其他方法访问到,”可能“这两字能让我们确定它的活动区域吗?显然不能 看这不就在你眼皮底下逃逸了;无独有偶如果这个指针存在全局变量或其他数据结构中,这些地方是不是都是有可能被共享的地方(一丁点可能性也算 不放弃不抛弃是我们的宗旨),共享是确定的但是不一定共享到哪里取,说白了就是它'作妖范围'无法确定,只能放任其到堆中遨游;简单说就是这个对象只在方法内,方法结束对象释放,当然这要开启逃逸分析和标量替换
有三种逃逸状态:
- 全局:一个对象的引用逃出了方法或线程:类变量为对象的引用,存在于一个逃逸的对象中,对象引用作为返回值 返给了调用方
- 参数级:方法调用过程中传递对象引用给方法,这句话明明很简单这么写书面气增加了不少
- 无:可进行标量替换(下面会讲)
说到这里就不得不说JIT了,又是一块带肉的骨头;篇幅太长,链接献上
写了那么多,为什么要逃逸分析,那可能是有很多好处的,本来不想写但是有一个小关键不写一点撑不起来这篇文章:
对象分离高大上一些也可以叫:标量替换,问把大象装冰箱……离题了,跑出来,扪心自问分配的时候真的是把对象完完整整的分配到栈上了吗?那肯定不是,那不明显违反了栈的初衷,寄人篱下就要有低人一等的融入姿态,实际上是把对象分解成一个个的标量,互相之间呐偷偷地联系着,这样就不用对象头了,锁就更不需要了,GC少了,内存的回收效率也高了,这么想想得到的快乐 也没有画的饼那么大……开启标量替换-XX:+EliminateAllocations,当然要在开启逃逸分析的基础上,1.8是默认开启的
上面有个要敲黑板的点:标量,首先应该get到一个信息:标量不可再分,谁不可再分?基数数据和对象引用、完美!那对应的能分解的是什么?对象,书面称呼是:聚合量,这个就不解释了
最近敖丙发生了有些事情,我不是偏袒他,大家可能会抗议:完了、这话一说完,就要开始偏袒了,其实还好,定位不一样、内容自然不一样,像语文考试一样,离题的作文分能高吗?别整那些剑走偏锋的场景,好好走路,我是谢谢熬丙的面试文章分享,同时希望他保持初心、少水文,当然这也是我对自己的要求,谢谢、说了一些胡话,不到之处、小哥哥小姐姐请见谅,爱你们♥
源码对象分配: https://juejin.im/post/6844903876194205710 c++ 挺详细的
对象创建方式
- User user= new User();//首先方法区常量池查是否有类名符号引用、类加载信息:被加载和解析过,无则加载类,实例分配内存,调用构造函数,初始化成员字段,对象指向分配的内存空间,非原子操作,可能先指向空间再调构造函数
- object类的clone:jvm创建新对象,可用看成是深度拷贝(想起了go的拷贝);这个不会调构造函数,是内存中二进制流进行拷贝的,重新分配内存块
- 反序列化:实现Serializable接口然后借助流,涉及到IO 有些开销的,要注意;parcelable是android内存序列化支持
- 反射:拿到类对象Class,通过反射创建类实例对象
获取Class三种方式:类.class ,对象.getClass,Class.forName("类全路径")
获实例对象两种方式:
Class.newInstance(将调用无参构造函数)
调用类对象构造方法:
getConstructors获取对象所有可见构造函数
getConstructors(Class…paramTypes)获取指定的构造函数
- 使用unsafe:只要类对象即可unsafe.allocateInstance(ClassName.class);
接下来分配内存,这个上面也写了这里也是结尾处所以不写了
内存分配好了之后初始化为零值(go也有零值),之后存信息,比如这个对象是哪个类的,怎样获取元数据、hashcode、GC分代年龄等,是的、这些在对象头中;
init方法完成对象初始化,这样就建了一个对象
其实这个粒度还是有点粗的,或者说近底层了一些,本来就是底层的knowledge,java层面或着说spring的……貌似又要欠下几篇文章了,惭愧~
对象访问方式
句柄
堆中句柄池,栈中引用存储句柄地址,句柄存储对象实例数据和类型数据:下面这个图很常见了、是吧
直接指针
这两个的优缺还是很明显,简单说对于句柄,栈引用的是地址,对象移动时只修改句柄地址即可,但是两次指针定位、比较慢;对于指针,一次定位即可访问速度快,当然这也是他的缺点,同时需要考虑如何存放类型数据等信息,指针时HotSpot的选择
参考:
https://juejin.im/post/6844903905323646990 创建类
《深入理解Java虚拟机:JVM高级特性与最佳实践》