类的生命周期(类的加载过程)
每个class文件加载到内存到卸载,都是经历过jvm的类的生命周期是加载、连接(验证、准备、解析)、初始化、使用、卸载;
简单介绍后续深入了解!!!
HotSpot虚拟机对象如何被创建的?
java程序中创建对象是通过new出来的,而虚拟机这会就会接收到一条new的指令后,先去检查这个指针是否能够在常量池中找着一个类的符号引用,或找着这个符号引用,则代表这个类经历了加载、解析和初始化过,如果没有那就要执行类的加载过程。当然加载检查全部通过后,虚拟机会为这个类分配内存,当这个类中的所有对象所需的内存大小被加载后就已确认。
内存是如何分配的?
当类加载完成后,这时已经完全确认内存后,分配内存有两种方式:指针碰撞(Bump The Pointer)和维护一张空闲列表(Free List);
指针碰撞(Bump The Pointer):
假设jvm内存是规整(连续意思)的。堆内存被一个指针一分为二。指针的左边是使用过的,内里存放着对象,右边则是空闲的,而指针的作用是当每次新创建对象向右边移动一个对象的size的距离,这种叫做指针碰撞。
个人理解:堆就像连续的一块耕地,左边种上大豆苗子(对象),右边还是空的,当有新的苗子运来的时候接着开始从右边靠近左边空的继续种上,直接种空。(不知道生动不~)
空闲列表(Free List):
假设jvm内存不是规整(连续意思)的,已被使用与空间的内存交错在一起,这时候就无法通过指针碰撞进行分配内存,那就必须维护一张空闲列表,这张表存放着所有空间的地址,当新创建一个对象的时候,需要多少的空间就从表里进行寻找到一位合适的空闲空间进行分配。(当然有大有小,如果大了剩余的是需要被虚拟机回收的,这块jvm对应一套自己的处理机制,这里不赘述,有兴趣可以更深入了解)。
个人理解:这个空闲列表就像有一整块地,有几十亩(内存),有些地房盖上了房子(已用),有些还是空的(未用)。那这时候空的地会被记录在案,当某一天,哪位有钱人家想块地进行盖房子时,就先从这个记录在案里面找一块,然后才进盖房子(创建对象)。
jvm具体选择是空闲列表,还是指针碰撞方式进行内存分配,是根据java堆是否规整来决定,而java堆是否规整又是由垃圾上集器是否带空间压缩整理来决定。(后续垃圾算法详细深入)。
Serial、ParNew等带压缩整理过程收集器采用:指针碰撞。
CMS基于标记清除算法采用:空间列表。
频繁内存分配怎么保证线程安全?
内存分配解决了,但是jvm在分配的时候频繁修改一个指针所指向的位置,在并发情况不是线程安全的,那jvm是通过同步处理和本地线程分配缓冲解决该问题。
同步处理:虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性。
本地线程分配缓冲:每个线程在java堆中预先分配一小块内存,称为地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,这块内存分配时线程独占的,读取、使用、回收是线程共享,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。由于TLAB空间一般不会很大,因此大对象无法在TLAB上进行分配,总是会直接分配在堆上。TLAB空间由于比较小,因此很容易装满。比如,一个100K的空间,已经使用了80KB,当需要再分配一个30KB的对象时,肯定就无能为力了。这时虚拟机会有两种选择,第一,废弃当前TLAB,这样就会浪费20KB空间;第二,将这30KB的对象直接分配在堆上,保留当前的TLAB,这样可以希望将来有小于20KB的对象分配请求可以直接使用这块空间。
jvm设定TLAB通过:-XX:+/-UseTLAB
比如:使用本地缓存区
-XX:+UseTLAB
注意:
jvm默认采用本地线程分配缓冲来进行解决线程安全问题。
推荐使用虚拟机默认配置,不建议修改TLAB配置。
两种优缺点对比:
名称 | 同步处理 | 本地线程分配缓冲 |
性能 | 较低 | 高效 |
占用空间 | 不会 | 会根据运行情况计算而得或配置 |
最后
简单介绍关于对象被创建的相关流程和条件及内存是如何分配的,这块在面试中倒是经常遇到,希望本文对你有帮助,下文继续关于对象的内存是如何布局的。
参考文章:
https://www.zhihu.com/question/56538259