总结不易,如果对你有帮助,请点赞关注支持一下
微信搜索程序dunk,关注公众号,获取以下整张完整图和思维导图
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来
序号 | 内容 |
---|---|
1 | Java基础面试题 |
2 | JVM面试题 |
3 | Java并发编程面试 |
4 | 计算机网络知识点汇总 |
5 | MySQL面试题 |
6 | Mybatis源码分析 + 面试 |
7 | Spring面试题 |
8 | SpringMVC面试题 |
9 | SpringBoot面试题 |
10 | SpringCloud面试题 |
11 | Redis面试题 |
12 | Elasticsearch面试题 |
13 | Docker学习 |
14 | 消息队列 |
15 | 持续更新… |
文章总览
JVM基础
JVM概述
JVM:Java Virtual Machine,也就是Java虚拟机,所谓虚拟机是指:通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的计算机系统,JVM是通过软件来模拟Java字节码的指令集,是Java程序的运行环境
JVM的主要功能
- 通过ClassLoader寻找和装载class文件
- 解释字节码文件成为指令并执行,提供class文件的运行环境
- 进行运行期间的内存分配和垃圾回收
- 提供与硬件交互的平台
JVM分类
- Sun/Oracle公司
- 虚拟机始祖:Sun Classic/Exact VM
- 武林盟主:HotSpot VM
- 小家碧玉:Moblie/Embedaed VM
- BEA公司
- 天下第二:JRockit
- 软硬合璧:BEA Liquid VM/Azul VM
- IBM
- 天下第二:J9
- Apache/Google
- 挑战这:Apache Harmony/Google Android Dalvik VM
- Microsoft
- 没有成功,但并非失败:Micosoft JVM
- 百家争鸣…
Java内存区域与内存溢出异常
![image-20210528190231019](https://gitee.com/zhang-songyao/blog-images/raw/master/20210528190233.png)
JVM包含两个子系统和两个组件,两个子系统为Class Loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)
- ClassLoader(类装载):根据给定的全限定名类名(如:Java.lang.Object)来装载class文件到Runtime data area中的method area
- Execution engine(执行引擎):执行classes中的指令
- Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口
- Runtime data area(运行时数据区域):这就是我们常说的JVM的内存
- Program Counter Register(程序计数器):当线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器
- Java Virtual Machine Stacks(Java 虚拟机栈):是线程私有的,它的生命周期与线程相同,用于存储局部变量表、操作数栈、动态链接、方法出口等信息
- Native Method Stack(本地方法栈):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的
- Method Area(方法区):用来存储已被虚拟机加载的类信息、常量、静态变量,即时编译后的代码数据
- Heap(堆):Java虚拟机汇中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存
本地库接口(JNI)
概述
JNI(Java Native Interface):Java本地开发接口
JNI是一个协议,这个协议用来沟通Java代码和外部的本地代码(C/C++),外部的代码也可以调用Java代码
为什么使用JNI
- 效率上 C/C++是本地语言,比java更高效
- 代码移植,如果之前用C语言开发过模块,可以复用已经存在的c代码
- java反编译比C语言容易,一般加密算法都是用C语言编写,不容易被反编译
在本地方法栈中,登记了native方法,在最终执行的时候加载本地方法库中的方法通过JNI调用
运行时数据区域
PC寄存器
- 每个线程都有一个OC寄存器,是线程私有的,用来存储指向下一条指令的地址
- 在创建线程的时候,创建相应的Pc寄存器
- 执行本地方法时,PC寄存器的值为undefined
- 是一块比较小的内存空间,是唯一一个在JVM规范中没有规定OutOfMemoryError的区域
Java栈
-
栈由一系列帧(栈帧(Frame))组成,因此Java栈也叫做帧栈,是线程私有的
-
栈帧用来保存一个方法的局部变量、操作数栈(Java没有寄存器,所有参数传递使用操作数栈)、常量池指针、动态链接、方法返回等
-
每一次方法调用创建一个帧,并压栈,退出方法的时候,修改栈顶指针就可以把栈帧中的内容毁掉
-
栈的优点: 存取速度比堆块,仅次于寄存器
-
栈的缺点:存在栈中的数据大小、生存区是在编译器决定的,缺乏灵活性
Java堆
- 用来存放应用系统的对象和数组,所有线程共享Java堆
- GC主要管理堆空间,对分代GC来说,堆也分代
- 堆的优点:运行期动态分配内存大小,自动进行垃圾回收
- 堆的缺点:效率相对较慢
方法区
- 方法区是线程共享的,通常用来保存装载类的结构信息
- 通常和元空间关联在一起,但具体的跟JVM实现和版本有关
- JVM规范把方法区描述为堆的一个逻辑部分,但它有一个别名称为 Non-heap(非堆),应是为了与 Java 堆分开
静态变量、常量、类信息(构造方法、接口定义)、运行时常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
方法区的发展
JDK8以前HotSpot设计团队选择吧收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区,这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去了专门为方法区编写内存管理代码的工作。
JDK6,常量池、静态变变量在方法区中
JDK7后,放弃弃了永久代的概念,逐步采用本地内存(Native Memory)来实现方法区的计划,把原本放在永久代的字符串常量池、静态变量等移除
到了JDK8后从,彻底废弃了永久代,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)代替,把JDK7中永久代剩余的内容(主要是类型信息)全部移到元空间
运行时常量池
- 是Class文件中每个类或接口的常量池表,在运行期间的表示形式,通常包括:类的版本、字段、方法、接口等信息
- 在方法区中分配
- 通常在加载类和接口到JVM后,就创建相应的运行时常量池
本地方法栈
- 在 JVM 中用来支持 native 方法执行的栈就是本地方法栈
完整细化JVM图
![image-20210603171544953](https://gitee.com/zhang-songyao/blog-images/raw/master/20210603171549.png)
Java程序运行机制
首先通过编译器把Java代码转化为字节码文件,类加载器(ClassLoader)再把字节码文件加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是JVM的一套指令规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution engine),将字节码翻译成底层系统指令,再交由CPU去执行,而这个过程中需要调用其他语言额本地库接口(Runtime data area)来实现整个程序的功能
![image-20210528192307777](https://gitee.com/zhang-songyao/blog-images/raw/master/20210528192310.png)
类的加载指的是将.class文件中的二进制数据读入内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
HotSpot虚拟机对象探秘
对象的创建
对象创建的方式
Header | 解释 |
---|---|
使用new关键字 | 调用了构造函数 |
使用Class的newInstance方法 | 调用了构造函数 |
使用Constructor类的newInstance方法 | 调用了构造函数 |
使用clone方法 | 没有调用构造函数 |
使用反序列化 | 没有调用构造函数 |
创建对象的过程
- 当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是够已经被加载、解析和初始化过。如果没有那必须先执行响应的类加载过程。
- 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从java堆中划分出来,选择那种分配方式是由Java堆的是否规整决定的
- 指针碰撞(Bump The Pointer):假设Java堆是完全规整的,所有被使用的内存都放在一边,空闲的内存被放在另一边,中间放一个指针为分界点的指示器,所分配的方式称为”指针碰撞“。
- 空间列表(Free List):Java堆不是规整的,已经使用的内存和空闲的内存相互交错在一起,那就没有办法简单的指针碰撞了,虚拟机需要维护一个表,记录上那些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给实例对象
- 除了如何划分空间外,还有另外一个需要考虑的问题是:对象创建中是非常频繁操作的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的。可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种可选的方案
- 一种是对分配内存空间的动作进行同步处理,实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性
- 另外一种是把内存分配的动作按照线程划分在不同的空间之中进行的,即每个线程预先分分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),那个线程要分配内存,就在那个线程的本地缓冲区分配,只有本地缓冲区分配用完了,分配新的缓冲区才需要同步锁定。
- 内存分配完成之后,虚拟机必须将分配的内存空间(但不包括对象头)都初始化为零值,如果使用TLAB的话,这一项工作也可以提前值TLAB分配时顺便进行,这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值
- 接下来Java虚拟机还要对对象进行必要的设置,例如对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希吗(实际上对象的哈希码会延后到真正调用Object::hashcode()方法时才计算)、对象的GC分代年龄等信息
- 上面的工作完成之后,从Java虚拟机的角度来看,一个新的对象已经产生了。但从Java程序的角度看来,对象创建才刚刚开始----构造函数,即Class文件中的方法还没有执行,一般来说是否构造这个类由new指令后跟随的invokespecial指令所决定的(一般new指令后会直接执行方法,其他方式产生的类则不一定),这样一个真正可用的对象才算完全被构造出来
对象的内存布局
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
- 对象头:包含两类信息(12B)
- 第一类用来存储对象自身运行时数据,如哈希吗(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向锁ID、偏向时间戳等(8B),这部分的数据长度在32位和64位的虚拟机分别为32个bit个64个bit,官方称为**”Make Word“**,为了考虑到虚拟机的空间效率,”Make Word“被设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间
- 另一类是类型指针(4B),即对象执行它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例
- 实例数据:对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。(相同宽度的字段总是被放在一起存放)
- 对齐填充:不是必然存在的,没有特别的意义,仅仅起到占位符的作用,由于HotSpot虚拟机的自动内存管理系统要求对象其实地址必须是8字节的整数倍,因此对象实例数据部分没有对齐的话,需要通过对齐填充来补全
对象的访问定位
创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具体对象,reference类型只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问的方式由虚拟机的实现而定
- 句柄:使用句柄的话Java堆可能会划分出来一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
![image-20210601203607884](https://gitee.com/zhang-songyao/blog-images/raw/master/20210601203610.png)
- 直接指针:Java堆中对象的内存布局必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象的对象地址,如果只是访问对象的话,就不会多一次间接访问的开销
![image-20210601203626963](https://gitee.com/zhang-songyao/blog-images/raw/master/20210601203630.png)
- 使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象移动(垃圾收集时移动对象是非常普遍的)时只会改变句柄中的实例数据指针,而reference不会改变
- 使用直接指针来访问的好处是速度快,节省了一次指针定位的时间开销,由于Java对象方位在Java中非常的频繁,所以这类开销积少成多也是一项即为可观的开销,HotSpot,主要使用第二种方式进行对象的访问(有例外)
OutOfMemoryError异常
除了程序计数器以外,虚拟机内存的其他几个运行时区域都有发生OOM异常的可能
Java堆溢出
Java堆用于储存对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达的路径来避免垃圾回收机制清除这些对象,那么随着对象的增加,总容量处理最大堆的容量限制