一、JVM的位置
Java Virtual Machine Java程序的运行环境(java二进制字节码的运行环境)
好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收机制

二、JVM的体系结构

从图中可以看出 JVM 的主要组成部分
- ClassLoader(类加载器)
- Runtime Data Area(运行时数据区:方法区、堆、JVM虚拟机栈、本地方法栈、程序计数器)
- Execution Engine(执行引擎)
- Native Method Library(本地库接口)
运行流程:
- 类加载器(ClassLoader)把Java代码转换为字节码
- 运行时数据区(Runtime Data Area)把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是有执行引擎运行
- 执行引擎(Execution Engine)将字节码翻译为底层系统指令,再交由CPU执行去执行,此时需要调用其他语言的本地库接口(Native Method Library)来实现整个程序的功能。
三、类加载器
3.1 什么是类加载器,类加载器有哪些?
JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是
将字节码文件加载到JVM中,从而让Java程序能够启动起来。如果new Student();(具体实例在堆里,引用变量名放栈里)
类加载器种类
- 启动类加载器(BootStrap ClassLoader):该类并不继承ClassLoader类,其是由C++编写实现。用于加载
JAVA_HOME/jre/lib目录下的类库。 - 扩展类加载器(ExtClassLoader):该类是ClassLoader的子类,主要加载
JAVA_HOME/jre/lib/ext目录中的类库。 - 应用类加载器(AppClassLoader):该类是ClassLoader的子类,主要用于加载
classPath下的类,也就是加载开发者自己编写的Java类。 - 自定义类加载器:开发者自定义类继承ClassLoader,实现自定义类加载规则。

类加载器的体系并不是"继承"体系,而是委派体系,类加载器首先会到自己的parent中查找类或者资源,如果找不到才会到自己本地查找。类加载器的委托行为动机是为了避免相同的类被加载多次。
3.2 双亲委派机制
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就返回成功;只有父类加载器无法完成此加载任务时,才由下一级去加载。

3.2.1 JVM为什么采用双亲委派机制
- 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性。
- 为了安全,保证类库API不会被修改
在工程中新建java.lang包,接着在该包下新建String类,并定义main函数
java
public class String {
public static void main(String[] args) {
System.out.println("demo info");
}
}
此时执行main函数,会出现异常,在类 java.lang.String 中找不到 main 方法

出现该信息是因为由双亲委派的机制,java.lang.String的在启动类加载器(Bootstrap classLoader)得到加载,因为在核心jre库中有其相同名字的类文件,但该类中并没有main方法。这样就能防止恶意篡改核心API库。
四、Native
native关键字主要用于修饰方法,被native关键字修饰的方法叫做
本地方法,一个native方法就是一个Java调用非Java代码的接口,该方法的实现由非Java语言实现,而是使用C或C++等其他编程语言实现。
编写一个多线程类启动
java
package com.jzj.controller;
public class TestNative {
public static void main(String[] args) {
new Thread(()->{ },"your thread name").start();
}
}
点进去看start方法的源码:

-
凡是带了
native关键字的,说明 java的作用范围达不到,去调用底层C语言的库! -
JNI:Java Native Interface(Java本地方法接口)
-
凡是带了native关键字的方法就会进入本地方法栈
五、程序计数器(PC寄存器)
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
六、方法区
- 方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间.
- 静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。
七、虚拟机栈
栈是一种数据结构,先进后出。队列先进先出。
- 每个线程运行时所需要的内存,称为虚拟机栈,先进后出
- 每个栈由多个
栈帧组成,对应着每次方法调用时- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 栈主要存储的内容:
8大基本类型、对象引用、实例的方法
简单理解:栈内存主管程序的运行,生命周期和线程同步。线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题。
八、三种JVM
- Sun公司HotSpot
- BEA JRockit
- IBM J9 VM
我们学习都是:Hotspot

九、堆
- Heap 堆,一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。
- 类加载器读取了类文件后,需要把类,方法,常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
- 堆内存分为三部分(JDK8之前):
- 年轻代(年轻代被划分为三部分,伊甸区和两个大小严格相同的幸存区)
- 老年代(老年代主要保存生命周期长的对象,一般是一些老的对象)
- 永久代(用于存放类和方法的元数据以及常量池,比如Class 和 Method)
为了避免方法区出现OOM,所以在java8中将堆上的方法区【永久代】给移动到了本地内存上,重新开辟了一块空间,叫做元空间。那么现在就可以避免掉OOM(堆内存溢出)的出现了。

9.1 年轻代、老年代、元空间
-
年轻代是
类诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。-
年轻代又分为两部分:
伊甸区(Eden Space)和幸存者区(Survivor Space),所有的类都是在伊甸区被new出来的,幸存区有两个:0区 和 1区,当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC 轻GC)。将伊甸园中的剩余对象移动到幸存0区,若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区,那如果1区也满了呢?(这里幸存0区和1区是一个互相交替的过程)再移动到养老区,若养老区也满了,那么这个时候将产生MajorGC(Full GC 重GC),进行养老区的内存清理,若养老区执行了Full GC后发现依然无法进行对象的保存,就会产生OOM异常 "OutOfMemoryError "。如果出现 java.lang.OutOfMemoryError:java heap space异常,说明Java虚拟机的堆内存不够,原因如下:-
1、Java虚拟机的堆内存设置不够,可以通过参数 -Xms(初始值大小),-Xmx(最大大小)来调整。
-
2、代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)或者死循环。
-
-
-
元空间(MetaSpace)介绍
- 在 HotSpot JVM 中,永久代( ≈ 方法区)中用于存放类和方法的元数据以及常量池,比如Class 和 Method。每当一个类初次被加载的时候,它的元数据都会放到永久代中。
- 永久代是有大小限制的,因此如果加载的类太多,很有可能导致永久代内存溢出,即OutOfMemoryError,为此不得不对虚拟机做调优。
- 元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
十、JVM实践(调优)
10.1 JVM 调优的参数可以在哪里设置参数值?
tomcat的设置vm参数
修改TOMCAT_HOME/bin/catalina.sh文件,如下图
JAVA_OPTS="-Xms512m -Xmx1024m"

springboot项目jar文件启动
shell
nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &
-Xms:设置堆的初始化大小
-Xmx:设置堆的最大大小
十一、垃圾收回
为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC(Garbage Collection)。有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成。在进行垃圾回收时,不同的对象引用类型,GC会采用不同的回收时机。换句话说,自动的垃圾回收的算法就会变得非常重要了,如果因为算法的不合理,导致内存资源一直没有释放,同样也可能会导致内存溢出的。当然,除了Java语言,C#、Python等语言也都有自动的垃圾回收机制。
对象什么时候可以被垃圾器回收
简单一句就是:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法。
引用计数法
一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收
优点:
- 实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
- 在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报OOM错误。
- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象。
缺点:
- 每次对象被引用时,都需要去更新计数器,有一点时间开销。
- 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。
- 无法解决循环引用问题,会引发内存泄露。(最大的缺点)
可达性分析算法
现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾。会存在一个根节点【GC Roots】,引出它下面指向的下一个节点,再以下一个节点节点开始找出它下面的节点,依次往下类推。直到所有的节点全部遍历完毕。
标记清除算法
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
- 1.根据可达性分析算法得出的垃圾进行标记
- 2.对这些标记为可回收的内容进行垃圾回收
复制算法
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。

- 1)将内存区域分成两部分,每次操作其中一个。
- 2)当进行垃圾回收时,将正在使用的内存区域中的存活对象移动到未使用的内存区域。当移动完对这部分内存区域一次性清除。
- 3)周而复始。
优点:
- 在垃圾对象多的情况下,效率较高
- 清理后,内存无碎片
缺点:
- 分配的2块内存空间,在同一个时刻,只能使用一半,内存使用率较低
标记整理算法
标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而解决了碎片化的问题。

1)标记垃圾。
2)需要清除向右边走,不需要清除的向左边走。
3)清除边界以外的垃圾。
优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时,标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。
与复制算法对比:复制算法标记完就复制,但标记整理算法得等把所有存活对象都标记完毕,再进行整理
分代收集算法
在java8时,堆被分为了两份:新生代和老年代【1:2】,在java7时,还存在一个永久代。

对于新生代,内部又被分为了三个区域。伊甸园区,幸存0区,幸存1区【8:1:1】
当对新生代产生GC:MinorGC【young GC】
当对老年代代产生GC:Major GC
当对新生代和老年代产生FullGC: 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免