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