文章目录
- 对象的创建过程
- 对象的组成
- 解析普通对象
- **结果分析:**
- 给对象添加属性
- 注意事项
- 补充
- jvm压缩指针
- 栗子:
- 对象头包含什么
- 对象怎么定位?
- **句柄方式和直接引用的优缺点:**
- 对象怎么分配?
- 为什么hotspot不使用c++对象来代表java对象?
- Class对象是在堆还是在方法区?
- DCL要不要加volatile问题
- DCL
- 指令重排
- volatile
来源于:B站马士兵的jvm视频总结!!
1、解释一下对象的创建过程?(半初始化)
2、DCL要不要加volatile问题?(指令重排)
3、对象在内存中的存储布局?(对象与数组的存储不同)
4、对象头具体包含什么?
5、对象怎么定位?(直接、间接)
6、对象怎么分配?(栈上-线程本地-Eden-Old)
7、Object 0 = new Object()在内存中占多少个字节
8、为什么hotspot不使用c++对象来代表java对象?
9、Class对象是在堆还是在方法区?
对象的创建过程
当执行 Object o = new Object();
这行代码时,一个新的 Object
对象会被实例化,而这个对象实例会存储在堆内存中。Object o
实际上是一个引用变量,它存储在栈内存中,指向堆内存中的那个 Object
对象。通过这种方式,我们可以通过引用变量 o
来访问和操作堆中的 Object
对象。当 o
超出其作用域或被重新赋值时,堆中的 Object
对象仍然存在,直到垃圾收集器回收不再使用的对象。
new Object()
:这是一个对象的实例化过程。new
关键字用于在内存中分配空间来存储新的对象。Object()
是一个构造函数调用,用于初始化Object
类的新实例。- 对象的内存分配:
new Object()
表示在Java堆中分配了一块内存空间,用于存储一个新的Object
对象。 - 构造函数的执行:一旦分配了内存空间,就会调用
Object
类的构造函数。在这种情况下,Object
类是Java中所有类的基类,它的构造函数是一个无参构造函数,因此没有任何参数传递。 - 初始化对象:在构造函数执行完毕后,对象被初始化为
Object
类的实例。此时,Object o
引用变量被赋值为指向这个新创建的对象。
注意事项:
在实际开发中,我们通常不会直接使用
Object
类的实例,而是创建自定义类,然后实例化自定义类的对象来进行具体的业务操作。Object o = new Object();
这种形式通常在示例代码或一些基础概念演示中使用。当刚new出来一个对象时,所有成员变量都有一个默认值,只有当调用构造方法时才会赋值;例子:
public class test { int n = 8; }
- 申请空间,给默认值0,而不是8,
- 调用构造方法赋值
- 引用,建立关联
对象的组成
1、markword:
8字节
2、class pointer(类型指针):
4字节
意思是当你new的是什么对象时,它指向的就是什么.class,规定一个对象具体是什么对象
例如:
new User(),那它指向的就是User.class
3、instance data(实例数据)
放入一个对象的成员变量,比如:
当User()对象里有一个属性是String name;时,那么这个name这个属性就是放在这里面
4、padding(对齐)
就是为了让数据对齐,假如前面那3个部分加起来不能被8字节整除,则自动补充尾数,直到能被8整除
为什么是8字节呢?因为8字节是64个bit,我们虚拟机或者说电脑来说就是64位,所以一次性存储64位对它来说是最方便的
比如我装货卸货的时候必须是一次64个,那当我的货只有63个时,我将补充一个空的货
解析普通对象
1、我们采用JOL(Java Object Layout)
类库来解析对象,依赖:
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
2、使用:
package com.example.demo.web;
import org.openjdk.jol.info.ClassLayout;
/**
* com.example.demo.web
*
* @version Id: test, v 0.1 2023/7/14 14:19 Exp $
*/
public class test {
private static class T{
}
public static void main(String[] args) {
T t = new T();
System.out.println(ClassLayout.parseInstance(t).toPrintable());
}
}
3、运行结果:
结果分析:
可以看到第一行和第二行都是4个字节,而且代表的是对象头,前面我们说markword
是由8个字节组成,那么第一行和第二行加起来就是代表markword
而第三行代表的是class pointer(类型指针)
,所以可以通过value的4个字节找到T.class
第四行显示loss due to the next object alignment
代表的是补齐4个字节,让8整除
所以可以得出
Object 0 = new Object()
一共占用16个字节,也就解决了第7个问题
给对象添加属性
我们给对象添加一个int a
,那么这个对象占多少个字节呢?
private static class T{
int a;
}
其实很好计算,int类型在java中就占4个字节,所以我们的执行结果是:
这里的第4行就不是补齐了,是instance data(实例数据)
因为加起来正好16个字节,可以被8整除,所以没有补齐这一部分
Java中8大数据类型对应的字节大小:
类型 | 字节 | 位数 |
---|---|---|
byte | 1 | 8 |
short | 2 | 16 |
int | 4 | 32 |
long | 8 | 64 |
float | 4 | 32 |
double | 8 | 64 |
char | 2 | 16 |
boolean | 1(通常是1个) | 8 |
String | 4 | 64 |
注意事项
boolean
类型虽然只占1个字节,但因为在jvm中会把它转换为int类型,所以它会补齐3个,所以也是占4个字节,byte
和short
同理;这重情况叫做内部对齐- String字段填写值,比如在T对象中添加属性
String s = "hello"
,它也是占4个字节,因为hello是存在字符串常量池中的,和对象本身是没关系的,只是可以通过这4个字节在常量池中找到对应字符
补充
jvm的指针长度是多少,由什么来决定呢?
长度的意思也就是说电脑的寻址能力有多强,能有多少中bit组合。每种组合就代表一个地址
由电脑的位数来决定:
随着电脑的运行内存的增大,就必须由高位的操作系统才能支持,比如32位的电脑,最大支持的运行内存就是2的32次方,也就是4G
那为什么String的指针只有4个字节呢?也就是32位呢?
因为使用了压缩指针,因为大多数的机器,用32位已经绰绰有余,就是为了省空间
jvm压缩指针
我们可以看到java启动参数有一个-XX:+UseCompressedClassPointers
意思是启用压缩类指针,一般我们启动jar包时都会携带这个参数;(默认)
如果没有,普通类不是由4个部分组成吗!那么它其中的class pointer类型指针
,就是8个字节了,而不是4个字节
以及-XX:+UseCompressedOops
,oops指的是:ordinary object pointers (普通对象指针)一样进行了压缩,也就是String这样的对象
栗子:
1、把压缩指针关掉:
把+改成-即可
2、运行结果:
可以看到第1、2、3、4行全是Object header,第1、2代表的是markword,第3、4行就代表的是类型指针了,多了4字节!
对象头包含什么
由上面我们可以得知:对象头包含一个markword
和class pointer类型指针
;
class pointer在上面已经说过,就是地址,比较简单,所以一般这样问的话就是问markword中有什么?
回收时需使用三色标记,当前 处于什么颜色就存在markword中的GC标记信息中,以及分代年龄
我们可以看到markword中还存着hashCode,当你调用hashCode时才会存储,不然为null,这样设计的目的就是,调用一次hashCode之后,这个hashCode是不变的,下次再调用就是直接拿markword中的值了,不需要重新计算;
1.hashcode调用前后对比:
2.锁前后对比:
发现上完锁之后,markword信息发生了变化,所以锁信息是存放在markword中的
到这里第3、4、7个问题得到了解决!!!!
对象怎么定位?
两种方式:
句柄方式(Handle-Based Approach
)是一种内存管理方式,用于处理对象在内存中的定位和访问。在句柄方式中,对象的引用由两部分组成:句柄和实际对象。
具体来说,句柄方式中有两级间接寻址:
- 句柄(Handle):每个对象的引用变量并不直接指向对象本身,而是指向一个句柄对象。句柄对象是一个固定大小的数据结构,其中包含了实际对象的地址(或者称为指针)和其他元数据信息。实际引用变量中存储的是指向句柄对象的引用。
- 实际对象:句柄对象中包含了指向实际对象的地址。通过这个地址,可以访问到实际的对象数据。
通过引入句柄对象,Java虚拟机可以更好地管理内存。由于句柄对象是固定大小的,它们可以在堆中更容易地进行移动和重新分配。而实际对象的地址则可以在句柄中进行更新,而无需修改引用变量的值。
Java曾经采用句柄方式作为一种内存管理方式,但从
JDK 1.4
版本开始,大部分的Java虚拟机实现转向使用直接引用的方式,即直接将引用变量指向实际对象,避免了句柄对象的额外开销,提高了访问效率。目前,大多数Java虚拟机都不再使用句柄方式。句柄方式在Java之外的其他编程语言和部分特定的嵌入式系统中可能仍然有应用。
句柄方式和直接引用的优缺点:
因为GC的回收涉及到对象的移动,所以对应的需要修改指针地址
存管理方式,在不同的情况下各有优缺点。下面是它们各自的优缺点:
句柄方式的优点:
- 内存管理灵活:句柄对象的固定大小使得内存管理更加灵活。因为句柄对象可以在堆中更容易地移动和重新分配,从而减少了碎片化问题,有利于提高内存的利用率。
- 安全性:句柄对象可以提供更好的安全性。由于引用变量存储的是指向句柄对象的引用,而不是指向实际对象的地址,可以在运行时做更多的安全检查和控制,防止悬空指针和野指针等问题。
句柄方式的缺点:
- 性能损失:由于句柄对象和实际对象之间需要进行两级间接寻址,这增加了访问对象数据的开销,导致性能上的损失。尤其是在频繁访问对象时,句柄方式可能会导致额外的开销。
- 需要额外内存:句柄方式需要为每个对象都分配额外的句柄对象,这会导致额外的内存开销。这在对象数量较多时可能会成为问题。
直接引用的优点:
- 性能更高:直接引用避免了句柄方式中的两级间接寻址,直接将引用变量指向实际对象,因此访问对象数据更加高效,减少了额外开销,提高了性能。
- 内存开销较小:直接引用不需要额外的句柄对象,节省了内存开销。这对于对象数量较大或内存有限的场景非常有利。
直接引用的缺点:
- 内存管理相对困难:直接引用使得内存管理相对困难,因为实际对象在堆中的位置可能会发生变化,导致引用的失效。为了解决这个问题,需要引入更复杂的内存管理策略,如压缩算法等。
综合来看,直接引用方式在性能和内存开销上更优,因此成为目前主流的内存管理方式。但是句柄方式在一些特定的场景下可能仍然有一定的优势,尤其是在安全性和内存管理灵活性方面。不同的编程语言和系统根据具体需求可能会选择适合的内存管理方式。在Java中,目前的主流Java虚拟机都采用了直接引用的方式。
对象怎么分配?
-
当我们new一个对象时,先会尝试是否能在栈上分配,如果可以,就直接存在栈上,不行才会在堆空间(通过
逃逸分析
来判断是否能在栈上分配)**逃逸分析:**指的是一个对象是否被传递到了方法外部,即对象的引用是否在方法的生命周期之外继续存在。如果对象没有逃逸,它的生命周期仅限于方法的执行过程,那么编译器可以选择将该对象在栈上分配内存而不是在堆上分配。这样做的好处是,在栈上分配的对象可以随着方法的退出而自动销毁,无需垃圾收集器的介入,减少了内存的使用和垃圾收集的开销,从而提高了程序的性能。
-
判断对象是否大,如果过大则直接分配到老年代,(通过JVM参数控制)
-
经过TLAB分配,进入到新生代(E区-诞生区)
TLAB:代表线程本地分配缓冲区(Thread-Local Allocation Buffer),是Java虚拟机(JVM)中的一种内存分配优化技术。
在Java中,对象的创建通常是在堆内存中进行的。为了提高对象的分配效率,JVM引入了TLAB机制。TLAB是每个线程私有的一块内存区域,用于在线程本地进行对象的分配。每个线程都有自己的TLAB,从而避免了多线程竞争分配内存的问题。
需要注意的是,TLAB机制只在启用了线程本地分配缓冲区的情况下才会生效。可以通过JVM参数来控制TLAB的启用和大小,例如使用
-XX:+UseTLAB
来启用TLAB,使用-XX:TLABSize
来设置TLAB的大小。在大多数情况下,JVM会根据硬件和应用程序的特点自动调整TLAB的大小。 -
在E区经过GC之后,回收掉了就结束,没回收掉就会进入S区(也就是幸存者区域)
-
如果再次GC还没清除掉,则判断年龄大小,达到赋值则进入老年代,没有则进入S2,(第二个幸存者区域),如此反复,直到GC回收掉
为什么hotspot不使用c++对象来代表java对象?
hotspot指的是Java虚拟机(Java Virtual Machine,JVM)的一种实现,HotSpot虚拟机以高性能为目标,通过即时编译(Just-In-Time Compilation,JIT)技术来将Java字节码转换为本地机器码,从而提高Java应用程序的执行速度。
- C++对象有一个virtual table 这个是java对象所没有的。所以相对应的c++的对象会占用内存更多
- 对象头信息:Java对象在HotSpot中包含对象头信息,用于存储元数据,如哈希码、锁状态、GC信息等。这些信息在C++对象中无法直接表示。
- 垃圾回收:Java虚拟机需要进行垃圾回收来管理内存,回收不再使用的对象。而C++没有垃圾回收机制,需要手动管理内存。(HotSpot中存有GC信息)
- 跨平台性:Java是一种跨平台的编程语言,而C++的对象模型和内存布局在不同平台上可能会有差异。使用C++对象来表示Java对象可能会导致不同平台之间的兼容性问题。
- JNI交互:Java虚拟机支持与本地代码的交互,通过JNI(Java Native Interface)可以调用C++代码。为了与Java对象交互,Java虚拟机需要一种独立于C++对象的对象表示方式。
Class对象是在堆还是在方法区?
在堆中
存放在方法区当中的是类的元数据,即类加载器从class文件中提取出来的类型信息、方法信息、字段信息等。元数据中又保存着指向class对象的引用。
参考文章
DCL要不要加volatile问题
需要!
cpu不一定是按照顺序执行程序,可能会出现乱序
DCL
DCL(双重检查锁定),经典例子是单例模式,双重检查锁定利用了以下两个原则:
- 减少同步开销: 通过首先检查实例是否已经创建,如果没有创建,再进行同步操作,从而减少了不必要的同步开销。
- 保证线程安全: 在同步块内进行实例的创建操作,确保只有一个线程能够成功创建实例。
public class Singleton {
private volatile static Singleton instance; // 使用volatile确保可见性
private Singleton() {
// 私有构造方法
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查,避免不必要的同步
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查,确保只有一个线程创建实例
instance = new Singleton();
}
}
}
return instance;
}
}
指令重排
在第一个问题我们说了Object 0 = new Object()大体分为三步:
- new 申请空间,半初始化对象,也就是给默认值
- 调用构造方法,赋值
- 引用,建立关联
这是按照正常顺序执行,但CPU会出现乱序执行,就一定会出现这三步乱序的问题,比如132的顺序,假如这种情况发生(还是拿上面单例模式举例),就会出现还没完成初始化,只是半初始化就会得到一个对象
volatile
volatile
是一个关键字,用于修饰变量,它的作用是确保变量在多线程环境下的可见性和禁止指令重排。volatile
变量具有以下作用:
- **可见性(Visibility):**当一个变量被声明为
volatile
时,任何线程对该变量的修改都会立即对其他线程可见,即使是在不同的CPU核心上。这可以确保当一个线程修改了volatile
变量的值后,其他线程能够立即看到最新的值,避免了线程之间的数据不一致问题。 - **禁止指令重排:**在多线程环境下,编译器和处理器为了提高执行效率可能会对指令进行重排,这种重排可能会影响多线程程序的正确性。通过将变量声明为
volatile
,可以防止编译器和处理器对该变量相关的指令重排,从而确保操作的顺序符合程序员的预期。
volatile
volatile
是一个关键字,用于修饰变量,它的作用是确保变量在多线程环境下的可见性和禁止指令重排。volatile
变量具有以下作用:
- **可见性(Visibility):**当一个变量被声明为
volatile
时,任何线程对该变量的修改都会立即对其他线程可见,即使是在不同的CPU核心上。这可以确保当一个线程修改了volatile
变量的值后,其他线程能够立即看到最新的值,避免了线程之间的数据不一致问题。 - **禁止指令重排:**在多线程环境下,编译器和处理器为了提高执行效率可能会对指令进行重排,这种重排可能会影响多线程程序的正确性。通过将变量声明为
volatile
,可以防止编译器和处理器对该变量相关的指令重排,从而确保操作的顺序符合程序员的预期。
需要注意的是,尽管
volatile
能够解决一些多线程问题,但它并不能完全取代锁。volatile
只适用于某些特定的场景,如标识状态变量、控制开关等,它无法保证复合操作的原子性,也无法解决一些更复杂的线程同步问题。对于一些需要保证原子性和临界区操作的情况,还是需要使用锁或其他线程同步机制