JVM类的加载
1、什么是字节码?采用字节码有什么好处?
Java供虚拟机解释的代码叫做字节码,字节码不面向任何特定的处理器(Windows、Linux等),只面向虚拟机。
每一种平台的解释器不同,但是其虚拟机是相同的,Java源码将源文件编译成字节码文件(.class文件),虚拟机将每一条字节码解释后发送给解释器,然后解释器将其翻译成特定机器对应的机器码,然后在特定的机器上运行。——Java的解释与编译并存。
Java通过字节码的形式,一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点,使Java代码运行更加高效。而且,字节码无需面向任何特定的机器,只面向虚拟机,因此无需重复编译可以在不同的机器上运行。——Java的跨平台。
2、Java类加载器
类加载器的加载过程:
加载——>链接(验证、准备、解析)——>初始化
类加载器的分类:
Java类加载器主要可分为BootStrap Class Loader引导类加载器和用户自定义类加载器:
BootStrap Class Loader引导类加载器:负责将
扩展类加载器(Extension Class Loader):它负责加载
系统类加载器(Application Class Loader):负责加载classpath指定路径下的类库
继承ClassLoader实现自定义类加载器
3、双亲委派机制
底层原理:
一般我们的程序执行时,收到类加载的请求时,首先都是在AppClassLoader类加载器开始进行加载,它不会自己直接进行加载,而是向上委派,请求父类的加载器查找其缓存中是否加载了该类。
如果父类的加载器还是没有在缓存中查找到,则继续向上委派查找缓存,直至顶层的的启动类加载器。
如果父类中的类加载器能够完成该类的加载,则直接进行类的加载;如果父类中无法完成类的加载任务,则向下查找,查找加载路径是否有可以加载该类,直至发起加载请求的类加载器。
总结:
向上委托查找父类加载器中的缓存是否可以加载该类,向下查找为查找加载路径中是否可以加载该类。
好处:
1、安全性,防止用户自己编写的动态类替换Java中的一些核心类,比如String类
2、避免重复加载,JVM区分不同类不仅需要判断类名是否相等,还要判断是否为同个类加载器加载。
JVM堆空间的分配
4、JVM堆空间大小的怎么配置?各区域怎么划分?(JVM调优)
堆空间内存概念:
JVM中堆是所有线程共有的区域,其主要目的是为了存储对象实例。
堆内存按照逻辑主要可以划分为:新生代(Eden区+Survivor0区+Survivor1区)+老年代+永久代三个区域,其内存大小是可以调节的。
堆内存空间大小配置的策略:
依据活跃数据的大小进行堆内存区域划分,通过查看GC日志得到长期存活的对象占用了多大的内存空间(老年代空间的活跃数量)
堆内存总大小一般为活跃数据的3~4倍
新生代区域大小一般为活跃数据的1~1.5倍
老年代区域大小一般为活跃数据的2~3倍
永久代区域大小一般为Full GC后永久代内存的1.2~1.5倍;
5、简述堆空间中对象创建的过程
Step1:判断对象所述类的类加载器是否加载完成(类的加载过程可分为加载、链接、初始化)
step2:为对象分配内存
- 指针碰撞法:通过一个指针作为分界指示器,其中一边是使用过的内存,另一边是空闲的内存,通过指针挪动来分配。
- 虚拟机维护一个空闲列表记录哪些内存可用。
step3:处理对象分配内存时的并发问题
step4:初始化分配的内存,虚拟机将成员变量赋值为零
step:5 设置对象的对象头等信息
step6:执行init()方法进行初始化,此时Java层面的初始化才刚刚开始:初始化成员变量、执行代码块、调用构造器、并把堆内对象的首地址复制给引用变量。
6、Java对象分配内存时如何保证线程安全?
方法1:对象分配内存时采用CAS乐观锁机制,配合失败重试的方式保证更新操作的原子性。该方式效率低。
方法2:采用TLAB分配,每个线程都会预先在Java堆内存中预留一块内存,在给对象分配内存的时候,首先将对象分配到自己“私有”的区域,当这部分内存用完时,再分配新的内存。
注:TLAB时线程独享的,但是只是在“分配”这个动作上是线程独占的,至于在读取、垃圾回收等动作上都是线程共享的。而且在使用上也没有什么区别。
7、对象的内存布局?对象头中包含的信息
对象的内存布局主要可分为对象头、实例数据和对齐填充。
- 1、对象头主要包含:
- MarkWord(存储对象自身的一些运行时数据):主要用于存储一些哈希值、GC分代年龄、锁状态标志位、线程持有的锁、偏向线程ID等信息。
- 类型指针:对象指向其他类的元数据指针。
- (若对象为一个数组,还会记录数组的长度)
- 2、实例数据主要存储一些代码中的所定义的各种类型的字段信息
- 3、对齐填充主要起占位作用
8、JVM堆空间分配对象内存的策略?
1、大多数新生对象分配新生代中,当新生代中没有足够内存时,进行一次Minor GC
2、大对象需要连续的内存空间,一般都是直接分配到老年代进行内存分配
3、 如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄 设置为 1,并且每熬过一次 Minor GC 年龄就加 1 ,不断循环,当增加到一定程度(默认15)就会被晋升到老年代。
4、如果Survivor区域中相同年龄对象数量大于Surbibor区域对象总和大小的一半,则大于该年龄的对象可直接分配至老年代。【动态年龄判断机制】
5、 空间分配担保。MinorGC 前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果 满足则说明这次 Minor GC 确定安全。如果不,JVM会查看HandlePromotionFailure 参数是否允许担保 失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满 足将Minor GC,否则改成一次 FullGC。
9、直接内存的概念
直接内存也称堆外内存,是直接把对象分配到JVM堆内存外部,直接内存是直接向系统申请的内存区域,不是JVM虚拟机运行时数据区的一部分,来源于NIO。
NIO相比于传统的IO模式,直接面向缓冲区,采用非阻塞模式,能够直接与物理内存和应用程序交互进行读写操作。
使用直接内存的好处在于可以很好的提升性能,比如我们向远程发送数据,我们需要把jvm内存中的对象首先复制到系统中的内存,然后再进行发送,现在我们可以通过将对象直接分配到直接内存,然后直接发送就可以。
10、通过Jvm简述String Table(字符串常量池)及String不可变性的底层原理?
1)new String的过程:以String s1=new String(“abc”)
1、首先new String 会在堆空间中开辟一个空间s1
2、堆空间中保存的不是字符串abc,字符串abc是保存在字符串常量池中的,堆空间保存的是指向abc的引用地址。
3、当s1=”abcd”时,会在堆空间中另外开辟一个空间,此时保存的是指向abcd对象的引用地址。
2)String table字符串常量池
主要用来存放字符串常量,jdk1.7之前String table是存放在方法区中,但是存放在方法区中默认的内存太小,回收效率比较低。jdk1.8之后String table存放在堆空间中。
String table本质是一个hash表,保证其中存放的字符串常量不会重复,我们可以调整StringTableSize来对其进行调优。
3)采用intern()方法将字符串放入常量池:
首先判断String table中是否已经存在该字符串的常量,如果存在,则直接返回对象的地址;若不存在,将对象的引用地址复制一份,将字符串放入到字符串池中,并返回引用地址。
JVM垃圾回收机制
Jvm堆内存的垃圾回收机制的主要流程为:
1、首先根据可达性分析算法判断对象是否需要进行回收;
2、进行二次筛选(当对象没有覆盖finalize()方法或者已经调用过finalize()方法将会被进行垃圾回收)
3、选择合适的垃圾回收算法进行垃圾回收
10、 如何判断对象是否需要回收
方法1:引用计数器算法
对每个对象都保存一个整数型的计数器属性,只要该对象被外部引用,计数器的值就+1,当引用失效后,计数器的值就减1,若计数器的值为0时,则对该对象进行垃圾回收。
优点:实现简单,且易于判断
缺点:需要额外开辟一个字段存储计数器,而且当两个对象之间循环引用时,将不会被回收。
方法2:可达性分析算法
step1:选取一个GC Root作为根节点集合(GC Root可以是虚拟机栈的局部遍历、引用类型的静态变量、方法区常量引用的对象、持有syn同步锁的对象等)
step2:以GC Root根节点开始,自上至下开始搜索被根对象引用的对象是否可达
step3:如果内存中的对象可以直接或者间接被根对象集合所连接,那么该对象就不需要被回收,我们称这一条搜索路径为引用链。
step4:如果内存中的对象没有任何引用链,那么意味着该对象将被垃圾回收。
11、简述三种基本垃圾回收算法
1、标记-清除算法
原理:当堆空间中的内存快要耗尽时,需要进行垃圾回收,标记所有被引用的对象,标记完成后,统一清除所有无标记(无引用)的对象。<br /> 特点:效率不高、清除时需要停止整个应用系统(STW)、容易造成“内存碎片化”,即再分配新的对象时无法分配连续的大对象。
2、复制算法
原理:将当前区域的内存空间一分为二,分成A和B区域,利用可达性分析算法判断存活的对象后,将所有存活的对象复制到区域B中,然后统一清除正在使用的A区域的所有对象,最后交换两个内存的角色,完成基于复制算法的垃圾回收。
特点:效率较高,但是需要2倍的内存空间,内存占用和时间的开销比较大,适合存活对象比较少的区域(新生代)
3、标记-整理算法
原理:在标记-清除算法的基础上,当堆空间需要进行垃圾回收时,首先标记所有被引用的对象,标记完成后将所有存活的对象压缩到内存的一端,然后统一清除边界外所有没有标记(无引用)的对象。
特点:避免了“内存碎片化”的产生,但效率略低于复制算法
4、分代回收算法
基于JVM堆空间中各个内存区域的分配,不同区域一般采用不同的垃圾回收算法,如新生代由于存活对象比较少,一般采用复制算法进行垃圾回收;老年代存活的对象比较多,且额外分配的内存比较少,一般采用标记-清除/整理的算法进行垃圾回收。
12、简述JVM垃圾回收的相关概念
内存溢出:
应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。
产生的原因一般是内存中加载的数据过多、设置的内存大小过小、集合类中有对对象的引用,使用完后未清空,使得JVM不能回收。
解决方法:适当设置堆内存的内存大小、检查错误日志、对代码进行分析查找可能存在OOM的位置。(重点检查数据库中是否一次性读取了大量的数据文件、检查代码中是否存在死循环)
内存泄漏:
堆内存中的对象不再被其他对象引用时,但又无法被GC所回收的情况。(比如说缓存的应用)
13、简述JVM中对象的引用类型
强引用
应用程序中比较普遍的一种引用,只要强引用关系存在,引用的对象就不会被GC所回收(一般我们new 对象就是为强引用)
软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。一般采用 SoftReference 类来创建软引用。
弱引用
垃圾收集器碰到即回收,也就是说它只能存活到下一次垃圾回收发生之前。一般采用 WeakReference 类来创建弱引用。
虚引用
无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必 须与引用队列联合使用。
14、简述几种垃圾回收器的性能指标
1、吞吐量:用户执行代码的时间/用户代码执行的时间+内存回收所需的时间。
2、垃圾收集开销:垃圾收集的时间/用户代码执行的时间+内存回收所需的时间。
3、暂停时间:进行垃圾回收收集时,需要暂停应用程序(STW)的时间。
4、内存占用:堆区所占的内存大小。
15、简述几种垃圾回收器
串行垃圾回收器 Serial、Serial old回收器
单线串行垃圾回收器Serial,进行垃圾回收时需要进行STW,一般新生代Serial垃圾回收器采用复制算法,老年代Serial old垃圾回收器采用标记-整理算法,收集完之后,用户线程继续开始执行。简单高效。
并行垃圾回收器 ParaNew 、Parallel Scavenge回收器
1)ParaNew:多线程并行垃圾回收器,多线程下新生代采用ParaNew垃圾回收器,采用复制算法,多线程并行老年代区域采用Paraller old垃圾回收器采用标记-整理算法进行工作,Para垃圾回收器进行垃圾回收时需要对应用程序进行STW;
2)Parallel Scavenge :多线程并行垃圾回收器,多线程新生代采用Parallel Scavenge垃圾回收器,其与 ParNew 的不同之处是ParNew 的目标是尽可能缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge 的目标是达到一个可控制的吞吐量。
并发垃圾回收器 CMS、 G1
1)多线程并发垃圾回收器CMS是一款以尽可能缩短垃圾收集时间为目标的回收器,提高用户体验。CMS垃圾回收器采用标记-整理算法,其收集机制为:
初始标记阶段:标记所有与GC Root 直接引用的对象,涉及STW
并发标记阶段:进行GC Root Tracing,遍历所有的对象,找出需要被清除的对象,不涉及STW,但耗时较长
重新标记阶段:修正并发标记阶段由于应用程序运行时而导致标记变化对象的记录,涉及STW
标记清除阶段:基于标记-清除算法完成对对象的垃圾回收,耗时较长。
2)缺点:对CPU资源敏感,由于并发标记阶段不涉及STW,该阶段仍有应用程序再执行,还会产生新的垃圾,而CMS无法在该阶段对新的垃圾进行回收清除、由于CMS是并发执行的,需要额外的内存供收集阶段应用程序的运行。、造成内存碎片化
3)多线程并发垃圾回收器G1是一款在延迟可控的情况下尽可能获得高吞吐量(尽可能缩短停顿时间)的垃圾回收器。G1垃圾回收器面向整个堆内存,将内存分为一个个大小相等的区域Region,在每个 Region 中,都有一个 Remembered Set 来实时记录该区域内的引用类型数据与其他区域数据的引用关系,这样就可以参考这些引用关系来判断对象是否需要进行清除。
G1 收集器可以 “ 建立可预测的停顿时间模型 ”,它维护了一个列表用于记录每个 Region 回收的价值大小(回收后获得的空间大小以及回收所需时间的经验值),这样可以保证 G1 收集器在有限的时间内可以获得最大的回收效率。
4)G1垃圾回收器的回收机制,在CMS回收的基础上建立可预测停顿时间模型
初始标记阶段:标记所有与GC Root 直接引用的对象,涉及STW
并发标记阶段:进行GC Root Tracing,遍历所有的对象,找出需要被清除的对象,不涉及STW,但耗时较长
重新标记阶段:修正并发标记阶段由于应用程序运行时而导致标记变化对象的记录,涉及STW
最终筛选清除阶段:筛选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是 Garbage First 的由来——第一时间清理垃圾最多的区块)
16 Java常见的GC调优策略
调优原则:
多数的 Java 应用不需要在服务器上进行 GC 优化;
多数导致 GC 问题的 Java 应用,都不是因为我们参数设置错误,而是代码问题;
在应用上线之前,先考虑将机器的 JVM 参数设置到最优(最适合);
减少创建对象的数量;
减少使用全局变量和大对象;
GC 优化是到最后不得已才采用的手段;
在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多。
调优的目的:
尽可能减少GC执行的时间
将转移到老年代的对象数量降低到最小
调优的方法;
1、由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代,最大限度降低新对象直接进入老年代的情况
2、大对象进入老年代,大对象如果首次在新生代分配可能会出现空间不足导致很多年龄不够的小对象被分配的老年代,破坏新生代的对象结构,可能会出现频繁的 full gc。
3、合理设置进入老年代对象的年龄,利用-XX:MaxTenuringThreshold设置进入老年代的年龄大小
4、设置稳定的堆大小,-xms和-xmx设置对的最小空间和最大空间
5、满足以下指标,一般不需要进行GC调优
MinorGC 执行时间不到50ms;
Minor GC 执行不频繁,约10秒一次;
Full GC 执行时间不到1s;
Full GC 执行频率不算频繁,不低于10分钟1次。
17、获取GC信息一些指令
- -XX:+PrintGC(-verbose:gc):输出打印简要的 GC 信息,包括 GC 前的堆栈情况和 GC 后的堆栈大小和堆栈的总大小
- -XX:+PrintGCDetails:输出打印详细的 GC 信息,不仅包括基本信息,还给出了新生代、老年代和永久区各自的 GC 信息
- -XX:+PrintGCTimeStamps:额外输出 GC 发生的时间,可以推断出 GC 的频率和间隔
- -XX:+PrintTenuringDistribution -XX:MaxTenuringThreshold=18:查看新生代晋升老年代的实际阀值(- XX:+PrintTenuringDistribution),设置的最大年龄为18( -XX:MaxTenuringThreshold=18)
- -XX:+PrintHeapAtGC:每次 GC 时都会打印堆的详细使用情况,输出量很巨大。它分为两个部分:GC 前的堆信息和 GC 后的堆信息,这里包含了新生代、老年代和永久区的使用大小和使用率,还包括了新生代中 eden 区和 survivor 区的使用情况
- -XX:+PrintGCApplicationStoppedTime:应用程序在 GC 发生时的停顿时间
- -XX:+PrintGCApplicationConcurrentTime:应用程序在 GC 停顿期间的执行时间
- -Xloggc:C:/gc.log:将 GC 日志信息输出到具体位置的文件中,便于日后日志分析
注意:详细的 GC 信息是进行 JVM 调优的重要参考信息,可以根据 GC 日志,设置合理的堆大小及相关垃圾回收器的参数
[
](https://blog.csdn.net/qq_38780765/article/details/105868953)