type
status
date
May 25, 2023 09:31 AM
slug
summary
tags
category
icon
password
日期
标签 2
状态
链接地址
Column
标签
先想想一些问题
1 我们开发人员编写的 Java 代码是怎么让电脑认识的
首先先了解电脑是二进制的系统,他只认识 01010101
比如我们经常要编写 HelloWord.java 电脑是怎么认识运行的 HelloWord.java 是我们程序员编写的,我们人可以认识,但是电脑不认识
Java 文件编译的过程 因此就需要编译:
- 程序员编写的. java 文件
- 由 javac 编译成字节码文件. class:(为什么编译成 class 文件,因为 JVM 只认识. class 文件)
- 在由 JVM 编译成电脑认识的文件 (对于电脑系统来说 文件代表一切)
(这是一个大概的观念 抽象画的概念)

2 为什么说 java 是跨平台语言
这个夸平台是中间语言(JVM)实现的夸平台 java 有 JVM 从软件层面屏蔽了底层硬件、指令层面的细节让他兼容各种系统
难道 C 和 C++ 不能夸平台吗 其实也可以 C 和 C++ 需要在编译器层面去兼容不同操作系统的不同层面,写过 C 和 C++ 的就知道不同操作系统的有些代码是不一样
3 Jdk 和 Jre 和 JVM 的区别
看 Java 官方的图片,Jdk 中包括了 Jre,Jre 中包括了 JVM
Jvm 在倒数第二层 由他可以在(最后一层的)各种平台上运行
Jre 大部分都是 C 和 C++ 语言编写的,他是我们在编译 java 时所需要的基础的类库
Jdk 还包括了一些 Jre 之外的东西 ,就是这些东西帮我们编译 Java 代码的, 还有就是监控 Jvm 的一些工具

4 为什么要学习 JVM
为什么要学习 Jvm,学习 Jvm 可以干什么
首先先想:为什么 Java 可以霸占企业级开发那么多年 因为:内存管理
那就有些人可能又会要说了,Jvm 都做完了这些操作,为什么我们还要学习,学习个屁啊
这就好像一个人一样,我一般情况吃什么从来不用考虑进入了身体那一个部位,可是总有一天,假如吃了不该吃的也是要进医院的
深入学习 JVM
注释:JVM 就是 Java 虚拟机,Java 虚拟机就是 JVM
1 JVM 运行时数据区
什么是运行时数据区(就是我们 java 运行时的东西是放在那里的)

2 解析 JVM 运行时数据区
2.1 方法区(Method Area)
- 方法区是所有线程共享的内存区域,它用于存储已被 Java 虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 它有个别命叫 Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出 OutOfMemoryError 异常。
2.2 Java 堆(Java Heap)
- java 堆是 java 虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
- 在 Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
- java 堆是垃圾收集器管理的主要区域,因此也被成为 “GC 堆”。
- 从内存回收角度来看 java 堆可分为:新生代和老生代。
- 从内存分配的角度看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区。
- 无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。
- 根据 Java 虚拟机规范的规定,java 堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。
2.3 程序计数器(Program Counter Register)
- 程序计数器是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址 (行号)
- 由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为 “线程私有” 的内存。程序计数器内存区域是虚拟机中唯一没有规定 OutOfMemoryError 情况的区域。
总结:也可以把它叫做线程计数器
例子:在 java 中最小的执行单位是线程,线程是要执行指令的,执行的指令最终操作的就是我们的电脑,就是 CPU。在 CPU 上面去运行,有个非常不稳定的因素,叫做调度策略,这个调度策略是时基于时间片的,也就是当前的这一纳秒是分配给那个指令的。
假如
:线程 A 在看直播

突然,线程 B 来了一个视频电话,就会抢夺线程 A 的时间片,就会打断了线程 A,线程 A 就会挂起

然后,视频电话结束,这时线程 A 究竟该干什么? (线程是最小的执行单位,他不具备记忆功能,他只负责去干,那这个记忆就由:
程序计数器来记录
)

2.4 Java 虚拟机栈(Java Virtual Machine Stacks)
- java 虚拟机是线程私有的,它的生命周期和线程相同。
- 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
解释:每虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量,操作数栈,动态链接,出口等。

解析栈帧:
- 局部变量表:是用来存储我们临时 8 个基本数据类型、对象引用地址、returnAddress 类型。(returnAddress 中保存的是 return 后要执行的字节码的指令地址。)
- 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
- 动态链接:假如我方法中,有个 service.add() 方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
- 出口:出口是什呢,出口正常的话就是 return 不正常的话就是抛出异常落
思考:. 一个方法调用另一个方法,会创建很多栈帧吗? 答:会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面
栈指向堆是什么意思? 栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址,堆中的数据等下讲
递归的调用自己会创建很多栈帧吗? 递归的话也会创建多个栈帧,就是一直排下去
2.5 本地方法栈(Native Method Stack)
- 本地方法栈很好理解,他很栈很像,只不过方法上带了 native 关键字的栈字
- 它是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)的服务
- native 关键字的方法是看不到的,必须要去 oracle 官网去下载才可以看的到,而且 native 关键字修饰的大部分源码都是 C 和 C++ 的代码。
- 同理可得,本地方法栈中就是 C 和 C++ 的代码
3 Java 内存结构

上面已经讲了运行时数据区,这里就差几个小组件了
3.1 直接内存(Direct Memory)
- 直接内存不是虚拟机运行时数据区的一部分,也不是 java 虚拟机规范中定义的内存区域。但是既然是内存,肯定还是受本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。
- 在 JDK1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道 (Channel) 与缓冲区(Buffer)的 I/O 方式,它可以使用 native 函数库直接分配堆外内存,然后通脱一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native(本地)堆中来回复制数据。
直接内存与堆内存的区别: 直接内存申请空间耗费很高的性能,堆内存申请空间耗费比较低 直接内存的 IO 读写的性能要优于堆内存,在多次读写操作的情况相差非常明显
代码示例:(报错修改 time 值)
测试结果:
代码来源:「猕猴桃 0303」 链接为:blog.csdn.net/leaf_0303/a…
3.2 JVM 字节码执行引擎
虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。
“虚拟机”是一个相对于 “物理机” 的概念,虚拟机的字节码是不能直接在物理机上运行的,需要 JVM 字节码执行引擎编译成机器码后才可在物理机上执行。
3.3 垃圾收集系统
程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java 虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。
垃圾收集系统是 Java 的核心,也是不可少的,Java 有一套自己进行垃圾清理的机制,开发人员无需手工清理
4 JVM 的垃圾回收机制
垃圾回收机制简称 GC
GC 主要用于 Java 堆的管理。Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
4.1 什么是垃圾回收机制
程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java 虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。
GC 是不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个 Java 程序中的执行是自动的,不能强制执行清楚那个对象,即使程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员唯一能做的就是通过调用 System.gc 方法来 “建议” 执行垃圾收集器,但是他是否执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。
手动执行 GC:
4.2 finalize 方法作用
- finalize() 方法是在每次执行 GC 操作之前时会调用的方法,可以用它做必要的清理工作。
- 它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的。
代码示例
4.3 新生代、老年代、永久代 (方法区) 的区别
- Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
- 在 Java 中,堆被划分成两个不同的区域:新生代 (Young)、老年代 ( Old )。
先不要管为什么要分代,后面有例子
- 老年代就一个区域。新生代 (Young) 又被划分为三个区域:Eden、From Survivor、To Survivor。
- 这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
- 默认的,新生代 (Young) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。
- 其中,新生代 (Young) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 From Survivor 和 ToSurvivor ,以示区分。
- 默认的,Edem : From Survivor : To Survivor = 8 : 1 : 1 (可以通过参数 –XX:SurvivorRatio 来设定),即: Eden = 8/10 的新生代空间大小,From Survivor = To Survivor = 1/10 的新生代空间大小。
- JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
- 因此,新生代实际可用的内存空间为 9/10 (即 90%) 的新生代空间。
- 永久代就是 JVM 的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。
4.3.1 为什么要这样分代:
其实主要原因就是可以根据各个年代的特点进行对象分区存储,更便于回收,采用最适当的收集算法:
- 新生代中,每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
- 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用 “标记 - 清理” 或者 “标记 - 整理” 算法。
新生代又分为 Eden 和 Survivor (From 与 To,这里简称一个区)两个区。加上老年代就这三个区。数据会首先分配到 Eden 区当中(当然也有特殊情况,如果是大对象那么会直接放入到老年代(大对象是指需要大量连续内存空间的 java 对象)。当 Eden 没有足够空间的时候就会触发 jvm 发起一次 Minor GC,。如果对象经过一次 Minor-GC 还存活,并且又能被 Survivor 空间接受,那么将被移动到 Survivor 空间当中。并将其年龄设为 1,对象在 Survivor 每熬过一次 Minor GC,年龄就加 1,当年龄达到一定的程度(默认为 15)时,就会被晋升到老年代中了,当然晋升老年代的年龄是可以设置的。
4.3.2 Minor GC、Major GC、Full GC 区别及触发条件
- Minor GC 是新生代 GC,指的是发生在新生代的垃圾收集动作。由于 java 对象大都是朝生夕死的,所以 Minor GC 非常频繁,一般回收速度也比较快。
- Major GC 是老年代 GC,指的是发生在老年代的 GC,通常执行 Major GC 会连着 Minor GC 一起执行。Major GC 的速度要比 Minor GC 慢的多。
- Full GC 是清理整个堆空间,包括年轻代和老年代
Minor GC 触发条件一般为:
- eden 区满时,触发 MinorGC。即申请一个对象时,发现 eden 区不够用,则触发一次 MinorGC。
- 新创建的对象大小 > Eden 所剩空间
Major GC 和 Full GC 触发条件一般为: Major GC 通常是跟 full GC 是等价的
- 每次晋升到老年代的对象平均大小 > 老年代剩余空间
- MinorGC 后存活的对象超过了老年代剩余空间
- 永久代空间不足
- 执行 System.gc()
- CMS GC 异常
- 堆内存分配很大的对象
4.4 如何判断对象是否存活
4.4.1 引用计数法
- 引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。
- 首先需要声明,至少主流的 Java 虚拟机里面都没有选用引用计数算法来管理内存。
- 什么是引用计数法:每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,计数器为 0 就代表该对象死亡
引用计数法的优点:
- 引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,
引用计数法的缺点:
- 主流的 Java 虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。
- 例如:
引用计数法的应用场景:
- 建议不要用
4.4.2 可达性分析法
- 该种方法是从 GC Roots 开始向下搜索,搜索所走过的路径为引用链。当一个对象到 GC Roots 没用任何引用链时,则证明此对象是不可用的,表示可以回收。

- 上图上图中 Object1、Object2、Object3、Object4、Object5 到 GC Roots 是可达的,表示它们是有引用的对象,是存活的对象不可以进行回收
- Object6、Object7、Object8 虽然是互相关联的,但是它们到 GC Roots 是不可达的,所以他们是可以进行回收的对象。
那些可以作为 GC Roots 的对象:
可达性算法的优点:
- 解决相互循环引用问题。
可达性算法的优点:
- 目前和引用计数法比没得缺点
可达性算法的应用场景:
- 这是目前主流的虚拟机都是采用的算法
4.5 垃圾回收机制策略(也称为 GC 的算法)
4.5.1 引用计数算法(Reference counting)
每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,计数器为 0 就代表该对象死亡,这时就应该对这个对象进行垃圾回收操作。
引用计数法的优点:
- 引用计数算法的实现简单,判定效率也很高。
引用计数法的缺点:
- 主流的 Java 虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。
- 例如:
引用计数法的应用场景:
- 建议不要用
4.5.2 标记–清除算法(Mark-Sweep)
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。 分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。
标记清除算法的优点:
- 是可以解决循环引用的问题
- 必要时才回收 (内存不足时)
标记清除算法的缺点:
- 回收时,应用需要挂起,也就是 stop the world。
- 标记和清除的效率不高,尤其是要扫描的对象比较多的时候
- 会造成内存碎片 (会导致明明有内存空间, 但是由于不连续, 申请稍微大一些的对象无法做到),
标记清除算法的应用场景:
- 该算法一般应用于老年代, 因为老年代的对象生命周期比较长。
4.5.3 标记–整理算法
标记清除算法和标记压缩算法非常相同,但是标记压缩算法在标记清除算法之上解决内存碎片化(有些人叫 “标记整理算法” 为 “标记压缩算法”)
标记 - 整理法是标记 - 清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记 - 整理的目的。
标记–整理算法优点:
- 解决标记清除算法出现的内存碎片问题,
标记–整理算法缺点:
- 压缩阶段,由于移动了可用对象,需要去更新引用。
标记–整理算法应用场景:
- 该算法一般应用于老年代, 因为老年代的对象生命周期比较长。
4.5.4 复制算法
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。
这个算法与标记 - 整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。
复制算法的优点:
- 在存活对象不多的情况下,性能高,能解决内存碎片和 java 垃圾回收算法之 - 标记清除 中导致的引用更新问题。
复制算法的缺点::
- 会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;如果存活对象的数量比较大,复制算法的性能会变得很差。
复制算法的应用场景:
- 复制算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用复制算法进行拷贝时效率比较高。
- jvm 将 Heap(堆)内存划分为新生代与老年代。又将新生代划分为 Eden 与 2 块 Survivor Space(幸存者区) ,然后在 Eden –>Survivor Space 与 To Survivor 之间实行复制算法。
- 不过 jvm 在应用复制算法时,并不是把内存按照 1:1 来划分的,这样太浪费内存空间了。一般的 jvm 都是 8:1。也即是说, Eden 区: From 区: To 区域的比例是始终有 90% 的空间是可以用来创建对象的, 而剩下的 10% 用来存放回收后存活的对象。
4.5.5 分代算法(主要的算法就是上面四种,这个是附加的)
这种算法,根据对象的存活周期的不同将内存划分成几块,新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。可以用抓重点的思路来理解这个算法。 新生代对象朝生夕死, 对象数量多,只要重点扫描这个区域,那么就可以大大提高垃圾收集的效率。另外老年代对象存储久,无需经常扫描老年代,避免扫描导致的开销。
新生代
- 在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代
- 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须 “标记清除法或者标记整理算法进行回收。
5 垃圾收集器
5.1 什么是垃圾收集器?
- 垃圾收集器是垃圾回收算法(引用计数法、标记清楚法、标记整理法、复制算法)的具体实现,不同垃圾收集器、不同版本的 JVM 所提供的垃圾收集器可能会有很在差别。
- 我这以 JDK8 为准:

图中展示了 7 种不同分代的收集器: Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old、G1
而它们所处区域,则表明其是属于新生代还是老年代的收集器:
- 新生代收集器:Serial、ParNew、Parallel Scavenge
- 老年代收集器:CMS、Serial Old、Parallel Old
- 整堆收集器:G1
两个收集器间有连线,表明它们可以搭配使用:
5.2 垃圾回收器详解
5.2.1 Serial
- Serial 收集器:新生代。发展历史最悠久的收集器。它是一个单线程收集器,它只会使用一个 CPU 或者线程去完成垃圾收集工作,而且在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
特点:
- 新生代收集器,使用复制算法收集新生代垃圾。
- 单线程的收集器,GC 工作时,其它所有线程都将停止工作。
- 简单高效,适合单 CPU 环境。单线程没有线程交互的开销,因此拥有最高的单线程收集效率。
使用方式:
5.2.2 ParNew
- ParNew 收集器:新生代。Serial 的多线程版本,即同时启动多个线程去进行垃圾收集。
特点:
- 新生代收集器。ParNew 垃圾收集器是 Serial 收集器的多线程版本,采用复制算法。
- 除了多线程外,其余的行为、特点和 Serial 收集器一样。
- 只有它能与 CMS 收集器配合使用。
- 但在单个 CPU 环境中,不比 Serail 收集器好,多线程使用它比较好。
使用方式:
5.2.3 Parallel Scavenge
- Parallel Scavenge 收集器:新生代。和 ParNew 的关注点不一样,该收集器更关注吞吐量,尽快地完成计算任务。
特点:
- 新生代收集器。
- 采用复制算法。
- 多线程收集。
- 与 ParNew 不同的是:高吞吐量为目标,(减少垃圾收集时间,让用户代码获得更长的运行时间)
使用方式:
5.2.4 Serial Old
- Serial Old 收集器:Serial 的老年代版本,使用标记 - 整理算法。
特点:
- 老年代收集器, 采用 “标记 - 整理” 算法。
- 单线程收集。
使用方式:
5.2.5 Parallnel old
- Parallnel old 收集器,多线程:Parallel 的老年代版本,使用标记 - 整理算法。
特点:
- 针对老年代。
- 采用 “标记 - 整理” 算法。
- 多线程收集。
- 但在单个 CPU 环境中,不比 Serial Old 收集器好,多线程使用它比较好。
使用方式:
5.2.6 CMS
- CMS 收集器:老年代。是一种以获取最短回收停顿时间为目标的收集器,适用于互联网站或者 B/S 系统的服务端上。
特点:
- 针对老年代,采用标记 - 清楚法清除垃圾;
- 基于 “标记 - 清除” 算法 (不进行压缩操作,产生内存碎片);
- 以获取最短回收停顿时间为目标;
- 并发收集、低停顿;
- CMS 收集器有 3 个明显的缺点:1. 对 CPU 资源非常敏感、2. 无法处理浮动垃圾,可能出现 “Concurrent Mode Failure” 失败、3. 产生大量内存碎片
- 垃圾收集线程与用户线程(基本上)可以同时工作
使用方式:
5.2.7 G1
- G1 收集器:分代收集器。当今收集器技术发展最前沿成果之一,是一款面向服务端应用的垃圾收集器。G1 可以说是 CMS 的终极改进版,解决了 CMS 内存碎片、更多的内存空间登问题。虽然流程与 CMS 比较相似,但底层的原理已是完全不同。
特点:
- 能充分利用多 CPU、多核环境下的硬件优势;
- 可以并行来缩短 (Stop The World) 停顿时间;
- 也可以并发让垃圾收集与用户程序同时进行;
- 分代收集,收集范围包括新生代和老年代
- 能独立管理整个 GC 堆(新生代和老年代),而不需要与其他收集器搭配;
- 能够采用不同方式处理不同时期的对象;
- 应用场景可以面向服务端应用,针对具有大内存、多处理器的机器;
- 采用标记 - 整理 + 复制算法来回收垃圾
使用方式:
6 JVM 参数配置
6.1 JVM 内存参数简述
6.2 JVM 的 GC 收集器设置
6.3 JVM 参数在哪设置
6.3.1 IDEA 在哪里设置 JVM 参数
1、单个项目的应用


2、全局的配置
- 找到 IDEA 安装目录中的 bin 目录
- 找到 idea.exe.vmoptions 文件
- 打开该文件编辑并保存。

6.3.2 Eclipse 在哪里设置 JVM 参数
1、配置单个项目
点击绿色图标右边的小箭头

在点击:Run Configurations ->VM arguments

2、配置全局 JVM 参数
修改 Eclipse 的配置文件,在 eclipse 安装目录下的:eclipse.ini 文件

6.3.3 war(Tomcat) 包在哪里设置 JVM 参数
war 肯定是部署在 Tomcat 上的,那就是修改 Tomcat 的 JVM 参数
1、在 Windows 下就是在文件 / bin/catalina.bat, 增加如下设置:JAVA_OPTS(JAVA_OPTS,就是用来设置 JVM 相关运行参数的变量)

2、Linux 要在 tomcat 的 bin 下的 catalina.sh 文件里添加

6.3.4 Jar 包在哪里设置 JVM 参数
Jar 包简单,一般都是 SpringBoot 项目打成 Jar 包来运行
6.4 调优总结
- 在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等, 这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。
- 初始堆值和最大堆内存内存越大,吞吐量就越高, 但是也要根据自己电脑 (服务器) 的实际内存来比较。
- 最好使用并行收集器, 因为并行收集器速度比串行吞吐量高,速度快。 当然,服务器一定要是多线程的
- 设置堆内存新生代的比例和老年代的比例最好为 1:2 或者 1:3。 默认的就是 1:2
- 减少 GC 对老年代的回收。设置生代带垃圾对象最大年龄,进量不要有大量连续内存空间的 java 对象,因为会直接到老年代,内存不够就会执行 GC
注释:其实最主要的还是服务器要好,你硬件都跟不上,软件再好都没用 注释:老年代 GC 很慢,新生代没啥事 注释:默认的 JVM 堆大小好像是电脑实际内存的四分之一左右,
我的电脑是 8G 的运行内存

7 类加载器
7.1 类加载的机制及过程
程序主动使用某个类时,如果该类还未被加载到内存中,则 JVM 会通过加载、连接、初始化 3 个步骤来对该类进行初始化。如果没有意外,JVM 将会连续完成 3 个步骤,所以有时也把这个 3 个步骤统称为类加载或类初始化。
Jvm 执行 class 文件

1、加载
- 加载指的是将类的 class 文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。
- Java 类加载器由 JVM 提供,是所有程序运行的基础,JVM 提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承 ClassLoader 基类来创建自己的类加载器。
- 类加载器,可以从不同来源加载类的二进制数据,比如:本地 Class 文件、Jar 包 Class 文件、网络 Class 文件等等等。
- 类加载的最终产物就是位于堆中的 Class 对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即 Java 反射的接口
2、连接过程
- 当类被加载之后,系统为之生成一个对应的 Class 对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到 JRE 中(意思就是将 java 类的二进制代码合并到 JVM 的运行状态之中)。类连接又可分为如下 3 个阶段。
- 验证:确保加载的类信息符合 JVM 规范,没有安全方面的问题。主要验证是否符合 Class 文件格式规范,并且是否能被当前的虚拟机加载处理。
- 准备:正式为类变量(static 变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配
- 解析:虚拟机常量池的符号引用替换为字节引用过程
3、初始化
- 初始化阶段是执行类构造器
<clinit>() 方法的过程。类构造器<clinit>() 方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 虚拟机会保证一个类的
<clinit>() 方法在多线程环境中被正确加锁和同步
总结就是:初始化是为类的静态变量赋予正确的初始值
7.2 类加载器的介绍
- 启动(Bootstrap)类加载器
- 扩展(Extension)类加载器
- 系统类加载器
- 自定义加载器

1 根类加载器(bootstrap class loader)
它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载 $JAVA_HOME 中 jre/lib/rt.jar 里所有的 class,由 C++ 实现,不是 ClassLoader 子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
2 扩展类加载器(extensions class loader)
扩展类加载器是指 Sun 公司 (已被 Oracle 收购) 实现的 sun.misc.Launcher$ExtClassLoader 类,由 Java 语言实现的,是 Launcher 的静态内部类,它负责加载 < JAVA_HOME>/lib/ext 目录下或者由系统变量 - Djava.ext.dir 指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
3 系统类加载器(system class loader)
被称为系统(也称为应用)类加载器,它负责在 JVM 启动时加载来自 Java 命令的 - classpath 选项、java.class.path 系统属性,或者 CLASSPATH 换将变量所指定的 JAR 包和类路径。程序可以通过 ClassLoader 的静态方法 getSystemClassLoader() 来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由 Java 语言实现,父类加载器为 ExtClassLoader。(Java 虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,)
类加载器加载 Class 大致要经过如下 8 个步骤:
- 检测此 Class 是否载入过,即在缓冲区中是否有此 Class,如果有直接进入第 8 步,否则进入第 2 步。
- 如果没有父类加载器,则要么 Parent 是根类加载器,要么本身就是根类加载器,则跳到第 4 步,如果父类加载器存在,则进入第 3 步。
- 请求使用父类加载器去载入目标类,如果载入成功则跳至第 8 步,否则接着执行第 5 步。
- 请求使用根类加载器去载入目标类,如果载入成功则跳至第 8 步,否则跳至第 7 步。
- 当前类加载器尝试寻找 Class 文件,如果找到则执行第 6 步,如果找不到则执行第 7 步。
- 从文件中载入 Class,成功后跳至第 8 步。
- 抛出 ClassNotFountException 异常。
- 返回对应的 java.lang.Class 对象
7.3 理解双亲委派模式
双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

双亲委派机制的优势:采用双亲委派模式的是好处是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次。其次是考虑到安全因素,java 核心 api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.Integer,而直接返回已加载过的 Integer.class,这样便可以防止核心 API 库被随意篡改。
7.4 类加载器间的关系
我们进一步了解类加载器间的关系 (并非指继承关系),主要可以分为以下 4 点 启动类加载器,由 C++ 实现,没有父类。 拓展类加载器 (ExtClassLoader),由 Java 语言实现,父类加载器为 null 系统类加载器 (AppClassLoader),由 Java 语言实现,父类加载器为 ExtClassLoader 自定义类加载器,父类加载器肯定为 AppClassLoader。
8 JVM 可视化工具
8.1 为什么要可视化工具
开发大型 Java 应用程序的过程中难免遇到内存泄露、性能瓶颈等问题,比如文件、网络、数据库的连接未释放,未优化的算法等。随着应用程序的持续运行,可能会造成整个系统运行效率下降,严重的则会造成系统崩溃。为了找出程序中隐藏的这些问题,在项目开发后期往往会使用性能分析工具来对应用程序的性能进行分析和优化。
8.2 visualVm
VisualVM 是一款免费的,集成了多个 JDK 命令行工具的可视化工具,它能为您提供强大的分析能力,对 Java 应用程序做性能分析和调优。这些功能包括生成和分析海量数据、跟踪内存泄漏、监控垃圾回收器、执行内存和 CPU 分析,同时,它能自动选择更快更轻量级的技术尽量减少性能分析对应用程序造成的影响,提高性能分析的精度。
他作为 Oracle JDK 的一部分,位于 JDK 根目录的 bin 文件夹下。VisualVM 自身要在 JDK6 以上的版本上运行,但是它能够监控 JDK1.4 以上版本的应用程序
8.2.1 打开 visualVm
位于 JDK 根目录的 bin 文件夹下的 jvisualvm.exe
注
:我的 JDK11 没有,不知道为什么,我 jdk8 就找的到此工具


8.2.2 本地测试项目 JVM 运行状态
我这本地有了好几个进程,这是我 IDea 工具的

我运行一个 SpringBoot 项目

此时就开始监控了

8.2.3 测试服务器项目 JVM 运行状态
省略:::
8.3 jconsole
从 Java 5 开始 引入了 JConsole。JConsole 是一个内置 Java 性能分析器,可以从命令行或在 GUI shell 中运行。您可以轻松地使用 JConsole(或者,它更高端的 “近亲” VisualVM )来监控 Java 应用程序性能和跟踪 Java 中的代码。
8.3.1 启动 JConsole
点击 jdk/bin 目录下面的 jconsole.exe 即可启动, 然后会自动自动搜索本机运行的所有虚拟机进程。选择其中一个进程可开始进行监控


8.3.2 远程连接项目也很简单,和 visualVm 基本一致,可以自己研究一下
本文由简悦 SimpRead 转码
- 作者:kk
- 链接:https://blog.kinnnnnnnnnng.cn//article/eae75eb5-1b8a-4759-9294-9a1940846aef
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。