1JVM发展史
第二种虚拟机:
2类加载的过程
3运行时数据区
1.PC寄存器
2虚拟机栈
设置栈的大小
这就是为什么不可以再静态方法中用this调用属性,因为slot中没有存放this,也就是在局部变量表中没有这个位置!
具体的位置
相对于栈结构来说,用寄存器来存储,寄存器的指令更少,执行的速度更快
栈帧中的有返回值,局部变量表,操作数栈和指向方法内区运行时常量池的动态连接
方法的调用
虚方法指的是类似于晚期绑定,被调用的方法不知道是哪一个类的,故不能在编译器确定下来,就称为虚方法
其中areturn表示引用类型的返回
虚拟机栈内部结构
如果局部变量在方法的内部产生,并且在内部消亡,就是线程安全的,但是如果局部变量在方法内部产生。但是返回到方法的外面,其他线程就会有可能抢夺,然后进行修改,从而就是不安全的,故具体问题要具体分析。
堆
设置堆的大小的指令
Xms10m:设置初始的堆空间的大小为10M
Xmx10m:设置最大的堆空间的大小为10M
注意:堆内存的大小=新生区+养老区,暂时不包括元空间
新生代老年代空间默认为1:2
但是实际的新生区中eden区和survivor区的的比例为6:1:1.此原因和自适应的内存分配策略有关
要想改变这个比例,就需要使用 -XX:SurvivorRatio=8指令来改变
从新生区到老年区的判断次数默认是15次,可以通过以下指令进行修改
显示垃圾回收的详细信息
堆空间中一定是线程共享的吗?
错误的,堆空间中TLAB是线程私有的
空间分配担保!但是jdk7之和就变了,默认为true了
逃逸分析!
当其他方法调用下面这个方法时,就会获取返回值,而这个返回值是由对象StringBuffer创建,当使用返回值时,就会用到此对象,故可能会发生了逃逸,所以要在堆中创建,不能使用栈上分配。
快速判断是否发生了逃逸
就看new的对象实体,是否有可能在方法外面被调用。(不返回就有可能不会发生逃逸)
jdk7
2同步省略
3分离对象或者标量替换
方法区
查看元空间的大小的指令
方法区
运行时常量池
运行时常量池就是在类加载时,把字节码文件中的常量池加载到方法区之后,就变成了运行时常量池
演进的过程,和为什么要这样呢?
方法区的垃圾回收
JVM面试题
对象的内存布局和访问定位
1对象的实例化方式
2对象的内存布局
例子:假设有一个Customer类,类中有id\name\Account account属性,在customer中已经对属性进行显示赋值后:新建Customer对象时的内存分布如图所示
3对象访问定位
第一种:引用指针(优点是直接指向对象的位置,hotspot使用)
第二种:句柄访问(好处是即使对象因为GC赋值和压缩整理算法位置发生了变动,动态链接的指向可以不动,只要句柄池内的指针变动就行)
执行引擎
执行引擎的内部结构
JIT将整个字节码编译成字节码之后会存放到方法区的缓存中,下次读取时就从缓存中读取,所以就非常的快。
解释器
解释执行就是边翻译为机器码边执行,一条一条地读取字节码,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢,没有JIT的配合下效率不高。
即时编译器JIT(just in time)
即时编译就是先将一个方法中的所有字节码全部编译成机器码之后再执行,即时编译器把整段字节码不加筛选的编译成机器码不论其执行频率是否有编译价值,在程序响应时间的限制下,没有达到最大的优化。
结合前面的问题:是不是所有的对象都在堆上分配?其实不是,和逃逸分析有关
混合模式
在HotSpot中默认采用混合模式,其利用解释器先解释执行字节码,然后将其中的热点代码(多次执行,循环等)直接编译成机器码,在使用JIT下次就不用再编译了,直接选中机器码让其更快速地运行。
热点代码及探测方式
知识点扩展
除了JIT编译器之外,还有Graal编译器和AOT编译器
从AOT编译器的工作过程可以看出,好处时可以提前将字节码转换成机器码,但是缺点是:不符合JAVA中的一次编译到处运行
String Table
在jdk1.8以及之前都是采用char[]数组来存放字符串,而1.9开始,就采用byte[]来存放,并且用编码标记来改变真正存储的结构,比如使用ISO-8859-1时使用byte[]数组,但是使用中文的时使用的仍然是char[]
为什么要把StringTable从方法区(永久代)放在堆中?
1首先永久代内存比较小,存放的内容不多
2其次是永久代中GC频率较低,产生的String垃圾不能及时清理,容易产生OOM
第一条的验证,常量的拼接时,经过编译优化就直接中常量池中取。所以地址也是同一个地址
第三条的验证:只要其中有一个是变量,就是在堆中。因为拼接符号的前后出现了变量则相当于在堆空间中new String 具体的内容为拼接的结果。因此会不同
第四条验证:intern()方法是判断字符串常量池中是否存在字符串,如果存在则返回常量池中字符串的地址,如果不存在则需要在常量池中加载一份判断的字符串,并且返回对象的地址。
字符串拼接的底层原理
那是因为s1和s2是final修饰的常量,此时不在是变量
字符串的拼接过程中,使用字符串拼接’+‘要远远慢于StringBuilder的append方法,因为在拼接时+时,就新建一个builder,并且还有toString方法。优化StringBuilder,可以直接对StringBuilder进行初始化长度确定,来减少扩容过程中的时间消耗。
intern()方法
使用方法:
String s=new String("倔强的加瓦").intern();
如何保证变量s指向的字符串就是常量池中的数据的两种方法
- 直接利用字面量进行复制,String s=“倔强的加瓦”;
- 使用intern方法
第一个全是false,因为s是栈中指向堆空间中new
String的地址,而s2是因为常量池中已经存在,所以之间返回的是常量池中的地址,所以不是同一个地址
第二个在jdk6中为false,是因为字符串常量池在永久代中,不在堆中,所以s3就是一个栈中指向堆中的地址,通过intern方法放到字符串常量池中,s4指向的是子符串常量池中“11”的地址,所以为false.
第二个在jdk7及以后,字符串常量池在堆中,当执行了intern方法时,为了省空间,不会在字符串常量池中新建一个“11”,而是在字符串常量池中,“11”的地址指向的是堆中new String的s3的地址,所以s4虽然指向了字符串常量池中“11”的地址,但是11的地址指向s3所指向的地址,所以为同一个地址故为true
小练习:
垃圾回收机制
1什么是垃圾:
垃圾是指在运行的程序中没有任何指针指向的对象,这个对象就是需要回收的垃圾。
标记阶段
在标记阶段有两种算法:1引用计数算法2可达性分析算法
1 引用计数算法,Java中并没用使用这个算法。。。python使用了这个方法
2可达性分析算法(或根搜索算法、追踪性垃圾收集)
roots的判断
finalize方法的存在导致对象有三种状态
清除阶段
三种算法:
1标记清除Mark-Sweep
清除只是把需要清除的对象地址保存,当写入时,直接可以在这上面进行覆盖数据,从而达到清除的效果。
2复制算法
复制算法流程图,每次将值复制到新的一片空间中,从而可以保证存储空间的连续性
3标记-压缩Mark-Compact算法
三种算法的比较
分代算法
分代收集算法
为了减少STW时间,一种名为增量收集算法使用
分区算法,也是为了降低STW时间
垃圾回收相关的概念
System.gc()方法的理解
当调用gc方法时,不一定立马就会开始GC,验证方法是调用gc后,重写finalize()方法,里面的代码不一定执行。
但是有一个方法可以立马执行GC,System.runFinalization()会强制调用使用引用对象的finalize()方法。
内存溢出
内存泄漏
Stop-The-World
垃圾回收的并行和并发
安全点
安全区域
在安全区域内的实际执行
引用
、
1.强引用—只要有引用,报oom也不回收
2.软引用–内存够时不回收,内存不足即使还有引用也要回收
3弱引用-发现就回收
WeakHashMap底层的entry数组,是用来弱引用,只要GC就回收
4 虚引用
垃圾回收器介绍
评估GC的性能指标
1吞吐量
2暂停时间
吞吐量和暂停时间综合
垃圾回收器的历史
7款经典的垃圾回收器
分代搭配使用的垃圾回收器
1.serial回收器:串行回收
2-ParNew回收器-并行回收
3 Parallel Scavenge回收器-吞吐量优先
4CMS(Concurrent Mark Sweep)回收器-低延迟
参数的设置
5 G1回收器–区域化分代式
G1回收器的优点
回收器的适用场景G1
Region化零为整
G1垃圾回收的回收过程
1 youngGc的过程
G1回收器垃圾回收过程:Remembered Set记忆集,主要是来记录下每一个region中的对象的引用,如果是本region中引用用到,就不需要记录,要是其他region中的引用用到,就要写在记忆集中,这样可以不用全局扫描就可以判断标记垃圾
G1工作过程
回收流程1,先进行年轻代的GC.
年轻代回收的过程
2并发标记过程,针对老年代的
3混合回收
G1回收器的优化建议
垃圾回收器的总结
怎么选择合适的垃圾回收器
GC信息的打印