jdk:java开发工具包(编译工具,打包工具)
jre:java运行时环境(库,类)
jvm:将字节码(class)编译为机器码,运行java程序
编译:源码(.java)->.class
jvm:.class->字节码
概述:jvm虚拟机负责装载字节码到其内部,解释/编译为对应平台上的机器码指令执行,是一个跨语言平台(只要是编译为字节码的文件都可以运行)
一.JVM的构成

1.类加载器
概述:这个模块负责从硬盘上加载类(.class文件)到虚拟机,类加载器只负责将类加载进去,是否能执行交给执行引擎
类加载的过程:

加载阶段->链接阶段->初始化阶段
1>加载阶段:利用io读取字节码文件
2>链接阶段:验证 字节码是否被污染(格式是否正确),准备 ,解析
3>初始化:对类中一系列静态 成员进行初始化赋值,当类加载完成初始化阶段时,一个类才真正完成加载
类什么时候会被加载?
类被使用时会加载(创建对象,调用类中静态属性/方法,反射机制,执行类中的main方法,子类被加载时也会加载对应的父类)
还有两种情况,类不会被加载:
1.只使用类中被final修饰的成员(静态常量),类不会被加载
2.类当作类型使用时(数组类型,泛型)
类加载器 :专门加载类的类
分类:
1>.启动类加载器(c/c++语言实现,嵌入在虚拟机中)
++用于加载java核心类,库(系统中提供的类)++
2>.扩展类加载器(java语言写的)
++继承于Class.Loader,用于加载java.jre.lib.ext.dirs目录下的扩展类,自己也可用写一些类放在此目录下++
3>.应用程序加载器(java语言写的)
++用于加载程序员自己定义的类++
双亲委派机制(类加载的时采用的机制)
java虚拟机对class文件采用的是按需加载的方式,将它的class文件加载到内存中生成class对象,而且加载某个类的class文件时,虚拟机采用的是双亲委派模式,即把请求交给父类处理,它是一种任务委派模式,想要先加载系统中的类
当加载一个类时,先让父级的类加载器去加载,优先加载系统中的类,如果系统中没有,才委派给下级类加载器,子级如果找到了就加载该类,如果下级没有找到,就抛出ClassNotFoundException异常


在Java虚拟机中,当一个类被加载时,它的所有父类(包括直接父类和间接父类)都会被加载,一直到 java.lang.Object
为什么要用双亲委派机制?
答:为了安全,避免我们自己写的类替换了系统中的类
如何打破双亲委派机制?
答:可以通过自定义类加载器来打破,写一个类继承ClassLoader类,重写findClass()方法
正常的类加载器会调用一个loaderClass方法,这个方法中存在向上委托的代码,所有走了双亲委派的机制,我们自定义了一个类加载器,重写了该方法,没有写向上委托的代码,所以打破了双亲委派机制

2.运行时数据区
概述:存储运行时数据的区域,类信息(方法区),对象(堆),变量(栈)...

运行时数据区可以分为5个区域:
1.程序计数器
程序计数器用来记录cpu下一条指令的地址,cpu要切换执行许多的程序,再切换之前需要记录程序本次执行的位置,下次回来时,继续接着执行
很小的内存空间(存储的都是地址),运行速度最快的存储区域,每个线程都私有一个自己的程序计数器,生命周期与线程生命周期一致(线程创建启动后计数器开始工作,线程结束后,程序计数器结束),在JVM中唯一一个不会出现内存溢出的区域
特点:内存空间小,运行速度快,线程私有,生命周期与线程一致,不存在内存溢出情况,不会出现垃圾回收
2.虚拟机栈

概述:栈是运行时单位,即栈解决程序的运行问题,每个线程在创建时都会创建一个虚拟机栈,是线程私有的,存在内存溢出问题,如递归调用过多
作用:虚拟机栈是运行的结构设计,管的是方法如何执行(java中总是main方法先执行)
特点:
1>.虚拟机栈是线程私有的,每个线程创建就会创建一个虚拟机栈
2>.方法入栈后,称为一个栈桢(方法的信息)
3>.不存在垃圾回收
4>.存在内存溢出问题,如递归调用次数过多
栈帧:一个方法入栈之后称为栈桢
栈帧的组成结构:局部变量表,方法返回地址,操作数栈(运算在操作数栈中),动态链接,附加信息
3.本地方法栈
java虚拟机栈管理java方法的调用,而本地方法栈用于管理本地方法(c/c++)的调用,本地方法栈也是线程私有的,不会出现垃圾回收,存在内存溢出问题
4.堆
作用:java中所创建的对象都存储在堆空间中
特点:所有线程共享堆。堆是运行时数据区中最大的一块内存区域。堆内存的大小是可以调节的(通过参数可以调节)。堆中的对象不会马上被移除,仅仅在垃圾收集时才会被移除(垃圾回收的重点区域)。堆空间存在内存溢出。
堆空间内存划分:

新生区:伊甸园(Eden),幸存者0,幸存者1
老年区:
为什么要分区 :将不同生命周期的对象,存储在不同的区域,针对不同的区域进行不同的垃圾回收算法,频繁的回收新生代,较少回收老年代
垃圾对象:没有任何引用指向的对象
对象内存分配过程:
首先java中新创建的对象都存储在新生区(伊甸园区),当垃圾回收发生时,会将伊甸园中存活对象移动到幸存者0区,清空伊甸园区
继续创建对象,当下一次垃圾回收执行时,将伊甸园区和幸存者0区存活的对象放在幸存者1区,清空伊甸园区和幸存者0区,每一次只使用一个幸存者区
当一个对象默认回收15次后依然存活,那么就把该对象移动到老年区

当一个对象经历15次垃圾回收,依然存活那么就把该对象移入老年区

为什么回收次数最大是15次就要移到老年区:对象头中有一块区域负责记录分代年龄,占四个bit位,最大表示为15(1111),可以通过参数更改
堆空间的大小如何设置(JVM调优):

5.方法区(元空间)
概述:是一个被线程共享的内存区域,其中主要存储加载的类字节码,class/method/field等元数据,static final 常量,static常量等数据

方法区的大小也可以通过参数设置
方法区存在内存溢出,当加载的类信息很多时,空间不足就会报错
方法区垃圾回收 :方法区的垃圾回收主要是卸载类信息,但类信息回收条件很苛刻
类被回收需要满足下列三个条件:
1.该类所有的实例都已经被回收,该类派生的子类也都被回收
2.加载该类的类加载器已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类
本地方法接口:
什么是本地方法:native关键字修饰的方法,不是用java语言实现的
为什么要使用本地方法:与java环境外交互,java语言是应用层语言,java语言没有权限直接操作硬件设备(内存,硬盘),操作系统就为上层的语言写好了一些操作硬件的接口,上层语言只需要调用这些系统接口即可,底层实现由操作系统处理
3.执行引擎
概述:将加载进来的一些字节码(汇编指令)文件转为机器码文件,是java虚拟机核心组成部分之一
前端编译:通过javac命令,把.java文件编译为.class文件
后端编译:运行时由执行引擎把字节码编译为机器码
java是半解释半执行语言
为什么执行引擎设计为半编译半解释将字节码翻译为机器码?
将高级语言翻译为机器码一半有两种方式:1.解释执行 2.编译执行
解释执行:html,css,js,sql,python
解释执行有一个特点:一行一行由解释器解释执行
缺点:执行效率低 优点:不需要编译,可以由解释器直接解释执行
编译执行:c,c++,java,需要先整体编译为字节码,编译后执行效率高
缺点:编译需要等待 优点:执行效率快
执行引擎可以对某些执行频繁的热点代码进行追踪,将热点代码编译后缓存起来,下次使用时省去编译时间
程序刚开始运行时,先通过解释器解释执行,提高响应速度,当热点代码编译后,使用编译执行,提高后续程序执行速度
5.垃圾回收
垃圾对象 :在程序运行时,一个对象没有任何引用指向时就是垃圾对象
垃圾对象如果不及时清理,长期占用内存空间,就会导致新创建的对象空间不足,最终发生内存溢出错误
手动内存管理好处:对内存管理更精确,用时申请,用完销毁
手动内存管理坏处:麻烦,容易造成内存泄露(申请的内存忘记回收)
现在的语言都支持自动内存管理(自动垃圾回收)
垃圾回收重点区域:堆,方法区(卸载类信息,条件苛刻)
堆里面频繁回收新生代,较少回收老年代
内存溢出和内存泄露:
内存溢出(out of memory oom):应用系统中存在无法回收的内存或使用的内存过多,内存空间不够了,后续创建的对象放不下了,此时程序会崩溃终止
内存泄露:一个对象已经不再被使用 ,本应该被回收掉,但是由于某些原因不能被回收,这就使垃圾对象占用了我们的内存空间,这种现象称为内存泄露
例如:数据库连接,io,socket需要我们手动close释放对象,创建的对象没有引用指向,丢失也是内存泄露
垃圾回收算法
1.标记阶段
标记阶段的目的:主要是为了判断对象是否是垃圾对象,对堆空间中的对象进行应该标记,
那一些对象是有用的,那一些对象已经是垃圾对象
判断对象是否为垃圾对象一般有两种方式:引用计数器算法和可达性算法
1>引用计数器算法(目前未被使用)
对象中有一个计数器,用来记录有几个引用变量指向对象,当计数器值为0,对象为垃圾对象,虽然实现比较简单,但是也存在下列问题
为什么不使用引用计数器算法?
1.需要单独字段存储计数器字段,增加存储开销
2.每次赋值都需要更新计数器,增加时间开销
3.无法处理循环引用的情况(致命原因,会出现内存泄露),导致java的垃圾回收器中没有使用这类算法
什么是循环引用?
A对象中有个b,B对象中有个c,C对象中有个a,他们三个互相引用,引用计数器都为1,但是如果p丢失,那么外界无法找到这三个对象,造成内存泄露(相当于单链表的头指针丢失)

2>可达性分析算法(根搜索算法)

可达性分析算法以根(活跃对象)为起点 开始搜索,按照从上到下的方式搜索被根对象所连接的目标是否可达 ,解决了引用计数器算法中循环引用的问题,避免了内存泄露
使用可达性分析算法,内存中存活的对象都会被直接或者间接的连接着,如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象可以标记为垃圾对象
可达性分析算法在标记中,会导致所有java线程停顿(确保一致性)
那些对象是活跃对象(根GCRoots)?
1.虚拟机栈中引用的对象(入栈后等待运行)
2.方法区中类静态属性引用的对象(类中定义的全局成员变量)
3.所有被同步锁synchronized持有的对象
4.java虚拟机内部的引用(系统中的类,Class对象,异常对象)
对象的finalization机制
final finally finalize
final:修饰关键字
finally:无论是否处理异常都会执行finally中的代码
finalize方法(Object中的方法,子类可以重写):
在对象首次被判定为垃圾对象后,在回收该对象前,由垃圾收集器调用finalize方法,可以在finalize方法中执行一些最终操作(自定义逻辑处理)
finalize方法只会被调用一次,如果重写该方法时又调用了该对象,该对象就不被标记为垃圾对象,下一次被判定为垃圾对象后,不会调用finalize方法
对象生成还是死亡?
由于finalize方法的存在,虚拟机中的对象一般有三种状态
1.可触及的:从根结点可以连接到的(可达性算法),对象被使用着,不是垃圾对象
2**.可复活**的:对象首次被判断为垃圾对象,finalize方法未被调用,有可能会复活(在finalize中被引用)
3.不可触及的:被判定为垃圾对象,且finalize方法被调用过了(不可能复活)
垃圾回收阶段算法
标记-复制算法
它将可用内存按容量划分为两块 (空闲,正在使用),每次时候只使用其中的一块,在垃圾回收时将正在使用内存中的存活对象(可达性算法)复制到未被使用的内存块中 ,之后清除正在使用的内存块中的所有对象(只剩下垃圾对象),然后互换两个内存块角色,最后完成垃圾回收

使用场景:适合存活对象较少,垃圾对象较多(新生代)
这种回收算法,可用减少内存中的碎片(将存活对象,按空间连续性移动)
标记-清除算法
保持存活对象原地不动,清除垃圾对象,适合老年代 ,因为老年代对象存活时间长,也有的对象内存比较大减少移动次数 ,会产生内存碎片(内存空间不连续)

标记-整理算法
将所有存活的对象压缩到内存中的一端 ,按顺序排放,减少内存空间的产生,但是要移动存活对象。之后,清理边界外所有的对象,适用于老年代

区别:
复制算法:
划分出来两块内存空间,将存活对象移动到另一块内存区域(有序排放),清理原区域所有对象
优点:不产生内存碎片 缺点:要移动对象 适用场景:年轻代
清除算法:
不移动存活对象,清除所有垃圾对象
优点:不需要移动存活对象 缺点:产生内存碎片 适用场景:老年代
整理算法:
将所有存活对象按顺序排放在内存一端,清除剩余区域所有空间
优点:不产生内存碎片 缺点:需要移动存活对象 适用场景:老年代

垃圾收集器
内存回收的实践者,垃圾回收算法是内存回收的理论者
由于jdk的版本处于高速迭代过程中,垃圾收集器也衍生了很多版本
按线程分类:单线程垃圾收集器和多线程垃圾收集器
单线程垃圾收集器:只有一个线程进行垃圾回收,会暂停其他用户线程
多线程垃圾收集器:有多个线程进行垃圾回收,会暂停其他用户线程
STW:指的是GC事件过程中,会产生应用程序停顿
按工作模式分类:独占式和并发式
独占式垃圾收集器:垃圾收集器执行时,用户线程暂停
并发式垃圾收集器:垃圾收集器执行时,不影响用户线程

按照工作内存内存分类:新手代垃圾收集器和老年代垃圾收集器
GC性能指标
吞吐量:
暂停时间(STW):用户线程暂停的时间越短越好
jdk8中内置的垃圾收集器
