JVM内存进阶

写在前面:博主是一位普普通通的19届二本大学生,平时最大的爱好就是听听歌,逛逛B站。博主很喜欢的一句话花开堪折直须折,莫待无花空折枝:博主的理解是头一次为人,就应该做自己想做的事,做自己不后悔的事,做自己以后不会留有遗憾的事,做自己觉得有意义的事,不浪费这大好的青春年华。博主写博客目的是记录所学到的知识并方便自己复习,在记录知识的同时获得部分浏览量,得到更多人的认可,满足小小的成就感,同时在写博客的途中结交更多志同道合的朋友,让自己在技术的路上并不孤单。

目录:
1.堆内内存与堆外内存
       
堆内内存
       
堆外内存
       
堆外内存的优缺点
2.直接内存
3.对象创建内存的内存分析
       
对象创建的初始化工作
       
对象分配内存方式的两种方式
       
对象分配内存的线程问题
       
对象实例化
4.对象的内存布局
       
对象的内存布局的简介
       
对象头
       
实例数据
       
对齐填充
5.对象的访问定位

1.堆内内存与堆外内存

1.1堆内内存

Java 堆内内存是用来存放对象实例及数组,也就是说我们代码中通过 new 关键字 new 出来的对象都存放在这里。

在使用堆内内存(on-heap memory)的时候,完全遵守JVM虚拟机的内存管理机制,采用垃圾回收器(GC)统一进行内存管理,GC会在某些特定的时间点进行一次彻底回收,也就是Full GC,GC会对所有分配的堆内内存进行扫描,在这个过程中会对JAVA应用程序的性能造成一定影响,还可能会产生Stop The World。

1.2堆外内存

堆外内存和堆内内存是相对的两个概念,和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。我们经常用java.nio.DirectByteBuffer对象进行堆外内存的管理和使用,它会在对象创建的时候就分配堆外内存。

1.3堆外内存的优缺点

优点:

  • 减少了垃圾回收提高效率(因为垃圾回收会暂停其他的工作)
  • 堆内在flush到远程时,会先复制到直接内存,然后在发送;而堆外内存相当于省略掉了这个工作

缺点:

  • 堆外内存的缺点就是内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。

在这里插入图片描述

2.直接内存

直接内存是一块堆外内存。直接内存并不是虚拟机运行时数据区的一部分,但是被经常的使用,而且也会导致OutOfMemoryError异常

JDK1.4的时候加入NIO类,引入了一种基于通道与缓冲区的I/O方法。它可以使用Native函数库直接分配堆外内存,然后通过Java堆里边的DirectByteBuffer对象来作为这一块内存的引用进行操作,能够在一些场景显著提高性能,因此避免了Java堆和Native堆来回复制数据。

Native函数库:

Native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。

在这里插入图片描述

3.对象创建的内存的内存分析

3.1对象创建的初始化工作

当JVM遇到一条字节码new指令时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否被加载过,没有的话执行类加载过程

3.2对象分配内存方式的两种方式

方式1:指针碰撞

当Java堆是绝对规整的,即所有使用的内存放在一边,没有使用的内存放在另一边。中间放一个指针作为指针的分界点指示器,那所分配内存是把那个指针向空闲方向挪动一段与对象大小相等的距离。这就是指针碰撞

方式2:空闲列表

当Java堆不是规整的,已被使用和没被使用的内存交织在一块,虚拟机会维护一个列表,记录那些内存是可用的,那些是不可用的,在列表里找到一块足够大的空间给对象,并更新列表里边的数据

总的来说怎么分配是根据 Java堆是否规整 决定的,是否规整有是根据虚拟机所采用的垃圾垃圾收集器 是否具有空间压缩整理的能力 来决定

3.3对象分配内存的线程问题

当我们使用指针碰撞的方式来给对象A分配内存,指针还没来得及进行修改对象B又用了原来的指针来分配内存,会引发线程问题

解决方法1:

一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败 重试的方式保证更新操作的原子性

解决方法2:

另外一种是把内存分配的动作按照线程划分在不同的空间之中进 行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完 了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来 设定

内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果 使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。这步操作保证了对象的实例字段 在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值

3.4对象实例化

我们对象的内存虽然分配好了但是所有的字段都 为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好

,Java编译器会在遇到new关键字的地方同时生成 这两条字节码指令.new指令之后会接着执行<init> ()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。

4.对象的内存布局

4.1对象的内存布局的简介

对象在堆内存中的存储布局可以划分为三个部分:

  • 对象头(Header)
  • 实例 数据(Instance Data)
  • 对齐填充(Padding)

4.2对象头

对象头部分包括两类信息。第一类是:用于存储对象自身的运行时数据,如哈 希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部 分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特。对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针 来确定该对象是哪个类的实例,并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话 说,查找对象的元数据信息并不一定要经过对象本身

4.3实例数据

来实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字 段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来

4.4对齐填充

这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作 用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是 任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者 2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

在这里插入图片描述

5.对象的访问定位

创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference(引用)数据来操作堆上的具 体对象 但是并没有定义 这个引用应该通过什么方式去定位、访问到堆中对象的具体位置

所以对象访问方式也是由虚拟机实 现而定的,主流的访问方式主要有使用句柄和直接指针两种:

  • 1.使用句柄法:如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就 是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息

在这里插入图片描述

  • 2.指针法:如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关 信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问 的开销

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_45737068/article/details/107138686