JVM学习

什么是JVM

  1. 定义:

    Java Virtual Machine --java程序的运行环境(java二进制字节码的运行环境)

  2. 好处:

    • 一次编写,到处运行 (可移植性、平台无关性)
    • 自动内存管理,垃圾回收功能
    • 数组下标越界检查
    • 多态
  3. 比较

jvm jre jdk

jre = jvm+基础类库

jdk = jvm + 基础类库 + 编译工具

开发javase = jdk + ide工具

开发javaee = jdk + 应用服务器 + ide工具

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PQ39xiG7-1655614298432)(/Users/renkaixuan/Desktop/typora/JVM/IMG_56B65EE215C8-1.jpeg)]

学习JVM有什么用

  • 面试
  • 理解底层实现原理
  • 中高级程序员必备技能

内存结构

1.程序计数器

1.1 定义

Program Counter Register 程序计数器(寄存器)

1.2作用
  • 记住下一条jvm指令的执行地址
  • 特点
    • 线程私有
    • 不会存在内存溢出

2.虚拟机栈

Java Virtual Machine Stacks (java虚拟机栈)

2.1定义
  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
  1. 问题辨析

    1. 垃圾回收是否涉及栈内存

      不会,随着方法运行结束栈内存会自动释放不需要垃圾回收

    2. 栈内存分配越大越好吗

      栈内存大了线程数量会变少

    3. 方法内的局部变量是否线程安全

      • 如果方法内局部变量没有逃离方法作用范围,它是线程安全的。

      • 如果时局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全

2.2栈内存溢出
  • 栈帧过多导致内存溢出(例如递归调用)
  • 栈帧过大导致内存溢出

3.本地方法栈

4.堆

4.1定义:Heap 堆
  • 使用new关键字创建对象会使用堆内存

  • 特点

    • 它是线程共享的,堆中对象都需要考虑线程安全

    • 有垃圾回收机制

4.2堆内存溢出
4.3堆内存诊断
  • jsp工具

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

  • jmap工具

    查看堆内存占用情况 (jmap -heap 进程id)

  • jconsole

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

​ 案例:

  • 垃圾回收后,内存占用仍然很高

​ jvisualvm

5.方法区

5.1定义

​ java虚拟机线程的共享区域

​ 存储类的结构信息:成员变量,方法数据,成员方法,构造器方法,运行时常量池等

​ 方法区在虚拟机启动时创建

​ 方法区在逻辑上是堆的一部分

5.2组成

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dKbvWtrM-1655614298433)(/Users/renkaixuan/Desktop/typora/JVM/IMG_3AEDB64BDE46-1.jpeg)]

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

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

5.4运行时常量池
  • 常量池:一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
  • 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
5.5StringTable
  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder(1.8)
  • 字符串常量拼接的原理是编译优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放到串池
    • 1.8将这个字符尝试放入串池,如果有则不会放入,如果没有则会放入,会把串池中的对象返回
    • 1.6将这个字符尝试放入串池,如果有则不会放入,如果没有则会复制一份对象放入,会把串池中的对象返回
5.6StringTable位置
5.7StringTable垃圾回收
5.8StringTable性能调优

StringTable底层是哈希表

  • 调整 -XX:StringTableSize=桶个数
  • 考虑将字符串对象是否入池

6直接内存

6.1定义

Direct Memory

  • 常见于NIO操作时,用于数据缓冲区

  • 分配回收成本较高

  • 不受JVM管理

垃圾回收

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

1.1引用计数法
1.2可达性分析算法
  • java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到表示可以回收
  • 哪些对象可以作为GC Root?
1.3四种引用
  • 强引用:例如赋值。存在强引用就不会被垃圾回收
  • 软引用:
    • 垃圾回收后内存仍不够会被回收
    • 可以配合引用队列来释放软引用自身
  • 弱引用:
    • 不管内存是否充足都会被垃圾回收
    • 可以配合引用队列释放软引用自身
  • 虚引用:
    • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存
  • 终结器引用:
    • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象

2.垃圾回收算法

2.1标记清除
  • 速度快
  • 会造成内存碎片
2.2标记整理
  • 速度慢
  • 没有内存碎片
2.3复制
  • 不会有内存碎片
  • 需要占用双倍内存空间

3.分代垃圾回收

  • 对象首先分配在伊甸园区
  • 新生代空间不足时,出发minor gc,伊甸园和from存活的对象使用copy算法复制到to中,存活的对象年龄加1并交换from 、to
  • minor gc会引发stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阀值时会晋升至老年代,最大寿命是15
  • 当老年代空间不足会先尝试出发minor gc,如果空间仍不足会触发full gc,STW的时间更长

4.垃圾回收器

4.1串行
  • 但线程
  • 适合堆内存较小、个人电脑
4.2吞吐量优先
  • 多线程
  • 堆内存较大,多核cpu
  • 让单位时间内STW的时间最短
4.3响应时间优先
  • 多线程
  • 堆内存较大,多核cpu
  • 尽可能让STW的时间最短
4.4G1

适用场景:

  • 同时注重吞吐量和低延迟,默认的暂停目标是200ms
  • 超大内存,会将堆划分为多个大小相等的Region
  • 整体上是标记+整理算法,两个区域之间是复制算法
  • 相关JVM参数
    • -XX:+UseG1GC
    • -XX:G1HeapRegionSize=size
    • -XX:MaxGCPauseMillis=time
1)G1垃圾回收阶段

​ 循环(Young Collection(新生代垃圾收集),Yong Collection+Concurrent Mark(新生代垃圾回收+并发标记),Mixed Collection(混合收集))

5.垃圾回收调优

类加载与字节码技术

1.类文件结构

2.字节码指令

javap工具
  • javap -v HelloWorld.class
    • Contant pool:常量池
方法执行流程

3.类加载阶段

3.1加载
  • 将类的字节码载入方法区
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的
3.2链接
  • 验证:验证类是否符合JVM规范,安全性检查

  • 准备:为static变量分配空间,设置默认值

    • static变量分配空间和赋值在两个阶段,分配空间在准备阶段,赋值在初始化阶段
    • 如果static变量是final的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
    • 如果static变量是final的,但属于引用类型,那么赋值也是在初始化阶段完成
  • 解析

    • 将常量池中的符号引用解析为直接引用
3.3初始化

发生时机:

  • main方法所在的类总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类没有初始化会引发
  • 子类访问父类的静态变量,只会引发父类的初始化
  • Class.forName
  • new会导致初始化

不会导致初始化的情况:

  • 访问类的static final静态常量(基本类型和字符串)不会触发初始化
  • 类对象.class不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载器的loadClass方法
  • class.forName的参数2为false时

JMM(java内存模型)

JMM定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性和原子性的规则和保障

1.java内存模型

1.1解决办法synchronized(同步关键字)

语法

synchronized(对象){
	要作为原子操作代买
}

2.可见性

3.有序性

jvm会进行指令重排,在多线程情况下会出现问题

volatile关键字可以禁用指令重排

4.synchronized优化

4.1轻量级锁

如果一个对象虽然有多个线程访问,但多线程访问的时间是错开的(也就是没有竞争关系),那么可以使用轻量级锁来优化

4.2锁膨胀

在尝试加轻量级锁的过程中,cas操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时要进行锁膨胀,将轻量级锁变为重量级锁

4.3重量级锁

重量级锁竞争的时候,可以使用自旋来进行优化,如果当前线程自旋成功,这时就可以避免阻塞

多核cpu自旋才有意义

4.4偏向锁

轻量级锁在没有竞争时,每次重入仍需要执行cas操作。java6中引入了偏向锁来优化:只有第一次使用cas将线程id设置到对象的mark word头,之后发现这个线程id是自己的就表示没有竞争,不用cas

  • 撤销偏向锁需要将持锁升级为轻量级锁,这个过程所有线程都需要暂停STW
  • 访问对象的hashcode也会撤销偏向锁
  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的ThreadID
  • 撤销偏向和重偏都是批量进行的,以类为单位
  • 如果撤销偏向达到某个阀值,整个类的所有对象都会变为不可偏向的
  • 可以主动使用-XX:-UseBiasedLocking禁用偏向锁
4.5其它优化
1.减少上锁时间

同步代码块中尽量短

2.减少锁的粒度

将一个锁拆分为多个锁提高并发度,例如:

  • ConcurrentHashMap
  • LongAdder分为base和cells两部分。
  • LinkedBlockingQueue入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率高
3.锁粗化

多次循环进入同步块不如同步块内多次循环

另外JVM可能会做如下优化,把多次append的加锁操作粗化为一次

4.锁消除

JVM会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程访问到,这时候就会被即使编译器忽略掉所有同步操作

5.读写分离

CopyOnWriteArrayList

CopyOnWriteSet

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值