目录
分代收集思想 Minor GC、Major GC、Full GC
JVM概述
为什么学习jvm?
Java Virtual Machine Java虚拟机
面试的需要,基础部分不能区分基础好与坏
对Java程序运行的过程更了解
为后期写出优质代码做好准备
虚拟机
在windows中,虚拟一个运行环境
分为系统虚拟机VMware,程序虚拟机JVM
jvm作用
负责将字节码加载到内存中(运行时数据区)
负责存储数据
把字节码翻译为机器码,执行
垃圾回收
jvm组成部分
-
类加载器(负责加载字节码文件)
-
运行时数据区(存储运行时数据, 堆, java虚拟机栈(运行java自己方法), 方法区, 程序计数器, 本地方法栈)
-
执行引擎(更底层, 把字节码翻译为机器码)
-
本地方法接口
-
垃圾回收
类加载器
作用
负责从硬盘/网络中加载字节码信息,加载到内存中(运行时数据区的方法区中)
每个Java类都有一个引用指它的ClassLoader
数组类不是通过ClassLoader创建的(数组类没有对应的二进制字节流),是由JVM直接生成
类加载过程
加载
使用IO读取字节码文件
转换并存储,为每个类创建一个Class类的对象
存储在方法区中
链接(验证、准备、解析)
验证:对字节码文件格式进行验证,文件是否被污染
对基本的语法格式进行验证
验证阶段阶段主要由四个检验阶段组成:
准备:为静态的变量进行内存分配
public static int value = 123;value在准备阶段后的初始值是0,而不是123
静态常量在编译期间就初始化
特殊情况:比如给 value 变量加上了 final 关键字public static final int value=111
,那么准备阶段 value 的值就被赋值为 111。
解析:将符号引用转为直接引用,将字节码中的表现形式转为内存中表现(内存地址),也就是得到类或者字段、方法在内存中的指针或者偏移量
举个例子:在程序执行中,系统需要明确知道这个方法所在位置,Java虚拟机为每个类都准备了一张方法表来存放该类的所有方法。当需要该类调用一个方法的时候,只要知道这个方法在表中的偏移量就可以使用该方法。通过解析符号引用可以直接转变为目标方法在方法表的位置。
初始化:
类的初始化,为类中定义的静态变量赋值
public static int value = 123;value在初始化阶段后值是123
类什么时候会被加载(初始化)
-
在类中运行main方法
-
创建对象
-
使用类中的静态变量,静态方法
-
反射 Class.forName("类的地址")
-
子类被加载
以下两种情况类不会被初始化:
static final int b = 20; 编译期间赋值的静态常量
System.out.println(User.b);
User[] users = new User[10]; 作为数组类型存在
类卸载
卸载类即该类的Class被GC
卸载类需要满足3个要求:
-
该类的所有实例对象已被GC,也就是堆中不存在该类的实例对象
-
该类没有在其他任何地方被引用
-
该类的类加载器的实例已经被GC
类加载器
具体的负责加载类的一些代码
-
BootstrapClassLoader
(启动类加载器):用c/c++语言开发的,jvm底层的开发语言,负责加载java核心类库,与java语言无关 -
ExtensionClassLoader
(扩展类加载器):java语言编写的,由sun.misc.Launcher$ExtClassLoader实现,继承ClassLoader类,从JDK系统安装目录的jre/lib/ext子目录(扩展目录)下加载类库
-
AppClassLoader
(应用程序类加载器):java语言编写的,面向用户的加载器,派生于ClassLoader类,加载程序中自己开发的类
-
自定义类加载器
public class ClassLoaderDemo {
public static void main(String[] args) {
new String();// 逐级向上委托 最终引导类加载器找到了系统中真正的String
ClassLoader classLoader = String.class.getClassLoader();
System.out.println(classLoader);// null 是由引导类加载器加载的
ClassLoader classLoader1 = ClassLoaderDemo.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader parent = classLoader1.getParent();
System.out.println(parent);//sun.misc.Launcher$ExtClassLoader@14ae5a5
ClassLoader parent1 = parent.getParent();
System.out.println(parent1);//null 引导类加载器
}
}
类加载器规则
JVM启动的时候并不会一次性加载所有的类,而是动态加载,根据需要去加载,这样对内存也更加友好。
对于已经加载的类会放ClassLoader中。在类加载的时候,系统会去先判断当前类是否被加载过,加载过的直接返回,否则才会尝试去加载。也就是说,对于一个类加载器而言,相同名称的二进制类只会被加载一次
双亲委派机制
加载一个类时,先委托给父类加载器加载,如果父类加载器没有找到,继续向上委托,直到引导类加载器.父级找到就返回,父级如果最终没找到,就委派给子级加载器,
最终没有找到,报ClassNotFoundException
为了先确保加载系统类
优点:安全,避免用户自己编写的类替换java核心类,例如java.lang.String
避免类重复加载,父级已经加载该类,就没必要子级再加载
双亲委派机制,是Java提供的类加载的规范,但不是强制不能改变的
我们可以通过自定义的类加载器,改变加载方式
打破双亲委派机制
实现自定义的类加载,继承ClassLoader,如果不想打破双亲委派机制,那就重写findClass()方法即可,无法被父类加载的类都会通过此方法加载
如果想打破双亲委派机制,需要重写loadClass()方法,重写后我们就可以改变传统的双亲委派,在子类委派给父类加载前,自己先加载此类。在尝试从其他地方加载
典型的tomcat中,加载部署在tomcat中的项目时,就使用的是自己的类加载器,优先加载Web目录下的类,然后加载其他目录下的类,自定义了WebAppClassLoader来打破这种机制
运行时数据区
1.程序计数器
是一块很小的内存空间,用来记录每个线程运行的指令位置,字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制
是线程私有的,每个线程都拥有一个程序计数器,生命周期与线程一致
是运行时数据区中,唯一一个不会出现内存溢出的空间
运行速度最快
2.本地方法栈
用来运行本地方法的区域,为虚拟机使用到的 Native 方法服务
是线程私有的
空间大小可以调整
可能会出现栈溢出以及内存溢出
3.Java虚拟机栈
基本运行特征:
栈是运行单位,管理方法的调用运行
是用来运行java方法的区域
可能会出现栈溢出(StackOverFlowError
)以及内存溢出(OutOfMemoryError
)
-
StackOverFlowError
:若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过Java虚拟机栈的深度,就会抛出该异常,例如死循环 -
OutOfMemoryError
:如果栈的内存大小允许动态扩展,但是虚拟机在动态扩展的时候无法申请到足够的内存空间,就会抛出该异常
是线程私有的
生命周期和线程相同,创建而创建,死亡而死亡
运行原理:
先进后出的结构,每一次方法调用的时候都会有一个对应的栈帧被压入栈中,方法调用结束后,都会有一个栈帧被弹出
最顶部的称为当前栈帧
栈帧结构:
一个栈帧包含:
局部变量表(存储在方法中声明的变量以及编译期可知的各种数据类型)
操作数栈(实际计算运行,计算过程中产生的临时变量也会放在这里)
动态链接(为了将符号引用转为调用方法的直接引用这个过程)
方法返回地址
4.堆
基本作用特征
是存储空间,用来存储对象,几乎所有的实例对象和数组都在这里分配内存,是内存空间最大的一块区域
在jvm启动时就被创建,大小可以调整(jvm调优)
本区域是存在垃圾回收的,是线程共享的,会出现OutOfMemoryError异常
堆空间的分区
-
年轻代(新生区/新生代)
-
伊甸园区(对象刚刚创建存储在此区域)
-
幸存者1区
-
幸存者2区
-
-
老年代(老年区)
为什么要分区
-
可以根据对象存活的时间放在不同的区域,可以区别对待
-
频繁回收年轻代,较少回收老年代
创建对象,在堆内存中分布
1.新创建的对象,都存储在伊甸园区
2.当垃圾回收时,将伊甸园中垃圾对象直接销毁,将存活的对象移动到幸存者1区
3.之后创建的新对象还是存储在伊甸园区,再次垃圾回收到来时,将伊甸园中的存活对象移动到幸存者2区,同样将幸存者1区的存活对象移动到幸存者2区,每次保证一个幸存者区为空的,相互转换
4.每次垃圾回收时,都会记录此对象经历的垃圾回收次数,当一个对象经历过15次回收,仍然存活,就会被移动到老年代
垃圾回收次数,在对象头中有一个4bit的空间记录 最大值只能是15
5.老年区回收次数较少,当内存空间不够用时,才会去回收老年代
堆空间的配置比例
默认的新生代与老年代的比例:1:2 可以通过 -XX:NewRatio=2 进行设置
如果项目中生命周期长的对象较多,就可以把老年代设置更大
在新生代中,伊甸园区和两个幸存者比例:8:1:1
可以通过-XX:SurvivorRatio=8进行设置
对象垃圾回收的年龄-XX:MaxTenuringTreshold,默认是15
5.方法区
作用: 主要用来存储加载的类信息, 以及即时编译期编译后的信息, 以及运行时常量池
特点: 在jvm启动时创建,大小也是可以调整,是线程共享,也会出现内存溢出
方法区,堆,栈交互关系
方法区存储类信息(元信息)
堆中存储创建的对象
栈中存储对象引用
方法区大小设置
-XX:MataspaceSize 设置方法区的大小
windows jdk默认的大小是21MB
也可以设置-XX:MaxMataspaceSize为-1,没有限制 就可以使用计算机内存
可以将初始值设置较大一点,减少了Full GC发生
方法区的内部结构
类信息
即时编译期编译后的信息,
运行时常量池(指的是类中各个元素的编号)
方法区的垃圾回收
在FULL GC时方法区发生垃圾回收
主要是回收类信息,类信息回收条件比较苛刻,满足以下3点即可:
1.在堆中,该类及其子类的对象都不存在了
2.该类的类加载器不存在了
3.该类的Class对象不存在了
也可以认为了类一旦被加载,就不会被卸载
特点总结
程序计数器,java栈,本地栈是线程私有的
程序计数器不会出现内存溢出
java栈,本地栈可能会出现内存溢出
java栈,本地栈大小是可以调整的
堆,方法区是线程共享的,是会出现垃圾回收的
字符串常量池
是JVM为了减少内存消耗专门为字符串开辟的一个区域,主要目的是为了避免字符串重建
// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true
在jdk7之后,将字符串常量池的位置从方法区转移到堆空间中
为什么将字符串常量池移动到堆中?
因为方法区的GC回收频率太低,只在FULL GC的时候进行GC,Java程序中通常有大量的字符串创建死亡,将字符串放到堆中,能够高效及时的回收
本地方法接口
什么是本地方法
用native关键字修饰的方法称为一个本地方法,没有方法体
hashCode();
为什么用本地方法
java语言需要与外部的环境进行交互(例如需要访问内存,硬盘,其他的硬件设备),直接访问操作系统的接口即可
java的jvm本身开发也是在底层使用了C语言
执行引擎
作用:将加载到内存中的字节码(不是直接运行的机器码),解释/编译为不同平台的机器码,
.java-->编译-->.class 在开发期间,由jdk提供的编译期(javac)进行源码编译(前端编译)
.class(字节码)-->解释/编译-->机器码(后端编译,在运行时,由执行引擎完成的)
解释器:将字节码逐行解释执行,效率低
编译器(JIT just in time 即时编译器):将字节码编译,缓存起来,执行更高效,不会立即使用编译器
将一些频繁执行的热点代码进行编译,并缓存到方法区中,以后执行效率提高了
程序启动后,先使用解释器立即执行,省去了编译时间
程序运行一段时间后,对热点编译缓存,提高后续执行效率
采用的解释器和编译器结合的方案
对象的创建(重要!重要!重要!)
-
类加载检查:虚拟机遇到一个new指令,会先去检查在常量池中是否能找到其符号引用,并且检查是否被类加载过,如果没有,需要先进行类加载
-
分配内存:类加载检查通过后,就开始给对象分配内存。对象所需内存大小在类加载过后便可确定
-
初始化零值:内存分配完后,虚拟机需要将分配到内存空间都初始化零值(不包括对象头),这一步是为了实例字段在Java代码中不赋初值就可以使用,程序可以直接访问这些字段的零值
-
设置对象头:初始化零值后,虚拟机对对象开始设置对象头,包含分代年龄,哈希码,类的元数据信息等
-
执行init方法:上面的操作一个新的对象诞生,但是Java的角度还需要执行init方法,将对象按照程序员的意愿进行初始化
对象引用
强引用 即对象有引用指向的,例如Object obj=new Object()
这种情况下new出来的对象 哪怕报OOM,都不会被垃圾回收
区别于软、弱、虚引用
软、弱、虚引用都是用来标记对象的一状态
当一些对象称为垃圾后,还需要有不同的状态,可以继承SoftReference、WeakReference、PhantomReference
或者把自己的对象添加到软、弱、虚对象中
软引用(SoftReference) 如果内存充足的情况下,可以保留软引用的对象
如果内存不够,在进行一次垃圾回收后,还是不够,那么就会清除软引用对象
弱引用(WeakReference) 弱引用管理的对象 无论内存空间是否充足,只要垃圾回收发现,就会回收它
虚引用(PhantomReference) 相当于没有任何引用 只是为了系统检测,能在这个对象被收集器回收时收到一个系统通知。也就是跟踪对象被垃圾回收的活动的。
垃圾回收
概述
java是支持自动垃圾回收,有些语言不支持需要手动
自动垃圾回收不是java语言首创的
垃圾回收关系的问题:
哪些区域需要回收 堆 方法区
什么时候回收
如何回收
java自动垃圾回收经过长时间的发展,已经非常强大
分代收集思想 Minor GC、Major GC、Full GC
我们知道,对象会优先在Eden区分配,如果伊甸园区空间满了,会先进行Minor GC/yong GC,对年轻代进行垃圾回收称为 Minor GC/yong GC 是频繁进行回收
但是在Minor GC前,会进行空间分配担保,确保老年代有足够的空间容纳新生代的对象,有的话进行Minor GC,不够的话会进行FULL GC
如果Minor GC后,伊甸园区还是不够,会将对象放在幸存者区,若不够,就会直接放在老年代中,后面如果还有新生对象,还是放在伊甸园区中
老年代中如果空间不够,则会进行FULL GC,对老年代进行垃圾回收称为 Major GC/ old GC 回收的次数较少
总结:针对虚拟机垃圾收集主要分为两种:
-
部分收集
-
新生代收集(Minor GC / Young GC):只针对新生代的垃圾收集
-
老年代收集(Major GC / Old GC):只对老年代收集,
-
混合收集(Mixed GC):对整个新生代和部分老年代收集
-
-
整堆收集 (Full GC):收集整个 Java 堆和方法区(尽量避免)
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象,例如字符串、数组
虚拟机要求大对象直接进入老年代,旨在减少新生代垃圾回收频率和成本
什么样的对象是垃圾
在运行过程中,没有被任何引用指向的对象,被称为垃圾对象
为什么需要GC?
如果不及时清理这些垃圾对象,会导致内存溢出
在回收时,还可以将内存碎片进行整理.(数组必须是连续空间的)
内存溢出和内存泄漏
内存溢出:经过垃圾回收后,内存中仍然无法存储新创建的对象,内存不够用溢出了
内存泄漏:IO流 close jdbc close 没有关闭,生命周期很长的对象, 一些不用的对象,但是垃圾回收器不能判定为垃圾,这些对象就默默地占用着内存,称为内存泄漏,大量的此类对象存在,也是导致内存溢出的原因
自动内存管理
好处:解放程序员,对内存管理更合理,自动化
坏处:对程序员管理内存的能力降低了,解决问题的能力变弱了,不能调整垃圾回收的机制
垃圾回收相关算法
标记阶段
作用:判断对象是否是垃圾对象, 是否有引用指向对象.
相关的标记算法:引用计数算法和可达性分析算法
引用计数算法(在现代的jvm中并没有被使用)
有个计数器来记录对象的引用数量
String s1 = new String("aaa");
String s2 = s1;//有两个引用变量指向aaa对象
s2 = null; -1
s1 = null; -1
优点:实现简单,效率高
缺点:
需要维护计数器,占用空间,频繁操作计数器需要时间开销
无法解决循环引用问题.多个对象之间相互引用,没有其他外部引用指向他们,计数器都不为0,不能回收,产生内存泄漏
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
可达性分析算法/根搜索算法
实现思路:从一些为根对象(GCRoots)的对象出发去查找,与根对象直接或间接连接的对象就是存活对象,不与根对象引用链连接的对象就是垃圾对象,此方法可以解决循环引用问题
GC Roots 可以是哪些元素?
-
在虚拟机栈(栈中的局部变量表)中引用的对象
-
在本地方法栈中引用的对象
-
在方法区中存储的静态成员指向的对象
-
方法区中常量引用的对象
-
所有被同步锁持有的对象
对象的 finalization 机制
当一个对象被标记为垃圾后,在真正被回收之前,会调用一次Object类中的finalize(). 是否还有逻辑需要处理
自己不要在程序中调用finalize(),留给垃圾回收器调用
有了finalization机制的存在,在虚拟机中把对象状态分为3种:
-
可触及的 不是垃圾,与根对象连接的
-
可复活的 判定为垃圾了,但是还没有调用finalize(),(在finalize()中对象可能会复活)
-
不可触及的 判定为垃圾了,finalize()也被执行过了,这种就是必须被回收的对象
垃圾回收阶段的算法
标记-清除算法
首先标记出所有不需要回收的对象,在标记完后统一回收掉所有没有被标记的对象。也是最基础的收集算法
优点:实现简单
缺点:标记和清除两个过程效率不高,清除后会产生大量不连续的内存碎片
标记-复制算法
将内存分为大小相等的两份空间, 把当前使用的空间中存活的对象, 复制到另一个空间中,将正在使用的空间中垃圾对象清除
优点:减少内存碎片
缺点:如果需要复制的对象数量多,效率就低
适用场景:存活对象少 新生代适合使用标记复制算法
标记-压缩算法(标记-整理)
标记过程还是标记不需要回收的对象,然后将所有存活的对象向一端移动,清理边界外的所有空间,可以认为是标记-清除算法执行完后,进行了一次内存碎片整理
优点:消除了内存区域分散特点,消除了内存减半高额代价,适合老年代这种收集不频繁的场景
缺点:效率低于复制算法
如果对象被其他对象引用,还需调整引用的地址
需要全程暂停用户应用程序,即STW
垃圾回收器
垃圾收集器是垃圾回收的实际实现者,垃圾回收算法是方法论
垃圾回收器分类
Serial收集器
串行收集器,是一个单线程的收集器,垃圾收集只会使用一个线程去完成垃圾收集工作。所以其在进行垃圾收集的时候需要暂停其他所有的工作线程(Stop The World),直到它收集结束
新生代采用标记-复制算法,老年代采用标记-整理算法。
优点:相比于其他收集器的单线程相比更加简单高效,因为没有线程交互的开销,对于运行在 Client 模式下的虚拟机来说是个不错的选择。
ParNew收集器
ParNew 收集器是Serial收集器的多线程版本,除了使用多线程进行垃圾回收,其他行为和Serial收集器完全一样
优点:是许多运行在 Server 模式下的虚拟机的首要选择,除了Serial 收集器外,只有它能与 CMS 收集器配合工作
Serial Old收集器
Serial 收集器的老年代版本,也是单线程收集器,用途是作为 CMS 收集器的后备方案
Parallel Scavenge收集器
也是多线程收集器,关注的是吞吐量(高效利用CPU),优点是提供多个参数给用户找到最合适的停顿时间或最大吞吐量
吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。
这是jdk1.8默认收集器
Parallel Old收集器
Parallel Scavenge 收集器的老年代版本。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是获取最短回收停顿时间为目标,注重在用户体验上,也是第一款真正意义上的并发收集器,第一次实现让垃圾回收和用户线程同时工作,是一种 “标记-清除”算法实现的
-
初始标记:暂停其他现线程,记录下与root相连的对象,速度很快
-
并发标记:垃圾回收线程和用户线程同时执行
-
重新标记:为了解决并发标记时用户线程导致的部分对象标记变化的情况
-
并发清除:开启用户线程,并且开始垃圾回收
优点:并发收集、低停顿
缺点:无法处理浮动垃圾(在清理垃圾时 用户线程也在工作 无法确定某个对象是否是垃圾)
使用的是标记清除算法,会导致大量的空间碎片
三色标记:
由于CMS有并发执行过程,所以在标记对象时有不确定性.
所以在标记时,将对象分为3中颜色(3中状态)
黑色:例如GCRoots 确定是存活的对象
灰色:在黑色对象中关联的对象,其中还有未扫描完的,之后还需要再次进行扫描
白色:与黑色,灰色对象无关联的,垃圾收集算法不可达的对象
标记过程:
1.先确定GCRoots, 把GCRoots标记为黑色
2.与GCRoots关联的对象标记为灰色
3.再次遍历灰色,灰色变为黑色,灰色下面有关联的对象,关联的对象变为灰色
4.最终保留黑色,灰色,回收白色对象
可能会出现漏标,错标问题
漏标:
假设 GC 已经在遍历对象 B 了,而此时用户线程执行了 A.B=null的操作, 切断了 A 到 B 的引用,本来应该回收BDE垃圾,但是B已经是灰色,所以只能回收DE
错标:
假设 GC 线程已经遍历到 B 了,此时用户线程执行了以下操作:
B.D=null;//B 到 D 的引用被切断
A.xx=D;//A 到 D 的引用被建立
G1收集器
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
G1收集器的运作大致是:
-
初始标记
-
并发标记
-
最终标记
-
筛选回收
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的区域(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
ZGC收集器
-
停顿时间不超过10ms,ZGC在标记、转移和重定位阶段几乎都是并发的;
-
停顿时间不会随着堆的大小,或者活跃对象的大小而增加,只依赖于GC Roots集合大小;
个人理解:ZGC的STW阶段只有初次的三个阶段,也就是初始标记,再标记,初始清理,之后就可以并发清理了,而G1不管是初始还是后面的多次在STW的阶段还是要STW