JVM运行时数据区域
Java虚拟机家族
- Sun Classic VM : JDK 1.0 ,Sun Classic VM 出现,但是这款虚拟机只能以纯解释器的方式来执行Java 代码,如果要使用即时编译器必须外挂,但是如果使用了外挂即时编译器的话,即时编译器会完全接管虚拟机的执行系统,解释器就不在工作了。(如果完全使用编译执行的话,编译器就必须对所有的 字节码进行编译,无论他们的执行频率是否都具有编译的价值,因此会影响程序的响应时间)
- Exact VM :为解决上述的问题,提升运行效率。JDK 1.2,Exact VM 出现,它的编译执行系统已经具有了现代高性能虚拟机的雏形,如热点探测,两级即时编译,编译器和解释器混合工作的模式。Exact VM 因使用准确式内存管理(exact memory management) 得名,虚拟机能够知道内存中某个位置处的数据具体是什么类型(例如,内存中有一个32bit 的整数123456,虚拟机能够区分它是一个指向地址“123456”的引用,还是一个数值为123456的整数),
因此,Exact VM 可以抛弃之前 Sun Classic VM 所使用的基于句柄的对象查找方式。 - HotSpot VM :如它的名称所示,Hotspot 指的就是他的热点代码探测技术(通过执行计数器找出最具有编译价值的代码,然后通知 即时编辑器以方法为单位进行编译)。
HotSpot 虚拟机中含有两个即时编译器,分别是
1),客户端编译器(C1):编译耗时短,但是输出代码优化程度低。
2),服务端编译器(C2):编译耗时长,但是输出代码优化质量也更高。
他们会在分层编译机制下与解释器互相配合共同构成HotSpot 的执行子系统。
JVM组成
-
类加载子系统
-
运行时数据区
-
执行引擎(一般都是JIT编译器和解释器共存)
- JIT编译器(主要影响性能):编译执行; 一般热点数据会进行二次编译,将字节码指令变成机器指令。将机器指令放在方法区缓存
- 解释器(负责响应时间):逐行解释字节码
Java 内存区域(JVM 运行时数据区域)
线程私有:
- 程序计数器 : 一块较小的内存区域,可以看作是当前线程所执行的字节码指令的行号指示器。它具有两个作用分别是
1),字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
2),为了在线程切换后能够恢复到正确的执行位置,(Java 虚拟机的多线程是通过线程轮流切换,分配处理器时间的方式来实现的,在任一时刻,一个处理器都只会执行一条线程中的指令)。
- 程序计数器在物理上是使用PC寄存器实现的,整个cpu中最快的一个执行单元
- JVM多线程的实现
在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射
(当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建,Java线程执行终止后,本地线程也会被回收)
操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化完毕,它就会调用Java线程中的run方法
- 虚拟机栈 :描述的是Java 方法执行的线程内存模型,每个方法执行的时候,Java 虚拟机都会同步创建一个栈帧(Stack Frame),其中存储的数据有
- 1),局部变量表 :定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量 (这些数据类型包括各种基本数据类型,对象引用(reference) 以及 return Address 类型)。
这些数据类型在局部变量表中的存储空间以局部变量槽(slot)(一个slot就是32个bit)来表示,JVM 会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
如果当前方法是由构造方法或者实例对象创建的,那么该对象引用 “this” 将会放在索引为0的slot处。
局部变量表在编译的时候就确定下来了,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变
- 2),操作数栈: 在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈或出栈、主要用于保存计算机过程的中间结果,同时作为计算过程中变量临时的存储空间。
例如,如果调用的方法有返回值的话,那么这个返回值会保存在操作数栈中,最后执行 “ireturn” 指令返回。
-
3),动态链接:指向运行时常量池的引用。动态链接的作用就是为了将这运行时常量池中的符号引用最终转换为调用方法的直接引用
-
4),返回地址: 存放调用该方法的程序计数器的值。
扩展1:动态链接
class 字节码文件的常量池中保存着大量的符号引用(在类加载过程中 java 代码的各种变量,字面量,方法名转化为符号引用保存其中),字节码中的方法调用指令就以指向常量池的该方法的符号引用作为参数。
a. 静态链接(早期绑定):当一个 字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知(非虚方法,静态方法、私有方法、final方法、实例构造器、父类方法(super调用)都是非虚方法),且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
b. 动态链接(晚期绑定):如果被调用的方法 (虚方法)在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接,体现了多态性。
扩展2:Java虚拟机栈的内存分配
- 固定大小:如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。
如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackoverflowError异常(例如:递归程序,递归太深,创建的栈帧过多时- 动态扩展:如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者是在创建新的线程时就没有足够的内存区创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常
- 本地方法栈 :类似虚拟机栈为Java 方法服务,本地方法栈为本地方法服务。
线程共享:
- Java 堆 :所有的对象实例以及数组都在堆上分配内存。可以是物理上不连续的的内存空间,但在逻辑上是连续的。
- 方法区 : 用于存储已被虚拟机加载的类型信息、域信息、方法信息,常量、即时编译器编译后的代码缓存等,方法区是一种规范,永久代和元空间都是其具体实现。
(The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods used in class and instance initialization and interface initialization)
![]() |
![]() |
扩展1
方法区的演变
- JDK1.6之前:永久代,静态变量、字符串常量池在方法区
- JDK1.7:有永久代,但已经逐步 " 去永久代 ",字符串常量池、静态变量移除,保存在堆中
- JDK1.8之后:元空间(常量池),静态变量、字符串常量池在堆中
扩展 2
- 类型信息:
这个类型的完整有效名称(全名=包名.类名)
这个类型直接父类的完整有效名(对于interface或是java. lang.Object,都没有父类)
这个类型的修饰符(public, abstract, final的某个子集)
这个类型直接接口的一个有序列表- 运行时常量池:存放编译时生成的各种字面量和符号引用
- 字面量:比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。
- 符号引用则属于编译原理方面的概念,包括下面三类常量:
1)、类和接口的全限定名
2)、字段的名称和描述符
3)、方法的名称和描述符
扩展3
方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误
导致方法区溢出的操作:
- 加载大量的第三方的jar包
- tomcat部署的工程过多(30-50个)
- 大量动态的生成反射类
总结
为什么使用元空间代替永久代?因为永久代的调优是困难的,
- 首先如果永久代的空间设置的太小,回到导致当程序需要加载很多类时,会导致永久代内存溢出异常
- 如果永久代空间设置的太大,会浪费空间
- 永久代的垃圾回收主要是废弃的常量以及不再使用的类型,对于不什么是不再使用的类型判断较为复杂,FULL GC 的执行频率较低
为什么将字符串常量池,静态变量放在堆中?
同上,因为方法区的FULL GC 的执行频率较低,我们的程序中会创建大量的字符串,如果放在方法区中会导致因回收不及时导致方法区溢出。
HotSpot 对象创建
- 当JVM遇到一条字节码 new 指令时,首先会检查这个指令的参数是否能够在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载,如果没有则执行相应的类加载过程。
- 类加载检查通过后,JVM 开始为新生对象分配内存
扩展1
1.对象内存分配方法有两种:
1)指针碰撞:Java 堆中的内存是规整的。
2)空闲列表:Java 堆中的不是规整的,已使用的内存和空闲的内存相互交错在一起。
扩展2
在并发情况下,仅仅修改一个指针所指向的位置也并不是线程所安全的,可能出现正在给对象A分配内存时,指针还没来得及修改,对象B又同时使用了原来的指针分配内存的情况。
解决方案有两种:
1) 一种是对分配内存的动作进行同步处理,实际上虚拟机采用 CAS(Compare and Swap)+ 失败重试的方式 保证操作的原子性。
2)把内存分配的动作按线程划分在不同的空间中进行,即每个线程在Java 堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程分配内存,就在哪个线程的 本地线程分配缓冲区中分配。
- 内存分配完之后,JVM 将分配到的内存空间(不包括对象头)初始化为零值。
- JVM 对对象头进行设置
- 最后new 指令之后会接着执行 < init >() 方法,按照程序员的意愿对对象进行初始化。
对象的内存布局
对象在堆内存中的存储布局可以划分为三个部分:
- 对象头(Header):主要存储两类信息:1),对象自身的运行时数据(哈希码,GC 分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。 2),类型指针,即对象指向它的类型元数据的指针,用来确定该对象是那个类的实例。
?个人理解:
所谓类型元数据就是,类加载时生成的Class 对象,
Object.getClass() 方法可能就是使用Native方法,通过该指针查找,最终返回Class 对象
-
实例数据(Instance Data) : 对象真正存储的有效信息,即我们在程序代码中定义的各种类型字段。
-
对齐填充(Padding):占位符,HotSpot VM 要求对象的起始地址必须是8字节的整数倍。即对象的大小必须是8字节的整数倍。
为什么是8字节的整数倍?
如果分配空间时候按照字节对齐的策略 就不会出现存储跨cpu缓存行的数据出现
以一些空间为代价加快内存访问。如果数据未对齐,则处理器需要在加载内存后进行一些转换才能访问它
对象的访问定位
- 句柄:好处:引用(reference)中存的是稳定的句柄地址,在对象移动时,只会改变句柄中的实例数据指针(Classic VM 使用这种方法)
- 直接指针:好处:访问速度快,节省了一次指针定位的时间(HotSpot VM 主要使用这种方法)
OutOfMemoryError 异常
除程序计数器以外,虚拟机内存的其他几个运行时数据区域都有发生 OOM 异常的可能
创建对象的几种方法
public class User implements Serializable, Cloneable {
/**
*
*/
private static final long serialVersionUID = 1L;
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public User() {
}
public Object clone() throws CloneNotSupportedException {
User cloneUser = null;
cloneUser = (User) super.clone();
return cloneUser;
}
public void myPrint() {
System.out.println(name + " " + age);
}
public static void main(String[] args) throws CloneNotSupportedException, InstantiationException,
IllegalAccessException, FileNotFoundException, IOException, ClassNotFoundException,
IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
User user1 = new User("liu", 21);
User user2 = (User) user1.clone();
Class userClass = Class.forName("object.User");
User user3 = (User) userClass.newInstance();
User user4 = (User) userClass.getConstructor(String.class, int.class).newInstance("liu2", 24);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(new File("src\\object\\user.txt")));
out.writeObject(user1);
out.close();
ObjectInputStream in =
new ObjectInputStream(new FileInputStream(new File("src\\object\\user.txt")));
User user5 = (User) in.readObject();
user1.myPrint();
user2.myPrint();
user3.myPrint();
user4.myPrint();
user5.myPrint();
}
}
参考资料
《深入理解Java 虚拟机》
Java 虚拟机规范官方文档
.