本人渣硕,经过半年的秋招折磨,终于收获了比较满意的offer,整理部分面经(涉及Java、JVM、高并发、数据库、计网等,会陆续发送)特此回馈牛友,拒绝白嫖,也希望广大牛友收到满意的offer,以下是整理的JVM的资料(主要是根据尚硅谷宋红康老师视频总结及部分面经),如有错误,恳请指正,第一次发帖,各位大佬多指教……
一、JVM概述
0.JVM作用:
①:将.class字节码文件生成为操作系统指令,传递给操作系统后通过计算机运行。
1.jvm的位置
2.Jvm的整体结构(HotSpot虚拟机)
3.Java代码的执行流程
①:首先java程序经过前端编译器生成.class字节码文件;
②:虚拟机将字节码文件生成为对应的操作系统指令
③:操作系统处理成计算机能够运行的指令
4.jvm的运行周期
①启动:通过类引导加载器创建一个初始类来完成;
②执行:程序开始执行的时候,Jvm启动,程序执行结束的时候,Jvm就停止;
执行一个java程序的时候,真真正正执行的是一个java虚拟机的进程
③停止:
a.程序正常执行结束;
b.程序因为异常终止;
c.程序因为操作系统的异常而终止;
d.自己调用了System类中的exit()方法使程序退出;
二、类加载子系统(☆)
1.作用:
将.class文件加载到内存,并对数据进行检验、解析及初始化,最终形成虚拟机能够直接使用的java类型,加载的类信息存放在方法区中。
注:只负责加载,能不能运行,由执行引擎决定。
2.类加载分为哪些过程:
①加载
②连接:验证、准备及解析
③初始化
3.类加载各阶段的作用:
①加载:
a.通过一个类的全限定名来获取此类的二进制字节流;
b.将该二进制字节流所代表的的静态存储结构转化为方法区的运行时数据结构;
c.在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的数据访问入口。
②连接:
a.验证:确保Class文件的字节流(二进制)中包含的信息符合规范,保证这些信息被当做代码运行后不会对虚拟机造成危害。分为:文件格式验证、元数据验证、字节码验证。符号引用验证四个方面。
b.准备:正式为类的静态变量分配内存并设置初始值(隐式初始化)。
特例:被final修饰的静态变量在准备阶段会被显式初始化。
c.解析:Java虚拟机将常量池中的符号引用替换为直接引用。分为:类或接口的解析、字段解析及方法解析、接口方法解析
如图所示:中间部分即为符号引用;//后面即为直接引用。
③初始化:在准备阶段,静态变量已经赋过一次系统要求的初始零值,而在初始化阶段,会根据程序员通过程序编码指定的主观计划去初始化静态变量和其他资源。
4.简单描述一下<clinit>()方法?
①定义:<clinit>()方法是由编译器自动收集类中的所有的静态变量的赋值和静态代码块语句合并产生的,没有的话则不会产生。
②顺序:编译器收集的顺序是由语句在源文件中出现的顺序决定,静态语句块中只能访问到定义在静态语句块之前的静态变量。
③其他:
a.java虚拟机会保证在子类的<clinit>()执行前,父类的<clinit>()方法已经执行完毕。
b.接口中不能使用静态代码块,但是仍然有赋值初始化操作,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。
c.Java虚拟机必须保证一个类的<clinit>()方法在多线程的环境中被同步加锁。如果多个线程同时去初始化一个类,必须只能其中一个去执行这个类<clinit>()方法。
5.类加载器是什么?
在类的加载阶段中“通过一个类的全限定名来获取类的二进制字节流”实现这个动作的代码称为类加载器。
6.类加载器的分类:
①启动类加载器(引导类加载器)
②扩展类加载器
③应有程序类加载器(系统类加载器)
④自定义类加载器
7.双亲委派模型
1.双亲委派模型的工作过程:
①:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委托给父类的加载器去执行;
②:如果父类加载器还存在父类加载器,则会继续向上委托,请求最终达到顶层的引导类加载器;
③:如果父类引导器可以完成加载任务,则成功返回;否则子加载器会尝试自己执行,这就是双亲委派机制。
2.双签委派模型的优势:
①:避免类的重复加载;
②:沙箱安全机制:保护程序安全,防止核心类库被随意更改(举例:自定义一个java.lang.String类)
8.破坏双亲委派模型
①第一次“破坏”
双亲委派机制是在jdk1.2时出现的,因此在jdk1.2之前不满足双亲委派机制,即java.lang.ClassLoader抽象类,用户编写子类,然后能够重写loadClass()方法,这样就破坏了双亲委派机制;
解决:在ClassLoader中添加了protected的findClass()方法,引导用户重写loadClass()方法时,尽可能重新给这个方法,而不是在loadClass()方法中进行修改。
②第二次“破坏”
缺陷:越基础的类由越上层的类加载器进行加载,但是越基础的类型又需要去调用用户的代码,用户的代码一般由系统类加载器进行加载,这样引导类加载器无法调。
解决:线程上下文类加载器,启动类加载器想要调用我们的用户代码时,首先委托给线程上下文类加载器,然后线程上下文类加载器再去调用用户的程序代码。
三、运行时数据区(☆)
1.概述:
运行时数据区的结构
其中:方法区和堆是线程共享的;程序计数器和本地方法栈及虚拟机栈都是每个线程一份
2.程序计数器
2.1程序计数器作用:用于存储下一条字节码指令的地址,执行引擎执行完当前指令,根据程序计数器的地址执行下一条指令。
2.2程序技术器的特点?
①线程私有:每个线程都有自己的程序计数器;
②当前方法:任何时间一个线程都只有一个方法在执行,程序计数器会存储线程当前执行方法的Jvm指令的地址。
③程序计数器的特点:唯一一个没有OOM的内存区域。
2.3为什么需要使用程序计数器来记录当前线程的地址?
①:CPU需要不断切换不同的线程,切换回来需要知道从哪条指令开始。
②:字节码解释器需要知道下一条执行的执行地址。
2.4为什么要将程序计数器设计为线程私有的?
为了能够准确记录各个线程的当前正在执行的指令地址,最好的办法就是为每一个线程设置一个程序计数器
3.Java虚拟机栈
3.1内存中的堆和栈
栈解决程序的运行问题,如程序如何执行,如何处理数据;堆解决数据存储问题,数据怎么放,放在哪儿等
3.2虚拟机栈的作用
①每个线程在创建时都会创建一个虚拟机栈,内存存储一个一个栈帧,正在执行的方法对应着一个一个栈帧。
②主管java程序的运行,保存方法的局部变量(8种基本数据类型及对象的引用地址)、部分结果参与方法的调用及返回。
3.3虚拟机栈的声明周期
与线程一致
3.4特点:
不存在垃圾回收;但是存在OOM
3.5JVM对虚拟机栈的操作有什么?
①每个方法执行,伴随着入栈。
②方法执行结束出栈。
3.6虚拟机栈常见两个异常
①:stackOverFlow:JVM允许用户自己设置虚拟机栈的大小为固定值,当方法数过多,致使栈帧数量超过虚拟机栈的大小时,就会报stackOverFlow;
②:OutOfMemoryError:Java虚拟机栈容量允许扩展,当无法申请到足够的内存时,会报OOM异常。
3.7虚拟机栈运行的原理:
如果在当前方法中调用了其他方法,对应新的栈帧会被创建出来,放在栈的顶端,称为当前栈帧
3.8栈帧的结构
局部变量表、操作数栈、动态链接、方法返回地址及一些附加信息;
3.9局部变量表
①:作用
局部变量表为定义一个数字数组,存储方法参数和定义在方法内部的局部变量。数字数组中,如果是基本数据类型变量,则存储的是变量值,如果是引用数据类型,则存储的是堆内对象的地址值。
②:安全性
由于是线程私有的,因此不存在安全问题。
③:大小
局部变量表的大小是在编译期就确定的,一旦确定,无法修改。
④:声明周期
局部变量表中的变量只在当前的方法调用中有效,一旦方法调用结束,则随着栈帧的出栈,局部变量表随之销毁。
⑤:与垃圾回收的关系
局部变量表中的变量也是垃圾回收的根结点,因为局部变量表中存在指向堆内存的对象的引用地址,一旦栈帧被弹出虚拟机栈,则相应的对象需要被回收,反之,被局部变量表中直接或者间接引用的对象都不会被回收。
3.10.操作数栈
①作用
用于保存计算过程中的中间结果,同时作为变量计算过程中的临时存储空间(可能存在逃逸分析)。
代码的具体执行过程:
①首先程序计算器中的字节码指令地址为0,然后创建操作数栈,首先将局部变量i的值压入操作数栈,然后此时局部变量表中为空,然后程序计算器中的地址更改为2,执行istore_1指令,即将变量i存储到局部变量表中,然后操作数栈中的15被弹出到局部变量表中,然后继续执行下一条指令。
3.11操作数栈的数据结构
底层是由数组来实现的栈,不能通过索引来操作具体的数据,只能是由栈的操作,即出栈和入栈操作。
3.12操作数栈的创建
当一个方法被执行的时候,相应的栈帧也就被创建出来,此时的操作数栈的底层数组时空的,在编译期数组的长度及操作数栈的深度也就被确定了。(因为数组的长度一旦确认,就不能更改了,只有集合类才存在动态扩容机制)
4.动态链接
①作用:在Java源文件被编译到字节码文件时,所有的变量和方法引用都作为符号引用保存在字节码文件的常量池中,动态链接的作用就是转换为调用方法的直接引用(记录在方法区的运行时常量池中的方法的地址)。
②四种方法调用指令及区分虚方法与非虚方法
③方法重写的本质与虚方法的使用
5.方法的返回地址
①作用:存放调用该方法的程序计算器的值即调用该方法的下一条指令的地址值。
②正常完成出口与异常完成出口
正常完成出口:执行引擎遇到任何一个方法返回的字节码指令时,会有返回值传递给上层的方法调用者。
异常完成出口:在方法的执行过程中遇到了异常,且没有在本方法内部进行处理,就会退出。
6.虚拟机栈面试题
①举例虚拟机栈溢出的情况(StackOverFlow情况)
答:当通过-Xss设置虚拟机栈为固定大小后,当实际运行的栈帧数量超过虚拟机栈的最大深度的时候,就会爆出stackOverFlow错误。
此外还可能存在OOM异常。
②调整虚拟栈的大小能够保证不溢出吗?
答:不能保证,比如说出现不会停止的递归过程,调整虚拟栈的大小也没有什么用,只会让溢出的时间晚一些而已。
③虚拟机栈的大小是越大越好吗?
答:肯定不是的,挤占其他的内存空间。
④垃圾回收是否涉及到虚拟机栈?
不会
⑤方法中定义的局部变量是否是线程安全的?
具体问题具体分析:
线程安全的情况:如果是单个线程或者说在方法内部创建内部消亡的话,则是线程安全的,
线程不安全的情况:多个线程共享这个局部变量时有可能存在线程安全问题。
4.本地方法栈
①虚拟机栈与本地方法栈的区别:
虚拟机栈用于管理Java方法的调用,而本地方法栈用于本地方法的调用。
②本地方法
本地方法是有C、C++等语言编写的,是Java的拓展方法库。
③其他
本地方法栈是线程私有的,Hot Spot虚拟机中将其和虚拟机栈合二为一。
5.堆
1.堆空间的概述
①一个JVM实例对应着一个堆内存;
②堆是进程私有的,是线程共享的;
③堆随着JVM的启动而创建,堆大小一旦创建完成就不能更改;
④堆内存的大小是可以调节的。
⑤堆在物理上是不连续的,但是在逻辑上连续的;
2.TLAB(Thread Local Allocation Buffer)
堆是线程共享的,但是在堆中存在线程私有的缓冲区。
3.作用:几乎所有的对象实例及数组都在堆分配内存
数组和对象可能永远不会存储在虚拟机栈上,因为栈帧中保存的是指向堆空间的对象或者数组的地址值。
4.与垃圾回收的关系
是垃圾回收的重点区域;
在方法结束后,堆中的对象不会马上被清除,需要GC进行判断。
5.堆内存细分
jdk1.7之前将堆分为:新生代+老年代+永久代;jdk1.8之后将堆从逻辑上分为:新生代+老年代+元空间
6.堆空间的内部结构
注:永久代/元空间其实是归于方法区管理,堆实际上只负责前两部分(新生代+老年代)
2.设置堆内存的大小及OOM问题
①:设置堆内存大小
a.-Xms:设置堆空间(新生代+老年代)的起始内存大小
b.-Xmx:设置堆空间(新生代+老年代)的最大内存大小
补充:-X是虚拟机的运行参数;ms:memory size的意思
c.默认的堆空间的起始内存大小:电脑运行内存的大小/64;默认的堆空间的最大内存大小:电脑运行内存的大小/4;
②OOM问题
当堆内存使用超过设置的最大堆内存时,会出现OOM异常
OOM举例:
过程:创建了一个List数组,然后不断向数组中添加Picture对象,然后逐渐内存就被占满了后,会出现OOM异常
3.新生代与老年代
①什么是年轻代?什么是老年代?
存储在堆中的Java对象可以分类两类,一类是新生代,一类是老年代;
新生代:一类是声明周期较短的瞬时对象,这类对象从创建到消亡时间较短暂;新生代包括伊甸园区,两个幸存区
老年代:一类对象的声明周期比较长,在极端情况下还可能与JVM的声明周期一致;
②新生代与老年代的默认内存比例
默认的新生代与老年代的比例是1:2
③HotSpot虚拟机中,新生代与Survivor的比例是多少?
通过-XX:SurvivorRatio的值进行调节;默认的是8:1:1;实际使用时不为8:1:1,因为存在自适应内存分配策略(默认开启)
④几乎所有的Java对象都是在Eden区被new出来(如果对象查过Eden大小,则直接进入老年代);绝大部分的java对象销毁都发生在新生代;
4.对象内存分配的一般过程(垃圾回收的一般过程)
a.:new出来的对象会被存储在伊甸园区,然后当伊甸园区的空间被占满后,此时会YGC,将没有被其他对象所引用的对象进行回收,然后将伊甸园区剩余对象加载到Survivor0区(from区),并将年龄标记为1,再加载新的对象放在伊甸园区。
b.:如果再次触发YGC垃圾回收,上次放在幸存者0区的对象会放在幸存者1区,年龄标记+1.然后伊甸园区幸存下来的对象会被放到幸存者1区,年龄标记为1,然后幸存者1区变为from区,幸存者0区变为to区;
c.:如果再次发生YGC,则会重复此过程,当from区的幸存对象的标记年龄超过15后,则会移动到老年代中;如果存在着大于伊甸园区的对象,则会直接进入到老年代中;当老年代内存不足时,会触发GC,进行老年代回收;
总结:对幸存者1区2区:复制后交换,谁空谁为to区;频繁在新生代收集,较少在老年代收集,几乎不在永久代/元空间收集
5.对象内存分配特殊情况(当出现超大对象的情况时):
6.Minor GC和Major GC和Full GC
1.三者的区别
部分收集(Partial GC):不是完整收集整个java堆的垃圾收集
①:新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
②:老年代收集(Major GC/Old GC):只是老年代的垃圾收集
目前只有CMS垃圾收集器才有单独的老年代的收集
③:混合收集:收集整个新生代和部分老年代的垃圾收集
目前只有G1垃圾收集器具备混合收集(因为它是基于region的垃圾收集)
整堆收集(Full GC):完整收集java堆及方法区的垃圾收集
2.Minor GC
①触发条件:当伊甸园区满了,就会触发Minor GC,回收整个年轻代的垃圾(s0和s1区满不会触发Minor GC)
②触发后果:当进行Minor GC时,会造成STW(Stop The World),会停止运行用户其他线程。
3.Major GC
①触发条件:当老年代的内存空间不足时,会优先触发Minor GC,如果之后还不足,则会触发Major GC
②触发后果:Major GC比Minor GC的速度要慢十倍以上,造成更长时间的STW。
4.Full GC
①:触发条件:
a.调用System.gc()时,系统建议执行Full GC;
b.老年代的空间不足;
c.方法区的空间不足时。
7.堆空间的分代思想
1.为什么分为新生代和老年代(面试重点)
不同对象的生命周期是不同的,70%-90%的对象的生命周期是短暂的,在新生代主要存放生命周期短暂的对象,在老年代存放新生代中经过多次垃圾回收之后依然没有没有被回收的对象,这样做主要是为了优化GC的性能,如果不分新生代和老年代,而是将对象都放在一起,则每次GC的时候都要遍历整个堆空间,会造成STW的时间较长,影响用户性能体验。
8.内存分配策略
a.优先分配到新生代
b.大对象直接分配到老年代;
c.长期存活的对象(s0,s1区中年龄计算器值超过设定阈值或默认阈值15)分配到老年代;
d.动态对象年龄判断:如果Survivor区中相同年龄的对象的总和大于等于Survivor区的一半,则大于等于该年龄的对象直接进入到老年代中。
补充:大对象直接进入到老年代代码(创建大小为20M的数组,设定堆空间的初始内存和最大内存都为60M,然后设置新生代与老年代的比例为1:2,即老年代40M,新生代20M,然后设定伊甸园区与幸存者区比例为8:1:1,则伊甸园区16M,不足以放下20M的数组)
9.TLAB(Thread Local Allocation Buffer:线程私有缓存区)
1.什么是TLAB?
jvm在伊甸园区为每一个线程开辟了一块私有的缓存区,每个TLAB只占伊甸园区的1%,JVM将TLAB作为内存分配的首选
2.为什么要有TLAB?
因为堆空间是线程共享的,如果存在多个线程同时访问堆空间的某个对象,则可能会造成线程安全问题,可以采用加锁机制实现同步处理,但是会影响分配速度,因此采用TLAB的方式解决。
3.TLAB补充
a.尽管不是所有的对象都在TLAB分配内存,但是JVM会优先在TLAB为对象分配内存;
b.当TLAB内存空间不足时,会采用同步加锁机制在伊甸园其他区域分配内存,确保数据操作的原子性。
10.堆空间的参数设置(后期补充)
11.堆是分配对象存储的唯一选择吗?
1.该问题
随着即时编译器的发展与逃逸分析技术的不断成熟,虚拟机栈上分配等技术的出现,使得对象分配在堆上就没有那么绝对了。
如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,则有可能会被优化成栈上分配。
2.简述逃逸分析(只知道什么逃逸分析即可)?
①什么是逃逸分析?
如果将堆上的对象分配到栈的话,需要使用逃逸分析技术。
虚拟机能够通过分析来判断对象的引用范围是否在方法内部,从而决定是否将对象份分配到堆上。
②如何判断是否发生了逃逸?
如果一个对象在方法中被定义后,对象只在方法内部进行使用,则认为没有发生逃逸。
如果一个对象在方法内被定义后,对象被外部方法所引用,则认为发生了逃逸。
6.方法区
1.堆、栈和方法区的交互关系
①内存层面
![b8cae0b5fe97f06a7f71ba3a407f565.png b8cae0b5fe97f06a7f71ba3a407f565.png](https://uploadfiles.nowcoder.com/files/20201111/849965361_1605096239226_1595564024898-a68373ed-edbc-434a-a81a-755bcc1e9ba7.png)
②线程共享层面
线程共享:堆和方法区
堆:GC的重点回收区,如果内存不足会报出OOM
方法区:GC也会对此部分进行回收,回收的频率极低;内存不足时也会报出OOM
线程私有:程序计数器、虚拟机栈及本地方法栈
程序奇数器:无GC、无异常;
虚拟机栈:无GC,存在异常:StackOverFlow(栈溢出)
本地方法栈:无GC,存在异常:StackOverFlow(栈溢出)
③创建对象层面
如上图所示:Person类信息会存储在方法区当中,而person变量如果定义在方法内,则会存储在虚拟机栈的栈帧的局部变量表中,而右侧的new出来的对象则会存放在堆中;
下面这个图为:虚拟机栈中存储着对象的引用地址,指向位于堆空间对象的实例,而堆空间中对象实例数据中包含指向存储在方法区的对象类型数据的指针。
2.方法区的理解
1.jdk1.8前后的不同
①:jdk1.8之前为永久代;jdk1.8之后称为元空间(MetaSpace)
②:永久代处于虚拟机中的内存中,而元空间不在虚拟机设置的内存中,使用本地内存
2.补充知识点
a.作用:保存类信息相关信息;
b.声明周期:随着JVM的创建而创建,随着JVM的关闭而死亡;
c.异常:方法区的大小决定了系统可以创建多少个类,如果创建过多的类,导致方法区溢出,就会出现OOM;
d.线程:是多个线程共享的区域;
3.调节方法区的大小和OOM
1.元空间的默认大小和设置方式
a.默认大小:初始默认大小为:21M;最大显示为-1,即没有限制
b.设置:初始内存大小:-XX:MetaspaceSize=想设置的值;最大内存大小:MaxMetaspaceSize=设置值;
2.如何解决OOM问题(后期补充):
①:内存泄露与内存溢出?
②如何解决OOM?
4.方法区的内部结构
1.方法区中都存储哪些信息?
a.类信息
①:类的名称(包名.类名)
②:父类名称
③:修饰符信息
④:接口的有序列表
⑤:...
b.常量
c.静态变量(存在问题)
被static修饰的变量或者静态代码块(jdk1.7之后,静态变量存储在堆空间中)
d.即时编译器代码缓存
将热点代码存储在方法区中,解释为机器指令后并不执行,以做备用
e.域信息(属性等)
域的修饰符、类型及域的名称等信息
f.方法信息(方法的)
方法的名称、修饰符、返回值类型、参数的类型及个数、异常表信息及字节码信息等
示例代码:
方法区的存储信息(示例中):
①类型信息
②域信息
③方法信息
2.反编译字节码文件的方法
①:找到字节码所在的文件位置,然后调用javap -v -p 类名.class
②:通过jclasslib插件
3.运行时常量池
a.什么是运行时常量池?
.class字节码文件中的常量池用于存放编译后的各种字面量及符号引用,在加载类和接口到虚拟机后,这部分内容就转为了运行时常量池。
b.为什么需要常量池?
常量池中包含了类中各种字面量及对类型、方法及域的各种符号引用,这样可以有效减少字节码文件的大小。
4.举例说明方法区的使用过程
代码:
字节码文件中的常量池:
程序实现的具体方法区及虚拟机栈的过程
5.方法区在jdk6和jdk7及jdk8的不同(重点)?
jdk1.6:静态变量存放在永久代上,字符串常量池存放在运行时常量池中
jdk1.7:逐渐已经放弃“永久代”,静态变量和字符串常量池存放在堆上
jdk1.8及以后,永久代被元空间取代,整体从JVM设置内存中移动到本地内存中,但是静态变量和字符串常量池依然保存在堆中。(在jdk1.7的基础上将方法区拿到本地内存中为元空间)。
6.永久代替换为元空间的好处有哪些?
①:永久代的大小比较有限,而元空间是基于本地内存,相对更大,因此OOM的发生的几率就越小;
②:对永久代的调优更加困难,因此当方法区空间不足时,会引起Full GC,类型的卸载条件相当苛刻,因此尽量保证不发生Full GC。
5.方法区的垃圾回收
1.回收的主要对象(内容)是什么?
常量池中废弃的常量(包括字面量和符号引用)及不再使用的类型;
2.常量的回收策略?
常量池中的常量没有被任何对象所引用,则会被回收
3.判定类不再被使用的条件?
a.该类的所有实例都已经被回收
b.加载该类的类加载器已经被回收
c.该类对应的大的Class对象没有被引用。
6.对象的实例化与访问
1.对象的实例化
①:对象的创建方式:
②:对象的创建/实例化的步骤(Object obj=new Object()的整个过程)(面试重点):
a.加载对象的类型信息
b.为对象分配内存
分配策略:
指针碰撞:
空闲列表:
c.处理并发安全问题
CAS算法和加锁机制
d.属性的默认初始化
非静态变量
e.设置对象头信息
对象头中的信息包含指向方法区类型等信息的指针
f.执行init方法进行初始化
属性的显式初始化
代码块的初始化
构造器中的初始化
2.对象的内存布局
①:对象的内存布局包含哪些?每部分的作用展开叙述
a.对象头(面试重点)
运行时元数据:主要包含对象的哈希值、GC分代年龄(from区向to区移动,分代年龄+1)、锁信息等
类型指针:指向方法区(元空间)的类元数据,确定对象所属的类
数组长度(如果为数组对象)
b.实例数据
它是对象真正存储的有效信息,包括程序中定义的各种字段(包括从父类继承下来的和自己拥有的字段)
c.对齐填充
任何对象的大小都是8字节的整数倍,如果实例数据不满足,采用对齐填充进行补齐
②举例子说明该过程(定义一个Customer类,然后里面有属性,代码块,调用另外一个类来创建类实例,然后创建该类的对象/实例)
代码如下:
创建类:
创建类的对象/实例:
整个内存情况:
过程:
3.对象的访问定位
①:对象的访问定位方式有几种(JVM是如何通过虚拟机栈中的栈帧中的局部变量表中的地址值访问到对象的实例数的呢?),两种方式的优缺点?
a.句柄方式
通过局部变量表中的地址值找到堆空间中的句柄池,句柄池中存在两个指针,一个指针指向堆空间的对象实例数据;另一个指针指向方法区(元空间)中对象的类元信息。
优点:在GC环节,from区到to区对象会频繁移动,此时只需改变句柄池中的指针即可,虚拟栈中的地址不需改动;
缺点:句柄池占用堆空间;对象的访问效率相对降低
b.直接指针(Hotspot虚拟机采用)
通过局部变量表中的引用地址值找到位于堆空间的对象的实例数据,实例数据中的对象头中包含指向方法区(元空间)的类元信息的类型指针。
优点和缺点与句柄访问相反。
四、执行引擎(☆)
1.执行引擎概述
①.作用:
将字节码指令解释/编译为对应平台上的本地机器指令。(因为本地机器指令无法识别字节码指令)
②.结构:
a.解释器
b.即时编译器
c.GC
③.执行过程:
a.首先通过程序计数器找到需要执行的字节码指令
b.然后通过虚拟栈的栈帧中的局部变量表中的地址找到堆空间中的对象的实例数据,然后通过实例数据中的对象头中的对象的类型指针找到方法区的类元信息。
2.java代码的编译和执行的过程
①:java代码编译和执行的过程
橙色部分:即前端编译器将.java源文件编译为字节码文件
绿色部分:解释器将字节码文件解释为本地机器指令,并进行执行;
蓝色部分:将字节码指令解释为本地机器指令,但是并不执行(解释热点代码,以备用)
②:为什么说java语言半编译半解释型语言?
因为java虚拟中的执行引擎同时存在解释器和即时编译器
举例子:
3.解释器
①:为什么需要字节码文件
可以实现跨语言,使其他的语言生成自己码指令,JVM也能执行。
②:解释器作用:
根据程序计数器的指令地址,逐条将字节码指令“翻译”为本地机器指令,以便程序能够运行。
4.即时编译器(JIT)
①:作用:
将整个函数体编译成机器码,每次函数执行时,只编译机器码即可,还可以将常用的机器码进行缓存在方法区(元空间)中,从而提高效率。
5.解释器与即时编译器的优缺点
解释器:
优点:程序一开始执行的时候,解释器就能够逐条执行,省去编译的时间。
缺点:相对于即时编译器而言,逐条进行翻译效率较低
即时编译器
优点:因为是提前编译好的机器指令,因此效率更高;
缺点:程序一开始执行的时候,需要事先对字节码指令进行编译,需要一定的时间。
综上:HotSpot虚拟机采用两者兼容的方式,虚拟机开始启动后,解释器可以立即发挥作用,不需要等到即时编译器编译完后再去执行,节省不必要的编译时间,随着时间的推移,即时编译器编译完成之后,采用即时编译器效率更高。
6.StringTable(字符串)
①String基本特性
a.存储结构的变更
jdk1.8时采用char[]进行存储;jdk1.9之后采用byte[]进行存储;
b.不可变特性
通过字面量的方式给一个字符串赋值,此时的字符串位于堆空间的常量池中,
当对字符串进行赋值时,需要重写指定内存区域进行赋值,不能使用原有的value进行赋值;
解释:因为ex.str已经确定为了good,然后再直接赋值的话,就不会改变
c.字符串常量池中不会存储相同的字符串
常量池中的字符串均是唯一的,如果两个字符串变量相等,则两个变量指向字符串常量池中的同一个地址。
String Pool(常量池)底层是一个固定大小的HashTable,默认长度为1009,可通过-XX:StringTableSize设置
②String内存分配
jdk1.6时,字符串常量池位于永久代内
jdk1.7之后,字符串常量池位于堆空间;
移动的原因/好处:
永久代几乎不进行来及回收,将其移动到堆空间后,更方便进行垃圾回收。
③String基本操作
a.java语言规范指出完全相同的字符串字面量,必须是指向同一个String实例。
④字符串的拼接
a.常量与常量拼接结果存放在常量池中
首先==号判断的是两者的地址值是否相等,则两者指向的是字符串常量池中的同一个字符串,地址值相等;
其次equals判断的是两个字符串序列是否相等,则也是相等的。
b.常量池中不会存在相同内容的常量。
c.如果在拼接前后出现变量,则连接的结果存放在堆中字符串常量池外(相当于new一个对象),原理是StringBuilder;
d.如果拼接的结果调用了intern()方法,则主动将常量池中还没有的字符串常量对象放入池中,并返回地址
⑤字符串拼接底层原理(后面补充)
⑥intern()方法(后面补充)
五、垃圾回收(☆)
1.垃圾回收概述
①什么是垃圾?
垃圾指的是在运行程序中没有任何指针指向的对象。
②为什么需要GC?
a.如果不断进行内存分配而不进行垃圾回收,内存迟早会被消耗完;
b.GC可以解决内存中的碎片化问题,从而能够为较大的对象分配足够的内存空间;
c.随着应用程序的不断完善,用户越来越多,更需要GC来进行性能优化。
③垃圾回收的主要区域
a.方法区:主要回收常量池中废弃的常量(字面量及符号引用)及不再使用的类型
b.堆空间:回收垃圾(没有任何指针指向的对象)
2.垃圾回收相关算法
判断对象存活方式:引用计数算法和可达性分析算法
①:标记阶段(表明什么对象需要回收)
a.引用计数算法
原理:每个对象都有一个引用计数器,来记录对象被引用的次数,每增加一次引用计数器+1;每减少一次引用计数器-1,当引用计数器记录值为0时,就表名该对象没有被任何对象引用,则表名该对象为垃圾。
优缺点:
优点:实现简单,垃圾对象便于识别;回收效率高;
缺点:
①:无法处理循环引用的情况;从而造成内存泄露;
②:空间:因为需要计数器,所以造成额外的空间开销;
③:时间:需要频繁的加1或者减1操作,造成一定的时间开销;
代码举例:
b.可达性分析算法(根搜索算法)
GC Roots:一组活跃引用的根对象集合
基本原理:以跟对象为起始点,从上到下搜索被根对象所连接的目标是否可以到达,内存中的存活对象都直接或者间接地与根对象集合相连,搜索所走过的路径被称为引用链,如果目标对象没有被任何引用链相连,则为不可到达,即为内存中的垃圾。
优点:可以解决循环引用的问题;
缺点:相较于引用计数算法来说,回收效率稍慢一些。
c.可以被当做GC Roots的元素有哪些?(重点是堆外保存堆内对象的地址的那些区域)
①:虚拟机栈中引用的对象;如:各个线程中被调用的方法的局部变量等
②:本地方法栈中引用的对象;
③:方法区(元空间)静态属性引用的对象:如静态变量
④:方法区(元空间)中常量引用的对象。如字符串常量池中引用的对象
⑤:所有被synchronized持有的对象;
⑥:基本数据类型对应的Class对象、异常类对象及系统类加载器
⑦:还需要一些“临时性”对象加入GC Roots结合中,如只针对新生代回收,则堆中新生代以外的引用对象也需要加入GC Roots集合中。
②:对象的finalization机制
a.什么是对象的finalization机制
在gc 回收某个对象之前,会先调用对象的finalize()方法。Object类中的finalize()方法没有任何方法体,对象类可以重写这个方法。
b.虚拟机中的对象有哪几种可能的状态(生存还是死亡)?
①:可触及的:能够与引用连相连接的对象;
②:可复活的:没有任何引用的对象,但是可以在finalize()中被复活的对象;
③:不可触及的:对象的finalize()被调用,但是没有被复活的对象。finalize()方法只能被调用一次。
c.判断一个对象是否可以被回收所经历的两次标记过程(如何判断一个对象是否可以被回收?)(面试重点)
①:如果对象到GC Root没有引用链连接,则进行第一次标记;
②:进行筛选,判断是否有必要调用对象的finalize()方法
1.对象类没有重写finalize()方法或者已经调用过了finalize()方法,则为不可触及的;
2.对象重写了该方法但是还没调用过该方法,则回收前会先调用此方法;
③:执行二次标记:如果执行finalize()方法后,该对象与引用链上的任何一个对象建立连接,则为复活状态,否则判定为不可达状态。
④:清除阶段(如何回收垃圾)
a.标记-清除算法
1.原理:当需要进行GC时,会停止整个程序(STW),然后整个GC过程分为标记阶段和清除阶段
标记:从引用根对象开始遍历,标记所有被引用的对象,一般在对象头中记录为可达对象。
清除:对堆内存进行从头到尾的遍历,如果发现某个对象的对象头没有被标记为可达对象,则进行回收
2.优点:最基本的垃圾回收算法
3.缺点:①:因为需要遍历,所以执行效率不高;
②:在进行GC时,需要STW,效率不高的话,会影响用户体验;
③:产生内存碎片化问题
b.复制
1.原理:将内存空间分为大小相等的两块,每次只使用其中的一块,垃圾收集时,将正在使用的内存中的存活的对象复制到另一块内存中,然后将正在使用的内存清空。交换两个内存的角色,完成垃圾回收
2.优点:
①:执行效率高,省去了清除中的遍历问题;
②:解决了内存碎片化的问题
3.缺点:
①:空间浪费较明显,始终有一块内存无法使用;
②:对于G1这种分成很多region的垃圾回收器来说,复制意味着需要维持region之间对象的引用关系,空间和时间的开销比较大。
4.适用场景:
比较适合于垃圾对象较多,存活对象较少的区域,如新生代中的Survivor0区和Survivor1区。
不适合老年代中垃圾回收。
c.标记-整理(压缩)
1.原理:
第一阶段和标记-清除算法的标记阶段一样,从根结点开始标记所有被引用的对象;
第二阶段将所有存活的对象压缩到内存的一端,按顺序排放,然后清除边界以外的内存空间
2.优点:
①:解决了标记-清除内存碎片化的问题
②:解决了标记-复制空间浪费一半的问题
3.缺点:
①:整理过程中需要移动对象,如果对象被其他对象所引用,则需要不断调整引用的地址;
②:效率相对另外两种算法较低;
4.适用场景:适合于老年代中的垃圾收集
d.三种垃圾回收算法对比
算法: |
标记-清除 |
复制 |
标记-压缩(整理) |
执行效率 |
中等 |
最快 |
最慢 |
空间开销 |
少(存在内存碎片化) |
大(浪费一半空间) |
小(不会产生内存碎片化) |
移动对象 |
不 |
是 |
是 |
e.分代收集理论
目的:不同声明周期的对象可以采用不用的回收算法,以提高整体的回收效率。
HotSpot虚拟机中的回收策略:
①新生代:
新生代特点:区域相对老年代小,对象声明周期短,回收频繁
针对这种情况应当采用复制算法,回收效率高,针对于空间利用率不高的问题,采用两个Survivor区得以缓解;
②老年代:
老年代特点:相对于新生代大,对象的声明周期长,回收不频繁。
针对这种情况采用标记-清除+标记-整理相结合的方法,首先标记阶段还是采用两者中的标记方法,即从根对象开始,依次标记所有的存活对象,然后采用清除算法,暂时容忍碎片化问题,等到碎片化问题影响到内存分配时,再采用整理算法,整理碎片化内存。
f.增量收集算法
如果一次性将所有垃圾进行回收,需要造成系统长时间的停顿,影响用户体验,可以让垃圾收集线程和用户线程交替执行,每次垃圾收集线程只收集一部分的内存空间,接着切换到应用程序,依次反复,直到垃圾收集完成。
优点:低延迟,用户体验更优;缺点:吞吐量下降
g.分区收集算法
将整个堆空间划分为连续不等的小区间,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间。每一个小区间都独立使用,独立回收。
3.垃圾回收相关概念
① System.gc()
会显示触发Full GC,但是无法保证对垃圾收集器的调用;底层调用Runtime.getRuntime().gc();
举例子:不太理解方法3和方法4(需要补充关于虚拟机栈Slot槽部分的内容)
② 内存溢出与内存泄露
a.内存溢出(OOM):没有空闲内存,并且垃圾收集器也无法提供更多内存。
b.没有空闲内存的原因:
①:设置的堆内存太小,此时可以通过-Xms:和-Xmx来设置堆空间的起始大小和最大大小;
②:代码中创建了大对象,并且长时间不能被垃圾回收器所收集;
举出OOM的例子:
c.内存泄露:(非常重要)
①严格意义:对象不会被程序用到,但是GC又没有办法回收掉该对象,此时就称为发生了内存泄露;
②宽泛意义:实际情况中,一些不好的编程实践造成了对象的声明周期变得很长甚至导致OOM,(如本来可以定义为方法内部的局部变量,定义为类的成员变量甚至定义为静态变量(随着类的加载而加载,随着类的消亡而消亡)造成变量的声明周期加长,本来方法弹栈后可能就会被释放(没有发生逃逸))。
③:举例说明内存泄露(举出关于内存泄露的例子)
1.单例的生命周期和程序一样长,单例程序中如果存在对外部对象的引用,则外部对象是没有办法被回收的,会造成内存泄露
2.一些提供close()的资源未关闭而导致内存泄露(如数据库的连接必须手动close,否则不能被回收)。
③ STW
如论那种垃圾收集器,在进行垃圾收集的过程中,都会使程序停顿,这称为STW,被STW中断的程序在完成GC后会自动恢复。
目的:为了保证数据的一致性(不能统计垃圾的时候还一边造垃圾)
④ 垃圾回收并行与并发
a.程序的并行与并发
并发:从一段时间来看,有多个任务在执行,从单一的时间点上来看,只有一个任务在执行,时间上是Cpu在快速切换任务交替执行;
并行:当系统有多个CPU时,一个cpu可以执行一个进程,另一个cpu可以执行另一个进程,两个进程不会互相抢cpu资源,可以同时进行,此称为并行。
对比:并发指的是同一个时间段,多个任务发生了,且抢占cpu资源;
并行指的是在用一个时间点,多个任务发生了,不抢占cpu资源;
b.垃圾回收的并行与并发
并行:多条垃圾回收线程并行执行,此时需要停顿用户线程
串行:只有一条垃圾回收线程,当内存不够时,程序暂停,启动垃圾回收,回收完,再启动程序的线程。
并发:用户线程与垃圾回收线程同时执行,垃圾回收线程在执行时,不会停顿用户程序的执行。(??)
用户程序在继续进行,而垃圾收集线程运行在另一个CPU,如:CMS和G1。
⑤ 安全点与安全区域(后期补充)
⑥ 强引用(Strong Reference)
a.定义:在java程序中通过new创建了一个对象,并将其赋值给一个变量,该变量就称为指向该对象的一个强引用
b.适用场景:99%以上的都是强引用。
c.垃圾回收:强引用的对象都是可触及的,GC不会回收掉被强引用的对象(强引用,不回收)。
⑦ 软引用(Soft Reference)
a.定义:描述的是一些还在用,但是非必须的对象。
b.适用场景:通常用来实现内存敏感的缓存,如高速缓存就用到软引用。
c.垃圾回收:内存不足即回收
⑧ 弱引用(Weak Reference)
a.定义:弱引用也是描述那些非必须的对象,与软引用的区别在于GC时,对于软引用来说需要判断当前内存是否不足,不足的话才进行回收,而对于弱引用不需要进行判断直接回收。
b.适用场景:保存那些可有可无的缓存数据。
c.垃圾回收:发现即回收
补充面试题:你用过weakHashMap吗?
采用weakHashMap进行存储后,当内存不足时,能够对该部分进行回收,因为内存的Entry<k,v>类继承了弱引用类。
⑨ 虚引用(Phantom Reference)
a.定义:所有引用中最弱的一个,为一个对象设置虚引用关联的目的是跟踪垃圾回收的过程,被回收后可以发出相应的通知。
b.适用场景:实现跟踪对象的垃圾回收过程
c.垃圾回收:跟踪对象回收过程
4.垃圾回收器
①垃圾回收主要性能指标
a.吞吐量:用户程序运行时间/用户程序运行时间+垃圾回收时间
吞吐量越高越好,这样会提升用户体验,认为只有应用程序在执行。
b.暂停时间:执行垃圾收集时,程序被暂停的时间。
暂停时间越低越好,对于交互式应用程序,暂停时间越长,越容易出现卡顿现象,影响用户体验。
c.现在标准:在最大吞吐量优先的情况下,降低停顿时间。
②垃圾回收器概述(分类)
串行(STW时只有一个垃圾回收线程):Serial 及Serial Old
并行(STW时有多个垃圾回收线程):ParNew、Parallel Scavenge及Parallel Old
并发(垃圾回收线程和用户线程并发执行):CMS及G1
③垃圾回收器的组合关系
新生代:Serial ParNew Parallel Scavenge
老年代:Serial Old Parallel Old和CMS
新生代和老年代:G1
组合关系:
补充:jdk9中移除了Serial+CMS和ParNew+Serial Old这两种组合
jdk14中,删除了CMS垃圾回收器。
目前的组合:Serial +Serial Old; Parallel Scavenge+Parallel Old和G1
④Serial垃圾回收器(串行回收)
a.Serial垃圾回收器
采用复制算法、串行收集和Stop The World的方式来对新生代进行垃圾收集。是HotSpot虚拟机在客户端模式下默认的新生代垃圾回收器。
b.Serial Old垃圾回收器
采用标记-整理算法、串行收集和Stop The World的方式来对老年代进行垃圾收集。是HotSpot虚拟机在客户端模式下默认的老年代垃圾回收器。
c.回收过程
d.回收优势:因为是单线程,因此没有线程交换的开销,相对于其他垃圾收集器的单线程相比,简单而高效。
e.参数设置:-XX:useSerialGC/useSerialOldGC设置
⑤ParNew垃圾回收器(并行回收)
a.回收模式:采用复制算法、并行回收和STW的机制进行垃圾回收,是JVM在服务端下的默认新生代垃圾回收器。
b组合搭配:可以和Serial Old搭配使用(JDK9中移除);可以和CMS配合使用(JDK14中删除)
c.优势:对于新生代,回收次数频繁,因此采用并行方式更加高效。
d.参数设置:-XX:useParNewGC
⑥Parallel垃圾回收器(吞吐量优先)
a.Parallel Scavenge垃圾回收器
采用复制算法、并行回收及STW的机制进行垃圾收集。
b.Parallel Scavenge垃圾回收器与ParNew的区别:
Parallel Scavenge的目标是达到一个可控吞吐量,而且具备自适应调节策略。
c.Parallel Old垃圾回收器
采用标记-整理、并行回收及STW的机制进行垃圾收集,在JDK8中,默认Parallel Scavenge+Parallel Old为垃圾回收器。
d.适用场景:高吞吐量则可以高效的利用cpu的时间,来快速实现计算。主要适合在后台运行而不需要太多交互的任务
⑦CMS(Concurrent Mark Sweep)垃圾回收器(低延时)
a.概念:CMS(Concurrent Mark Sweep)垃圾回收器是HotSpot虚拟机第一款并发垃圾回收器,实现了垃圾收集线程和用户线程同时工作。是基于标记-清除算法,并发回收的老年代垃圾收集器,只搭配ParNew和Serial使用,在jdk14时,CMS垃圾回收器被删除。
b.工作原理:
1.初始标记:仅仅只是标记出GC Roots能够直接关联到的对象,存在STW机制。
2.并发标记:从GC Root直接关联到的对象开始,遍历整个对象图的过程,该阶段不需要停顿用户线程。
3.重新标记:修正并发标记阶段,因用户线程运行而导致标记产生变动的那一部分对象的标记记录,存在STW机制。
4.并发清除:清除标记为已经死亡的对象,释放内存空间。
c.优缺点:
1.优点:并发收集;低延迟;
2.缺点:
①:会产生内存碎片,当老年代需要为大对象分配内存时,不得不提前触发Full GC;
②:无法处理"浮动垃圾",可能导致"并发失败",从而引发Full GC
浮动垃圾:因为在并发清理阶段,垃圾收集线程和用户线程在并发执行,用户线程运行过程中,会产生新的垃圾,而这部分垃圾是发生在标记阶段之后的,所以只能等到下一次GC时,才能够进行回收,这时候需要预留出一定的内存空间。
③:对CPU资源比较敏感:CMS默认的垃圾回收线程数为(处理器核心数+3)/4,对于处理器核心数比较少的情况,垃圾回收线程就占比较大,影响执行速度。
d.使用场景:适用于强交互的应用。
e.参数设置:
1.-XX:+UseConcMarkSweepGC:设置使用CMS垃圾收集器;
2.-XX:CMSinitialingOccupanyFraction:设置堆内存使用率阈值;jdk5,默认为68%;jdk6.默认为92%。
3.-XX:ParallelCMSThreads:设置CMS的线程数量
f.面试题:
1.为什么说CMS是一款低延时的垃圾回收器?
因为在初始标记阶段和重新标记阶段,会发生STW,但是此部分时间很短,而在并发标记和并发清除阶段,虽然占据的时间比较长,但是在此期间,垃圾回收线程和用户线程并发执行,因此为低延时垃圾回收器
2.CMS会产生内存碎片化问题,为什么不用标记整理算法呢?
因为CMS在并发标记和并发清除阶段,用户线程和垃圾回收线程并发执行,而标记整理算法中存在对象在内存中的定向移动,用户线程在执行过程中,如果发生移动,会造成安全问题。因此无法采用标记-整理算法。
⑧G1垃圾回收器
a.概述:
1.目标是在停顿时间可控的情况下尽可能的提高吞吐量的垃圾收集器
2.将整个堆内存分为多个region区,跟踪各个Region区的垃圾堆积的价值大小,然后后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值大的region.
3.基于复制算法和标记-整理算法的并行垃圾回收器,为jdk9之后默认的垃圾回收器。
b.回收过程(整体):
整体分为:年轻代GC(Young GC),并发标记老年代,混合回收过程,如果G1失效,Full GC作为后备机制
当伊甸园区满之后,开始进行年轻代回收,移动存活对象到Survivor区或者Old区;
当堆内存到达一定阈值(默认45%)时,启动并发标记老年代;
并发标记结束后,进行混合回收,收集老年代时,一次只需回收一部分老年代对象,(因为存在时间限制)。
c.优缺点:
优点:
1.并发与并行
并行性:在G1垃圾回收期间,可以有多条垃圾回收线程同时进行回收,有效利用多核计算能力。
并发性:在G1并发标记阶段,允许垃圾收集线程和用户线程并发执行。
2.分代收集
G1同时兼顾了年轻代和老年代。因为G1垃圾收集器将堆空间分为不同的Region区,这些区域包含了逻辑上的新生代和老年代。
3.不存在内存碎片化问题
因为G1垃圾回收器以Region为基本内存回收单元,Region之间采用复制算法,但是从整体上来说,是采用标记-整理算法,因此可以避免内存碎片问题。
4.可预测的停顿时间模型(软实时)
使使用者明确在一个长度为M的时间片段内,用于垃圾回收的时间不超过N
主要是因为G1跟踪各个Region的垃圾堆积价值的大小,然后后台维护一个优先级列表,在有限的时间内,优先收集价值大的Region。
缺点:由于垃圾收集产生的内存占用相对大,小内存应用上性能不如CMS。
d.适用场景:面向服务端的垃圾收集器,主要针对配备多核CPU和大容量内存的机器(低延时,大内存)。
e.Region相关介绍
1.大小:G1将整个堆大小分为约2048个Region,每个Region大小相同,且为2的n次幂(一般在1M-32M之间),且在JVM的生命周期内不会改变。
2.分布:一个Region有可能属于伊甸园区,S区或者Old区。G1还在堆内存中存放了一个Humogous区,简称H区,当对象大小超过1.5倍的Region区时,将其称为大对象,放在H区。(问题:如果是1.2倍Region大的对象放在哪儿?)
3.设置H区的原因?
原因在于,如果不设置H区,那么大的对象则会直接进入到老年代,但是如果这个对象的声明周期比较短,则会长时间存在,并且占据着较大内存,所以将其存放在H区,如果对象大于一个H区大小,则会存在在连续的H区中。如果整个H区都装不下,则会触发Full GC.
4.内存分配
Region区采用指针碰撞的方式进行内存分配,并且每个Region内部存在TLAB(线程本地分配缓冲区)。
f.Remembered Set(记忆集)
1.存在必要性:因为存在跨Region引用的存在,判断存活时,如果挨个遍历每个Region的话,势必造成效率降低,于是引进了记忆集。
2.原理:每个Region都有一个记忆集,当进行引用数据写入时,先判断是否存在其他Region的引用关系,如果存在的话,则将该引用关系对应的对象写入记忆集的卡表(CardTable)中,然后在GC过程中将Rset加入到GC Roots中。
g.回收过程(具体)
1.年轻代GC(与之前讲的一样,只不过是基于分区思想)
JVM优先分配对象到伊甸园区,当伊甸园区满了之后,开始进行年轻代GC,然后,存活下来的对象进入到S区,当S区的对象分代年龄达到阈值后,进入到老年代中,清理过程中的算法采用复制算法。
2.标记老年代(深入理解java虚拟机中此部分为G1的收集过程)
①:初始标记:只标记GC Roots直接关联到的对象,此过程需要STW;
②:并发标记:从GC Root关联到的对象开始,递归遍历整个对象图,此时垃圾收集线程和用户线程并发执行
③:最终标记:由于在并发标记阶段,用户线程在执行,需要对标记结果进行修正,此时需要STW.
④:筛选回收:根据用户设置停顿时间,采用复制算法进行清理(将决定回收的Region中的存活对象复制到空的Region中,然后清空旧的Region空间),此过程需要STW。
3.混合回收:
⑨垃圾回收器总结
⑩如何选择垃圾回收器
a.优先调整堆的大小,让JVM能够适应;
b.如果是内存比较小,选择Serial +Serial Old垃圾回收器;
c.如果是单核,没有停顿时间要求,选择Serial +Serial Old垃圾回收器;
d.如果是多核CPU,需要高吞吐量,并且停顿时间不长,选择Parallel+Parallel Old垃圾回收器;
e.如果是多核CPU,低停顿时间
如果是jdk14以前,则可以选用ParNew+CMS或者G1;jdk14后,选择G1;
六、补充面试题总结
1.JVM调优
1.调优参数
1.设置堆区的大小
-Xms:设置初始堆大小;
-Xmx:设置最大堆大小;
2.设置新生代和老年代的比例
-XX:NewRatio:设置年轻代和老年代的比例;
3.设置伊甸园区和幸存者区的比例
-XX:SurvivorRatio=n;
4.设置永久代/元空间的大小
-XX:permSize;-XX:permMax;
-XX:metaSpaceSize;-XX:metaSpaceMax;
5.设置垃圾回收器
-XX:useG1GC;
-XX:useCMSGC;
6.打印相关日志
-XX:+printGC:打印垃圾回收
-XX:+printGCDetails:打印垃圾回收细节信息;
2.调优目的
减少GC的频次和Full GC的次数
3.调优过程
a.首选需要监控GC的状态,查看当前的堆内存快照及gc日志,根据实际的各区域划分和GC执行时间,判断是否需要优化。
b.生成堆的dump文件
c.分析dump文件
可以通过eclipse的工具Memory Analyzer
d.分析结果,判断是否需要优化
e.调整GC的类型和内存分配
-XX:use+垃圾回收器+GC;
f.不断分析和调整参数
2.OOM及其解决?
1.OOM场景有哪些?
a.java堆内存溢出
原因:堆内存设置小或者内存泄露问题
解决:对于内存泄露的话,可以使用内存监控查找程序中的内存泄露的地方 加以更正;
对于堆内存设置问题,可以通过-Xms或者-Xmx来进行设置;
b.方法区溢出:
原因:出现大量的Class或者jsp页面,过多的常量等会导致方法区溢出;
解决:可以通过设置方法区的大小来调节
-XX:Permsize;-XX:MaxPermSize设置永久代的大小;
-XX:metaSpaceSize;-XX:MaxmetaSpace设置元空间的大小;
2.OOM的解决方法
a.首先采用内存映像分析工具如(MAT)对dump出来的堆转储快照进行分析,确认内存中的对象是否是必要的,查明到底是内存泄露还是内存溢出
b.如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链,准确定位到泄漏代码的位置;
c.如果不存在泄漏的话,分析是堆内存溢出还是方法区溢出;
如果是堆内存溢出,则适当调整堆的大小:通过-Xms或者-Xmx来进行设置;
如果是方法区溢出,适当调整方法区的大小:通过-XX:Permsize;-XX:MaxPermSize设置永久代的大小;-XX:metaSpaceSize;-XX:MaxmetaSpace设置元空间的大小;
3.jdk1.7和jdk1.8比较?
1.字符串放在什么位置?
均放在堆空间的字符串常量池中;
2.默认的垃圾回收器?
是一样的,都是Parallel scavenge+parallel old垃圾回收器。jdk1.9之后是G1垃圾回收器。
4.Full GC与Minor GC
①Full GC触发条件
a.老年代内存被占满;
b.永久代/元空间内存被占满;
c.System.gc()时会显式调用Full GC;
d.空间担保失败
当老年代的剩余空间小于等于S区的平均对象大小时,表示空间担保失败;
②Full GC定义
完整收集整个堆和方法区的垃圾
③Minor GC
定义:只发生在新生代的垃圾回收
④Minor GC触发条件
当新生代的伊甸园区满了之后,就会触发Minor GC,回收整个年轻代的垃圾;
⑤Minor GC出发结果
当进行Minor GC时,会产生STW,存活的对象移动到幸存者区;
5.垃圾回收相关
1.CMS和G1垃圾回收器比较
①CMS垃圾回收器(主打低延时)
a.介绍
是hotspot虚拟机第一款并发的垃圾回收器,垃圾回收线程和用户线程可以并发执行,不需要STW,因此能够提升用户体验,降低延迟性,适合于频繁交互的场景。
b.执行过程
初始标记:仅仅标记被GC Roots直接关联到的对象,存在STW。
并发标记:从GC Roots直接关联的对象开始,遍历整个对象图的过程。此过程不需要停顿用户线程;
重新标记:重新标记那些在并发标记阶段,由于用户线程的执行标记记录产生变动的对象;
并发回收:采用标记-清除算法,并发清除垃圾对象
c.缺点:
①内存碎片化
产生内存碎片化问题,当老年代需要为大对象分配内存时,不得不提前触发Full GC;
②浮动垃圾
无法处理"浮动垃圾",可能导致并发失败,从而引发Full GC
浮动垃圾:因为在并发清理阶段,垃圾回收线程和用户线程并发执行,因此该 过程会产生新的垃 圾,这部分垃圾是再标记阶段之后产生的,因此这部分垃圾只能等到下一次垃圾回收时才能被回收,这个时候需要预留出一部分的内存空间。
③对Cpu比较敏感:(核心数+3)/4
CMS默认的垃圾回收线程的数量是:(处理器核数+3)/4,对于处理器核心数较少的情况,垃圾回收线程所占的比较较大,影响运行效率。
d.使用场景
主打低延时,用于频繁交互的场景;
②G1垃圾回收器
a.概述
1.目标是在停顿时间可控的情况下,尽可能提高吞吐量的垃圾回收器
2.将整个堆内存分为多个region区域,跟踪每个Region区的垃圾堆积的价值大小,然后后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值大的region;
3.基于复制算法和标记整理的算法的并行垃圾回收器,为jdk1.9默认的垃圾回收器;
b.G1的回收过程
①初始标记:只标记GC Roots直接关联到的对象,此过程需要STW;
②并发标记:从GC Root直接关联到的对象开始,递归遍历整个对象图,此时垃圾回收线程和用户线程交替执行;
③最终标记:由于并发标记阶段,用于线程在执行,需要对标记结果进行修正,此时需要STW;
④筛选回收:根据用户设置停顿时间,采用复制算法进行清理(将决定回收的Region中的存活对象复制到空的region中,然后清空旧的Region空间),因为涉及到引用的变更,因此需要STW;
c.G1的优点
1.并行和并发
并行性:在G1垃圾回收期间,可以允许多条垃圾回收线程同时进行回收,有效利用多核计算能力;
并发性:在G1并发标记阶段,允许垃圾收集线程和用户线程并发执行;
2.分代收集
G1同时兼顾了老年代和新生代。因为G1垃圾收集器将堆空间分为不同的region区,这些区域包含了逻辑上的新生代和老年代。
3.不存在内存碎片化的问题
因为G1垃圾回收器以Region为基本单元进行回收,Region之间采用的是复制算法,但是从整体上看是基于标记整理算法,因此可以避免内存碎片化的问题。
4.可预测的停顿时间模型
使得使用者明确在一个长度为M的时间片段内,用于垃圾回收的时间不超过N;原因在于G1垃圾回收器跟踪每个region区垃圾堆积的大小,然后后台维护一个优先级列表,在规定的时间内,优先回收价值大的region区。
d.缺点:
由于垃圾收集产生的内存占用大,小内存上性能不如CMS;
f.应用场景:
面向服务端的垃圾收集器,适用于配备多核CPU和大容量内存的机器。
③什么是垃圾
对于那些没有任何引用指向的对象称之为垃圾
2.其他垃圾回收器
①serial及serial old垃圾回收器(串行回收)
serial是复制算法、串行的,需要STW的针对新生代的垃圾回收器,是hs在客户端模式下默认的新生代垃圾回收器
serial old是标记-整理算法,串行的,需要STW的针对老年代的垃圾回收器,是hs在客户端默认的老年代垃圾回收器
优点:因为是单线程,因此没有线程交换的开销,相对于其他垃圾回收器的单线程相比,简单而且高效
②parNew垃圾回收器(并行回收)
parNew是复制算法,并行回收,需要STW的针对新生代的垃圾回收器,是hs在服务端默认的新生代垃圾回收器;
优点:对于新生代,回收次数频繁,比较高效;
③parallel scavenge和parallel old垃圾回收器(吞吐量优先)
parallel scavenge采用复制算法、并行回收,需要STW的针对新生代的垃圾回收器,注重吞吐量优先;
parallel old垃圾回收器是标记整理、并行回收,需要STW的针对老年代的垃圾回收器;
使用场景:高吞吐量则可以高效利用cpu的时间,来实现快速计算,主要适合于在后台运行而不需要进行太多交互的场景。
以上两种组合是jdk1.7和1.8中默认的垃圾回收器,适用于后台大量计算而不需要太多交互的场景;
3.为什么要分代?
因为java对象的生命周期是不同的,大部分对象的声明周期是比较短暂的,少部分对象的声明周期是比较长的,甚至是伴随着JVM的消亡而消亡,通过分为新生代和老年代两种内存区域,将声明周期短暂的对象放在新生代,将声明周期较长的对象放在老年代,在进行垃圾回收的时候,能够提高效率;如果不采用分代的话,在进行垃圾回收的时候,需要遍历整个堆空间,造成STW时间过长,影响用户体验。
6.内存泄漏与内存溢出问题及解决方式
内存泄漏场景:
①单例模式中,存在对其他对象的引用,这个对象已经用不到了,但是因为单例的存在没法释放该对象。
②将某些对象添加到集合中,集合中的某些对象可能已经用不到了,但是因为集合对该对象还存在引用关系,导致无法被及时回收;
7.JVM内存结构
①类加载子系统
②运行时数据区
a.程序计数器
1.作用:记录虚拟机字节码指令的下一条地址,当一个线程再次获得CPU执行权的时候,能够保证下一条执行从哪儿继续;
b.虚拟机栈
1.作用:主管java程序的运行,虚拟机栈中存储着栈帧,对应着一个一个执行的方法,当方法执行的时候,会进行压栈操作,方法执行完后进行弹栈。
2.组成:
①局部变量表
存储方法的参数及局部变量,如果是基本数据类型,则会存储变量值,如果是引用数据类型的话,则存储的是引用地址值。
②操作数栈
临时存储变量,作为变量计算的中间结果的临时存储区域
③动态链接
将虚拟机栈中的符号引用转为方法区中的方法的直接引用;
④方法返回地址
存储方法的返回地址,方便调用者能够获取到方法的返回值。
3.与本地方法栈的区别:
虚拟机栈负责java程序的执行;本地方法栈作用类似于虚拟机栈,负责本地方法的执行,本地方法指的的用C/C++编写的程序,作为方法的扩展库;hotSpot虚拟机将两者合二为一。
c.堆
1.作用:绝大多数的创建的对象将会存储在堆中;jdk1.7之后静态变量及字符串常量存储在堆空间中
d.方法区
1.作用:存储类信息,域信息,方法信息,常量,即时编译器的代码缓存,jdk1.6及之前存储静态变量。
e.本地方法栈
1.作用:负责本地方法的运行,本地方法指的是用C/C++编写的程序,作为方法的扩展库。
③执行引擎
④本地方法接口/本地方法库
8.JVM类加载过程
a.加载阶段
作用:
①根据类的全限定名获取类的二进制字节流;
②将该二进制字节流所代表的静态存储结构转换为方法区的运行时的数据结构;
③在内存中生成一个代表该类的大的Class对象,作为方法区这个类的数据访问入口。
b.链接阶段:
1.验证:
验证该二进制字节码文件是否符合虚拟机规范,确保在运行过程中不会对虚拟机造成伤害;
2.准备:
对类的静态变量及静态代码块进行隐式的初始化,但是有final修饰的变量会进行过显示初始化
3.解析:
将常量池中的符号引用转换为直接引用
c.初始化:
根据程序显示初始化类的静态变量及其他资源。
9.类加载如何保证线程安全
类加载器在加载类时采用的loadClass()方法使用了Synchronized关键字进行修饰。
10.双亲委派机制
1.定义:
当一个类需要加载时,负责加载此类的加载器不会立即对其进行加载,而是递归的委托给其父类加载,父类收到这个加载任务后也会向上进行委托,直到引导类加载器,如果引导类加载器能够加载此类,则会进行加载,如果无法实现加载此类,则会返回给其子类进行加载。
2.好处
①:这样做可以有效地防止类被重复加载
②:能够保护java的核心类库,放置核心的API被随意篡改
3.各个类加载器加载的类的文件名
a,引导类加载器
java的核心类库都是使用引导类加载器加载的,加载的文件存放在<JAVA_HOME> /lib文件夹中;
b.扩展类加载器
扩展类加载器负责加载<JAVA_HOME>/lib/ext目录中的jar文件
c.应用程序类加载器
应用程序类加载器加载的是用户类路径上的类库
11.垃圾回收算法
1.标记-清除算法
a.算法概述
当进行垃圾回收时,会停止整个程序(stw),然后整个过程分为标记阶段和清除阶段
标记阶段:从引用根对象开始遍历,标记所有被引用的对象,一般在对象头中纪录为可达对象;
清除阶段:对堆内存从头到尾遍历,如果发现某个对象的对象头没有被标记为可达对象,则进行回收;
b.优点:
最早的比较经典的算法
c.缺点:
①无法解决内存碎片化的问题
②在进行GC时,需要STW,效率不高的话,会影响用户的体验
③因为需要遍历,所以执行效率不高
2.复制算法
a.算法概述:
将内存分为大小相等的两部分,每次只使用其中的一块,当进行垃圾回收时,将正在使用的内存中的存活对象复制到另一块内存空间中,然后将正在使用的内存清除,交换两内存空间,继续执行上述操作,完成垃圾回收
b.优点:
①可以解决内存碎片化的问题;
②执行效率高,省去了清除阶段的遍历过程;
c.缺点:
①内存空间利用率不高,因为总是需要一块空的内存;
②对于G1这种多个region的垃圾回收器来说,复制并且移动意味着需要维持多个region间的引用关系,时间和空间消耗比较大。
d.适用场景:比较适合于垃圾对象较多,存活对象较少的区域;新生代中得S区;不适合老年代中的垃圾回收
3.标记-整理算法
a.算法概述
标记阶段与标记-清除算法中的标记阶段是一样的
整理阶段:将所有存活的对象移动到内存一侧,按顺序存放,然后清除边界以外的内存空间。
b.优点:
①解决内存碎片化的问题;
②空间利用率更高
c.缺点:
①整理过程中需要移动对象,如果对象被其他对象所引用,需要不断调整引用地址;
②效率相对另外两种算法较低
d.适用场景:
适用于老年代的垃圾回收。
12.如何选择垃圾回收器
a.优先调整堆的大小,让jvm能够适应;
b.如果是内存比较小,选择serial+serial Old垃圾回收器
c.如果是单核,没有停顿时间要求,选择serial+serial Old垃圾回收器
d.如果是多核CPU,需要高的吞吐量,选择parallel scanvenge+parallel Old垃圾回收器;
e.如果是多核cpu,低停顿时间
如果是jdk14以前,可以选用CMS+ParNew;jdk14后,G1垃圾回收器
13.对象的内存分配策略
a.优先分配到新生代
b.大对象直接分配在老年代
c.长期存活的对象(新生代中的S区的对象年龄达到了设定的阈值)分配到老年代;
d.动态判断对象年龄:如果相同年龄的对象所占的空间大于等于S区的一半,则大于等于该年龄的对象则会被分配到老年代中。
14.垃圾判定方法
a.引用计数算法(python中采用)
①概述
每个对象都有一个引用计数器,对象每增加一次引用,计数器的值就增加1,没减少一次引用,则计数器就-1,当引用计数器的值为0时,判定为对象不存在引用,即认为是垃圾。
②算法的优/缺点
优点:实现简单,执行效率高;
缺点:
1.最大的缺点是不能够解决循环引用问题,如果几个对象存在循环引用,但是这几个对象已经没有其他引用指向他们,但是因为其相互引用,则引用计数器的值也不为零,因此会造成内存泄露的问题。
2.因为是计数器,所以也占用一定的空间开销。
b,可达性分析算法(java采用)
①概述
可达性分析算法的思想为:首先说一下GC Roots,一组活跃引用的跟对象集合,以跟对象为起始点,从上向下搜索被根对象所连接的目标是否可达,内存中对象都直接或者间接的与根对象相连,搜索所走过的路径称为引用链,没有被引用链所连接的对象即为不可达,即被判定为垃圾。
②算法优/缺点
优点:可以解决循环引用的问题
缺点:相较于引用计数算法而言,判定效率低一些。
全部评论
(23) 回帖