JVM学习

前言

本文以 HotSpot 虚拟机进行说明。

一、内存模型

在这里插入图片描述

1、程序计数器

当前线程所执行的字节码行号指示器,用来记录当前字节码执行指令的位置。线程私有,唯一 一个不会产生OOM的区域。

2、虚拟机栈

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都
会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信
息。线程私有,会产生OOM。

3、本地方法栈

和虚拟机栈作用非常相似,区别是虚拟机栈为执行JAVA方法服务,本地方法栈为本地方法(Native)服务。线程私有,会产生OOM。

4、JAVA堆

Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”。线程共享,会产生OOM。

5、方法区

方法区和JAVA堆一样,是各个线程共享区域,他用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器后的代码缓存等数据,也叫“非堆”。
JDK7之前人们习惯把方法去叫做永久代。JDK8开始使用元空间取代了永久代。元空间和永久代本质上类似,都是对方法区的实现。最大的区别就是元空间不使用虚拟机的内存,而是直接使用本地内存。
为什么用元空间取代永久代呢?因为永久代设置内存大小很难确定,在某些场景下动态加载类过多,可能会导致OOM。而元空间占用虚拟机内存,默认情况下,元空间大小只受本地内存大小限制。
在这里插入图片描述
在这里插入图片描述

6、直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中 定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现

在JDK 1.4中新加入了NIO类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据。

7、内存泄露和内存溢出

内存泄露: 应用程序中,当对象已经不在使用了,但是JAVA垃圾回收器仍没有回收对象导致内存溢出。

造成内存泄露的常见情形:

  1. 集合类,比如HashMap,ArrayList等,这些对象经常会发生内存泄露。比如当它们被声明为静态对象时,它们的生命周期会跟应用程序的生命周期一样长,很容易造成内存不足。
  2. 释放对象的时候却没有记住去删除对应的监听器,从而增加了内存泄漏的机会。
  3. 连接不释放,比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。一般都会在try里面去的连接,在finally里面释放连接。
  4. 不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏。

**内存溢出:**内存不够用,导致OOM。

内存溢出场景:
1、系统承载高并发请求,请求量过大导致大量对象都是存活的,所以要继续放入新的对象实在是不行了,此时就会引发OOM。
2、内存泄露导致的内存溢出。
3、JAVA虚拟机分配的内存过小。

二、垃圾回收

垃圾是指运行的程序中没有任何指针指向对象,这个对象就是要被回收的垃圾。

判断是否是垃圾的算法

1、引用计数法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
2、根可达性算法:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。JAVA选择此算法作为判断是否是垃圾的算法。

在这里插入图片描述

垃圾回收算法

把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。顾名思义,在新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。

1、标记-复制算法
它可以将内存分为大小相同的两块,每次使用其中的一块,当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉,这样就使每次的内存回收都是对内存区间的一半进行回收。
现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的Eden 空间和两块较小的Survior 空间,每次使用Eden 空间和其中一块Survivor,在回收时,将Eden和Survivor中还存活着的对象一次性复制到另一块Survivor 空间上,最后清理Eden 和使用过的那一块Survivor。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1。
优点:实现简单高效,不会产生内存碎片。
缺点:需要两倍的空间;对象存活过多的话效率会降低,所以复制算法一般用于新生代垃圾回收。
2、标记-清除算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
缺点:效率不是太高,会产生内存碎片。
3、标记-整理算法
其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
优点:不会产生内存碎片
缺点:效率上比另外两种算法低

垃圾回收器

在这里插入图片描述

Serial收集器

Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代
收集器的唯一选择。这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程(Stop The World),直到它收集结束。
在这里插入图片描述Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。所以Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。

ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之
外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:
PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。
在这里插入图片描述
CMS作为老年代的收集器的时候,新生代只能选择ParNew或者Serial收集器中的一个。ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。

Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是
能够并行收集的多线程收集器。注重控制吞吐量。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实
现。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除
    在这里插入图片描述
    初始标记和重新标记会STW。
    CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来:并发收集、低停顿。缺点:
    1、CMS收集器对处理器资源非常敏感。CMS默认启动的回收线程数是(处理器核心数量
    +3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的
    处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时,
    CMS对用户程序的影响就可能变得很大。
    2、CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode
    Failure”并发失败进而导致另一次完全“Stop The World”的Full GC的产生。程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。
    3、基于标记-清除算法会产生内存碎片。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。

Garbage First收集器

JDK 9默认垃圾收集器。G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M。
G1会在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。
G1收集器的运作过程大致可划分为以下四个步骤:
1、初始标记:暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快。
2、并发标记:同CMS
3、最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
4、筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
在这里插入图片描述

ZGC

ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器。
1.支持TB量级的堆。我们生产环境的硬盘还没有上TB呢,这应该可以满足未来十年内所有JAVA应用的需求吧。
2.最大GC停顿时间不超10ms。目前一般线上环境运行良好的JAVA应用Minor GC停顿时间在10ms左右。
3.最糟糕的情况下吞吐量会降低15%。这都不是事,停顿时间足够优秀。至于吞吐量,通过扩容分分钟解决。
另外,Oracle官方提到了它最大的优点是:它的停顿时间不会随着堆的增大而增长!也就是说,几十G堆的停顿时间是10ms以下,几百G甚至上T堆的停顿时间也是10ms以下

垃圾收集器总结

在这里插入图片描述

三、类加载机制

1、过程

在这里插入图片描述
1、加载: 加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,希望读者没有混淆这两个看起来很相似的名词。在加载阶段,Java虚拟机需要完成以下三件事情:

1、通过一个类的全限定名来获取定义此类的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入
口。

2、验证:确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
3、准备:正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初
始值的阶段。
4、解析:Java虚拟机将常量池内的符号引用替换为直接引用的过程。

·符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何
形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引
用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,
但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规
范》的Class文件格式中。
·直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能
间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚
拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机
的内存中存在。

5、初始化:Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。

2、类加载器

1、启动类加载器(Bootstrap Class Loader):C++语言实现,负责加载存放在%JAVA_HOME%\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。
2、扩展类加载器(Extension Class Loader):Java语言实现,负责加载%JAVA_HOME%\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
3、应用程序类加载器(Application Class Loader):Java语言实现,负责ClassPath中的类库。
4、自定义类加载器只需继承ClassLoader类并重写findClass方法。例如定义一个TestClassLoader类继承ClassLoader,重写findClass方法,此方法要做的事情是读取Test.class字节流并传入父类的defineClass方法,然后就可以通过自定义累加载器TestClassLoader对Test.class进行加载。

除了启动类加载器,其他类加载器都由Java层实现并继承java.lang.ClassLoader。

3、双亲委派机制

在这里插入图片描述

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
优点:
1)沙箱安全机制:例如自己写的String.class类不会被加载,这样便可以防止核心API库被随意篡改。
2)避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

4、破坏双亲委派模型

打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法。默认的loadClass方法是实现了双亲委派机制的逻辑,即会先让父类加载器加载,当无法加载时才由自己加载,这里为了破坏双亲委派机制必须重写loadClass方法,即这里先尝试交由System类加载器加载,加载失败才会由自己加载。它并没有优先交给父类加载器,这就打破了双亲委派机制。

破坏双亲委派模式应用案例
JDBC: JDBC 4.0之后实际上我们不需要再调Class.forName来加载驱动程序了,我们只需要把驱动的jar包放到工程的类加载路径里,那么驱动就会被自动加载。
这个自动加载采用的技术叫做SPI,数据库驱动厂商也都做了更新。可以看一下jar包里面的META-INF/services目录,里面有一个java.sql.Driver的文件,文件里面包含了驱动的全路径名

Connection con = DriverManager.getConnection(url , username , password ) ;

Tomcat: 每个Tomcat的webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。
tomcat破坏双亲委派大致是处于下面三个目的:

1、对于各个 webapp中的 class和 lib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的lib以便不浪费资源。
2、安全性问题,使用单独的 classloader去装载 tomcat自身的类库,以免其他恶意或无意的破坏;
3、实现热部署

注:以上内容来自 《深入理解JAVA虚拟机第三版》周志明著

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值