【JavaEE初阶】-- JVM
文章目录
- 1. JVM运行流程
- 2. Java运行时数据区
- 2.1 方法区(内存共享)
- 2.2 堆(内存共享)
- 2.3 Java虚拟机栈(线程私有)
- 2.4 本地方法栈(线程私有)
- 2.5 程序计数器(线程私有)
- 3. JVM 类加载
- 3.1 类加载的过程
- 3.1.1 加载
- 3.1.2 验证
- 3.1.3 准备
- 3.1.4 解析
- 3.1.5 初始化
- 3.1.6 使用
- 3.1.8卸载
- 3.2 双亲委派模型
- 3.2.1 什么是双亲委派模型
- 3.2.2 破坏双亲委派模型
- 4. 垃圾回收
- 4.1 死亡对象的判断方法
- 4.1.1 引用计数算法
- 4.1.2 可达性分析算法(JVM使用的算法)
- 4.2 垃圾回收算法
- 4.2.1 标记-清除算法
- 4.2.2 复制算法
- 4.2.3 标记-整理算法
- 4.2.4 分代法
- 4.3 垃圾收集器
- 4.3.1 Serial收集器(新生代收集器,串行GC)
- 4.3.2 ParNew收集器(新生代收集器,并行GC)
- 4.3.3 CMS 收集器(标记-清楚算法)
JVM就是Java虚拟机。虚拟机是指通过软件虚拟出来的具有完整硬件功能的计算机系统,它的运行环境是完全隔离的。
我们知道Java是一个跨平台的语言,可以不加修改的在任何操作系统中运行,这就是依托于其运行在JVM中实现的。
1. JVM运行流程
- .java 文件 被编译成 .class 文件,。
- 通过 类加载子系统 将 .class 二进制字节码文件加载到内存中。
- 方法区保存类对象,类对象是new对象的模板。
- new出来的对象全都放在堆内存中。
- 每个线程都会在Java虚拟机栈中分配一个与之对应的内存空间,栈中存放是是线程对方法的调用层级。
- 本地方法栈中存放的是本地方法的调用层级。
- 程序计数器,保存的是当前线程执行的行号。
2. Java运行时数据区
2.1 方法区(内存共享)
方法区保存的是类的类对象,这个类对象就是我们在new对象时的模板。由于存放的是类对象,是公共的数据,所以方法区是线程共享的,所以线程都可以访问这个区域。
在JDK7的实现中被称为永久代。
在JDK8的实现中被称为元空间。
2.2 堆(内存共享)
所有new出来的对象都存放在堆中。堆是JVM内存中使用最大的内存区域,默认占内存的八分之一,不过这个比例是可以JVM参数设置进行设置的。
2.3 Java虚拟机栈(线程私有)
每创建一个线程就会在内存中创建一个与之对应的Java虚拟机栈。Java虚拟机栈的生命周期和线程是相同的,线程结束,对应的Java虚拟机栈就会被垃圾回收掉。
在这个Java虚拟机栈中,调用一个方法就会将该方法压入栈,此时我们将其成为栈帧,当方法执行完之后就会出栈,知道这个栈中的栈帧全部出栈,此时就代表着栈空了,也就意味线程结束了。
2.4 本地方法栈(线程私有)
调用本地方法时使用的栈,记录本地方法的调用层级。
2.5 程序计数器(线程私有)
我们知道多个线程在CPU上是抢占式执行的。那么线程重新调度到CPU上怎么知道上一次执行到了什么地方呢,就是通过程序计数器来记录的。
3. JVM 类加载
3.1 类加载的过程
3.1.1 加载
- 通过类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
3.1.2 验证
验证.class文件是否符合JVM规范。
JVM17规范
3.1.3 准备
比如我们定义了一个常量count。
java public static int count = 100;
在准备阶段仅仅是给count分配一个内存空间,并给设置其初始值,就想count,其设置的初始值是0。
3.1.4 解析
将常量池内的符号引用替换为直接引用的过程。也就是把字节码中的符号引用和真实的内存进行了解析关联。
3.1.5 初始化
执行代码中的真正的赋值操作。
3.1.6 使用
使用阶段就是new对象的过程,执行构造方法,以及父类的构造方法,初始化完成之后一个对象就创建出来了。
3.1.8卸载
程序停止时从jvm中卸载。
3.2 双亲委派模型
使用哪个类加载器进行加载类的过程。
3.2.1 什么是双亲委派模型
- 当我们创建一个类时,先从应用程序加载器开始向上转发,一直转发到启动类加载器。
- 类启动加载器在自己的路径下找,看有没有要创建的这个类,有则加载,没有就继续向下转发到扩展加载器。
- 扩展加载器在自己的路径下找,看有没有要创建的这个类,有则加载,没有就继续向下转发到应用程序加载器。
- 应用程序加载器在自己的路径中找到类并加载。
3.2.2 破坏双亲委派模型
JDBC就是一个典型的案例。
这段源码的说明的翻译:返回此线程的上下文类加载器。该上下文类加载器由线程的创建者提供,供在此线程中运行的代码在加载类和资源时使用。如果未设置,则默认值为父线程的类加载器上下文。原始线程的上下文类加载器通常设置为用于加载应用程序的类加载器。 返回值:此线程的上下文类加载器,若无则返回 null,表示系统类加载器(若上述情况均不成立,则返回引导类加载器) 异常情况:如果存在安全管理者,并且调用者的类加载器不为空且与上下文类加载器不同或不是其祖先,同时调用者未拥有“getClassLoader”这一运行时权限,则会抛出 SecurityException 异常。
Java平台里面自身定义了一套API访问接口,数据库厂商需要实现这个API,厂商实现了这个接口之后会自己提供的一些jar包,供用户来使用。
比如我们使用的是MySQL,DriveManager 调用getConnection,getConnection并不知道要使用MySQL,这里就自己指定了自己要用的加载器,使用的是当前线程的上下文问的加载器,此时就破坏了双亲委派模型机制,在加载类时并没有向上传递,而是直接指定了相应的加载器。
4. 垃圾回收
垃圾回收的是对象占用的内存空间,主要说的是堆内存。程序计数器、虚拟机栈、本地方法栈都是和线程同生同死。
4.1 死亡对象的判断方法
4.1.1 引用计数算法
给每一个对象增加一个引用计数器,每当一个地方引用了该对象时,引用计数器就加一,引用失效时,就减一。当引用计数器为0时,就代表该对象死亡了,是可被回收的。
引用计数法实现较简单,判断效率也比较高,但是引用计数法无法解决循环引用的问题 会导致出现内存泄露的情况。
循环依赖的例子:
public class Test {public Object instance = null;private static int _1MB = 1024 * 1024;private byte[] bigSize = new byte[2 * _1MB];public static void testGC() {Test test1 = new Test();Test test2 = new Test();test1.instance = test2;test2.instance = test1;test1 = null;test2 = null;
// 强制jvm进⾏垃圾回收System.gc();}public static void main(String[] args) {testGC();}
}
JVM并未采用这种方法,但是python使用的是这种算法。
4.1.2 可达性分析算法(JVM使用的算法)
通过以一系列的GC-root的对象作为起始点,从起始点开始向下进行搜索,搜索时走过等我路线,就是引用链,当一个对象没有在任何一个引用链上,就表示该对象是不可用的,就会被标记为可回收。那么在下次垃圾回收的时候就会被回收掉。
在Java语言中,可作为GC Roots的对象包含下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引⽤的对象;
- 方法区中类静态属性引⽤的对象;
- 方法区中常量引⽤的对象;
- 本地方法栈中 JNI(Native⽅法)引⽤的对象。
4.2 垃圾回收算法
4.2.1 标记-清除算法
- JVM会根据可达性分析算法来标记可回收的内存区域。
- 对标记可回收的内存区域进行回收。
但是标记-清除算法会使内存区域变得很分散 如果此时进来了一个大对象,将会没有足够的空间进行存储,此时就会触发垃圾回收,如果垃圾回收之后还没有足够的空间进行存储,还会再次进行垃圾回收,一直这样操作,直到有足够大的空间能够存储这个大对象。
当进行垃圾回收的时候,会停止所有的线程STW,这个停止的时间是没有办法控制的,这是非常危险的。
4.2.2 复制算法
这种算法会将内存区域分成两个部分,我们称为内存区域一和内存区域二。程序运行的时候只使用一个内存区域,另一个内存区域是空闲的。
- 把内存区域一中存储的对象,复制到内存区域二中。
- 在内存区域二中把对象按内存地址顺序排列好,相当于对内存进行了整理。
- 把内存区域一的空间全部清空。
- 每次回收都重复上述工作。
但是这种算法的内存利用率不高。
4.2.3 标记-整理算法
标记整理算法和复制算法一样,但是标记整理算法不是将可回收对象进行清理,而是将存活对象向内存的一边缘移动,然后清除掉边缘以外的内存区域。
4.2.4 分代法
分代法是将内存分为两个区域:新生代 和 老年代,这两个区域的默认比例是1:2。
新生代使用的复制算法,老年代使用的是标记整理算法。
新生代中存放的是刚new出来的对象,老年代中存放的是经过多次GC(默认是15次),也没有被回收掉的对象。
新生代的内存区域又被分为Eden区和s1区、s2区 这个比例默认是8:1:1。
垃圾回收的过程:
-
所有的新 new的对象都会存放在新生代的Eden区。
-
当Eden区满了之后,就会触发一轮GC,如果对象没有被回收将会被复制到FROM区,然后把Eden区内存全部清空。
-
当触发下一次GC时,会将FROM区和Eden区仍然存活的对象复制到TO区。
-
接下来就是s1 和 s2进行呼唤from 和 to区,重复上面的步骤。
-
如果经历了一轮GC对象没有被回收掉,年龄 +1,如果年龄超过15岁(默认是15 ,但是可以进行设置),年龄保存在对象头中。
-
如果对象的年龄超过15岁就会被移入老年代。
如果一个很大的对象被创建,Eden区放不下怎么办??
会尝试将该对象直接放入老年代。
以上的所有比例都是可以设置的!!!
4.3 垃圾收集器
垃圾收集器是对垃圾回收算法的具体实现。
4.3.1 Serial收集器(新生代收集器,串行GC)
Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。
与其他收集器相比,该收集器简单而高效(原因:由于是单线程的,没有线程交互的开销)。
4.3.2 ParNew收集器(新生代收集器,并行GC)
它是Serial收集器的多线程版本。
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。
4.3.3 CMS 收集器(标记-清楚算法)
CMS(Concurrent Mark Sweep)收集器是⼀种以获取最短回收停顿时间为目标的收集器。
CMS的运行过程:
- 初始标记:标记一下GC Roots 能直接关联到的对象,速度很快,需要STW(为什么?因为保证在标记开始时,引用关系不会发生变化)。
- 并发标记:从“初始标记”的对象开始,并发的便利整个对象图,标记所有可达对象。整个操作是和用户线程并发进行的,所以对象的引用关系可能会发生变化。
- 重新标记:会STW,修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分。
- 并发清除:和用户进程并发进行,清除掉在标记阶段判断为死亡的对象。