目录
JVM内存结构
JVM内存空间分为五部分,分别是:方法区、堆、Java虚拟机栈、本地方法栈、程序计数器。
线程共有:方法区、堆
线程私有:虚拟机栈、本地方法栈、程序计数器
方法区:常量、静态变量、类信息、运行时常量池(字面量、符号引用)。
堆主要存放的是数组、类的实例对象、字符串常量池等。
栈存放变量,包括基本类型变量,局部变量,对象的引用(地址)。
运行本地方法(可能不是java实现,而是c实现的方法)
程序计数器:线程切换时,记录执行位置,以便后面重新执行。
栈和堆的关系就是,堆放对象内容,栈放对象的引用地址。
栈
一个类里面的方法,在栈中都会有一个栈帧,栈帧又包含了局部变量表、操作数栈、方法出口、动态链接,方法执行完虚拟机释放空间。
局部变量表:存放变量
操作数栈:计算时,压入操作数栈中,最后从栈顶取出
方法出口:指定方法结束时返回地址。
动态链接:符号引用一部分在类加载阶段转为为直接引用,另一部分在运行期间转化为直接引用,这部分称为动态链接。(静态解析|动态链接)
栈溢出:方法递归容易造成栈溢出。因为是递归,方法一直未结束,所以一直创建变量,堆满了栈的空间,所以造成溢出。
栈溢出代码:
public class Test {
public void test(){
String a;
test();
}
public static void main(String[] args) {
Test t =new Test();
t.test();
}
}
堆
堆存放对象和数组,凡是new出来的都放堆中。
栈和堆的关系就是,堆放对象内容,栈放对象的引用地址。
堆超过阈值 就是常见的内存溢出OutOfMemoryError。
堆是动态分配内存,是不连续的内存区域,由Java垃圾回收器管理。
public class Test {
public static void main(String[] args) {
List list = new ArrayList<>();
while (true) {
list.add(1);
}
}
}
栈和堆的关系:堆放对象内容,栈放对象的引用地址。
垃圾回收
判断一个对象死亡的方法
判断对象死亡的方法:引用计数法和可达性分析法。
引用计数法就是通过计数器值控制,被引用就加1,引用失效就减1. 缺点:A引用B,B引用A就无法回收,也就是循环引用。
可达性分析法就是挑选一个稳定的对象作为GCROOT,然后寻找可达的对象,不可达就回收。缺点:产生内存碎片。
垃圾回收算法
垃圾回收算法有:引用计数法、标记--清除算法、复制算法、标记--压缩(整理)算法、增量算法。
引用计数法:对象被别人引用,它的计数器加1,引用结束减1,对象的计数器值为0就回收。 缺点:循环引用就可能没发回收。
标记清除算法:根据根节点寻找可达对象,如果不可达就回收。缺点:产生空间碎片。
复制算法(新生代串行垃圾回收器使用):把空间分钟两块,回收的时候就复制到另一块区域,然后删除本区域。
标记压缩算法(老年代中使用):它在标记--清除算法的基础上做了一些优化。在一块内存空间内,标记可达的对象,压缩到内存的一边,然后删除其他对象,这样就不会产生内存碎片。
垃圾收集器
Parallel Scavenge 复制算法,重要的特点就是:关注系统的吞吐量。
Parallel OLD 采用标记压缩算法
CMS收集器 标记-清除算法 回收停顿时间会比较小,但是相应的牺牲了吞吐量 适合B/S架构
G1 收集器(Garbage First) G1 收集器是目前最新的垃圾回收器,与 CMS 收集器相比,G1 收集器是基于标记--压缩算法的,它不会产生内存碎片
它将整个Java堆划分为多个大小相等的独立区域(Region),region集合定义为年轻代和老年代,但他们不再是物理隔阂。
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
Synchronized关键字
Synchronized原理
用javap反汇编可以看到底层的JVM指令,有一个monitorenter和两个moniterexit指令, 分别是加锁和释放锁。一个moniterexit是正常的情况,还有一个是异常的时候。
还有就是锁升级的过程。
锁升级
对象大致可以分为三个部分,分别是对象头、实例变量和填充字节。
对象头由MarkWord和Klass Point(类型指针),Klass Point是是对象指向它的类元数据的指针,Mark Word用于存储对象自身的运行时数据。
实例变量存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐
填充字符,因为虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍的
偏向锁--》轻量级锁--》重量级锁
具体:不是每次都有竞争(单线程)--》引入偏向锁---有多个线程竞争---》引入轻量级锁----竞争的线程会自旋等待,如果还有其他更多的线程开始竞争---》引入重量级锁---线程会阻塞,但CPU不再消耗自旋操作。
偏向锁
jdk1.6以前Synchronized还是重量级锁,性能不是很好,Hotspot作用就发现不是每次都有那么多线程竞争,于是有了偏向锁。
对象头Mark word和栈帧里面放了线程ID,当同一个线程再次进来的时候,发现是同一个ID,就直接进来了。
如果不是同一个线程进到同步块,就会根据Kclas指向的内容查看是否存活.
(1)如果死了,就会CAS重新加锁,更新新的线程ID.
(2)如果存活,就会去栈帧里面查看是否还需要持有这个锁对象。
(2.1)如果不需要再持有,就撤销,重新偏向新的线程ID
(2.2)如果还需要持有锁,则暂停该线程,取消偏向锁,升级为轻量级锁。
偏向锁可以通过参数配置来取消-XX:-UseBiasedLocking = false
轻量级锁
如果竞争的线程不多,并且等待的时间不长,就用轻量级锁,通过自旋等待锁释放。
轻量级锁是在多个:
(1) 线程的情况下如果第二个线程进来,发现锁对象的对象头已经被占用,就会自旋等待,如果等待时间长,就会升级为重量级锁。
(2)第三个线程进来,发现有另一个线程在自旋等待,也会升级为重量级锁。
注意:锁升级后不能再降级!
面试回答Synchronized锁升级思路1:
1.这个是jdk1.6以后的优化吧,就是Hotspot作者发现不是每次都有那么多线程来竞争,就引出了偏向锁,偏向锁呢就是有一个对象头的概念,对象头里面有markword和kclass point,markword里面会存线程ID等其他运行时信息。有线程进来的时候就cas更新线程ID,但是不会释放锁,下次同一个线程就可以直接进来。
2.如果有多个线程来竞争的话,这个时候cas的时候如果失败了,首先会去对象头里可以知道有没有存活,,也会去栈帧里面再去验证是否可存活,如果还存活,就会升级为轻量级锁。3. 轻量级锁就是线程会进行自旋,如果自旋次数多了,就会升级为重量级锁。还有一个就是,如果还有更多的线程进来,发现有人在自旋,那么也会升级为重量级锁。
理解辅助:
偏向锁:上厕所不用排队,就你一个人在家,不用锁门,每次直接进来。 (单线程)
轻量级锁:家里来了一个朋友,有一个朋友要上厕所,就在外面等一会儿马上就好,门也不用锁。 (少量线程数,竞争少,时间短,不阻塞)
重量级锁:家里来了一堆朋友,个个抢着上厕所,于是就把门锁上。 (大量线程竞争,阻塞)