5.1 JVM体系结构
-
线程独占区-程序计数器(Program Counter Register)
-
线程独占区-Java虚拟机栈(Java Virtual machine Stacks)
-
线程独占区-本地方法栈(Native Method Stack)
-
线程共享区-Java堆(Java Heap)
-
线程共享区-方法区(Method Area)
-
举例:
String str1 = "abd"; String str2 = new String("abd"); System.out.println(str1 == str2); System.out.println(str1 == str2.intern()); 输出: false true 分析: (1) String str1 = "abc" ,str1指向常量池; (2) String str2 = new String("abc");str2指向堆内存对象,二者地址不同所以str1 == str2 结果为false; (3) 但是str2.intern()会把字符串值从堆内存移动到常量池中(如果常量池存在则返回该值的地址),这样一来str2和str1都是指向常量池的abc。 如下图所示:
5.2 JVM详细架构图
5.3 JVM架构之运行时数据区
- 线程共享区包括:堆、元空间
- 线程私有区包括:虚拟机栈、本地方法栈、程序计数器
运行时数据区
包括:程序计数器(PC寄存器)、Java虚拟机栈、Java堆、方法区、运行时常量池、本地方法栈等等。
5.3.1 PC 寄存器,也叫程序计数器
- 1、JVM支持多个线程同时运行,每个线程拥有一个程序计数器,是线程私有的,用来存储指向下一条指令的地址。
- 2、在创建线程的时候,创建相应的程序计数器。
- 3、执行本地native方法时,程序计数器的值为undefined。
- 4、是一块比较小的内存空间,是唯一一个在JVM规范中没有规定OutOfMemoryError的内存区域。
5.3.2 虚拟机栈
- 栈是由一系列帧(Frame)组成(因此Java栈也叫作帧栈),是线程私有的。
- 帧是用来保存一个方法的局部变量、操作数栈(java没有寄存器,所有的参数传递使用操作数栈)、常量池指针、动态链接、方法返回值等。
- 每一次方法调用创建一个帧并压栈,退出方法的时候,修改栈顶指针就可以把栈帧中的内容销毁。
- 局部变量表存放了编译期可知的各种基本数据类型和引用数据类型、每个slot存放32位的数据,long、double占两个槽位。
- 栈的优点:存取速度比堆快,仅次于程序计数器。
- 栈的缺点:存在栈中的数据太小,生存期是在编译期决定的,缺乏灵活性。
- StackOverflowError异常:当线程请求的栈深度大于虚拟机所允许的深度;
- OutOfMemoryError异常:如果栈的扩展时无法申请到足够的内存。
5.3.3 Java堆
- 用来存放应用系统创建的对象和数组,所有线程共享Java堆。
- GC主要管理堆空间,对分代GC来说,堆也是分代的。
- 堆的优点:运行期动态分配内存大小,自动进行垃圾回收。
- 堆的缺点:效率相对较慢。
5.3.4 方法区的理解
HotSpot中方法区的演进:
5.3.5 运行时常量池:
- 党创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM就会抛出OOM异常。
5.3.6 本地方法栈
- 在JVM中用来支持native方法执行的栈就是本地方法栈。
- 在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。
5.4 类加载器
-
作用:加载class文件
-
加载器:
-
图示:
-
双亲委派机制
5.5 Java对象的实例化过程
java世界里面对象无处不在,在创建对象的时候主要经过哪些步骤?
5.5.1 对象的创建过程
类加载检查–>分配内存–>初始化零值–>设置对象头–>执行init方法
如图:
1.类加载检查
虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用并且检查这个符号引用代表的类是否被加载,解析,初始化过,如果没有,那必须执行相应的类加载过程
new 的指令对应到语言层面上讲: new关键词,对象克隆,对象序列化等
2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完成后便可完成确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来
3.初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值()不包括对象头
4.设置对象头(分不同的操作系统,如32位的,64位的)
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等这些信息,这些信息存放在对象的对象头Object Header之中 在HotSpot虚拟机中,对象在内存中的储存布局可以分为3块区域: 对象头(Header),实例数据(Instance Data)和对齐填充(Padding)
HotSpot虚拟机的对象头包括两部分信息:
第一部分: 储存对象本的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等
第二部分: 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
如下是32 位的对象头
5.执行init()方法
对象按照程序员的意愿进行初始化;对应到语言层面来讲,就是属性赋值(注意:这是程序员自己赋的值)和执行构造方法
5.5.2 从Java源码到编译class到加载整体过程
对象创建的过程,主要经过如下5步:
-
判断类有没有被加载
-
如果没有被加载过(才开始加载类(就是类的加载过程))
-
初始化 :就是给一些变量进行初始化。
-
设置对象头(比较难理解)。
-
执行方法: 对对象进行赋值,和执行构造方法。
这里再从源码.java文件到编译的.class文件到加载,详细描述第2步中的类的加载过程:
加载.class文件的时候 window系统下调用底层的应该jvm.dll文件创建java虚拟机去创建一个引导类加载器(C++实现的) 此时java虚拟机已经创建 此时会调用java实现的类加载器启动 加载loadClass方法加载真正的磁盘文件上面的字节码文件,再去发起调用main()方法,此时程序就启动了
类加载到使用整个过程有如下几步: 加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
1、加载:
在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用 类的main()方法,new对象等等
2、验证:
校验字节码文件的正确性
3、准备:
给类的静态变量分配内存,并赋予默认值 比如Boolean类型默认 false 这些默认值是java虚拟机自己规定的,如果是加final修饰直接就会变成常量 直接赋值了
4、解析:
将符号引用替换为直接引用,就是会将该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链 接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接 引用,下节课会讲到动态链接(在类加载的时候可能不会加载 只有程序运行到这里才会去加载)
5、初始化:
对类的静态变量初始化为指定的值,执行静态代码块