jvm学习笔记

注明: 内容是视频公开课的总结的一些学习笔记。

jvm虚拟机


一、内存模型:包括堆、栈、本地方法栈、方法区(元空间)、程序计数器

在这里插入图片描述


1、栈

①、栈又称为线程栈,是每个线程独有的内存空间,存放线程中的局部变量。

(栈中存放的是对象的内存地址,对象是存放在堆中的)

②、栈帧,一个方法对应一个栈帧内存空间。在每个方法执行时,在栈的内存空间中,会分配一块独立的内存空间,称之为栈帧。用于存放方法内部的局部变量表、操作数栈、动态链接和方法出口。
  • 例如一个方法中存在 int a = 1,在栈帧中,首先会将int类型的常量1压入操作数栈,之后在局部变量表中为a划分一小块内存区域,分配完之后,会将操作数栈中的1弹出放入代表a的内存区域中。
  • 方法出口,在被调用方法的栈帧空间,存储调用方法时调用方的代码行数位置,被调用方执行完之后,根据记录的位置,从而返回到调用方法中某一行代码
③、jvm中的栈是利用栈的数据结构,先进后出,先压栈进入的方法后出栈销毁释放
④、线程栈的内存空间中会存在自己程序计算器和本地方法栈

2、堆

①堆=年轻代+老年代,年轻代=Eden区+Survivor区
年轻代和老年代默认比例是1:2,也就是默认年轻代占堆内存的1/3,老年代占堆内存的2/3。
年轻代中存在Eden区(伊甸园区)和Survivor区(幸存者区),在Survivor区中有两块内存区域,这里及下文可以称为S0区和S1区。Eden区和Survivor区默认比例为4:1,也就是Eden区占年轻代内存空间的8/10,Survivor区占年轻代内存空间的2/10。Survivor区中的S0区和S1区以1:1的比例存在。简而言之,Eden区、S0区、S1区在年轻代中的默认比例为8:1:1。
在程序中创建对象时,首先会将创建的对象放入Eden区。
Eden区经过不断的对象被创建放入,Eden区被放满,此时,字节码执行引擎会暂停用户线程(STW 注释①),在后台开启一个线程执行 minor GC(可达性分析算法),minor GC会将垃圾对象回收,非垃圾对象移入Survivor区的S0分区(复制算法),并将对象头中的年代年龄加1。
经过一次minor GC后,Eden区被清空,之后创建的新对象依旧放入Eden区,直到下次Eden区被放满。
Eden区再次被放满时,暂停用户线程,再次开启minor GC,这一次 minor GC会对Eden区和Survivor区上一次放入对象的分区进行一垃圾回收,将垃圾对象回收,非垃圾对象放入Servivor区的另一个分区,同样对象头中的分代年龄加1。
如此反复,当对象的分代年龄达到15,也就是执行了15次minor GC依旧没有被回收的对象,会被移入老年代中。
随着老年代中的对象增多,老年代被放满,字节码执行引擎暂停用户线程,在后台开启一个线程执行full GC(可达性分析算法),当老年代被放满并且其中对象无法被回收时,出现OOM(out Of Memory 注释③)。
  • 其他进入老年代的方式
    • 大对象直接进入老年代,大对象就是需要大量连续内存空间的对象,比如字符串、数组。jvm参数-XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在Serial和ParNew两个收集器下有效。**为什么要这样做呢?**为了避免为大对象分配内存时的复制操作而降低效率。
    • 对象动态年龄判断,当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio 可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor GC之后触发的
    • Minor GC后存活的对象Survivor区存放不下,这种情况会把存活对象部分挪到老年代,部分可能还会放到Survivor区。

3、程序计算器,每个线程都有自己的程序计数器,记录代码执行到哪一行,其中存储着执行代码行的内存地址,由字节码执行引擎动态修改


4、方法区(元空间),类装载子系统会将字节码文件(.class)的相关信息放到方法区里,方法区中包含:常量、静态变量、类信息、对象引用(内存地址)等

  • 注意:public static User user = new User();,静态变量user和new User()的内存地址存放在方法区,new User()产生的对象存放在堆中。

5、本地方法栈,native关键字修饰的方法称为本地方法,当java执行本地方法时,会去操作系统底层语言的库函数中找相关实现。

例如windows,会去windows中C语言的库函数(xx.dll)找方法相关实现,并在栈中开辟一块空间,称为本地方法栈,用于运算和数据存放

6、windows调用jvm监控插件

cmd -> jvisualvm

7、jvm参数

参考

https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html

8、class文件加载到内存的过程

①、类的加载过程

一个java文件从被加载到被卸载这个生命周期,一共需要经历5个阶段,jvm将类加载分为:
加载(loading)->链接(linking 验证+准备+解析)->初始化(initializing 使用前的准备)->使用->卸载回收(GC)

  • 加载(loading)
    首先通过一个类的全限定名来获取此类的二进制字节流;
    其次将这个字节所代表的静态存储结构转化为方法区的运行时数据结构;
    最后在java堆中生成一个代表这个类的class对象,作为方法区这些数据的访问入口。
    总的来说就是class文件从硬盘,经过loading的过程(classloader),读到内存,进行linking(链接),之后进行initializing(类初始化),最后由GC回收。。

  • 链接(linking)
    链接分为 验证(verification)、准备(preparation)、解析(resolution)
    什么方法可以进行解析:构造方法,private方法;带多态的方法是无法静态解析的
    验证: 对格式进行校验,确保被加载类的正确性;
    准备: 为类的静态变量分配内存,并将其初始化为默认值;
    解析: 静态解析,把类中的符号引用转换为直接引用;

  • 初始化(initializing)
    此时静态变量赋值为初始值

③、类的初始化

1 类什么时候才被初始化?

  • 创建类的实例,也就是new一个对象
  • 访问某个类的或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(Class.forName())
  • 初始化一个类的子类(会首先初始化子类的父类)
  • JVM启动时表明的启动类,既文件名和类名同名的那个类

2 类的初始化顺序
1)如果这个类还没有被加载和链接,那就先进行加载和链接
2)如果类存在父类并且这个类还没有被初始化,那就初始化直接的父类(不适用于接口)
3)static静态变量、静态块
4)总的来说,初始化顺序依次为: 静态变量、静态初始化块->变量 初始化块->构造器; 如果有父类,则:父类static方法->子类static方法->父类构造方法->子类构造方法。

④、类的加载

类的加载是指将类的.class文件中的二进制数据读入内存,将其放在运行时数据区的方法区内,然后在堆里创建一个这个类的java.lang.Class的对象,用来封装类在方法区类的对象。
类的加载的最终产品是位于堆中的Class对象。Class对象封装了类在方法区内的数据结构,并且提供了访问方法区内的数据接口。加载类的方式依次为:
* 从本地系统直接加载
* 通过网络下载.class文件
* 从zip,jar等归档文件中加载.class文件
* 从专有数据库中提取.class文件
* 将java源文件动态编译为.class文件(服务器)

⑤、类加载器

9、GC

①、什么是可回收垃圾对象


如图分析: A为栈中的一个局部变量,B是堆中的一个对象,C是B的一个局部变量,D为C指向的一个对象。
此时,虽然D被C指向,但是B没有被任何对象引用,C是B的一个实例成员变量(无法算作GC Root),所以BCD都是垃圾对象。
如果A指向于B,那么此时BCD就不为垃圾对象。

②、如何找到垃圾对象(主要两种算法)

1)引用计数法:
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是可回收对象。
这个方法实现简单,效率高,但是目前主流的虚拟机并没有选择使用这个算法,其主要原因是它很难解决对象之间相互循环引用的问题。
所谓对象之间相互循环引用,如:objA和objB相互引用对方,除此之外无其他引用,但因为他们相互引用对方,导致他们引用计数器无法为0,于是引用计数器无法通知GC回收器回收它们。

2)可达性分析算法:
将GC Root做作为起点,从这些节点向下搜索引用的对象,找到的对象都标记为‘非垃圾对象’,其余未标记的对象都是垃圾对象。 GC Root,根节点,包括静态变量、本地方法栈的变量、线程栈的局部变量等

⑤、jvm垃圾回收算法:

1)Mark-Sweep 标记清除:
将内存区域进行标记,分为存活对象、未使用、可回收,对可回收区域对象进行回收,回收后,可回收区域变为未使用区域,标记的存活对象区域依然保留。

弊端:-位置不连续,产生碎片。

2)Copying 复制算法:
将空间分为两份,用户使用一半空间,回收时,将可回收对象回收,未使用(包括回收后的空间此时也成为未使用区域)、存活对象复制到另一半空间,上一半空间全部清空成为未使用区域。

弊端:-没有碎片,浪费空间。

3)Mark-Compact 标记整理:
在标记清除算法上做了一个改进,回收后对空间进行了整理。

弊端:-没用碎片,效率偏低。

④、常用垃圾收集器解析

java中至今为止,有10种垃圾收集器。 下图1:

在jdk1.8之前 主要以左侧的垃圾收集器为主,1.8默认的垃圾收集器是Parallel Scavenge(PS 收集年轻代)、Parallel Old(PO 收集老年代);分代模型;
1.8之后以右侧的垃圾收集器为主,像G1 是jdk1.9默认的垃圾收集器;分区模型

1)在jdk早期1.0/1.1/1.2的时代,默认的垃圾收集器是Serial和Serial Old,Serial(串行)

使用-XX:UseSerialGC,年轻代使用SerialGC,老年代自动使用Serial Old GC

例如,用户线程进入,发现年轻代的Eden区满了,暂停用户线程,开启一个GC线程,SerialGC年轻代使用复制算法进行垃圾回收,当老年代满了,暂停用户线程,开启一个GC线程,Serial Old GC 使用标记-整理算法进行垃圾回收。

2)在jdk1.8,默认的垃圾收集器是Parallel Scavenge、Parallel Old,Parallel(并行)

使用-XX:UseParallelGC/-XX:UseParallelOldGC,年轻代使用Parallel Scavenge GC,老年代使用Parallel Old GC

例如,用户线程进入,发现年轻代的Eden区满了,暂停用户线程,开启多个GC线程,Parallel Scavenge GC年轻代使用复制算法进行垃圾回收,当老年代满了,暂停用户线程,开启多个GC线程,Parallel Old GC 使用标记-整理算法进行垃圾回收。

3)CMS(Concurrent Mark Sweep – 并发标记清除),由于无论是SerialGC、Serial Old GC、Parallel Scavenge GC、Parallel Old GC,它们在垃圾回收的时候都会暂停用户线程(STW stop the world),所用出现了CMS。但是由图1可见,Parallel Scavenge是无法和CMS一起使用的,所以出现了ParNew,进行年轻代的垃圾回收。ParNew和Parallel Scavenge十分相似,在Parallel Scavenge上做了改良,能够搭配CMS。

使用-XX:UseConcMarkSweepGC,年轻代使用ParNew GC,老年代使用CMS GC与Serial Old GC收集器的组合,Serial Old GC将作为CMS出错的后备收集器。

例如,用户线程进入,发现年轻代的Eden区满了,暂停用户线程,开启多个GC线程,ParNew GC年轻代使用复制算法进行垃圾回收(这里和Parallel Scavenge没什么区别);当老年代满了,也会暂停用户线程(时间短,几乎可以忽略不计),进行初始标记(初始标记: 找到GC Root下的第一个引用)—> 第二阶段开始并发标记(延着初始标记继续向下找引用对象),此时用户线程可以并行执行,不会产生STW,但此时可能产生浮动垃圾(并发标记为非垃圾的对象,在并行执行的用户线程中失去引用),还可能产生错标(在初始标记时,标记的垃圾对象,在并发标记时,并行的用户线程对这个对象添加了引用) 浮动垃圾影响不是很大,因为可以在下次垃圾回收时,再进行回收,但是错标,会导致非垃圾对象被回收掉,程序产生错误 --> 所以为防止错标,并发标记之后,会进行重新标记,此时也会暂停用户线程(时间短,几乎可以忽略不计) —> 最后进行并发清理,此时用户线程可以并行执行,不会产生STW。

为CMS GC与Serial Old GC收集器的组合使用?
在并发标记、并发清理的时候,由于用户线程是可以执行的,所以有可能会产生新的对象,在空间不足又产生新的对象时,CMS会出错,此时便会停止CMS,使用Serial Old GC进行垃圾回收。

CMS,是为了解决STW时间过长的问题,CMS的STW据说不会超过100ms;G1是JDK9的默认垃圾收集器,据说STW时间不会超过10ms;
ZGC是JDK11的默认垃圾回收器,据说STW时间不会超过1ms

⑤、G1管理内存分块

G1会把内存分为很多的Region(区域),Region之间不是连续的,每一块Region可能是老年代、Eden、Survivor、也可能是Humongous(大对象),大对象一块Region装不下,就会用连续的多块Region来装。
好处:可以多线程的进行垃圾回收。
注意:但G1的内存区域里不是固定的E或O等,假如一块Region一开始装的Eden区的对象,在这个对象回收掉之后,这块区域可能分配老年代的对象。所以G1对比以前的垃圾回收就不需要指定年轻代与老年代的占比了


注释

  1. STW(stop the world):minor GC、full GC会暂停用户线程,进行垃圾回收,结束后继续其他线程。如果不暂停用户线程,minor GC执行中没有执行完,之前被判定的非垃圾对象,可能会失去引用,成为垃圾对象。
  2. OOM(out Of Memory) :
    • 内存泄露与内存溢出:内存泄漏指申请使用完的内存没有释放,导致虚拟机不能再次使用该内存区域,此时这段内存就泄漏了,因为申请者不用了,又不能被虚拟机分配给别人用;内存溢出指申请的内存超出了jvm能提供的内存大小,此时称之为溢出。
    • 常见的三种OOM情况:
      • java.lang.OutOfMemoryError:Java heap space -------> java堆内存溢出,此种情况最为常见,一般由于泄漏或者堆的大小设置不当引起。对于内存泄漏,需要通过内存监控软件查找程序中的泄漏代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
      • java.lang.OutOfMemoryError: PermGen space -------> java永久代溢出了,即方法区溢出了,一般出现于大量class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的class信息存储于方法区。此情况可与通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m 的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
      • java.lang.StackOverflowError ------->不会抛OOM error,但也是比较常见的java内存溢出。java虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置大小。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张矜持

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值