- 一次编写,处处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界越界检查
- 多态
jre 、jdk 、 jvm
- 面试
- 理解底层实现原理
- 中高级必备技能
Program Counter Register :寄存器
记住下条jvm指令执行地址
- 线程私有
- 不会内存溢出
栈 :先进后出
线程运行时需要的内存空间
栈帧 : 每个方法运行时需要的内存
Java Virtual Machine Stacks
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
-
垃圾回收是否涉及栈内存?
不用,栈内存在栈帧释放后自动回收
-
栈内存分配越大越好吗?
不好,栈内存越大,线程越少,不会提高运行效率,只会增加方法调用数目。
-
方法区的局部变量是否线程安全?
方法内局部变量 x 线程私有,不会存在线程安全。
当 x 改变未 static 线程共享,线程不安全。
结论 :
- 如果方法内部局部变量没有逃离方法的作用范围,线程安全
- 如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全性
-
栈帧过多,递归调用中容易发生
-
栈帧过大,比如栈大小4k,栈帧大小128k
注:栈默认大小mac, linux, oracle 1024k,win看虚拟机大小
- Linux中使用 top 检测cpu使用情况
- ps H -eo pid, tid, %cpu | grep 进程号 排查cpu高占用线程
- jstack 进程id 查看该进程下所有线程
- 将问题进程id换算为十六进制在jstack中寻找相应线程,定位cpu占用过高代码位置
- jstack 进程id 查看该进程下所有线程
- 在最后输出位置查看是否存在 Found one Java-level deadlock
给本地方法的运行提供内存空间
- 通过new关键字,创建对象都会使用堆内存
- 线程共享,堆中对象需要考虑线程安全
- 有垃圾回收机制
检查方法 将虚拟机堆内存设置变小-Xxm8m
- jps工具
- 查看系统中有哪些Java进程
- jmap工具
- 查看堆内存占用情况 jmap -heap 进程ID
- jconsole工具
- 图形工具,多功能监测工具可以连续监测
- 一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息
- 运行时常量池是 *.class 文件中的,当该类被加载,他的常量池信息就会放入运行时常量池,并把里面的符合地址变为真实地址
System.out.println(s3 == s4);
return false;
System.out.println(s3 == s5);
return true;当出现一个新的未出现过的串,放入StringTable,执行一行后才进行懒加载。
1.8
将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回
1.6
将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则将此对象复制一份放入串池,会把串池中的对象返回
- 调整-XX:StringTableSize=桶个数
- 存在大量重复字符串时可以使字符串入池,减少堆内存的使用
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
普通IO
NIO
循环引用无法回收
- 确定根对象
- 对堆中对象进行扫描,查看是否能够沿着GC Roots对象为起点找到该对象
-
强引用
只有所有GC Roots对象都不通过 【强引用】引用该对象,该对象才能被垃圾回收
-
软引用
- 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用对象
- 可以配合引用队列来释放软引用自身
-
弱引用
- 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
- 可以配合引用队列来释放弱引用自身
-
虚引用
- 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Refernce Handler 线程调用虚引用相关方法释放直接内存
-
终结器引用
优点 速度快
缺点 容易产生碎片
优点 没有碎片
缺点 速度慢
优点 不会产生碎片
缺点 占用双倍内存空间
- 对象首先分配在Eden区
- 新生代空间不足,触发 Minor GC, Eden 和 From 存活的对象使用 copy 复制到 to 中,存活的对象年龄加一,并交换 From 和 to
- Minor GC 会引发 stop the world, 暂停其他用户线程,等垃圾回收结束,用户线程恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命15(5bit)
- 当 老年代空间不足,尝试触发 Minor GC 如果之后空间仍不足触发 Full GC ,STW时间加长
当新生对象过于大时,超出了新生代限制,则直接分配到老年代区域
当一个线程内发生OutOfMemory并不会影响主线程
内存碎片过多CMS退化为Serial Old响应时间增长
- Young Collection + CM
- 在Young GC时进行GC Root 的初始标记
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定
- Mixed Collection
会对E、S、O进行全面垃圾回收
- 最终标记(Remark)会STW
- 拷贝存活(Evacuation)会STW
-
Young Collection 跨代引用
- 老年代引用新生代
- currentTable与Remember Set
- 在引用变量时通过post-write barrier+dirty card queue
- concurrent refinement threads 更新 Remember Set
-
JDK 8u20 字符串去重
- 优点:节省大量内存
- 缺点:略微多占用cpu时间,新生代回收时间略微增加
-
JDK 8u40 并发标记类卸载
所有对象都经过并发标记后,就能知道那些类不再被使用,当一个类加载器的所有类都不在使用,则卸载它所加载的所有类
-
JDK 8u60 回收巨型对象
- 一个对象大于region一半时,就是巨型对象
- G不会对巨型对象进行copy
- 回收时优先考虑
- G1会跟踪老年代所有incoming引用,当老年代incoming为0的巨型对象就可以在新生代垃圾回收时处理掉
java class 标识 ca fe ba be
常量池类型表
访问标识与继承信息
Feild信息
- byte、short、char都会按照int比较,因为操作数栈都是4字节
- goto用来进行跳转到指定行号的字节码
几种不同方法调用对应的字节码指令
invokespecial 与 invokestatic 为静态绑定
invokevirtual 为动态绑定
当执行 invokevirtual 指令时
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际Class
- Class 结构中有 vtable, 它在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查表得到方法的具体地址
- 执行方法的字节码
- 多出来一个Exception table结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过type匹配异常类型,如果一致,进入 target 所指向的行号
- 第8行字节码指令 astore_2 是将异常对象引用存入局部变量表的 solt 2 位置
finally 中的代码被复制了3份,分别放入 try 流程, catch 流程 以及 catch 剩余的异常类型流程。
如果在finally中出现 return 会吞掉异常
方法级别的 synchronized 不会在字节码指令中体现
只能得到方法参数及方法返回类型泛型信息
如果调用了 foo() 则等价代码为 foo(new String[]{}) , 创建了一个空数组,而不会传递 null 进去
addSuppressed(Throwable e) 添加被压制异常的原因:防止异常信息的丢失
压制异常测试
方法重写时对返回值的分类
- 父子类返回值一致
- 子类返回值为父类返回值的子类
引用局部变量
匿名内部类引用局部变量时,局部变量必须是 final 的,因为在创建Candy11$1对象时,将x的值赋值给了 Candy11$1 对象的valx属性, 所以x不应该再发生变化了,如果变化, valx属性不再有机会一起编号
- 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 Java 类, 他的重要field有:
- _java_mirror java的类镜像
- _super 父类
- _fields 成员变量
- _methods 方法
- _constants 常量池
- _class_loader 类加载器
- _vtable 虚方法表
- _itable 接口方法表
- 如果这个类有父类没有加载,先加载父类
- 加载和链接可能交替执行
-
验证 : 验证类是否符合JVM规范, 安全性检查
-
准备 :为static变量分配空间,设置默认值
- static 变量在 JDK7 之前存储于 instanceKlass 末尾(方法区), 从JDK7开始,存储于_java_mirror 末尾(堆)
- static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始阶段完成
- 如果 static 变量是final 的基本类型以及字符串常量, 那么编译阶段就确定了,赋值在准备阶段完成
- 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
-
解析 : 将常量池中符号引用解析为直接引用
-
初始化 : 初始化调用()V, 虚拟机会保证这个构造方法的线程安全
调用类加载器的loadClass 方法时,查找类的规则
定义了一套在多线程读写共享数据时(成员变量、数组),对数据的可见性,有序性、和原子性的规则和保障
两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0吗
不是,多线程情况下互相覆盖
解决方法
main线程对run变量的修改对于t线程不可见,导致t线程无法停止
解决方法
volatile 修饰(易变关键字)
修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
synchronized 和 volatile 区别
synchronized 语句块可以保证代码块的原子性和可见性,但它为重量级操作,性能相对低
volatile 能保证可见性和有序性,性能相对高
解决方案
volatile 修饰
异常原因
JVM在不影响正确性的前提下,可以调整指令顺序,这种特性称为指令重排,单线程模式下不会影响正确性,多线程模式下会影响正确性,例如 double-checked locking 模式实现单例
happens-before 规定了哪些写操作对其它线程读操作可见,它是可见性和有序性的一套规则总结
-
线程解锁m之前对变量的写,对于接下来对m加锁的其他线程对该变量读可见
-
线程对 volatile 变量的写,对接下来其他线程对该变量的读可见
-
线程 start 前对变量的写,对该线程开始后对该变量的读可见
-
线程结束前对变量的写,对其他线程得知它结束后的读可见,(比如其他线程调用 t1.Alive() 或 t1.join() 等待它结束)
-
线程 t1 打断 t2 (interrupt) 前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过 t2.interrupted 或 t2.isInterrupted)
-
对变量默认值 ( 0, false, null )的写,对其他线程对该变量的读可见
-
具有传递性, 如果 x hb-> y 且 y hb-> z 则 x hb -> z
它体现的是一种乐观锁的思想,如多个线程要对一个共享整型变量执行 + 1 操作
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。 结合 CAS 和 volatile 可以实现无锁并发,适用于竞争不激烈,多核CPU的场景下。
- 没有使用 synchronized ,线程不会阻塞,效率较高
- 如果发生竞争激烈,会多次重试,效率下降
CAS 底层依赖于一个 UnSafe 类 来直接调用操作系统的 CAS 指令
- CAS 是基于乐观锁的思想,最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,可以多次重试
- synchronized 是基于悲观锁思想,最悲观的估计,每次将线程上锁,当解锁后别人才可修改
juc 提供原子操作类,可以提供线程安全的操作, 例如:AtomicInteger、 AtomicBoolean 等, 底层实现采用 CAS + volatile 实现















































































































































