文章目录
1.简介
java是一个跨平台语言
java是一个跨平台的语言,一次编译,到处运行,java编译过后生成的字节码文件可以在不同的平台上解释和运行,而c语言是到处编译,到处运行
什么是虚拟机
虚拟机就是一台虚拟的计算机,他是一款软件,用来执行一系列虚拟计算机指令大体上虚拟机可以分为:系统虚拟机和程序虚拟机
系统虚拟机:
程序虚拟机:
jvm是跨语言的平台
以上的几种语言经过各自的编译器也可以生成字节码文件,所以他们也都可以在java虚拟机上运行
java虚拟机的作用
java代码执行流程
计算机本身是不识别编程语言的
高级语言需要编译为汇编语言,汇编语言变为机器指令后才可以在操作系统上执行
jvm的位置
系统虚拟机是对 硬件 的模拟
jdk比jre多了一些 ‘前端编译器’比如javac
jvm 的整体结构
类装载器子系统相当于编译器的前端,执行引擎也算是编译器的后端,
字节码指令不等同于机器指令,执行引擎就是把高级语言翻译成机器语言的翻译者
jvm的架构模型
寄存器的指令更少,更高效,但是栈的可移植性更好
jvm的生命周期:
自己自定义的类开始执行时,他会加载很多类,object只是其中的一个
exit
在一个if-else判断中,如果我们程序是按照我们预想的执行,到最后我们需要停止程序,那么我们使用System.exit(0),而System.exit(1)一般放在catch块中,当捕获到异常,需要停止程序,我们使用System.exit(1)。这个status=1是用来表示这个程序是非正常退出
exit(0)和exit(1)都可以退出程序的
2.类的加载器和类加载过程
第一点:用特定的软件打开java 的class文件会发现前面是cafe babe 巴拉巴拉…不一样的语言,会有不一样的class文件
我们平常导入的jar包就是字节码文件
验证:字节码文件也算是二进制文件,可以手写,所以需要验证是不是合法
准备:
这个变量在准备的时候a的值是0,到初始化环节a才会被赋值为1
整数型初始值为0
浮点型 0.0
char的初始值为’\u0000’
boolean false
final 修饰的变量是常量
clinit不是我们自己定义的,当我们的代码中有类变量(static修饰的变量)和类静态代码块的时候,他会自动出来
执行顺序:
static修饰的变量,即使静态代码块在他的前面时,也可以进行赋值,但是不可以调用它
类加载器的分类
代码分析:
引导类:
扩展类:
他就是加载核心包之外的哪些扩展包
自定义类加载器:
获取类加载器的方法
双亲委派机制
他的作用是为了保护项目,防止恶意攻击
就好比小孩子吃水果,首先让给父母,父母让给爷爷奶奶,爷爷奶奶不吃又让给父母,父母不吃再让给孩子,上面不处理的最后都会交给系统类加载器处理
沙箱安全机制
这里说的比较浅
注意点
不会导致类的初始化就是前面类加载子系统中的初始化步骤
3.运行时数据区概述及线程
内存,硬盘,CPU。https://www.cnblogs.com/resn/p/5766142.html
jvm线程说明
当一个java线程要执行以后,操作系统的本地线程也同时创建,java线程终止后,本地线程还要去决定jvm要不要终止,这取决于还有没有非守护线程
守护线程和非守护线程(用户线程)
守护线程不用管他,守护线程保护着用户线程直到用户线程执行完,守护线程才能完
4.pc寄存器
作用
他没有gc(垃圾回收) 也不会出现OOM(OutOfMemory)异常,栈也没有gc
问题
1.使用pc寄存器存储字节码指令地址有什么用?
(为什么使用pc寄存器记录当前线程的执行地址呢?)
因为CPU需要不停的切换啊各个线程,这时候切换过来以后,就得知道接着从哪开始继续执行
pc寄存器为什么被设定为线程私有(每个线程都会单独有pc寄存器)
因为每个线程都会有自己执行的位置,如果共用的话,cpu来回在线程中切换,就不会知道每个线程到底执行到哪里了
CPU时间片
5.虚拟机栈
栈也是线程私有的(每个线程都有自己单独的栈),其内部保存一个个的栈帧(里面装的方法)生命周期和线程相同
作用:
栈中不存在gc,但是会出现oom
栈的大小,我们可以自己设置
设置方法:-Xss栈大小
栈帧内部结构
1.局部变量表结构
局部变量表中最基本的存储单位是slot
32位以内的类型用一个slot,64位的类型占两个
long,double用两个,其他的用一个
并且,非静态的方法,他们都还会分配一个this变量,占用一个
slot也可以重复利用:
变量的分类:
成员变量中 :用static修饰的变量叫做类变量,不用他修饰的叫做实例变量。
只有局部变量在使用前是必须显式赋值的,而成员变量他会给默认值
2.操作数栈
数栈用的数组或者链表的方式来存储的。但是不能用索引的方式进行数据访问,而只能通过标准的入栈出栈来进行操作
栈顶缓存技术
3.动态链接
方法的调用
静态链接-》早期绑定
动态链接-》晚期绑定
有两个动物,一猫一狗,这个两个刚开始没有传对象肯定时不知道调用的哪个,所以都属于晚期绑定
这两个在编译器就会知道调用的哪个,所以属于早期绑定
因为final修饰的方法不再能被继承
虚方法和非虚方法
一般在maven jar包冲突是容易出现此异常
虚方法表
因为虚方法需要一层一层的往上找父类,所以有了虚方法表来进行优化
方法返回地址
一些附加信息
栈面试题
1.如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError异常,
2.如果虚拟机的占内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OOM异常
hotspot虚拟机不支持动态扩展,创建线程申请内存会因为无法获得足够内存出现oom,或者因为占内存无法容纳新的栈帧而StackOverFlowError
当把栈设置为动态扩展的时候
6.本地方法栈
什么是本地方法
为什么要使用native方法
里面的内容基本上和虚拟机栈是一样的
除了:
他用的本地的内存,本地的寄存器,和虚拟机有相同的权限,执行效率比较高
其他的虚拟机不一定有本地方法栈,还是方法区也是一样,不是每个虚拟机都会有的
7.堆
堆中存放对象示例
一个进程对应一个jvm实例,一个jvm实例对应一个运行时数据区(runtime)
TLAB
堆内存细节
默认比例:新生代:老年代1:2
eden:survivor0:survivor1 8:1:1
注意:堆空间的大小不包括永久区
面试题
jdk8中的内存结构有什么变化,主要的变化就是堆空间中的元空间
永久区-》元空间
堆空间大小
就算手动设置的600m,但是最后可用的也不够600
OOM的说明和举例
当面试官问有没有遇到什么‘’异常‘’的时候,是可以回答OOM的,面试官的异常指的是程序不正常运行,而不是,错误和异常的异常,
新生代和老年代的一些参数
查询地址
新生代分为 eden ,survivor0和survivor1
当我们知道项目中的对象的生命周期确实很长,此时可以将老年代的空间调大一些
Eden区和survivor0 survivor1区的比例应该是8:1:1但是实际上确不是,此时是因为他有自适应,需要用上面的 -XX:-UseAdaptiveSizePolicy关闭,但是关闭了他也没有变为8:1:1,而只能通过-XXSurvivorRatio=8进行设置才会变为8:1:1
对象分配的一般过程
首先在eden中创建对象,当eden的空间满的时候会触发minor GC(幸存者区满了不会触发),此时会将eden中不被其他对象所引用的对象销毁,剩余的对象放入幸存者区(from区).并且他们还有阈值(年龄),此时的阈值是1
之后再创建对象还是放到eden区,当满了以后再次进行上面的操作,但是此时剩余的对象会放到空的那个幸存者区(to区),也会判断from区的对象是否还在被引用,如果不被引用则被销毁,若还在引用则会被放到to区,阈值增加。此时的from区就变成空的了,随之它就变成了to区,空的那个永远是to区。
当阈值到达15(默认15)的时候,会将该对象放到养老区。
特殊情况:有的对象直接在养老区创建,有的对象阈值还没有到15的时候就进入养老区
流程图:
什么对象会被分配到老年代
1.当survivor区的对象阈值达到一定程度(默认15,最大也为15,因为存储阈值的地方只有4位.2的四次方)
2.当对象太大时eden区放不下,且survivor也放不下时直接放到老年代
3.当survivor区中相同年龄的所有对象大小总和占survivor区的一半,大于或等于该年龄的对象直接到老年代
Minor GC,Major GC, Full GC
只有cms里面才有oldGC,其他垃圾回收器中,fullGC和majorGC是同一个概念
为什么要分代
堆空间一定都是共享的么 TLAB又是什么?
不是,还有个被每个线程所私有的空间
为什么要有TLAB?
什么是TLAB?
TLAB是内存分配的首选
它只占用eden 1%的空间
堆空间的参数设置
解释,比如年轻代此时有20m,老年代剩余的空间为19m,这时minor GC是不安全的,因为该参数的存在,我们会将之前年轻代晋升老年代的平均大小和老年代的大小进行比较,如果以前晋升的平均大小为15m,此时会现场时minor gc,如果以前进行的平均大小为20,此时比19m大,这时候就需要full gc
在jdk7之后,该参数就是true,抛弃了false那一种情况
不合理的分配年轻代的内存会导致什么
eden大,survivor小
此时会导致 survivor区的对象存的比较少,minor GC存在的意义就变小了,当survivor满了以后会导致,每minor GC,原本准备移到survivor区,但是满了,所以只能移到old区
eden小,survivor大
minor GC的频率变高,会影响用户进程,stw变多,所以运行的变慢了
堆是分配对象存储的唯一的选择么
逃逸分析
逃逸分析开启:-XX:-DoEscapeAnalysis
最后这个容易搞错, e没有调用到外面,但是他看的不是e,而是看的对象实体
如何快速的判断是否发生了逃逸分析,大家就看new的对象实体是否有可能在方法外被调用
结论:开发中能使用局部变量的,就不要使用在方法外定义。
逃逸分析代码优化
一,
二
其实第一个的锁原本就是有误的,当两个线程进入时,创建了两个hollis对象,两个锁就不是一样的锁,
三,
但是现实中,因为一些因素,逃逸分析中的 一 还是没有实践
标量替换确实用到了。
8.方法区
栈堆方法区的交互关系
解决OOM
方法区中存储什么
存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等
non - final变量
常量池VS运行时常量池
常量池
字节码文件中包含了常量池
运行时常量池
方法区的演变
永久代为什么要被替换为元空间
方法区的垃圾收集
面试题
9.对象实例化,内存布局,访问定位
对象创建的六种方式:
1.new的方式中包括了单例和工厂模式
2.基本上已经不使用了
3.也算是反射的方式,比第二种好
4.clone()对已经存在的对象克隆出来一个一样的
5.让类实现Serializable接口才可以序列化,反序列化是从文件还原到程序中的对象
6.第三方库Objenesis
对象创建的六个步骤
1.判断对象对应的类是否加载,链接,初始化
2.为对象分配内存
如果内存规整:指针碰撞(其实就是紧接着前面的对象的地址分配了对象内存),当压缩算法过后,里面的内存就是规整的,此时就会使用指针碰撞的方式分配内存
**内存不规整:**空闲列表分配
这种分配方式是当标记清除算法之后。
所以选择那种分配方式由java堆是否规整决定的,而java堆是否规整又由所采用的的垃圾收集器是否带有压缩整理功能决定的
3.处理并发安全问题
1.采用CAS失败重试,区域加锁保证更新的原子性
2.每个线程预先分配一块TLAB
4.对所有实例变量赋默认值
所有属性设置默认值,保证对象实例字段在不赋值时可以使用。
static修饰的变量在类加载子系统的链接阶段进行初始化,在类加载子系统的初始化阶段对类变量赋值,对实例变量初始化
5.设置对象的对象头
6.执行init方法进行初始化
对象的内存布局
对象的内存结构:
对象头:
1.运行时元数据:哈希值,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳
2.类型指针,指向类元数据InstanceKlass,确定对象所属类型
3.如果是数组,还需要记录数组的长度
实例数据:
对齐填充
不是必须的,仅仅起到占位符的作用
原因是HotSpot要求对象起始地址必须是8字节的整数,假如不是,就采用对齐填充的方式将其补齐8字节整数倍,那么为什么是8呢?原因是64位机器能被8整除的效率是最高的。
对象的访问定位
对象的访问主要有两种
1.句柄访问
2.直接指针(hotspot采用)
对比:
直接指针不需要创建句柄,节省空间,而且速度比较慢
句柄访问好处:如果对象被移动(垃圾手机时移动明对象很普遍),时只会改变句柄中实例数据指针即可,reference本身不需要被改变
10.直接内存
IO 和 NIO对比()
直接内存的oom和设置内存大小
11.执行引擎
java代码编译和执行的过程
机器码,指令,汇编语言,字节码
解释器
那为什么解释器效率低下而不删除他呢。
JIT编译器
什么时候选择JIT编译器
方法调用器
回边计数器
hotspot设置执行模式
默认就是混合的,当数据多的时候,混合的最快
JIT其实是分两类的
12.String
在jdk1.9之 前String底层都是char数组,1.9之后(包括1.9)就变成了byte数组
长度越大,写入的时间可能就越短,因为hash碰撞少了
面试题
答案:第一个是good,第二个变成best
= =比的是地址,equals比的是值
String StringBuilder StringBuffer区别和效率
String是final类不能被继承且为字符串常量。在字符串不经常发生的变化的业务场景优先使用String
StringBuilder 线程不安全 ,在单线程下,比如有大量的字符创操作的情况下,应该使用StringBuilder
StringBuffer 线程安全,在多线程下,有大量的字符串大的操作的情况下,应该使用StringBuffer
运行速度
StringBuilder>StringBuffer>String
StringBuffer和StringBuilder都继承了AbstractStringBuilder
append 和 +的效率比
StringBuilder 的append速度远大于String 的+
而且StringBuilder还可以优化,当我们知道后面添加的字符串长度不高于某个限定值的情况下,建议使用StringBuilder s=newStringBuilder(最大值)
这里,当用new String 的方式和创建字符串的时候,此时会创建两个"ab",一个是在堆中,一个是在堆中的字符串常量池中,而S1引用的是堆中的,所以s1不等于s2
如何保证变量指向字符串常量池
方式一: String s =“Start”;
方式二: String s = new String (“Start”).intern;
:StringBuffer s = new StringBuffer.intern;
只要最后加上intern就能保证变量指向字符串常量池
面试题
String s=new String("a")+new String("b");创建了几个对象
对象1:new StringBuilder
对象2:new String
对象3:常量池中的"a"
对象4:new String
对象5:常量池中的"b"
深度剖析:StringBuilder的toString
对象6:new String (“ab”)
强调:toString()的调用,在字符串常量池中,没有生成"ab"
0 new #2 <java/lang/StringBuilder>//因为有拼接操作,前/后出现变量,此时就会穿线StringBuilder
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init>>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <a>
13 invokespecial #6 <java/lang/String.<init>>
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <b>
25 invokespecial #6 <java/lang/String.<init>>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>
34 astore_1
35 return
第一个在jdk6/7/8中都是false。
解释:首先创建两个对象,一个在堆中,一个在字符串常量池中,调用intern时,因为字符串常量池中已经有了,所以这时这段代码是没有用的
这里解释一下:上面还说只要最后加上intern就能保证变量指向字符串常量池,怎么到这里就没有用了,因为加上.intern只会返回字符串常量的地址,但是没有变量去接的话是没有什么用的,所以这里说这段代码没有用,但如果前面加上s=s.intern的话就有用了。
第二个 String s3=new String(“1”)+ new String(“1”) 后在字符串常量池中是没有生成11的,所以在执行s3.intern时,jdk6会在方法区的字符串常量池中生成一个新的"11",jdk7/8中会在字符串常量池中"创建"一个11,而这个"11"中方的是堆中的那个11的地址
intern的效率测试
对于程序中大量存在的字符串,尤其其中存在很多重复字符串时,使用intern()可以节省内存空间
13.GC
什么是垃圾
为什么需要GC
对象标记阶段
很多垃圾回收是不会去管方法区的
引用计数算法
当面试官问内存泄漏的例子时不要举这个例子,因为java中就没有用这个引用计数算法,所以就不会出现该情况
虽然但是,python还是用着引用计数算法
可达性分析算法(根搜索算法或者跟踪性垃圾收集)
哪些对象可以作为gc root的
finalization
垃圾清除阶段
标记清除算法
周志明的深入理解java虚拟机中就写错了,写成了标记垃圾对象
当磁盘被格式化的时候,此时只要不往里面写东西,里面的文件还是可以恢复过来的
复制算法
因为最后这一点,所以复制算法适用于新生代
标记压缩算法
标记清除和标记压缩:之所以不使用标记清除是因为当对象和对象之前存在垃圾,大的对象放不进去,所以这些碎片的空间就浪费掉了,而标记压缩将存活的对象整理到一块了,剩余的地方都是空的,所以可以放大的对象
三种算法的比较
老年代用的标记清除算法和标记整理算法混用
增量收集算法
分区算法
垃圾回收的一些相关概念
System.gc()
使用system.runFinalization()强制调用使用引用的对象的finalize()方法
手动gc理解不可达对象的回收行为
不会gc(如果eden区的大小小于10m则会到老年代)
会gc
不会gc,buffer虽然不使用了,作用域已经过了,但还占用着空间,
会gc,value变量把buffer的slot的槽给站住了
会gc
内存溢出
内存泄漏
内存泄漏举例:
当可以把变量设置为 局部变量的时候,结果把他设置成了成员变量/类变量,此时也可以叫做宽泛意义上的“内存泄漏”
STW
垃圾回收的并发和并行
安全点和安全区域
引用
强引用
软引用
当内存足够时,不会回收软引用的可达对象
当内存不够时,会回收软引用的可达对象
有两种情况,
当内存不够时,把软引用关联的对象清除,清除过后内存还是不够,这时候就会报oom
或者把软引用关联的对象清除过后,内存不溢出了,这个时候就不会报oom
弱引用
虚引用
垃圾回收器的分类
评估gc的性能指标
吞吐量
七种经典的垃圾回收器
如何查看默认的垃圾收集器
-XX:+PrintCommandLineFlags
查看命令行相关的系数(包含使用的垃圾收集器)
使用命令行指令:
jinfo -flag 相关垃圾回收器参数 进程ID
serial回收器:串行回收
记住serial old是穿行回收的,所以parallel 在jdk1.6出现了parallel old 回收器来代替serial old
ParNew回收器:并行回收
在jdk9中serial old gc已经弃用,jdk14中的cms gc弃用
因为是并行的,所以可以限制他们的CPU数,但是不能超过cpu的个数,一般就不去设置他
Parallel Scavenge回收器:吞吐量优先
适用于服务器端
自适应调节,在堆中也有,年轻代中的 e s s ,的比例应该时8:1:1,但是有味自适应调节,他变成了6:1:1
CMS垃圾回收器:低延迟
初始标记和重新标记就是STW的过程,而且非常短暂
重新标记是把之前怀疑是垃圾的重新进行确认是否是垃圾
而在并发过程是和用户线程一块执行的,此时原来的一些不是垃圾的现在又变成了垃圾,而重新标记的过程中是不包含这些的,不能使这些垃圾即使回收,所以这些垃圾就成为了浮动垃圾。
标记压缩算法需要移动对象的地址,而CMS算法他是垃圾回收和用户线程并发进行的,让正在使用的对象的地址发生了改变,可能会导致程序出现问题,所以不可以使用标记压缩算法
浮动垃圾:重新标记是把之前怀疑是垃圾的重新进行确认是否是垃圾
而在并发过程是和用户线程一块执行的,此时原来的一些不是垃圾的现在又变成了垃圾,而重新标记的过程中是不包含这些的,不能使这些垃圾即使回收,所以这些垃圾就成为了浮动垃圾。
CMS弊端,当正在CMS的时候,突然间来了一大批用户,此时放不进去,会执行serial old回收,而serial old 的速度很慢,所以此时可能会导致程序崩溃
G1回收器:区域化 分代式
一般每一个region中会存放多个对象,因为有的对象可回收,有的对象不可回收,当里面的可回收对象多了之后此region就是一个价值高的region,此时就比较容易回收。
当eden区的对象放到servivor区的时候就是用的复制算法,而且回收完也会把存留的放在一起,此时就体现出了标记压缩算法
G1有更高的内存占用(rememberset占用内存10%~20%)
低延迟,堆空间大就用G1
内置的JVM线程执行GC就是一般GC所用的,执行的优先级比较低,而采用应用线程的话优先级就比之前高了,但是吞吐量可能会受影响
G1垃圾回收的详细过程
这里担心的是 回收年轻代的时候,有老年代引用,而回收老年代的时候,不用担心有年轻代的引用,因为回收老年代的时候是混合回收,原本就会去回收年轻代,
所以这里就引出了RememberSet的概念
CMS和G1细节比较
- 记忆集:为了解决对象跨代引用带来的问题,这里就用到了记忆集RememberSet,记忆集只是一种意图,卡表是记忆集的一种具体实现,CMS中的卡表只有唯一一份,而G1中的每一个region都有一份卡表。
- 虽然有的记忆集,但是如何去维护,卡表中的数据什么时候变脏,谁来把他们变脏,很明确:有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏的时间点原则上应该发生在引用类型字段赋值的那一个,但问题是如何在那一刻去维护?如果是解释执行的字节码,那虚拟机负责每条字节码指令的执行,有充分的呃呃介入空间,但在编译执行的场景中呢?经过即时编译后的代码已经是存粹的机器指令流了。这时就很难操作了,所以我们提出了写屏障。
写屏障:写屏障可以开左是在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个缓刑通知。供程序执行额外的动作。
写屏障又分为写前屏障,写后屏障,CMS使用的是写后屏障,G1两个都是用,因为她要实现原始快照搜索算法,还需要使用写前屏障来跟踪并发时的指针变化情况,相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点所以CMS的写屏障实现是直接的同步操作,而G1就不得不将其实现类似于消息队列的结构,把写前屏障和写后屏障中要做的事情都放到队列中,然后再异步处理。
垃圾回收的总结
GC日志分析
jdk7和jdk8中处理的不同
jdk1.7
当来了一个大对象,年轻代放不下, 在1.7中,他会直接把年轻代中的三个放到老年代,把新来的这个放到年轻代
而在1.8中他会直接把这个大对象放到老年代
垃圾回收未来
总的流程
遇到OOM该如何处理
首先oom分为三种
permgen OOM ,元空间
heap OOM,堆
stack overflow栈
把内存大户找出来。
- 可通过命令定期抓取heap dump 或者 启动参数OOM时 自动抓取heap dump内存快照
利用它可以分析是否存在内存浪费,可以检查内存管理是否合理,
获取dump文件的方式分为主动和被动:
i.主动方式:
1.利用jmap,也是最常用的方式:jmap -dump:[live],format=b,file=
2.利用jcmd,jcmd GC.heap_dump
3.使用VisualVM,可以界面操作进行dump内存
4.通过JMX的方式
ii.被动方式:
被动方式就是我们通常的OOM事件了,通过设置参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath= - 通过对比多个heap dump,以及heap dump的内容,找出内存大户
- 分析占用的内存对象,是否是因为错误导致的内存未及时释放,或者 数据过多导致的内存溢出