二、jvm对象创建及内存分配

目录

类加载

内存分配

对象结构

对象分配

垃圾对象判断

finalize的作用


类加载

当我们去new Object()的时候,首先先判断当前的类的父类是否被加载过,如果没有则先加载父类。然后再加载当前类。将类的信息放入元空间

1、先加载父类

2、再加载本类

而类的加载分为以下几步:

  1. 加载:就是将类的信息加载到元空间,将静态常量池变成运行时常量池,同时在元空间生成c++语言的class对象,同时在堆空间生成一个java语音的class对象
  2. 链接
    1. 验证。校验当前类文件是否符合java规范
    2. 准备。为静态变量开辟空间,并赋予默认值。
    3. 解析。将符号引用变为地址引用。这块后续再说
  3. 初始化:开始按照代码声明的顺序为静态变量赋值,同时执行static代码块

内存分配

一般来讲在jvm中,创建对象时,会在堆中分配空间,而分配方式有两种:

  1. 指针碰撞:在内存非常整洁的时候,采用这种,即有一个指针在记录最后一个地址,当需要分配空间的时候,将指针想后移动。这种分配方式适用于标记整理、标记复制的gc垃圾器。因为这种gc之后,内存基本上都是一块正片的。
  2. 内存分配表:这种分配方式适合存在垃圾碎片的。即利用空闲列表来记录哪一块内存空闲,那么就可以直接分配。

不管是那种内存分配方式,都会存在并发的问题,例如有多个线程同时new对象,那么就会有并发的问题。那么如何解决呢?

  1. cas:这个就相当于每次来了就会进去cas分配
  2. TLAB:在eden区,为每个线程都单独分配一块空间,每个线程都在自己的TLAB空间内分配空间。但是这个TLAB的空间不会太大,默认是eden区的1%,可以通过-XX:TLABSize来配置。如何TLAB的空间放不下了,那么就恢复成CAS的方式,去向公共空间分配。

对象结构

一个java对象的结构大体分为三个部分:

  1. markword:对象头(32位、64位)。根据当前对象的锁状态不同,markword的每个bit位的标志是不一样的
    1. 有上面的图看出来,利用3个bit表示锁信息。其中1个bit表示是否偏向,2个bit表示锁级别。
      1. 01-无锁。当01-无锁的时候,需要根据是否偏向也确定当前的对象的锁状态,并且记录线程id。
      2. 00-轻量级。00-轻量级的时候,记录栈中锁记录的地址。
      3. 10重量级。10-重量级的时候,指向互斥量monitor对象的地址。
  2. klass point:(4B或者8B)指向当前对象对应的类元信息(在方法区)。之所以有4B和8B主要是因为是否开启了指针压缩。-XX:+UseCompressedOops【开启压缩,默认】 -XX:-UseCompressedOops【禁止压缩】
    1. 解释一下什么是指针压缩,为什么要压缩。正常我们用4个B去表示klasspointer,那么4个B对应的是32位,32位正好是4Gb,也就是说如果我们jvm的堆空间在4G以内,那么4B正好能够完全覆盖
    2. 但是如果堆内存大于4G,我们就需要33位或者34位,甚至35位。那么我们用4B就不能完全表示了,那么就需要用8B去表示。
    3. 假如我们用8B去表示,那么对象的空间就会严重放大,jvm成千上万的对象都会膨胀。因此我们可以采用算法,用4B来表示大于4G的空间,这个就叫压缩。
    4. 但是压缩一定是有失真的,如果我们强制用4B表示100T的空间,那一定会有问题。因此,如果我们使用指针压缩,那么堆空间不易过大,最好不要超过32G。
    5. 也就是说当我们堆空间比较大的时候,不要开启指针压缩,因为压缩之后,失真了,会有问题。
  3. 数组长度:只有数组对象才会有
  4. 实例数据:主要是存储当前对象中成员变量,例如如果有一个int类型的的成员变量a,那么将会分配4个字节。如果有一个long类型的变量b,那么会分配8个字节。如果有一个object类型的对象,则分配一个4字节
  5. 补齐空间。如果当前对象的所占空间不是8个的倍数,那么就进行空间补齐。这块和内存页加载页有关系。

如果想要查看当前对象的头数据,可以引入jol包 

对象分配

对象的分配是有具体步骤的:

  1. 首先尝试在栈上分配,这样空间就会随着线程的回收而回收,但是不一定所有的对象都需要再栈上分配,是有前提条件的:
    1. 逃逸分析。主要判断当前对象是否逃出了函数的作用域,如果没有逃出,那么可以进行栈上分配。jvm默认开了逃逸分析
    2. 标量替换。所谓标量替换指的是,但是在栈上分配对象的时候,栈上没有足够的整块的空间,而是碎片空间,因为可以将一个对象打散,存放在不同的位置上。
  2. 判断当前对象是否足够大【可以用一个参数-XX:PretenureSizeThreshold】,如果超过了这个阈值,则直接放入老年代。但是这个参数只有在ParNew回收器有效
  3. 如果没有超过,则首先在TLAB上进行分配
  4. 如果TLAB上没有足够的空间则在eden区其他空间进行分配,此时就需要cas来进行控制并发
  5. 如果eden不够,则发生ygc。当对象的年龄超过15,则需要晋升到old区,当然这个15是可以通过参数-XX:MaxTenuringThreshold来设置,但是最大也就是15,因为对象头的markword中只通过4个bit来存储
  6. 上述ygc的时候会有一个【动态年龄】的问题。所谓的动态年龄是指,当eden区向s区copy的时候,将这批对象按照年龄由低到高排序,然后从age1+age2+……+agen的空间大于s区的50%,则age-n以及大于age-n的对象将被送往老年代。
    1. 因此这块有一个问题,比如我们设置s1的空间为100m,如果有60m的对象进入s区,那么可能导致这批对象直接进入老年代,下次ygc就不会被回收,那么久而久之就会导致fullgc。
    2. 因此如果我们调整一下s1的空间为150m,那么就不会进入老年代,那么效率就十分高了
  7. 老年代担保。【其实就是提前计算,防止动态年龄导致fullgc,不如提前fullgc】
    1. 当执行ygc之前,jvm有一个判断,校验当前old区的剩余空间是否能容得下eden的当前所有对象(包含垃圾)。
    2. 如果说能容得下,那么就正常走ygc。这块主要是怕如果直接ygc,然后触发了动态年龄,但是老年代又不够,那么就fullgc了吗。所以jvm会提前判断 ,只有在old区够用的情况下才正常ygc,
    3. 如果容不下,会判断历史的平均【eden区ygc之后剩余的对象容量】是否能被old区容下, 如果能容下,则直接ygc,如果容不下,则直接fullgc。  

垃圾对象判断

第一种是引用计数,这个基本没啥用,python好像用了。

第二种是可达性分析。这块主要是从gcroot开始进行遍历。但是这快设计到ygc和fullgc,不同的实现方式,里面涉及到card表。

  1. 栈中本地变量引用的对象
  2. class的静态变量引用的对象
  3. 本地方法栈引用的对象
  4. 还有一些锁对象

finalize的作用

当gc发生的时候,对象如果被标记为了垃圾,则判断当前对象是否复写了finalize,如果没有复写,则直接清除,如果复写了,则将对象放入到一个队列中,然后又gc线程去执行队列中的对象的finalize方法。如果在finalize方法中,当前对象又和非垃圾对象关联上,则当前对象复活。

Klass的回收

klass的回收指的是回收方法区中的klass信息。那么什么情况下一个klass应该被回收呢?

  1. 首先该klass的所有对象实例都被回收掉。
  2. 当前klass的classloader也被回收了。
  3. 当前klass的class对象也有被引用了
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值