三、JVM整体结构脉络知识点梳理(一)

1、JVM内存模型

(1)、理论知识点

  • 线程共享:垃圾回收机制的场所只发生在线程共享区域(大部分发生在堆上),即生命周期与Java进程相同。
    • 方法区(非堆,内存可调整,回收主要针对常量池和类的卸载,不会走垃圾回收算法,也会垃圾回收)
      • 静态变量
      • 即时编译后的代码(不包含方法的出参入参)
      • 元数据/类的描述文件(类的版本、字段、方法、接口等相关信息描述)
      • 常量,运行时常量池,存储编译器产生的字符串常量和符号引用(类被加载的信息)
      • string,类对象的属性值,当对象实例在堆中分配好以后,需要在栈中保存一个对象的引用指针。堆处于物理不连续的内存空间中,只要逻辑上连续即可。
  • 非线程共享:当线程终结时,所占的内存空间也会被释放掉,即生命周期与所属线程相同。
    • 程序计数器
    • JVM栈
      • 操作码,方法内部编译后的代码(包含方法的出入参)
      • 操作数,方法内的局部变量(boolean、byte、char、short、int、float、long、double、对象的引用指针),局部变量表所需要的内存空间在编译期间完成分配,在方法运行之前,内存空间是固定的,运行期间也不会改变。
    • 本地方法栈
      • 与Java栈类似,主要提供native方法服务。

(2)、什么是实例什么是对象?

  • Class a = new Class();a是实例是引用保存在栈上占4字节,new 出来的Class是对象保存在堆上。

(3)、堆栈的理解?

  • 栈中的数据和堆中的数据销毁并不同步。方法一旦结束,栈中的局部变量立即销毁,但是堆的对象不一定销毁,因为可能有其他变量也在使用这个对象,直到所有在线程中(栈)都不引用这个对象,则才会被垃圾回收机制销毁。
  • 堆中对象可以全局访问,栈内存空间属于线程私有。
  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出栈溢出;堆内存不够用抛出内存溢出。

(4)、方法区和栈都能存放类的方法信息,有什么联系?

  • 类的方法是一个类所有对象共享的,只有一套,保存在方法区,当被对象使用的时候才会被压入栈。

(5)、方法区和永久代的区别?

  • 方法区是jvm中的一个概念,永久代是方法区的一个具体的实现(jdk1.6之前)

(6)、jdk1.8之后,用元空间取代了永久代,为什么这样做?

  • 永久代,存的是常量、静态变量、即时编译后的代码
  • 元空间以后,常量、静态变量存在堆里,元空间只存即时编译后的代码(元数据)
  • 这样做极大的避免了方法区内存溢出,现在改为元空间,就存在本地机器的内存上,故可以根据机器性能指定元空间的大小。

(7)、对象一定分配在堆上吗?

  • 不一定,也可能分配在栈上。

(8)、什么是逃逸分析?

  • 一种算法,如果对象没有被其他线程引用,即没有逃逸,就在栈上分配;如果对象被其他线程引用了,即逃逸了,就在堆上分配。
  • 怎么判断对象逃逸?看这个对象有没有被其他线程所引用,被引用就逃逸。

2、内存分配

image-20210219111519502.png

(1)、创建对象有哪些方式?

  • new对象
  • 反射

(2)、为类创建对象的过程?

  1. 类加载检查:jvm收到new的指令时,检查类的元数据是否在常量池中,检查该类是否已经被加载、解析初始化过,若没有则需要执行类加载
  2. 为对象分配内存:
    • 通过逃逸分析,确定对象是在堆上分配还是在栈上分配(逃逸分析默认是不开启的)
      • 栈上分配:说明该对象只会被一个线程访问,没有并发问题
      • 堆上分配:JVM在新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer),默认占Eden的1%。由于不是线程共享的,所以小对象分配在这里比较好。
        1. 如果TLAB可以放下该对象,则直接分配,否则2
        2. 在Eden区加锁,若可以放下该对象,则分配,否则3
        3. 执行Young GC,再尝试在Eden上分配,若可以放下该对象,则分配,否则4
        4. 直接分配到老年代
    • 总结:对象不在堆上分配的主要原因是堆事共享的,需要考虑并发问题,有一定的开销,TLAB分配和栈分配是线程私有的,避免了资源的竞争,但是会有数据间可见性的问题(空间换效率)。
      image-20210219141019216.png
  3. 将内存空间初始化为零值:int、boolean等基本数据结构赋初始值
  4. 对对象进行必要的设置:在对象头中设置改对象属于那个类的实例

(3)、垃圾回收算法

  • 垃圾回收器是标记压缩、复制算法:堆是规整的,采用指针碰撞的方式分配内存
  • 垃圾回收器是标记清理算法的:堆是不规整的,采用空闲列表的方式分配内存

3、对象头(Mark Word)

(1)、对象在内存怎么存储的?存储区域有哪些?

  • 对象头
  • 实例数据
  • 对其填充,8的倍数

(2)、对象头的结构?

1.32位操作系统

image-20210219142908892.png

  • 25Bits对象的hashcode
  • 4Bits对象分代年龄
  • 2Bits锁标志
  • 1Bits标记是否是偏向锁,默认值为0
2.64位操作系统
  • 无锁:25bit(没有用到) + 31bit(hashcode) + 1bit(cms_free) + 4bit(分代年龄) + 1bit(偏向锁) + 2bit(锁标识位)
  • 偏向锁:54bit(线程id) + 2bit(Epoch时间戳版本的意思) + 1bit(cms_free) + + 4bit(分代年龄) + 1bit(偏向锁) + 2bit(锁标识位)

(3)、synchronized锁住的是代码还是对象?

  • 因为对象头上有锁标志位,故锁的是对象

(4)、synchronized锁状态(只能升级)

  • 无锁:代码不加锁
  • 偏向锁:
    • 参考别人的解释:
      • 当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID
      • 因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致
      • 如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;
      • 如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活
      • 如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;
      • 如果存活,那么立刻查找该线程(线程1)的栈帧信息
      • 如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁
      • 如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
    • 整体过程:
      • 当线程访问同步块获取锁时过程中,会更新锁对象对象头信息,线程栈会记录锁信息
        • 怎么生成的?对象头又一个ThreadId字段,若字段是空,当线程第一次获取到锁的时候就将当前线程的ThreadId写到这个字段内,是否偏向锁状态设置为1。
      • 当线程尝试获取锁时,会检查锁对象的对象头锁状态
      • 若是无锁状态,则获取到锁并且在锁对象的对象头记录线程id和偏向锁标志;
      • 若是偏向锁状态,则对比对象头记录的线程id与当前线程的id是否一致,
      • 若一致则成功获取到锁;
      • 若不一致,检查锁对象记录的线程id是否存活,
      • 若不存活,则标记锁对象状态为无锁状态,其他线程(当前线程)可以获取到偏向锁;
      • 若存活,则根据锁对象的线程id检查该线程是否需要继续持有该锁对象
      • 若无需持有则标记锁对象为无锁状态,其他线程(当前线程)可以获取到偏向锁
      • 若继续持有则暂停锁对象的线程,撤销偏向锁,升级为轻量级锁。
    • 有哪些作用呢?下一次获取锁的时候,之间检查该对象头中的ThreadId和当前线程的ThreadId是否一致,若一致说明已经获取到锁。这样就略过了轻量级锁和重量级锁的加锁阶段,提高了效率。
  • 轻量级锁:CAS+失败重试,失败过多升级为重量级锁
    • 参考别人的解释:
      • 线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;
      • 如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。
      • 但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。
      • 重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
      • 注意:为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是 锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
  • 重量级锁:我们常用的那种锁。

(5)、Integer对象的大小是int的几倍?

  • Int 4字节
  • Integer共计16字节:4字节数据+4字节markword+4字节指针+4字节对齐补充(8的倍数)

4、JVM整体结构

image-20210219170213516.png

5、类加载

(1)、类加载机制

image-20210219170959208.png

1、加载:
  • 描述:把类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,创建一个java.lang.Class对象,用于封装类在方法区内的数据结构。
  • 补充:
    • 通过类的全限定名(包名+类名)来获取定义此类的二进制字节流(Class文件)。获取方式(jar、war、网络中抓取)
    • 将字节流中的数据转化为方法区运行时数据结构,保存在方法区中。

2、连接
  • 验证:加载的类在方法区的运行结构是否正确,是否符合虚拟机要求,确保不会有安全问题。
  • 准备:
    • 静态变量赋初始值,例如 代码中写到static int a = 100,这里a=0
    • 静态常量赋值,例如 代码中写到 static final int b = 100,这里b=100
    • 对于成员变量是在类实例化的时候,随对象一起分配在堆中。
  • 解析:将类的二进制数据中的符号引用换为直接引用。

3、类的初始化
  • 静态变量赋值,例如上一步的a=0,这里a=100

4、小总结
  • 使用new字节码指令创建类的实例,调用一个static静态方法的时候
  • 反射调用类的时候
  • 初始化一个类的时候,会先初始化他的父类,若发现其父类没有初始化,则初始化父类。对于静态字段,只有定义这个字段的类才会被初始化,因此,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
  • 虚拟机启动时,需要产生一个主类(包含main()方法的类)
class A{
	static{
		sout("init")
	}
	public static final String P = "123";
}

public class B{
	psvm(){
		sout(A.P);
	}
}

输出结果:123
结论:调用常量不需要初始化
原因:静态常量在类连接的时候就已经赋值了,所以在B中不会触发A的初始化操作。

(2)、双亲委派模型

image-20210220143553246.png

1、什么是双亲委派?
  • 类在加载的时候,需要通过类的加载器完成,分别是自定义类,应用类加载器,拓展类加载器,引导类加载器,当JVM需要加载一个类的时候,会委托给父加载器去做,然后父类加载器会在自己的加载路径中搜索目标类,若找不到才会还给子类去加载。我们平时写的代码都属于应用类(不属于jdk自带的)。

2、java为什么用双亲委派?双亲委派的意义?
  • 安全性,保护jdk提供的一些核心类库布被覆盖
  • 避免类重复加载

3、怎么破坏双亲委派?
  • 线程上下文类加载器Thread.currentThread().getContextClassLoader()
  • 把loadClass重写掉(只重写findClass不会破坏双亲委派)

4、为什么要破坏双亲委派?
  • java中存在很多服务提供者接口(SPI),这些接口需要第三方为它提供实现,如JDBC、JNDI等,这些SPI接口属于java核心类库,存在rt.jar中,由根加载器加载,由于根加载器无法直接加载SPI的实现类,所以需要线程上下文类加载器去做这件事情。

image-20210220180957756.png

5、如果我重写了系统的jar包,例如rt.jar,jvm还能加载成功吗?
  • 不能,安全校验过不去

6、如果我有两个A.class,那么加载到内存中两个class一定是相同的吗?
  • 不一定,还得考虑加载他们的加载器是否相同,若不通,则两个class是不同的(不同类加载器加载出来的对象是不同的)

7、为什么用自定义类加载器?自定义加载器可以做什么?
  • 加密:java代码可以反编译,可以把自己的代码进行加密以防止反编译,可以将编译后的代码加密,类加密后就不能再用java的classLoader去加载了,这时就需要自定义ClassLoader在加载类的时候,先解密再加载。
  • 从非标准的来源加载代码:UDF
  • 处理网络传输的加密后的java字节码

8、java提供JDBC接口,那么其SPI是怎么加载的?
  • java有DriverManager类,其中有个方法loadInitialDrivers,通过ServiceLoader.load(Driver.class)加载外部实现的驱动类,ServiceLoader会读取取mysql jar包的META_INF文件内容加载,类似于SpringBootSPI的方式
  • 核心包SPI类对外部实现类的加载都是基于线程上下文类加载起执行的,通过这种方式实现了Java核心代码内部去调用外部实现类。

9、什么是ContextClassLoader,哪来的?为什么可以在任何时候从当前线程中取出?
  • jvm加载的时候,会讲AppClassLoader设置到当前线程中,把该加载器设置为全局的,这样在任何时候需要用到ContextClassLoader时都可以从当前线程中获取到
  • ContextClassLoader只用于加载java的SPI接口实现类库。

(3)、tomcat类加载器结构

1、tomcat为什么要破坏双亲委派?
  • 保证部署在同一个tomcat web容器下的多个应用之间所使用的一些java类库相互隔离。
  • 也要保证部署在同一个tomcat web容器下的多个应用所使用的一些java类库可以共享,避免jvm虚拟机方法区过度膨胀。
  • 容器本身也有自己的核心库,tomcat web容器也需要把自身类库单独放在一个地方保护起来。

2、Web应用类加载起默认的加载顺序是?
  1. 先从缓存中加载
  2. 若无,从JVM的Bootstrap类加载器加载
  3. 若无,从当前类加载器加载(WEB-INF/classes、WEB-INF/lib的顺序)
  4. 若无,从父类加载起加载,顺序是,AppClassLoader、Common、Shared

6、小总结小技巧

(1)、怎么看GC日志

  • YoungGC(MinorGC)

image-20210220110831758.png

  • FullGC(MajorGC)

image-20210220111256122.png

(2)、GC是什么?

  • 分代收集算法
    • 次数上频繁收集新生代
    • 次数上较少收集老年代
    • 基本不动永久代/元空间

(3)、为什么FullGC比YoungGC慢很多?

  • FullGC发生在老年代,老年代空间比新生代要大(2:1)

(4)、垃圾回收器,以及对应的有哪些算法?

1.CMS

1>.引用计数法
  • 算法描述:当有一个对象引用我时,会在计数器上+1,当计数器为0是,说明没被对象引用,故需要清理
  • 缺点:每次对对象赋值时需要维护引用计数器,较难处理循环引用(持有并等待,哲学家思考问题)

2>.复制算法
  • 算法描述:年轻代使用,from区(包含eden)复制到to区。因为在该区域,对象存活率低,所以比较适合用,若存活率比较高,则不适合使用,当达到60%会直接放到老年代。
  • 原理思想:将内存分为两块,每次只用其中的一块,当这一块内存用完就开始垃圾回收,并且把还存活的数据复制到另一块空的内存上面。
  • 优点:不会产生内存碎片
  • 缺点:耗空间,空间没有充分利用

3>.标记清除
  • 算法描述:老年代使用,先标记要回收的对象,然后统一回收这些对象。
  • 原理思想:先埋点,后清除
  • 优点:节约空间,充分利用空间
  • 缺点:产生内存碎片,需要先标记后才能清除,速度上会慢点,效率低。

4>.标记压缩
  • 算法描述:先标记,在清除,后压缩整理
  • 原理思想:在标记清除的基础上在整理下碎片。
  • 优点:节约空间,没有碎片
  • 缺点:耗时比较长。

2.G1垃圾收集算法

https://github.com/doocs/jvm
https://docs.oracle.com/javase/8/docs/technotes/tools/#troubleshoot
https://github.com/vipcolud/monitor
[https://github.com/yikebocai/blog/issues/32](

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

showluu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值