导图社区 java虚拟机
java虚拟机相关知识点梳理、1、通过类的全限定名获取定义此类的二进制字节流;2、将二进制字节流所代表的静、态存储结构转化为方法区的运行时数据结构;3、在内存中生成该类的Class对象放入元空间中,作为方法区这些数据的访问入口。
编辑于2023-02-11 21:28:50 河南java后端面试必备的mysql知识点梳理、InnoDB存储引擎是一种兼顾高可靠性和高性能的通用存储引擎,也是MySQL5.5之后默认的存储引擎。
java虚拟机相关知识点梳理、1、通过类的全限定名获取定义此类的二进制字节流;2、将二进制字节流所代表的静、态存储结构转化为方法区的运行时数据结构;3、在内存中生成该类的Class对象放入元空间中,作为方法区这些数据的访问入口。
系统的梳理并发编程相关概念,助力面试!一种温柔的关闭线程池的方式,不会接收新的任务,但在关闭前将之前提交的任务处理完毕、一种粗暴的方式关闭线程池,既不会接收新的任务,也不会处理已经提交的任务,但会返回队列中未处理的任务。
社区模板帮助中心,点此进入>>
java后端面试必备的mysql知识点梳理、InnoDB存储引擎是一种兼顾高可靠性和高性能的通用存储引擎,也是MySQL5.5之后默认的存储引擎。
java虚拟机相关知识点梳理、1、通过类的全限定名获取定义此类的二进制字节流;2、将二进制字节流所代表的静、态存储结构转化为方法区的运行时数据结构;3、在内存中生成该类的Class对象放入元空间中,作为方法区这些数据的访问入口。
系统的梳理并发编程相关概念,助力面试!一种温柔的关闭线程池的方式,不会接收新的任务,但在关闭前将之前提交的任务处理完毕、一种粗暴的方式关闭线程池,既不会接收新的任务,也不会处理已经提交的任务,但会返回队列中未处理的任务。
java虚拟机
类加载过程
加载
1、通过类的全限定名获取定义此类的二进制字节流;2、将二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构;3、在内存中生成该类的Class对象放入元空间中,作为方法区这些数据的访问入口。
连接
验证
目的在于确保class文件中的字节流所包含的信息符合《java虚拟机规范》的全部约束要求,保证类被加载的正确性,不危害虚拟机的安全。(包括文元字符,文件格式验证、元数据验证、字节码验证、符号引用验证)
准备
为类变量分配内存并设置默认初始值,例如int为0,boolean为false
解析
将常量池内的符号引用转换为直接引用
初始化
为类变量初始化为代码中指定的值,执行初始化方法
使用
卸载
该类的所有实例对象都被回收
该类的类加载器被回收
该类的Class对象没有在任何地方被使用
类加载器
定义
在类加载阶段,通过一个类的全限定名来获取定义此类的二进制字节流的这个动作的“代码”称为类加载器
分类
启动类加载器(BootstrapClassLoader)
启动类加载器又称根的类加载器,是由C++语言实现的 负责加载lib目录下的rt.jar、resources.jar、charsets.jar 以及被-Xbootclasspath参数所指定的路径中存放的类库。
由C++实现,是虚拟机自身的一部分
扩展类加载器(ExtensionClassLoader)
负责加载lib\ext目录下面的jar包和类, 或被java.ext.dirs系统变量所指定的路径中所有类库。
应用程序类加载器(APPClassLoader)
应用程序类加载器又称系统类加载器 负责加载当前应用的classpath下的jar包和类。
用户自定义类加载器
java语言实现,独立于虚拟机外部,并且都继承自抽象类ClassLoader
jvm三层类加载器是继承关系吗?
不是继承关系 启动类加载器是由C++写的,和java没有联系。 扩展类加载器、应用程序类加载器以及用户自定义类加载器三者是java写的,三者之间不存在继承关系,但是都有相同的父类,即java.long.ClassLoader这个类。
扩展类加载器(ExtensionClassLoader)
应用程序类加载器(AppClassLoader)
双亲委派机制
1、当一个类加载器收到类加载请求,它不会自己去加载,而是先委托给父类加载器去加载。 2、如果父类加载器还有父类,则会继续向上委托,一直到最顶层的启动类加载器。 3、如果一个父类可以完成类加载任务,也就是在它的搜索范围内能够找到这个类时,就成功返回,如果不能加载,则会委派给子类加载器去加载。如果最底层的类加载器也不能加载这个类时,则会抛出ClassNotFoundException异常。
定义
1、当一个类加载器收到类加载请求,它不会自己去加载,而是先委托给父类加载器去加载。
2、如果父类加载器还有父类,则会继续向上委托,一直到最顶层的启动类加载器。
3、如果一个父类可以完成类加载任务,也就是在它的搜索范围内能够找到这个类时,就成功返回,如果不能加载,则会委派给子类加载器去加载。如果最底层的类加载器也不能加载这个类时,则会抛出ClassNotFoundException异常。
原理图
优点
保证程序的安全,防止核心API被篡改
例如重写String这个类,他是在rt.jar包下面的,能够被启动类加载器加载。根据双亲委派机制,启动类加载器就直接加载String类了,就不会向下委托,因此用户自定义的这个String类就不会被加载了。
避免类被重复加载,一旦一个类被加载,就不会再委托给子类加载器去加载
保证类的唯一性
可以打破双亲委派机制吗?
可以打破, 自定义一个类加载器,重写loadClass()方法,使其不进行双亲委派即可。
如何自定义自己的类加载器?
继承ClassLoader 覆盖loadClass()方法,会打破双亲委派,因为双亲委派的核心代码就在loadClass()方法中。 覆盖findClass()方法,不会打破双亲委派
ClassLoader中的loadClass()、findClass()与defineClass()的区别?
loadClass()主要进行类加载的方法,默认的双亲委派机制模型就在这个方法中实现,因此重写此方法会破坏双亲委派机制
findClass()根据名称或位置加载.class字节码,重写此方法不会破坏双亲委派机制
defineClass()把字节码转化成java.lang.Class对象
加载类的Class.forName()与ClassLoader有什么区别?
Class.forName()得到的class是已经初始化的(执行静态代码块)
ClassLoader.loadClass()得到的是还没有初始化的(没有指定静态代码块)
内存结构划分
线程私有
虚拟机栈
栈是线程运行所需要的内存空间,主要用于存储方法、局部变量、运行数据。 1、【线程私有】线程私有的一块区域 2、【入栈出栈】栈里面的元素是栈帧,每一次方法调用,都会有一个相应的栈帧被压入栈中,方法调用结束,该栈帧就会被弹出。 3、【栈帧】栈帧存储局部变量表、操作数栈、动态链接(执行其他方法)、方法出口信息。 4、【栈溢出】当栈深度大于虚拟机所允许的最大深度时会抛出StackOverFlowError栈溢出错误。 5、【内存不足】当栈需要扩展,但无法获取足够空间时,会抛出OutOfMemoryError 6、【空间大小】栈空间大小默认为1M,可通过-Xss显示设置大小 7、【生命周期】该区域不会被垃圾回收,其生命周期随着线程的创建而创建,随着线程的结束而死亡。
本地方法栈
存储Native方法,与java虚拟机栈类似 在Hotspot虚拟机中,java虚拟机栈与本地方法栈合二为一。也会出现StackOverFlowError和OutOfMemoryError
程序计数器
存储字节码行号指示器 特点: 1、程序计数器是一块非常小的内存空间,几乎可以忽略。 2、它是当前线程所执行的字节码的行号指示器,其中分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成。 3、线程私有的一块内存,每个线程都有一个独立的程序计数器,各个线程之间的计数器互不影响。 4、它是唯一一块不会存在内存溢出OutOfMemoryError的内存区域。 5、不会被垃圾回收,其生命周期随着线程的创建而创建,随着线程的结束而死亡。
线程共享
堆
【存储】存储所有创建的对象、数组 【描述】java虚拟机启动时创建堆,是内存共享的一块区域,也是虚拟机管理内存最大的一块区域。垃圾收集器主要收集这一块区域。 【划分】堆可以划分为新生代、老年代,默认比例为1:2 新生代划分为eden区、s1区、s2区,默认比例为8:1:1 可以通过-Xmx 、-Xms调整堆大小 【异常】当堆空间不足时会发生 OutOfMemoryError:java heap space 【内存分配】从内存分配角度来看,堆为每一个线程都分配一个线程私有的缓冲区,叫作TLAB本地线程分配缓存,就是一个线程为对象分配内存时线程TLAB中进行分配,当TLAB内存不够时再通过CAS配上失败重试的方式从堆中分配内存。 下面的不问就不说: 参数-XX: +UseTLAB表示使用TLAB 参数-XX: -UseTLAB表示不用TLAB 默认使用TLAB 参数-XX: TLABSize来设置大小
方法区(元空间)
与java堆类似,也是线程共享的内存区域。jdk8之后才会出现元空间的概念。 主要存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据 元空间的主要回收废弃的常量和不再使用的类。 元空间采用本地内存,容量仅受到本地内存的限制。也可以通过参数设置元空间内存大小。 -XX:MetaspaceSize初始内存 -XX:MaxMetaspaceSize最大内存
内存分配方式
指针碰撞
如果堆内存是规整的,所有被用过的内存在一边,没有用过的内存在另一边,中间放置一个指针作为它们的分界点,在为新对象分配内存时,只需将指针向空闲那边移动一段与对象大小相等的距离即可分配。 如果采用Serial和ParNew垃圾收集器,可以采用指针碰撞的方式经内存分配。因为它们的垃圾收集算法是压缩整理的,内存是规整的
空闲列表
如果堆内存不是规整的,需要采用空闲列表的方式。 java虚拟机会维护一个列表用于记录哪些内存是可用的,哪些是不可用的,在为对象分配内存时,从空闲列表中找到一块足够大的区域用于内存的分配,并更新列表记录。 当使用CMS垃圾收集器时需要使用空闲列表算法。
TLAB+CAS失败重试
在多线程情况下创建对象会出现线程安全的问题。 例如会出现线程A给对象分配内存时,在指针还未来得及修改,线程B又用相同的指针来分配内存的情况 解决方式: 为每个线程分配一小块内存,称为TLAB,jvm分配内存时首先在TLAB中进行分配。当TLAB内存不足时,再采用CAS+失败重试的方式进行分配。
对象的内存布局
对象头
存储对象自身的运行时数据
哈希码,GC分代年龄、锁状态标志
类型指针
虚拟机通过这个指针来确定它是那个类型的实例
实例数据
对象真正存储的有效信息,也就是在程序中定义的各种类型的字段内容。
对齐填充
不是必要的,主要起到占位作用,保证对象的大小是某个字节的整数倍,例如Hotspot虚拟机中,任何对象大小都是8字节的整数倍。
垃圾回收机制
什么是垃圾回收机制
当程序创建对象、数组等引用类型实例时,系统会在堆中为之分配一块内存区域,对象就保存在内存区域中,当对象不再被任何引用变量引用时,这块内存就变成了垃圾内存,需要进行垃圾回收
哪些内存需要回收?
堆
主要回收废弃的对象
方法区
主要回收废弃的常量和不再使用的类
何时回收?
GC的触发条件
YoungGC触发条件
YoungGC是经常发生的,当Eden区满了之后就会触发一次YoungGC
Full GC触发条件
当老年代没有足够空间存放对象时,触发一次FullGC
当老年代最大可用连续空间小于新生代对象总大小,或当老年代最大可用空间小于历次晋升到老年代对象的平均大小,会触发一次Full GC
当元空间区域的内存达到了所设定的阈值-XX:MetaSpaceSize,也会触发一次Full GC
调用System.gc(),或者Runtime.getRunTime().gc()会显示触发一次Full GC
如何判断废弃的对象?
引用计数法
给对象添加一个计数器,每当有一个地方引用它,计数器加一,当引用失效,计数器减一,当计数器为零,代表该对象没有被引用。优点:方法实现简单,效率高。缺点:很难解决对象之间的相互引用问题。
可达性分析算法
通过一系列称之为“GC Roots”的对象为起点,从这些节点开始向下检索,节点走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象是不可用的,需要回收。
可作为GC Roots的对象
①虚拟机栈(栈帧中的本地变量表)中引用的对象
②本地方法栈中引用的对象
③方法区中类静态属性引用的对象
④方法区中常量引用的对象
⑤所有被同步锁持有的对象
如何判断废弃的常量
没有被任何String对象引用的字符串常量就是废弃的常量。
如何判断不再使用的类
堆中无实例
对应的类加载器被回收
对应的Class对象没有在任何地方被使用
如何回收?
垃圾回收算法
标记-清除
在标记阶段标记出所有需要回收的对象,在清除阶段回收所有标记的对象。 缺点:碎片化严重,标记清除后会产生大量不连续的碎片空间。
缺点是碎片化严重
复制
首先将内存被等分成两半,每次只使用其中的一半,当这一块内存满后将尚存活的对象复制到另一半,再把使用的内存一次清理掉。 缺点: 1、可使用的内存只有原来的一半 2、且存活对象太多的话,复制算法效率会降低(一般新生代使用此算法)。
标记整理
标记阶段仍与“标记-清除”算法一样,后续步骤不是直接对可回收对象回收,而是将所有存活对象向一端移动,然后直接清理掉端边界以外的内存。 优点: 1、不会像复制算法那样需要划分两个区域,提高了空间利用率。 2、不会产生内存碎片。 缺点: 效率不高,移动存活对象并更新所有引用这些对象的引用是非常耗时的一个过程。
分代回收
根据对象的存活周期的不同将对象划分为几个区域,一般将java堆分为新生代和老年代,这样就可以根据每个年代的特点来选择合适的垃圾收集算法。 新生代存活的对象比较少,可以选择标记-复制算法。老年代存活的对象比较多,必须选择标记-清除或标记整理算法来进行垃圾收集。
垃圾回收器
新生代收集器
Serial
新生代收集器,也是单线程收集器,在它进行垃圾收集工作时必须暂停其他所有的工作线程,直到它收集结束。 优点:简单高效。适合于运行在Client模式下的虚拟机。
需要暂停其他线程
ParNew
新生代收集器,是多线程收集器,也是Serial收集器的多线程版本。适合于运行在Server模式下的虚拟机。 Serial与ParNew和CMS配合使用,Serial和Parn收集新生代,CMS收集老年代。 通过-XX:+UseConcMarkSweepGC来指定使用CMS收集器,默认使用ParNew作为新生代垃圾收集器。 -XX:ParallelGCThread 用于指定垃圾收集线程数量,ParNew默认开启与CPU个数相同的数量。
Parallel Scavenge
新生代垃圾收集器,是一种多线程收集器,与ParNew收集器类似。这种收集器侧重于吞吐量,因此这种收集器也叫吞吐量收集器或吞吐量优先的收集器。 这种收集器拥有自适应调节策略,通过参数-XX:UseAdaptiveSizePolicy来设置,默认是开启的。 jdk1.8 默认使用的是 Parallel Scavenge + Parallel Old。
侧重于吞吐量
老年代收集器
Serial Old
Serial收集器的老年代版本,也是单线程收集器,既可在client模式下使用,又可在server模式下使用,采用标记-整理算法。 这种收集器可以作为CMS收集器发生失败后的预案。
Parallel Old
parallel scavenge收集器的老年代版本,多线程收集器,使用标记-整理算法,是一种注重吞吐量的收集器。 jdk8默认情况下,新生代使用parallel scavenge收集器,老年代使用parallel old收集器。
侧重于吞吐量
CMS
关注用户线程的停顿时间。整个过程分为以下四个阶段: 初始标记:仅仅标记一下GC Roots能够直接关联的对象,速度很快,但仍然需要暂停所有工作线程。 并发标记:进行GC Roots的跟踪过程,和用户线程一起工作,不需要暂停工作线程。 重新标记:为了修正在并发标记期间,由于用户线程的继续运行导致标记产生变动的那一部分对象的标记记录,此时仍然需要暂停所有的工作线程。 并发清除:清除GC Roots不可达的对象,和用户线程一起工作,不需要暂停工作线程。
侧重于用户线程停顿时间
缺点
降低吞吐量
在并发阶段,由于占用了一部分线程导致应用程序变慢,降低总吞吐量。
浮动垃圾
无法处理在并发清除阶段用户线程所产生的新垃圾,也就是浮动垃圾。
空间碎片
由于其采用标记清除算法,因此会产生一定的**空间碎片**。
整堆收集器
G1
Garbage First收集器避免全区域垃圾回收,它把堆内存划分为几个固定大小的独立区域,同时在后台维护一个优先级列表,每次在允许的时间内,优先回收垃圾最多的几个区域.
相比于CMS收集器,G1收集器两个最突出的改进是:
基于标记-整理算法,不产生内存碎片;在不牺牲吞吐量的前提下,实现低停顿垃圾回收,并且是一种可预测的停顿。
最新的两个收集器
ZGC
Shenandoah
新生代垃圾回收过程?
由于新生代大部分对象存活年龄比较小,因此新生代采用复制算法 创建的对象首先放在Eden区,当Eden区首次满了之后,把存活的对象放入s1区,然后把Eden区进行回收。 当Eden区又一次满了之后,会把Eden区和s1区中的存活对象放入s2区。再对Eden区和s1区进行回收。以此类推。
为什么要分新生代和老年代
因为有的对象寿命短,有的对象寿命长,将寿命长的对象放在一个区,将寿命短的对象放在另一个区,不同的区选择不同的垃圾收集算法,寿命短的区清理频率高一些,寿命长的区清理频率低一些,这样可以提高垃圾收集效率。
为什么要有survivor区
如果没有Survivor区,当Eden区满了清理垃圾,需要将存活的对象放到老年区,老年区满了会触发Full GC,而Full GC是非常耗时的。只有加入了Survivor区,当Eden区满了将存活对象放在Survivor区,等对象反复清理几遍之后都没有清理掉,再放入老年区,降低了Full GC的执行次数,使得老年区的压力变小。
为什么要有两个survivor区
为了解决内存碎片化的问题,如果只有一个survivor区,当Eden区满了之后将存活对象放入survivor区。这样循环下去,当下一次Eden区满了之后,survivor区不再是空的,如果强制把Eden区的内容放入survivor区,会导致碎片化,而如果使用两个survivor,每次使用Eden区和其中一个survivor来存储对象,当Eden满了之后把存活对象放入另一个survivor区,这样总能保证其中一个survivor是空的,另一个非空的survivor没有内存碎片。
Eden、S1和S2的比例为何是8:1:1
统计和经验表明,90%的对象朝生夕死,存活时间短,每次gc都会有90%左右的对象被回收,剩下的10%要留一个survivor空间去保存。
对象进入老年代的四种情况
(年龄超过15岁)经历15次minor GC,对象年龄达到了15岁,默认值15可以通过参数-XX:MaxTurningThreshhold来设置。
(动态年龄判断)survivor区对象年龄从小到大进行累加,如果累加到某个年龄时占用空间总和超过50%,那么大于这个年龄的对象都会晋升到老年代。默认值50%可以通过参数-XX:TargetSurvivorRatio来设置。
(S区空间不足)在Eden区和s1区向s2区复制对象的过程中,如果s2区满了的话,那么剩余的对象会直接放入老年代。
(大对象直接放入老年代)大对象是指需要大量连续内存空间的对象。例如很长的字符串或很大的数组或list集合。由于大对象容易躲过垃圾回收,并且复制过程开销比较大。因此会把大对象直接放入老年代。
空间分配担保机制
jdk6 update24之前
jdk6 update24之后,不再检查是否允许担保失败
java四种引用类型
强引用
类似于生活必需品。只要引用没事释放,当内存空间不足时,java虚拟机会宁愿OutOfMemoryError错误,也不会回收这种强引用的对象。 Object o = new Object();
软引用
如果内存充足,不会对象不会被回收,内存不足才回收。 软引用可以用来实现内存敏感的高速缓存。,内存充足缓存数据,内存不足就清理掉数据。 Object o = new Object(); SoftReference ref = new SoftReference(o);
弱引用
如果垃圾回收器发现弱引用的对象,不管内存是否充足,都会回收。 Object o = new Object(); WeakReference ref = new WeakReference(o);
虚引用
虚引用并不会决定对象的生命周期,主要用来跟踪对象被垃圾回收的活动。
与内存相关的jvm参数
堆内存
-Xms堆初始内存 -Xmx堆最大内存
新生代内存
-Xmn新生代大小 -XX:NewRatio设置新生代老年代比例 -XX:SurvivorRatio设置Eden区和survivor区比例
晋升老年代年龄阈值
-XX:MaxTenuringThreshold 年龄阈值
方法区
方法区: -XX:PermSize永久代初始内存 -XX:MaxPermSize永久代最大内存 -XX:MetaspaceSize元空间初始内存 -XX:MaxMetaspaceSize元空间最大内存
栈内存
-Xss每个线程的栈内存大小
收集器
-XX:+UseXXXGC使用指定的收集器 -XX:+UseG1GC使用G1收集器