JVM复习重点(待续)
1. JVM的主要组成部分和作用
JDK1.8之前:
1)运行时数据区域:
程序计数器和虚拟机栈和本地方法栈都是线程私有的。
- 程序计数器:
- 线程执行java代码时,存放虚拟机字节码的指令地址,执行Native代码时,值为空。可以看着做字节码的行号指示器。
- Java虚拟机栈:
- 每个虚拟机栈执行方法执行时也会创建线程私有的栈帧。
- 存放java方法执行的内存模型。栈帧:局部变量表,操作数栈,动态链接,方法出口。
- 局部变量表存放了编译器可知的八种数据类型和对象引用。
- 局部变量表存放基本数据类型、对象引用、returnAddress类型。
- 本地方法栈:
- 为Native方法提供服务。
Java的堆、方法区都是线程共享的。
-
Java堆:
- 存放对象实例和数组。
- 垃圾回收的主要区域,称为GC堆,采用分代收集算法,分为新生代和老年代。
-
方法区:
- 存放加载的类信息、常量、静态变量(以static修饰的变量和方法)、即时编译器编译的代码。
- 运行时常量池是方法区的一部分。存放编译器生成的各种字面量和符号引用。
(这个和堆中的字符串常量池有什么关系?应该是上述常量的一部分)
-
虚拟机规范可以选择不实现垃圾收集。
2)本地库接口:与本地库接口交互
3)类加载器:根据类的全限定名,将类装载到方法区
4)执行引擎:包括JIT编译器和GC垃圾回收器。
JDK 1.8及之后, 方法区被元空间(Metaspace)取代,是直接内存的一部分。
一个java指令的运行过程:
Java代码(.java)-编译器javac - 字节码文件(.class)- 类加载器 - 内存方法区并封存一个类对象 - 执行引擎翻译 - 底层指令
堆栈的区别:
- 物理地址:堆不连续、栈连续
- 内存:堆的内存分配到运行期确定,大小不固定;栈是连续的,分配的大小在编译器确定
- 内容:栈存放局部变量、操作数栈、返回值,堆存放对象的实例和数组。
- 注意,静态变量和静态方法位于方法区,但是其所在类的对象依然位于堆。
2. (HotSpot虚拟机中)对象的创建:
- 类加载:指令中的参数是否能在堆中的常量池中定位到一个类的符号引用。没有,执行类加载。
- 堆中分配空间:对象所需的内存在类加载这一步就可以确定,通过指针碰撞法或者空闲列表法在堆中分配空间。
- 保证安全:使用两种方式保证划分空间的动作是安全的。
- 初始化:将分配到的空间初始化为零值。
- 写入对象头:将对象是那个类的实例,如何找到类的元数据信息、对象哈希码、对象的GC分代信息写入对象头。
- 执行init方法。
注意:init是实例构造器,调用实例的构造方法;clinit是类构造器。
对象在内存中存储的布局可以分为三部分:对象头、实例数据和对齐填充。其中对象头又包括两部分,一部分是运行时信息:hash码、GC分代信息、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳;另一部分是类型指针,指向类的元数据(位于方法区)。
3.为对象分配内存
指针碰撞法:Java堆是规整的,用过的在一边,空闲的在另一边,分配内存时将指针指示器向空闲一端移动和对象大小相等的距离。
空闲列表法:如果Java堆不是规整的,需要由虚拟机维护一个列表来记录哪些内存可用,需要的时候,从列表中查询合适的内存分配给对象,更新列表记录。。
Java堆是否规整由Java使用的垃圾回收器是否带有压缩功能决定,如:标记-整理法的Java堆就是规整的。
4.处理并发安全问题
对象的创建在并发条件下会导致线程的不安全(原因看单例模式)。两种解决方法:
-
对分配内存空间的动作进行同步处理(CAS+失败重试来保证操作的原子性)
-
内存分配按照线程划分在不同空间中进行,每个线程在Java堆中预先分配内存,叫“本地线程分配缓冲”
5.对象的访问定位
java程序使用栈上的reference数据来操作堆上的具体对象。
- 句柄方式:栈的ref指向堆中的句柄池,句柄池有指向堆中对象实例和指向方法区的类型数据的指针。优点:对象实例被移动时,句柄不用修改。
- 直接指针方式,ref直接指向堆中的对象实例,对象实例的对象头中保存指向方法区类型数据的指针。优点:快。
6.对象创建的不同方式
使用new关键字 - 调用构造函数
使用Class类的newInstance() - 调用构造函数
使用Constructor类的newInstance() - 调用构造函数
使用clone() - 没有调用构造函数
使用反序列化 - 没有调用构造函数
7.简述垃圾回收机制
Java中不需要显式释放对象内存,由虚拟机自动执行。GC线程是专门用于回收垃圾的线程,低优先级,在虚拟机空闲或者堆内存不足时,扫描不需要的对象,添加到待回收的集合中,执行回收。
垃圾回收机制,有效得防止了内存泄露,可以有效地使用可以使用得内存。
8.确认哪些对象可以回收的算法
- 引用计数法。有一个地方引用,引用计数+1;引用失效,引用计数-1;(很难解决对象之间的循环引用问题)
- 可达性算法。设定GCroot,从节点往下搜索,搜索走过的路径称为引用链。当一个对象到GCroot没有引用链,称这个对象不可达。
- GC Roots对象有:
- 虚拟机栈中(本地变量表)中引用的对象。
- 方法区中类静态变量引用的对象。
- 方法区中常量引用的对象。
- Native方法引用的对象。
- GC Roots对象有:
引用可以分为:强引用、软引用、弱引用、虚引用。
9.永久代(方法区)的垃圾回收
垃圾回收主要发生在堆区,其实方法区也有垃圾回收。永久代的回收主要包括废弃常量和无用的类。永久代满了或者超过临界值,会触发Full GC完全垃圾回收。(JDK8 中移除了永久代,新加了叫元数据区的native内存区)
10.垃圾回收算法:
- 标记-清除算法:先标记,后清除。缺点:会产生大量的内部碎片。
- 复制算法:将内存按照容量分为两块,每次将存活的对象复制到另一块上。(新生代)
- 标记-整理算法:先标记,再清除,后整理,让存活的对象往一侧移动,清理掉端边界以外的内存。
- 分代收集算法:新生代使用复制算法(死亡率高),老年代使用标记算法。
HotSpot的垃圾回收机制的实现:
- 使用OopMap数据结构记录所有GC Roots。
- 只有在安全点才生成OopMap,进入GC。
11.分代收集算法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GfKAkm36-1623304051849)(Java虚拟机第二版.assets/虚拟机分代算法.png)]
新生代-老年代-永久代 。新生代:老年代 = 1:2;新生代中的Eden: To Survivor: From Survivor = 8:1:1.
对象的分配主要在堆上,主要在Eden区,如果启动了本地线程分配缓冲,则优先分配在TLAB上。
对象优先在新生代的Eden区分配,当Eden区没有足够空间分配时,就会发起一次Minor GC;如果Minor GC后还是没有足够的空间,启动分配担保机制在老年代中分配内存。
如果对象在Eden区出生,能被Survivor容纳,将进入Survivor空间,对象年龄+1.
大对象直接在老年代分配。因为新生代是复制算法,这样可以避免大对象在新生代反复复制。
长期存活的对象将进入老年代。
12.分代垃圾回收算法
-
把 Eden + From Survivor 存活的对象放入 To Survivor 区;
-
清空 Eden 和 From Survivor 分区;
-
From Survivor 和 To Survivor 分区交换。
-
每次gc在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。
关于Minor GC 和 Full GC
- 发生在新生代的GC,频繁,速度快
- 发生在老年代的GC,至少伴随一次Minor GC,速度很慢。一般使用标记-整理算法。
13.class文件中包含的内容
-
常量池
-
访问标志
-
类索引、父类索引、接口索引集合
-
字段表集合
常量池可以理解为class文件中的资源仓库。
常量池中存放了两大类常量:字面量和符号引用。
其中,字面量包括:
- 文本字符串
- 声明为final的常量值。
符号引用包括:
- 类和接口的全限定名。
- 字段的名称和描述符。
- 方法的名称和描述符。
字段表集合:包括了类级变量和实例级变量
字段可以包含的信息:作用域、类还是实例变量(static)、可变性(final)、并发可见性(volatile)、可被序列化(transient)、字段数据类型。
14. 必须进行类初始化的几种情况(初始化一定会导致类加载)
Java语言,动态可拓展,先编译成class字节码文件,在运行时再执行动态加载(加载到jvm)和动态链接。
java的一个class文件对应一个类或者一个接口,类的生命周期一般包括加载、验证、准备、解析、初始化、使用、卸载。
JVM规范规定了初始化的几种情况,加载、验证、准备一定要在此之前。
①new一个对象;读取或者设置一个类的静态字段(final修饰的常量在编译期放入常量池,除外),调用类的静态方法。
②使用reflect包反射调用
③调用一个类时发现其父类没有初始化
④main所在的类
⑤Methodhandle
其余的引用方式都不会发生初始化,称为被动引用。而上述导致初始化的方式称为主动引用。
15. java类加载
Java的类加载机制,指的是JVM把类的数据从Class文件加载到内存方法区,并且对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接只用的java类型。
加载:
① 通过类的全限定名获得二进制字节流(zip包、class文件、网络、动态代理技术)(这一步是由类加载器决定的)
②将类的静态存储结构转为方法区的运行时数据结构。
③生成代表这个类的Class对象,作为方法区这个类的各个数据的返回入口。
验证:
文件格式、元数据、字节码、符号引用验证
准备:
给类变量(static修饰的静态变量)在方法区分配内存并且初始化为0。
解析:
将常量池中的符号引用替换为直接引用的过程。
初始化:
真正执行字节码,执行类构造器()方法,收集类变量的赋值动作和静态语句块合并而成。(和实例构造器不同,不需要显式调用父类构造器)
16.四种类加载器
- 启动类加载器:用于加载Java核心类库
- 扩展类加载器:用于加载Java扩展库
- 应用程序类加载器:根据全类名加载Java类
- 自定义类加载器:继承自java.lang.ClassLoader
17.双亲委派模型
上述四种加载器是父类和子类继承的关系。每一个类+其类加载器,一起确定了在JVM的唯一性。
双亲委派模型:一个类加载器获得类加载请求,不会自己加载这个类,而是把这个请求委派给父类。当传送到最顶层,只有当最顶层的加载器无法完成加载时,子加载器才会加载这个类。
用父类构造器)
16.四种类加载器
- 启动类加载器:用于加载Java核心类库
- 扩展类加载器:用于加载Java扩展库
- 应用程序类加载器:根据全类名加载Java类
- 自定义类加载器:继承自java.lang.ClassLoader
17.双亲委派模型
上述四种加载器是父类和子类继承的关系。每一个类+其类加载器,一起确定了在JVM的唯一性。
双亲委派模型:一个类加载器获得类加载请求,不会自己加载这个类,而是把这个请求委派给父类。当传送到最顶层,只有当最顶层的加载器无法完成加载时,子加载器才会加载这个类。
好处:保证Java运作的稳定性。