后端开发学习笔记(1)JVM运行机制 内存区域划分 垃圾回收与算法 类加载机制

JVM运行机制

Java程序的具体运行过程如下:
(1)Java源文件被编译器编译成字节码文件(.class)。
(2)JVM中的解释器将字节码文件编译成相应操作系统的机器码。
(3)机器码调用相应操作系统的本地方法库执行相应的方法。
不同操作系统的解释器是不同的,但基于解释器实现的虚拟机是相同的,这也是Java之所以能实现跨平台开发的原因。

JVM内存区域

JVM的内存区域分为线程私有区域程序计数器虚拟机栈本地方法栈),线程共享区域方法区)和直接内存。

线程私有区域的生命周期与线程相同,随着线程的启动而创建,随线程的结束而销毁。而线程共享区域随虚拟机的启动而创建,随虚拟机的关闭而销毁。

直接内存也叫堆外内存,它并不是JVM运行时数据区的一部分,但在并发编程中被频繁使用。JDK的NIO模块提供的基于Channel与Buffer的I/O操作方法就是基于堆外内存实现的。

程序计数器

程序计数器是一块很小的内存空间,用于存储当前运行的线程所执行的字节码的行号指示器。在方法执行时,这个方法的程序计数器记录的是实时虚拟机字节码指令的地址;如果该方法执行的是Native方法,则程序计数器的值为空(Undefined)。
程序计数器是唯一一个没有OutOfMemory(内存溢出)的区域。

虚拟机栈

虚拟机栈是描述Java方法执行过程的内存模型。它在当前栈帧(Stack Frame)中存储了方法的局部变量表操作数栈动态链接方法出口等信息。
在线程内部,每个方法的执行和返回都对应一个栈帧在虚拟机栈中的入栈和出栈。每个运行中的线程当前只有一个栈帧处于活动状态。

本地方法栈

本地方法栈和虚拟机栈的作用类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务。

堆也叫做运行时数据区、运行时内存。在JVM中创建的对象和产生的数据都被存储在堆中。堆是被线程共享的内存区域,也是垃圾回收的最主要的内存区域。现代JVM采用分代回收算法,因此Java堆从GC的角度还可以细分为:新生代老年代。这将在后面详细介绍。

方法区

方法区用于存储常量静态变量类信息(类的版本、字段、方法、接口等描述信息以及常量信息)、即时编译器编译后的机器码运行时常量池等数据。
在即时编译后,代码的内容将在执行阶段(类加载完成后)被保存在方法区的运行时常量池中。Java虚拟机对Class文件每一部分的格式都有明确的规定,只有符合JVM规范的Class文件才能通过虚拟机的检查,然后被装载、执行。

java7之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变;java7中,static静态变量从永久代移到堆中;java8中,取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中。

新生代

JVM新创建的对象(除了大对象)会被存放在新生代,默认占1/3堆内存空间。由于JVM会频繁创建对象,所以新生代会频繁触发MinorGC(YoungGC)进行垃圾回收。新生代又分为 Eden区、SurviveTo区和SurviveFrom区。
(1)Eden区:默认占8/10新生代空间。Java新创建的对象首先会被存放在 Eden区,如果新创建的对象属于大对象,则直接将其分配到老年代。在Eden区内存不足时会触发MinorGC,对新生代进行一次垃圾回收。
大对象的定义和具体的JVM版本、堆大小和垃圾回收策略有关,一般为2KB~128KB,可通过XX:PretenureSizeThreshold设置其大小。
(2)SurvivalTo区:保留上一次MinorGC时的幸存者。
(3)SurvivalFrom区:将上一次MinorGC时的幸存者作为这一次MinorGC的被扫描者。

为便于理解,简要介绍一下新生代垃圾回收MinorGC的过程。它采用复制算法实现。
(1)把在Eden区和SurvivalFrom区中存活的对象复制到SurvivalTo区。如果某对象的年龄达到老年代的标准(对象晋升为老年代的标准由XX:MaxTenuringThreshold设置,默认为15),则将其复制到老年代,同时将这些对象的年龄加1;如果SurvivalTo区的内存空间不够,则直接将复制到老年代;如果对象属于大对象,则也直接将复制到老年代。
(2)清空Eden区和SurvivalFrom区中的对象。
(3)将SurvivalTo区和SurvivalFrom区互换,原来的SurvivalTo区成为下一次GC的SurvivalFrom区。也就是将上一次GC中存活的对象放回到下一次GC要扫描的地方。

老年代

老年代主要存放有长生命周期的对象大对象。老年代的GC过程叫做 MajorGC。在老年代的对象比较稳定,MajorGC不会被频繁触发。在进行 MajorGC前,JVM会进行一次 MinorGC,在 MinorGC过后仍然出现老年代空间不足或无法找到足够大的连续空间分配给新的大对象时,就会触发 MajorGC进行垃圾回收,释放JVM的内存空间。
MajorGC采用标记清除算法,该算法首先会扫描所有对象并标记存活的对象,然后回收未被标记的对象,并释放内存空间。因为要扫描所有的对象,所以MajorGC耗时会比较长,并且标记清除算法还容易产生内存碎片。在老年代没有内存可分配时,会抛出OutOfMemory异常。

永久代,元数据区

永久代指内存的永久保存区域,主要存放 Class和 Meta(元数据)的信息。Class在类加载时被放入永久代。永久代与老年代、新生代不同,GC不会在程序运行期间对永久代的内存进行清理,这也导致了永久代的内存会随着加载的Class文件的增加而增加,在加载的Class文件过多时会抛出OutOfMemory异常。
从Java8开始,永久代已经被元数据区(也叫做元空间)取代。二者最大的区别在于:元数据区并没有使用虚拟机的内存,而是直接使用操作系统的本地内存。因此,元空间的大小不受JVM内存的限制,只和操作系统的内存有关。
Java8之后,JVM将类的元数据放入内地内存中,将常量池和类的静态变量放入Java堆中。

垃圾回收与算法

如何确定垃圾

Java采用引用计数法可达性分析来确定对象是否应该被回收。
(1)引用计数法:在Java中如果要操作一个对象,就必须获取该对象的引用,因此可以通过引用计数来判断一个对象是否可以被回收。在为对象添加一个引用时,引用计数加1;在为对象删除一个引用时,引用计数减1;如果一个对象的引用计数为0,则该对象此刻没有被引用,可以被回收。
引用计数法容易产生循环引用问题。循环引用指两个对象相互引用,导致它们的引用一直存在,而不能被回收。
(2)可达性分析:为了解决引用计数法的循环引用问题,Java还采用了可达性分析来判断对象是否可以被回收。它采用根搜索算法,具体做法是首先定义一些 GC roots 对象,然后以这些对象作为根起点向下搜索,如果在 GC roots 和一个对象之间没有可达路径,则称该对象是不可达的。
不可达对象要经过至少两次标记才能判定其是否可以被回收,如果在两次标记后该对象仍然是不可达的,则将被垃圾收集器回收。

垃圾回收算法

标记清除算法(Mark-Sweep)

标记清除是基础的垃圾回收算法。在标记阶段标记所有需要回收的对象,在清除阶段清除可回收的对象并释放其所占用的内存空间。由于该算法在清理对象占用的内存空间后没有重新整理可用的内存空间,因此如果内存中可被回收的小对象居多,会引起内存碎片化的问题,继而使得大对象无法获得连续可用空间

复制算法(Copying)

复制算法旨在解决标记清除算法内存碎片化的问题。它首先将内存划分为两块大小相等的区域,即区域1和区域2,新生成的对象都被存放在区域1中,在区域1内的对象存储满后会对区域1进行一次标记,并将标记后仍存活的对象全部复制到区域2中,然后直接清理整个区域1的内存即可。复制算法的内存清理效率高且易于实现,但由于同一时刻只有一个内存区域可用,即可用的内存空间被压缩到了原来的一半,因此存在大量的内存浪费。并且当系统中存在大量长时间存活的对象时,这些对象会在内存区域1和区域2之间来回复制而影响系统的运行效率

标记整理算法(Mark-Compact)

标记整理算法结合了标记清除算法和复制算法的优点,它也把内存分为区域1和区域2,标记过程在两个区域内进行,然后将区域2中标记后存活的对象复制到区域1中,并释放区域2的内存。这样区域1中只保留两个区域内所有标记后存活的对象,而区域2被完全释放。

分代收集算法(Generational Collecting)

无论是前面哪种算法,都无法对所有类型(长生命周期、短生命周期、大对象、小对象)的对象都进行垃圾回收。因此针对不同的对象类型,JVM采用不同的垃圾回收算法,这就是分代收集算法。

分代收集算法根据对象的不同类型将内存划分为不同的区域,JVM将堆划分为新生代和老年代。新生代主要存放新生成的对象,其特点是对象多但是生命周期短,在每次进行垃圾回收时都有大量的对象被回收;老年代主要存放大对象生命周期长的对象,因此可回收的对象相对较少。

目前,大部分JVM在新生代都采用了复制算法,因为在新生代中每次进行垃圾回收时都有大量对象被回收,需要复制的对象(存活的对象)较少,不存在大量的对象在内存中被来回复制的问题,因此采用复制算法能安全高效地回收新生代大量的短生命周期的对象并释放内存。

老年代主要存放生命周期较长的对象和大对象,因此每次只有少量非存活的对象被回收,因而在老年代采用标记清除算法或标记整理算法。

JVM内存中的对象主要被分配到新生代的Eden区和SurvivalFrom区,在少数情况下会被直接分配到老年区。在新生代的Eden区和SurvivalFrom区的内存空间不足时会触发一次GC,该过程称为MinorGC。在MinorGC后,在Eden区和SurvivalFrom区中存活的对象会被复制到SurvivalTo区,然后Eden区和SurvivalFrom区被清理。SurvivalTo区成为了下一次GC的SurvivalFrom区。如果此时在SurvivalFrom区无法找到连续的内存空间存储某个对象,则将这个对象直接存储到老年代。若Survival区的对象经过一次GC后仍然存活,则其年龄加1。在默认情况下,对象在年龄达到15时将被移到老年代。

Java中的4种引用类型

在Java中一切皆对象,对象的操作是通过该对象的引用(Reference)实现的。Java中的引用类型有四种。
(1)强引用:在Java中最常见的引用。在把一个对象赋给一个引用变量时,这个引用变量就是一个强引用。有强引用的对象一定为可达性状态,所以不会被垃圾回收机制回收。因此,强引用是造成 Java 内存泄漏(Memory Link)的主要原因。
(2)软引用:通过 SoftReference 类实现。如果一个对象只有软引用,则系统空间不足时该对象会被回收。
(3)弱引用:通过 WeakReference 类实现。如果一个对象只有弱引用,则在垃圾回收过程中一定会被回收。
(4)虚引用:通过 PhantomReference 类实现。虚引用和引用队列联合使用,主要用于跟踪对象的垃圾回收状态。

垃圾收集器

JVM针对新生代和老年代分别提供了多种不同的垃圾收集器,针对新生代提供的垃圾收集器有 Serial、ParNew、Parallel Scavenge,针对老年代提供的垃圾收集器有 Serial Old、Parallel Old、CMS,还有针对不同区域的G1分区收集算法。

Serial 垃圾收集器

单线程,基于复制算法实现。单线程收集器在它正在进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束。
Serial垃圾收集器采用了复制算法,简单、高效,对于单CPU运行环境来说,没有线程交互开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器是Java虚拟机运行在Client模式下的新生代的默认垃圾收集器。

ParNew 垃圾收集器

是 Serial 垃圾收集器的多线程实现,同样采用了复制算法。与 Serial 垃圾收集器唯一的区别是它采用多线程模式工作。
ParNew垃圾收集器在垃圾收集过程中也会暂停其他所有工作线程。它是Java虚拟机运行在Server模式下的新生代的默认垃圾收集器。ParNew垃圾收集器默认开启与CPU同等数量的线程进行垃圾收集,在Java应用启动时可通过-XX:ParallelGCThreads参数调节ParNew垃圾收集器的工作线程数。

Parallel Scavenge 垃圾收集器

是为了提高新生代垃圾收集效率而设计的垃圾收集器,基于多线程复制算法实现。它在系统吞吐量上有很大的优化,可以更高效地利用CPU尽快完成垃圾回收任务。
Parallel Scavenge垃圾收集器通过自适应调节策略提高系统吞吐量,提供了三个参数用于调节、控制垃圾回收的停顿时间及吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数,控制吞吐量大小的-XX:GCTimeRatio参数和控制自适应调节策略开启与否的UseAdaptiveSizePolicy参数。

Serial Old 垃圾收集器

是 Serial 垃圾收集器的老年代实现,同 Serial 一样采用单线程执行,不同的是针对老年代的对象特点采用标记整理算法实现。它是Java虚拟机运行在 Client 模式下的老年代的默认垃圾收集器。

Parallel Old 垃圾收集器

采用多线程并发进行垃圾回收,基于标记整理算法实现。设计上优先考虑系统吞吐量,其次考虑停顿时间等因素。

CMS 垃圾收集器

是为老年代设计的垃圾收集器,其主要目的是在多线程并发环境下达到最短的垃圾收集停顿时间,提高系统的稳定性。基于标记清除算法实现。
CMS的工作机制相对复杂,垃圾回收过程包含如下4个步骤。
(1)初始标记:只标记和 GC roots 直接关联的对象,速度很快,此时需要暂停所有工作线程。
(2)并发标记:和用户线程一起工作,执行 GC roots 跟踪标记过程。
(3)重新标记:在并发标记过程中用户线程继续运行,导致在垃圾回收过程中部分对象的状态发生了变化,为了确保这部分对象的状态正确性,需要对其重新标记并暂停工作线程。
(4)并发清除:和用户线程一起工作,执行清除 GC roots 不可达对象的任务。

G1 垃圾收集器

G1(Garbage First)垃圾收集器为了避免全区域垃圾收集引起的系统停顿,将堆内存划分为大小固定的几个独立区域,独立使用这些区域的内存资源并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,在垃圾回收过程中根据系统允许的最长垃圾收集时间,优先回收垃圾最多的区域。

G1垃圾收集器通过内存区域独立划分使用和根据不同优先级回收各区域垃圾的机制,确保了G1垃圾收集器在有限时间内获得最高的垃圾收集效率。相对于CMS收集器,G1收集器有两个突出的改进:一、基于标记整理算法,不产生内存碎片。二、可以精确地控制停顿时间,在不牺牲吞吐量的前提下实现短停顿垃圾收集。

类加载机制

类加载阶段

JVM的类加载分为五个阶段:加载、验证、准备、解析、初始化。在类初始化完成后就可以使用该类的信息,在一个类不再被需要时可以从JVM中卸载。

加载

指 JVM 读取 Class 文件,并且根据 Class 文件描述创建 java.lang.Class 对象的过程。首先将 Class 文件读取到运行时区域的方法区内,然后在创建 java.lang.Class 对象,最后封装类在方法区的数据结构
在读取Class文件时既可以通过文件的形式读取,也可以通过jar包、war包读取,还可以通过代理自动生成 Class。

验证

确保 Class 文件符合当前虚拟机的要求,保障虚拟机自身的安全。只有通过验证的 Class 文件才能被 JVM 完成加载。

准备

方法区中为类变量分配内存空间并设置类中变量的初始值。初始值指不同数据类型的默认值
注意final类型的变量和非final类型的变量在准备阶段的数据初始化过程不同。例如定义一个int类型的静态变量并赋值,则它在准备阶段的初始值是0,将它赋值是在初始化时完成的,因为JVM在编译阶段会将静态变量的初始化操作定义在构造器中。而如果是final类型的变量,则JVM在编译阶段后会为该变量生成其对应的ConstantValue属性,虚拟机在准备阶段就会根据该属性将变量赋值。

解析

JVM 会将常量池中的符号引用替换为直接引用

符号引用就是一个类中(当然不仅是类,还包括类的其他部分,比如方法,字段等),引入了其他的类,可是JVM并不知道引入的其他类在哪里,所以就用唯一符号来代替,等到类加载器去解析的时候,就通过符号引用找到那个引用类的地址,这个地址也就是直接引用。符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。

初始化

通过执行类构造器的< client >方法为类进行初始化。< client >方法是在编译阶段由编译器自动收集类中静态语句块变量的赋值操作组成的,当一个类中没有这两个部分时就不会生成< client >方法。JVM 规定,只有在父类的< client >方法都执行成功后,子类中的< client >方法才可以被执行。

类加载器

JVM 提供了三种类加载器,分别是启动类加载器、扩展类加载器和应用程序类加载器。

启动类加载器

负责加载 Java_HOME/lib 目录中的类库,或通过-Xbootclasspath参数指定路径中被虚拟机认可的类库。

扩展类加载器

负责加载 Java_HOME/lib/ext 目录中的类库,或通过 java.ext.dirs 系统变量加载指定路径中的类库。

应用程序类加载器

负责加载用户路径(classpath)上的类库。
除了上述3种类加载器,我们也可以通过继承 java.lang.ClassLoader 实现自定义的类加载器。

双亲委派机制

JVM通过双亲委派机制对类进行加载。双亲委派机制指一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类去完成,其父类在接到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中。

若父类加载器在接收到类加载请求后发现自己也无法加载该类(通常原因是该类的 Class 文件在父类的类加载路径中不存在),则父类会将该信息反馈给子类并向下委派子类加载器加载该类,直到该类被成功加载,若找不到该类,则 JVM 会抛出 ClassNotFound 异常。

双亲委派机制的核心是保障类的唯一性和安全性。例如在加载 rt.jar 包中的 java.lang.Object 类时,无论时哪个类加载器加载这个类,最终都将类加载请求委派给启动类加载器加载,这样就保证了类加载的唯一性。如果在 JVM 中存在包名和类名相同的两个类,则该类将无法被加载,JVM 也无法完成类加载流程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

成名在望xy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值