JVM笔记

这篇博客详细介绍了JVM的内存结构,包括程序计数器、虚拟机栈、本地方法栈、堆、方法区和直接内存。讨论了各部分的作用、内存溢出情况及诊断方法。此外,深入探讨了垃圾回收机制,如如何判断对象可回收、各种垃圾回收算法、分代垃圾回收和G1垃圾回收器的工作原理。最后,提到了垃圾回收调优和类加载器与字节码技术的相关内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM笔记

一、内存结构

在这里插入图片描述

执行过程

  • 类放在方法区 Method Area
  • 类创建的实例(对象)放在堆Heap
  • 堆里的对象调用方法时,会用到虚拟机栈 JVM Stacks程序计数器 PC Register本地方法栈 Native Method Stacks
  • 每行代码由解释器Interpreter逐行执行
  • 热点代码由即时编译器 JIT Compiler执行
  • GC模块 垃圾回收模块会对堆中不再使用的实例进行回收
  • java代码不方便实现的功能,必须调用底层操作系统的功能,就需要调用本地方法接口

1、程序计数器

  • 作用:记住下一条jvm指令的执行地址
  • 特点
    • 线程私有的 (每个线程都有自己的程序计数器,随着线程创建而创建,随着线程销毁而销毁)
    • 不会存在内存溢出
    • 是一块较小的内存空间

2、虚拟机栈

2.1 定义

栈-线程运行需要的内存空间,每个线程都需要一个栈,多个线程运行就会有多个虚拟机栈

  • 一个栈由多个栈帧组成
  • 一个栈帧对应着一次方法的调用

栈帧-每个方法运行时需要的内存

在这里插入图片描述
如果方法一调用方法二,方法二会生成一个栈帧,压入栈中,方法运行完毕,弹出栈。

问题辨析

  • 垃圾回收是否涉及栈内存?
    垃圾回收涉及堆,并不涉及栈内存
  • 栈内存分配越大越好吗?
    栈内存分配越大,线程数会越少,所以不是栈内存分配越大,程序运行越快 ,因为总共的物理内存是一定的。
  • 方法内的局部变量是否线程安全?
    线程安全,当线程1运行一个方法,定义一个局部变量,进行操作,同时,线程2也调用运行同一个方法,会在线程2 中再定义一个局部变量,互不影响,所以方法内的局部变量是线程安全的。
    • 局部变量是线程私有的。
    • 全局变量是线程共有的,是需要考虑线程安全问题(不是线程安全的)。
    • 当变量是参数,或者是返回值的时候==(逃离了线程的作用范围)==也是需要考虑线程安全问题(不是线程安全的)。

2.2 栈内存溢出 StackOverflowError

  • 栈帧过多导致栈内存溢出。(例如不正确的递归调用,递归时没有正确结束)
  • 栈帧占用内存过大。(不太容易出现)

2.3 线程运行诊断

  • 案例1:CPU占用过多(while死循环)
  • 案例2:程序运行很长时间没有结束(发生死锁)

3、本地方法栈 Native Method Stacks

本地方法(Native)使用的内存空间-本地方法栈
有一些java代码不方便实现的功能,必须调用底层操作系统的功能,就需要调用本地方法接口的本地方法

4、堆

4.1定义

  • 通过new关键词,创建的对象都会使用堆内存
    特点
  • 它是线程共享的,堆中的对象都需要考虑线程安全的问题
  • 有垃圾回收机制

4.2 堆内存溢出 OutOfMemoryError

在这里插入图片描述
创建的对象不能被回收,并且仍在扩大对象所占用的内存。

4.3堆内存诊断

1.jps工具

  • 查看当前系统有哪些java进程

2.jmap工具

  • 查看堆内存占用情况 jmap -heap $id

3.jconsole工具

  • 图形界面的,多功能的检测工具,可以连续监测

5、方法区

5.1定义

方法区是所有jvm虚拟机共享的区;它存储了跟类的结构相关的信息,有成员变量,方法数据和成员方法以及构造方法的代码部分,包括特殊的方法(类的构造器)
方法区在虚拟机启动时被创建,逻辑上是堆的组成部分

5.2 组成

在这里插入图片描述
在1.6被称为永久代
在1.8之后也是一种概念,永久代被替代,由元空间实现,包括类,类加载器和常量池,不再占用堆内存,不是由jvm管理内存结构,由本地内存管理。

5.3方法区内存溢出

  • 1.8以前会导致永久代内存溢出

在这里插入图片描述
在这里插入图片描述

  • 1.8之后会导致元空间内存溢出

在这里插入图片描述
在这里插入图片描述

场景

  • spring
    cglib 动态代理
  • mybatis

5.4运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型、字面量等信息。
  • 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。

5.5 StringTable特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder(1.8)
  • 字符串常量拼接的原理是编译期优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
    • 1.8将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,然后都会将串池中的对象返回
      在这里插入图片描述
      在这里串池中有ab 所以定义s2时给s2返回的是串池中的对象,所以是true,而串池中原本就已经有ab,所以尝试将s放入串池时会失败,也就是s并没有被放入串池中所以为false
    • 1.6将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把对象复制一份,放入串池,然后都会将串池中的对象返回

5.6 StringTable的位置

永久代只有FullGC的时候才会垃圾回收

5.7 StringTable垃圾回收

当内存分配失败的时候会进行垃圾回收

5.8 StringTable性能调优

如果应用里有大量的字符串,并且可能存在字符串重复的情况,我们可以选择将字符串入池,来减少对应字符串个数,减少内存的使用
intern入池

6、直接内存

6.1定义

  • 常见于NIO操作时,用于数据缓冲区(ByteBuffer)
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理
正常io

在这里插入图片描述
正常io进行读写的时候,通过调用本地System方法,来进行读写,进行读写时,首先将磁盘中的信息先拷贝到系统缓冲区,再从系统缓冲区拷贝到java缓冲区,由此可见拷贝了两次,浪费了内存,又浪费了时间

ByteBuffer(直接内存)

在这里插入图片描述
由图可见,直接内存在系统缓冲区和Java堆内存之上相当于共享的区域,Java代码可以直接访问,系统内存也可以直接访问,这就节省了很多的大量的时间

6.2分配和回收原理

  • 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用free Memory方法
  • ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory方法来释放直接内存

显示的垃圾回收,就是由程序员在程序中运行的System.gc(),这是一个Full GC,比较影响性能,不仅回收老年代,也回收新生代,会造成程序暂停时间比较长
通过 -XX:DisableExplicitGC参数来使gc方法无效

二、垃圾回收(*)

1、如何判断对象可以回收

1.1引用计数法

当方法被引用时计数+1,当为0的时候被回收
弊端
在这里插入图片描述
当其他方法没用引用时理论上需要被回收但是A和B在循环引用,引用计数无法归0,导致无法被回收,造成内存泄露

1.2可达性分析算法

根对象:就是肯定不能被当作垃圾被回收的对象
先对所有方法和对象进行扫描,判断是否被根对象直接或间接引用,如果被引用就不能被垃圾回收,如果没有被引用,就可以被看作是垃圾,将来可以被垃圾回收。

1.3四种引用(*)

在这里插入图片描述

1.3.1强引用

例如new了一个对象,将这个对象通过等号=来辅助给一个变量,那么这个变量就是强引用了这对象。

  • 当所有强引用都断开的时候,垃圾回收发生时就会被垃圾回收。
1.3.2软引用
  • 当垃圾回收时,被软引用引用时并且内存不足时,会被回收掉
  • 可以配合引用队列来释放软引用自身
    在这里插入图片描述
1.3.3弱引用
  • 当垃圾回收时,被弱引用引用时无论内存是否充足,都会被回收掉
  • 可以配合引用队列来释放弱引用自身
    在这里插入图片描述
1.3.4虚引用
  • 必须配合引用队列,虚引用对象被垃圾回收时,会将ByteBuffer对象直接回收,然后将虚引用对象存放直接内存地址,并放入引用队列,来对直接内存进行垃圾回收
    在这里插入图片描述
1.3.5终结器引用

在这里插入图片描述

  • 垃圾回收时,先将终结器引用放入引用队列,然后会有一个优先级很低的线程,去检测引用队列,检测是否有终结器引用,如果有,先调用对象的finallize()方法,然后再对其进行垃圾回收

2、垃圾回收算法

2.1标记清除

在这里插入图片描述

优点:速度快
缺点:容易产生内存碎片

2.2标记整理

在这里插入图片描述
优点:没有内存碎片
缺点:效率低,速度慢

2.3复制

在这里插入图片描述

在这里插入图片描述
优点:速度快,没有内存碎片
缺点:占用内存较大

3、分代垃圾回收

在这里插入图片描述

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发Minor GC,伊甸园和from存活的对象使用copy算法复制到To区中,存活的对象年龄加1并且交换From和To
  • Minor GC会引发stop the world (所有的其他用户线程都暂停,等待Minor GC完成,用户线程恢复运行)
  • 当某个年龄到达预定值(最大寿命是15(4bit ---- 1111)),会晋升至老年代中
  • 当新生代和老年代内存都不足时,会触发Full GC,将新生代和老年代的内存都进行一次垃圾回收STW的时间更长。
    在这里插入图片描述
  • 大对象:当对象大小大于伊甸园大小,并且小于老年代大小,此时会将该对象直接晋升到老年代中。

4、垃圾回收器

在这里插入图片描述

4.1串行

在这里插入图片描述

4.2吞吐量优先

在这里插入图片描述
1.8默认开启

4.3响应时间优先

在这里插入图片描述
有可能会造成碎片过多,导致并发失败,从而退化成串行,导致响应时间突然变长。

4.4 G1

定义:Garbage First

  • 2004论文发布
  • 2009JDK 6u14体验
  • 2012 JDK 7u4官方支持
  • 2017 JDK 9 默认(取代了CMS)
    使用场景:
  • 同时注重吞吐量和低延迟,默认的暂停目标是200ms
  • 超大堆内存,会将堆划分为多个大小相等的Region
  • 整体上是标记+整理的算法,两个区域之间是复制算法
    在这里插入图片描述
4.4.1 G1垃圾回收阶段

在这里插入图片描述

4.4.2 Young Collection
  • 会STW
    在这里插入图片描述
4.4.3 Young Collection +CM
  • 再Young GC时会进行GC Root的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
    在这里插入图片描述
4.4.4 Mixed Collection

会对E、S、O 进行全面垃圾回收

  • 最终标记会STW
  • 拷贝存活会STW
    在这里插入图片描述
    会回收垃圾较多的老年代区,并不是所有都拷贝。
4.4.5 Full GC

在这里插入图片描述

4.4.6 新生代跨代引用
  • 新生代回收的跨代引用(老年代引用新生代)问题
    在这里插入图片描述
4.4.7 Remark

在这里插入图片描述
在理论上,当B强引用C断开后,C被标记为白色,在结束后会被回收掉,然而时并发处理,此时如果有用户,将A强引用C,而C还被标记为白色,结束后仍会被回收,就出现了问题
在这里插入图片描述
为了解决这个问题,就出现了一个 写屏障,当强引用断开时,也就是状态改变时,会触发写屏障,将C放入处理队列 (stab_mark_queue) 中,置为灰色,结束后会逐一对队列中的对象进行判断,如果没有被引用就置为白色,被回收掉,如果仍有被引用就置为黑色。
CMS中是将A放入队列

4.4.8 JDK 8u20字符串去重
  • 优点:节省大量内存
  • 缺点:略微多占用了CPU时间,新生代回收时间略微增加
    在这里插入图片描述
  • 当所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果它们值一样,让他们引用同一个char[]
  • 注意,与String.intern()不一样
    • String.intern()关注的是字符串对象
    • 而字符串去重关注的是char[]
    • 在JVM内部,使用了不同的字符串表
4.4.9 JDK 8u40 并发标记类卸载

在这里插入图片描述

4.4.10 JDK 8u60 回收巨型对象

在这里插入图片描述
在这里插入图片描述

4.4.11 JDK 9 并发标记起始时间的调整

在这里插入图片描述

5、垃圾回收调优(*)

5.1 调优领域

  • 内存
  • 锁竞争
  • CPU占用
  • io

5.2 确定目标

  • 【低延迟】还是稿吞吐量,选择合适的回收器
  • CMS,G1,ZGC
  • ParallelGC
  • Zing

5.3最快的GC是不发生GC

  • 查看FullGC的内存占用,考虑下面几个问题
    • 数据是不是太多?
      • resultSet=statement.executeQuery(“select * from 大表”)
    • 数据表示是否太臃肿?
      • 对象图
      • 对象大小
    • 是否存在内存泄漏

5.4 新生代调优

  • 新生代特点
    • 所有的new操作的内存分配非常廉价
      • TLAB thread-local allocation buffer
    • 死亡对象的回收代价是0
    • 大部分对象用过即死
    • Minor GC的时间远远低于Full GC

最简单的新生代调优就是将新生代大小变大

  • 但是真的是越大越好吗?
    当新生代大小太大时,就意味着老年代的大小很小,新生代大小很充裕,老年代大小会很紧张,当老年代大小紧张时,会触发Full GC这要比Minor GC的时间更长
    Oracle建议是新生代大小大于堆大小的25%小于50%
    在这里插入图片描述

新生代能容纳所有【并发量*(请求-相应)】的数据

幸存区大到能保留【当前活跃对象+需要晋升的对象】

晋升阈值配置得当,让长时间存活的对象尽快晋升
在这里插入图片描述

5.5 老年代调优

5.6 案例

在这里插入图片描述

链接: gceasy.io
在这里插入图片描述

在这里插入图片描述

三、类加载器与字节码技术

1.类文件结构

在这里插入图片描述

1.1 魔数

在这里插入图片描述

1.2 版本

在这里插入图片描述

1.3常量池

在这里插入图片描述
在这里插入图片描述

1.4 访问标识与继承信息

1.5 Field信息

1.6 Method信息

1.7 附加属性

2.字节码指令

2.1 入门

2.2 javap工具

2.3 图解方法执行流程

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.4 i++分析

2.5 条件判断指令

在这里插入图片描述

  • byte,short,char都会按int比较,因为操作数栈都是4字节
  • goto用来进行跳转带指定行号的字节码在这里插入图片描述

2.6 循环控制指令

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值