JVM面试笔记

秋招的时候做的最多的准备的东西就是JVM,结果就是每次面试官问JVM的时候我都能信誓旦旦地答得近乎完美,然后后边的东西都不咋会,让面试官期望降低,这可不太行,看来秋招不能只有三板斧,得啥都会呀。

一.JVM内存划分
线程私有:
(1)虚拟机栈
虚拟机中的栈内存,描述Java方法执行的内存模型,每个方法被执行的时候都会创建一个栈帧,存储局部变量表,操作栈,动态链接,方法出口;每个方法被调用到执行完毕的过程相当于一个栈帧入栈到出栈的过程
(2)程序计数器
计数器指向当前线程执行的class文件字节码的行号
(3)本地方法栈
虚拟机栈为Java方法(字节码)服务,本地方法栈为虚拟机使用到的native方法服务
native方法:Java调用非Java代码的接口
线程共享:
(4)堆
存放对象实例,物理上不一定连续,逻辑上连续;垃圾回收器主要管理区域;在虚拟机启动时被创建,所有线程共享;是Java虚拟机内存最大的一块
(5)方法区【包含运行时常量池】
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
(0)可以这样理解,栈中存放的是一个对象的地址,指向堆中的对象实例,程序计数器指向要操作的字节码,方法区存储静态变量,常量等静态数据。
二.判定对象是否死去(是否可回收)
1.引用计数法
给对象添加一个引用计数器,每有一个地方引用它,计数器就+1,引用失效时,计数器-1.
如果一个对象的计数器为0,那么就可回收。
引用计数法的缺点是难以解决对象之间的循环引用问题,如果对象A和B互相引用,但是在线程中没有用到A和B,这两个对象不会被回收,那么这块空间就浪费了。
2.根搜索算法(可达性分析)
以一系列名为GC Roots的对象为起点,从这些节点出发开始搜索,如果一个对象没有任何路径可以到达GC Root,那么这个对象是可回收的
可作为GC Root的对象:
(1)虚拟机栈中的引用对象
(2)方法区中类静态属性引用的对象
(3)方法区中常量引用对象
(4)本地方法栈中JNI引用对象
三.Java4种引用方式
1.强引用(Strong Reference)
Object o=new Object();
只要强引用还在,垃圾收集器就永远不会收回该对象
2.软引用(Soft Reference)
还有用,但非必需的对象,在系统发生内存溢出异常之前,将这些对象列入垃圾回收范围,进行第二次回收
3.弱引用(Weak Reference)
只能生存到下次垃圾回收发生前,当垃圾收集器工作时,无论是否内存满,都会被回收
4.虚引用(Phantom Reference)
无法通过虚引用获取一个对象实例,它唯一的目的就是在这个被关联的对象被回收时可以收到一个系统通知
四.垃圾收集算法
1.标记-清除算法
分为标记和清除两个阶段,先标记所有需要回收的对象,标记完成后统一回收
缺点:清除后的内存不连续,会产生大量不连续的内存碎片;效率不高;
2.复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。
优点:每次都是针对一半的内存进行回收,内存分配时不需要考虑内存碎片的情况,只需要移动堆顶指针,按顺序分配即可,实现简单,运行高效
缺点:由于内存缩小到了原来的一半,所以当对象存活率较高时,需要进行很多的复制操作,效率降低
一般来说虚拟机都采用复制算法回收新生代,内存分为三个区,一个Eden和两个Survivor比例是8:1:1,一块较大的Eden和两块Survivor,将Eden和一个Survivor中存活的对象复制到另一个Survivor中,只浪费了10%的内存
3.标记-整理算法
将所有存活的对象向内存的一端移动,其余的清除
4.分代收集算法
根据对象的存活周期,将堆区划分为新生代和老年代
对于新生代,对象死亡率高,所以采用复制算法,只需复制少量存货对象即可
对于老年代,对象存活率高,所以采用标记清除或标记整理算法
Minor GC:新生代垃圾回收,发生在新生代的垃圾收集,新生代中的Java对象频繁死亡,所以GC操作频繁,回收速度快
Full GC:老年代垃圾回收,对象存活率高,速度一般比Minor GC慢10倍以上
五.堆内存的划分
1.Young Generation Space:新生区/新生代,存储新创建的对象,内存小,垃圾回收频繁,分为一个Eden和两个Survivor(互相转换的A和B),垃圾回收时,如果目标复制区已满,则转入老年代;如果对象经过几次垃圾回收扫描仍存活,将转入老年代
2.Tenure Generation Space:养老区/老年代,用于存储长时间引用的对象,内存大,垃圾回收频率较小。
3.Permanent Space:永久存储区,存储不会改变的类定义,字节码,常量等
Java堆内存分区的目的
是为了进行模块化管理,对不同的对象,进行不同的操作,提高JVM执行的效率
六.分代收集算法
根据对象的存活周期,将堆区划分为新生代和老年代
对于新生代,对象死亡率高,所以采用复制算法,只需复制少量存货对象即可
对于老年代,对象存活率高,所以采用标记清除或标记整理算法
内存分配原则:
(1)对象优先分配在新生代的Eden
(2)较大的对象直接进入老年代
(3)长期存活的对象将被转入老年代
(4)动态判定对象年龄
(5)空间分配担保
七.Class文件
事实上,无论是什么语言,只要成功编译成正确的class文件,都可以在JVM上执行
class文件是一组8位字节为基本单位组成的二进制流。
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件,接下来的第5,6字节是副版本号,7,8字节是主版本号
八.类加载器
1.类加载器的作用
加载一个类,同时确定这个类的唯一性。
对于一个类,由类加载器和类本身来确定这个类在Java虚拟机中的唯一性。这由双亲委派模型机制来实现,即使是同一个class文件,只要类加载器不同,就不是同一个类
2.类加载器分类:
1.启动类加载器
用本地代码实现的类加载器,是虚拟机的一部分,负责将/lib目录下的核心类库加载到内存中,不能直接被Java程序引用
2.扩展类加载器
由ExtClassLoader实现,将/lib/ext目录下的类库加载到内存,可以直接使用
3.应用程序类加载器
由APPClassLoader实现,负责将用户类路径(即当前类所在路径及其引用的第三方类库的路径)上的类 加载到内存,可以直接使用
九.类加载机制
1.双亲委派模型:
类加载器相当于一个栈,从上到下的结构
双亲委派模型要求除了最顶层的启动类加载器之外,其余任何一个类都有自己的父类加载器,当一个类请求加载的时候,不会尝试自己加载这个类,不会启动它自己的类加载器,而是将请求委派给父类加载器,依次递归向上调用直到最顶层的启动类加载器,只有当父类加载器无法完成这个请求时,子加载器才会自己尝试加载
2.目的:
保障类的唯一性,比如Object类,是java.lang.Object,如果其他地方也有object类的定义,那么加载类的时候就无法确定是哪一个类被加载,在双亲委派模型的作用下,无论是哪一个类加载器想要加载Object类,都会传递到最顶层的启动类加载器,保证了Object类在各个类加载器环境中都是同一个类
能够有效确保一个类的全局唯一性
十.运行时栈帧
一个栈帧就是一个虚拟机进行方法调用和执行的数据结构,存储着方法的一些属性:
1.局部变量表
2.操作数栈
3.动态链接
4.方法返回地址
每一个方法从调用到返回的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
十一.解释器和编译器
Java虚拟机可以选择解释执行和编译执行方法里的字节码指令
这里指的是JVM执行.class文件的解释和编译,.class文件是与平台无关的字节码文件,正常来说是JVM将其解释执行,但是可以通过编译,将其转化为本地机器码来提高效率
1.解释执行:使用解释器执行,当程序需要快速启动和执行的时候,解释器发挥作用,省去编译的时间,直接执行
2.编译执行:使用即时编译器产生本地代码然后执行,当程序运行后,随着时间的推移,编译器逐渐发挥作用,越来越多的代码被编译成本地机器码,提高执行效率
3.即时编译器:当虚拟机发现某一块代码或某个方法执行频率高,就会把这段代码认定为热点代码,为了提高执行效率,虚拟机会把热点代码编译成本地机器码,并进行优化,完成这个任务的编译器就叫做即时编译器
4.热点代码:被多次调用的方法和被多次执行的循环体
热点代码的判断:
1.基于采样的热点探测:虚拟机周期性检查各个线程的栈顶,如果一个方法多次出现在栈顶,就被认定为热点方法
2.基于计数器的热点探测:虚拟机会为每个方法建立计数器,统计方法执行的次数,如果计数器的值超过了一个范围,这个方法就被认定为热点代码
如果内存资源限制较大,可以用解释器直接执行以节约内存,如果资源充足,就可以用编译器编译执行来提高效率,编译器的代码还可以退化成解释器的代码
即时编译器的优化技术:
1.公共子表达式消除:用已经计算出的结果来替换公共子表达式
2.数组范围检查消除:判定循环内的变量取值范围在数组区间内,就消去数组边界检查
3.方法内联:略
4.逃逸分析:逃逸分析的基本行为就是分析对象动态作用域:
一个对象可能产生方法逃逸和线程逃逸,如果能证明一个对象不会逃逸到方法或线程外(即不会被其他未知的方法和线程引用),则可能为这个变量进行一些高效的优化。
如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者被其它线程所引用。如果一个变量不会逃逸出线程,那也就不存在竞争的问题,就可以去除同步的操作以节省时间。
十二.javac编译的过程(将.java文件编译成.class文件)
1.解析与填充符号表
2.插入式注解处理器的注解处理
3.分析与字节码的生成
十三.物理机处理并发问题
计算机的存储设备和处理器之间加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache),作为内存与处理器之间的缓冲:将运算需要的数据复制到缓存中,让运算快速运行。当运算结束后再从缓存同步回内存,这样处理器就无需等待缓慢的内存读写了。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是引入了一个新的问题:缓存一致性。在多处理器系统中,每个处理器都有自己的高速缓存,它们又共享同一主内存。当多个处理器的运算任务都涉及同一块主内存时,可能导致各自的缓存数据不一致。
为了解决一致性的问题,需要各个处理器访问缓存时遵循缓存一致性协议(MESI)。
十四.Java内存模型
Java内存模型是JVM的规范,屏蔽掉不同硬件和操作系统的内存访问差异,实现让Java在各个平台下都能达到一致的并发效果。
1.主内存与工作内存:
所有的变量(非局部变量,局部变量是线程私有不存在竞争问题)都存在于主内存;
每条线程有自己的工作内存,保存着被该线程使用到的从主内存复制过来的变量副本;
线程之间不能相互访问,只能通过主内存交互,线程间变量的传递也需要通过主内存;
线程不能对主内存的变量直接读写,只能在工作内存中进行操作,对变量的副本进行操作;
十五.并发编程三个性质:原子性,可见性,有序性

并发程序要正确地执行,必须要保证其具备原子性、可见性以及有序性;只要有一个没有被保证,就有可能会导致程序运行不正确
原子性:原子性指的是相应的操作是单一不可分的;对基本数据类型的访问和修改是具备原子性的,对于更大范围的原子性,可以使用关键字synchronized,它转换成字节码指令就是monitorenter和monitorexit进行的lock和unlock操作,所以synchronized是具备原子性的
可见性:当一个线程修改了共享变量的值,其他线程可以立刻知道这个变化,Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取之前从主内存刷新变量值来实现可见性的。volatile和synchronized都可以实现可见性。
有序性:如果在本线程内观察,所有操作都是有序的,如果观察其他线程,所有操作都是无序的,volatile和synchronized都可以实现有序性。
十六.volatile
JVM提供的轻量级同步机制,可以实现有序性和可见性,但是不能实现原子性;在并发下不一定是安全的。但是某些情况下的性能要优于synchronized。
被定义成volatile的变量保证了该变量的可见性和有序性(禁止指令重排序);
volatile变量的读操作的效率和普通变量是一样的,但是写操作由于需要设置指令屏障防止指令乱序,写操作的效率要比普通变量低。
十七.并发与多线程
Java中的并发是由多线程来实现的;线程:线程是CPU调度的基本单位,各个线程既可以独立调度,又可以共享进程资源(内存地址、文件IO等)
线程的实现方法:
1.使用内核线程
2.使用用户线程
3.用户线程+轻量级进程
线程的创建方式:
1.继承Thread类并重写run方法,创建对象,调用start方法
2.实现Runnable接口,重写run方法,把实现Runnable接口的类的对象作为参数传入Thread类的start方法,启动线程
3.实现Callable接口并使用Future,重写call方法,创建对象,使用FutureTask包装callable对象,用FutureTask作为Thread中start方法的参数,调用start方法启动线程,FutureTask的get方法可以获取线程结束后的返回值
十八.Java线程调度
线程调度是系统为线程分配处理器使用权的过程
Java语言的线程分为1-10的10个优先级,优先级越高的线程,越可能被先执行
线程调度的方法:
1.协同式调度:实现简单,没有线程同步问题,但是线程执行时间不可控,系统容易崩溃
2.抢占式:每个线程由系统分配执行时间,不会出现一个线程导致整个进程阻塞的情况
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步;或者在调用方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果;那这个对象就是线程安全的。
在Java中的不可变对象绝对是线程安全的
十九.如何实现线程安全
JVM提供了同步和锁的机制
1.阻塞(互斥)同步
互斥是实现同步的手段;临界区,互斥量,信号量,都是互斥同步的实现方式;
Java中最基本的同步手段就是synchronized关键字,编译后会在被标记的代码块的前后分别加上monitorenter和monitorexit字节码指令。
锁有计数器,在加monitorenter的时候计数器加一,monitorexit的时候计数器减一
如果获取对象失败,那当前线程需要阻塞等待,直到对象锁被释放
除synchronized之外,可以使用java.util.concurrent包中的重入锁(Reentrantlock)实现同步
Reentrantlock比synchronized多了几个功能:等待可中断,可实现公平锁,锁可以绑定等
等待可中断:持有锁的线程长期不释放,等待线程可以放弃等待。
公平锁:多个线程在等待同一个加锁对象时,必须按照申请的时间顺序依次获得锁,synchronized中的锁是非公平的
2.非阻塞同步
阻塞同步中锁是悲观锁,由于任何情况都需要加锁,维护锁计数器,和检查是否有被阻塞的线程需要被唤醒等操作,会对性能造成很大影响;
非阻塞同步是乐观的并发策略,基于冲突检测的乐观并发策略,先进行操作,如果没有其他线程使用锁,就成功操作,如果发生了冲突,再进行补救措施;这种乐观的并发策略的很多实现,不需要线程挂起操作,所以叫非阻塞同步。
二十.JDK1.6的锁优化
1.自旋锁
如果电脑是多个CPU,可以让多个线程并行,那么让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放。为了让线程等待,我们只需让线程执行一个循环(自旋)。避免线程切换的开销。
缺点:占用了处理器的时间,如果等待时间很长,就会白白浪费处理器资源。所以自旋要有一个时间限度,如果超过了阈值,就应该挂起(传统手段)。
自适应自旋:根据前一次在同一个锁上的自旋时间以及锁的拥有者的状态决定时间阈值
如果一个锁对象,自旋刚成功获得锁,并且持有锁的线程正在运行,那么JVM认为这次自旋仍然可能成功,进而运行自旋等待更长的时间。
如果对于某个锁,自旋很少成功,那在以后要获取这个锁,可能省略掉自旋过程,以免浪费处理器资源
2.锁消除
虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。
3.锁粗化
原则上,锁的作用范围要尽量小,但是如果一系列的连续操作都对同一个对象反复加锁解锁,频繁地进行操作也会产生不必要的损耗,锁粗化就是增大锁的作用域。
4.轻量级锁
在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
5.偏向锁
在无竞争的情况下,把整个同步都消除掉。这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。
二十一.JVM内存溢出问题



未完待续

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值