什么是JVM
java虚拟机
JVM的功能
1.解释和运行
对字节码文件中的指令,实时的解释成机器码,让计算机执行
2.内存管理
自动为对象、方法等分配内存
自动的垃圾回收机制,回收不再使用的对象(c++不会自动回收,相当于降低了编程的下限)
3.即时编译(JIT)
对热点代码进行优化,提升执行效率
java需要实时解释是为了不同平台的兼容性,然而由于需要实时编译,所以性能方面比不上c++,所以就需要即时编译
即时编译的过程
把热点代码(就是频繁出现的代码)放到内存,这样下次就不用再编译了,提高性能。
java虚拟机的组成
1.类加载器(加载字节码文件到内存)
2.运行时的数据区域(JVM管理的内存) 负责管理jvm使用到的内存,比如对象的创建和销毁
3.执行引擎(即时编译器、解释器、垃圾回收器等)(将字节码内的指令解释成机器码,同时视同JIT即时编译器优化性能)
4.本地接口(调用本地编译的方法,比如c/c++实现的方法 native方法)
字节码文件的组成
jclasslib查看字节码文件
字节码文件的组成
魔数:java字节码文件,将文件头称为魔数
主副版本号:jdk1.2是46 之后每升级一个大版本就+1 比如jdk1.8就是46+6 52
主版本号作用就是判断当前字节码的版本和运行时的jdk是否兼容
常量池:避免相同的内容重复定义,节省空间
类的生命周期
五个阶段:加载、链接、初始化、使用、卸载
加载阶段:第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息,第二步类加载器加载完类以后,java虚拟机就会把字节码中的信息放到一个内存区里面(就是方法区),第三步生成一个INstanceklass对象,保存类的所有信息,里面还包含实现特定功能的信息如多态信息。
第四步:同时,java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.class对象。
作用是在java代码中去获得类的信息一级存储静态字段的数据。
问:既然都方法区都有信息了,为什么还要在堆区存储信息?
答:方法区中的instanceklass是用c++实现的,对于我们java程序是不好直接操作的。所以我们要转换为java.lang.class对象才好操作。并且堆区的字段少于instanceklass,不是所有字段都需要访问的,所以要控制开发者的访问范围
连接阶段:有三个小阶段 验证、准备。解析。
验证:检验程序的内容是否符合java虚拟机的规范
准备:准备阶段为静态变量(static)分配内存并设置初始值(这里的初始值是指默认值 不是你赋的值 比如int的初始值就是0)。
但是如果是final修饰的静态变量的话,在准备阶段就会赋你给定的值,不用等到初始化阶段。
解析阶段:将常量池的符号引用替换成直接引用。
符号引用:在字节码文件中使用编号来访问常量池的内容。
直接引用:不再使用编号,直接使用内存的地址进行访问具体数据
初始化阶段:会执行静态代码块中的代码,并为静态变量赋值。
从字节码角度分析就是执行字节码文件中clinit部分的字节码指令
初始化阶段不一定存在
出现继承的初始化阶段:
final修饰的变量如果赋值的内容需要执行指令才能出结果,那么就会执行clinit指令进行初始化
类加载器
java虚拟机提供给应用程序去实现获得类和接口字节码数据的技术
类加载器分为两类 一类是java代码实现 一类是java虚拟机底层源码实现的
类加载器在jdk8和8之后的差别很大
启动类加载器:用于加载java安装目录/jre/lib下的类文件
可以帮我们扩展我们要用的核心类
1.放入jre/lib进行扩展(不推荐使用)
2.使用参数进行扩展
扩展类加载器:用于加载/jre/lib/ext下的类文件
类似于启动类加载器 我们也可以通过扩展类加载器去加载用户的jar包
应用程序类加载器:用于加载classpath下的类文件
类加载器的双亲委派机制(重点)
这东西的核心就是解决一个类到底由谁加载的问题(有多个类加载器)
作用:
1.保证类加载的安全性
2.避免重复加载
双亲委派机制就是:当一个类加载器接收到加载累的任务时,会自底向上查找是否加载过,再自顶向下进行加载。
现在思考一个问题
如果一个类 三个加载器都能加载 那应该谁加载?启动类加载器
面试题
打破双亲委派机制
三种方式:自定义类加载器 线程上下文类加载器 Osgi框架的类加载器
为什么要打破?
自定义类加载器打破双亲委派机制
双亲委派机制的核心代码是在loadclass()方法里面,只要重写这个方法,把核心代码删了,就可以打破双亲委派机制了。
自定义类加载器默认的父类加载器是应用程序类加载器
问题
但是考虑到loadclass方法是通过调用findclass实现的双亲委派机制,所以真正实现一个自定义类加载器的方式是重写findclass方法。这样就不会破坏双亲委派机制,并且创建一个新的自定义类加载器。
线程上下文类加载器
SPI机制是JDK内置的一种服务提供发现机制
思考一个问题,spi机制是如何拿到应用进程的应用程序类加载器的?(因为DriveManger是由启动类加载器加载的,但是SPI最后却可以用应用进程类加载器加载DriveManger)
OSGI框架打破双亲委派机制
热部署
注意事项
JDK9之后的类加载器
JDK9引入了module概念 类加载器的设计发生了变化 (jdk的类不再位于jar包中,而是放到一个jmod文件夹里面)
启动类加载器不再使用c++编写,直接使用java编写,位于jdk.internal.loader.ClassLoaders
拓展类加载器变成平台类加载器
由于JDK9之后是用模块化的设计思路,所以其实平台类加载器是没什么用的,它的存在只是为了和老版本兼容
JVM的第二部分运行时数据区域(JVM管理的内存)
运行时数据区分为 程序计数器、java虚拟机栈、本地方法栈(这些都是线程不共享的)。
方法区、堆(线程共享的)
程序计数器也叫pc寄存器,每个线程都会通过程序计数器记录当前要执行的字节码指令的地址
作用:
1.控制程序指令的进行,实现分支、跳转、异常等逻辑
2.在·多线程的情况下,可以保存当前指令的内存地址,以确保下一次的执行
内存溢出:程序在使用某一块内存区域的时候,存放的数据需要占用的内存大小超过了虚拟机能提供的内存上限。
思考一下 程序计数器会不会出现内存溢出?
当然不会 每个线程只存储一个固定长度的内存地址,程序计数器是不会发生内存溢出的
程序员不需要对程序计数器做任何处理,java虚拟机实现的
栈
本地方法栈是由c++实现的方法 java虚拟机栈是由java实现的
java虚拟机栈随着线程的创建而创建,而回收则在线程销毁时进行。由于方法可能会在不同线程中执行,每个线程都会包含一个自己的虚拟机栈
栈帧的组成:局部变量表 操作数栈 帧数据
局部变量的作用是在方法执行过程中存在所有的局部变量。编译成字节码文件时,就可以确定局部变量表的内容
操作数栈
存放临时数据,一般都是把临时数据存到操作数栈中,当要用到这个数据了 就会取出来 存到局部变量表中。在编译期就可以确定操作数栈的最大深度。
帧数据
动态链接
当我们在使用别的类的方法和属性时,在链接阶段是不会把fai该符号引用变成直接引用的,所以我们要用动态链接 吧这个符号引用的内存地址保存在运行的常量池里面
方法出口
方法出口是指方法在正确或异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一个指令的地址。所以在当前栈帧中,需要存储此方法的出口地址(简单来说就是在一个方法被弹出栈之前,他会告诉你下一个方法的栈帧执行到哪里了)
异常表
异常表存放代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置(简单来说就是存放trycatch等处理异常的执行流程)
那么问题来了 Java虚拟机栈会出现内存溢出吗?
答案是有可能的
死递归就是栈内存溢出吧
java虚拟机栈的默认大小是根据操作系统和计算机体系结构决定的
一般都不会栈内存溢出 一般都是程序员操作失误 写了一个死递归才会这样
设置虚拟机栈大小
一般情况下,工作中使用了递归操作,栈的深度也就几百,不会出现栈溢出的情况,所以可以设置成-Xss256k节省内存。
本地方法栈
堆
创建出来的对象都在堆上(空间占用最大的一块区域)
在栈内存里面会保存这个对象在堆内存里面的地址这样就可以调用了。
堆内存会不会溢出?
会的
堆内存有三个要关注的值:used total max
其实不是滴,堆内存的溢出判断条件比较复杂,会在《垃圾回收器》中详细讲
手动设置max和total
在开发中,直接把max和total设置成相同的数值,这样不用重复申请,减少开销
方法区
方法区存放基础信息的位置、线程共享
主要包括三部分:类的元信息、运行时常量池、字符串常量池
这里的常量池(静态常量池)一般都是放引用 不是放数据 真正的数据在运行时常量池
运行时常量池
方法区在每个虚拟机上的实现不是都一样的
方法区的溢出 :不停往方法区添加类的信息就可以了
字符串常量池
放的就是字符串常量
字符串常量池和运行时常量池有什么关系?
在早期设计中,字符串常量池是运行时常量池的一部分,他们存储的位置都是一样的,后面做了拆分
但如果是这样
返回的结果就是true
区别就在于
String的intern()方法可以手动将字符串放到字符串常量池中
jdk6的版本
jdk7及以后的版本
这里要注意的是字符串常量池里面的java是在虚拟机加载的时候就已经被加载进来了,因为虚拟机要用到。所以java资格字符串不是后面加入的
静态变量到底存放在哪里?
jdk6和之前的版本是放在方法区,也就是永久代中。
jdk7以后的版本就放在堆里面的class对象中,脱离了永久代
直接内存
直接内存不在java虚拟机的规范中,所以并不属于java运行时的内存区域
主要是为了解决一个特定的问题
想要在直接内存中创建数据 可以使用ByteBuffer
直接内存也是可以设置大小的
思考 运行时数据区分为哪几部分,每一部分的作用是什么?
不同JDK版本之间运行时数据区域的区别是什么?
jdk6
jdk7
jdk8
自动垃圾回收
在c/c++没有自动垃圾回收,如果一个对象不再使用,需要手动释放,否则就会出现内存泄漏。我们称释放对象的过程为垃圾回收。
内存泄漏指的是不再使用的对象在系统中未被回收,内存泄漏的积累可能会导致内存溢出。
java的内存管理
java为了简化对象的释放,引入了垃圾回收(GC)机制。通过垃圾回收期对不再使用的对象完成自动的垃圾回收,垃圾回收期主要负责对堆上的内存进行回收。
自动垃圾回收:方法区和堆(主要是堆)
思考一下 运行时的数据区有五部分:程序计数器 java虚拟机栈 本地方法栈 方法区 堆 那么为什么GC只对方法区和堆进行回收?
因为程序计数器 java虚拟机栈 本地方法栈是线程不共享的,它们随着线程的创建而创建,随着线程的销毁而销毁。而方法的栈帧在执行完方法之后就会自动弹出栈并释放掉对应的内存
方法区的回收:
主要回收不再使用的类
一个类可以被卸载有三个条件
堆回收
什么对象可以被回收:如果对象被引用了就不能回收
再思考一下,如果上图的a1=null b1=null 那么可不可以回收A和B对象呢?
可以的 方法中已经没有办法使用引用去访问A和B对象了(即使他们的属性有相互引用)
常用的两种判断方法:引用计数法和可达性分析法
引用计数法会为每一个对象维护一个引用计数器,当对象被引用的时候加1,取消引用的时候减1
那么这个的缺点就很明显了,那么java是不是解决了这个循环引用的问题? 其实不是 java是用了另外一个方法:可达性分析算法
可达性分析算法
可达性分析算法将对象分为两类:垃圾回收的根对象(GCRoot)和 普通对象,对象与对象之间存在引用关系
首先确定的是这个GC Root对象是不可以被回收的,java会给一个GCRoot表去保存这些GcRoot
什么对象可以被称为GCRoot对象
这里关联栈内存的线程对象是主线程对象
可达性软法的引用的强引用,除了强引用以外,java中还有很多引用
软引用
弱引用
虚引用
终结器引用
软引用
软引用是比较弱的引用关系,如果一个对象只有软引用关联它,当程序内存不足的时候,就会将软引用中的数据进行回收
由于软引用可以被回收,所以软引用一般不用于引用重要的数据,所以一般用于缓存。
如果软引用的引用对象被回收了,那么我们是不是应该把软引用对象也回收掉?
答案是肯定要回收的,,那么怎么回收?
弱引用
与软引用基本一致,区别就是弱引用包含的对象在垃圾回收机制时,不管内存够不够都会直接被回收。
虚引用和终结器引用
不会在开发使用
垃圾回收算法
核心思想:
1.找到内存中存活的对象
2.释放不再存活对象的内存,使得程序能再次利用这部分空间
有四种垃圾回收算法:标记-清除算法 复制算法 标记-整理算法 分代GC
评价标准:1.吞吐量 2.最大暂停时间 3.堆使用效率
标记回收算法
分为两个阶段:
标记阶段:将所有存活的对象进行标记。java中用可达性分析算法,从GCRoot开始通过引用链遍历出所有存活对象。
清除阶段:从内存中删除没有被标记也就是非存活对象
优缺点
复制算法
把整个堆内存的空间分为From和To空间,每次在对象分配阶段,只能使用其中一块空间(From空间)。
优缺点
标记整理算法
也叫标记压缩算法,是对吊机清理算法中容易产生内存碎片问题的一种解决方案。
分代GC算法-----分代垃圾回收算法
分代GC会把整个内存区域分为年轻代(存活时间短的对象)和老年代(存活时间长)
年轻代中还有几个小区
调整内存的大小
回收的步骤
为什么分代GC算法把堆分成年轻代和老年代
1.可以通过调整年轻代和老年代的比例适应不同类型的应用程序,提高内存的利用率和性能。
2.新生代和老年代使用不同的垃圾回收算法,新生代一般选择复制算法,老年代可以选择标记-清除和标记-整理算法,由程序员来选择灵活度较高。
3.分代的设计中允许只回收新生代,如果能满足对象分配的要求就不需要对整个堆进行回收(full gc),STW时间就会减少。
垃圾回收器
年轻代-serial垃圾回收器
serial是一种单线程串行回收年轻代的垃圾回收器
老年代-SerialOld垃圾回收器
SerialOld是Serial的老年代版本,也是采用单线程串行回收 用的是标记-整理算法
年轻代-ParNew垃圾回收器
ParNew就是对Serial在多线程CPU下的优化,使用多线程进行垃圾回收
老年代-CMS(Concurrent Mark Sweep)垃圾回收器
CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程等待的时间。
过程
那么有什么缺点呢?
年轻代-Parallel Scavenge垃圾回收器
ps是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点。
老年代-Parallel Old垃圾回收器
G1垃圾回收器
G1垃圾回收有两种方式:
1.年轻代回收(Young Gc)
2.混合回收(Mixed GC) 年轻代和老年代都要回收
G1在进行YoungGC 的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多回收多少个Region区域了。
混合回收
这里和cms长的差不多,但是有点区别 cms的最终标记阶段会把上一个用户线程新创建的对象也标记,但是G1这里不会 并且最后的用户清理 G1用的是复制算法 不会产生内存碎片。但是cms用的是标记清除算法
FULL GC
如果FULLGC都进行不了就会产生内存溢出
1.方法区一般不需要回收,jsp等技术会通过回收类加载器去回收方法区中的类
堆就由垃圾回收器回收