JVM总结
JVM
- 1.JVM总结
- 2.JVM结构
- 3.JVM堆内存
- 4.垃圾收集器和垃圾收集算法
- 5.MinorGC,MajorGC,FullGC和YoungGC在堆中的发生区域
- 6.如何查看jvm内存使用情况
- 7.JVM加载类的机制
- 8.什么时候触发YGC,什么时候触发FGC?
- 9.JVM的双亲委派机制
- 10.哪些情况下对象会进入老年代?
- 11. 为什么GC分代年龄最大为15?
- 12. JVM指令重排
- 12.1 JVM指令重排的概念
- 12.2 JVM指令重排带来的问题
- 12.3 如何解决 多线程环境下的JVM指令重排 带来的问题?
- 13. java的虚引用有什么用?
1.JVM总结
2.JVM结构
1程序计数器(线程私有,无GC,Garbage Collection):指向当前线程正在执行的字节码的地址、行号。
2虚拟机栈(线程私有,无GC):存储当前线程运行方法所需要的数据、指令、返回地址。每一个方法都对应一个栈帧,可以通过配置Xss来配置栈帧的大小。
3本地方法栈(线程私有,无GC):和虚拟机栈类似。不同的是,本地方法栈存储的是本地方法的数据。
4方法区(线程共享):被所有方法线程共享的一块内存区域。用于存储已经被虚拟机加载的类信息、常量、静态变量等。这个区域的内存回收目标主要针对常量池的回收和堆类型的卸载。
5Java 堆(线程共享,要GC):被所有线程共享的一块内存区域,在虚拟机启动的时候创建,用于存放对象实例。一个JVM实例只有一个堆内存。
3.JVM堆内存
- JVM中堆空间可以分成三个大区,新生代、老年代、永久代。
- 新生代可以划分为三个区, Eden区,两个Survivor区(SurvivorFrom、SurvivorTo)。
- Minor GC 的过程:采用复制算法。
首先,把 Eden 和 SurvivorFrom 区域中存活的对象复制到 SurvivorTo 区域(如果有对象的年龄以及达到了老年的标准,一般是 15,则赋值到老年代区)同时把这些对象的年龄 + 1(如果 SurvivorTo 不够位置了就放到老年区)。
然后清空 Eden 和 SurvivorFrom 中的对象;最后,SurvivorTo 和 SurvivorFrom 互换,原 SurvivorTo 成为下一次 GC 时的 SurvivorFrom 区。 - SurvivorTo和SurvivorFrom互换主要有以下原因:
保证内存连续性:新生代通常采用复制算法进行垃圾回收,将Eden区和Survivor From区中存活的对象复制到Survivor To区,然后清空Eden区和Survivor From区。通过这种方式,可以将存活对象集中存放在一个Survivor区,保证内存的连续性,减少内存碎片的产生,提高内存分配的效率2。
实现对象分代:对象在Survivor区中经历多次垃圾回收后,如果仍然存活,其年龄会不断增加。当对象的年龄达到一定阈值(由JVM参数MaxTenuringThreshold决定,默认是15)时,会被晋升到老年代。SurvivorTo和SurvivorFrom互换机制有助于记录对象的年龄,实现对象的分代管理,使得不同生命周期的对象存放在不同的内存区域,提高垃圾回收的效率1。
提高垃圾回收效率:每次垃圾回收时,只需要扫描Survivor From区和Eden区,将存活对象复制到Survivor To区,避免了对整个新生代的扫描,减少了垃圾回收的时间和开销。同时,由于Survivor区相对较小,复制操作的成本也较低,从而提高了垃圾回收的效率2。
4.垃圾收集器和垃圾收集算法
- 1引用计数法(没有被java采⽤):
a. 原理理:对于一个对象A,只要有任何一个对象引⽤用了了A,则A的引用计数器器就加1,当引用失效时,引用计数器器就减1,只要对象A的引⽤用计数器器的值为0,则对象A就会被回收。
b. 问题:
i. 引⽤用和去引⽤用伴随加法和减法,影响性能;
ii. 很难处理理循环引⽤用。 - 2 标记清除法:
a. 原理: 现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记节点,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后在清除阶段,清除所有未被标记的对象。
b. 问题:
i. 标记和清除两个过程效率不高,产生内存碎片导致需要分配较大对象时无法找到足够的连续内存而需要触发一次GC操作。 - 3标记压缩法:
a. 原理: 适合用于存活对象较多的场合,如老代。它在标记-清除算法的基础上做了一些优化。标记阶段一样,但之后,将所有存活对象压缩到内存的一端。之后,清除边界外所有的空间。
b. 优点:
i. 解决了标记- 清除算法导致的内存碎片问题和在存活率较高时复制算法效率低的问题。 - 4.复制算法:
a. 原理:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
b. 问题:
i. 不适用于存活对象⽐较多的场合,如老年代。 - 5分代回收法:
a. 原理:根据对象存活周期的不不同将内存划分为几块,一般是新生代和老年代, 新生代基本采用复制算法,老年代采用标记整理算法。
5.MinorGC,MajorGC,FullGC和YoungGC在堆中的发生区域
- 1.Minor GC通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生GC的频率较高,回收速度比较快,一般采用复制-回收算法。
- 2.Major GC 发⽣在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,所采⽤的是标记-压缩算法。
- Minor GC / YoungGC : 清理年轻代
Major GC : 清理老年代
Full GC : 清理整个堆空间,包括年轻代和永久代
6.如何查看jvm内存使用情况
- 可以使用JDK自带的JConsole、 JVisualVM、 JMap、 JHat等工具,或者使用第三方工具,比如Eclipse Memory Analyzer。
7.JVM加载类的机制
- 1.概念:JVM把描述类的数据文件(字节码)加载到内存,并对数据进行验证、准备、解析以及类初始化,最终形成可以被虚拟机直接使用的java类型(Java.lang.Class对象)。
- 2.类加载过程:
a.加载过程:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时结构。在内存中(方法区)生成这个类的Class对象,作为方法区这个类的访问入口。
b.验证过程:为了确保Class文件的字节流中包含的信息符合当前JVM要求,进行文件格式验证,元数据验证,字节码验证,符号引用验证。
c.准备过程:正式为类属性分配内存并设置类属性初始值的阶段,这些内存都将在方法区中进行分配。(准备阶段,static对象会被设置默认值,static final对象会被赋上给定的值)
d.解析过程:虚拟机将常量池内的符号引用替换为直接引用的过程。
i. 符号引用:字符串,引用对象不一定被加载;
ii. 直接引用:指针或者地址偏移量,引用对象一定在内存中。
e.初始化阶段:类初始化阶段是类加载过程的最后一步,是调用类的构造器过程。
f.使用阶段:
g.卸载阶段:
8.什么时候触发YGC,什么时候触发FGC?
- 1.YGC(Young GC):
a. 概念:对新生代堆进行GC。频率⽐较高,因为大部分对象的存活寿命较短,在新生代里被回收。性能耗费较小。
b. 触发时机: 新生代的Eden区空间不足。 - 2.FGC(Full GC):
a.概念:全堆范围的GC。 默认堆空间使用到达80%(可调整)的时候会触发FGC。以我们生产环境为例,一般比较少会触发FGC,有时10天或一周左右会有一次。
b. 触发时机:
1、 老年代空间不⾜;
2、 永久代空间不足;
3、显示调⽤用System.gc() ;
9.JVM的双亲委派机制
- a.定义:当一个类加载器实例需要加载某个类时,它会在亲自搜索这个类之前,先把这个任务委托给它的父类加载器,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器.首先由最顶层的类加载器BootStrapClassLoader(启动类加载器)试图加载,如果没有加载到,则把任务转交给ExtensionClassLoader(扩展类加载器)试图加载,如果没有加载到,则转交给AppClassLoader(应用类加载器)进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果都没有加载得到的话就抛出ClassNotFoundException异常。如果加载成功就会生成一个Class实例对象在内存中。
- b.双亲委派机制的作用是什么?
1.避免重复加载同一个类。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2.避免自定义的类去替代系统核心类造成安全问题,如String。
10.哪些情况下对象会进入老年代?
- 1.新生代对象每经历一次minorGC,分代年龄就会加1,当年龄达到阀值15岁的时候会直接进入老年代.
- 2.大对象直接进入老年代.
- 3.如果出现大量对象在minorGC后仍然存活的情况时,就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代.
11. 为什么GC分代年龄最大为15?
- 存储方面: 对象的对象头里面有4个bit位来存储GC年龄,二进制的4位 最大值位1111 = 十进制的 15
- 设计方面:
- 参考
12. JVM指令重排
12.1 JVM指令重排的概念
- CPU和编译器为了提升程序执行效率,会按照一定的规则去调整JVM指令的执行顺序从而达到优化效果。
12.2 JVM指令重排带来的问题
- JVM指令重排在单线程环境下是不会影响最终结果的,但是在多线程环境下可能会影响到最终结果。
- 以下 r.score 可能出现的结果为 4,1,0。
- 情况1:线程2 执行完 m2方法,线程1 再执行 m1方法,r.score 等于 4。
- 情况2:线程1 执行完 m1方法,线程2 再执行 m2方法,r.score 等于 1。
- 情况3:m2() 发生了JVM指令重排,把B1和B2代码互换了(B2在B1前面),线程2执行完了m2-B2代码行,接着线程1执行m1-A1 ,m1-A2,最后线程2再执行m2-B1,最终r.score 等于0。
// 线程2 执行此方法 public void m2() { ready = true;// B2 num = 2;// B1 }publicclassMAIN{privateintnum=0;privatebooleanready=false;publicstaticclassStudent{privateintscore;}// 线程1 执行此方法publicvoidm1(Studentr){if(ready){// A1r.score=num+num;// A2}else{r.score=1;// A3}System.out.println("r = "+r.score);}// 线程2 执行此方法publicvoidm2(){num=2;// B1ready=true;// B2}publicstaticvoidmain(String[]args)throwsInterruptedException{MAINmain=newMAIN();Studentstudent=newStudent();CountDownLatchcountDownLatch=newCountDownLatch(1);newThread(()->{try{countDownLatch.await();System.out.println("XC2_主线程放行!!");main.m2();}catch(InterruptedExceptione){e.printStackTrace();}}).start();newThread(()->{try{countDownLatch.await();System.out.println("XC1_主线程放行!!");main.m1(student);}catch(InterruptedExceptione){e.printStackTrace();}}).start();// 主线程休眠3秒TimeUnit.SECONDS.sleep(3);// 计数器减一 让2个线程并发执行countDownLatch.countDown();}}- 使用CountDownLatch 模拟2个线程并发执行,控制台打印可能出现的结果
XC1_主线程放行!!XC2_主线程放行!!r=1========================================XC2_主线程放行!!XC1_主线程放行!!r=4========================================XC2_主线程放行!!XC1_主线程放行!!r=012.3 如何解决 多线程环境下的JVM指令重排 带来的问题?
使用volatile关键字解决:volatile可以局部阻止重排序的发生,对volatile变量的操作指令都不会被重排序。
JVM将内存组织为主内存和工作内存两个部分。
主内存主要包括本地方法区和堆。每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)。
1)所有的成员变量都存储在主内存中(堆中),对于所有线程都是共享的。
2)每条线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
3)线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成。任何被volatile修饰的变量,都不拷贝副本到工作内存,任何修改都及时写在主存。因此对于volatile修饰的变量的修改,所有线程马上就能看到。
13. java的虚引用有什么用?
- 虚引用作用:虚引用 和 引用队列 配合使用, 跟踪对象被GC回收的活动。
- 通过判断引用队列中是否有虚引用来了解被引用对象是否被回收。
// 声明 强引用Studentst=newStudent();// 声明 关联被回收对象的 引用队列 ⇒ 当对象被GC回收后 会进入 引用队列ReferenceQueue<Object>referenceQueue=newReferenceQueue<>();// 声明 虚引用PhantomReference<Object>reference=newPhantomReference<>(st,referenceQueue);st=null;// help GCSystem.gc();// 显式GC,对象st被回收后 会进入引用队列 referenceQueue中System.out.println("referenceQueue = "+referenceQueue.poll());- Student对象被回收后,虚引用 进入了引用队列
