开篇问题:
-
一句话描述类加载过程?
类加载过程实际是将Java文件编译为class文件并装载到JVM中最终解析为01机器代码供服务器进行的过程,涉及到的过程包括:编译、装载、链接、初始化、使用、卸载6个过程,其中各个过程的作用分别是:
- 编译:通过javac命令将Java文件转换成class文件。
- 装载:查找并加载class文件。
- 链接:包括:验证、准备、解析。
- 验证:通过对二进制流的内容进行校验来检查是否符合JVM的要求规范以及是否会对程序运行时是否会对JVM造成危害。其中包括:文件格式验证 --> 元数据验证 --> 字节码验证 --> 符号引用验证。
- 准备:在方法区中为类变量(静态变量)分配内存并设置系统默认初始值。
- 解析:将方法区中的符号引用转变成直接或引用,并对解析结构进行缓存。
- 初始化:调用构造方法对类变量赋予程序中设置的值。
- 使用:
- 卸载:类卸载的三个条件必须都满足才能进行卸载,一般情况下
-
详细描述对象的内存布局每一个部分干了什么,用到了什么技术?
对象内存布局包括:对象头、实例数据、对其填充三部分。其中
对象头包含:Mark Word、Class Pointer、Length。
- Mark Word:一系列标志位,包括:哈希码、分代年龄、锁状态标志等,其中哈希码用到了大端存储技术,便于数据类型的符号判断)
- Class Pointer:指向对象对应类的内存地址,其中引用定位到对象的方式包括:句柄池访问、直接访问。
- 句柄池访问:使用句柄访问对象,会在堆中开辟一块内存作为句柄池,句柄中储存了对象实例数据 的内存地址,访问类型数据的内存地址,优点:reference存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要改变;缺点:增加了一次指针定位的时间开销。
- 直接访问:指reference中直接储存对象在heap中的内存地址,但对应的类型数据访问地址需要 在实例中存储。优点:节省了一次指针定位的开销;缺点:在对象被移动时,reference本身需要被修改。
- Length:数据对象特有,用于记录数组长度。
实例数据包含:包含了对象的所有成员变量,大小由变量本身的类型决定,用到的技术-- 指针压缩技术作用包括:减少GC次数,提供CPU的OOP缓存。
对其填充的作用:为了保证对象的大小为8字节的整数倍,对其填充技术的作用是提高CPU访问数据的效率。
运行时数据区
CPU内存模型通过CPU与主存的关系可以推断出JVM是如何跟服务器的CPU和内存进行交互的 – java是多线程机制,当多个任务执行(类比我们的CPU运算核心)对同一块内存(类比:主存)数据进行操作时(PS:线程共享)必然会发生数据不一致的情况,这个时候需要有一块区域或者一种操作(类比:协议)保证数据的一致性。而每个线程又有自己单独的工作内存(类比高速缓冲区),当我们线程进行运作时,数据肯定会从JVM主存拷贝到线程自己的工作内存,然后再进行操作。
运行时数据区数据结构.png方法区
方法区是被线程共享的Non-Heap(非堆) 内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,在虚拟机启动时创建。
注意:
JVM运行时数据区是一种规范,真正的实现
方法区在JDK 8中就是Metaspace,在JDK6或7中就是Perm Space
堆
堆是Java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程共享。其中 Java对象实例以及数组都在堆上分配。
虚拟机栈
问题:那一个线程执行的状态如何维护?一个线程可以执行多 少个方法?这样的关系怎么维护呢?
- �虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。
- 每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。 调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
栈帧:
栈帧:每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间。
栈帧中包括局部变量 表、操作数栈、动态链接、方法返回地址和即时信息。
�局部变量 表:方法中定义的局部变量以及方法的参数存放在这张表中, 局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用。
操作数 栈:以压栈和出栈的方式存储操作数的。如 1+1 两个1存储在局部变量 表中, 1+1这个操作在操作数栈中完成。
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(符号引用编程直接引用)。因为类加载机制 仅仅是将本文件的符号引用变成直接引用 ,当遇到多态的调用时,只能通过运行来确定子类对接的引用是哪一个。
方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇 见异常,并且这个异常没有在方法体内得到处理。
本地方法栈
当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行
思考:在Java方法执行的时候如何调用native的方法呢?
通过动态链接来进行调用。
动态链接.png
程序计数器
作用:是记录当前线程的执行位置,线程私有。
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,则这个计数器为空。
除了上面五块内存之外,其实我们的JVM还会使用到其他两块内存
直接内存(Direct Memory)
并不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但是这部分内存也被频 繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆 里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能, 因为避免了在Java 堆和Native 堆中来回复制数据。
本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总 内存的大小及处理器寻址空间的限制。因此在分配JVM空间的时候应该考虑直接内存所带来的影 响,特别是应用到NIO的场景。
其他内存:
Code Cache:**JVM本身是个本地程序,还需要其他的内存去完成各种基本任务,比如,JIT 编译器在运行时对热点方法进行编译,就会将编译后的方法储存在Code Cache里面;GC等 功能。需要运行在本地线程之中,类似部分都需要占用内存空间。这些是实现JVM JIT等功能 的需要,但规范中并不涉及
其中栈可以指向堆 case: Object obj=new Object()
方法区指向堆 case: private static Object obj=new Object();
堆指向方法区 case: Class类对象指向它的元数据。 new Object().getClass();
Java对象内存模型
一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充。
Java对象内存布局.png
内存模型设计之–大小端存储
小端存储:便于数据之间的类型转换,例如:long类型转换为int类型时,高地址部分的数据可以 直接截掉。
大端存储:便于数据类型的符号判断,因为最低地址位数据即为符号位,可以直接判断数据的正 负号。
内存模型设计之–Class Pointer
直接指针访问对象.png引用定位到对象的方式有两种,一种叫句柄池访问,一种叫直接访问
句柄池访问对象.png
区别:
句柄池:
使用句柄访问对象,会在堆中开辟一块内存作为句柄池,句柄中储存了对象实例数据(属性值结构体) 的内存地址,访问类型数据的内存地址(类信息,方法类型信息),对象实例数据一般也在heap中开 辟,类型数据一般储存在方法区中。
优点:reference存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为) 时只会改变句柄中的实例数据指针,而reference本身不需要改变。
缺点:增加了一次指针定位的时间开销。 直接访问:
直接指针访问方式指reference中直接储存对象在heap中的内存地址,但对应的类型数据访问地址需要 在实例中存储。
优点:节省了一次指针定位的开销。 缺点:在对象被移动时(如进行GC后的内存重新排列),reference本身需要被修改
内存模型设计之–指针压缩
指针压缩的目的:
- 为了保证CPU普通对象指针(oop)缓存
- 为了减少GC的发生,因为指针不压缩是8字节,这样在64位操作系统的堆上其他资源空间就少了。
64位操作系统中 内存 > 4G 默认开启指针压缩技术,内存< 4G,默认是32位系统默认不开启。内存 > 32G 指针压缩失效。所以我们通常在部署服务时,JVM内存不要超过32G,因为超过32G就无法开启 指针压缩了。
内存 > 32G指针压缩失效的原因是:4G*8 = 32G
32位系统的CPU 最大支持2^32 = 4G ,如果是64位系统,最大支持 2^64, 但是对其填充是按照8字节进行填充,指针压缩可以理解为在32位系统在64位上面使用,因为32位系统的CPU寻址空间最大支持4G,对其填充*8 = 32G,这就是内存>32G指针压缩失效的原因。
关闭指针压缩 : -XX:+UseCompressedOops
内存模型设计之–对齐填充
对齐填充的意义是提高CPU访问数据的效率,主要针对会存在该实例对象数据跨内存地址区域存储的情况。
例如:在没有对齐填充的情况下,内存地址存放情况如下:
对其填充反例.png
因为处理器只能0x00-0x07,0x08-0x0F这样读取数据,所以当我们想获取这个long型的数据时,处理 器必须要读两次内存,第一次(0x00-0x07),第二次(0x08-0x0F),然后将两次的结果才能获得真正的数值。
那么在有对齐填充的情况下,内存地址存放情况是这样的:
对其填充正例.png
现在处理器只需要直接一次读取(0x08-0x0F)的内存地址就可以获得我们想要的数据了。
网友评论