JVM总结

1.JVM内存区域

1.1 运行时数据区域

在这里插入图片描述

程序计数器 :记录的是正在执行的虚拟机字节码指令的地址,线程私有,没有内存溢出。

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

本地方法栈:java本地方法存放区域,线程私有。

方法区:静态变量和常量存放区域,线程共享,垃圾回收也可能会在这里进行。

堆:存放对象实例和数组,也是垃圾回收的区域,线程共享。

JDK1.8

在这里插入图片描述

1.元空间

永久代是有大小限制的,因此如果加载的类太多,很有可能导致永久代内存溢出,即万恶的 java.lang.OutOfMemoryError: PermGen,为此我们不得不对虚拟机做调优。

1.移除了永久代(PermGen),替换为元空间(Metaspace);
2.永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
3.永久代中的 interned Strings(字符串常量) 和 class static variables(静态变量) 转移到了 Java heap;
4.永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)

2.直接内存

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

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。

显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。如果内存区域总和大于物理内存的限制,也会出现 OOM。

Q:int a = 1; 其中的对象存放在哪个区域?
A:

  • a作为类的成员变量,存放于方法区中;1保存在堆(Heap)的实例中
  • a作为方法局部变量,存放于Java虚拟机栈(JVM Stacks)的局部变量表中;1也保存在栈内存中。

int a = 1,到底存在于JVM的哪里

1.2 对象的创建与内存布局

1.2.1 对象的创建

1.2.2 对象的内存布局

  1. 对象头(Header):
    包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 “Mark Word”。
    第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。

  2. 实例数据(Instance Data):
    程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。

  3. 对齐填充(Padding):
    不是必然需要,主要是占位,保证对象大小是某个字节的整数倍

32位是8字节,64位是16字节,对象头+实例数据+对齐填充是8字节的整数倍。
java对象在内存的大小

2.垃圾回收

2.1触发垃圾回收的情况

  1. 新生代转入老年代空间不足时
  2. 方法区空间不足
  3. 调用System.gc()

在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。
永久代垃圾回收主要两部分内容:废弃的常量和无用的类。

判断废弃常量:一般是判断没有该常量的引用。
判断无用的类:要以下三个条件都满足

  • 该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例
  • 加载该类的 ClassLoader 已经被回收
  • 该类对应的 java.lang.Class 对象没有任何地方呗引用,无法在任何地方通过反射访问该类的方法

2.2判断对象是否可回收

2.2.1 可达性分析法

通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。
当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用

可作为 GC Roots 的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象

2.2.2 引用
强引用

类似于 Object obj = new Object(); 创建的,只要强引用在就不回收。

软引用

SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。

弱引用

WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。

虚引用

PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

从强到弱:不回收 -> 内存溢出前回收 -> 垃圾回收 -> 垃圾回收时通知

2.3 垃圾回收算法

2.3.1标记-清除
特点:
效率不高
空间会产生大量碎片

2.3.2 复制

把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。

特点:解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。

2.3.3 标记-整理

不同于针对新生代的复制算法,针对老年代的特点,创建该算法。主要是把存活对象移到内存的一端。

2.3.4 分代回收

根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。

新生代

每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。

老年代

老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记 —— 清除 或者 标记 —— 整理 算法回收。

2.4.垃圾收集器

在这里插入图片描述
2.4.1 垃圾回收器特点
新生代/老年代
Serial/Serial Old:单线程
ParNew/Serial Old:Serial的多线程版本
Parallel Scavenge/Parallel Old:多线程并行

Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同,CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput)

2.4.2 CMS和G1比较
CMS

CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除 算法实现。

运作步骤:

  1. 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象(Stop The World)
  2. 并发标记(CMS concurrent mark):进行 GC Roots Tracing
  3. 重新标记(CMS remark):修正并发标记期间的变动部分(Stop The World)
  4. 并发清除(CMS concurrent sweep)

CMS以流水线方式拆分了收集周期,将耗时长的操作单元保持与应用线程并发执行。只将那些必需STW才能执行的操作单元单独拎出来,控制这些单元在恰当的时机运行,并能保证仅需短暂的时间就可以完成。这样,在整个收集周期内,只有两次短暂的暂停(初始标记和重新标记),达到了近似并发的目的。

优点:并发收集、低停顿
缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记 —— 清除 算法带来的空间碎片

安全点(Safepoint)
安全点,即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。

程序运行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。

对于安全点,另一个需要考虑的问题就是如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。
两种解决方案:

  • 抢先式中断(Preemptive Suspension)
    抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。
  • 主动式中断(Voluntary Suspension)
    主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

G1

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-the-world停顿的时间,部分其他收集器原来需要停顿Java线程执行的GC操作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
  • 分代收集
  • 空间整合:与CMS的标记-清除算法不同,G1从整体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的一个优势,降低停顿时间是G1和CMS共同的关注点。

运作步骤

  1. 初始标记(Initial Marking)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Final Marking)
  4. 筛选回收(Live Data Counting and Evacuation)

对于打算从CMS或者ParallelOld收集器迁移过来的应用,按照官方 的建议,如果发现符合如下特征,可以考虑更换成G1收集器以追求更佳性能:

  • 实时数据占用了超过半数的堆空间;
  • 对象分配率或“晋升”的速度变化明显;
  • 期望消除耗时较长的GC或停顿(超过0.5——1秒)。

区别:

CMSG1
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
CMS收集器以最小的停顿时间为目标的收集器G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片
初始标记、并发标记、重新标记、并发清除初始标记、并发标记、最终标记、筛选回收

2.4.3 ZGC

3.类加载机制

3.1 类加载过程
在这里插入图片描述
加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对 象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。

  • 符号引用:符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
  • 直接引用:直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。

3.2 双亲委派模型
在这里插入图片描述

  1. 启动类加载器
    加载 lib 下或被 -Xbootclasspath 路径下的类
  2. 扩展类加载器
    加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类
  3. 应用程序类加载器
    ClassLoader负责,加载用户路径上所指定的类库。

Q:如何打破双亲委派机制
最常见的例子:我们常用数据库驱动Driver接口,Driver定义在jdk当中,当其实现却是各个数据库服务商

  1. 通过自定义类加载器,继承classloader,重写loadclass方法(不委派)
  2. 通过spi机制,使用ServiceLoader.load去加载(向下委派)

3.3 类文件结构

4.JVM调优

  1. 观察GC日志,分析需要调优的方面,如停顿时间,吞吐量,内存占用
  2. 利用JVM命令和可视化工具等等观察GC日志情况
    JVM命令:jps,jstat,jmap,可视化工具:jconsole 等
  3. 调整JVM参数,如新生代survival和Eden区的比例,新生代和老年代的比例,更换垃圾回收器,垃圾回收器参数调整等。

JVM性能调优

引申知识:
线程模型,线程安全,内存屏障等

总结:把JVM主要部分梳理了下,但还有一些地方还未深入探究,待遇需要时再来补充。

Java虚拟机(JVM)你只要看这一篇就够了!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值