文章目录
前言
感慨:读万卷书,行万里路,程序员的一生都在不停的学习,在一次失败面试之后,我感觉自己还有很大的问题,所以,针对自己薄弱的模块就狂补理论知识,以下是我读书时精选的笔记整理,方便别人,也方便自己,不足之处,请多多指教。
JVM
一 JVM的运行机制
JVM
(Java Virtual Machine)是用于运行Java字节码的虚拟机,包括一套字节码指令集、一组程序寄存器、一个虚拟机栈、一个虚拟机堆、一个方法区和一个垃圾回收器。JVM
运行在操作系统之上,不与硬件设备直接交互。
首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader
)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area
)的方法区内,而字节码文件只是 JVM
的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine
),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface
)来实现整个程序的功能。
二 JVM的内存区域
JVM
的内存区域分为线程私有区域(程序计数器、虚拟机栈、本地方法区)、线程共享区域(堆、方法区)和直接内存。- 线程私有区域的生命周期与线程相同,随线程的启动而创建,随线程的结束而销毁。
- 线程共享区域随虚拟机的启动而创建,随虚拟机的关闭而销毁。
- 直接内存也叫作堆外内存,它并不是
JVM
运行时数据区的一部分,但在并发编程中被频繁使用(操作系统上分配堆外内存)。 - 程序计数器:是一块很小的内存空间,用于存储当前运行的线程所执行的字节码的行号指示器。它是唯一没有Out Of Memory(内存溢出)的区域。
- 虚拟机栈:是描述Java方法的执行过程的内存模型,它在当前栈帧(Stack Frame)中存储了局部变量表、操作数栈、动态链接、方法出口等信息。栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机栈中的入栈和出栈。
- 本地方法栈:和虚拟机栈的作用类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务。
- 堆:在
JVM
运行过程中创建的对象和产生的数据都被存储在堆中,堆是被线程共享的内存区域,也是垃圾收集器进行垃圾回收的最主要的内存区域。 - 方法区:也被称为永久代,用于存储常量、静态变量、类信息、即时编译器编译后的机器码、运行时常量池等数据,常量被存储在运行时常量池(
Runtime Constant Pool
)中,是方法区的一部分。静态变量也属于方法区的一部分
三 JVM类的加载
3.1 类的加载过程
JVM
的类加载分为5个阶段:加载、验证、准备、解析、初始化。在类初始化完成后就可以使用该类的信息,在一个类不再被需要时可以从JVM
中卸载。
- 加载:指
JVM
读取Class文件,并且根据Class
文件描述创建java.lang.Class
对象的过程。 - 验证:主要用于确保Class文件符合当前虚拟机的要求,保障虚拟机自身的安全,只有通过验证的Class文件才能被
JVM
加载。 - 准备:主要工作是在方法区中为类变量分配内存空间并设置类中变量的初始值。
- 解析:
JVM
会将常量池中的符号引用替换为直接引用。 - 初始化:主要通过执行类构造器的
<client>
方法为类进行初始化。
3.2 类的加载器
JVM
提供了3种类加载器,分别是启动类加载器、扩展类加载器和应用程序类加载器、自定义启动类加载器
- **启动类加载器(
Bootstrap ClassLoader
)**负责加载Java_HOME/lib
目录中的类库,或通过-Xbootclasspath
参数指定路径中被虚拟机认可的类库。 - 扩展类加载器(
Extension ClassLoader
):负责加载\lib\ext
目录或Java. ext. dirs
系统变量指定的路径中的所有类库; - 应用程序类加载器(
Application ClassLoader
)。负责加载用户类路径(classpath
)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。 - 自定义类加载器:我们也可以通过继承
java.lang.ClassLoader
实现自定义的类加载器。
3.3 双亲委派机制
JVM
通过双亲委派机制对类进行加载。双亲委派机制指一个类在收到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上委派给其父类去完成,其父类在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中。双亲委派机制的核心是保障类的唯一性和安全性。
3.4 OSGI
OSGI
(Open Service Gateway Initiative)是Java动态化模块化系统的一系列规范,旨在为实现Java程序的模块化编程提供基础条件。基于OSGI
的程序可以实现模块级的热插拔功能,在程序升级更新时,可以只针对需要更新的程序进行停用和重新安装,极大提高了系统升级的安全性和便捷性。
四 JVM 运行时内存
4.1 内存区域划分
JVM
的运行时内存也叫作JVM
堆,从GC
的角度可以将JVM
堆分为新生代、老年代和永久代。其中新生代默认占1/3堆空间,老年代默认占2/3堆空间,永久代占非常少的堆空间。新生代又分为Eden
区、ServivorFrom
区和ServivorTo
区,Eden区默认占8/10新生代空间,ServivorFrom
区和ServivorTo
区默认分别占1/10新生代空间(8:1:1)
。
- 新生代:
JVM
新创建的对象(除了大对象外)会被存放在新生代,默认占1/3堆内存空间。由于JVM会频繁创建对象,所以新生代会频繁触发MinorGC
进行垃圾回收。新生代又分为Eden区、ServivorTo
区和ServivorFrom
区。- Eden区:
Java
新创建的对象首先会被存放在Eden
区,如果新创建的对象属于大对象,则直接将其分配到老年代。 ServivorTo
区:保留上一次MinorGC
时的幸存者。ServivorFrom
区:将上一次MinorGC时
的幸存者作为这一次MinorGC
的被扫描者。- 白话:你可以把他理解栈为战场,经过扫荡,幸存者,来到了
ServivorFrom
- Eden区:
- 老年代:老年代主要存放有长生命周期的对象和大对象。老年代的
GC
过程叫作MajorGC
。在老年代,对象比较稳定,MajorGC
不会被频繁触发。 - 永久代:永久代指内存的永久保存区域,主要存放
Class
和Meta
(元数据)的信息。Class在类加载时被放入永久代。 - 注意:
- 在Java 8中永久代已经被元数据区(也叫作元空间)取代。元数据区的作用和永久代类似,二者最大的区别在于:元数据区并没有使用虚拟机的内存,而是直接使用操作系统的本地内存。
- 在Java 8中,
JVM
将类的元数据放入本地内存(Native Memory)中,将常量池和类的静态变量放入Java堆中,这样JVM
能够加载多少元数据信息就不再由JVM
的最大可用内存(MaxPermSize
)空间决定,而由操作系统的实际可用内存空间决定。
4.2 垃圾回收
Java采用引用计数法和可达性分析来确定对象是否应该被回收,其中,引用计数法容易产生循环引用的问题,可达性分析通过根搜索算法(GC Roots Tracing)来实现。
- 引用计数法:在为对象添加一个引用时,引用计数加1;在为对象删除一个引用时,引进计数减1;如果一个对象的引用计数为0,则表示此刻该对象没有被引用,可以被回收。循环引用指两个对象相互引用,导致它们的引用一直存在,而不能被回收。
- 可达性分析:
GC Roots
的点作为起点向下搜索,在一个对象到任何GCRoots
都没有引用链相连时,说明其已经死亡。
4.4 垃圾回收算法
Java中常用的垃圾回收算法有标记清除(Mark-Sweep)
、复制(Copying)
、标记整理(Mark-Compact)
和分代收集(Generational Collecting)
这4种垃圾回收算法,
-
复制算法:(新生代中存活的对象比较少)
- 经过一次扫荡之后,把在
Eden
区和ServivorFrom
区中存活的对象复制到ServivorTo
区, - 清空
Eden
区和ServivorFrom
区中的对象。 - 将
ServivorTo
区和ServivorFrom
区互换,原来的ServivorTo
区成为下一次GC
时的ServivorFrom
区。
- 经过一次扫荡之后,把在
-
标记清除:
MajorGC
采用标记清除算法,该算法首先会扫描所有对象并标记存活的对象,然后回收未被标记的对象,并释放内存空间,因为要先扫描老年代的所有对象再回收,所以MajorGC
的耗时较长。MajorGC
的标记清除算法容易产生内存碎片。 -
标记整理:标记整理算法结合了标记清除算法和复制算法的优点,其标记阶段和标记清除算法的标记阶段相同,在标记完成后将存活的对象移到内存的另一端,然后清除该端的对象并释放内存。、
-
分代回收:分代收集算法根据对象的不同类型将内存划分为不同的区域,
JVM
将堆划分为新生代和老年代。新生代主要存放新生成的对象,其特点是对象数量多但是生命周期短,在每次进行垃圾回收时都有大量的对象被回收;老年代主要存放大对象和生命周期长的对象,因此可回收的对象相对较少。因此,JVM
根据不同的区域对象的特点选择了不同的算法。- 新生代:复制算法新生代主要存储短生命周期的对象,因此在垃圾回收的标记阶段会标记大量已死亡的对象及少量存活的对象,因此只需选用复制算法将少量存活的对象复制到内存的另一端并清理原区域的内存即可。
- 老年代:**标记整理算法 **老年代主要存放长生命周期的对象和大对象,可回收的对象一般较少,因此
JVM
采用标记整理算法进行垃圾回收,直接释放死亡状态的对象所占用的内存空间即可。
五 垃圾回收器
5.1 概述
JVM
针对新生代和老年代分别提供了多种不同的垃圾收集器,针对新生代提供的垃圾收集器有Serial、ParNew、Parallel Scavenge,
针对老年代提供的垃圾收集器有Serial Old、Parallel Old、CMS
,还有针对不同区域的G1
分区收集算法。
5.2 新生代垃圾回收器
- Serial:
Serial
垃圾收集器基于复制算法实现,它是一个单线程收集器,在它正在进行垃圾收集时,必须暂停其他所有工作线程,直到垃圾收集结束。 - ParNew:
ParNew
垃圾收集器是Serial垃圾收集器的多线程实现,同样采用了复制算法,它采用多线程模式工作,除此之外和Serial
收集器几乎一样。ParNew
垃圾收集器默认开启与CPU同等数量的线程进行垃圾回收,在Java应用启动时可通过-XX:ParallelGCThreads
参数调节ParNew
垃圾收集器的工作线程数。 - Parallel Scavenge:
Parallel Scavenge
收集器是为提高新生代垃圾收集效率而设计的垃圾收集器,基于多线程复制算法实现,在系统吞吐量上有很大的优化,可以更高效地利用CPU
尽快完成垃圾回收任务。
5.3 老年代垃圾回收器
- Serial Old:
Serial Old
针对老年代长生命周期的特点基于标记整理算法实现。Serial Old
垃圾收集器是JVM
运行在Client
模式下的老年代的默认垃圾收集器。新生代的Serial
垃圾收集器和老年代的Serial Old
垃圾收集器可搭配使用,分别针对JVM
的新生代和老年代进行垃圾回收。 - Parallel Old:
Parallel Old
垃圾收集器采用多线程并发进行垃圾回收,它根据老年代长生命周期的特点,基于多线程的标记整理算法实现。Parallel Old
垃圾收集器在设计上优先考虑系统吞吐量,其次考虑停顿时间等因素,如果系统对吞吐量的要求较高,则可以优先考虑新生代的Parallel Scavenge
垃圾收集器和老年代的Parallel Old
垃圾收集器的配合使用。 - CMS(Concurrent Mark Sweep): 垃圾收集器是为老年代设计的垃圾收集器,其主要目的是达到最短的垃圾回收停顿时间,基于线程的标记清除算法实现,以便在多线程并发环境下以最短的垃圾收集停顿时间提高系统的稳定性。
CMS
垃圾收集器在和用户线程一起工作时(并发标记和并发清除)不需要暂停用户线程,有效缩短了垃圾回收时系统的停顿时间,同时由于CMS
垃圾收集器和用户线程一起工作,因此其并行度和效率也有很大提升。
5.4 G1垃圾回收器
G1
(Garbage First)垃圾收集器为了避免全区域垃圾收集引起的系统停顿,将堆内存划分为大小固定的几个独立区域,独立使用这些区域的内存资源并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,在垃圾回收过程中根据系统允许的最长垃圾收集时间,优先回收垃圾最多的区域。G1
垃圾收集器通过内存区域独立划分使用和根据不同优先级回收各区域垃圾的机制,确保了G1
垃圾收集器在有限时间内获得最高的垃圾收集效率。
总结
感谢大家的阅读,欢迎一键三连。