JVM-对象实例化与直接内存

一、java中创建对象的方法

  • new对象的方法
    主要有:直接new,工厂类、创建者类的获取对象方法。
  • Class类的getInstance(); 只可以创建无参public构造
    Constructor类的getInstance() 无参有参构造器都能使用,且private也可以使用
  • 第三方类库提供对象
  • clone(),Object类的clone()
  • 反序列化: 从网络、文件等二进制流中读取对象信息进行对象创建。

【指针引用和其他大小小于等于32位的数据类型一样【char,byte,short,int,float,boolean】,都占据4字节空间】

二、字节码分析对象创建

一句简单的new对象的代码:

Demo demo = new Demo();

对应的字节码:
在这里插入图片描述
一共三步操作:

  • new:
    会首先检查这个 Class有没有加载,即加载、链接、初始化?并按照编译中的大小信息分配空间,进行创建对象 ,并对其临时初始化
  • dup:
    第一句new会在操作数栈中生成一个指向该对象的引用,dup指令会将这个引用再复制一遍,放到操作数栈的栈顶。上面那个引用是作为一个句柄指向方法区的对应方法。
  • invokespecial :
    进行真实初始化,为其赋实际的初始值【即调用构造器方法<init>

<init>与<clinit>的区别:
前者是一个类的构造器在字节码中对应的方法;
后者习惯被称为“类构造器方法”’【注意断句,类构造/器/方法,即构造类信息的东西】。他会在类加载的初始化阶段对类的静态部分进行初始化【如静态代码块,静态成员变量等】

小问题:
对象到底是在 new 的时候就创建了还是在 Person()【调用构造方法】时创建的?
答:看从什么角度考虑。
以涅糖人为比喻。new的动作就好像用面粉捏了一个白面人,只有形状没有任何装饰。
Person()的动作就好像为唐人加上了装饰,表情,即为内部的变量进行赋值。
通常意义上说的,应该还是在Person()对象初始化之后对象创建完成。

三、JVM创建对象的步骤

在这里插入图片描述
1.检查类是否有了,没有就去加载这个类【加载类元信息,类的元数据(元数据并不是类的Class对象。Class对象是加载的最终产品,存放在堆区。类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)才是存在方法区的。】
在这里插入图片描述

  1. 内存是否规整
    如果在内存回收时,因为不同区域的对象被回收,导致内存空间的碎片化,会使得可能存不下大的对象,因此内存规整处理很重要。
    在这里插入图片描述
    有两类垃圾回收器:压缩回收算法【Serial,ParNew。回收之后内存空间是规整的】,标记清除算法【CMS为代表,哪个对象不需要就直接原地释放,因此空间是不规整的】。
  • 回收压缩算法的空间采用指针碰撞法【在当前非空闲空间的尾部维护一个指针,当需要存放对象时,从指针所指区域开始分配,分配完毕后指针到达新的非空闲末尾。】
  • 标记清除算法使用空闲列表分配【维护一个列表,存储堆中所有空闲区域的起始位置和大小,每次需要空间时从表中选取一块分配】
    在这里插入图片描述
  1. 处理并发问题
    堆空间是共享区域,可能某一块空闲的空间会同时被几个线程申请创建对象,因此需要机制处理并发问题。
  • 共享堆区的并发控制
    (1)CAS失败重试
    CAS:Compare And Swap,比较与交换,一般叫做乐观锁。这种加锁的方式是假设该空间不会发生冲突,直接不加锁进行分配,若分配失败,说明确实发生了冲突,就一直重试,继续分配。
    (2)区域更新加锁:在给一个线程分配一块区域之后,在对象创建过程中对这块区域加锁。
  • 私有空间
    TLAB创建对象【ThreadLocal】
  1. 默认初始化
    为了让对象不赋值就可以使用,在对象的new环节,即分配空间并设置好所有field信息之后,对这些域变量进行初始化,初始化值为默认值。

  2. 设置头信息
    将对象的类元信息、hashCode值、GC信息存储在对象头部
    在这里插入图片描述

  3. 显式初始化
    对对象按照类中信息描述的那样进行初始化。


对象初始化初始化顺序为:

默认 -> 显式 -> 静态代码块 -> 构造方法 -> setter

case:演示类中初始化顺序:

/**
 * 初始化顺序
 */
public class Demo2 {

    public int a = 1;
    private static final double b = 1.0;
    String c;
    float d;
    static Demo1 e;
    private static byte f = 2;

    {
        c = "hell0";
    }

    public Demo2() {
        this.d = 1.0f;
    }

    static {
        e = new Demo1();
    }
}

在这里插入图片描述
可以看到,对象初始化分为两个部分:
(1)<init>方法部分。
表示显式初始化过程;
(2)<clinit>
表示静态内容初始化部分。


<init>部分:

 0 aload_0
 1 invokespecial #1 <java/lang/Object.<init>>
 4 aload_0
 5 iconst_1
 6 putfield #7 <com/peng/chapter7/Demo2.a>
 9 aload_0
10 ldc #13 <hell0>
12 putfield #15 <com/peng/chapter7/Demo2.c>
15 aload_0
16 fconst_1
17 putfield #19 <com/peng/chapter7/Demo2.d>
20 return

“行”表示字节码中的行数。
①第6行:a
此为显式赋值内容;
②第12行:c
此为代码块内容
③17行:d
此为构造方法赋值内容。

故非静态赋值顺序为: 显式 -> 代码块 -> 构造方法

<clinit>部分:

 0 iconst_2
 1 putstatic #23 <com/peng/chapter7/Demo2.f>
 4 new #27 <com/peng/chapter7/Demo1>
 7 dup
 8 invokespecial #29 <com/peng/chapter7/Demo1.<init>>
11 putstatic #30 <com/peng/chapter7/Demo2.e>
14 return



①在字节码加载成功之前就已经对常量完成赋值【准备阶段】;
②对静态变量进行赋值;
③对静态代码块进行赋值

四、对象内存布局

在堆区中,一个对象主要有三个部分组成【数组有四个】:

对象头(面试常问) :
(一)运行时元数据

  • 哈希码:用于栈帧中访问该对象的地址标识;
  • GC分代年龄:用于记录当前对象存活年限;
  • 锁状态标志:记录对象所在的这块堆内存区域是否上锁。
  • 对象拥有的锁:记录这个对象占有的资源的锁。
    在这里插入图片描述

(二)类型指针:即访问方法区中类元信息的指针
方法区中主要存放类元信息,而堆中对象需要通过getClass()得到他的类对象,必须参考方法取得类元信息。
因此对象头留有一个指向该类类元信息的指针

(三)填充信息:就像保护花瓶的泡沫,只起到填充空间的作用。


在这里插入图片描述
上图是一个对象创建语句执行流程示意图:Account acct = new Account();
当方法进栈时,开始执行new语句,在堆区中创建对象,并将首地址返还给栈帧,栈帧将引用存放在局部变量表的引用变量那里。
堆中的对象将信息保存在运行时元数据中,并有一个类型指针指向这个类对应的方法区的类元信息。
对象中的字符串会指向堆中的字符串常量池。

五、对象访问

域变量、局部变量如何找到堆中的自己的实例块呢?一共有两种方式:


在这里插入图片描述
(1)句柄访问。
在堆区中单独开辟一片空间,叫做句柄池,存放每个变量引用和对象实例的指针指向。
句柄的作用是进行栈帧引用(reference)到实例地址和类元信息的转换。
句柄使得reference访问对象实例都需要经过句柄的中转,较低效。
在这里插入图片描述
优点:reference稳定【由于GC后堆中对象的首地址需要经常变动,句柄的存在使得只需要堆中句柄变化即可,而栈帧的reference无需改变。同是堆中的数据切换起来也要方便一些。】
确定:额外开辟空间,访问效率低

句柄:
句柄(Handle)是一个是用来标识对象或者项目的标识符,可以用来描述窗体、文件等,值得注意的是句柄不能是常量 [1] 。
Windows之所以要设立句柄,根本上源于内存管理机制的问题,即虚拟地址。简而言之数据的地址需要变动,变动以后就需要有人来记录、管理变动,因此系统用句柄来记载数据地址的变更。在程序设计中,句柄是一种特殊的智能指针,当一个应用程序要引用其他系统(如数据库、操作系统)所管理的内存块或对象时,就要使用句柄 [1] 。

看了半天,我理解到句柄是一个高级指针,用来智能的监控其所指内存单元的地址变动并随着改变指向。


直接访问【HotSpot采用】:
reference直接指向堆中的对象实例,且实例中也保存着方法区类元信息的引用。
这样,访问对象只需要一步,访问方法区可能还需要两步。但是总体来说非常高效,因此被HotSpot采用。
在这里插入图片描述
优点:不用开辟额外空间,访问效率高
确定:reference不稳定,要常做修改。

六、直接内存

JVM直接向系统申请的内存区域 ,又称为堆外内存【即jvm中不属于堆区的内存区域】。
堆外内存通常比堆内存的IO速度更快,主要是因为堆内存的读取需要经过堆内存到操作系统内存的映射,由操作系统内核态处理IO
而堆外内存的读取直接与操作系统内核交互,少了中间模块【没有中间商赚差价】
在这里插入图片描述
JVM通过NIO中的DirectByteBuffer类获取堆外内存。

NIO, New IO 或者 Non-Blocked IO。非阻塞式IO。
传统IO是基于流的,即数据会像流水一样流过,我们必须拿个桶接着才能读取数据,而char[]或者byte[]就好像水瓢,可以一次多装些水。
NIO是基于缓冲区的。就好像在你家旁边修一个水库,每过一段时间都会有卡车往水库那里送水,你也可以高效的从水库取得水。
详细看这篇

阻塞与非阻塞
阻塞是等待数据传入之后再进行io,没有数据传输进入就一直等待数据传输,比较耗费系统资源。
在这里插入图片描述


申请1g堆外内存,查看系统内存占用:

public static void main(String[] args) {
        ByteBuffer bf = ByteBuffer.allocateDirect(1024 * 1024 * 1024);
        Scanner sc = new Scanner(System.in);
        sc.nextLine();
        System.out.println("开始执行内存释放!");
        System.gc();
        sc.nextLine();
        System.out.println("任务休了!");
    }

任务管理器的信息:
释放前:
在这里插入图片描述

释放后:
在这里插入图片描述


为什么直接内存访问速度高于堆内存?
在这里插入图片描述


堆外内存也会出现OOM。
OOM的内容有所不同。
直接内存的空间不足不那么容易察觉,往往经过大量排查。
主要是因为JVM检测软件都不能监控直接内存变化,这是因为JVM的系统快照【每隔一段会记录JVM系统中的数据,称为dump】不会记录堆外内存信息,导致很难发现直接内存的空间异常。
在这里插入图片描述


设置最大直接内存:-XX:MaxDirectMemorySize=_M
默认大小与-Xmx一致,即与最大堆空间一致。


java内存的主流看法:
java process memory = JVM heap + Native Memory【剩下的空间都不怎么大,所以被忽略了】
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值