一、JVM概述:
为什么学习JVM:
面试的需要,基础部分不能区分基础好坏
对JAVA程序运行的过程更加了解
为后期写出优质的代码做好准备
项目管理、性能调优
虚拟机:
在Windows中,虚拟一个运行环境
系统虚拟机:VMware
可运行完整操作系统的软件平台
程序虚拟机:JVM
专门执行某个单个计算机指令
jvm作用:
负责将字节码装载到java内部(运行时效数据区)
负责存储数据
把字节码翻译为机器码,执行
垃圾回收
JVM特点:
一次编译到处运行
自动内存管理
自动垃圾回收功能
可执行多语言编译后的字节文件
jvm组成部分:
1.类加载器(负责加载字节码文件)
2.运行时数据区(存储运行时数据,堆,java虚拟机栈(运行java自己的方法),方法区,程序计数器,本地方法栈)
3.执行引擎(更底层,把字节码翻译成机器码)
4.本地方法接口
5.垃圾回收
二、JVM结构-类加载
2.1类加载子系统
作用:
负责从硬盘/网络中加载字节码信息
加载到内存中(运行时数据区的方法区中)
2.2类加载过程
2.2.1加载
使用IO读取字节码文件、
转换并存储,为每个类创建一个Class类的对象
存储在方法区中
2.2.2链接(验证,准备,解析)
验证:对字节码格式进行验证,文件是否被污染。
对基本的语法格式进行验证。
准备:为静态的变量进行内存分配
public static int value = 123;value 在准备阶段后的初始值是 0,而不是 123
不包含用final修饰的static常量,静态常量在编译期间进行初始化
2.2.2解析:
将类的二进制数据中的符号引用替换成直接引用。
将字节码中的表现形式,转化为内存中的表现(内存地址)
2.2.3初始化
类的初始化,为类中的定义的静态变量进行赋值。
public static int value = 123;value 在初始化阶段后值是 123.
类什么时候会被加载(初始化)
1.在类中运行main方法
2.创建对象
3.使用类中的静态变量、静态方法
4.反射Class.forName("类的地址")
5.子类被加载
以下两种情况不会被初始化:
编译期间赋值的静态常量
作为数组
2.3类加载器分类
具体负责加载类的一些代码
2.3.1引导类加载器(启动类加载器)
用C/C++语言开发,jvm底层的开发语言,负责加载java核心类库,与java语言无关的。
2.3.2扩展类加载器
java 语言编写的,由 sun.misc.Launcher$ExtClassLoader 实现,继承ClassLoader类.
从 JDK 系统安装目录的 jre/lib/ext 子目录(扩展目录)下加载类库
2.3.3应用程序类加载器
Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现. 派生于 ClassLoader 类.
加载程序中自己开发的类
2.3.4自定义加载器
2.4双亲委派机制
加载一个类时,先委托给父类加载器加载,如果父类加载器没有找到,继续向上级委托,直到引导类加载器。
父级找到就返回,父级如果没有找到,基于委派给子级加载器,最终没有找到,报ClassNotFoundException.
为了先确保加载系统类
双亲委派机制,是java提供的类加载的规范,但不是强制不能改变的。
可以通过自定义的类加载器,改变加载方式。
打破双亲委派机制:
可以通过继承ClassLoader类,重写loadClass/findClass方法,实现自定义的类加载
典型的tomcat中,加载部署在tomcat中的项目时,就使用的是自己的类。
三、运行时数据区
3.1程序计数器
是一块很小的内存空间,用来记录每个线程运行的指令位置,是线程私有的,每个线程都拥有一个程序计数器,声明周期与线程一直,是运行时数据区中,唯一一个不会出现内存溢出的空间。
运行速度最快。
3.2.本地方法栈
用来运行本地方法的区域
是线程私有
内存空间大小可以调整
可能会出现栈溢出
3.3java栈
基本作用:
栈是运行单位,管理方法的调用。
是用来运行java方法的区域。
可能会出现栈溢出。
是线程私有的。
运行原理:
先进后出
最顶部的称为当前栈帧
栈帧结构:
一个栈帧中包含:
局部变量表(存储在方法中声明的变量)
操作数栈(实际计算运行)
动态链接
程序计数器,java栈,本地栈是线程私有的
程序计数器不会出现内存溢出
java栈,本地栈可能会出现内存溢出
java栈,本地栈大小是可以调整的
3.4堆
基本作用特征:
是存储空间,用来存储对象,是内存空间最大的一块区 域,
在JVM启动时就被创建,大小可以调整(JVM调优)。
本区域是存在垃圾回收的,是线程共享的区域。
堆空间的分区:
年轻代(新生区/新生代)
伊甸园区(对象刚刚创建存储在此区域)
幸存者1
幸存者2
老年代(老年区)
为什么要分区
可以根据对象的存活时放在不同的区域,可以区别对待。
频繁回收年轻代,较少回收老年代。
创建对象,在堆内存中分布
1.新创建的对象,都存储在伊甸园区
2.当垃圾回收时,将伊甸园中垃圾对象直接销毁,将存活的对象,移动到幸存者1区
3.之后创建的新对象还是存储在伊甸园区,当垃圾回收到来时,将伊甸园中的存活对象移动到幸存者2区,同样将性幸存者每次保证一个幸存者区为空的,相互装换。
4.每次垃圾回收时,都会记录此对象经历的垃圾回收次数,当一个对象经历过15此回事,任然存活,就会被移动到老年代
垃圾回收次数,在对象头中有一个4bit的空间记录,最大值只能是15;
5.老年区回收次数较少,当内存空间不够用时,才回去回收老年代。
堆空间的配置比例:
默认的新生代与老年代的比例:1:2,可以通过-XX:NewRatio=2进行设置
如果项目中生命周期长的对象较多,就可以把老年代设置更大。
新生代中,伊甸园和两个幸存者区比例:8:1:1,
可以通过-XX:SurvivorRatio=8进行设置。
对象垃圾回收年龄:-XX:MaxTenuringThreshhold=<N>
分代收集思想Minor GC、Major GC、Full GC
对年轻代进行垃圾回收称为Minor GC/yong GC 是频繁进行的回收;
对老年代进行垃圾回收称为Major GC/old GC 回收次数较少;
Full GC 整堆收集,尽量避免;
System.gc();时
老年区空间不足时;
方法区空间不足时;
字符串常量池:
在jdk7之后,将字符串常量池的位置从方向区转移到了堆空间中,因为方法区的回收在整堆收集时发生,回收频率低;
3.5方法区
作用:主要用来存储加载的类信息,以及及时编译期编译后的信息,以及运行时常量池;
特点:在jvm启动时创建,大小也是可以调整,是线程共享,也会出现内存溢出;
方法区、堆、栈交互关系:
方法区存储来信息(元信息)
堆中存储创建的对象
栈中存储对象引用
方法区大小设置:
-XX:MetaspaceSize设置方法区的大小
Windows jdk默认值的大小是21MB
也可以设置为-XX:MaxMetaspaceSize的值是-1,没有限制,就可以使用计算机内存;
可以将初始值设置较大一点,减少FULL GC发生
方法区的内部结构:
类信息
以及即时编译后的信息
以及运行时常量池(指的就是类中各个元素的编号)
方法区的垃圾回收:
在FULL GC时方法区发生垃圾回收
主要是回收类信息,类信息回收条件比较苛刻,满足一下3点即可:
1.在堆中,该类及其子类的对象都不存在了
2.该类的类加载器不存在了
3.该类的Class对象不存在了
也可以认为类一旦被加载就不会被卸载了。
特点总结:
程序计数器,java栈,本地栈是线程私有的
程序计数器不会出现溢出
java栈,本地栈,堆,方法区可能会出现内存溢出
java栈,本地栈,堆,方法区大小是可以调整的
堆,方法区是线程共享的,是会出现垃圾回收的
本地方法接口:
什么是本地方法:
用native关键字修饰的方法称为一个本地方法,没有方法 体。hashCode();
为什么用本地方法:
java语言需要与外部的环境进行交互(例如需要访问内存, 硬盘,其他的硬件设备),直接访问操作系统的接口即可。
java的jvm本身开发也是在底层使用到了C语言
执行引擎:
作用:将加载到内存中的字节码(不是直接运行的机器码),解释/编译为不同平台的机器码;
Java--编译--.class 在开发期间,由JDK提供的编译器(javac)进行源码编译(前端编译);
.class(字节码)-->解释/编译-->机器码(后端编译,在运行时,由执行引擎完成的)
翻译器:将字节码逐行解释执行
编译器(JIT即时编译器):将字节码编译,缓存起来,执行 更高效,不会立即使用编译器,将一些频繁执行的热点代码 进行编译,并缓存到方法区中,以后执行效率提高了。
程序启动后,先使用解释器立即执行,省去了编译时间;
程序运行一段时间后,对热点编译缓存,提高后续执行效率
采用的解释器和编译器结合的方案。
垃圾回收:
概述:
java是支持自动垃圾回收,有些语言不支持需要手动。
自动垃圾回收不是java语言首创的
垃圾回收关系的问题:
哪些区域需要回收 堆 方法区
什么时候回收
如何回收
java自动回收
java的自动垃圾回收经过长时间的发展,已经非常强大;
什么样的对象是垃圾
在运行过程中,平没有被任何引用指向对象,被称为垃圾对象。
为什么需要GC
如果不及时清理这些垃圾对象,会导致内存溢出。
在回收时,还可以将内存碎片进行整理(数组必须是连续空间的)
内存溢出和内存泄漏
内存溢出:经过垃圾回收后,内存中任无法存储创建的对象,内存不够用溢出;
内存泄漏:IO流close jdbc连接close没有关闭,声明周期很长的对象,一些已经不用的对象,但是垃圾回收器不能判定为垃圾,这些对象就默默的占用的内存,称为内存泄漏,大量的此类对象存在,也是导致内存溢出的原因;
自动内存管理:
好处:解放程序员,对内存管理更合理,自动化;
缺点:对程序员管理内存的能力降低了,解决问题能力变弱了,不能调整垃圾回收的机制;
垃圾回收相关算法:
标记阶段:
作用:判断对象是否为垃圾对象,是否有引用指向对象。
相关的标记算法:引用计数算法和可达性分析算法
引用计数算法(在现代的jvm中并没有被使用)
有个计数器来记录对象的引用数量
String s1 = new String("aaa");
String s2 = s1; //有两个引用变量指向aaa对象
s2 = null; -1
s1 = null; -1
缺点:需要维护计数器,占用空间,频繁操作需要事件开销;
无法解决循环问题。多个对象之间相互引用,没有其他哇爱不引用指向他们,计数器都不为0,不能回收,产生内存泄漏。
可达性分析算法/根搜索算法
实现思路:从一些为根对象(GCRoors)的对象触发去寻找,与根据对象直接或间接连接的对象就是存活对象,不与根对象引用链连接的对象就是垃圾对象。
GCRoots可以是哪些元素的?
在虚拟机栈中被使用的。
在方法中存储的静态成员指向的对象
作为同步锁使用的synchronized
在虚拟机内部使用的对象
对象的finalization机制
当一个对象被标记为垃圾后,在真正被回收之前,会调用一次Object类中finalize(),留给垃圾回收器调用。
有了finalization机制的存在,在虚拟机中把对象状态分为3种:
1.可触及的不是垃圾,与根对象连接的
2.可复活的 判定为垃圾了,但是还没有电源finalize(),在finalize()中对象可能会复活
3.不可触及的:判定为垃圾了,finalize()也被执行过了,这种就是必须被回收的对象;
垃圾回收阶段的算法:
标记--复制算法:
将内存分为大小相等的两份空间,把当前使用的空间中存活的对象复制到另一个空间中,将正在使用的空间中垃圾对象清除。
优点:减少内存碎片
缺点:如果需要复制的对象数量多,效率低。
使用场景:存活对象少,新生代适合标记复制算法;
标记--清除算法:
清除不是真正的把垃圾对象清除掉,将垃圾对象地址维护到一个空闲列表中,后面有新对象到来时,覆盖掉垃圾对象即可。
特点:
实现简单
效率低,回收后有碎片产生
标记--压缩算法(标记-整理)
垃圾回收器:
垃圾收集器是垃圾回收的实际实现者,垃圾回收算法是方法论。
垃圾回收器分类:
按线程分类:
单线程垃圾回收器:
Serial serial old
多线程垃圾回收器:
Parallel
按照工作模式分类:
独占式:垃圾回收线程执行时,其他线程暂停
并行式(并发式):垃圾回收线程可以和用户线程同时执行
按工作的内存分类:
年轻代垃圾回收器
老年代垃圾回收器
垃圾回收器性能指标:
吞吐量
暂停时间
回收的速度
占用内存大小
CMS垃圾回收器
Concurrent Mark Sweep 并发标记清除器
支持垃圾回收线程与用户线程并发(同时)执行
初始标记:独占式的暂停用户线程
并发标记:垃圾回收线程与用户回收线程(同时)执行
重新标记:独占式的暂停用户线程
并发清除:垃圾回收线程与用户线程并发(同时)执行,进行垃圾对象的清除;
优点:可以做到并发收集
缺点:使用标记清除算法,会产生内存碎片,并执行影响到用户线程,无法处理浮动垃圾;
三色标记:
由于cms有并发执行过程,所以在标记垃圾对象时有不确定性。
所以在标记时,将对象分为3种颜色(3种状态)
黑色:例如GCRoots确定是存活的对象
灰色:在黑色对象中关联的对象,其中还有未扫描完的,之后还需要再次进行扫描;
白色:与黑色,灰色对象无关联的,垃圾收集算法不可达的对象;
标记过程:
1.先确定GCRoots,把GCRoots标记为黑色
2.与GCroots关联的对象标记为灰色
3.再次遍历灰色,灰色变为黑色,灰色下面有关联的对象,关联的对象变灰色
4.最终保留黑色,灰色,回收白色对象
G1(Garbage-First) 垃圾优先
将堆内存各个区又分成较小的多个区域, 对这些个区域进行监测,对某个区域中垃圾数量大的区域优先回收.
也是并发收集的.