1. 发展历程
- Classic VM:只能使用纯解释器方式执行Java代码
- Exact VM:准确内存管理
HotSpot VM:热点代码探测技术
C:\Users\dyliang>java -version
java version "1.8.0_221"
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
JRockit、IBM J9
……
2. Java内存区域
- 程序计数器:它是当前线程所执行的字节码的行号指示器,用于程序控制流(循环、分支、线程切换、异常处理等)。程序计数器由每个线程独立有拥有,互不影响、独立存储
- Java虚拟机栈:它用于执行Java方法(字节码),线程私有,当线程中方法被调度时,虚拟机会创建用于保存局部变量表、操作数栈、动态连接和方法出口等信息的栈帧(Stack Frame)。
- 局部变量表:存放各种基本类型、对象引用和returnAddress类型的数据,数据的保存在存储空间中以局部变量槽的形式表示
- 异常:
- StackOverFlow:线程请求的栈深度超过了虚拟机允许的最大深度
- OutOfMemoryError:栈扩容时无法申请到足够的内存
- 本地方法栈:它用于执行本地方法,原理和表现类似于Java虚拟机栈
- Java堆:它是虚拟机管理的内存中最大的一块区域,唯一作用就是存放数组和对象实例,即通过new指令创建的对象,包括数组和引用类型;线程共享;它既可以被实现成固定大小,也可以是可扩展的;堆内存空间可通过GC进行回收。
JDK1.8之后,字符串常量池从方法区中转移到了堆内存中。
- 方法区:它用于存储已被虚拟机加载的类型信息、常量、静态常量、即时编译器编译后的代码缓存等数据;线程共享
- 运行时常量池:用于存放Class文件编译期生成的各种字面量与符号引用;动态性,可以实现将运行形式的常量方法池中
直接内存:通过Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内粗的引用进行操作。
3. 堆内存的分配机制
当使用new指令创建对象时,系统会在堆内存中为对象分配所需大小的内存空间。根据已使用内存和空闲内存是否规整,常用的分配机制有:
- 指针碰撞(Bump The Pointer):当已使用内存和空闲内存规整时,通过指针作为两类内存空间的分界点
- 空闲列表(Free List):当已使用空间和空闲空间交织时,需使用空闲列表来维护当前空闲的内存块。在创建对象申请空间分配时,系统从中选出一块足够大的空间进行分配
如何保证在内存空间分配时线程安全?
- 对分配内存空间的动作进行同步处理
- 把内存分配的动作按照线程划分在不同的空间之中进行,这里不同的空间指的是每个线程在Java堆中预先分配的一小块称为本地线程分配缓冲(Thread Local Alloction Buffer,TLAB)的一小块内存
4. 对象的访问定位方法
主流的对象访问方式有如下两种:
- 使用句柄:句柄中保存了对象示例数据与类型数据各自具体的地址信息,它是一种间接的访问方式,即需通过句柄中保存的地址来访问对象
此时,对象的实例数据和类型数据分别存放在堆内存的实例池和方法区中。为了保存所有对象的实例数据和类型数据的指针,堆内存中会划分一块称为句柄池的内存空间。使用句柄的方式访问对象的好处:当对象的地址发生改变时,只需要修改句柄保存的地址,而栈内存中保存的引用地址无需更改。但缺点就是:间接的访问方式增加了访问的时间,效率较低。 - 使用直接指针:指针直接存储的就是对象指针,因此它是一种直接访问方式,通过保存的对象地址直接访问
相较于使用句柄的访问方式,使用直接指针进行访问效果更高。因为,堆中只保存了执行对象类型数据的一类指针,对象的实例数据可直接访问。不足之处在于,当对象的地址发生改变时,栈内存中保存的地址也要随之更新。
5. IDEA中设置JVM参数
点击菜单栏的Run,选择下拉菜单中的Edit Configurations
在右侧的VM options中添加想要设置的JVM参数即可: