java垃圾回收及对象创建

安全点、安全区域

JVM垃圾回收线程工作的时候往往需要stop the world(stw),即停止正在运行中用户线程。因为JVM垃圾回收是基于可达性分析,可达性分析第一步首先就是要找到能标识对象仍可到达的GC Root,包括(方法栈中的局部变量,方法区的静态变量、常量池中的引用、本地方法中的引用等),如果用户线程一直在运行的话,GC root集合可能不停在变化,因此必须使得用户线程能够暂时停止改变GC root集合。

为了实现这个目的,JVM在启动垃圾回收之后会通知所有的用户线程进去安全点或安全区域。如果直接让线程停止执行则需要产生大量的中断,JVM采取的方式是让用户线程继续运行,一直运行到安全区域之后在停止。
用户线程可能处于阻塞状态也可能正在运行中。阻塞中的线程并不会改变GC root,因此继续阻塞即可。运行中的线程,可能正在执行本地方法、执行字节码,也有可能正在执行即时编译器编译后的机器指令。
对于本地方法,在进入之前可以判断本地方法是否会对JVM中的GC root产生影响,例如是否会调用堆中的对象、返回结果是否被JAVA对象使用等。

对于执行字节码,JVM可以控制字节码的执行,字节码与字节码之间均为安全区域,当有安全点请求之后,JVM会对执行的每一条字节码进行安全点检查。

对于编译后的机器指令,JVM无法控制,因此编译后的机器指令中都会带有安全点检测的代码。

安全点检测毕竟是耗时的,因此不能每一条指令都进行安全点检测,但是也不能将安全点设置的过远,那样会导致长时间无法进入安全区域,从而使STW时间进一步拉长。

卡表

JVM对新生代的回收都是采用复制算法,但是新生代对象有可能被老年代对象引用,那难道回收新生代也需要将老年代对象作为GC root吗?这样的话数据量太大了,而且新生代回收很频繁。JVM采用了卡表的形式,将老年代区域划分成一个个单独的区域,例如512字节一个区域,如果区域中有对象引用了新生代对象,则将卡表上对应的标识设置为脏表。这样新生代垃圾回收的时候,只需要查看卡表变脏的那些区域中的老年代对象即可。

卡表虽然能够减少新生代回收时老年代需要遍历的对象数量,但是当新生代对象的引用发生变化时都需要更新老年代对应的卡表位置。对于解释执行,比较简单,因为字节码执行完全由JVM控制,但是即时编译器编译后的机器指令JVM则无法控制。因此编译后的指令在每次引用变化之后都会有一个写屏障来进行卡表更新。为了使写屏障的内容尽量简单,写屏障并不会判断老年代对象的引用实际是否真的指向新生代。

同时卡表由于是使用字节数组实现的,存在虚共享问题。例如一个卡表节点占用一个字节,但是CPU的缓存行有64个字节,那么CPU一次加载便会加载64个卡点,写回时也是64个卡点一起写回。假如有两个线程分别修改的两个卡点位于同一个缓存行中,为了保证线程安全,两次操作就无法并发进行,只能等一个线程写回之后另一个线程再重新读取、更改然后写回。

线程分配对象

由于堆是共享区域,因此多个线程同时使用时便会出现线程安全问题,创建对象是其中很重要的一步。由于新生代的垃圾回收算法都是复制算法,不存在内存碎片,因此创建对象可以采用指针碰撞的方式。创建一个对象只需要将指针从初始区域向后移动指定大小即可。假如多个线程同时移动一个指针就会出现线程安全问题。

为了解决创建对象的线程安全问题,JVM为每一个线程提前分配好了缓冲区,线程创建对象时先在各自的缓冲区中进行创建,当缓冲区用完之后再重新申请。由于不同线程的缓冲区指向不同的区域,因此不会有线程安全问题。这种方式被称为Thread Local Allocation Buffer(TLAB)

JVM压缩指针

java的一个引用长度是4个字节,即32位,理论上来说,假如使用4个字节的指针来表示地址的话,最大寻址空间为232个字节,即4GB。但是倘若约定每个java对象的起始地址都必须是8的倍数,那我们就可以将指针的每一位表示8个字节中是否有值,下一位则表示上一位地址+8字节。这样每次拿到一个指针的时候,只需要将指针的地址*8,就能得到对象的地址;这样的话可寻址空间就相较于原来扩大了8倍,即最大寻址空间为235个字节,32GB。这种指针表示方式被称为压缩指针。采用压缩指针可以让指针的长度缩短,原本需要35位表示的地址,现在只需要32位即可。但是同样的要求所有对象存放的起始位置都必须是8的倍数,势必导致一部分空间的浪费。

java对象的空间占用包括如下几部分:1. 对象头(压缩指针指向元数据、标识信息(锁标识、哈希吗)、数组长度)2.对象内容,3.对齐填充。其中最后一部分的对齐填充就是为了保证java对象的起始位置都必须是8的倍数而设立的。

字段重排列

为了满足java对象的大小必须是8的倍数,JVM对对象的字段数据也进行了对齐要求。所有字段的位置必须满足以下两个条件

  1. 字段长度是N,那么字段在对象中的起始位置与对象起始位置的差值必须是N的倍数
  2. 子类中从父类继承而来的字段的排列必须与父类中一致
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值