JVM学习笔记整理

本文最后更新于:5 天前

JVM

一、JVM内存区域

1.内存模型

jdk1.6的运行时数据区域
jdk1.8之后,方法区被移动到直接内存,并且有个新名字元空间,而原来的运行时常量池被移动到堆空间里。

2.堆

  • 堆是JVM管理的最大一块内存,所有线程共享,几乎所有的对象实例以及数组都是在这里面开辟空间
  • GC主要是在堆里面回收垃圾
  • 为了方便垃圾回收,堆又分成新生代和老年代,新生代和老年代采取不用的垃圾回收策略

3.方法区

这里存放的是被加载的各个类的信息、常量、静态变量等数据,还包含有常量池,不过jdk1.8后,方法区被迁移到直接内存并更名为元空间,而常量池在jdk1.7就被放到堆空间

4.常量池

包括一些字面量和符号引用,比如

  • String str="Hello World!"Hello World!就是在常量池
  • int a=5其中5也是在常量池
  • 还有被final修饰的常量
  • 符号引用:类和结构的完全限定名,字段名称和描述符,方法名称和描述符

5.虚拟机栈

这是线程的私有栈,线程里的各个方法互相调用就需要栈的支持,Java 栈中保存的主要内容是栈帧,每⼀次函数调⽤都会有⼀个对应的栈帧被压⼊Java栈,每⼀个函数调⽤结束后,都会有⼀个栈帧被弹出。
栈帧里面包含有:

  • 局部变量表
  • 方法出口
  • 操作数栈
  • 动态链接

6.本地方法栈

是线程私有的
这个栈和虚拟机栈十分类似,但它是服务于本地方法的,也就是被native关键字修饰的方法,它们不是由java语言实现的,也没有被编码进.class文件

7.程序计数器

线程私有的
Program Counter Register
它的功能和CPU中的pc寄存器很类似,存储的是下一条需要执行的字节码指令

  • 在单线程时,它可以控制程序执行的顺序
  • 多线程时,它可以记录当前线程执行到哪一条指令,方便切换回来时沿着原来的顺序执行

8.直接内存

直接内存其实不属于JVM范畴,但是它是Native方法直接访问的堆外内存

二、对象创建过程

1.分为四步

  • 类的加载
  • 分配内存
  • 初始化零值
  • 设置对象头

这四步走完就是接着

  • init方法初始化

2.类的加载

类加载过程:加载–>连接(验证、准备、解析)–>初始化
Java的类加载机制是懒加载,也就是用到时再去加载,采用的加载机制是著名的双亲委派机制

双亲委派机制

java有三个类加载器:

  • BootstrapClassLoader:启动类加载器,负责加载java.lang.*,并且会启动后面两个类加载器
  • ExtClassLoader:扩展类加载器,负责加载一些扩展的jar包
  • AppClassLoader:系统类加载器,负责加载应用程序的主函数类,我们自己写的类一般通过它加载,classpath下的所有jar包和类

双亲委派机制
先是AppClassLoader–>ExtClassLoader–>BootstrapClassLoder来检查是否已经加载过,然后BootstrapClassLoder–>ExtClassLoader–>AppClassLoader来判断是否是它们可以加载的类,如果都没有就是ClassNotFoundException

双亲委派机制的优缺点

优点:

  • 保证核心源码不会被用户修改
  • 避免类的重复加载

缺点:

  • 被父类加载器加载的类不能调用子类加载器加载的类
  • 只能用一个版本的jdk

打破双亲委派机制

SPI(Service provied interface)打破了双亲委派机制
Tomcat打破了双亲委派机制

类的字节码结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic; //Class ⽂件的标志
u2 minor_version;//Class 的⼩版本号
u2 major_version;//Class 的⼤版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//⽗类
u2 interfaces_count;//接⼝
u2 interfaces[interfaces_count];//⼀个类可以实现多个接⼝
u2 fields_count;//Class ⽂件的字段属性
field_info fields[fields_count];//⼀个类会可以有个字段
u2 methods_count;//Class ⽂件的⽅法数量
method_info methods[methods_count];//⼀个类可以有个多个⽅法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}

3.分配内存

对象所需的内存大小在类加载完后就可以确定,然后为对象在堆的新生代分配空间,依据不同的垃圾回收机制有不同的内存分配方式:指针碰撞、空闲链表

分配方式

  • 指针碰撞:适用于堆内存是规整的,堆的中间有个分隔指针,指针前面是被使用过的内存,后面是未使用的内存,分配内存时只需将指针后移即可,Serial、Parnew垃圾收集器
  • 空闲链表:适用于堆内存是不规整的,虚拟机会维护一个可用内存块的链表,分配时找一块足够大的分配,CMS垃圾收集器

分配内存的线程安全

  • CAS+失败重试: CAS 是乐观锁的⼀种实现⽅式。所谓乐观锁就是,每次不加锁⽽是假设没有冲突⽽去完成某项操作,如果因为冲突失败就重试,直到成功为⽌。虚拟机采⽤ CAS 配上失败重试的⽅式保证更新操作的原⼦性。
  • TLAB: 为每⼀个线程预先在Eden区分配⼀块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已⽤尽时,再采⽤上述的CAS进行内存分配

4.初始化零值

在分配完内存后需要将除对象头的内存空间全部都初始化零值,保证了对象实例字段没有赋初值也可以访问到一个零值

5.设置对象头

包含一些必要信息,比如:类名、GC分代年龄信息等信息

三、对象的访问定位

有句柄和直接指针两种,

1.句柄:

如果使⽤句柄的话,那么Java堆中将会划分出⼀块内存来作为句柄池,reference 中存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃的具体地址信息;

句柄定位对象

2.直接指针:

如果使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象的地址。

直接指针定位对象

四、垃圾回收

1.新生代、老年代、年龄

堆空间划分
如上图所示,将堆空间划分为edens0s1tentired四个区,其中edens0s1都是新生代,tentired是老年代,对象在eden中产生,在经历第一次eden上的垃圾回收后如果对象还存活,就会被移到s0s1,并且年龄加1,之后在s0和s1再每经历一次垃圾回收都会年龄加1,最后当年龄超过阈值(默认设置为15)时会被移动到老年代tentired中。
另外大对象和长期存活的对象会被直接移动到老年代中。

2.Minor GC和Full GC(Major GC)

  • Minor GC:新生代的GC,当eden没有足够内存分配时,就会发起Minor GC,Minor GC速度快,频率高
  • Full GC:老年代的GC,频率低,速度要比Minor GC慢十倍左右

3.判断对象死亡的两种方法

  • 引用计数法:通过计数该对象被多少引用变量引用,如果0引用那么该对象就不会再被使用,就是死亡对象
  • 可达性分析:从一个根节点出发,如果不能遍历到目标对象的引用,那么这个对象也就是不会被使用,就是死亡对象

4.引用类型(强、软、弱、虚)

  • 强引用:一个对象持有强引用,那么JVM即便内存不足了,GC也不能回收该对象
  • 软引用:当一个对象仅持有软引用,那么GC在内存充足的时候不会回收它,而在内存不足的时候回收它
  • 弱引用:当一个对象仅持有弱引用,那么GC在扫描到它时就会直接被回收
  • 虚引用:虚引用影响不了对象的生命周期,但是它必须配合一个引用队列使用,当对象被回收前,要将它的虚引用放入引用队列,因此可以通过判断虚引用是否被加入引用队列来判断对象是否将被回收

5.垃圾回收算法

标记-清除算法

  • 方法:逐个对堆里的对象进行判断,将需要被回收的对象标记,之后直接依次回收掉被标记的对象的内存
  • 特点:简单高效,但是会在堆里留下很多空间碎片

标记-整理算法

  • 方法:和标记-清除算法一样先进行对象标记,但之后不是清除,而是让存活的对象逐个向前移动,将所有存活的对象移到前面一段,最后将剩余的内存清理

对半复制算法

  • 方法:将整个空间分为两半,一半用来分配内存,一半留着待用。当分配内存的那一半快满了,就进行垃圾回收,就把还存活的对象复制到另一半内存中去
  • 特点:高效可用,无碎片空间,但是可用内存空间变为原来的一半

不同的区代使用不同算法

一般会分新生代和老年代:

  • 在新生代对象的产生很频繁,回收也很频繁,因此采用对半复制算法,
  • 在老年代则采用标记-清除或者标记整理算法,因为老年代的对象绝大部分都是存活的,对半复制的话就大大的降低了空间利用率

6.常见垃圾回收器

  • Serial串行单线程收集器:在工作时要将其他所有的线程停掉,并且自己只有一个线程在垃圾回收,简单高效,但是停掉所有线程的用户体验差。新生代使用对半复制算法,老年代用标记-整理算法
  • ParNew串行多线程收集器:Serial的多线程版,同样要停掉其他所有线程,但是自己本身是多个线程并发工作
  • CMS垃圾回收器(Concurrent Mark Sweep):
    它基于标记-清除算法
    1.先暂停一小会用户进程,把所有与root相连的对象记录下来
    2.然后和用户进程并发一起进行,自己进行可达性分析,将所有的可达的对象记录下来
    3.然后暂停用户进行,把第2步中用户进程对一些可达的对象的变化重新记录,然后将要清除的对象全部标记
    4.继续和用户进程并发执行,将被标记的对象逐个回收
    特点:并发收集,低停顿,大量空间碎片,处理不了浮动垃圾
  • G1收集器(Garbage-First):
    1.与CMS的标记-清除不同,G1整体像标记-整理,局部像对半复制。
    2.G1可以预测回收时间,根据允许的回收时间,来选择一个回收价值最大的区域进行回收