三、JVM
· JVM堆的基本结构。
java_heap_struct.jpg
参考阅读:JVM内存堆布局图解分析
· JVM的垃圾算法有哪几种?CMS垃圾回收的基本流程?
基本的算法有:
标记-清除算法
等待被回收对象在被标记后直接对对象进行清除,会带来另一个新的问题——内存碎片化。如果下次有比较大的对象实例需要在堆上分配较大的内存空间时,可能会出现无法找到足够的连续内存而不得不再次触发垃圾回收。
复制算法(Java堆中新生代的垃圾回收算法)
此GC算法实际上解决了标记-清除算法带来的“内存碎片化”问题。首先还是先标记处待回收内存和不用回收的内存,下一步将不用回收的内存复制到新的内存区域,这样旧的内存区域就可以全部回收,而新的内存区域则是连续的。它的缺点就是会损失掉部分系统内存,因为你总要腾出一部分内存用于复制。
标记-压缩算法(或称为标记-整理算法,Java堆中老年代的垃圾回收算法)
对于新生代,大部分对象都不会存活,所以在新生代中使用复制算法较为高效,而对于老年代来讲,大部分对象可能会继续存活下去,如果此时还是利用复制算法,效率则会降低。标记-压缩算法首先还是“标记”,标记过后,将不用回收的内存对象压缩到内存一端,此时即可直接清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。老年代的垃圾回收称为“Major GC”。
jvm_gc.png
CMS的基本流程:
初始标记 :在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
并发标记 :这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。
并发预清理 :并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World。
重新标记 :这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"跟对象"开始向下追溯,并处理对象关联。
并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
并发重置 :这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收
延伸说明:
CMS(Concurrent Mark-Sweep) 在启动JVM参数加上-XX:+UseConcMarkSweepGC,这个参数表示对于老年代的回收采用CMS。CMS采用的基础算法是:标记—清除(Mark-Sweep)。CMS不会整理、压缩堆空间。这样就会有一个问题:经过CMS收集的堆会产生空间碎片。CMS的另一个缺点是它需要更大的堆空间。
如果你的应用程序对停顿比较敏感,并且在应用程序运行的时候可以提供更大的内存和更多的CPU(也就是硬件牛逼),那么使用CMS来收集会给你带来好处。还有,如果在JVM中,有相对较多存活时间较长的对象(老年代比较大)会更适合使用CMS。
· JVM有哪些常用启动参数可以调整,描述几个?
各主要JVM启动参数的作用如下:
-Xms:设置jvm内存的初始大小
-Xmx:设置jvm内存的最大值
-Xmn:设置新域的大小(这个似乎只对jdk1.4来说是有效的,后来就废弃了)
-Xss:设置每个线程的堆栈大小(也就是说,在相同物理内存下,减小这个值能生成更多的线程)
-XX:NewRatio:设置新域与旧域之比,如-XX:NewRatio=4就表示新域与旧域之比为1:4
-XX:NewSize:设置新域的初始值
-XX:MaxNewSize:设置新域的最大值
-XX:MaxPermSize:设置永久域的最大值
-XX:SurvivorRatio=n:设置新域中Eden区与两个Survivor区的比值。(Eden区主要是用来存放新生的对象,而两个Survivor区则用来存放每次垃圾回收后存活下来的对象)
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimestamps -XX:+PrintGCApplicationStopedTime
JVM启动参数使用中常见的错误:
java.lang.OutOfMemoryError相信很多开发人员都用到过,这个主要就是JVM参数没有配好引起的,但是这种错误又分两种:java.lang.OutOfMemoryError:Javaheapspace和java.lang.OutOfMemoryError:PermGenspace,其中前者是有关堆内存的内存溢出,可以同过配置-Xms和-Xmx参数来设置,而后者是有关永久域的内存溢出,可以通过配置-XX:MaxPermSize来设置。
· 如何查看JVM的内存使用情况?
jstat,jmap,jstack,j viusalvm
· Java程序是否会内存溢出,内存泄露情况发生?举几个例子。
静态集合类引起内存泄漏:
Static Vector v =newVector(10);for(inti =1; i<100; i++){Objecto =newObject(); v.add(o); o =null;}//o的对象还在Vector中,因此需要从中移除,或者直接把vector=null
各种资源性连接没有正确关闭.比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。
不正确使用单例模式是引起内存泄漏的一个常见问题.
当集合里面的对象属性被修改后,造成该对象的Hashcode改变了,再调用remove()方法时不起作用。
· 你常用的JVM配置和调优参数都有哪些?分别什么作用?
Trace跟踪参数(跟踪GC、类、变量的内存变化情况)
打开GC跟踪日志(每次执行GC的信息都能打印,获得执行时间,空间大小):
-verbose:gc 或 -XX:+printGC 或 -XX:+printGCDetails
类加载监控:(监控类加载的顺序)-XX:+TraceClassLoading
堆的分频参数
-Xmx1024M 指定最大堆,JVM最多能够使用的堆空间 (超过该空间引发OOM)
-Xms128M 指定最小堆,JVM至少会有的堆空间(尽可能维持在最小堆)
-Xmn 128M(new) 设置新生代大小
栈的分配参数
-Xss 每个线程都有独立的栈空间(几百k,比较小)需要大量线程时,需要尽可能减小栈空间
栈空间太小-----StackOverFlow栈溢出(一般递归时产生大量局部变量导致)
总结:
1.根据实际情况调整新生代和幸存代的大小
2.官方推荐:新生代占堆空间3/8
3.幸存代占新生代1/10
4.OOM时,dump出堆到文件,方便排查
· 常用的GC策略,什么时候会触发YGC,什么时候触发FGC?
常见内存回收策略:
串行&并行
串行:单线程执行内存回收工作。十分简单,无需考虑同步等问题,但耗时较长,不适合多cpu。
并行:多线程并发进行回收工作。适合多CPU,效率高。
并发& stop the world
stop the world:jvm里的应用线程会挂起,只有垃圾回收线程在工作进行垃圾清理工作。简单,无需考虑回收不干净等问题。
并发:在垃圾回收的同时,应用也在跑。保证应用的响应时间。会存在回收不干净需要二次回收的情况。
压缩&非压缩©
压缩:在进行垃圾回收后,会通过滑动,把存活对象滑动到连续的空间里,清理碎片,保证剩余的空间是连续的。
非压缩:保留碎片,不进行压缩。
copy:将存活对象移到新空间,老空间全部释放。(需要较大的内存。)
YGC :对新生代堆进行GC: edn空间不足
FGC的时机:old空间不足; 2.perm空间不足;3.显示调用System.gc() ,包括RMI等的定时触发; 4.YGC时的悲观策略;dump live的内存信息时(jmap –dump:live)。
四、多线程/并发
· 如何创建线程?如何保证线程安全?
保证线程安全: 竞争与原子操作、同步与锁、可重入、使用valatile防止CPU过度优化。
继承Thread类,必须重写run方法,在run方法中定义需要执行的任务。
实现Runnable接口。
MyRunnable runnable =newMyRunnable();Threadthread=newThread(runnable);thread.start(); class MyRunnable implements Runnable{...}
Note: Java如何创建进程:
第一种方式是通过Runtime.exec()方法来创建一个进程,第二种方法是通过ProcessBuilder的start方法来创建进程。
· 什么是死锁?如何避免死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
按同样的顺序访问(锁定)资源
尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。 3. 使用Atom原子操作(无锁模式)
· Volatile关键字的作用?
volatile特性一:内存可见性,即线程A对volatile变量的修改,其他线程获取的volatile变量都是最新的。volatile特性二:可以禁止指令重排序,避免重排序指令后访问数据错乱.
Volatile可以看做一种轻量级的锁,但又和锁有些不同。
a) 它对于多线程,不是一种互斥(mutex)关系。
b) 用volatile修饰的变量,不能保证该变量状态的改变对于其他线程来说是一种“原子化操作”。
· HashMap在多线程环境下使用需要注意什么?为什么?
并发操作HashMap,是有可能带来死循环以及数据丢失的问题的。当我们调用get()这个链表中不存在的元素的时候,就会出现死循环。
HashMap在并发执行put操作时会引起死循环,导致CPU利用率接近100%。因为多线程会导致HashMap的Node链表形成环形数据结构,一旦形成环形数据结构,Node的next节点永远不为空,就会在获取Node时产生死循环。
一句话总结就是,并发环境下的rehash过程可能会带来循环链表,导致死循环致使线程挂掉。
HashTable,它并未使用分段锁,而是锁住整个数组,高并发环境下效率非常的低,会导致大量线程等待。同样的,Synchronized关键字、Lock性能都不如分段锁实现的ConcurrentHashMap。
· Java程序中启动一个线程是用run还是start?
start方法:通过该方法启动线程的同时也创建了一个线程,真正实现了多线程。而run方法只是当初普通的方法调用.
· 什么是守护线程?有什么用?
守护线程(即daemon thread)是服务线程,处理后台事务,如垃圾回收等,JVM内部的实现是如果运行的程序只剩下守护线程的话,程序将终止运行,直接结束。所以守护线程是作为辅助线程存在的。应用程序里的线程,一般都是用户自定义线程,用户也可以在应用程序代码自定义守护线程,只需要调用Thread类的t.setDaemon(true);设置一下即可。
· 线程和进程的差别是什么?
进程是系统进行资源分配的基本单位,有独立的内存地址空间; 线程是CPU调度的基本单位,没有单独地址空间,有独立的栈,局部变量,寄存器, 程序计数器等。
创建进程的开销大,包括创建虚拟地址空间等需要大量系统资源; 创建线程开销小,基本上只有一个内核对象和一个堆栈。
一个进程无法直接访问另一个进程的资源;同一进程内的多个线程共享进程的资源。
进程切换开销大,线程切换开销小;进程间通信开销大,线程间通信开销小。
线程属于进程,不能独立执行。每个进程至少要有一个线程,成为主线程
· Java里面的Threadlocal是怎样实现的?
作用:在并发环境下避免竞争、简化编程,高效.在并发环境下提供了一个逻辑上全局的访问点访问线程本地对象
原理: 每个线程内部都有一个hastable作为存储存储器保存线程本地对象集,ThreadLocal实例对象作为key可以被所有线程共享,这个实例对象就是我们所说得全局的访问点,通过它可以访问线程本地对象。所以我们可以这样说:通过ThreadLocal提供了逻辑上全局的线程本地对象。
· ConcurrentHashMap的实现原理是?
在ConcurrentHashMap中,它使用了多个锁代替HashTable中的单个锁,也就是锁分离技术(Lock Stripping)。 每个hash区间使用的ReentrantLock锁。
ConcurrentHashMap源码剖析
· sleep和wait区别
这两个方法来自不同的类分别是Thread和Object
最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用.
synchronized(x){x.notify()//或者wait()}
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
· notify和notifyAll区别
wait此方法导致当前线程(称之为 T)将其自身放置在对象的等待集中,然后放弃此对象上的所有同步要求。
被wait的线程,想要继续运行的话,它必须满足2个条件:
由其他线程notify或notifyAll了,并且当前线程被通知到了
经过和其他线程进行锁竞争,成功获取到锁了
2个条件,缺一不可。其实在实现层面,notify和notifyAll都达到相同的效果,都只会有一个线程继续运行。但notifyAll免去了,线程运行完了通知其他线程的必要,因为已经通知过了。什么时候用notify,什么时候使用notifyAll,这就得看实际的情况了。
· 什么是条件锁、读写锁、自旋锁、可重入锁?
条件锁
在lock中提供了与之关联的条件,一个锁可能关联一个或多个条件,这些条件通过condition接口声明。目的是运行线程获取锁并且查看等待某一个条件是否满足,如果不满足则挂起直到某个线程唤醒它们。condition接口提供了挂起线程和唤起线程的机制;
读写锁
Java中读写锁有个接口java.util.concurrent.locks.ReadWriteLock,也有具体的实现ReentrantReadWriteLock,它们不是继承关系,但都是基于 AbstractQueuedSynchronizer来实现。ReentrantReadWriteLock的锁策略有两种,分为公平策略和非公平策略
注意: 在同一线程中,持有读锁后,不能直接调用写锁的lock方法 ,否则会造成死锁。。
可重入(Reentrant)锁
如果锁具备可重入性,则称作为可重入锁。像synchronized和 ReentrantLock都是可重入锁。假如某一时刻,线程A执行到了method1,此时线程 A获取了这个对象的锁,而由于method2也是synchronized方法,因为可重入,线程A不需要申请加锁即可执行。(可以理解锁的维度是线程,所以已拥有锁不需要再次申请加锁)。不可重入锁(自旋锁):不可以再次进入方法A,也就是说获得锁进入方法A是此线程在释放锁钱唯一的一次进入方法A。
可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
· 线程池ThreadPoolExecutor的几个参数说明?
corePoolSize 核心线程池大小
maximumPoolSize 线程池最大容量大小
keepAliveTime 线程池空闲时,线程存活的时间,
TimeUnit 时间单位
ThreadFactory 创建线程的工厂
BlockingQueue任务队列,用于保存等待执行的任务的阻塞队列。比如基于数组的有界ArrayBlockingQueue、,基于链表的无界LinkedBlockingQueue,最多只有一个元素的同步队列SynchronousQueue,优先级队列PriorityBlockingQueue
RejectedExecutionHandler 线程拒绝策略
线程池在执行excute方法时,主要有以下四种情况:
如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(需要获得全局锁)
如果运行的线程等于或多于corePoolSize ,则将任务加入BlockingQueue
如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(需要获得全局锁)
如果创建新线程将使当前运行的线程超出maxiumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
线程池采取上述的流程进行设计是为了减少获取全局锁的次数。在线程池完成预热(当前运行的线程数大于或等于corePoolSize)之后,几乎所有的excute方法调用都执行步骤2。
”我自己是一名从事了十余年的后端的老程序员,辞职后目前在做讲师,近期我花了一个月整理了一份最适合2018年学习的JAVA干货(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)从事后端的小伙伴们都可以来了解一下的,这里是程序员秘密聚集地,各位还在架构师的道路上挣扎的小伙伴们速来。“
加QQ群:585550789(名额有限哦!)
网友评论