JVM学习笔记(一)


前言

JVM
JVM+基础类库 = JRE java运行环境
JVM+基础类库+编译工具 = JDK java开发工具包

学习笔记规划

  • 1.JVM内存结构
  • 2.GC垃圾回收
  • 3.Java Class
  • 4.ClassLoader
  • 5.JIT Compiler

一.JVM内存结构

程序计数器

记住下一条jvm指令的执行地址
**特点 😗*线程私有,每个线程有自己的程序计数器 不会存在内存溢出

虚拟机栈

线程运行时需要的内存空间,多个线程则有多个虚拟机栈
栈内就是一个一个栈帧,栈帧就是每个方法运行时需要的内存空间
方法内调用方法,则会在栈内存入多个栈帧
定义:
– 每个线程运行时所需要的内存,称为虚拟机栈
– 每个栈由多个栈帧组成,对应着每次方法调用所占用的内存
– 每个线程只能有一个活动栈帧,对应着正在执行的方法
在这里插入图片描述
继续执行后栈帧会慢慢pop出虚拟机栈
在这里插入图片描述

栈的默认大小一般是 -Xss 1024KB

栈内存设置过大的话,能同时运行的线程数就越少,因为每个线程独立划分一个栈内存,而物理内存是一定的,每个线程占用大则会导致总数减少

栈内存设置大的好处是可以运行更多的递归调用,不容易栈满

线程安全
栈是线程私有的,不用考虑线程安全
如果数据是共享的,则在多线程下需要考虑线程安全
方法内的局部变量是否线程安全?
传入调用参数,或者有return返回变量,则线程不安全数据可能被其他线程共享

栈内存溢出
StackOverflow
改变栈内存的方法: VM potions:-Xss256k

栈溢出情况:
1栈帧过多导致栈内存溢出(类似死循环递归)
2栈帧过大导致栈内存溢出(一般不容易出现,方法内的变量不多)

线程诊断
用top命令查看哪个进程对cpu占用过高
查看某个进程下的所有线程占用的cpu情况
ps -H -eo pid,tid,%cpu | grep 进程id

Jstack 进程id 查看进程下的所有线程运行情况,线程号默认变成16进制的,里面可以看出代码的第几行报错

本地方法栈

Native本地方法调用需要的内存提供是本地方法栈
Object类底层就有许多native本地方法,是用c语言编写的

Head堆

线程共享,堆中对象都需要考虑线程安全的问题
有垃圾回收机制

–堆内存溢出
如果不断产生对象,对象一直都有被引用使用,则会造成堆内存溢出
Java.lang.OutOfMemoryError:Java heap space
改变堆内存的方法: VM potions:-Xmx8m

–堆内存诊断
Jps工具: 查看当前系统中有哪些java进程
Jmap工具: 查看堆内存占用情况
Jconsole工具: 图形界面工具

命令行输入:jps 查看当前运行的java进程号
Jmap -Heap java进程号 (查看此进程的堆使用情况)
Jconsole (会出现图形化界面,监控堆的使用情况)
在这里插入图片描述

案例:执行多次垃圾回收后,内存占用仍然很高
一般就是堆内的对象一直被引用,没办法回收释放内存,可以使用Jvirsualvm 代替jconsole工具来查排查内存的使用情况

方法区

定义:所有java虚拟机线程共享的区域,存储跟类相关的信息,运行时的常量池
在虚拟机启动时被创建,逻辑上是堆的一个组成部分,方法去也会导致内存溢出的错误
1.6和1.8的方法区结构:
在这里插入图片描述

1.6是永久代里面存有class,classloader,常量池,是放在堆内的一个内存空间
1.8是元空间,而且不再放在堆内,是放在本地操作系统的内存内


方法区内存溢出
1.6会导致永久代内存溢出
设置永久代的最大内存:-XX:MaxPermSzie = 8m

1.8会导致元空间内存溢出
加载的类过多,就会造成元空间内存溢出,元空间是使用的系统内存,因此一般不容易造成溢出
设置元空间的最大内存-XX:MaxMetaspaceSzie = 8m
Spring,mybatis的底层都需要生成代理对象,使用不当都会造成方法区溢出


运行时常量池

反编译类class文件:
D:\IDEA\IDEA-project\rabbitMQ-study\JVM_study\out\production\01>
javap -v Demo02.class
这些就是常量池
在这里插入图片描述

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

Stringtable串池

常量池中的信息,在类还未加载对象的时候,a b ab都是常量池的符号
在对象创建时,才会把a符号变成“a“字符串对象放入stringtable串池内

是懒加载的行为,Stringtable是hashtable结构,不能扩容


String s1 = “a”
String s2= “b”
String s3= “ab”
String s4 = s1+s2
String s5 = “a”+”b”

1.在类创建之前,常量池会放入a,b,ab符号
2.S4底层的实现是创建一个stringbuilder,分别append进去s1和s2,动态拼接字符串,tostring转换成string类型的对象返回给s4
3. s3== s4 false s3从常量池找,s4在串池找s1,s2
4. s3==s5 true 因为s5执行的时候是直接从常量池寻找值为ab的符号

执行到串池内没有的对象,则会创建新的串池对象,这也是懒加载


Intern使用
如果s是由串池的a和串池的b拼接成创建成的ab字符串对象,此时ab只存在于堆内,不存在于串池中,这时执行s.intern()则会 把ab尝试放入串池,如果串池有ab对象,则不会放入,反之放入

Stringtable 的特性
常量池中的字符串仅是符号,第一次用时才变为对象
利用串池的机制,来避免重复创建字符串对象
字符串变量拼接的原理是StringBuilder
字符串常量拼接的原理是编译期优化
可以使用intern方法,主动将串池中还没的字符串对象放入串池

面试题:

public class Main {
    public static void main(String[] args) {
        //"a" "b" 被放入串池中,str则存在于堆内存之中
        String str = new String("a") + new String("b");
        //调用str的intern方法,这时串池中没有"ab",则会将该字符串对象放入到串池中,此时堆内存与串池中的"ab"是同一个对象
        String st2 = str.intern();
        //给str3赋值,因为此时串池中已有"ab",则直接将串池中的内容返回
        String str3 = "ab";
        //因为堆内存与串池中的"ab"是同一个对象,所以以下两条语句打印的都为true
        System.out.println(str == st2);
        System.out.println(str == str3);
    }
}
public class Main {
    public static void main(String[] args) {
        //此处创建字符串对象"ab",因为串池中还没有"ab",所以将其放入串池中
        String str = "ab";
        //"a" "b" 被放入串池中,str则存在于堆内存之中
        String str2 = new String("a") + new String("b");
        //此时因为在创建str3时,"ab"已存在与串池中,所以放入失败,但是会返回串池中的"ab"
        String str3 = str.intern();
        //false
        System.out.println(str == str2);
        //true
        System.out.println(str == str3);
        //false
        System.out.println(str2 == str3);
    }
}

–Stringtable位置
1.6的stringtable在永久代的常量池中
1.8的stringtable在Heap堆内,垃圾回收频率比较高,减轻内存的负担

–Stringtable垃圾回收
Stringtable底层就是hashtable数组加链表的形式
在intern过多字符串对象时,串池的内存会达到一个阈值,触发GC的垃圾回收,这样执行的结果串池内的对象就会减少一大部分

–Stringtable性能调优
调节stringtable内桶的数量,不同的桶的大小,执行相同的字符串引入的运行时间不同,因为每一次字符串引入都需要先遍历寻找串池内是否已经含有此字符串,寻找的时间长短导致了执行的时间长短
如果字符串常量过多的话,建议把桶的数量设大,这样可以增加stringtable效率

直接内存

操作系统内的内存,NIO操作,直接内存的IO操作效率比较高
Java要读取系统内存的缓冲区文件,系统内存会划分一个direct memory直接内存空间,共享内存区是java和操作系统都可以访问的内存区,这样就不用实现系统内存到java内存之间的io流操作,这样读取大文件的效率就会非常快。

在这里插入图片描述

–直接内存_溢出
Direct Memory 不受java程序管理,不受JVM内存回收管理,直接内存是存在于系统内存,查看直接内存的占用情况要在操作系统开启后台资源管理查看,也会溢出和泄露

–直接内存_释放原理
直接内存不受JVM管理,但是执行GC操作后,直接内存也会释放一部分内存,原因是:

底层是jdk自己使用了一个unsafe,里面调用了freeMemory释放分配的直接内存
byteBuffer在分配直接内存的时候,构造器里面就调用了unsafe对象,完成对直接内存的分配,里面有一个cleanner,当byteBuffer这个对象被垃圾回收掉,cleaner就执行create方法,方法执行了unsafe.freeMemory方法达到对直接内存的释放

禁用显示回收对直接内存的影响 -XX:-DisableExplicitGC
让system.gc() 无效

二.垃圾回收

如何判断对象可以回收?

1.引用计数法
当出现A和B互相引用,而这两个又不会使用,这样就出现了内存泄露
在这里插入图片描述

2.可达性分析算法
JAVA虚拟机使用的垃圾回收算法,如果没有被直接或者间接的引用,则会被销毁
·扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收

哪些对象能成为GC Root对象?
Object,hashMap,或者Native等基础运行的对象,Monitor加锁的对象,Thread获得线程类对象
四种引用

  • 强引用,软引用,弱引用,虚引用,终结器引用


在这里插入图片描述

软引用的使用SoftReference
//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
List<SoftReference<byte[]>> list = new ArrayList<>();

如果GC后,内存还是很大,则会释放掉软引用的对象,这样软引用的对象就是为null,此时我们还可以配合引用队列,把无用的软引用清除掉

public class Demo1 {
    public static void main(String[] args) {
        final int _4M = 4*1024*1024;
        //使用引用队列,用于移除引用为空的软引用对象
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
        //使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
        List<SoftReference<byte[]>> list = new ArrayList<>();
        SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);

        //遍历引用队列,如果有元素,则移除
        Reference<? extends byte[]> poll = queue.poll();
        while(poll != null) {
            //引用队列不为空,则从集合中移除该元素
            list.remove(poll);
            //移动到引用队列中的下一个元素
            poll = queue.poll();
        }
    }
}

弱引用的使用WeakReference
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用所引用的对象
可以配合引用队列来释放弱引用队列自身

垃圾回收算法

<- 标记-清除, 标记-整理, 复制 ->

1标记清除 Mark Sweep
优点:速度快 清除不是删除内存容量,而是标记为空闲内存,后面新创建的对象可以存入
在这里插入图片描述

缺点:容易产生内存碎片,即内存不连续。清理内存后不会再对内存进行整理,虽然碎片的总内存够,但是塞不下大的对象

2标记整理Mark Compact
缺点:速度慢
优点:没有内存碎片
在清理垃圾的过程中,会把可用的对象向前移动,使得内存更紧凑,连续的内存更多,避免标记清除的内存碎片的问题
在这里插入图片描述

3复制Copy
不会有内存碎片
需要占用双倍的空间
在这里插入图片描述

分代垃圾回收

结合上面的三种算法,协同工作,分代的垃圾回收机制
新生代每次MinorGC就会执行复制算法,把没有被GC的对象放在幸存区To中,并且每次GC,对幸存的对象的寿命加一,最后倒换两个幸存区,把To与From的引用倒换,使得每次GC的结果幸存区To都为空
在这里插入图片描述

当幸存区的对象的寿命超过了阈值(最大为15),则此对象晋升到老年代中
老年代晋升的对象多了,空间不足,且新生代的空间也不足了,则会触发Full GC
执行标记整理算法扫描新生代和老年代所有不再使用的对象并回收
在这里插入图片描述

总结:
对象首先分配在伊甸园区域
新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行(复制算法会把内存转移位置,此时操作会出错)
当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长

JVM的参数

在这里插入图片描述

GC分析

1大对象处理策略:
当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代
2线程内存溢出:
经过GC后内存还是满着,抛出异常,此时不会让其他线程结束运行,所占据的内存会全部被释放掉,不会影响其他线程的运行

垃圾回收器

吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
1串行
单线程,一个线程来完成垃圾回收,堆内存较小,适合个人电脑
在这里插入图片描述

**安全点:**让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象
因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

新生代和老年代各有自己的垃圾回收器
新生代使用复制算法,老年代使用标记整理算法

2吞吐量优先
多线程
堆内存较大,多核cpu
让单位时间内,STW所占用的时间最短
在这里插入图片描述

3响应时间优先,低延迟
多线程
堆内存较大,多核cpu
尽可能让单次垃圾回收的STW时间最短
在这里插入图片描述
一般是CMS垃圾回收器,目前是G1垃圾回收器比较热门
在这里插入图片描述

只有初始标记和重新标记会触发STW,其他并发标记不会触发,和用户线程一起运行,达到了单次STW最短

CMS 收集器
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器
CMS收集器的内存回收过程是与用户线程一起并发执行的

  • -XX: ParallelGCThreads 一般设置为CPU核数
  • -XX: ConcGCThreads 一般设置为ParallellGCThreads数的四分之一
  • -XX: CMSInitiatingOccupancyFraction:执行CMS垃圾回收的内存占比;设为80,即老年代内存占到80%时就开启垃圾回收,为了预留空间给浮动垃圾;早期默认值为65
    由于在并发清理阶段,用户线程还有可能产生垃圾,这部分垃圾只能等到下一次CMS时才能清理,这部分垃圾称作浮动垃圾;即:cms过程中还会产生新的垃圾,故不能等到老年代全部满了再垃圾回收
    -XX: +CMSScavageBeforeRemark :再重新标记之前,先对新生代做一次垃圾回收,减轻重新标记时的压力
    标记-清除算法,会导致碎片过多,并发失败,会退化为SerialOld收集器,进行一次标记-整理回收,响应时间会变得很长。

三.G1垃圾回收器

JDK9后默认使用,代替CMS收集器
注重吞吐量和低延迟
超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域(Region)
整体上是标记-整理算法,两个区域之间是复制算法
在这里插入图片描述

执行流程

新生代伊甸园垃圾回收 —–> 当老年代内存不足,新生代垃圾回收+并发标记 —–> 混合收集:回收新生代伊甸园、幸存区、老年代内存 ——> 新生代伊甸园垃圾回收(重新开始)

2分区算法region
分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间

E:伊甸园 S:幸存区 O:老年代
在这里插入图片描述
新生代对象通过复制算法复制到幸存区
在这里插入图片描述
当幸存区中对象也比较多了或者辛存区对象年龄达到阈值,会再次触发新生代垃圾回收,部分幸存区对象会晋升老年代
在这里插入图片描述

3 Young Collection + CM
CM(Concurrent Marking):并发标记
在 Young GC 时会对 GC Root 进行初始标记
在老年代占用堆内存的比例达到阈值(默认45%)时,进行并发标记(不会STW),阈值可以根据用户来进设定
在这里插入图片描述

4 Mixed Collection
会对E S O 进行全面的回收
最终标记(Remark):会STW
拷贝存活(Evacuation):会STW

为什么有的老年代被拷贝了,有的没拷贝?
因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)
在这里插入图片描述

G1 Full GC

SerialGC 串行
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足发生的垃圾收集 - full gc

ParallelGC 并行
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足发生的垃圾收集 - full gc

CMS 并发
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足 - 分情况

G1 并发
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足 - 分情况

以G1为例:
G1在老年代内存不足时(老年代所占内存超过阈值)
如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理
如果垃圾产生速度快于垃圾回收速度,便会触发Full GC

G1 跨代引用

新生代回收的跨代引用很多gc root在老年代中,遍历老年代查找gc root太耗时
解决方法:
对引用了新生代的老年代内存加入脏卡,这样新生代回收直接去脏卡找即可
在这里插入图片描述

G1 并发标记阶段

在垃圾回收时,收集器处理对象的过程中
黑色:已被处理,需要保留的
灰色:正在处理中的
白色:还未处理的
在这里插入图片描述

但是因为是并发,所以可能在标记的时候,用户线程可能对对象之间的引用进行修改,等于说对象之间的引用断了,并发结束后,此”没被引用“的对象就会被当成垃圾回收掉,这是并发标记会出现的bug情况,解决这种bug,则需要加入重新标记阶段

G1 重新标记阶段

当对象C的引用发生改变时,JVM就会给C加一个写屏障,写屏障的指令会被执行,将C加 入一个队列当中,并将C变为 处理中 状态
在并发标记阶段结束以后,进入重新标记阶段,重新标记阶段会STW,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它
在这里插入图片描述

G1 字符串去重

-XX:+UseStringDeduplication
过程
将所有新分配的字符串(底层是char[])放入一个队列
当新生代回收时,G1并发检查是否有重复的字符串
如果字符串的值一样,就让他们引用同一个字符串对象
注意,其与String.intern的区别
intern关注的是字符串对象
字符串去重关注的是char[]
优点与缺点
节省了大量内存
新生代回收时间略微增加,导致略微多占用CPU

G1 类卸载

如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类
-XX:ClassUnloadingWithConcurrentMark 默认启用

巨型对象
一个对象大于region的一半时,就称为巨型对象
G1不会对巨型对象进行拷贝
回收时被优先考虑
G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

四.GC垃圾回收调优

查看虚拟机参数命令
“C:\Program Files\Java\bin\java” -XX:+PrintFlagsFinal -version | findstr “GC”
调优领域

  • 内存
  • 锁竞争
  • CPU占用
  • IO
  • GC

确定目标
低延迟/高吞吐量? 选择合适的GC

  • CMS,G1,ZGC(java 12) 低延迟 互联网项目
  • ParallelGC 高吞吐量 数字运算项目
  • Zing

最快的GC
最快的GC是不发生GC,不发生STW,则不会打断线程

查看Full GC前后的内存占用,考虑以下几个问题
数据是不是太多?
resultSet = statement.excuteQuery(“select * from 大表 limit n”)
数据表示是否太臃肿
对象图 对象大小 16 Integer24 int4
是否存在内存泄漏?
使用软弱引用 或者 第三方缓存实现,redis使用自己的内存管理


GC调优-新生代调优

新生代的特点
所有的new操作分配内存都是非常廉价的
TLAB thread-local allocation buffer
死亡对象回收零代价
大部分对象用过即死(朝生夕死)
MInor GC 所用时间远小于Full GC(1到2个数量级)

新生代内存越大越好么?
新生代内存太小:频繁触发Minor GC,会STW,会使得吞吐量下降
新生代内存太大:太大的话触发Minor GC时STW的时间变长,清理新生代所花费的时间会更长造成吞吐量降低。 而且老年代内存占比有所降低,会更频繁地触发Full GC。

新生代内存设置为内容纳[并发量*(请求-响应)]的数据为宜

幸存区调优
幸存区需要大到能够保存 当前活跃对象+需要晋升的对象
晋升阈值配置得当,让长时间存活的对象尽快晋升
-XX:MaxTenuringThreshold=threshold 最大晋升阈值,可以相对调小
-XX:+PrintTenuringDistribution 打印晋升详细信息


GC调优-老年代调优
CMS 的老年代内存越大越好
先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代
观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent 开启Full Gc时老年代空间使用比例

案例
案例1.Full GC 和Minor GC频繁
新生代内存过小,先增大新生代内存,同时增大幸存区空间和晋升阈值,使生命周期较短的对象尽可能留在新生代,让老年代的GC也不那么频繁了

案例2 请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)
查看gc日志,看cms哪个阶段耗时最长,发现重新标记阶段耗时最长(并发标记是不会打断用户线程的),所以打开CMSScavageBeforeRemark开关,在重新标记之前对新生代先进行一次GC,这样重新标记的对象就减少了

案例3 老年代充裕情况下,发生 Full GC (CMS jdk1.7)
1.7以前是永久代,永久代空间设小了就会触发整个堆的一次Full GC

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值