引入
java程序不是直接运行在操作系统上的,而是先通过编译器编译成.class字节码文件,再由jvm负责加载和执行,jvm相当于是位于java程序和操作系统之间的,由于各个操作系统有不同的jvm,所以造就了说java一次编译到处运行,不需要因为说我操作系统的不同,要对代码进行很大的改变,因为不同平台有对应的jvm负责来运行起来,所以你可以理解为我只要拿到了这个.class文件,就可以到处运行了,到处都是我的翻译官,所以你也可以叫这个jvm为java翻译官,并且jvm不仅仅是单纯的负责翻译代码,还要考虑类从哪里来,类的信息放哪,不要的对象怎么进行回收,这也是我接下来要讲的jvm的核心作用
内存区域划分

程序计数器
记录当前线程下一条要执行的字节码指令的位置,因为jvm执行这些代码的时候,不是一瞬间全部执行完,而是一条一条执行,所以我们这个相当于是记录了当前执行到哪了,下一条应该执行哪里,由于cpu会在不同的线程之间进行切换,所以我们这个程序计数器是线程私有的
Java虚拟机栈
这里存放了我们的Java方法,每调用一个方法就创建一个栈帧,方法调用就入栈,方法结束就出栈,栈帧里面包含了局部变量,方法返回地址,返回值等等,这个也就可以看一些方法的调用关系,就比如下面这个

本地方法栈
这个主要存放的是native方法,也就是存底层c++代码的
堆
Java对象的存放,就是你new出来的对象,这个堆不是私有的,所以你不同的线程改同一个对象就可能出现线程安全问题,这个堆也是我们垃圾回收的只要区域
元数据区(方法区)
主要存放类的信息,比如你这个类有哪些字段,哪些方法,父类是谁等等
所以我们可能会在日常代码中看见说堆溢出以及栈溢出这些内存泄露问题,就可以根据这些空间存放的数据类型来进行判断了
类加载机制
类加载流程
Java代码经过编译之后变成.class文件,但是jvm真正运行程序的时候需要知道这个类的一些信息,所以需要先把这个.class变成jvm中真正可以使用的类,我们类加载主要包含加载-验证-准备-解析-初始化
加载
解决去哪找这个.class我那件,并把它读到jvm当中,主要通过ClassLoader(类加载器)去找,这里也是我们后面要讲的双亲委派模型
验证
因为我们拿到一个.class文件不能直接就去执行,而是先要去检查一下是否合法,是否安全,主要对照classfile里的格式

准备
为类变量,也就是 static 变量分配相关内存,并设置默认初始值,这里还没有赋值,而是初始值,int是0,Boolean是false这样
解析
JVM 怎么把 Class 文件里的“名字”变成真正能找到目标的引用
public void test() {
String s = "hello";
}
就比如这个例子,我们test类里面有一个string对象hello,并且是用s这个局部变量,也就是根据刚刚上面讲的那个内存划分来说,这个s是存在栈帧当中的,是一个局部引用变量,而这个hello是一个对象,是存在堆上面的,我们.class文件最开始无法直接找到堆上面的这个hello,所以需要指向,也就是虚拟机栈当中的s来指向这个hello,我们才能找到它
初始化
在这个阶段才会真正赋值我们给静态对象的值,在准备阶段时默认值
疏通一下刚刚几个变量的出现时机
静态变量:跟随着类一起,在加载类的时候,通过类加载机制,最终进行了初始化,毕竟静态变量是大家共享的,因为是类的属性,放在元数据区
成员变量:虽然在类当中,但是跟静态变量不一样,这个是只有在new的时候进行赋值,每一个new对象中的成员变量是可以不一样的,new出来的,放在堆里
临时变量:随着这个方法的调用存在以及方法结束之后销毁,放在虚拟机栈中
双亲委派模型


这就是上面加载说到的类加载器,分别有这三种,以及上面有他们处理的一些模块,整体过程是这样的:左图中越上面的越是“父亲”,所以我们收到一个请求,先是会来到最底下的application ClassLoader,它拿到请求之后不会马上处理,而是直接转交给上层,然后第二层也是一样,最终交给bootstrap ClassLoader,由它看看自己能不能加载,如果可以就加载,如果不可以就向下层转交,这里有两个好处:1,避免重复加载,为什么这么说呢,因为即使是他们分工明确,但是不代表完全不可能重复,也就是说很可能出现上层可以加载,我下层也可能可以加载,所以当我上层先记载出来之后,下层就可以直接使用这个,而不是重复的加载一遍;2,保护Java核心类,因为我们如果需要的是string类,假设我们自己也写了一个string类,但是根据上面这个流程,我们还是会先加载出bootstrap ClassLoader中的Java.lang.String类,这就是双亲委派模型
垃圾回收机制
这个垃圾回收主要对应的就是堆上的回收
找到谁是垃圾
我们先介绍一下python和PHP中的方法:
引用计数
我们会给每个对象再分配一个空间,用来放一个整数,记录我们这个对象当前被引用的次数,如果这个次数为0就代表是垃圾,new一个对象,对应的计数就会加一,但是可能会出现循环计数的一个bug,就好比下面这个代码,我们a和b互相指向,在前两行new的时候,a和b都为1,在互相引用之后,a和b都加为2,但是当我们将a和b都置为null的时候,他们的计数会又变成1,这个时候就有问题了,我们a和b都为空了,我们压根就找不到这两个引用了,那就说明那最后一个1就永远不可能消除掉了,当然在Python中他们进行了改良,可以避免这个问题,引用计数还有一个问题就是会消耗更多的空间,因为每一个对象都分配了一小块空间,加起来可能就会很多
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
所以我们Java中不采取这样的方法,而是采用:
可达性分析
我们从GC Roots开始往后找,可以找到一系列相关联的也就是可到达,类似构成一棵树一样,可到达就是存活的,不可到达就可能被回收,一次GC(垃圾回收)会尽可能把所有的GC Roots都遍历一遍,GC Roots一般有哪些:虚拟机栈中的引用;static 静态变量引用;JNI / Native 引用;正在运行线程相关对象
回收的机制
垃圾找到之后怎么进行回收,我们主要的经典算法有三种
1. 标记-清除
就找到存活的对象以及垃圾对象,然后直接对垃圾对象进行删除
A | 垃圾 | B | 垃圾 | C
A | 空闲 | B | 空闲 | C
类似于这个过程,但是这个过程会导致我们剩余的空间会很零零散散,就比如我们如果要申请一块连续的,较大的空间就可能会出问题
2. 标记-复制
就将我们的堆分成两个区域,我们先只使用一半的区域,找到存活的对象之后,将存活的对象复制到另一半里面,然后依旧另一半清空空着,这样不会产生零零散散的情况,但是对于空间有一定的浪费
3. 标记-整理
我们将标记为活的挪到一边,垃圾挪到一边,然后对垃圾进行删除操作
原来:
A | 垃圾 | B | 垃圾 | C
后来:
A | B | C | 空闲 | 空闲
就类似于这样的操作,但是这样搬运元素的开销也会有一些,所以我们结合上面三种方法,推出了分代回收
分代回收

我们将堆分成了两块区域,一块是新生代区,一块是老年区,新生代里面又分出了两块幸存区,每一块大概占10%的新生代空间,整体的过程就是:我们新创建的对象先放到Eden区,然后进行GC检验是否是垃圾,垃圾就回收掉,存活下来的就放到幸存者区0,然后继续进行GC,这个GC是新生代以及老生代都要进行的,只不过老生代的频次会低一些,新生代里还是那样的操作,如果在幸存者区还活下来了,那就复制到另一块幸存者区1,每复制一次,年龄加一,多次之后还活着就会进入老年代区了,当然这只是垃圾回收的一个简化版本,Young GC / Minor GC主要回收新生代区,Major GC是扫描老生代,Full GC是这两者的总称
1085



被折叠的 条评论
为什么被折叠?



