JVM概述
对Java程序的运行过程更加了解,中、高级程序员必备技能。
JVM作用:jvm负责将字节码文件加载到虚拟机中,再将字节码文件,解释\编译为机器码,管理运算时数据存储,垃圾回收(GC);现在的jvm还可以执行其他语言编译后的字节码文件。
JVM构成
- 类加载系统:负责从硬盘上加载字节码文件到JVM中
- 运行时数据区:按照不同的数据分区进行存储(方法区,堆,栈,本地方法栈,程序计数器)
- 执行引擎:将字节码再次编译/解释为机器码
- 本地库接口:负责调用本地操作系统方法
类加载子系统
负责从硬盘上加载字节码文件到jvm中
类加载过程:
- 加载
以二进制字节流的方式加载字节码,在内存中为类生成一个Class对象,将静态存储转为运行时存储
- 链接
验证:验证字节码格式是否正确; 验证语法是否正确
准备:准备阶段则负责为类的静态属性分配内存,并设置默认初始值;
static int a = 0; 准备阶段
static int a = 10; 初始化阶段
解析: 将静态文件中的指令符号引用替换成内存中直接引用
- 初始化
对类的静态变量赋予正确的初始值
类什么时候会被初始化?
使用类中的静态变量,静态方法,在一个类中运行main方法,创建对象,使用反射加载一个类,当加载一个类的子类,而且优先加载父类
注意: 当只使用某个类中静态常量时,类不会被初始化,因为在编译阶段就初始化
当类在加载阶段初始化完成,才说明类的整个加载过程结束.
类加载器
真正实施类加载的实现者
宏观上分为俩类:
- 引导类加载器(启动类加载器), 不是用java语言实现的, C/C++ jvm底层实现
- 其他类加载器,用java语言写的实现类, 都继承java.lang.ClassLoader
细分:
- 引导类加载器(启动类加载器)
java中系统提供的类,都是由启动类加载器加载 例如String
- 扩展类加载器
Java 语言编写的,由sun.misc.LauncherExtClassLoader 实现. 派生于 ClassLoader 类. jre/lib/ext 子目录(扩展目录)下加载类库
- 应用程序类加载器(系统类加载器)
Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现. 派生于 ClassLoader 类. 加载我们自己定义的类,用于加载用户类路径(classpath)上所有的类
- 自定义的类加载器
例如我们自己写一个类,继承ClassLoder
再例如tomcat这种容器,都会有自己加载类的加载器
双亲委派机制
当加载一个类的时候,先让上一级的类加载器去加载,直到找到引用到类加载器
如果上级类加载器找到了类,就是要上级类加载器加载的类;如果上级找不到,就逐级向下委托,使用子级类加载器加载的类;如果都找不到,就报异常了
这样做的好处就避免我们子级开发的类替换了系统中的类。
如何打破双亲委派机制?
通过自定义类加载器,重写ClassLoader类中的findClass(),从而打破双亲委派机制。
tomcat这种容器,也会自己定义类加载器。
运行时数据区
- 程序计数器
用来存储下一条指令的地址。
特点:一块很小的内存空间,也是运行速度最快的存储区域;每个线程都有自己的程序计数器,是线程私有的,生命周期与线程生命周期保持一致;不会存在内存溢出
- 虚拟机栈
栈:运行时的单位,解决程序的运行问题,程序如何执行,或者如何处理数据。管理方法(java自己写的方法)运行,一个方法对应一个栈帧,调用方法入栈,运行完成出栈。不会有垃圾回收,但是会存在内存溢出的可能。
栈帧构成:
- 局部变量表:存储方法中定义的变量,参数
- 操作数栈:程序中的所有计算过程都是借助于操作数栈来完成的。
- 方法返回地址:当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
public static void main(){
test();
}
public void test(){
int a = 10;
int b = 5;
int c = 15;
int c = a++ + b;
}
- 本地方法栈(运行空间)
管理运行本地方法的地方
本地方法: native关键字修饰的方法(Obiect hashCode(),getClass(),clone(克隆),notifyAll(),wait(),Thread start(),FileInputStream read0()),没有方法体.
本地方法栈也是线程私有,会出现内存溢出,不会有垃圾回收
- 堆(存储空间)
存放程序中产生的对象,是运行时数据区中最大的一块空间,大小可以调节,线程共享的,会出现内存溢出,会进行垃圾回收,是垃圾收集器重点回收区域。
堆内存区域划分:
- 新生区
1. Eden(伊甸园)区:刚刚创建的对象放在疑点园区
2. Survivor(幸存者0)区:存放伊甸园区和另一个幸存者区经过垃圾回收后存活下来的对象
3. Survivor(幸存者1)区:俩个幸存者区可以交替使用,始终有一个区域空闲
- 老年代:存放生命周期长的,非常大的对象,经历过15次垃圾回收依然存活的对象,就会被存放到老年代
分区意义:
可以根据不同对象的存活时间进行划分,生命较长的对象放在老年代减少垃圾回收的频率和扫描次数。
对象创建以及在内存分布过程:
1. 新创建的对象存放在伊甸园区
2. 当垃圾回收时,将伊甸园存活的对象移入到幸存者0区
3. 继续运行,再次创建的对象还是保存到伊甸园区
4. 下一次垃圾回收到来时,将伊甸园区存活的对象和幸存者0区存活的对象移入到幸存者1 交替反复执行
5. 当一个对象经历过15次垃圾回收后仍然存活,那么就将此对象移入到老年代
在对象头中4个bit位用来记录回收次数.
可以设置回收次数,但是最大值是15.(1111)
老年代和新生代比例: 2:1
伊甸园和两个幸存者区比例: 8:1:1
堆空间的参数设置 (jvm调优就是根据程序实际运行的需要设置参数,调整各个区间比例大小)
垃圾回收的名词:
Minor GC:一般针对新生代的垃圾回收
Major GC:是针对老年区进行的垃圾回收
FULL GC:整堆收集 实际开发中应避免FULL GC
- 方法区
主要存储加载到虚拟机的类信息。大小可以调整。线程共享的,会出现内存溢出
方法区垃圾回收:
方法区是存在垃圾回收的,只不过条件比较苛刻,方法区主要存储的是类信息,,类什么时候才可以被卸载?
同时满足3个条件:
1. 该类的所有对象以及子类对象都不存在,
2. 加载该类的类加载器不存在了,
3. 该类的Class对象没有被其他地方引用.
一般情况也可以认为类是不会被卸载的.
本地方法接口
本地方法:被Native关键字修饰的方法,不是由Java实现的,是由操作系统实现。
Java调用本地方法的原因:因为上层的高级语言没有对底层硬件直接操作的权限,而是需要调用操作系统的系统提供的接口进行访问。
执行引擎
负责将装载到虚拟机中的字节码 解释/编译为机器码
.java–javac 前端编译—>.class
.class --执行引擎----> 机器码 (后端编译)
解释执行:是用一个解释器,对代码进行逐行解释执行(一般常见的脚本语言就是解释执行),效率低,省去编译的时间
编译执行:是将某段代码进行整体编译,然后执行编译后的结果,编译是需要花费时间的,执行效率高
Java执行引擎采用的是半解释半编译方式将字节码转换为机器码,程序刚开始执行时,立即采用逐行解释执行,程序运行过程中,会将热点代码编译并缓存起来,这样两者结合使用,提高运行效率.
垃圾回收
垃圾:运行程序中没有任何引用所指向的对象,需要被清理回收,否则会占用内存空间,其他新对象无法使用垃圾对象空间,严重会造成内存溢出(OOM)
早期的垃圾回收
早期例如c/c++是需要程序员手动在程序对不再使用的对象进行删除释放。 给程序员造成了繁重的工作量,万一忘记回收, 会造成内存泄漏。
垃圾回收区域:重点涉及堆(频繁回收新生代,较少回收老年代),较少回收方法区
内存溢出与内存泄漏
内存溢出: 经过垃圾回收后,内存依然不够使用,导致程序崩溃
内存泄漏:一个对象在程序中不会被使用,但是垃圾收集器又不能回收.。一直会占用着内存空间,久而久之也是造成内存溢出的原因之一
例如:单例模式中单例对象,整个程序中使用一个唯一的对象
数据库连接对象:Socket、 IO等对象,使用完毕后应该close 关闭资源,如果不关闭,回收器无法回收这些对象.
Stop the world(STW)
STW指的是GC事件发生过程中,会产生应用程序的停顿。当垃圾回收时(标记,回收),会导致其他用户线程暂停。必须保证分析时其他程序不再运行,保证分析准确性
垃圾回收算法
标记阶段:标记出哪些对象是垃圾对象
算法:
引用计数算法(有没有被使用),在对象中有一个计数属性,只要有引用指向该对象,计数器加1,计数器值如果为0,则表示此对象是垃圾对象。
缺点: 计数器占用空间;加一 减一需要时间开销; 无法解决循环引用问题
可达性分析算法(根搜索算法,追踪性垃圾收集):从一些活跃对象开始(GCRoots)搜索,与根对象相关联的对象都是被使用的,与根对象或者根对象相关的引用链不相关的对象,就称为垃圾对象.
可以被称为根对象:
- 虚拟机栈中(正在运行的方法)被引用的对象
- 类中静态属性
- 被用来当做同步锁的对象
- java系统中的类
对象的finalize()方法 :
Object类中protected void finalize() throws Throwable { }
在对象被回收前,可以在此方法中执行一些需要的逻辑(可以被重写).当对象被判定为垃圾,在回收之前会调用finalize(),而且finalize()方法只会被调用一次。
不需要自己调用,由垃圾回收调用,可以将对象分为:
- 可触及的: 不是垃圾对象
- 可复活的: 被标记为垃圾对象,但是finalize()还没有被调用
- 不可触及的: 被标记为垃圾对象,finalize()已经被调用过了.
回收阶段算法:
- 标记-复制算法:可以有多块内存,每次有一块是空闲的,将存活的对象移动到未被使用的空间中,清楚其他内存块的垃圾对象
好处:内存碎片少,适合存活对象小,垃圾对象多的场景(新生代回收)
- 标记-清除算法:将存活对象存活位置保持不变,将垃圾对象地址记录在一个空闲列表,后期创建新对象,就会覆盖列表中的垃圾对象
优点: 不移动对象,适合老年代回收,回收后,会产生内存碎片, 效率高
- 标记-压缩:将存活对象重新排列,排列到内存一端,将其他区域空间进行清理,进行标记,清除,压缩
移动对象,适合老年代回收, 回收后,进来压缩, 不会产生内存碎片 ,效率低
- 分代收集:根据不同的区域特点进行各自的回收,年轻代,对象周期短,存活对象少,回收频繁,采用标记-复制算法;老年代,对象周期长,存活对象多,回收频率低,可采用标记-清除算法和标记-压缩算法混合使用。
垃圾回收器
垃圾回收算法是理论,垃圾回收器是真正操作回收的。不同厂商不同版本实现不同。
分类:
- 按照线程数分:
单线程(串行)垃圾回收器:适用于一些小设备,只有一个线程进行垃圾回收
多线程(并行)垃圾回收器:多个线程进行垃圾回收
- 按照工作模式分:
独占式:垃圾回收线程执行时,其他用户线程停止执行
并发式:垃圾回收线程可以和用户线程同时执行
- 按工作的内存区间分:
年轻代垃圾回收器
老年代垃圾回收器
垃圾回收器典型举例:
CMS(Concurrent Mark Sweep,并发标记清除):是首个实现垃圾收集线程和用户线程可以同时执行的,注意,不是所有都是并发执行,也会有独占执行。
首次做到用户线程和垃圾回收线程并发执行
- 初始标记: 独占 会暂停用户线程
- 并发标记: 并发 与用户线程同时执行
- 重新标记: 独占 会暂停用户线程
- 并发清除: 并发 与用户线程同时执行
G1(Garbage First)回收器:适合大型服务器端,内存大,cpu更加先进的。
将每个区域(伊甸园,幸存者,老年代)又划分成若干个小的区域,哪个区域垃圾数量多,优先回收哪个区域,可以做到整堆管理收集, 也可以做到并发执行。