目录
[2. JVM内存区域划分](#2. JVM内存区域划分)
[3. JVM类加载](#3. JVM类加载)
[3.1 类加载的步骤](#3.1 类加载的步骤)
[3.2 类加载中的双亲委派模型](#3.2 类加载中的双亲委派模型)
[4. 垃圾回收(GC)](#4. 垃圾回收(GC))
[4.1 JVM的垃圾回收](#4.1 JVM的垃圾回收)
[4.2 找到垃圾](#4.2 找到垃圾)
[4.3 释放垃圾](#4.3 释放垃圾)
1.简单介绍JVM
JVM就是Java虚拟机,是专门为Java程序服务的模型,可以将Java代码编译后的字节码文件通过JVM将字节码转换成不同操作系统对应的机器码进而运行,是Java跨平台性的核心机制。
2. JVM内存区域划分
一个Java进程就相当于一个Java虚拟机,JVM向操作系统申请一块内存空间,JVM再自己对这块内存空间进行分配,划分了四个核心区域:
程序计数器:
用来存储当前线程正在执行的Java方法对应的JVM中的指令地址。
方法区(元空间):
用来记录被JVM加载的类的信息,静态变量,常量等数据,相当于把.class文件加载到内存里面,比如:类的名字,类的使用范围,继承自哪些类,实现了那些接口,方法的名字,返回值类型,参数的类型和数量。
Java虚拟机栈:
每个线程都会创建一个Java虚拟机栈,与线程的生命周期相同,每个方法在执行时候都会创建一个栈帧,用来存储局部变量表,操作数栈,返回出口等信息,每调用一个方法就会创建一个栈帧放到栈里面,JVM的栈是利用C++代码实现的用来存放Java方法的栈。
堆:
用来保存对象的实例的,是JVM里面最大的内存区域,如果对象不使用了就会回收掉。
Test t = new Test();
这里的t是局部变量,是放在栈上的。
t是成员变量是放在堆上的。
t是静态变量是放在元数据区的。

元数据区和堆,一个进程共享一个,程序计数器和栈,一个进程存在多个。
3. JVM类加载
3.1 类加载的步骤
类加载就是将.class文件加载成类文件,通过三个阶段,具体是五个步骤来实现类加载的。
加载:
首先根据类的全限定名(包名+类名)找到.class文件的位置,将.class文件里面的二进制字节流的静态存储结构,转换为方法区中运行时的数据结构,并且在在内存中创建一个该类的对象,作为后续该类的各种数据的访问入口。
验证:
确保读取到的字节流数据符合JVM的要求,并且保证这个被加载的类不会危害到虚拟机的安全。
准备:
给类中的静态字段分配内存空间,并设置默认的初始值,如果是被final修饰的静态字段,不会分配,因为被final修饰的字段在类编译时候就分配了。
解析:
针对字符串常量,进行初始化,将.class文件中解析出来的字符串常量放到内存中,也就是元空间中的常量区。
初始化:
执行类的构造方法,初始化类的各种属性,包括静态成员,如果有父类还未被加载,就会触发父类的加载。
3.2 类加载中的双亲委派模型
在JVM中有专门的模块来进行类加载过程,这样的模块就是类加载器。
JVM中默认的类加载器有三种:
BootstrapClassLoader,在Java标准库的目录里面进行查找。
ExtensionClassLoader,在Java拓展库的目录里面查找。
ApplicationClassLoader,在Java的第三方库目录或者当前项目目录里面找。
双亲委派模型的过程就是:
类加载器接收到类加载的请求后,需要通过全限定类名来找到对应的class文件,首先从ApplicationClassLoader作为入口,将请求传给引用的ExtensionClassLoader,然后ExtensionClassLoader把类加载请求传给引用的BootstrapClassLoader来加载请求,接着BootstrapClassLoader在标准库里面找是否有符合要求的class文件,如果没有找到,就把请求返回给ExtensionClassLoader,ExtensionClassLoader在Java拓展库中寻找,没有找到,把请求返回给ApplicationClassLoader,ApplicationClassLoader在第三方库和当前项目中寻找,如果没有找到就会抛出异常。
4. 垃圾回收(GC)
4.1 JVM的垃圾回收
JVM中的程序计数器,栈区的生命周期都是随着线程的结束而销毁的。而JVM的垃圾回收主要是针对堆里面的不再使用的对象的回收,而GC的工作流程就是找到垃圾,释放垃圾。
4.2 找到垃圾
找到垃圾的方法有两种:引用计数和可达性分析。
引用计数:
引用计数是在内存中每创建一个新对象,就会申请一小块空间来表示该对象被多少引用指向,每有一个引用指向该对象,引用计数就会+1,如果某个对象的引用计数为0,该对象就没有引用使用,就会被释放。
该方法存在的缺陷:
如果对象内存比较小,而引用计数占用的内存的占比就会比较大,比较消耗内存空间。
有可能出现"循环引用"这样的问题:

执行完左边的逻辑后,此时两个对象不再使用,应该释放,但是此时的两个对象的引用计数都是1,不能释放,此时就导致了循环引用的问题。
可达性分析:
JVM通过一些引用的对象,比如栈中的引用类型的局部变量,常量池引用指向的对象,引用类型的静态成员变量,作为起点,然后进行遍历,判断每个对象是否能访问到,如果能访问到的对象就标记为可达,遍历完成后,所有为不可达的对象都应该被释放。
4.3 释放垃圾
释放垃圾的算法有下面几种:
标记-清除算法:
该算法直接将对象释放掉,但是会产生内存碎片化的问题,导致空闲内存是不连续的,在申请内存时,都是申请连续的内存的,这种方法会导致,空闲内存很大,但连续的空闲内存很小,导致可申请的内存空间很小。
复制算法:
该算法是将可用的内存空间分成两部分,一部分空闲着,一部分用来存储对象,将存储对象内存中的垃圾对象释放后,然后将不是垃圾的对象复制到空闲的内存空间,这样剩下的内存空间就是连续的了。
该算法的缺点:
该算法的内存利用率比较低,并且如果对象比较大或者多的情况,复制的成本就比较大。
标记-整理算法:
该算法是在清理完垃圾对象后,将不是垃圾对象的对象进行移动,移动成连续的内存空间。
该方法解决了内存利用率低的问题,但是如果对象比较多或者大的情况,移动的成本就比较大。
Java释放垃圾的机制是:分代回收。
这里的代指的是对象的年龄,也就是GC的次数,如果某个对象经过一次GC后没有被删除,此时该对象的年龄就会+1。

分代回收是将堆分成新生代和老年代两个区域,并且将新生代分为伊甸区和幸存区两种类型的区域,伊甸区比幸存区大。
新创建的对象会先放在伊甸区,经过一轮GC后,会将存活下来的对象通过复制算法放到一个空闲的幸存区,然后幸存区里面的对象也会经过GC,幸存下来的对象会放到另一个幸存区里面,如果一个对象经过多次GC之后,仍然幸存,就会晋升成为老年代,而老年代内部的学习销毁对象是通过标记-整理算法来实现的,因此新生代GC的次数比较频繁,老年代GC的次数比较少。
如果一个对象很大,会直接晋升成为老年代。
而在JVM中会有专门的垃圾收集器模块来完成上面的GC过程,比如:CMS G1 ZGC等。