JVM内存区域划分
核心区域有4个:
程序计数器
这是一个很小的区域,用来记录当前的指令执行到哪个地址了。
元数据区
这个区域用来保存当前类被加载好的数据。
Java源文件(.java )经过编译后会生成字节码文件(.class ),这些二进制文件需要通过类加载机制被载入内存才能执行。
栈
栈里面保存的是方法的调用关系。
我们在写代码的时候,肯定会涉及到方法的调用。当我们每次调用方法的时候,会先进入方法内部执行代码,执行完之后需要回到调用方法处继续往下执行代码。

假设我们的代码中现在有main方法和test1方法,那么这个栈中就会保存这样两个元素,像这样的元素就叫做"栈帧"。
这里的栈帧就保存了这个方法的参数,局部变量,返回值,返回的地址(该方法执行完毕之后,回到哪里继续执行原代码。)
堆
堆是用来保存new的对象的。
java
Test t=new Test();
以上述代码为例:
如果t是一个局部变量,那么t就是在栈上;
如果t是一个成员变量,那么t就是在堆上;
如果t是一个静态成员变量,那么t就是在元数据区
Java类加载
类加载的步骤
1.加载:找到.class文件,打开该文件,并将文件的内容读取到内存里。
2.验证:进行解析和校验,这一步会检查.class文件所读到的内容是否是合法的,并且把这里的内容转化为结构化的数据。
3.准备:给类对象申请内存空间(此时该对象还没进行初始化)
4.解析:针对字符串常量进行初始化,由于字符串常量本身就在.class文件中,所以我们需要把.class文件中解析出来的字符串常量放到内存空间当中(元数据区的常量池中)
5.初始化:针对刚刚提到的类对象进行初始化,对它的各种属性进行补充,包括类中的静态成员。
如果这个类的父类也没加载,那么这个时候父类也会进行类加载操作。
类加载中的双亲委派模型
在JVM中有专门的板块来负责类加载,这样的板块叫做类加载器。
JVM中默认提供了三种类加载器:

这三个类加载器之间存在"父子关系",但这并不是父类和子类之间的关系。
而是在子类中有parent这样的引用来指向父类。
这三个类加载器的一个主要工作就是要找到".class"文件,但是他们负责查找的目录的范围是不一样的。
双亲委派模型的过程
进行类加载的时候,需要通过全限定类名(完整的类名,包括包名),找.class文件的时候。
就会先从ApplicationClassLoader 作为入囗开始,
然后把加载类这样的任务,委托给父亲(ExtensionClassLoader)来进行,
ExtensionClassLoader也不会立即进行査找,而是也委托给父亲(BootstrapClassLoader)来进行,
BootstrapClassLoader也想委托给父亲,由于没有父亲,只能自己进行类加载,根据类名,找标准库范围,是否存在匹配的.class 文件。
BootstrapClassLoader如果没有找到,再把任务还给孩子ExtensionClassLoader,
接下来ExtensionClassLoader来负责进行找 .class 文件,找到就加载,没找到,也就把任务还给孩子 ApplicationClassLoader,
接下来ApplicationClassLoader负责找.class.
找到就加载,没找到就抛出异常。
上述的三个类加载器是属于JVM自带的,我们也可以自定义一些类加载器。
这些自定义的加载器可以放到双亲委派模型中,也可以不放进去。
垃圾回收(GC)
所谓垃圾回收就是Java中释放内存的手段。
GC的工作过程分为2步:找到垃圾(不再使用的对象)和释放垃圾(把对应的内存释放掉)
找到垃圾
找到垃圾有2种方法:
引入计数
所谓引入计数就是在每new一个对象的时候会搭配一个小的内存空间,这段内存空间中就用来存放一个整数。
这个整数就表示目前有多少个引用来指向该对象,这样我们每次在进行引用赋值的时候,都会自动触发引用计数的修改。于是我们就可以通过引用计数来记录目前有多少个引用指向该对象。
由于在Java中要想使用某个对象,一定是要通过引用来完成的。
如果引用计数为0了,那么就说明没有引用指向这个对象了,那么这个对象就是垃圾,需要进行回收。
但是这样的方法会引入2个问题:
1.内存消耗的更多
尤其是当这个对象本身比较小的时候,引入计数所消耗的空间的比例就会更大。
2.循环引用问题

如上图所示,此时我们已经把a和b置为空。但是a和b中的成员变量还在互相引用。
这个时候就会出现空指针异常。
可达性分析
引用计数的方法会带来额外的空间开销,而可达性分析就是用时间换空间的一个策略。
执行过程讲解
1.以代码中的一些特定对象作为遍历的起点(这些起点称为"GCRoots")
2.接下来会尽可能进行遍历,这是为了判断某个对象是否能被访问到
3.当每次访问到一个对象的时候,会把这个对象标注为"可达",当我们完成对所有的对象遍历之后,那些未被标记成"可达"的对象就会被标记为"不可达"。
JVM中有多少个对象,JVM本身是知道的。
通过可达性分析,JVM就会知道有哪些对象是可达的,那么剩下的就是不可达的对象。
这些不可达的对象就是我们接下来要回收的垃圾。
释放垃圾
1.标记清除
这个方法是把垃圾对象所对应的内存直接进行释放,这个方法会产生内存碎片的问题。

比如我们直接把上图中的2 4 6部分进行释放,那么这就会导致这段空闲的空间并不是连续的。
由于我们在申请内存的时候,申请的都是连续的内存空间,不能是多个部分拼接在一起的。
这就会导致当内存碎片很多的时候,总的空闲空间虽然很大,但是我们但凡想申请一个稍微大一点的内存空间就会申请失败。
2.复制算法
这个方法是当每次使用内存空间的时候只使用其中的一半,当进行释放内存空间到时候,会先把不是垃圾的对象拷贝到另一侧的空间中,然后再把这一侧的整体的这个空间都进行释放。
这种操作就可以确保空闲的内存空间都是连续的。
但是这个方法也存在两个缺点:
1.内存的空间利用率较为低下
2.当不是垃圾的对象比较多的时候,复制的成本就会很高。
3.标记整理
这个方法的优点在于可以解决内存碎片问题的同时也可以保证内存的利用率。
这个方法的执行过程类似于顺序表的"搬运"。

当2 4 6区域要被回收的时候,会让3 5 7 向前移动一个空间。
但是这个方法的缺点就是内存搬运数据的操作的开销也是挺大的,复制成本较高的问题仍然存在。
在Java中采用的方式是"分代回收"。
分代回收
分代回收主要是把复制算法和标记整理结合起来的一种方法。
代
代在这里用来标识对象的年龄,它的单位是GC的轮次。
某个对象当经过一轮GC可达性分析之后,如果它不是垃圾,那么它的代就会+1,初始情况的时候,对象的代是0。
堆中会划分2个区域来保存对象

这2大区域是新生代和老生代,在新生代中又分为一片伊甸区和两篇幸存区,这三块区域的比例是8:1:1。
这样的划分就可以让我们针对不同年龄的对象所采取的策略也是不用的。
我们通常认为如果一个对象的年龄是比较大的,那么这个对象大概率还会继续存在很久。
同理,如果一个对象的年龄较小,那么我们就认为它很快就会被回收。
基于这样的特点,我们针对老年代的对象的GC的频率就可以降低,而针对新生代的对象GC的频率就可以提高。
伊甸区和幸存区
新创建的对象会先放到伊甸区,绝大部分伊甸区的对象,在第一轮GC之后就会被回收。
如果这个对象在第一轮GC之后没有被回收,这个时候就会引用"复制算法":把伊甸区的对象复制到第一个幸存区当中。
由于这里需要复制对象的规模是比较小的,所以复制的开销是可控的。
处于第一个幸存区中的对象,也是需要经历GC的扫描,每一轮的GC也会回收很大一部分对象,剩余的对象再次通过复制算法,复制到另一片幸存区当中。
如果一个对象在幸存区中经历了多次的复制操作之后,都存活了下来,这个时候该对象的年龄也比较大了,这个时候该对象就会到老年代中。
综上所述一个对象在伊甸区和幸存区中的移动是通过复制算法来实现的,当对象到老年代之后,会通过标记整理的方法来进行处理。
新生代中的对象大部分都会很快消亡,这使得我们每次复制的开销都是可控的。
而老年代中的对象大部分都会存在很长时间,这使得整理的开销也是可控的。