深入java虚拟机面试

请你说一下jvm:

对于虚拟机我给它分成三大块内容分别是:内存管理、类加载引擎技术、线程安全

首先就内存管理来说,

虚拟机给内存划分为:堆、栈、方法区、运行时常量池、本地方法栈、直接内存。

堆是线程共享的内存块,而堆又细化分为eden区年轻代、survivor区幸存区、Old老年代。

在堆内存中创建对象的时候,首先判断eden区是否能满足当前对象的存储空间,如果不满足,则对eden进行使用复制算法进行gc,

将满足年龄对象移到幸存区,如果还不满足,则启动幸存区担保策略,对老年代使用 标记-整理 或 标记清除算法 进行Full GC,

将满足年龄的对象移到老年代,如果空间还是不满足,则报出OOM内存溢出异常。否则在eden区中分配对象。

而虚拟机对对象年龄的判断可以通过参数来设置,不过目前默认是动态设置,我认为这个方式也比较科学。

其次就是对于大对象,虚拟机也有做判断,如果超过参数指定的大小则直接放到老年代。

所以开发的时候应该避免大对象的使用,因为Full GC的是通过 stop world进行的,而且耗时比年轻代要大的多,

10倍以上,因此会造成服务短时间暂停服务的现象。

对于栈来说,是提供给线程独立使用的,每个线程都有自己的栈空间,线程的方法在栈中会独占一块空间称为栈帧,

程序的执行通常是通过出栈和压栈的方式将栈帧放到栈顶去执行的,栈内创建的私有变量和栈帧中的局部变量只能本线程访问,

如果通过传参的方式使栈帧中的变量被其他栈帧访问,则称为方法逃逸,如果栈中的私有变量被其他线程访问称为线程逃逸。

该技术在1.7以后虚拟机中给同步锁提供锁消除的技术支持。

方法区是当字节码数据经过类加载器加载后将类描述信息等一些指令和初始化的静态对象和常量存放的空间,1.7以后就和堆区别开

使用内存块了,当虚拟机进行GC的时候会顺便来回收一下方法区的对象。

本地方法栈使用的是系统内存,属于本地方法的c或c++的底层方法自己管理的内存

直接内存,是nio通过本地函数库直接分配的堆外内存,通过堆中的DirectByteBuffer对象来引用这块内存操作。

说到GC算法,那么就可以说到GC的收集器的选择。

年轻代收集可以使用并行收集器(parallel scavenge),它采用的是复制算法,并且可以通过参数来控制gc的时间或吞吐量,

当然也可以设置自适应调整。那老年代的收集就可以使用serial old收集器了,他是使用标记-整理算法。

当然如果你的服务对吞吐量和cpu特别敏感,那么不妨老年代使用parallel old来执行。

如果是对实时性要求比较高的网络应用来说,可以使用cms收集器,它是可以和用户线程一起执行的收集器,但是在收集垃圾的时候

用户线程响应会变慢,因为对cpu敏感,而且如果浮动垃圾过多无法处理的话,它会重新对老年代开启serial old收集器收集,停顿时间更长。

不过我比较看好G1收集器,它是将内存标记成若干个region,通过维护一个标记region按价值大小排序的队列,去根据允许的时间来优先收集

价值比较大的region,可以做到低停顿的响应时间和次数的平衡。

当然还可以扩展到jvm检测工具、jvm的实战优化

其次就是类加载引擎技术:

对执行引擎来说,它不会去管字节码的来源是.class文件还是来源与符合执行引擎规则的十六进制通过socket流来的数据集(这里就不把《深入理解java虚拟机》的列出来了,有兴趣的同学可以看书),

这都是可以执行的指令(热部署技术的核心思想就是基于该技术的实现)。

它会通过双亲委派模型来对这些数据集进行加载、验证、准备、解析、初始化加载到内存中。这里就不展开讲这些过程了。

线程安全:“当多个线程访问一个对象时,如果不用考虑这些线程的调度和交替执行,也无需额外进行同步,或者

调用方进行其他协同操作,最后调用这个对象的行为都可以获得正确的结果,这就是线程安全”

线程安全产生的原因是并发,接下来就可以聊并发的

锁、锁优化、volatile 的语义、cas、以及jdk的原子操作的类Atomioc*、

Concurrent包

———————————————————————————————–

如何判断对象可以被GC?

可达性分析,是指当一个对象的根节点指向为空的时候表示这个对象没有引用了,可以被回收。

————————————————————

乐观锁悲观锁:

悲观锁:每次拿数据都上锁,确保自己使用的过程中不会被其他线程访问,使用完后再解锁。

乐观锁:每次访问数据都不上锁,但是更新的时候去判断该期间是否被别人修改过

(阿里人说版本号控制,AtomicStampedReference类,

深入理解jvm书中jdk7版本[第十三章]说这个比较鸡肋,因为大部分情况下ABA问题不会影响并发正确性,

如果需要解决ABA问题传统的synchronized效率可能比这个要高)

期间数据可以被其他线程任意访问。

———————————————————————

ThreadLocal,主要用于变量安全隔离,实现原理是Thread引用ThreadLocal中的map来存储这个变量。每次访问变量都是访问当前线程的ThreadLocal中的map。

不过当不在使用map时一定要显示的remove掉当前线程key=value的变量,因为value是强引用,如果不显示干掉,其他的线程无无法访问到可能

会导致OOM的发送。key是弱引用,当当前线程结束就可以被回收了。hibernate的session就是通过ThreadLocal来保存实现安全隔离的。

——————————————————————————————-

说一下ReentrantLock比synchronized的优势?

区别:语法上ReentrantLock通过 lock() unlock() 来完成,synchronized是原生语法层面上的互斥锁

ReentrantLock 优势:

1.增加了等待可中断,

2.可实现公平锁(按申请时间顺序获得锁,通过构造方法带boolean值来要求使用公平锁,默认费公平)、

3.锁可以绑定多个条件(可多次通过newCondition()方法绑定条件)

———————————————————————

synchronized 实现原理:对象互斥同步通过临界区、互斥量、信号量等实现方式实现互斥。

java中通过monitorenter、monitorexit两个字节码指令和reference类型的参数来指明要锁定和解锁的对象。

如果synchronized修饰的是实例方法还是类方法,取对应的对象实例或Class对象做为锁对象。

当monitorenter执行时,开始尝试获取对象锁,如果这个对象没锁定或当前线程已经有了那个对象的锁,就将

锁计数器加1.而执行monitorexit则锁对象减1,如果获取锁对象失败,则将当前线程阻塞等待锁被其他线程释放。

在jdk1.7的hotspot之后,传统synchronized做了锁优化,效率跟ReentrantLock差不多了

锁优化:自旋锁:-XX: preBlockspin 改变自选次数、jdk1.6后默认开启通过虚拟机在线统计自旋成功概率比较高的次数动态设定

(自旋是,线程A持有对象锁后,线程B来尝试获取该锁,但发现已经被其他线程持有了,线程B就自己执行了一次空操作

jvm运行时,会统计自旋成功率最高的次数动态设置线程的自旋次数,超过该次数后线程释放执行权)

锁消除:检测到不可能存在数据竞争的锁进行消除,主要依靠逃逸分析技术支持

轻量锁:和传统锁一样通过使用操作系统的互斥量来实现的,但比传统锁更少的互斥量产生的性能消耗,旨在没有多线程竞争的前提下

线程获取锁的流程:通过在栈帧中建立LockRecord锁记录空间,然后通过cas将对象头Mark word信息写到锁记录空间,

如果更新失败检查是否已持有该锁(Mark word指向当前栈帧),否则产生竞争该锁膨胀为重量锁10

偏向锁:当获取第一个获取该锁的线程再次获取该锁则不用做同步,假如有其他线程尝试获取该锁,则结束该状态转向未锁定或轻量锁状态

—————————————————————

你跟我说一下并发:

并发是指通过压榨cpu资源达到应用服务性能提高的一种手段。随着并发的高效会产生共享数据的线程安全问题。

传统的做法是通过将共享资源同步,阻塞式的手段让共享数据实现线程安全。但是这样就会让效率的瓶颈卡在共享数据。

为了应对更多的线程安全问题,jdk中实现了两大类的技术手段,阻塞式,非阻塞式。

所谓的非阻塞式思想是基于硬件指令集的进步,语义上多次操作的行为使用一条指令完成。这就是CAS(原子性操作)。

jdk实现的类有AtomicInteger、AtomicStampedReference等类,都是通过volatile关键字来控制字面量

内部使用Unsafe.compareAndSwap提供支持,操作系统上(x86)cmpxchg指令来执行

————————–以下部分来源于importnew——————————————————————————————————

阻塞式的类:

ArrayBlockingQueue,有界阻塞队列的数组,按先进先出原则存取,定义定长后不可变,

如果试图在队列已满中插入元素会导致操作受阻塞,试图从空那个队列取元素将导致类似阻塞

适合生产消费者模型的公平策略。使用ReentrantLock锁住整个队列

LinkeBlockingQueue,基于已链接节点的,范围任意的队列,先进先出存取,吞吐量高于数组队列,但是其可预知的性能要低

要指定队列扩展范围,默认Integer.MAX_VALUE。队列没有超出容量的情况下,每次插入都是动态创建链接节点。

若队列没有指定容量,生产者的速度一旦大于消费者速度,会把系统内存全部吃掉。

使用读锁 和 写锁来控制同步

数组队列和链表队列的区别在于,链表动态扩展队列,每次在next创建节点,用读锁和写锁分别控制生产消费者的同步,

数组队列,创建后不能扩展,使用单个锁控制读写同步

ConcurrentLinkedQueue,使用非阻塞式线程安全策略,使用cas原理,在源码中使用hops>1(默认)来控制减少cas的写操作,

达到优化效率的目的

取节点:首先取head节点,判断是否为null,是则表示其他线程取走了,不为空则使用cas方式设置头节点引用为null,

如果设置失败表示已经被取走,需要重新获取head节点

ConcurrentHashMap,使用数组+链表的数据结构采用分段式segmens锁表技术来实现线程安全的map,

在get数据的时候segmens是不加锁的(volatile 的“先行原则”先写在读),put操作是先判断是否需要扩容,

第二定位存放的HashEntry,扩容只针对segment

CopyOnWriteArrayList,写时复制容器,当添加元素是,把旧的容器cp到新的容器中并添加元素,读的时候也能读到旧值,

因为旧的容器没有被锁。

线程管理:线程池:通过工程类Executors 去创建一个线程池,来管理线程的生命周期。

1.初始化ThreadPoolExecutor对象。当提交任务时,创建一个线程执行任务,直到指定线程数量等于corePoolSize

2.如果继续提交任务,则保存阻塞队列中等待被执行,如果阻塞队列被塞满了,且继续提交任务,则创建新的线程

处理任务且当前线程数量小于maximumPoolSize,如果队列饱和线程无空闲,则默认抛出异常AbortPolicy,

可选(丢弃任务,DiscardPolicy|CallerRunsPolicy用调用者来执行|DiscardOldestPolicy丢掉靠前的,执行当前任务)

如果跑完所以任务后线程会存活一定时间keepAliveTime

3.实现方法:

3.1 newFixedThreadPool 初始化指定数量的线程池,corePoolSize==maximumPoolSize,使用

LinkedBlockingQueue作为阻塞队列,没有任务也不释放线程

3.2 newCachedThreadPool 初始可以缓存线程的池子,默认缓存60s,maximumPoolSize=Integer.MAX_VALUE

使用 SynchronusQueue 作为阻塞队列,当线程空闲时间超过keepAliveTime,使用该方式注意控制线程数量

3.3 newSingleThreadExecutor。初始化的线程池中只有一个线程,如果线程异常结束,重启新的线程继续执行任务

内部使用LinkedBlockingQueue做阻塞队列

3.4 newSchedulerThreadPool 初始化线程池可以在指定的时间内周期性的执行所提交的任务,实际业务场景中

可以是hi用该线程池定期的同步数据。

execute()方法提交的任务必须实现Runnable接口,无法判断任务是否执行成功

ExecutorService.submit() 提交任务可以获取任务执行完的返回值

新建线程的流程:

1.通过workerCountOf方法根据ctl的低29为得到线程池的当前线程数,如果当前线程小于corePoolSize

则创建新线程执行任务,否则

2.判断线程是否是RUNNING状态,且提交的任务放到阻塞队列,否则再次检查线程池的状态,不是RUNNING

在执行reject按默认策略处理这个任务;

3.否则执行addWorker方法创建新线程执行任务,失败。执行reject处理任务。

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/a410215373/article/details/77802526
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞