JVM

目录

类加载过程

(0 加载、链接(验证、准备、解析)、初始化;1 加载:将class文件加载到机器内存中,并在内存中构建出Java类的原型,JVM在运行期可通过此原型获取Java类的任意信息;2 链接:2.1 验证:保证加载的字节码合理合法;2.2 准备:为类变量分配内存并设置初始值;2.3 解析:将类、接口、字段、方法的符号引用转为直接引用;3 初始化:执行类构造器方法<clinit>(),此方法由编译器收集类中所有类变量的赋值动作和静态代码块并自动生成)
  1. 包括加载、链接(验证、准备、解析)、初始化三阶段;
  2. 加载:将Java类的class文件加载到机器内存中,并在内存中构建出Java类的原型,即类模板对象。所谓类模板对象,其实就是Java类在JVM内存中的一个快照,JVM将从class文件中解析出的常量池、类字段、类方法等信息存储到类模板对象中。JVM在运行期可以通过类模板对象获取Java类中的任意信息,能够访问Java类中的成员变量,也能调用Java方法。(1)通过类全限定名获取类的二进制数据流;(2)解析类的二进制数据流为方法区的数据结构;(3)创建java.lang.Class类的实例,作为方法区中访问类数据的入口;
  3. 链接:1 验证:保证加载的字节码是合法、合理并符合规范的;2 准备:为类变量分配内存并设置初始值;3 解析:将类、接口、字段和方法的符号引用转为直接引用;
  4. 初始化:执行类构造器方法<clinit>(),此方法由编译器收集类中所有类变量的赋值动作和静态代码块并自动生成;

Java对象的创建过程

(1 类加载检查:检查类是否已被加载过,如果没有,先执行类的加载 2 分配内存:对象所需内存在类加载完成后可以确定,从堆中分配一块内存给对象,分配方式有“指针碰撞”和“空闲列表”两种,指针碰撞(内存规整时)向着没用过的内存方向将指针移动一段与对象大小相等的距离,空闲列表从虚拟机维护的空闲列表中找一块足够大的内存块划分给对象,内存分配并发问题(1 CAS+失败重试 2 TLAB(本地线程分配缓冲,先在TLAB(每个线程预先在Java堆中分配一块内存)分配,TLAB用完并分配新的TLAB时再CAS重试)) 3 初始化零值:所有字段设零值 4 设置对象头:对象头两部分(1 对象的运行时数据(哈希码、GC分代年龄、锁状态标志)、2类型指针(指向类元数据确定是哪个类的实例)) 5 执行init方法:构造函数给字段初始化)

在这里插入图片描述

1 类加载检查

虚拟机遇到一条new指令时,首先去检查能否在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过。如果没有,就必须先执行相应的类加载过程;

2 分配内存

  1. 在类加载检查通过后,接下来JVM将为新生对象分配内存;
  2. 对象所需的内存大小在类加载完成后便可确定,为对象分配内存就是把一块确定大小的内存从 Java 堆中划分出来;
  3. 分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定;

2.1 内存分配的两种方式

内存分配的两种方式指针碰撞空闲列表
适用场景堆内存规整(没有内存碎片)的情况下堆内存不规整的情况下
原理用过的内存整合在一边,没用过的在另一边,中间有一个分界指针,只要向着没用过的内存方向将指针移动一段与对象大小相等的距离虚拟机维护一个列表,该列表会记录哪些内存块是可用的,在分配时,找一块足够大的内存块划分给对象,然后更新列表
GC收集器Serial、ParNewCMS

2.2 内存分配并发问题

在创建对象的时候有一个很重要的问题,就是线程安全。虚拟机采用两种方式来保证线程安全:

  1. CAS+失败重试

    CAS是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用CAS+失败重试的方式保证更新操作的原子性。

  2. TLAB

    每个线程预先在Java堆中分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才采用上述的CAS进行内存分配。

3 初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4 设置对象头

初始化零值完成之后,虚拟机要对对象进行对象头的设置。对象头包括两部分信息:

  1. 一部分用于存储对象自身的运行时数据(哈希码、GC分代年龄、锁状态标志等);
  2. 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;

5 执行init方法

  1. <init>方法没有执行,所有的字段都还为零;
  2. 执行 <init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来;

对象的内存布局

(1 对象头:两部分(1 对象的运行时数据(哈希码、GC分代年龄、锁状态标志)、2类型指针(指向类元数据确定是哪个类的实例) 2 实例数据; 3 对齐填充:JVM要求对象大小必须是8字节的整数倍,当对象的实例数据部分没有对齐时,通过填充来对齐)

在JVM中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据、对齐填充。
在这里插入图片描述

1 对象头

JVM的对象头包括两部分信息:

  1. 一部分用于存储对象自身的运行时数据(哈希码、GC分代年龄、锁状态标志等);
  2. 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;

2 实例数据

实例数据部分是对象真正存储的有效信息,即在程序中所定义的各种类型的字段内容。

3 对齐填充

对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为JVM要求对象的大小必须是8字节的整数倍,因此当对象的实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问方式

(1 句柄:堆中有一个句柄池,引用变量存储句柄地址,句柄中包含了对象实例数据的地址和对象类型数据的地址。优点:句柄地址稳定,在对象被移动时只改变句柄中实例数据地址,句柄地址不变 2 直接指针:引用变量存储对象直接地址,对象中还要考虑放置对象类型数据的地址。优点:速度快,节省了一次指针定位的时间)

Java通过栈中的引用变量来操作堆中的具体对象。 对象的访问方式视JVM的实现而定,目前主流的访问方式有两种:使用句柄、直接指针。

1 使用句柄

如果使用句柄访问,Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息 。

在这里插入图片描述

2 直接指针

如果采用直接指针访问,引用变量中存储的就是对象的直接地址,因此在对象的布局中还必须考虑如何放置对象类型数据的地址。
在这里插入图片描述

3 两种方式对比

这两种对象访问方式各有优势:

  1. 使用句柄的好处是引用变量中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据地址,而句柄地址不会改变。
  2. 使用直接指针的好处就是速度快,它节省了一次指针定位的时间开销。

JVM的运行机制

  1. JVM是用于运行Java字节码的虚拟机,它运行在操作系统之上,不与硬件设备直接交互;
  2. Java源文件在通过编译器编译后生成字节码文件(.class文件),字节码文件又被JVM中的即时编译器和解释器编译成机器码在不同的平台(Windows、Linux、Mac)上运行;
  3. Java在不同平台上运行时不需要重新编译成字节码文件,任何平台只要装有针对于该平台的JVM,字节码文件就可以在该平台上运行,也就是“一次编译,多次运行”,即跨平台特性;
  4. 在一个Java进程开始启动后,虚拟机就开始实例化了,有多少个进程启动就会实例化多少个虚拟机实例;
  5. 进程退出,则虚拟机实例消亡,在多个虚拟机实例之间不能共享数据;

JVM的构成

  1. 包括类加载器子系统、运行时数据区、执行引擎和本地接口库;
  2. 类加载器子系统用于将编译好的字节码文件加载到JVM中;
  3. 运行时数据区用于存储在JVM运行过程中产生的数据,包括程序计数器、虚拟机栈、本地方法栈、堆、方法区和直接内存;
  4. 执行引擎包括即时编译器和垃圾回收器,即时编译器(配合解释器)用于将字节码编译成具体的机器码,垃圾回收器用于回收在运行过程中不再使用的对象;
  5. 本地接口库用于调用操作系统的本地方法库完成具体的指令操作;

JVM的内存区域解析

  1. 0.1 JVM的内存区域分为线程私有区域(程序计数器、虚拟机栈、本地方法栈)、线程共享区域(堆、方法区)和直接内存;0.2. 线程私有区域的生命周期与线程相同,随线程启动而创建,随线程结束而销毁;线程共享区域的生命周期与虚拟机实例相同,随虚拟机实例启动而创建,随虚拟机实例关闭而销毁;0.3. 直接内存不是JVM运行时数据区的一部分,但也被频繁使用,常见于NIO,用于数据缓冲区;
  2. 程序计数器:线程私有,当前线程所执行的字节码行号指示器;
  3. 虚拟机栈:1 线程私有,描述Java方法的执行过程;2 每个 Java 方法在执行时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程;3 会出现两种异常:(1)StackOverFlowError:如果Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前虚拟机栈的最大深度时,就抛出StackOverFlowError异常 (2)OutOfMemoryError:如果Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,就抛出OutOfMemoryError异常;
  4. 本地方法栈:线程私有,本地方法栈和虚拟机栈作用类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为执行本地方法服务;
  5. 堆:1 线程共享;2 JVM运行过程中创建的对象和产生的数据都存储在堆中,是垃圾回收的最主要区域,从GC的角度还可以细分为新生代和老年代;
  6. 方法区:1 线程共享;2 用于存储已加载的类信息、运行时常量池、静态变量、被final修饰的常量等;3 GC使用永久代来实现方法区,方法区的回收主要针对常量池的回收和类的卸载,一般可回收性较小;

在这里插入图片描述

程序计数器的作用

()
  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制;
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够恢复到正确的执行位置;

栈和堆的区别与联系

()
  1. 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址;
  2. 区别:2.1物理地址:栈物理地址分配连续,性能好;堆物理地址分配不连续,性能差些2.2分配内存:栈分配的内存在编译期确定,大小固定;堆分配的内存在运行期确定,大小不固定;堆内存远远大于栈2.3存放内容:栈中存放局部变量,关注的是程序方法的执行;堆中存放对象,关注的是数据的存储2.4是否线程私有:栈线程私有,堆线程共享;

方法区、永久代、元空间

  • 方法区是一种JVM规范,永久代和元空间都是其一种实现方式;
  • JDK1.8开始,移除永久代,原来永久代的数据被分到了堆和元空间中,元空间存储类的元信息,静态变量和常量池等放入堆中;
  • 永久代使用JVM内存,而元空间使用本地内存;
  • 元空间相比永久代的优势:
    4.1 常量池存在永久代中,容易出现性能问题和内存溢出;
    4.2 类和方法的信息大小难以确定,给永久代的大小指定带来困难;
    4.3 永久代会给GC带来不必要的复杂性;

Java3种内存分配策略

  • 栈内存:每个线程在创建时都会创建一个私有的虚拟机栈,用于存储基本数据类型、对象引用等。每个方法从调用直到执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程;
  • 堆内存:堆内存主要用于存储对象,线程共享;堆的大小可以在运行时动态调整;通过参数-Xms和-Xmx来设置堆的初始和最大大小;是垃圾回收的最主要区域;
  • 方法区:方法区用于存储已加载的类信息、运行时常量池、静态变量、被final修饰的常量等,线程共享;方法区的回收主要针对常量池的回收和类的卸载,一般可回收性较小;

如何确定对象可以被回收

  1. 引用计数法:1 为对象添加一个引用时,引用计数+1;为对象删除一个引用时,引用计数-1;2 如果一个对象的引用计数为0,表示该对象没有被引用,可以被回收;3 引用计数法容易产生循环引用问题,循环引用指两个对象互相引用,导致它们的引用一直存在,而不能被回收;
  2. 可达性分析:1 采用根搜索算法(GC Roots Tracing)来实现;2 根搜索算法以一系列GC Roots对象作为起点向下搜索,在一个对象到任何GC Roots对象都没有可达路径时,则称该对象是不可达的;3 不可达对象要经过至少两次标记才能判定其是否可以被回收,如果在两次标记后该对象仍然不可达,则可以被回收;3 Java采用可达性分析来判定对象是否可以被回收;

可以作为GCRoots的对象

  1. 虚拟机栈(栈帧中的局部变量区,也叫作局部变量表)中引用的对象;
  2. 方法区中的类静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. 本地方法栈中引用的对象。

在这里插入图片描述
从上图,reference1、reference2、reference3都是GC Roots,可以看出:

reference1-> 对象实例1;
reference2-> 对象实例2;
reference3-> 对象实例4;
reference3-> 对象实例4 -> 对象实例6;

可以得出对象实例1、2、4、6都具有GC Roots可达性,也就是存活对象,不能被GC回收的对象。

而对于对象实例3、5直接虽然连通,但并没有任何一个GC Roots与之相连,这便是GC Roots不可达的对象,这就是GC需要回收的垃圾对象。

常用的垃圾回收算法

  1. 标记清除算法:分标记和清除两阶段,标记阶段标记存活的对象,清除阶段清除未被标记的对象;由于标记清除算法不会重新整理可用内存空间,因此如果内存中需要被回收的小对象居多,就会引起内存碎片化问题,导致无法给大对象分配内存;
  2. 标记整理算法:让标记的所有存活对象都向一端移动,然后直接清理掉端边界以外的对象;优点:不会产生内存碎片;缺点:需要移动大量对象,处理效率较低;
  3. 复制算法:把内存划分为大小相等的两块,每次只使用其中一块;当这一块内存不足时就将还存活的对象复制到另一块上面,然后把这一块全部清理,下一次分配内存时使用另一块,如此循环往复;主要不足是只使用了内存的一半,同时如果有大量长时间存活的对象,这些对象将在两区域之间来回复制,降低运行效率;因此适合区域中的对象为“朝生夕死”的状态,比如新生代的GC过程;
  4. 分代收集算法:根据对象的存活周期将内存划分为不同的区域,JVM将堆分为新生代和老年代;新生代采用复制算法,老年代采用标记清除或标记整理算法;新生代之所以采用复制算法,是因为在新生代每次进行垃圾回收时都有大量对象被回收,需要复制的对象很少,不存在大量对象在内存中被来回复制的问题;老年代之所以采用标记清除或标记整理算法,是因为老年代存放生命周期较长的对象或大对象,每次只有少量对象被回收;

JVM垃圾回收策略

  1. JVM的垃圾回收区域包括JVM堆和方法区,可以将JVM堆分为新生代和老年代,方法区为永久代,其中新生代占堆空间1/3,老年代占堆空间2/3;
  2. 新生代:分Eden区、ServivorFrom区和ServivorTo区,Eden占8/10,ServivorFrom占1/10,ServivorTo占1/10;Java新创建的对象首先存放在Eden区,如果新创建的对象属于大对象(2KB~128KB),直接将其分配到老年代,在Eden区的内存空间不足时触发MinorGC(复制算法):(1)把在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区,同时年龄+1,当对象年龄达到老年代标准或ServivorTo区的内存空间不足时,将其移动到老年代;(2)清空Eden和ServivorFrom区中的对象;(3)将ServivorTo区和ServivorFrom区互换,原来的ServivorTo区成为下一次GC时的ServivorFrom区;
  3. 老年代:存放有较长生命周期的对象和大对象,老年代的GC过程称MajorGC(标记清除/标记整理算法),在进行MajorGC之前,JVM会先进行一次MinorGC,使得有些新生代对象晋升入老年代,导致老年代空间不足触发MajorGC;
  4. 永久代:即方法区的回收,主要针对常量池的回收和类的卸载,一般可回收性较小;

Minor GC和Full GC 有什么不同

大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起⼀次Minor GC。

  • Minor GC:指发生在新生代的的垃圾收集动作,Minor GC非常频繁,回收速度⼀般也比较快。
  • Major GC/Full GC:指发生在老年代的GC,出现了Major GC经常会伴随至少⼀次的Minor GC(并非绝对),Major GC的速度⼀般会比Minor GC的慢10倍以上。

何时触发GC

  • Young GC :新生代的Eden区满了之后会触发YoungGC;
  • Full GC:1 发生Young GC之前进行检查,如果“老年代可用的连续内存空间” < “新生代历次Young GC后升入老年代的对象总和的平均大小”,说明本次Young GC后升入老年代的对象大小可能超过了老年代当前可用内存空间,此时会触发FullGC;2 当老年代没有足够空间存放对象时,会触发一次FullGC;3 如果元空间区域的内存达到了所设定的阈值-XX:MetaspaceSize=,也会触发FullGC;

垃圾收集器

(1 Serial:客户端场景下默认新生代收集器。单线程收集器,垃圾收集时暂停其他所有工作线程。复制算法。适用于客户端模式下小型应用 2 ParNew: 服务端场景下默认新生代收集器。Serial收集器的多线程版本,可利用多CPU核心并行执行垃圾收集,垃圾收集时暂停其他所有工作线程。复制算法。适用于多CPU的服务器环境;3 Parallel Scavenge:新生代多线程收集器,垃圾收集时暂停其他所有工作线程。复制算法。吞吐量优先,适合在后台运算而不需要太多交互的任务; 4 Serial Old:Serial收集器的老年代版本。单线程。垃圾收集时暂停其他所有工作线程。标记整理算法。客户端场景下使用; 5 Parallel Old:Parallel Scavenge收集器的老年代版本,多线程,吞吐量优先,在进行垃圾收集工作的时候暂停其他所有工作线程。标记整理算法。在注重吞吐量以及CPU资源敏感的场合,优先考虑Parallel Scavenge + Parallel Old; 6 CMS:老年代收集器,多线程,以最短回收停顿时间为目标。标记清除算法。低停顿时间但吞吐量低。分六个流程(1 初始标记:标记 GC Roots 能直接关联到的对象,暂停所有用户线程,耗时很短 2 并发标记:GC Roots对堆中对象进行可达性分析,找出存活对象,它在整个回收过程中耗时最长,但可与用户线程并发执行 3 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象。通过重新扫描,减少下一个阶段”重新标记”的工作,因为下一个阶段会Stop The World;4 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,扫描堆中剩余的对象,需要暂停用户线程;5 并发清除:清除GC Roots不可达的对象,可与用户线程并发执行;6 并发重置:重置CMS收集器的数据结构,等待下一次垃圾回收),耗时最长的是并发标记和并发清除,总体上与用户线程并发执行。 缺点:(1 吞吐量降低。 并发阶段垃圾回收占用一部分CPU导致程序变慢,总吞吐量降低;2 无法处理浮动垃圾:并发清除阶段由于用户线程继续运行而产生的垃圾只能到下一次GC时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,不能像其它收集器那样等待老年代快满的时候再回收;3 空间碎片导致提前触发Full GC:标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC)7 G1:G1将整个Java堆划分为多个大小相等的独立区域(Region),跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(Garbage First名称的由来),保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。四步骤(1 初始标记:只标记GC Roots能直接关联到的对象,需要暂停所有用户线程,但耗时很短;2 并发标记:GC Roots开始对堆中对象进行可达性分析,找出存活的对象,它在整个回收过程中耗时最长,但可与用户线程并发执行;3 最终标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要暂停工作线程,但是可并行执行 ;4 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户线程一起并发执行,因为只回收一部分Region,因此时间是用户可控制的,而且停顿用户线程将大幅提高收集效率)。特点(1 并行与并发:能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间;2 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果;3 空间整合:从整体来看基于“标记—整理”算法实现,从局部(两个Region之间)来看是基于“复制”算法实现,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存;4 可预测的停顿:但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过M毫秒))

1 Serial收集器

  1. 客户端场景下默认新生代收集器;
  2. 单线程收集器,在进行垃圾收集工作的时候暂停其他所有工作线程( “Stop The World”);
  3. 复制算法;
  4. 适用于客户端模式下小型应用或者对延迟要求不高的场景;
    在这里插入图片描述

2 ParNew收集器

  1. 服务端场景下默认新生代收集器;
  2. Serial收集器的多线程版本,可以利用多个CPU核心并行执行垃圾收集,在进行垃圾收集工作的时候暂停其他所有工作线程( “Stop The World”);
  3. 复制算法;
  4. 适用于多CPU的服务器环境;
    在这里插入图片描述

3 Parallel Scavenge收集器

  1. 新生代多线程收集器,在进行垃圾收集工作的时候暂停其他所有工作线程( “Stop The World”);
  2. 复制算法;
  3. 吞吐量优先,适合在后台运算而不需要太多交互的任务;

在这里插入图片描述

4 Serial Old收集器

  1. Serial收集器的老年代版本,单线程,在进行垃圾收集工作的时候暂停其他所有工作线程;
  2. 标记整理算法;
  3. 客户端场景下使用;
    在这里插入图片描述

5 Parallel Old收集器

  1. Parallel Scavenge收集器的老年代版本,多线程,吞吐量优先,在进行垃圾收集工作的时候暂停其他所有工作线程;
  2. 标记整理算法;
  3. 在注重吞吐量以及CPU资源敏感的场合,优先考虑Parallel Scavenge + Parallel Old;

在这里插入图片描述

6 CMS(Concurrent Mark Sweep)收集器

  1. 老年代收集器,多线程,以最短回收停顿时间为目标;
  2. 标记清除算法;
  3. 低停顿时间但吞吐量低;

分以下六个流程:

  1. 初始标记:只标记 GC Roots 能直接关联到的对象,需要暂停所有用户线程,但耗时很短;
  2. 并发标记:GC Roots开始对堆中对象进行可达性分析,找出存活的对象,它在整个回收过程中耗时最长,但可与用户线程并发执行;
  3. 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象。通过重新扫描,减少下一个阶段”重新标记”的工作,因为下一个阶段会Stop The World;
  4. 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,扫描堆中剩余的对象,需要暂停用户线程;
  5. 并发清除:清除GC Roots不可达的对象,可与用户线程并发执行;
  6. 并发重置:重置CMS收集器的数据结构,等待下一次垃圾回收;

在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起执行,因此从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

具有以下缺点:

  • 吞吐量会降低:在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量会降低;
  • 无法处理浮动垃圾:浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次GC时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS;
  • 空间碎片导致提前触发Full GC:标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC;

在这里插入图片描述

7 G1收集器

  1. 使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region);
  2. G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也是Garbage First名称的由来);
  3. 这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率;
  4. G1收集器的运作分以下4个步骤:1 初始标记 2 并发标记 3 最终标记 4 筛选回收:
    1. 初始标记:只标记GC Roots能直接关联到的对象,需要暂停所有用户线程,但耗时很短;
    2. 并发标记:GC Roots开始对堆中对象进行可达性分析,找出存活的对象,它在整个回收过程中耗时最长,但可与用户线程并发执行;
    3. 最终标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要暂停工作线程,但是可并行执行;
    4. 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户线程一起并发执行,因为只回收一部分Region,因此时间是用户可控制的,而且停顿用户线程将大幅提高收集效率;

在这里插入图片描述

特点

  1. 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿用户线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行;
  2. 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果;
  3. 空间整合:与CMS的“标记—清除”算法不同,G1从整体来看基于“标记—整理”算法实现,从局部(两个Region之间)来看是基于“复制”算法实现,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC;
  4. 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒;

Java中的4种引用类型

(1 强引用:最常见,把一个对象赋给一个引用变量时就是强引用,如Object o = new Object(),被强引用关联的对象不会被回收;2 软引用:通过SoftReference类实现,如果一个对象只有软引用,在系统空间不足时该对象将被回收;3 弱引用:通过WeakReference类实现,如果一个对象只有弱引用,则在垃圾回收过程中一定会被回收;4 虚引用:通过PhantomReference类实现,最弱的引用关系,为一个对象设置虚引用的唯一目的是在这个对象被回收时收到系统通知,起哨兵作用)
  1. 强引用:最常见的就是强引用,在把一个对象赋给一个引用变量时,这个引用就是强引用,如:Object o = new Object(),被强引用关联的对象不会被回收,因此强引用是造成Java内存泄露的主要原因;
  2. 软引用:通过SoftReference类实现,如果一个对象只有软引用,则在系统空间不足时该对象将被回收。例如:缓存;
  3. 弱引用:通过WeakReference类实现,如果一个对象只有弱引用,则在垃圾回收过程中一定会被回收;
  4. 虚引用:通过PhantomReference类实现,最弱的引用关系,完全不影响对象的回收,等同于没有引用。为一个对象设置虚引用的唯一目的是在这个对象被回收时收到一个系统通知,起哨兵作用;

class文件

(1 即字节码文件,可以在所有平台上都能使用的中间代码,Java跨平台能力;2 一class文件对一Java类或Java接口作出全面描述,一class文件中只能包含一个类或接口 3 )
  1. 为了让Java语言具有跨平台能力,JVM提供了一种可以在所有平台上都能使用的中间代码——字节码,class文件即字节码文件;
  2. 一个class文件对一个Java类或Java接口作出了全面描述,一个class文件中只能包含一个类或接口;
  3. 无论class文件在何种系统上产生,无论JVM在何种系统上运行,JVM都能够正确读取和解释class文件;

class文件结构

()
ClassFile {
	u4 magic; //魔数:确定这个文件是否是一个能被JVM接受的class文件
	u2 minor_version;//次版本号
	u2 major_version;//主版本号
	//JVM拒绝执行超过其可处理版本号的class文件
	u2 constant_pool_count;//常量池容量计数值
	cp_info constant_pool[constant_pool_count-1];//常量池集合
	u2 access_flags;//访问标记:用于识别一些类或者接口层次的访问信息,包括:是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等
	u2 this_class;//类索引,用于确定这个类的全限定名
	u2 super_class;//父类索引,用于确定这个类的父类的全限定名
	u2 interfaces_count;//直接实现的父接口的数量
	u2 interfaces[interfaces_count];//接口索引集合就用来描述这个类实现了哪些接口
	u2 fields_count;//字段数量
	field_info fields[fields_count];//字段集合
	u2 methods_count;//⽅法数量
	method_info methods[methods_count];//⽅法集合
	u2 attributes_count;//属性数量
	attribute_info attributes[attributes_count];//属性集合
}

在这里插入图片描述

常量池

(1 Class常量池:Class文件的资源仓库,用于存放编译器生成的各种字面量和符号引用 2 运行时常量池:每一个类或接口的Class常量池的运行时表示形式,分配在方法区中,在类或接口被加载到JVM后,对应的运行时常量池就被创建出来。包含两类常量:2.1 编译期可知的字面量和符号引用(来自Class常量池) 2.2 运行期获得的常量(如String的intern方法)3 字符串常量池:可以理解为运行时常量池分出来的部分。加载时,对于Class常量池中的常量,如果是字符串将被放进字符串常量池中)

Class常量池

  1. Class常量池可以理解为Class文件的资源仓库,用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);
  2. 由于不同的Class文件中包含的常量的个数是不固定的,因此在Class文件的常量池入口处会设置两个字节的常量池容量计数器,记录常量池中常量的个数。Class常量池中主要存放两大类常量:字面量和符号引用;

  1. 字面量:一个字面量用于表示源代码中的一个基本值。几乎所有编程语言都有对基本值的字面量表示,例如:整数、浮点数、字符串,还有很多对字符类型、布尔类型、枚举类型的值也支持字面量表示,甚至还有一些对数组、对象等复合类型的值也支持字面量表示。下面的代码事例中,123和"hello"都是字面量;
int a = 123;
String s = "hello";
  1. 符号引用:符号引用是相对于直接引用来说的,主要包括了三类常量: 1 类和接口的全限定名;2 字段的名称和描述符;3 方法的名称和描述符;
  2. Class常量池的作用:Java代码在进行Javac编译的时候,并不像C/C++那样有“连接”这一步骤,而是在JVM加载class文件的时候进行动态连接。也就是说,在class文件中不会保存各个方法、字段的最终内存布局信息,因此这些方法、字段的符号引用如果不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被JVM使用。当JVM运行时,需要从常量池中获得对应的符号引用,然后解析、翻译到具体的内存地址中;

运行时常量池

  1. 运行时常量池是每一个类或接口的Class常量池的运行时表示形式;
  2. 运行时常量池分配在方法区中,在类或接口被加载到JVM后,对应的运行时常量池就被创建出来;
  3. 运行时常量池中包含两类常量:1 编译期可知的字面量和符号引用(来自Class常量池) 2 运行期获得的常量(如String的intern方法);

Class常量池、运行时常量池、字符串常量池的联系与区别

  1. Class常量池只是一个媒介场所,在JVM运行时,需要把Class常量池中的常量加载到内存中,进入到运行时常量池;
  2. 字符串常量池可以理解为运行时常量池分出来的部分。加载时,对于Class常量池中的常量,如果是字符串将被放进字符串常量池中;

类加载器与双亲委派机制

(1 启动类加载器:Java 核心类库的加载,即%{JDK_HOME}\lib 下的 rt.jar、resources.jar 等;2 扩展类加载器:负责加载%Java_HOME%/lib/ext目录中的jar包和class文件;3 应用程序类加载器;负责加载当前应用CLASSPATH下的jar包和class文件;4 自定义类加载器:通过 ClassLoader 类实现自定义加载器,可满足一些特殊场景的需求;5 双亲委派机制:将类加载逐级请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。本质上规定了类加载的顺序。首先是引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由应用程序类加载器或自定义的类加载器进行加载。好处:(1)避免了类的重复加载,因为如果父类已经加载过,子类就不会再加载了;(2)避免了Java API中定义的核心类被篡改(如JVM不允许定义一个java.lang.String的类,会出现java.lang.SecurityException,类加载器会做安全检查。)
  1. 类加载过程就是Java从源代码到最终运行的过程,包括编译和类加载,编译的过程就是把.java 文件编译成.class 文件;类加载的过程,就是把 class 文件装载到 JVM 内存中,装载完成以后就会得到一个 Class 对象,我们就可以使用 new 关键字来实例化这个对象;
  2. 类加载的过程涉及到了类加载器,JVM提供了3种类加载器:Bootstrap ClassLoader(启动类加载器)、Extension ClassLoader(扩展类加载器)、Application ClassLoader(应用程序类加载器);
  3. 1 启动类加载器:负责 Java 核心类库的加载,即%{JDK_HOME}\lib 下的 rt.jar、resources.jar 等;2 扩展类加载器:负责加载%Java_HOME%/lib/ext目录中的jar包和class文件;3 应用程序类加载器:负责加载当前应用CLASSPATH下的jar包和class文件;4 自定义类加载器:可以通过 ClassLoader 类实现自定义加载器,去满足一些特殊场景的需求;
  4. 双亲委派机制:在类加载时,首先判断该类是否被加载过,如果被加载过就直接返回,否则尝试加载。一个类加载器首先将类加载逐级请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。如果最终找不到该类,JVM抛出ClassNotFound异常。本质上规定了类加载的顺序。首先是引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由应用程序类加载器或自定义的类加载器进行加载。好处:(1)避免了类的重复加载,因为如果父类已经加载过,子类就不会再加载了;(2)避免了Java API中定义的核心类被篡改(如JVM不允许定义一个java.lang.String的类,会出现java.lang.SecurityException,类加载器会做安全检查。);

在这里插入图片描述

在这里插入图片描述

Java存在内存泄漏吗

(1 不再使用的对象占据在内存中不能被回收;2 长生命周期的对象持有短生命周期对象的引用时就很可能发生内存泄露,尽管短生命周期对象不再被使用,但由于长生命周期对象持有它的引用而导致其不能被回收。例如加载了一个对象放在全局Map缓存中,然后不再使用它,这个对象一直被Map引用,但却不再被使用)
  1. 内存泄露就是指不再被程序使用的对象占据在内存中不能被回收;
  2. Java中有垃圾回收机制,它可以保证对象不再被引用时自动被垃圾回收器从内存中清除掉。由于Java 使用有向图的方式进行垃圾回收管理,可以消除循环引用的问题。例如有两个对象相互引用,只要它们和GC Roots对象不可达,那么GC就可以回收它们;
  3. Java中的内存泄露情况举例:长生命周期的对象持有短生命周期对象的引用时就很可能发生内存泄露,尽管短生命周期对象不再被使用,但由于长生命周期对象持有它的引用而导致其不能被回收。例如加载了一个对象放在全局Map缓存中,然后不再使用它,这个对象一直被Map引用,但却不再被使用。

内存溢出的原因及解决方法

(1 原因(1内存加载数据量过大例如一次从数据库取出过多数据;2 代码死循环或循环产生过多重复的对象实体;3 过深的递归调用;4 集合类中有对对象的应用,使用完后未清空,这些对象不能被清空;5 JVM启动参数内存值设定过小)2 解决方案(1 检查数据库查询是否一次读取过多数据,可以采用分页方式避免;2 检查代码中是否有死循环或循环产生过多重复的对象实体;3检查代码中是否有过深的递归调用;4 检查List、Map等集合对象是否有使用完后未清空的问题;5 修改JVM启动参数,直接增加内存。(-Xss指定栈内存大小,-Xms指定堆内存初始值,-Xmx指定堆内存最大值)))

内存溢出原因

代码层面

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
  2. 代码中存在死循环或循环产生过多重复的对象实体;
  3. 有过深的递归调用;
  4. 集合类中有对对象的引用,使用完后未清空,List、Map等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收;

JVM层面

启动参数内存值设定过小。

解决方案

代码层面

  1. 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询;
  2. 检查代码中是否有死循环或循环产生过多重复的对象实体;
  3. 检查代码中是否有过深的递归调用;
  4. 检查List、Map等集合对象是否有使用完后未清空的问题;

JVM层面

修改JVM启动参数,直接增加内存。(-Xss指定栈内存大小,-Xms指定堆内存初始值,-Xmx指定堆内存最大值)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

hellosc01

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

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

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

打赏作者

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

抵扣说明:

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

余额充值