JVM学习总结

JVM

虚拟机

谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。大体上,虚拟机可以分为系统虚拟机和程序虚拟机。

为什么要学JVM

学习理解更高层次内容的需要(项目管理,性能调优)

作用:实现了代码的一次编写到处运行,自动内存管理,自动垃圾回收;现在的 JVM 不仅可以执行 java 字节码文件,还可以执行其他语言编译后的字节码文件,是一 个跨语言平台。

JVM位置

JVM 是运行在操作系统之上的,它与硬件没有直接的交互。

在这里插入图片描述

详细图

在这里插入图片描述

类加载器

加载

将硬盘上的字节码文件读入到内存中,生成class对象,把硬盘上的结构转为内存结构,使用的是classloader进行加载,充当快递员的角色

链接

  1. 验证:

    验证字节码格式是否被修改
    验证语法,eg:是否继承了final修饰的类

  2. 准备

    为类中静态的变量赋默认值,不为静态常量进行赋值,这是jvm的一种调优机制

    补充:静态常量是编译阶段赋值

    ​ 实例变量将会在对象初始化的时候随着对象一起分配在java堆中

  3. 解析

    将符号引用转为直接引用~(文件中的逻辑引用到内存中的实际地址)

初始化

对类中的静态成员进行赋值
类什么时候初始化:new对象,反射机制,子类被加载,访问类中的静态变量或静态方法, main方法

  1. 分类

    jvm角度:

    ​ 启动类加载器(非java语言);

    ​ 其他类加载器(java语言)

    程序员角度:

    ​ 启动类加载器:加载\jre\lib目录下的类

    ​ 扩展类加载器:加载\jre\lib\ext目录下的类

    ​ 应用程序类加载器:包含自定义的类加载器

  2. 双亲委派机制

    为了保证类加载的正确性,安全性,在加载类时,会让其父类先去加载。知道顶级的类加载器,如果找到则返回,如果仍然没有找到则委派给其子级去加载,如果找到则返回,如果都没有找到则抛出异常(ClassNotFoundException)

    优点:安全,避免我们自己写的类替换系统中的类;避免类的重复加载(使用了findLoadClass()判断当前类是否已加载)

  3. 如何打破类的双亲委派机制

    java中提供了一个ClassLoader类,定义那些方法加载类;

    像Tomcat这种服务器,里面就会自定义类加载器

    loadClass(String classpath)//建议的 底层使用双亲委派机制加载类;
    findClass(String classPath) // 如果需要自定义,可以重写findClass()
    defineClass() //将读到的class文件的数据构造出一个Class对象
    
  4. 类的主动使用与被动使用

    1. 主动:new对象,反射机制(Class.forName(“”)),子类被加载,使用静态变量,静态方法 main方法

    2. 被动:访问静态常量,作为引用类型使用(创建数组)

运行时数据区

程序计数器

是一块内存很小的区域,用来存放下一条要执行指令的地址,便于在线程切换执行时记录位置,是线程私有的,生命周期与线程一样,运行数度快,不会出现内存溢出

虚拟机栈

栈是运行单位,存储一个一个的方法,当调用一个方法时,创建一个栈帧,将方法中的信息存储到栈帧.

​ 操作只有两个,调用方法入栈,方法执行完成后出栈. 先进后出的结构

​ 运行速度非常快,仅次于程序计数器, 当入栈的方法过多时,会出现栈溢出(内存溢出)

​ 线程独立的,不同线程之间的方法不能相互调用

栈帧的内部结构:
局部变量表:方法内声明的局部变量,方法参数;对于基本类型的变量,直接储存它的值,对于引用类型的变量则存储的是指向对象的引用
操作数栈:运算区域
动态链接:指向运行时常量池的方法引用
方法返回地址:当一个方法执行完毕后,要返回之前调用它的地方

附加信息

本地方法栈

native修饰的方法,没有方法体,eg:hashCode(),read(),start(),arraycopy()
本地方法不是用java语言写的,例如操作系统的方法,如果调用本地方法则在本地方法栈中运行,也可能出现内存溢出问题

  1. 概述:

    是jvm管理的最大的一块空间,主要用来储存对象,是线程共享的,JVM启动堆空间就创建了,大小就确定了,但是可以通过参数改变大小,物理上不连续,逻辑上连续,是GC的重点区域

  2. 区域划分:

    新生代(伊甸园区,S0,S1),老年代

  3. 为什么要区域划分?

    不同对象,它的生命周期不同,这样可以将不同的对象存放在不同的区域,不同的区域采用不同的垃圾回收算法进行处理,扬长避短

  4. 创建对象在区域中的分布:

    一个对象刚刚被创建时存放在伊甸园区,当垃圾回收时会把伊甸园区存活下来的对象移动到幸存者区,幸存者区有区域1和区域2,假设先把存活下来的对象放在幸存者1区,当下次垃圾回收时,会把伊甸园区和幸存者1区存活的对象移动到幸存者2区,这时幸存者1区就被清空了,当下次垃圾回收时,会将伊甸园区和幸存者2区存活下来的对象放在幸存者1区,清空幸存者2区,交替执行,当垃圾回收时,每次会对对象进行标记,在对象头中有一块空间会记录被标记的次数,在对象头中记录分代年龄只有4bit,也就是最大15次进入老年代

  5. 新生代与老年代配置比例:

    默认1:2 可以通过参数来设置
    默认**-XX:NewRatio**=2,表示新生代占 1,老年代占 2,新生代占整 个堆的 1/3
    当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年 代的大小,来进行调优
    Eden 空间和另外两个 survivor 空间缺省所占的比例是 8 : 1 : 1,当然开发人员可以通过选项**-XX:SurvivorRatio**调整这个空间比例。比如-XX:SurvivorRatio=8

    新生区的对象默认生命周期超过 15 ,就会去养老区养老

  6. 分代收集思想:

    jvm垃圾回收机制可以通过不同的区域进行回收,针对新生代会频繁回收,称为yong(Minor) GC;j较少回收老年代,称old(Major) GC
    当调用System.gc();老年代内存不足;方法区内存不足时,会触发FULL GC(整堆收集),尽量避免整堆收集,因为整堆收集时,其他用户线程暂停时间较长

  7. 字符串常量池为什么要调整位置:

    JDK7以后将字符串常量池的位置由方法区调整到了堆中,因为方法区只用在触发FULL GC 时才会进行回收,回收效率低,所以将字符串常量池移到堆中,提高垃圾回收效率

方法区

存储类信息(方法、属性、静态常量、静态变量、编译器、编译后的代码、运行时常量池(字面量值))

线程共享的,也会出现内存溢出,也会涉及到垃圾回收,生命周期也是JVM启动就创建,关闭就销毁

方法区大小设置:-XX:MaxMataspaceSize: window默认是21MB,满后触发full GC;也可以是-1,没有上限,可以占用整个计算机内存

方法区的垃圾回收:主要回收的是静态常量,类信息

3条件:

  1. 该类产生的对象都被回收了
  2. 该类的class对象不再被其他类使用
  3. 该类对应的类加载器也被回收了

执行引擎

  1. 装载字节码文件到执行引擎中,字节码不是机器码,只是jvm规范定义的指令码,执行引擎需要将字节码 解释/编译为不同平台所能识别的机器码

  2. 解释器:jvm运行程序时逐行对字节码进行翻译,效率低

  3. JIT编译器:对某段代码整体编译后执行,效率高,编译需要一定的时间

  4. java为什么是半编译半解释型?

    起初java中只提供了解释执行的方式,但是解释执行的效率较低,后来引入编译器,可以对程序执行的热点代码进行编译,并把编译后的内容缓存起来,后期执行效率高,热点代码是采用计数器的方式来记录的。

    程序启动后可以通过解释器立即对代码进行解释执行,不需要等待编译,提高了响应速度,之后对热点代码采用编译执行,从而提高了后续效率

本地方法接口

什么是本地方法

使用native修饰的方法,不是java语言实现的,是操作系统实现的

为什么要使用本地方法

  1. 需要获取一些硬件信息,如内存地址,启动线程,IO,调用本地方法接口
  2. jvm底层进行字节解释/编译部分有C语言实现

垃圾回收

java提供自动垃圾回收功能,c++没有自动垃圾回收功能,自动垃圾回收功能并不是java首创的,但是java在垃圾回收这一块一致在更新升级

什么是垃圾:没有被任何引用指向的对象

为什么要回收:

  1. 内存方面:垃圾对象如果一直不回收就会一直占用空间,垃圾对象越存越多,从而导致内存溢出
  2. 内存碎片:对内存空间的碎片进行整理,如果不整理,需要储存像数组这种需要连续内存空间的对象就有可能存不下

早期怎么回收:

早期c++代码,需要程序员手动销毁垃圾对象;
不足:麻烦,程序员可能忘记删除就会造成内存泄漏

内存溢出:内存不够用,报内存溢出错误

内存泄漏:有些对象已经不再被使用了,但是垃圾回收对象又不能回收它,这种悄悄占用内存资源的情况叫做内存泄漏

现在怎么回收:

  1. 方式:

    现在引进了自动内存管理;
    优点:降低了程序员的工作量,降低了内存溢出和内存泄露的风险

  2. 担忧:自动内存管理降低了程序员内存管理的能力,一旦出现问题无法下手解决

那些区域回收:方法区和堆(在新生代频繁回收,较少回收老年代,几乎不回收方法区)

标记阶段算法

引用计数算法(未使用)

对每个对象保存一个整型 的引用计数器属性。用于记录对象被引用的情况。

在这里插入图片描述

缺点:

  1. 不能解决循环引用问题
  2. 需要维护计数空间
  3. 赋值后要对计数器进行更新操作
根可达分析算法

从一组GCRoots对象(一组活跃的对象,当前栈帧中使用的对象),开始向下查找,如果与GCRoots是相关联的,那么就不是垃圾对象,否则判定为垃圾对象

GCroots对象:

  1. 栈中所使用的的对象
  2. 静态成员变量所指向的对象
  3. synchronized同步锁对象
  4. jvm系统内部的对象

避免了循环引用问题

补充

对象的finalization机制:

finalize() 是Object类中定义的方法,子类可以重写它,但是不要主动去调用它,finalize()在对象被回收前,由垃圾回收线程调用且只调用一次,一般情况下不需要重写此方法,如果在对象销毁之前需要执行一些释放资源的操作可以重写此方法 ,比如关闭文件、套接字和数据库 连接等

但是要注意,不要在此方法将对象复活,或者出现死循环

对象状态

  1. 可触及的:从根节点开始可以达到这个对象
  2. 可复活的:已经被标记为垃圾finalize()方法还没有被执行(可能调用该方法将对象复活)
  3. 不可触及的:当对象的finalize()方法已经被执行了,而且对象没有被复活,那么对象就进入到了不可触及状态

细节:

如果一个对象第一次被标记为垃圾且finalize()方法没有被执行,则会将这些对象放在队列中,调用他们的finalize()方法,如果在执行次方法时,对象与GCroots中的某个对象关联上了则将其从列表中移除,
当第二次被标记为垃圾对象,就是不可触及的状态会直接被回收掉

回收阶段算法

复制算法

使用两块内存空间,将正在使用区域中的存活对象,复制到另一个区间,按序排放,清除原来的空间

优点:适用于新生代,效率高,无碎片

缺点:需要两块空间,不适用于G1垃圾回收器,G1垃圾回收器每块又分为很多小的区域,需要记录其地址

标记清除

并非真正意义上的清除,而是将垃圾对象地址记录在一个列表里面,当有新的对象需要分配空间时,判断列表中的空间是否够用,够用就用新对象把原来的旧对象覆盖掉

优点:实现简单不需要移动对象,适用于老年代

缺点:会产生内存碎片

标记压缩

针对于标记清除算法去的不足,将存活对象进行了整理,清除垃圾对象,不会产生内存碎片,适用于老年代

分代收集思想

新生代存活对象生命周期短;需要频繁回收,复制算法效率高,适用于年轻代

老年代对象生命周期较长,不需要频繁回收,使用标记清除和标记压缩

垃圾回收相关概念

System.gc()相关概念

通过 System.gc()者 Runtime.getRuntime().gc() 的调用,会

显式触发 Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用

的内存。

无法保证对垃圾收集器的调用(不能确保立即生效)。

JVM 实现者可以通过 System.gc() 调用来决定 JVM 的 GC 行为。而一般情况

下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些

特殊情况下,我们可以在运行之间调用 System.gc()。

Stop the World

在垃圾回收线程标记时,需要在某个时间点上让所有的用户线程暂停一下,保证在判断对象是否为垃圾的准确性,性能好的垃圾回收器发生STW次数少。

垃圾回收器

对垃圾回收的落地实现,jvm中分不同的垃圾回收器,可根据不同场景选择使用

分类:

  1. 按线程数量:

    1. 单线程(Serial):垃圾回收线程只有一个,适用于小场景
    2. 多线程(Parallel):垃圾回收线程有多个,效率高
  2. 按工作模式:

    1. 独占式:垃圾回收线程工作时,用户线程全部暂停STW
    2. 并发式:垃圾回收线程执行时可以不用暂停用户线程
  3. 按内存工作区域:

    1. 新生代:新生代的垃圾回收器
    2. 老年代:老年代的垃圾回收器

GC性能指标

吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运 行时间+内存回收的时间)

垃圾收集开销:垃圾收集所用时间与总运行时间的比例。

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。

内存占用:Java 堆区所占的内存大小。

快速:一个对象从诞生到被回收所经历的时间。

HotSpot 垃圾收集器

在这里插入图片描述

CMS垃圾回收器

CMS概述

CMS之前不管是单线程还是多线程的垃圾回收器都是独占式的,并发标记清除首创用户线程可以和垃圾回收线程并发执行,追求低停顿

  1. 初始标记::Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
  2. 并发标记:垃圾回收线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。
  3. 重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
  4. 并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。 这个过程非常耗时。

在这里插入图片描述

优点:可以做到并发收集

缺点:基于标记清除算法实现,回产生内存碎片;用户线程和垃圾回收线程并发执行,导致吞吐量低;无法处理浮动垃圾(垃圾并发清理时,用户线程不暂停,随时会产生新垃圾,无法处理,只能等下次垃圾回收处理)

三色标记算法

将对象分为三个状态:黑色,灰色,白色

黑色:已经标记过的不是垃圾对象,且该对象下的关联属性也标记过了

灰色:已经被垃圾回收器标记过,但是还有没有被标记过的

白色:没有被垃圾回收器扫描过的,标记是垃圾

步骤:

  1. 刚开始,确定GCRoots对象为黑色
  2. 将GCRoots直接关联的对象设置为灰色
  3. 遍历灰色对象,下面如果还有关联的对象,灰色变为黑色,下边关联对象变为灰色
  4. 重复标记
  5. 将白色对象清除

可能会出现漏标和错标问题

漏标:A关联B,B关联C,A是黑色,B是灰色,C是白色,此时A断开与B的联系,但是B已经是灰色,B和C就成为了浮动垃圾,需要等到下次回收。

错标:A关联B,B关联C,A是黑色,B是灰色,C是白色,此时B与C断开联系,A关联C,但因为A是黑色,不会再进行扫描,就会清理掉不是垃圾的C

解决:将发生变化的关系进行记录,重新标记

G1垃圾回收器

G1也是用的是并发标记和清除,将整个堆的内存空间分为更小的区间,回收时可以根据每个区间的优先级(里面垃圾数量),优先回收优先级较高的区间,降低了用户线程的停顿,提高了吞吐量。

对整堆进行统一的管理,没有年轻代和老年代

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值