目录
一、JVM简介
二、JVM 运行流程
三、JVM中的内存区域划分
1、堆
2,Java虚拟机栈
3,本地方法栈
4,程序计数器
5,方法区
四、JVM延申知识点
1,数据类型和引用类型
2,解引用和对象
3,局部变量,成员变量和静态变量
4,静态方法和普通方法
五、GC
1,GC回收哪些内存
2,死亡对象的判断算法
引用计数算法
可达性分析算法
3,垃圾回收算法
标记清除算法
标记复制算法
标记整理算法
4,对象的一生
5,垃圾回收器
6,深拷贝与浅拷贝
六、类加载器
1,类加载过程
【JavaWeb|浅谈JVM】3,双亲委派模型
4,能否违背双亲委派模型
一、JVM简介
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。二、JVM 运行流程
虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。
常见的虚拟机:JVM、VMwave、Virtual Box。
JVM 和其他两个虚拟机的区别:
1. VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
2. JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪。
JVM 是一台被定制过的现实当中不存在的计算机。
三、JVM中的内存区域划分
文章图片
程序在执行之前先要把java代码转换成字节码(class文件),JVM 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中 运行时数据区(Runtime Data Area) ,而字节码 文件是 JVM 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine)将字节码翻译成底层系统指令再交由CPU去执行,而这个过程中需要调 用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能,这就是这4个主要组成部分的职责与功能。
文章图片
堆(运行时常量池)
new的对象放在堆里
方法区
加载好的类放在方法区,静态成员也在
栈
局部变量
程序计数器
存的是地址,描述当前线程,接下来要执行的指令在那个地方
什么是线程私有?1、堆
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时 刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能 恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存 储。我们就把类似这类区域称之为"线程私有"的内存。
堆的作用:程序中创建的所有对象都在保存在堆中。2,Java虚拟机栈
堆里面分为两个区域:新生代和老生代,新生代放新建的对象,当经过一定 GC 次数之后还存活的对象 会放入老生代。新生代还有 3 个区域:一个 Endn + 两个 Survivor(S0/S1)。
Java 虚拟机栈的作用:Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的 内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数 栈、动态链接、方法出口等信息。咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。
文章图片
局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表 所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变 量空间是完全确定的,在执行期间不会改变局部变量表大小。简单来说就是存放方法参数和局部变 量。3,本地方法栈
操作栈:每个方法会生成一个先进后出的操作栈。
动态链接:指向运行时常量池的方法引用。
方法返回地址:PC 寄存器的地址。
本地方法栈和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使用的,而本地方法栈是给本地方法使用 的。4,程序计数器
程序计数器的作用:用来记录当前线程执行的行号的。5,方法区
程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。 如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是一个Native方法,这个计数器值为空。
方法区的作用:用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。四、JVM延申知识点 1,数据类型和引用类型
文章图片
2,解引用和对象
文章图片
3,局部变量,成员变量和静态变量
局部变量在栈上
文章图片
成员变量在堆上
文章图片
静态变量在方法区4,静态方法和普通方法
文章图片
普通方法中有this,和实例相关五、GC 1,GC回收哪些内存
静态方法没有this,和类相关,和实例无关
堆:主要回收这里2,死亡对象的判断算法 引用计数算法
方法区:GC需要回收方法区的内存,但是方法区空间小,数据失去作用的概率低
栈:不需要回收,栈上的内存核实释放时机是明确的(线程结束,栈上的内存就被释放了)
程序计数器:只是存了地址,不需要回收
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任 何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。
引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如Python语言就采 用引用计数法进行内存管理。
但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题
文章图片
可达性分析算法
此算法的核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索 走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的
class Node{
public int val;
public Node right;
public Node left;
public Node(int val) {
this.val = val;
}
}
public class Text {public static Node build(){
Node a = new Node(1);
Node b = new Node(2);
Node c = new Node(3);
Node d = new Node(4);
Node e = new Node(5);
Node f = new Node(6);
Node g = new Node(7);
a.left = b;
a.right = c;
b.left = d;
b.right = e;
e.left = g;
c.right = f;
return a;
}public static void main(String[] args) {
Node root = build();
root.right = null;
}
}
root.right = null;
此时a开始遍历,无法访问到c了,c和f就被标记为垃圾
引用
引用本质就是低配指针,为了找到对象,引用不仅能找对象,还能决定对象的生死3,垃圾回收算法
强引用:平时用到的引用,既能找到对象,也能决定对象的生死
软引用:既能找到对象,也能一定程度决定对象生死(保对象一时)
弱引用:只能找到对象,不能决定对象生死
虚引用:不能找到对象,也不能决定对象生死
通过上面的学习我们可以将死亡对象标记出来了,标记出来之后我们就可以进行垃圾回收操作了,在正式学习垃圾收集器之前,我们先看下垃圾回收机器使用的几种算法(这些算法是垃圾收集器的指导思想)。标记清除算法
标记复制算法
文章图片
1. 效率问题 : 标记和清除这两个过程的效率都不高
2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中 需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
标记整理算法
文章图片
优点:能够解决内存碎片问题,保存内存回收后,并不存在碎片
缺点:需要额外一块内存空间,如果生存对象较多,效率就比较低
4,对象的一生
文章图片
优点:不再需要复制一样依赖更大的空间,也没有碎片
缺点:搬运效率相对较低,不适合频繁操作
文章图片
1,对象诞生于新生代伊甸区,新对象的内存就是新生代中的内存
2,第一轮GC扫描伊甸区之后,就会把大量的对象干掉
3,进入生存区的对象,也会进行GC扫描,如果发现该对象已经不可达,也就要被销毁,被销毁的对象,通过复制算法,拷贝到另外的生存区
4,对象在生存区经经历了若干伦次的拷贝之后,也没被回收,此时说明,这个对象存活时间比较久,就拷贝到老年代
5,老年代的对象也是要经历GC扫描,但是由于老年代的对象,存活时间比较长,所以扫描老年代的周期比新生代周期长。
Particial GC:只进行一部分内存区域的GC5,垃圾回收器
Full GC:针对整个内存区域进行GC
Minor GC:针对新生代内存的GC,执行频繁,速度较快
Major GC:针对老年代的GC,执行没有那么频繁,速度较慢,通常由Major发起
垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。
文章图片
评价垃圾回收器的好坏的标准Serial收集器
回收的空间效率:扫一遍地,能扫除多少垃圾
回收速度:扫一遍要花多少时间
垃圾回收和应用线程之间能否并发执行,扫地的时候会不会影响到别人干活
垃圾回收是否是多线程
回收时间是否是可以预测的
新生代收集器,串行GCParNew收集器
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一 条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线 程,直到它收集结束(Stop The World,译为停止整个程序,简称 STW)。
复制算法,单线程进行标记+回收
新生代收集器,并行GCParallel Scavenge收集器
Serial收集器的多线程版本
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。Serial Old收集器
老年代收集器,串行GCParallel Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。CMS收集器
老年代收集器,并发GC6,深拷贝与浅拷贝
初始标记
只是把和GCRoot直接相关的对象先标记出来,这个标记过程时间比较短
并发标记
执行整个标记遍历过程,不需要暂停用户线程,消耗时间相对较久,但是可以和用户线程并发
当进行并发标记的时候,由于用户线程也在执行,可能导致某个对象,刚刚标记成不是垃圾之后,被相应代码一改就变成垃圾
重新标记
修正刚才的误差,由于刚才出现的误差的毕竟是少数,重新标记代价太大,虽然STW了,用不了多长时间就结束了
并发清除
多线程的方式把刚才的垃圾对象都释放掉了,也可以和应用多线程并发执行
优点: CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。
缺点:内存碎片;GC操作和应用线程是并发执行的,对CPU比较敏感
深拷贝:针对对象A拷贝之后得到对象B,对A的修改不会对B造成任何影响,这种叫深拷贝
浅拷贝:针对对象A拷贝之后得到对象B,对A 的修改会影响到B,这种叫浅拷贝
private static B copy3(B b) {
B result = new B();
result.count = b.count;
result.a = new A();
result.a.num = b.a.num;
return result;
}private static B copy2(B b) {
B result = new B();
result.count = b.count;
result.a = b.a;
return result;
}private static B copy1(B b) {
return b;
}
public static void main(String[] args) {
B b = new B();
b.count = 10;
b.a = new A();
b.a.num = 100;
// B b1 = copy1(b);
B b2 = copy2(b);
//B b2 = copy3(b);
System.out.println(b2.count);
System.out.println(b2.a.num);
System.out.println("修改后的内容");
b.count = 20;
b.a.num = 200;
System.out.println(b2.count);
System.out.println(b2.a.num);
}
文章图片
public static void main(String[] args) {
B b = new B();
b.count = 10;
b.a = new A();
b.a.num = 100;
//B b1 = copy1(b);
//B b2 = copy2(b);
B b2 = copy3(b);
System.out.println(b2.count);
System.out.println(b2.a.num);
System.out.println("修改后的内容");
b.count = 20;
b.a.num = 200;
System.out.println(b2.count);
System.out.println(b2.a.num);
}
文章图片
文章图片
文章图片
六、类加载器 1,类加载过程
3,双亲委派模型
文章图片
1,加载:根据类名,找到文件,读取文件,解析架构,把内容放到内存中,并构造出对应的类对象
2,链接:如果该类依赖一些其他类,链接过程就是把依赖内容进行引入
3,初始化:初始化静态成员,并执行静态代码块
2,类加载触发条件
构造该类的实例
调用该类的静态属性或方法
使用子类时会触发父类加载
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父 类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载,类加载中,根据类名找类的.class文件查找的过程
以加载String类为例,三个加载器直接存在“父子关系”(和继承无关),类加载器中有一个parent属性,执行自己的父亲
4,能否违背双亲委派模型
文章图片
1,先从AppClassLoder开始
2,AppClassLoader不会立刻查找,而是先把类名交给它的父亲,先让父亲查找
3,ExtClassLoader也不会立刻进行查找,而是把类名交给它的父亲去寻找
4,BootstrapClassLoader拿到类名之后,只能自己查找,如果自己查找到了,直接加载类即可,如果未找到,再把这个类交还给ExtClassLoader来查找
5,ExtClassLoader再根据类名在目录中查找,如果找到就加载,未找到就还给AppClassLoader
6,AppClassLoader再去CLASS_PATH环境变量,-cp指定目录,当前目录去查找,如果找到了就加载,如果没找到,就抛出ClassNotFoundException
目的就是让标准库的类优先加载
可以违背,只是标准库中的三个类加载器要遵守,其他的类加载器不太需要遵守
推荐阅读
- JavaWeb|TCP/IP协议
- JavaWeb|Java模拟实现HTTP服务器
- JavaWeb|TCP原理(三次握手四次挥手)
- Go语言|【Golang】做算法题可能会用到的知识
- Java|【操作系统】Nachos 内核线程
- java|ios 按时间排序_如何按应用而不是时间对iOS通知进行排序
- 玩转JAVA系列|【JavaSE】集合框架及背后的数据结构
- java|java 树结构递归设计,java实现递归树形结构
- Java开发|05-JavaSE【泛型,数据结构,List接口,Set接口,Collections工具类】