1. 内存结构
1.1 程序计数器
作用:记住下一条jvm指令的执行地址(跳转)
特点:线程私有,不会存在内存溢出
1.2 虚拟机栈
每个线程运行时所需要的内存
每个栈帧对应每次方法调用占用的内存
每个线程只有一个活动栈帧,对应当前正在执行的方法
栈帧过多,可能会导致栈内存溢出(StackOverflow)
1.3 堆
线程共享,需要考虑线程安全问题
有垃圾回收机制
堆内存溢出(OOM)
1.4 方法区(jvm1.8)
jvm中存在一个概念上的方法区,指向一块本地内存区域
本地内存区有一块元空间,存在着class文件,类加载器以及常量池
而StringTable存在于jvm的堆空间中。
常量池:一张表,存在于.class文件中,当该类被加载,它的常量池信息会放入运行时常量池
1.5 StringTable
常量池中字符串仅是符号,第一次用到时,变为对象(a -> “a”),被放入StringTable(串池)中
利用StringTable可以避免重复创建字符串对象
字符串变量拼接原理:StringBuilder
字符串常量拼接原理:编译器优化(“a” + “b” == “ab”)
intern方法可以将串池中没有的字符串对象放入串池
1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
StringTable调优:增加桶个数、考虑是否将字符串对象入池
2. 垃圾回收
2.1 四种引用
-
强引用
对象上有强引用,该对象就不能被垃圾回收 -
软引用
对象仅有软引用,在垃圾回收后内存仍不足时,回收该对象 -
弱引用
对象仅有弱引用,在垃圾回收时,无论内存是否充足,都会回收该对象 -
虚引用
配合引用队列 ByteBuffer使用,被引用对象回收时,会将虚引用入队,
由 Reference Handler 线程调用虚引用相关方法释放直接内存
2.2 垃圾回收算法
-
标记清除
速度快,但会造成内存碎片 -
标记整理
速度慢,但没有内存碎片 -
复制
没有内存碎片,但会占用双倍内存空间
可达性分析算法:探寻堆中所有存活的对象
不能沿着GC Root对象为起点的引用链找到的对象,表示可以回收
2.3 分代垃圾回收
新生代(伊甸园、幸存区From、幸存区To)、老年代
对象首先分配在伊甸园,伊甸园空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄+1,交换from和to。
minor gc 会引发 stop the world(后简称stw),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
当对象寿命超过15时,会晋升至老年代
当老年代空间不足,先尝试触发minor gc,如果空间仍不足,触发full gc
2.4 垃圾回收器
-
串行
单线程,适合个人电脑 -
吞吐量优先
让单位时间内stw时间最短,垃圾回收时间占比最低(所有线程一起垃圾回收) -
响应时间优先
让单次stw时间最短,响应时间最短
3. 类加载与字节码技术
3.1 回顾基础
静态代码块:类加载时执行,且只执行一次
类代码块:类被初始化时执行
finally语句块中出现return,返回结果以finally的为准
如果在 finally 中出现了 return,会吞掉异常,所以尽量不要在finally中出现return语句。
但是如果在finally中对try语句块中返回的变量值进行修改,不会有任何影响
因为jvm在执行finally语句块以前会暂存原值,目的是固定返回值
语法糖:是指java编译器把.java文件编译为.class字节码的过程中,自动生成和转换的一些代码。
3.2 类加载器
-
启动类加载器
是C++写的,无法直接访问。加载JAVA_HOME/jre/lib的类 -
扩展类加载器
上级为 Bootstrap,显示为 null,加载JAVA_HOME/jre/lib/ext的类
双亲委派机制:加载一个类时,首先通过启动类加载器加载,如果加载不到,再通过扩展类加载器加载,如果还没有加载到,再通过应用类加载器加载。应用类加载器专门加载我们配置jdk时classpath路径下的类。
线程上下文类加载器:当前线程使用的类加载器,默认就是应用类加载器。
自定义类加载器:继承 ClassLoader 父类,重写findClass方法,调用父类的defineClass方法加载类,使用者调用该类的loadClass方法,实现类的加载。
破坏双亲委派机制:自定义类加载器时重写loadClass方法就可以了。
4. 内存模型(jvm多线程)
内存模型(Java Memory Model)
JMM定义了一套在多线程读写共享数据时,数据的原子性、可见性、有序性的规则
4.1 多线程三大特性
4.1.1 原子性
多线程环境下,一个线程操作时,不会被其他线程所打扰(synchronized关键字保证)
4.1.2 可见性
在多个线程之间,一个线程对变量的修改对另一个线程可见,保证频繁读取某变量的线程不从高速缓存中读取,统一从主内存中读取(volatile关键字保证)synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized属于重量级操作,性能相对更低
4.1.3 有序性
多线程方法运行时可能会出现指令重排,可以使用volatile修饰变量,禁用指令重排
4.2 happens-before
happens-before:规定了哪些写操作对其它线程的读操作可见
- 线程解锁obj之前对变量的写,对接下来对同一obj加锁的其他线程的读可见
- 线程对volatile变量的写,对接下来其他线程对该变量的读可见
- 线程start前对变量的写,对该线程开始后对该变量的读可见
- 线程结束前对变量的写,对其他线程得知它结束后的读可见(比如其他线程调用join()方法等待它结束)
- 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见
4.3 CAS(compare and swap)
一种乐观锁思想,一般与volatile结合使用,实现无锁并发
它的工作原理:每次在修改并取得共享变量结果后都会进入CAS判断,如果返回false,重新读取,直到返回true,表示本线程改取变量数值的时候,其他线程没有改变这个值,退出循环。
优点:没有使用synchronized,线程不会陷入阻塞,可以提高效率
缺点:如果竞争激烈,很多线程都有可能修改变量,会导致重试次数过多,反而影响效率
乐观锁与悲观锁:CAS是乐观锁思想,就算别的线程修改了变量,我吃点亏,多重试几次。synchronized是悲观锁思想,我上了锁你们都别想改,我改完了解开锁,你们才能改。
4.4 锁
如果一个对象虽然有多线程访问,但是多线程访问的时间是错开的(没有竞争),那么可以使用轻量级锁。但是如果A线程访问期间,B线程也来访问,轻量级锁就会升级为重量级锁。
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
重量级锁竞争的时候也可以通过自旋来进行优化