1、介绍
① JVM 是Java Virtual Machine(Java虚拟机)的缩写,它在整个 JDK 中处于最底层,负责与操作系统交互。
② 作用:屏蔽了与具体平台相关的信息,一般的高级语言如果要在不同的平台上(例如windows、Linux)运行的话,至少要编译成不同的目标代码,而Java语言引入JVM之后,Java语言在不同平台上面运行时不需要重新编译,Java语言编译程序只需要生成在Java虚拟机上运行的目标代码(字节码,这个是Java语言跨平台的基石),就可以在多个平台上不加修改地运行。JVM在执行字节码的时候,把字节码解释成具体平台上的机器指令执行。
③ 三种主要的JVM:
1、Sun公司的 HotSpot,这个时目前使用范围最广的
2、BEA公司的 JRockit
3、IBM公司的 J9VM,高性能的企业级Java虚拟机
2、基本结构(组成)
基本模块:
- 类加载器
- 执行引擎
- 垃圾回收系统(GC)
- 内存模型
- 方法区
- Java栈
- Java堆
- 本地方法栈
- PC寄存器
简图:
详细图:
3、类加载器
① 作用:将 .class文件字节码内容加载到虚拟机内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
② 类加载器的分类(前三种是系统自带的类加载器)
1、Bootstrap 启动类加载器
2、Extension扩展类加载器
3、Application应用程序类加载器
4、Custom Class Loader自定义类加载器
③ 双亲委派机制
Ⅰ、内容:当某个类加载器需要加载某个 .class文件时,【它不会自己立刻去加载这个类,而是把这个类委托给它的父类的类加载器】,递归前面括号内的操作,直到该类加载器不再有父类的类加载器,此时,【该类加载器尝试加载该类,如果能够加载该类就加载,如果无法加载该类就往下交给子类的类加载器加载】,递归前面括号内的操作,直到找到一个类加载器能够加载该类
Ⅱ、案例分析:当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader(应用程序类加载器)中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader(启动类加载器)之前,都是在检查是否加载过,并不会选择自己去加载。直到Bootstrap ClassLoader(启动类加载器),已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
Ⅲ、流程图:
Ⅳ、作用:
1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了就不会被重复加载,保证了数据安全
2、保证核心.class不能被篡改。即使篡改了核心.class文件,通过委托 的方式往上委托到启动类加载器时,启动类加载器已加载过该类所以不会再去加载篡改后的文件,而且,不同的加载器加载同一个.class也不是同一个class对象,这样子保证了class执行安全
4、沙箱安全机制
① 介绍:Java安全模型的核心就是Java沙箱(sandbox),沙箱就是限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源的访问,通过这样子来保证代码的有效隔离,防止对本地系统造成破坏。所以说,沙箱主要是限制系统资源访问,包括CPU、内存、文件系统、网络等。不同级别的沙箱对这些资源访问的权限也不一样
② 几种安全模型
- 在Java中将执行程序分为本地代码和远程代码两种,本地代码默认为可信任的,而远程代码看作是不受信任的,对于授权的本地代码,可以访问一切本地资源,对于非授权的远程代码,在早期的Java实现中,安全依赖于沙箱机制,如下图 JDK1.0安全模型
- 但是如此严格的安全机制也给程序的扩展带来了障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现,因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限,如下图 JDK1.1安全模型
- 在Java1.2版本中,再次引进了安全机制,增加了代码签名,无论是本地代码还是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制,如下图 JDK1.2安全模型
- 当前最新的安全机制实现,则引入了域的概念,虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域对应不一样的权限,存在于不同域中的类文件就具有当前域的全部权限,如下图 JDK1.6安全模型
③ 沙箱的基本组成
-
字节码校验器(bytecode verifier):确保Java类文件.class遵循Java语言规范,这样子可以帮助Java实现内存保护。但是并不是所有类都会经过字节码校验,比如核心类。
-
类装载器(class loader):三个作用:防止恶意代码干涉善意代码、守护了被信任的类库边界、将代码划归入保护域,确定了代码可以进行哪些操作
-
存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定
-
安全管理器(security manager):是核心 API 和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高
-
安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:安全提供者、信息摘要、数字签名 keytools https(需要证书)、加密、鉴别
5、Native
native是一个关键字,用来修饰方法,表示告知 JVM,该方法在外部定义,我们可以用任何语言去实现它
JNI(Java Native Interface),本地方法接口:通过 JNI 我们可以用 Java代码调用操作系统相关的技术实现的库函数,从而与其他技术和系统交互,使用其他技术实现的系统的功能;同时其他技术和系统也可以通过 JNI 提供的相应原生接口调用Java应用系统内部实现的功能
JNI 的缺点:
1、程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分
2、程序不再是绝对安全的,本地代码的不当使用可能导致整个程序奔溃
Native Method Stack(本地方法栈):登记native方法,在执行引擎执行的时候,通过JNI加载本地方法库中的方法。
6、PC寄存器
程序计数器:Program Counter Register
① 定义
JVM 中的PC寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器中才能运行,JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。它是一块很小的内存空间,也是运行速度最快的存储区域。在JVM规范中,每个线程都有自己的PC寄存器,是线程私有的,生命周期与线程的生命周期相同。任何时间一个线程都只有一个方法在执行,这就是所谓的当前方法。PC计数器会存储当前线程正在执行的Java方法的JVM指令地址,或者如果是在执行native方法,则是未指定值(undefined)。它是程序控制流的指示器,分支、循环、跳转、异常处理、线程回复等基础功能都需要依赖这个计数器来完成。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。它是唯一一个在Java虚拟机规范中没有规定任何OutofMemoryError情况的区域
② 作用
用来存储指向下一条指令的地址,也就是即将执行的指令代码。由执行引擎读取下一条指令并执行该指令
7、方法区
① 基本理解:
1、方法区和堆一样,是各个线程共享的内存区域,它有一个别名叫非堆。
2、方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区的溢出,虚拟机同样会抛出内存溢出的错误: java.lang.OutOfMemoryError:PermGen space 或者 java.lang.OutOfMemoryError:Metaspace
3、方法区在 JVM启动的时候创建,关闭 JVM 就会释放这个内存区域
4、 静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
② Hotspot中方法区的演进
1、在JDK6及之前,运行时常量池逻辑包含字符串常量池存放在方法区,对方法区的实现为永久代(位于堆内存中)
2、在 JDK7中,字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区。
3、在 JDK8,移除了永久代用元空间取而代之,此时字符串常量池还在堆,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(堆外内存)
即常量池一直在方法区,其中的字符串池 JDK1.7之后保存到了堆中。
为什么要移除永久代?------>解决永久代OOM的问题
8、栈
① 基本理解
1、是一种数据结构
2、先进后出、后进先出,与之相反的是队列
3、栈内存,主管程序的运行,生命周期与线程同步,线程结束栈内存也就释放,对于栈来说,不存在垃圾回收问题
4、栈存放8大数据类型 + 对象引用 + 实例的方法
问题:为什么main()先执行最后 结束(因为一开始main()先压入栈)
栈运行原理:栈帧(局部变量表 + 操作数栈),每调用一个方法都会有一个栈帧,栈满了main()无法结束,就会抛出错误,栈溢出StackOverflowError
栈帧图:
9、堆
① 基本理解
1、一个 JVM只有一个堆内存,堆的大小是可调节的
2、类加载器读取了类文件后,会把类、方法、常量、变量、所有引用类型的真实对象放在堆中
3、堆可分为新生区和养老区,新生区又可分为伊甸园区和两个幸存区
新生区(伊甸园区 + 幸存者区 * 2)
类诞生和成长甚至是死亡的地方
伊甸园,所有对象都是在伊甸园区new 出来的
幸存者区(两个),轻GC定期清理伊甸园区,活下来的放入幸存者区,幸存者区满了之后重GC清理伊甸园区和幸存者区,新生区和老年区都满了就报OOM
99%的对象都是临时对象,会直接被清理
老年区
新生区活下来的,轻GC杀不死了
② 常见问题解答
-
什么时候会出现OOM?
一个启动类,加载了大量的第三方jar包,Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载,直到内存满了
-
报OOM怎么办?
1、尝试扩大内存,如果还是报错,说明有死循环代码或者垃圾代码
2、分析内存,看一下哪个地方有问题(借助专业工具)
-
扩大内存如何操作?
Edit Configration>add VM option>输入:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
JVM堆内存调优常用参数
-
为什么要分区?
将对象根据存活概率进行分类,对存活时间长的对象,放在老年区,从而减少扫描垃圾时间以及GC频率,针对分类进行不同的垃圾回收算法,对算法扬长避短
-
为什么幸存者区要分为两个大小相等的空间?
主要是解决碎片化,如果内存碎片化严重,也就是两个对象占用不连续的内存,已有的连续内存不够新对象存放,就会触发GC
③ 使用MAT或者Jprofiler排除OOM故障
MAT、Jprofiler的作用:
1、分析Dump内存文件,快速定位内存泄漏
2、获得堆中数据
3、获得大的对象
public class ProfileText {
byte[] array = new byte[1*1024*1024]; //1m
public static void main(String[] args) {
ArrayList<ProfileText> list = new ArrayList<>();
int count = 0;
try {
while (true){
list.add(new ProfileText()); //不停地把创建对象放进列表
count = count + 1;
}
} catch (Exception e) {
System.out.println("count: "+count);
e.printStackTrace();
}
}
}
安装好Jprofiler软件和在idea中装Jprofiler插件
直接打开文件即可查看
10、GC:垃圾回收
① 基本认识
1、GC的作用区域为方法区和堆,不过大部分作用于新生区
2、GC可分为两种,轻GC(Minor GC) 和 重GC(full GC,也叫全局GC)
② 垃圾回收算法
- 引用计数法
对所有对象的引用进行记录,没有对象引用的对象就会被清除
优点:可即刻回收垃圾,当计数值为0时,马上回收对象,提高内存使用效率
缺点:计数器的增减操作频繁,计数器需要占一定内存,循环引用无法回收
- 复制算法
复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中
优点:没有内存碎片,内存效率高,当存活的对象较少时比较高效
缺点:需要一块内存交换空间,当存活对象较多时复制成本高
- 标记清除算法
标记清除算法采用从根集合进行扫描,对存活对象进行标记,标记结束之后,再扫描整个空间未被标记的对象进行回收。
优点:不需要额外空间,优化了复制算法,不需要进行对象的移动,在存活对象较多的情况下比较高效
缺点:两次扫描,严重浪费了时间,且会产生内存碎片
- 标记整理算法
针对标记清除算法的优化,在标记清除后,将所有对象移动整理放入连续空间内存中,解决了碎片化的问题
优点:解决了碎片化的问题
缺点:增加了对象的移动,成本更高了
- 算法总结
内存效率:复制算法 > 标记清除 > 标记压缩 (看时间复杂度)
内存整齐度:复制算法 = 标记压缩 > 标记清除
内存利用率:标记压缩 = 标记清除 > 复制算法
GC采用分代收集算法,即不同的区域采用不同的垃圾回收算法
年轻代:存活率低,用复制算法
老年代:存活率高,区域大,用标记清除/标记压缩算法
11、流程介绍
过程解释:
1、一个 XXX.java文件经过编译之后会生成 XXX.class字节码文件(有几个类或接口就生成几个字节码文件)。
2、类加载器将class文件加载到虚拟机内存中,真正执行字节码的操作,是在加载完成后才开始的
3、执行引擎找到main这个入口方法,执行其中的字节码指令,这里需要使用栈,每一个方法的调用开始到执行完成,就对应这一个栈帧的入栈到出栈的过程
4、在执行方法的过程中,new出来的对象放在堆中,其引用值放入栈帧中。
GC采用分代收集算法,即不同的区域采用不同的垃圾回收算法
年轻代:存活率低,用复制算法
老年代:存活率高,区域大,用标记清除/标记压缩算法
11、流程介绍
过程解释:
1、一个 XXX.java文件经过编译之后会生成 XXX.class字节码文件(有几个类或接口就生成几个字节码文件)。
2、类加载器将class文件加载到虚拟机内存中,真正执行字节码的操作,是在加载完成后才开始的
3、执行引擎找到main这个入口方法,执行其中的字节码指令,这里需要使用栈,每一个方法的调用开始到执行完成,就对应这一个栈帧的入栈到出栈的过程
4、在执行方法的过程中,new出来的对象放在堆中,其引用值放入栈帧中。
5、垃圾回收,操作完成后方法返回给调用方,该栈帧出栈,内存空间被GC回收,堆里的被new出来的哪些也被GC回收