JVM从入门到精通(超详细汇总所有JVM知识点,包括面试题)

目录

JVM内存结构

垃圾回收

判断一个对象死亡的方法

垃圾回收算法

垃圾收集器

Synchronized关键字

Synchronized原理

锁升级

偏向锁

轻量级锁

面试回答Synchronized锁升级思路1:


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. 轻量级锁就是线程会进行自旋,如果自旋次数多了,就会升级为重量级锁。还有一个就是,如果还有更多的线程进来,发现有人在自旋,那么也会升级为重量级锁。

理解辅助:

偏向锁:上厕所不用排队,就你一个人在家,不用锁门,每次直接进来。   (单线程)

轻量级锁:家里来了一个朋友,有一个朋友要上厕所,就在外面等一会儿马上就好,门也不用锁。  (少量线程数,竞争少,时间短,不阻塞)

重量级锁:家里来了一堆朋友,个个抢着上厕所,于是就把门锁上。  (大量线程竞争,阻塞)

猜你喜欢

转载自blog.csdn.net/x18094/article/details/114140817