JVM-1

JVM内存模型的初步认识

内存模型

image-20210425121015195

image-20210425121057530

上图其实就是jdk1.8之前和之后的一个堆内容的区别,就是把永久代改名为元空间而已

如上图所示

方法区、堆是所有线程共享的,java栈、本地方法栈、程序计数器都是线程私有的

默认情况下,JVM使用的内存的最大内存为电脑内存的四分之一,初始化的内存为六十四分之一

内存布局

image-20210425202425177

对象头

对象头中主要是有两类数据

  1. 官方称它为“Mark Word”,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
  2. 类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例
  • 通过类型指针来确定该对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身
  • 果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。
实例数据

这部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来

在默认的分配策略来看

  • 前提条件:相同宽度的字段总是被分配到一起存放
  • 在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前
对齐填充

由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

对象指针
句柄

·如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息

image-20210425205226086

直接指针

如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销

image-20210425205250644

对比

直接指针的优势:速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本

就虚拟机HotSpot而言,采用的是直接指针

句柄的优势: 当在进行垃圾回收的时候,会产生对象移动,而句柄,只需要修改句柄池到对象实例数据的指针即可,速度非常快;而直接指针是需要修改java栈中的reference的

永久代(元空间)

作用:这个区域是常驻内存的,一般用来存放JDK自身携带的Class对象,interface原数据,存储的是java运行时的一些环境吗,这个区域不会存在垃圾回收,关闭jvm会释放这个区域的内存。

jdk1.6之前:永久代,常量池在永久代中

jdk1.7:永久代,但是慢慢退化了,“去永久代”,常量池在堆中

jdk1.8之后:无永久代,改名为元空间,常量池在元空间内

元空间逻辑上存在,但是物理上不存在。

新生代内存+老年代内存=JVM最大内存

本地方法栈

本地方法栈、java栈、程序计数器这三个部分中是不会有垃圾垃圾产生的,栈的作用是就是排序,用完就弹出了,所以不会有垃圾,而程序计数器只是单纯的进行数字的加减,当然不会有垃圾。

所以说平常说的jvm调优通常都是在堆(方法区其实是特殊的堆)中进行调优的

本地方法栈中的方法就是 带有native关键字的方法,带有native关键字表示java语言已经访问不到,需要调用c的库来执行,通过JNI来调用

JNI作用:扩展java的使用,融合不同的编程语言供java所使用

方法区

方法区时被所有线程所共享的,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间

静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

垃圾清理过程

垃圾清理过程

gc日志查看

young gc

image-20210425152637368

full gc

image-20210425152702546

垃圾回收

算法

引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的

问题在于,当a引用b,b也引用a,除此之外,无任何引用的时候,两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。

可达性分析算法

从GC Roots开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain)

如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

GC Roots
  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。·
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • ·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。·
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • ·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。·
  • 所有被同步锁(synchronized关键字)持有的对象。·
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。

譬如后文将会提到的分代收集和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生代的垃圾收集),必须考虑到内存区域是虚拟机自己的实现细节(在用户视角里任何内存区域都是不可见的),更不是孤立封闭的,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性

引用

强引用

强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

软引用

·软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用

弱引用

·弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。

虚引用

·虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。

对象死亡

即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:

  1. 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选
  2. 筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。

任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行

不建议使用finalize

它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。有些教材中描述它适合做“关闭外部资源”之类的清理性工作,这完全是对finalize()方法用途的一种自我安慰。finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及时,所以笔者建议大家完全可以忘掉Java语言里面的这个方法。

时机

metadata

metadata区域满了之后会触发full gc

young gc

young gc之前

  1. 空间担保机制:检查老年代可用对象是否大于新生代所有空间的大小,大于则进行young gc,小于则走第二步
  2. 是否开启HandlePromotionFailure 这个参数,开启则判断 判断老年代的内存大小是都大于之前每一次minor gc之后的进入老年代的对象的平均大小,如果大于则冒险直接往下走,小于则提前进行fullgc,否则提前进行full gc

young gc之后

  1. 年龄太大了
  2. suvivor区域放不下新生的对象
  3. 动态年龄判断:相同年龄对象总和大于s区的50%,此时年龄n以上的对象进入老年代
full gc
  1. 老年代可以设置一个阈值,大于这个这个阈值就会触发full gc
  2. 在执行Young GC之前,如果判断发现老年代可用空间小于了历次Young GC后升入老年代的平均对象大小的话,那么就会在Young GC之前触发Full GC,先回收掉老年代一批对象,然后再执行Young GC
  3. 如果Young GC过后的存活对象太多,Survivor区域放不下,就要放入老年代,要是此时老年代也放不下,就会触发Full GC,回收老年代一批对象,再把这些年轻代的存活对象放入老年代中
进入老年代时机
  1. 年龄到默认的15
  2. 对象太大了,直接进入老年代,不经过新生代
  3. Minor gc之后,发现剩余对象太多,s区放不下
  4. 动态年龄判断,所有年龄大小的内存加起来超过一个s区的50%,n个年龄及以上对象直接进入老年代
oom

如果是full gc之后,老年代还是没有足够的大小来放minor gc存活的对象,就会触发oom

三种情况

metaspace
  • 元空间设置参数使用默认的
  • 动态代理动态生成了一些类,可能会导致内存溢出
虚拟机栈

可能是方法递归调用,就是有些残忍

堆内存
  • 高并发
  • 内存泄露