什么是JVM?
- JVM全称(Java Virtual Machine),中译为:Java虚拟机
- 本质:是一个运行在计算机上的程序
- 职责:运行Java字节码文件(因为计算机只能认识机器码文件,所以需要JVM将自字节码转为机器码)
- 功能:
- 解释和运行:字节码转机器码
- 内存管理:自动为对象/方法分配内存,垃圾回收
- 即时编译:对热点代码进行优化
常见的JVM
JVM的组成部分
类加载器:加载class文件
运行时数据区域:管理内存
执行引擎:垃圾回收器/即时编译器/解释器
本地接口:java使用native修饰的方法
字节码文件的组成
- 打开方式:
- 字节码是以二进制存储,不能直接使用文本软件打开
- 使用软件:jclasslib-bytecode-viewer
MAC安装命令: brew install jclasslib-bytecode-viewer
- 组成(5部分)
- 基础信息:存储java版本号,访问标识
- 常量池:存储字符串常量/类或接口名/字段名
- 字段:存储变量名/变量类型/标识
- 方法:存储方法原代码指令
- 属性:存储类的属性
字节码文件组成详解(基本信息/常量池/方法)
- 基本信息包含:Magic魔数/副版本号/主版本号/访问标识/类等
- Magic魔数(固定字符):是字节码文件的前几个字节,用于确定文件的文件类型
- 主副版本号:主(大版本号 )使用当前的主版本号-44得到真正的jdk版本号
* 例如:主版本号为:52,减完后得到jdk1.8
- 副版本号:为主版本号相同时,区分不同版本的标识
- 版本号的作用:判断当前字节码的版本和运行时jdk是否相兼容
* 例如:报错:<font style="color:#DF2A3F;">字节码文件为jdk8,运行时jdk为6</font>
- 版本号不同的解决方案:
- 
字节码文件的组成-常量池
- 常量池作用:避免相同的内容重复定义
字节码文件的组成-方法
- 字节码指令
- 字节码执行过程:
iconst_0 # 现将o放入到操作数栈中,
istore_1 #0从操作数栈弹出,存储到局部变量表中下标为1的位置中
注意:下标为0中存储args数组
iload_1 # 将局部变量中的下标为1的内容复制到栈中
iinc1 by 1 将下标为1的值变为1
istore_1 # #0从操作数栈弹出,存储到局部变量表中下标为1的位置中
return 返回
总结:最后的结果还是为0
字节码文件-练习题:
0 iconst_0
1 istore_1
2 iconst_0
3 istore_2
4 iconst_0
5 istore_3
6 iinc 1 by 1
9 iload_2
10 iconst_1
11 iadd
12 istore_2
13 iinc 3 by 1
16 return
字节码文件常用工具
- 命令:javap -v
- jclasslib插件
- 注意点:想看哪一个文件,先点击文件后在点击视图打开jclasslib
- 注意点:修改后的文件,需要重新编译后才会有对应的字节码文件
- 阿里arthas
- jad命令可以将class文件还原java文件
类的生命周期
- 类的生命周期描述了一个类加载/使用/卸载的整个过程
- 概述:
- 生命周期分为5个阶段:加载(Loading)》连接(Linking)〉初始化(Init)》使用(Using)〉卸载(Unloading)
- 注意:连接可以在分为三个阶段为:验证》准备〉解析
类的加载阶段
- 第一步:类加载器根据类的全限定名通过不同渠道以二进制方式获取字节码信息
- 第二步:加载类完成后,JVM会将字节码信息存到内存中的方法区中
- 生成一个instanceKlass对象,保存了类的所有信息(例如:基本信息,常量池/字段/方法等)
- 第三步:JVM还有在堆中生成一份与方法区数据类似的java.lang.Class对象
- 总结:类加载器根据类的类名会将类的字节码信息存入到内存中,并在方法区的对区分别分配一个对象,保存类的信息
- 注意:方法区和堆区的对象是有关联的
类的连接阶段
- 验证阶段
- 举例:
- 魔数校验
- 举例:
2. 元信息校验(类必须有父类)
3. 主版本号校验
- 准备阶段
- 准备阶段:在堆区分配一块空间给Student对象,并对属性value赋默认值0
注意点:如果使用final修饰的话,在准备阶段就是赋值
- 解析阶段
- 作用:将常量池中的符号引用转为直接引用
类加载器的初始化阶段
- 行为:会执行静态代码块中的代码,并为静态变量赋值
- 使用的字节码命令:clinit
- 示例:
- 先赋值2,在赋值1,最后value中的值为:1
- 类初始化的几种方式
- 访问一个类的静态变量或者静态方法,
public class Test {
public static void main(String[] args) {
int i = Test1.i;
System.out.println(i);
}
}
class Test1{
public static int i=0;
static{
System.out.println("init");
}
}
- 调用Class.forName(String className)
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("Test1");
}
}
class Test1{
static{
System.out.println("init");
}
}
- new一个该类的对象时
- 执行Main方法的当前类
public class Test {
static {
System.out.println("init Test");
}
public static void main(String[] args) throws ClassNotFoundException {
Test1 test = new Test1();
}
}
class Test1{
static{
System.out.println("init Test1");
}
}
- 面试题(静态代码块先执行,在执行main方法,在执行构造方法和构造器方法)
- 答案:DACBCB
- clinit不会出现的情况:
- 面试题2
- 练习题1
- 练习题2
- 总结
类加载器ClassLoader
- 用途:将二进制流》加载〉本地接口调用》生成方法区和堆区的对象
- 应用场景:
- SPI机制
- 类的热部署
- Tomcat类的隔离
- 面试题:双亲委派机制,如何打破双亲,自定义类加载
类加载器的分类
- 两类
- java虚拟机低层实现
- 例如:hotspot使用C++
- java代码实现
- 根据需要自定义
- java虚拟机低层实现
- 特点:需要继承ClassLoder
类加载器-启动类加载器Bootstrap
- String类是由启动类加载器加载的,但是在使用String.class.getClassLoader()方法时,返回的结果为null,因为,类加载器存在于jvm中,所以获取不到。
- 命令:sc -d java.lang.String
class-loader 为空
- 如何使用启动类加载器加载自定义的jar包
- 练习:使用方法二:
类加载器-扩展类加载器(Extension Class Loader)
- 默认加载Java安装目录/jre/lib/ext下的类文件
package cn.varin.Test;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import javax.script.ScriptEngineManager;
import java.io.IOException;
public class Test {
public static void main(String[] args) {
ClassLoader classLoader = ScriptEnvironment.class.getClassLoader();
System.out.println(classLoader);
}
}
- 将自己编写的扩展jar包,添加到ext包下
类加载器-应用程序加载器(Application Class Loader)
- 程序员自己创建的类和第三方类会使用Application加载
package cn.varin.Test;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import javax.script.ScriptEngineManager;
import java.io.IOException;
public class Test {
public static void main(String[] args) {
ClassLoader classLoader = Person.class.getClassLoader();
System.out.println(classLoader);
}
}
class Person{
private String name;
private int age;
}
双亲委派机制
- 核心:解决一个类到底是由谁加载的问题
- 作用:
- 保证类加载的安全性
- 避免重复加载
- 解释:
1. 示例1:假设cn.varin.Person对象加载,它会先找Application,如果没有找到加载,继续找Ext,没有再继续找 Boot,找到,返回 2. 示例2:加载cn.varin.Person对象都没有被三个加载器加载过,它会从boot找,是否在路径中,没有的话,继续找Ext,没有的话,继续找Application,找到,返回
问题:如果一个类重复出现在三个类加载器中,谁来加载
答案:启动类加载器加载,根据双亲委派机制,它的优先级最高
问题2:在自己的项目中创建一个java.lang.String类,会被加载吗
答案:不能,会返回启动类加载器加载在rt包中的String类
package cn.varin.Test;
import jdk.nashorn.internal.runtime.ScriptEnvironment;
import javax.script.ScriptEngineManager;
import java.io.IOException;
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = Test.class.getClassLoader();
System.out.println(classLoader);
Class<?> aClass = classLoader.loadClass("java.lang.String");
System.out.println(aClass.getClassLoader());
}
}
# 结果为:
Application
null
面试题:类的双亲委派机制是什么?
当一个类加载器区加载某一个类的时候,会自底向上的查询父类是否加载过,
如果加载过,直接返回,
如果没有加载过,会自顶向下查询路径进行加载。
app 的父为:Ext
Ext 的父为:Boot
打破双亲委派机制
- 打破双亲委派机制的三种方式
- 自定义类加载器
- 利用上下文类加载器
- 使用Osg框架的类加载器
- 需要打破双亲委派机制的场景
- 有两个应用需要运行,但是两个应用中的某一个类的全路径名是相同的,假设A加载了,B再加载,类加载器会认为是A类
打破双亲委派机制-自定义类加载器
实现方法:重写findClass方法,这样就不会破坏双亲委派机制
打破双亲委派机制-线程上下问加载器
利用上下文类加载器加载类,比如:JDBC和JNDI
- 快速获取到线程上下文类加载器
Thread.currentThread().getContextClassLoader()
- 总结
- 思考:JDBC案例是否真正的打破了双亲委派
打破双亲委派机制-Osgi模块化(了解)
JDK9之后的类加载器
- 区别一
- 区别二:扩展类加载器
- 区别三:应用程序类加载器
类加载器小结
- 类加载器的作用
- 有几种类加载器?
- 什么是双亲委派机制
- 怎么打破双亲委派机制
运行时数据区域
运行时数据区是:jVM在运行Java程序过程中管理的内存区域
- 分类:
运行时数据区域-程序计数器
程序计数器:
作用一:用于存放下一条指令需要执行的地址
作用二:在多线程执行下,程序计数器可以记录CPU切换前每个线程解释执行到那一条指令
问题:程序计数器会发生内存溢出吗:
答案:不会,因为每个线程只存储了一个固定长度的地址,是不会发生内存溢出的。
运行时数据区域-栈
- 分为了:
- Java虚拟机栈
- 保存在Java中实现的方法
- 本地方法栈
- 保存方法中有native关键字的
- 源代码保存在c++中
运行时数据区域-栈-Java虚拟机栈
- 存储顺序:先进后出
- 栈帧的组成:
- 局部变量表:
- 方法执行过程中存放所有的局部变量
- 保存的内容:有实例的this对象,方法的参数,方法体中声明的局部变量。
例题:
答案:6个
2. 操作数栈: 1. 存放临时数据,例如:常量 3. 栈数据 1. 存储:动态链接,方法出口,异常表
设置修改栈的大小
- 注意点:
运行时数据区-堆
- 堆内存是空间最大的一块内存区域,创建出来的对象都存在于堆上
- 堆空间可以分为:use+total+max
- use:使用空间
- total(默认是系统内容的六十四分之一):剩余空间(可以动态增加, )
- max(默认分配系统内存的四分之一):最大可用空间
- 手动设置total和max
运行时数据区域-方法区
- 存储 :
- 类的基本信息
- 运行时常量池
- 字符串常量池
- jdk7把方法区存储在堆区域中的永久代空间
- jdk8将方法区存放在元空间中,云空间位于内存中,只要不超过内存,可以一直存
运行时数据区域-方法区-字符串常量池
- 代码示例
String s1 = new String(“1”);
String s2 = “1”;
解释:s1中的字符串是通过new对象创立的,内容会存储在堆中
s2中的字符串是直接创立的,会存储在方法区的字符串常量池中
s1和s2都是存储在栈中,
运行时常量池和字符串常量池的区别
练习题1:
- false和true
- 练习题2
- 练习题3
- 总结
- 在jdk7及以后版本中,静态变量是存储在堆的Class对象中,脱离了永久代
直接内存
- 直接内存的作用:
- 适应NIO机制
- 提升IO操作效率
3. 在jdk8后,直接保存方法区中的数据
- 直接内存设置
运行时数据区域总结
- 运行时数据区分为几部分,每一部分的作用是什么?
两大部分:
线程不共享:
程序计数器:记录当前要执行的字节码指令的地址(不会出现内存溢出)
Java虚拟机栈:
本地方法栈:
Java虚拟机栈和本地方法栈都是采用栈式存储,用于保存方法调用的基本数据(局部变量,操作数等)(会出现内存溢出)
线程共享:
方法区:主要存储类的元信息,以及常量池(会出现内存溢出)
堆区: 存放创建出来的对象(会出现内存溢出 )
- 不同JDK版本直接的运行时数据区的区别是什么?
jdk6:
jdk7:
jdk8:
自动垃圾回收
范围:负责对堆上的内存进行回收(回收不再使用的对象)
优点:降低程序员实现难度
自动垃圾回收-方法区回收
- 回收条件
- 扩展:System.gc()
- 作用:手动出发垃圾回收器
自动垃圾回收-堆区回收判断-引用计数法
- 如何判断堆上的对象有没有被引用:使用引用计数法
- 每一个对象有一个引用计数器:当对象被引用时+1,取消引用-1
- 存在缺陷:
- 可能产生循环引用
自动垃圾回收-堆区回收判断-可达性分析算法
- 可达性分析将对象分为两类:
- 垃圾回收的根对象(GC root) :
- 普通对象
- 回收原理:可达性分析中,存在一个不可回收的GC Root对象, 该对象会引用其他对象,可达分析通过判断,如果从GC Root开始找,没有找到的对象就是可以回收的。›
- GC Root对象种类
- 线程Thread对象,
- 系统类加载器加载的Java.lang.Class对象
- 监视器对象,用来保存同步锁synchroized关键字的对象
- 本地方法调用时使用的全局对象
- 可达性分析法中的引用属于强引用
自动垃圾回收-软引用
软引用:如果一个对象只有软引用,当程序内存不足时,就会将软引用中的数据进行回收。
常用于:缓存中
如何实现软引用:提供SoftReference类实现
案例分析:
当先A对象属于一个强引用,不会不回收
此时:A对象为一个软引用,可能被回收
- 注意点:
- SoftReferenc对象也需要被GC对象强引用,否则也会被回收
自动垃圾回收-弱引用(WaekReference)
- 弱引用和软引用类似,不同点就是**软引用是在内存不足时才会回收,但是弱引用不需要看内存够不够直接回收**。
自动垃圾回收-虚引用和终结器引用
- 虚引用作用:当对象被垃圾回收器回收时可以接收到对应的通知
- 终结器引用:
垃圾回收算法的评价标准
核心思想:找到内存中存活的对象 ,把不再存活的对象释放
常见的垃圾回收算法:
标记-清除算法
复制算法
标记-整理算法
分代GC
评价标准:
STW(stop the world):停止所有的用户线程的时间
吞吐量(越高效率越好):执行用户代码时间➗(执行用户代码时间+GC使劲)
最大暂停时间:在垃圾回收时,STW时间的最大值
垃圾回收算法-标记清除算法
2个阶段:
- 标记阶段:将所有存活的对象标记(使用可达性分析法)
- 清除阶段:从内存中删除没有被标记的对象
优点:实现简单,第一阶段将存活的标记为1,第二阶段删除非1的对象
缺点:
碎片化:对象删除后,出现多个很小的可用单元;
分配速度慢
垃圾回收算法-复制算法
执行过程:
准备2块空间(from和To),只使用from空间,
在垃圾回收阶段,将存活的对象复制到to中
回收结束后,将from和to的名称互换。
优点:吞吐量高,不会产生碎片化
缺点:内存使用效率低(每次只能使用一般的空间)
垃圾回收算法-标记-整理算法
2个阶段:
- 标记阶段:将所有存活的对象标记(使用可达性分析法)
- 整理阶段:将存活的对象移动到堆的一端,清除掉间空隙和碎片
优点:内存使用效率高,不会产生碎片化
缺点:整理阶段效率不高
垃圾回收算法-分代GC
- 分区
- 年轻代(yong区):使用复制算法
- 分为:
- 伊甸园区
- s0:from
- s1:to
- 老年代(old区)
- 年轻代到老年代:年轻代没执行一次GC,会在存活对象标记一个属性并加一,到属性值达到某一个值,就会转移到老年代
- 为什么要分为年轻代和老年代?
1.
垃圾回收算器
- 垃圾回收器的种类
组合:
G1垃圾回收器
垃圾回收器的选择
自动垃圾回收总结
Java中有那几块内存需要进行垃圾回收
- 在运行时数据区总堆中的数据需要垃圾回收器进行回收
有哪几种常见的引用类型
有哪几种常见的垃圾回收算法
- 标记清除:标记可用的,没用的清除
- 复制算法:分两块空间,将可用的复制同一块区域中,清除掉另一块,并交换名字
- 标记整理:将可用的标记并放到另一端后,将另一端清除
- 分代GC:分为年轻代和老年代,可以用多中回收算法
常见的垃圾回收器有哪些?
- serial和serial old:单线程回收,使用单核CPU场景
- parNew和CMS:暂停时间较短,适用于大型互联网应用中与用户交互的部分
- parallel Scavenge和Parallel old:吞吐量高,适用于后台进行大量数据操作
- G1:适用于较大的堆,具有可控的暂停时间