JVM

一、类加载子系统

  类加载子系统负责从文件系统或者网络中加载class文件,Class类信息存放于一块称为方法区的内存空间

1.1、加载模块

  1. 类的全限定名获取定义此类的二进制字节流

  2. 将字节流所代表的的静态存储结构转化为方法区的运行时数据;

(1)、加载器

引导类加载器BootStrapClassLoader

  • 加载java核心类库 只加载包名为java、javax、sun等开头的类

  • C/C++语言实,所以没有父加载器

  • 拓展类加载器(Extension ClassLoader)

派生于ClassLoader类

  • java语言编写

  • 派生于ClassLoader类

  • j加载jre/lib/ext子目录(扩展目录)下加载类库

应用程序类加载器(系统类加载器,AppClassLoader)

  • java语言编写

  • 加载环境变量classpath或系统属性 java.class.path指定路径下的类库

  • 程序中默认的类加载器

1.2、链接模块

(1)、验证

  验证加载类的正确性,不会危害虚拟机自身安全,主要包括文件格式验证,源数据验证,字节码验证,符号引用验证.

(2)、准备

  为类变量(static关键字修饰的变量)分配内存并且设置默认初始值,类变量会分配在方法区中

(3)、解析

  将常量池内的符号引用转换为直接引用的过程。(在编写T.java文件的时候会引用其他的java文件,但是在编译时不可能把所有引用的java文件都编译到T.class文件中,class文件中之保存了符号引用)

  解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。

1.3、初始化模块

  初始化阶段就是执行类构造器方法<clinit>,该方法 是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来

  如果类中没有静态变量,那么字节码文件中就不会有clinit方法,

二、双亲委派机制工作原理

工作原理:

  • 先将请求委托给父类的加载器去执行

  • 向上委托,依次递归,请求最终将到达顶层启动类加载器

  • 父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,在加载器将会尝试自己去加载,这就是双亲委派机制。

2.1、双亲委派机制的优势

  1. 避免类的重复加载

  2. 保护程序安全,防止核心API被随意修改

  3. 保证核心API包的访问权限

2.2、JVM中表示两个class对象是否为同一个类

  类的完整类名必须一致,包括包名

  要求加载这个类的ClassLoader(指ClassLoader实例对象)必须相同

三、程序计数器

  程序计数器(Program Conputer Register)这是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器

  作用:用来存储指向下一条指令的地址

  特征:

    1. 线程私有

    2. 记录当前字节码指令执行地址

    3. 不抛 OutOfMemoryError 异常

3.1、为什么使用PC寄存器记录当前线程的执行地址呢

  多线程宏观上是并行(多个事件在同一时刻同时发生)的,但实际上是并发交替执行的。因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行

  JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。

3.2、PC寄存器为什么会设定为线程私有?

  我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停的做任务切,这样必然会导致经常中断或恢复,如何保证分毫无差呢?

  为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

四、虚拟机栈

4.1、JVM栈的基本内容

  • 虚拟机栈(Java Virtual Machine Stack)是什么? 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),一个栈桢对应这一次次java方法调用。它是线程私有的。栈是一种快速有效的分配存储方式,访问速度仅次于PC寄存器

  • 虚拟机栈(Java Virtual Machine Stack)`生命周? 生命周期和线程是一致

  • 作用 主管java程序的运行,它保存方法的局部变量、8种基本数据类型、对象的引用地址、部分结果,并参与方法的调用和返回。

    • 局部变量:相较于成员变量(成员变量或称属性)

    • 基本数据变量:8种基本数据类型

    • 引用类型变量:类,数组,接口

  • 栈的特点(优点) 访问速度仅次于程序计数器。栈来说不存在垃圾回收

  • 栈中可能出现的异常

    • 如果采用固定大小的java虚拟机,那每一个线程的虚拟机栈容量可以在线程创建时独立选定,如果线程请求分配的栈容量超过Java虚拟机栈的允许的最大容量,Java虚拟机就会抛出一个堆栈溢出(StackOverFlowError)异常

    • 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机就会抛出内存溢出OutOfMemeoryError)异常。

  • 设置栈的大小

    • -Xss选项来设置线程的最大栈空间

五、本地方法栈

  本地方法栈执行的是 Native 方法,本地方法栈也会抛出抛出 StackOverflowError 和 OutOfMemoryError 异常

六、Java堆

  Java虚拟机所管理内存最大、被所有线程共享的一块区域,目的是用来存放对象

    • 线程共享 堆存放的对象

    • 存放对象 基本上所有的对象实例和数组都要在堆上进行分配

    • 垃圾收集 Java堆也被称为“GC堆”;是垃圾回收器的主要操作内存区域

    • 抛出 OutOfMemoryError 异常 Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可,实现时既可以实现成固定大小,也可以是扩展的。如果在堆中没有完成实例分配,并且堆也无法扩展,将抛出OutOfMemoryError 异常。

七、方法区

  存储已被Java虚拟机加载的类(static修饰的)信息、常量、静态变量、即时编译器编译后的代码等数据。

  方法区也称为**“永久代”,这是因为垃圾回收器对方法区的垃圾回收比较少主要是针对常量池的回收以及对类型的卸载,回收条件比较苛刻**

  在JDK1.8 的 HotSpot 虚拟机中,已经去掉了方法区的概念,Metaspace 代替,并且将其移到了本地内存来规划了。

八、运行时常量池

  运行时常量池(Runtime Constant Pool)用于存放编译期生成的各种字面量和符号引用是方法区的一部分

  抛出 OutOfMemoryError 异常: 运行时常量池是方法区的一部分,会受到方法区内存的限制,当常量池无法申请到内存时,会抛出该异常。

九、直接内存

  直接内存,并不是虚拟机运行时数据区的一部分,它也不是Java虚拟机规范定义的内存区域,1.8后就将方法区移除了,用元数据区来代替,并且将元数据区从虚拟机运行时数据区移除了,转到了本地内存中

  这块区域是受本机物理内存的限制,当申请的内存超过了本机物理内存才会抛出 OutOfMemoryError 异常

------------------------------垃圾回收--------------------------

10 、为什么要进行垃圾回收

如果不回收这些无用的对象占据的内存,那么新创建的对象申请不了内存空间,系统就会抛出异常而无法运行,所以必须要经常进行内存的回收

11、回收哪部分区域内存

Java运行时内存结构,其中程序计数器、虚拟机栈、本地方法栈这三个区域是线程私有的,随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊的执行着入栈和出栈操作,这几个区域的内存分配和回收都具备确定性,在方法结束或线程结束时,内存也就跟着回收了,所以不需要我们考虑。

那么现在就剩下Java堆和方法区了,这两块区域在编译期间我们并不能完全确定创建多少个对象,有些是在运行时期创建的对象,所以Java内存回收机制主要是作用在这两块区域

12、如何判断对象为垃圾对象

12.1、引用计数算法

给每一个创建的对象增加一个引用计数器,每当有一个地方引用它时,这个计数器就加1;而当引用失效时,这个计数器就减1。

当这个引用计数器值为0时,也就是说这个对象没有任何地方在使用它了,那么这就是一个无效的对象,便可以进行垃圾回收了

这种算法无法解决对象之间的循环引用问题。

12.2、根搜索算法

通过一系列名为“GC Roots” 的对象作为终点,当一个对象到GC Roots 之间无法通过引用到达时,那么该对象便可以进行回收了

12.3、那么有哪些对象可以作为 GC Roots 呢?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象

  • 方法区中的静态变量属性引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中(JNI)(即一般说的Native方法)的引,用的对象

13、如何进行垃圾回收

13.1、标记-清除算法

  分为标记-清除两个阶段,首先根据上面的根搜索算法标记出所有需要回收的对象,在标记完成后,然后在统一回收掉所有被标记的对象。

  缺点

    • 效率低:标记和清除这两个过程的效率都不高

    • 容易产生内存碎片:因为内存的申请通常不是连续的,那么清除一些对象后,那么就会产生大量不连续的内存碎片,而碎片太多时,当有个大对象需要分配内存时,便会造成没有足够的连续内存分配而提前触发垃圾回收,甚至直接抛出OutOfMemoryExecption。

13.2、复制算法

  将可用内存按容量划分为大小相等的两块区域,每次只使用其中一块,当这一块的内存用完了,就将还活着的对象复制到另一块区域上,然后再把已使用过的内存空间一次性清理掉。

  优点:每次都是只对其中一块内存进行回收,不用考虑内存碎片的问题,而且分配内存时,只需要移动堆顶指针,按顺序进行分配即可,简单高效。

  缺点: 将内存分为两块,但是每次只能使用一块,也就是说,机器的一半内存是闲置的,这资源浪费有点严重。并且如果对象存活率较高,每次都需要复制大量的对象,效率也会变得很低。

13.3、标记-整理算法

  首先标记出所有存活的对象,然后让所有存活对象向一端进行移动,最后直接清理到端边界以外的内存。

  局限性:只有对象存活率很高的情况下,使用该算法才会效率较高。

13.4、分代收集算法

  当前商业虚拟机都是采用此算法

  根据对象的存活周期不同将内存分为几块,然后不同的区域采用不同的回收算法。

    • 存活周期较短,每次都有大批对象死亡,只有少量存活的区域,采用复制算法

    • 存活周期较长,没有额外空间进行分配担保的区域,采用标记-整理算法,或者标记-清除算法

  堆有新生代和老年代两块区域组成,而新生代区域又分为三个部分,分别是 Eden,From Surivor,To Survivor ,比例是8:1:1。

  新生代采用复制算法,每次使用一块Eden区和一块Survivor区,当进行垃圾回收时,将Eden和一块Survivor区域的所有存活对象复制到另一块Survivor区域,然后清理到刚存放对象的区域,依次循环。

  老年代采用标记-清除或者标记-整理算法,根据使用的垃圾回收器来进行判断。

  至于为什么要这样,这是由于内存分配的机制导致的,新生代存的基本上都是朝生夕死的对象,而老年代存放的都是存活率很高的对象。

14、何时进行垃圾回收

  程序员可以调用 System.gc()方法,手动回收,但是调用此方法表示希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策。

  其次虚拟机会自行根据当前内存大小,判断何时进行垃圾回收,比如前面所说的,新生代满了,新产生的对象无法分配内存时,便会触发垃圾回收机制。

  这里需要说明的是宣告一个对象死亡,至少要经历两次标记,前面我们说过,如果对象与GC Roots 不可达,那么此对象会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法,当对象没有覆盖 finalize()方法,或者该方法已经执行了一次,那么虚拟机都将视为没有必要执行finalize()方法。

  如果这个对象有必要执行 finalize() 方法,那么该对象将会被放置在一个有虚拟机自动建立、低优先级,名为 F-Queue 队列中,GC会对F-Queue**进行第二次标记**,如果对象在finalize() 方法中成功拯救了自己(比如重新与GC Roots建立连接),那么第二次标记时,就会将该对象移除即将回收的集合,否则就会被回收。

15、垃圾回收器

①、新生代垃圾收集器:Serial、ParNew、Parallel Scavenge;

  老年代垃圾收集器:Serial Old(MSC)、Parallel Old、CMS;

  整堆垃圾收集器:G1

②、垃圾收集器之间的连线表示可以搭配使用,有如下几种组合:

  Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、 Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;

③、串行收集器Serial:Serial、Serial Old

  并行收集器 Parallel:Parallel Scavenge、Parallel Old

  并发收集器:CMS、G1

16、Serial收集器

  • 作用于新生代,采用的垃圾回收算法是复制算法

  • 单线程

  • 进行垃圾收集时,必须暂停所有工作线程,系统这时候会有卡顿现象产生。

  • 适用场景

Serial 收集器由于没有线程交互的开销,对于限定单个CPU的环境,可以获得最高的单线程收集效率。

  一般在用户的桌面场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆或一两百兆的新生代,定顿时间可以控制在几十毫秒,只要不是频繁发生的,这点停顿是可以接受的。

  所以 Serial 收集器对于运行在 Client 模式下的虚拟机是一种很好的选择。

17、ParNew收集器

其实就是Serial收集器的多线程版本。也就是说其特点除了多线程,其余和Serial收集器一样

18、Parallel Scavenge收集器

Parallel Scanvenge 收集器是为了达到一个可控制的吞吐量。

吞吐量 = 运行用户代码的时间 / (运行用户代码的时间+垃圾收集时间)

  可以用下面两个参数进行精确控制:

  -XX:MaxGCPauseMills 设置最大垃圾收集停顿时间

  -XX:GCTimeRatio 设置吞吐量大小

  • 作用于新生代:一个新生代垃圾收集器,采用的垃圾回收算法是复制算法。

  • 多线程

  • 这个收集器可以精确控制吞吐量。

  • 适用场景

设置垃圾收集停顿时间短适合需要与用户快速交互的程序;而设置高吞吐量可以最高效的利用CPU效率,尽快的完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

19、Serial Old收集器

Serial Old 收集器是 Serial 收集器的老年代版本

①、作用于老年代 ②、单线程 ③、使用标记-整理算法 ④、进行垃圾收集时,必须暂停所有工作线程

20、Parallel Old收集器

Parallel Old 是 Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。 ①、作用于老年代 ②、多线程 ③、使用标记-整理算法 除了具有以上几个特点,比较关键的是能和新生代收集器 Parallel Scavenge配置使用,获得吞吐量最大化的效果。

21、CMS收集器

CMS,全称为 Concurrent Mark Sweep ,顾名思义并发的,采用标记-清除算法。另外也将这个收集器称为并发低延迟收集器(Concurrent Low Pause Collector)

这是一款跨时代的垃圾收集器,真正做到了垃圾收集线程与用户线程(基本上)同时工作。和 Serial 收集器的 Stop The World(妈妈打扫房间的时候,你不能再将垃圾丢到地上) 相比,真正做到了妈妈一边打扫房间,你一边丢垃圾。

  ==①、作用于老年代   ②、多线程   ③、使用标记-清除算法==

整个算法过程分为如下 4 步:

  • 初始标记(CMS initial mark):只是仅仅标记GC Root 能够直接关联的对象,速度很快,但是需要“Stop The World”  

  • 并发标记(CMS concurrent mark):进行GC Root Tracing的过程,简单来说就是遍历初始标记阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象。

  • 重新标记(CMS Remark):修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,需要“Stop The World”。这个时间一般比初始标记长,但是远比并发标记时间短。

  • 并发清除(CMS concurrent sweep):对上一步标记的对象进行清除操作。

由于整个过程最耗时的操作是第二(并发标记)、四步(并发清除),而这两步垃圾收集器线程是可以和用户线程一起工作的。所以整体来说,CMS垃圾收集和用户线程是一起并发的执行的。

缺点:

对CPU资源敏感:因为在并发阶段,会占用一部分CPU资源,从而导致应用程序变慢,总吞吐量会降低

产生浮动垃圾:由于CMS并发清理阶段用户线程还在工作,这个时候产生的垃圾,CMS无法在本次收集中处理掉它们,只能留在下一次GC时再将其处理掉,这部分垃圾称为“浮动垃圾”。

产生内存垃圾碎片:因为采用的算法是标记-清除,很明显,会有空间碎片产生

22、G1收集器

可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收

它并不像前面介绍的所有垃圾收集器是区分新生代,老年代的,它作用于全区域将整个Java堆划分为多个大小固定的独立区域(Regin),并且跟踪这些区域的垃圾堆积面积在后台维护一个优先级列表每次根据允许的收集时间,优先回收垃圾最多的区域

前面讲的 CMS 垃圾收集器相比,有两个显著的改进

  • 采用 标记-整理 的回收算法:这样不会产生空间碎片

  • 可以精确的控制停顿时间:能让使用者明确指定一个长度为M毫秒的时间片内,消耗在垃圾回收上的时间不超过 N 毫秒。

  • 作用于整个Java堆:G1收集器不区分年轻代和老年代,是整堆垃圾收集器。

23、如何选择垃圾收集器

除非应用程序有相当严格的暂停时间要求,否则就让JVM自己选择垃圾收集器。并且可以适当优先调整堆的大小来提高性能

1. 如果应用程序内存小于100M,那么使用选项选择串行收集器-XX:+UseSerialGC。
2. 如果应用程序将在单核处理器上运行,并且没有停顿时间的要求,选择串行-XX:+UseSerialGC或者 JVM 自己选
3. 如果允许停顿时间超过1秒,选择并行或 JVM 自己选
4. 如果响应时间比总吞吐量更重要,并且垃圾收集暂停必须保持短于大约1秒,则使用-XX:+UseConcMarkSweepGC或选择并发收集器-XX:+UseG1GC。

24、JVM参数

  形式又分为两大类:

  • Boolean类型

格式:-XX:[+-]<name> 表示启用或者禁用name属性。
例子:-XX:+UseG1GC(表示启用G1垃圾收集器)
  • Key-Value类型
格式:-XX:<name>=<value> 表示name的属性值为value。
例子:-XX:MaxGCPauseMillis=500(表示设置GC的最大停顿时间是500ms)

25、最大堆和最小堆内存设置

-Xms512M:设置堆内存初始值为512M
-Xmx1024M:设置堆内存最大值为1024M

 

26、内存分配

Java是自动进行内存管理的,所谓自动化就是,不需要程序员操心,Java会自动进行内存分配内存回收这两方面。

27、Minor GC

Young GC,指的是新生代 GC,发生在新生代(Eden区和Survivor区)的垃圾回收。因为Java对象大多是朝生夕死的,所以 Minor GC 通常很频繁,一般回收速度也很快。

28、Major GC

也叫Old GC,指的是老年代的 GC,发生在老年代的垃圾回收,该区域的对象存活时间比较长,通常来讲,发生 Major GC时,会伴随着一次 Minor GC,而 Major GC 的速度一般会比 Minor GC 慢10倍

29、Full GC

指的是全区域(整个堆)的垃圾回收,通常来说和 Major GC 是等价的。

1、对象优先在 Eden 上分配。当 Eden 区没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC(新生代GC)。

30、大对象直接进行老年代

通常大对象是指需要大量连续内存空间的Java对象,比较典型的就是那种很长的字符串以及数组。

系统中出现大量大对象是很影响性能的,这样会导致还有不少空间时就提前触发垃圾回收来放置这些对象。

31、何时会发生 Full GC?

新生代内存分为一块 Eden区,和两块 Survivor 区域,当发生一次 Minor GC时,虚拟机会将Eden和一块Survivor区域的所有存活对象复制到另一块Survivor区域,通常情况下,一块 Survivor 区域是能够存放GC后剩余的对象的,但是极端情况下,Minor GC后仍然有大量存活的对象,那么一块 Survivor 区域就会存放不下这么多的对象,那么这时候就需要老年代进行分配担保,让无法放入 Survivor 区域的对象直接进入到老年代,当然前提是老年代还有空间能够存放这些对象。但是实际情况是在完成GC之前,是不知道还有多少对象能够存活下来的,所以老年代也无法确认是否能够存放GC后新生代转移过来的对象,那么这该怎么办呢?

在发生 Minor GC 时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间,如果大于,则改为 Full GC。如果小于,则查看 HandlePromotionFailure 设置是否允许担保失败,如果允许,那只会进行一次 Minor GC,如果不允许,则也要进行一次 Full GC。

-XX:-HandlePromotionFailure

回到第一个问题,老年代也无法确认是否能够存放GC后新生代转移过来的对象,那么这该怎么办呢?

也就是取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,然后与老年代剩余空间进行比较,来决定是否进行 Full GC,从而让老年代腾出更多的空间。

通常情况下,我们会将 HandlePromotionFaile 设置为允许担保失败,这样能够避免频繁的发生 Full GC。

32、虚拟机监控和分析工具(1)——命令行

jps:显示指定系统内所有的 HotSpot 虚拟机进程。

jstat:是用于监视虚拟机各种运行时状态信息的

jinfo:实时的查看和调整虚拟机各项参数通过此命令,可以实时的查看和调整虚拟机的各项参数**

jmap:用于生成堆存储快照:生产环境中,发生OOM(堆内存溢出)异常时,我们可以通过这个快照文件来快速定位到具体代码位置。

jstack: jstack [选项] pid:于生成虚拟机当前时刻的线程快照。

33、内存溢出&&内存泄漏

内存溢出(OutOfMemoryError):没有空闲内存,并且垃圾收集器也无法提供更多内存(意思是即使GC之后,也无法提供空闲的内存)

 

 

内存泄漏(Memory Leak):只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现OutOfMemory异常,导致程序崩溃。

34、强、软、弱、虚引用

  • 强引用(StrongReference) 是指在程序代码之中普遍存在的引用赋值;无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

  • 软引用(SoftReference) :软引用: 内存不足即回收

    系统将要发生内存溢出之前,会把这些对象列入回收范围之中,以备进行第二次(第一次指的是回收了不可触及的垃圾对象)垃圾回收的时候回收它们。软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。

  • 弱引用(WeakReference)发现即回收 当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。软引用、弱引用都非常适合来保存那些可有可无的缓存数据

  • 虚引用(PhantomReference) 对象回收跟踪 **它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null。==为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程**==

 

posted @ 2021-06-04 11:32  jingdy  阅读(131)  评论(0编辑  收藏  举报