虚拟机对象解密

二:虚拟机对象解密

1.对象的创建

  1. 当遇到一条new指令时,首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析,初始化过。如果没有,必须先执行类加载过程。
  2. 类加载检查通过后,接下来就该分配内存。对象所需内存的大小在类加载完成后便可完全确定。
  3. 如果堆中内存是绝对规整的,使用过的内存和空闲的内存由中间的指针作为分界点的指示器,分配内存时仅需把指针往空闲空间方向移动一段与对象大小相等的距离,这种分配方式叫"指针碰撞"。
    但如果内存不是规整的,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候找到一块足够大的空间用于分配,并更新列表上的记录,这种分配方式叫"空闲列表"。
    上诉2种分配方式由堆是否规整决定,而堆是否规整又由垃圾收集器是否具有空间压缩的能力决定,党使用Serial,ParNew等带压缩整理的收集器时,采用指针碰撞,使用CMS基于清除时,理论上就只能采用空闲列表。(在CMS,为了能在多数情况下分配得更快,设计了一个分配缓存区,通过空闲列表拿到一大块分配缓冲区后,在它里面仍然可以使用指针碰撞的方式来分配)
  4. 对象创建分配内存修改指针的位置在并发情况下也并不是线程安全的,有2种解决方案。1.采用CAS配上失败重试的方式保证更新操作的原子性(这是一种乐观锁的实现)。2.每个线程在堆中预先分配一小块内存,称为本地线程分配缓存(TLAB),哪个线程需要分配内存,就在本地缓存区分配,只有本地缓存区用完了,分配新的缓存区才需要同步锁定。可以使用-XX:+/-UseTLAB来设定。
  5. 内存分配完毕时后,需要将这些空间都初始化为零值(除了对象头),使用TLAB,可以把该过程提前到分配时顺便进行,这步操作保证了对象的实例字段在Java代码中不赋初始值就可以直接使用。
  6. 对象头保存的信息有这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码(实际上会延后到真正调用Object::hashCode方法时才计算),对象的GC分代年龄等信息。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
  7. -----从虚拟机的视角看,一个新的对象已经产生了,但是从Java程序的视角来看,对象创建才刚刚开始---------
  8. 构造函数,即Class文件中的方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说(由new指令后面是否跟随invokespecial指令所决定,编译器遇到new关键字的地方会同时生成这2条指令,但如果直接通过其他方式产生的则不一定如此),new指令之后接着执行方法,对对象进行初始化,这样就构造了一个真正可用的对象。(invokespecial的作用:用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态派发。)

2.对象的内存布局

  1. 对象在堆内存的存储布局可以分为:对象头,实例数据和对齐填充。
  2. 对象头包含两类信息:1.用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。32位虚拟机为32比特,64位为64比特,官方称为Mark Word。对象需要储存的运行时数据其实已经超过了32,64比特的最大限度,但对象头里的信息是与对象自身定义数据无关的额外储存成本,Mark Word被设计成了动态定义的数据结构,以便储存尽量多的数据,根据对象的状态复用自己的储存空间。2.类型指针,即对象指向它的类型元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例。(不一定必须保留类型指针,因为可以通过句柄访问对象的元数据信息)Ps:如果对象时一个数组,那在对象头还必须有一块记录数组长度的数据。因为虚拟机一颗通过对象的元数据信息确定Java对象的大小,但是如果长度是不确定的,则无法推断。
  3. 实例数据是对象真正储存的有效信息(即定义的各种类型的字段内容,包括父类继承和子类定义的字段),相同宽度的字段总是被分配到一起存放,在满足这个前提条件下,在父类中定义的变量会出现在子类之前,如果+XX:CompactFields为true(默认为true),子类中较窄的变量也允许插入父类变量的空隙之中。
  4. 第三部分是对齐填充,这不是必然存在的,也没有特别的含义,任何对象的大小都必须是8字节的整数倍,对象头已经设计好了,因此,如果实例数据部分没有对齐,就需要占位符。

3.对象的访问定位

  1. Java程序会通过栈上的reference数据来操作堆上的具体对象,主流的访问方式主要有句柄和直接指针两种:1.句柄访问:reference储存的是对象的对象的句柄地址,而句柄中包含对象实例数据和类型数据各自具体的地址信息2.直接指针访问:reference中储存的直接就是对象地址,可以节省一次访问的开销。
  2. 句柄优势:在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要被修改。 直接指针优势:速度更快,HotSpot主要使用该方式进行对象访问。

4.OOM

  1. 常规的处理办法是通过内存映像分析工具对Dump出来的堆转储快照进行分析,第一步确定导致OOM的对象是否是必要的(也就是内存卸载还是内存溢出)
  2. 如果是内存泄露,可进一步查看对象到GC Roots的引用链,找到泄露对象的引用路径,与哪些GC Roots相连,才导致无法回收。
  3. 如果不是内存泄露,那就应该检查虚拟机的堆参数,与机器的内存对比,看看是否有调整的空间,再检查代码看是否有可以优化的地方。
  4. 1.如果线程请求的栈深度大于允许的最大深度,将抛出StackOverflowError。2.如果栈内存允许动态扩展,当无法申请到足够的内存时,将抛出OOM。
  5. 虚拟机实现可以自行选择是否支持栈的动态扩展,而HotSpot不支持,所以除非创建线程一开始就无法获得足够内存导致OOM,只会因为栈容量无法容纳新的栈帧导致StackOverflowError。
  6. 若在多线程情况下,不支持动态拓展栈的虚拟机(HotSpot)也可以产生OOM异常,但是这和栈空间没有任何关系,主要取决于操作系统本身的内存使用状态,这种情况下,每个线程分配的栈越大,反而越容易出现OOM。
  7. String::intern是一个本地方法,如果字符串常量池中已经有一个该字符串,则返回这个字符串的引用。否则会将该字符串添加到常量池中,并返回引用。
  8. .使用DirectByteBuffer类直接通过反射获取Unsafe实例进行内存分配(只有引导类加载器才会返回实例),使用该类分配内存也会抛出OOM,但他并没有真正想操作系统申请分配内存,而是通过计算得知无法分配在代码手动抛出。
  9. 由直接内存导致的内存溢出,一个明显特征是Heap Dump不会看见有什么异常的情况,文件很小,程序中又直接或间接使用了DirectMemory(NIO),就该考虑直接内存方面的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值