八、探秘HotSpot虚拟机对象与直接内存
作者:互联网
一、对象的创建
1、创建对象的几种方式
2、从字节码角度看对象创建过程
public class ObjectTest {
public static void main(String[] args) {
Object o = new Object();
}
}
编译之后通过
javap -c -p ObjectTest.class
查看方法对应的字节码,-v:会输出所有字节码信息
查看main()对应的字节码,首先调用
new指令
后找到#2
位置字符串常量池,找到了Object类,首先判断方法区中是否将类加载了,如果没有加载需要使用类加载器将类进行加载。然后调用Object类的init()
方法。
Classfile /D:/IdeaProbject/JVM/target/classes/day5/ObjectTest.class
Last modified 2021-9-11; size 430 bytes
MD5 checksum 5eb0fa7172fbe9c882683b37b9a32a50
Compiled from "ObjectTest.java"
public class day5.ObjectTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#19 // java/lang/Object."<init>":()V
#2 = Class #20 // java/lang/Object
#3 = Class #21 // day5/ObjectTest
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lday5/ObjectTest;
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 args
#14 = Utf8 [Ljava/lang/String;
#15 = Utf8 o
#16 = Utf8 Ljava/lang/Object;
#17 = Utf8 SourceFile
#18 = Utf8 ObjectTest.java
#19 = NameAndType #4:#5 // "<init>":()V
#20 = Utf8 java/lang/Object
#21 = Utf8 day5/ObjectTest
{
public day5.ObjectTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lday5/ObjectTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: return
LineNumberTable:
line 9: 0
line 10: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
8 1 1 o Ljava/lang/Object;
}
3、判断对象对应的类是否加载、链接、初始化
1、当Java虚拟机遇到一条字节码
new指令
时,首先将去检查这个指令的参数是否能在Matespace的常量池中定位到 一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。2、如果没有,那么在
双亲委派模式
下,使用当前类加载器以ClassLoader+包名+类名
作为Key
进行查找定位到对应的.class文件
。如果没有找到class文件,则抛出ClassNotFoundException
异常;如果找到,则进行类加载,并生成对应的Class类对象。
4、为新生对象分配内存空间
1、对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上等同于把一块确定大小的内存块从Java堆中划分出来。如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。
2、Java堆中内存是绝对规整的:采用
指针碰撞(Bump The Pointer)
方式分配内存3、Java堆中内存并不是规整的:采用
空闲列表(Free List)
方式分配内存
已被使用的内存和空闲的内存相互交错在一起,就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
4、分配方式的选择
由Java堆是否规整
决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定
当使用
Serial
、ParNew
等带压缩整理
过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;当使用
CMS
这种基于清除(Sweep)算法
的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。5、标记清除算法与标记压缩算法区别
5、处理并发问题
1、对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
2、两种解决方式
6、初始化分配到的内存空间
内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。
7、设置对象的对象头
将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC分代年龄等信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
8、执行init方法进行初始化
在上面步骤都完成之后,从虚拟机的角度来看,一个新的对象已经产生了。但是从Java程序角度来看,对象创建才刚刚开始——构造函数,即Class文件中的
<init>()
方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说(由字节码流中new指令
后面是否跟随invokespecial指令
所决定,Java编译器会在遇到new关键字的地方同时生成这两条字节码指令
,但如果直接通过其他方式产生的则不一定如此),new指令之后会接着执行<init>() 方法
,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。
/**
* 测试对象创建过程:
* 1、加载类元信息;2、为对象分配内存;3、处理并发问题;4、属性的默认初始化(零值初始化)
* 5、设置对象头信息;6、属性的显式初始化、代码块中初始化、构造器中初始化
* 给对象属性赋值操作:
* 1、属性的默认初始化
* 2、显式初始化/代码块中初始化(并列关系,谁先谁后看代码编写的顺序)
* 3、构造器初始化
*/
public class CreateObject {
int id = 1001;
String name;
Users users;
{
name = "匿名用户";
}
public CreateObject() {
users = new Users();
}
}
1、属性的默认值初始化:id=1001
2、显式初始化/代码块初始化:neme=“匿名用户”
3、构造初始化:users=new Users()
二、对象的内存布局
1、概述
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例 数据(Instance Data)和对齐填充(Padding)。
2、对象头(Header)
HotSpot虚拟机对象的对象头部分包括两类信息:
运行时元数据(Mark Word)与类型指针
。运行时元数据(Mark Word):
用于存储自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分的数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,称它为“Mark Word”。
对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的最大限度,但对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间。
例如在32位的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,Mark Word的32个比特存储空间中的25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,1个比特固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下图所示
64位Mark Word存储内容如下:
类型指针:
3、实例数据(Instance Data)
它是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。
这部分的存储顺序会受到虚拟机分配策略参数(
-XX:FieldsAllocationStyle
参数)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配顺序为
longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)
,从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存放
,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前
。如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认就为true)
,那子类之中较窄的变量也允许插入父类变量的空隙之中
,以节省出一点点空间。
4、对象填充(Padding)
这并不是必然存在的,也没有特别的含义,
它仅仅起着占位符的作用
。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全
。
5、图解内存布局
public class CreateObject {
int id = 1001;
String name;
Users users;
{
name = "匿名用户";
}
public CreateObject() {
users = new Users();
}
}
public class Users {
}
public class CreateObjTest {
public static void main(String[] args) {
CreateObject create = new CreateObject();
}
}
内存布局图解
三、对象的访问定位
1、概述
创建对象自然是为了后续使用该对象,Java程序会通过栈上的
reference
数据来操作堆上的具 体对象。由于reference
类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有使用句柄
和直接指针
两种。
2、句柄方式访问
句柄访问就是说栈的局部变量表中,记录的对象的引用,
Java堆中将可能会划分出一块内存来作为句柄池
,reference
中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
优点:
缺点:
3、直接指针方式访问(HotSpot采用)
直接指针是局部变量表中
reference
中存储的直接就是对象地址,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据。
优点:
四、直接内存(Direct Memory)
1、概述
不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
直接内存是在Java堆外的、直接向系统申请的内存区间。
来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存
通常,访问直接内存的速度会优于Java堆。即读写性能高。
/**
* 直接内存的占用与释放
*/
public class BufferTest {
private static final int BUFFER = 1024 * 1024 * 1024;//1GB
public static void main(String[] args){
//直接分配本地内存空间
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
System.out.println("直接内存分配完毕,请求指示!");
Scanner scanner = new Scanner(System.in);
scanner.next();
System.out.println("直接内存开始释放!");
byteBuffer = null;
System.gc();
scanner.next();
}
}
2、非直接缓存区
原来采用BIO的架构,在读写本地文件时,需要与磁盘交互,需要由用户态切换到内核态。这里需要两份内存存储重复数据,效率低。
3、直接缓冲区
使用NIO时,操作系统直接划出的直接缓存区可以被Java代码直接访问,只有一份。NIO适合对大文件的读写操作。直接操作物理磁盘。
4、分别使用NIO和BIO复制大文件
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 分别使用NIO和BIO复制大文件
*/
public class CopyFileWithNio {
private static final String TO = "D:\\Project\\lifecycle-lib.zip";
private static final int _100Mb = 1024 * 1024 * 100;
public static void main(String[] args) {
long sum = 0;
String src = "D:\\Project\\lifecycle-lib.zip";
for (int i = 0; i < 3; i++) {
String dest = "D:\\lifecycle-lib_" + i + ".zip";
//使用传统方式复制文件
// sum += io(src,dest);//1673
//使用NIO方式复制文件
sum += directBuffer(src, dest);//957
}
System.out.println("总花费的时间为:" + sum);
}
/**
* 使用NIO方式
* @param src
* @param dest
* @return
*/
private static long directBuffer(String src, String dest) {
long start = System.currentTimeMillis();
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
inChannel = new FileInputStream(src).getChannel();
outChannel = new FileOutputStream(dest).getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
while (inChannel.read(byteBuffer) != -1) {
//修改为读数据模式
byteBuffer.flip();
outChannel.write(byteBuffer);
//清空
byteBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inChannel != null) {
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outChannel != null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
long end = System.currentTimeMillis();
return end - start;
}
/**
* 使用传统方式
* @param src
* @param dest
* @return
*/
private static long io(String src, String dest) {
long start = System.currentTimeMillis();
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(src);
fos = new FileOutputStream(dest);
byte[] buffer = new byte[_100Mb];
while (true) {
int len = fis.read(buffer);
if (len == -1) {
break;
}
fos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
long end = System.currentTimeMillis();
return end - start;
}
}
5、直接内存与OOM
直接内存也可能导致OutofMemoryError异常
由于
直接内存在Java堆外,因此它的大小不会直接受限于-Xmx指定的最大堆大小
,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存
。
缺点:
直接内存大小可以通过
-XX:MaxDirectMemorySize
设置,如果不指定,默认与堆的最大值-Xmx参数值一致
/**
* 测试本地内存的OOM
* java.lang.OutOfMemoryError: Direct buffer memory
*/
public class DirectOOM {
private static final int BUFFER = 1024 * 1024 * 20;//20MB
public static void main(String[] args) {
ArrayList<ByteBuffer> list = new ArrayList<>();
int count = 0;
try {
while(true){
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
list.add(byteBuffer);
count++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
System.out.println(count);
}
}
}
6、通过Unsafe类直接申请本地内存
在
ByteBuffer.allocateDirect()
方法中new DirectByteBuffer()
中使用Unsafe类分配本地内存
/**
* -Xmx20m -XX:MaxDirectMemorySize=10m
* 设置最大堆内存20M,直接内存10M
*/
public class MaxDirectMemorySizeTest {
private static final long _1MB = 1024 * 1024;
public static void main(String[] args) throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while(true){
unsafe.allocateMemory(_1MB);
}
}
}
标签:Java,对象,虚拟机,HotSpot,内存,new,探秘,public 来源: https://blog.csdn.net/qq_42200163/article/details/120258377