HotSpot虚拟机对象探索与OutOfMemoryError异常
1.HotSpot虚拟机对象探索
1.1对象的创建
不是一直有一个笑话,别人问程序员有没有对象,程序员会说我没有对象,但是我可以new一个出来
这里就可以判断他学过c++或者java等语言
在java中对象的创建一般我们都是通过new来创建的,但是这针对的只是java的语言层面,如果在虚拟机的层面上来说的话,东西还很多
1.1.1虚拟机方面
我们来看看下面的代码
String a = "123";
String b = new String("123");
请问这里面创建了多少个对象
答案是两个,分别是第一行的"123"与第二行的new String()
new String我们都知道,只要用了这个方法那么在堆内存。但是为什么只有上面的123创建了对象,下面new的那个123没有创建对象呢?
这是因为java虚拟机在创建对象前会先去检查在常量池里面能否定位到一个类的符号引用,因为我们在第一行在常量池里面已经创建了一个123对象,在第二行中123对象可以在常量池里面找到,所以总共就创建了两个对象
如果我们最开始没有
String a = "123";
那么在执行
String b = new String("123");
的时候我们会在常量池里面创建123,然后将在堆内存中创建一个新的String对象,其值为123,然后就要为这个对象从堆内存分配内存,
1.1.1.1堆内存的分配方式
堆内存给对象分配空间是有2种情况的
- 使用过的内存放在一边,空闲的内存放在另一边
- 使用过的内存和空闲的内存交织在一起
为什么会有这两种情况的产生主要是因为所采用的垃圾收集器是否带有空间压缩整理能力决定的
如果带有的话,则会采用指针碰撞这种方式来进行第一种分配内存的方式
如果没有的话,则会采用空闲列表这种方式来进行第二种分配内存的方式
1.1.1.2并发情况下对象的创建
在并发情况下,可能出现正在给A对象分配内存,指针还没来的及修改,对象B又同时使用了原来的指针来分配内存,解决这个有两种方法
- 使用同步处理,让A执行完后才让B执行
- 使用本地线程分配缓冲,即每个线程在java堆中预先分配一小块内存,哪个线程要分配内存,就在哪个线程的本地缓冲区进行分配
我最开始以为这个本地线程分配缓冲和工作内存是一个概念。后来发现不是
本地线程分配缓冲是对分配内存的,而工作内存是线程对一些变量或常量进行操作从某个地方取的
1.1.1.3对对象的设置
Java虚拟机还要对对象进行必要的设置,并把这些信息存放在在对象的对象头(Object Header)之中,会将对象的所有实例变量设置为其默认值。默认值取决于实例变量的类型。例如,对于基本类型(如int、float、boolean等),默认值为0或false。对于对象引用类型,默认值为null。
1.1.2Java程序角度
在虚拟机方面我们其实主要干了两件事,第一件事是分配对象的内存空间,第二件事是把一些基本信息传递给对象
虚拟机的弄完并不代表着对象就创建完了,我们需要在java程序上进行构造函数(记住,无参构造是默认的,一般你不写都会进行无参构造。有参构造得你自己写)
构造函数的主要作用就是把对象需要的状态和其他信息传递给对象
至此,java中的对象就创建好了
1.2对象的内存布局
对象的内存布局主要可以分为3个,分别是对象头,实例数据,对齐填充
1.2.1对象头
对象头主要是由两个构成的分别是存储对象自身运行时的数据与类型指针
这个构成看起来很像c语言中的链表,c语言链表中也是由数据还有指针构成的
这个存储对象自身运行时的数据并不和下面那个实例数据起冲突
它主要存储的是自身运行时的数据,如
哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等等
在这里面我只知道锁状态标志和线程持有的锁,其他不是很能理解,悲
这些数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别位32个比特和64个比特
上面提到的哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳的对象头的数据被称为Mark Word
类型指针就是对象指向它的类型元数据的指针,Java虚拟机可以通过这个类型来确定该对象为哪个类的实例。
但是要记住一句话:并不是所有的虚拟机实现都必须在对象数据上保留类型指针
1.2.2实例数据
存储在代码中定义的各种类型的字段内容,无论是从父类继承还是在子类定义的字段都得记录下来
1.2.3对齐填充
单纯占位符
1.3对象的访问定位
Java中对象创建好了,我们需要去使用对象,一般都是通过虚拟栈中的reference对象去操作堆上的具体对象
这个reference对象就是我们之前说过的存放在Java虚拟机栈的对象
我们上面不是写过两端代码嘛
String a = "123";
String b = new String("123");
其中,a和b就是这个reference对象
在Java中主流的访问有使用句柄和直接指针两种
1.3.1使用句柄
使用句柄访问的话,java堆可能会划分出一块内存来作为句柄池,引用对象存储的就是对象的句柄地址,而句柄中包含了对象的实例数据与类型数据各自的地址信息
1.3.2直接指针
java堆中对象的内存布局就必须考虑如何放置访问类型数据信息,引用数据中存储的直接就是对象地址
1.3.3对这两种访问方式的理解
无论是使用句柄还是直接指针,我们的引用对象都得通过java堆来访问
但是区别就在于如果是直接指针的话,到对象类型数据的指针就在对象实例数据中,我们可以直接通过这个指针指向方法区的对象类型数据
但是如果是使用句柄,它会在堆内存中专门再划分一块区域,用来存放两个指针,一个就是到对象实例数据的指针,一个是到对象类型数据的指针。两个指针分开指,到对象实例数据的指针指向堆内存中对象实例数据。到对象类型数据指向的就是方法区中对象类型数据
这里面刚好就和上面的内容联系起来了,这个指向对象类型数据的指针就是我们刚才在对象头的那个指针
用图来表示以下就是这样
1.3.4两者的好处
使用句柄访问的话,引用数据存储的就是句柄地址,在对象被移动的时候只会改变句柄中的实例数据指针,引用数据本身不用改变
而使用直接指针访问的话,速度更快
1.4总结
对象的创建主要是对象内存的分配与对象的引用
对象内存的分配主要是两个方面,一个是虚拟机上我们给它分配内存,从堆内存分点空间给它分点空间,然后第二个是在java的无参构造或者有参构造把一些属性给它
对象的引用也是两个方法,第一个是句柄访问对象,这个方法是堆内存给它分一部分空间成为句柄空间,然后一个指向类型数据的指针指向方法区的类型数据,一个指向实例数据的指针指向堆内存的实例池的实例数据
第二个是直接访问对象,这个方法是类型数据指针就在实例数据里面,然后类型数据指针指向方法区的类型数据
2.OutofMemoryError异常
在Java中除了程序计数器以外,虚拟机的其他都容易出现OutofMemoryError异常
2.1Java堆溢出
Java的堆溢出主要是因为Java的堆内存是有大小限制的,因为我们的Java堆内存是用来存储Java对象实例的,所以要是我们Java的实例对象创建的太多的话,就容易引起Java堆溢出
2.1.1如何解决Java堆溢出
我们首先是确认内存中导致OOM的对象是否是必要的,也就是到底是内存泄漏还是内存溢出
如果是内存泄漏的话我们要找到是什么导致的GC回收器没办法回收它
如果是内存溢出那就代表着内存中的每一个对象都得存活下来,那么就得检查Java虚拟机的参数,看是否可以向上调整内存的空间。然后再看哪些对象生命周期过长,持有状态时间过长,存储结构设计不合理等情况,尽量减少程序运行期的内存消耗
2.2虚拟机栈和本地方法栈溢出
HotSpot虚拟机不区分虚拟机栈和本地方法栈,所以本地方法栈在这里面没什么用,
在虚拟机栈中主要有以下两种异常
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
- 如果虚拟机的栈允许动态扩展,当扩展栈容量无法申请到足够的内存,将抛出OutofMemoryError异常
第一个就可以举一个这2个例子
public static void infiniteRecursion() {
infiniteRecursion();
}
这个函数如果无限的自己调用自己,那么就会有大量的局部变量.方法参数,临时变量存放在虚拟机栈中,就有可能导致栈溢出
还有这个
public static void createLargeObjects() {
for (int i = 0; i < 1000000; i++) {
byte[] largeObject = new byte[1000000];
}
}
每一次循环创建的东西比较多,然后又进行了很多次的循环,就容易导致栈溢出
2.3方法区和运行时常量池溢出
在 JDK 7 及之前的版本中,运行时常量池是存放在 PermGen 中的,PermGen 是方法区的一部分,用于存放类的元数据信息、静态变量、即时编译器编译后的代码等。由于运行时常量池的大小是在编译期间就确定的,所以将其存放在 PermGen 中是比较合适的。
但是,随着 Java 应用程序的发展,动态生成和卸载类的需求越来越多,而 PermGen 的大小是固定的,无法动态调整。这就导致了 PermGen 空间容易被占满,从而触发 Full GC,影响了应用程序的性能和稳定性。因此,为了解决这个问题,从 JDK 8 开始,PermGen 被彻底移除,Java 堆中的元数据区(Metaspace)被用来代替 PermGen 存储类的元数据信息。
而运行时常量池也随之从 PermGen 中移动到了 Java 堆中的元数据区。这样做的好处是,运行时常量池的大小可以根据应用程序的需要进行动态调整,从而更好地满足应用程序的需求。同时,也避免了 PermGen 空间被占满的问题,提高了应用程序的性能和稳定性。
2.4本地直接内存溢出
直接内存的容量大小可通过**-XX:MaxDirectMemorySize**参数来指定,如果不指定就和Java堆内存的最大值一样
直接内存无法直接进行分配的时候就会造成直接内存溢出