4/13
今天是13号
解释下双亲委派机制以及为什么要设计双亲委派机制
答案就在《从JDK源码级别彻底剖析JVM类加载机制》课程里面有介绍
我的答案:
双亲委派就是每次加载类时首先委派父类加载器去加载,父类加载器没有找到再让子类加载器去加载 a.防止核心API被修改。b.保证类的唯一性
标准答案:
a)双亲委派机制加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
b)双亲委派机制可以实现沙箱安全机制与避免类的重复加载
4/14
今天问题是:
Tomcat 如果使用默认的双亲委派类加载机制行不行,为什么?
答案依然就在《从JDK源码级别彻底剖析JVM类加载机制》课程里面有介绍
我的答案:
答案是不行的。为什么?
第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
参考答案:
第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
第三个问题和第一个问题一样。
我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
4/15
今天问题是
说一下 JVM 的主要组成部分及其作用
答案请在《JVM内存模型深度剖析与优化》找寻答案
今天晴晴不开心,飞吻就只有伯乐老师的了
我的答案:
类装载子系统:将类装载到运行时数据区的方法区
运行时数据区:包括堆(存放实例对象,数组)、方法区(存放类的元数据)、虚拟机栈(执行java方法)、本地方法栈(执行native方法)、程序计数器(存放下一条要执行的字节码指令)
字节码执行引擎:执行字节码指令
参考答案:
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
Execution engine(执行引擎):执行classes中的指令。
Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
作用 :首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
4.16
今天问题
jvm优化原则是什么
答案请在《JVM内存模型深度剖析与优化》找寻答案
礼品就不说了,要保持神秘
参考答案:
今天问题参考答案
尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收。
4/17
4月18号,今日问题是
解释下对象栈上分配
答案请在《JVM对象创建与内存分配机制深度剖析》查找答案
我的答案:
为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
参考答案:
我们通过JVM内存分配可以知道JAVA中的对象都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。
今天19号
问题是:
class文件内部结构组成
答案请在“深度剖析class文件结构”查找
我的答案:魔数,主副版本号,常量池,访问标志,类索引,父类索引,接口技术器,接口信息数据区
参考答案
class字节码文件由魔数,主次版本号,常量池,类信息,类的构造方法,类的中的方法信息,类变量与成员变量等信息组成
今天是4月20号
问题是:
说下常量池类型分类
答案请在“深度剖析class文件结构”查找答案
参考答案:
按类型分为字面量、符号引用类型
字面量分:
CONSTANT_Utf8_info: utf8字符串
CONSTANT_Integer_info 整型字面量
CONSTANT_Float_info 浮点型字面量
CONSTANT_Long_info 长整型字面量
CONSTANT_Double_info 双精度字面量
符号引用类型分:
CONSTANT_Class_info:表示类或接口
CONSTANT_String_info:String类型的常量对象
CONSTANT_Fieldref_info:字段信息表
CONSTANT_Methodref_info:方法
CONSTANT_NameAndType_info:名称和类型表
CONSTANT_InterfaceMethodref_info:表示接口方法符号引用
CONSTANT_MethodType_info:方法类型表
CONSTANT_MethodHandle_info: 方法句柄表
CONSTANT_InvokeDynamic_info:动态方法调用点
今天4月21号,问题是:
说一下 JVM 运行时数据区
参考答案:
不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为以下 5 个部分:
1.程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
2.Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
3.本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
4.Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
5.方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
4月22日, 今天的问题是:
说一下 JVM 有哪些垃圾回收算法
请在“垃圾收集器ParNew&CMS与底层三色标记算法详解”查找答案
我的答案
分代回收
新生代: 复制算法(复制一块内存,把新生代存活的对象放入其中,原内存区域回收)
老年代:
标记-清除算法(标记存活的对象,统一回收未被标记的对象)
标记-整理算法(将标记的存活对象向一端移动,清理端边界之外的内存)
参考答案
标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
4/24
今天问题是:
生产级系统JVM线上运行情况如何评估
答案请 "JVM调优工具详解及调优实战"查找
我的答案
用 jstat gc -pid 命令可以计算出如下一些关键数据,有了这些数据就可以采用之前介绍过的优化思路,先给自己的系统设置一些初始性的JVM参数,比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。
年轻代对象增长的速率
可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。
Young GC的触发频率和每次耗时
知道年轻代对象增长速率我们就能推根据eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC 公式算出,根据结果我们大概就能知道系统大概多久会因为Young GC的执行而卡顿多久。
每次Young GC后有多少对象存活和进入老年代
这个因为之前已经大概知道Young GC的频率,假设是每5分钟一次,那么可以执行命令 jstat -gc pid 300000 10 ,观察每次结果eden,survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率。
Full GC的触发频率和每次耗时
知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。
参考答案:可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。
评估的主要指标有
a)Young GC的触发频率和每次耗时
b)每次Young GC后有多少对象存活和进入老年代
c)Full GC的触发频率和每次耗时
今天是4月25号
today问题是:
说下Arthas常用的JVM调优诊断功能
答案请 "JVM调优实战及常量池详解"查找
我的答案:
输入dashboard可以查看整个进程的运行情况,线程、内存、GC、运行环境信息:
输入thread可以查看线程详细情况
输入 thread加上线程ID 可以查看线程堆栈
输入 thread -b 可以查看线程死锁
输入 jad加类的全名 可以反编译,这样可以方便我们查看线上代码是否是正确的版本
ognl 命令可以查看线上系统变量的值,可以修改变量的值
4/26
ZGC运用了哪些技术?“
答案请在《为Java开疆拓土的ZGC深度剖析》查找"
参考答案:
a)着色指针技术,使用着色指针技术来快速实现GC中的并发标记、转移和重定位等功能
b)堆空间使用分页模型
c)巧妙的使用转发表技术实现GC中的对象的并发转移
d)使用读屏障技术解决指针修正问题
4/27
问题是:
在对象创建过程中的分配内存这一步具体是如何做的
答案我也不知道哪视频里
参考答案:
分配内存有两种方法:指针碰撞和空闲列表
a)“指针碰撞”(Bump the Pointer)(默认用指针碰撞)
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
b)“空闲列表”(Free List)
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录。
今天是4月28日
今天问题是:
解释漏标问题以及解决方案
我依然忘记在哪有这个知识
参考答案:
漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案: 增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB) 。
增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。
原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)
4/29
今天问题是:
简述并发和并行的区别
答案在“并发编程之深入理解JMM&并发三大特性(一)” 查找
参考答案:
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。 并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在。
4/30
今天问题是:
谈谈伪共享问题和其解决方案
答案在“并发编程之深入理解JMM&并发三大特性(二)” 查找
参考答案
如果多个核的线程在操作同一个缓存行中的不同变量数据,那么就会出现频繁的缓存失效,即使在代码层面看这两个线程操作的数据之间完全没有关系。这种不合理的资源竞争情况就是伪共享(False Sharing)。
避免伪共享方案 :1.缓存行填充 2.使用 @sun.misc.Contended 注解(java8)
5/01
今天问题是:
为什么说创建Java线程的方式本质上只有一种?Java线程和go语言的协程有什么区别?
答案在“并发编程之深入理解Java线程” 查找
参考答案
本质上Java中实现线程只有一种方式,都是通过new Thread()创建线程,调用Thread#start启动线程最终都会调用Thread#run方法。
Java线程属于内核级线程,线程的创建、撤消、切换都由内核实现。 结合并发专题深入理解Java线程课程理解。
协程,是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特性。
今天是5月2日,今天问题是:
CAS存在那些缺陷?ABA问题如何解决?
答案在“并发编程之CAS&Atomic原子操作详解” 查找
参考答案:
CAS 虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方面:
1.自旋 CAS 长时间不成功,则会给 CPU 带来非常大的开销
2.只能保证一个共享变量原子操作
3.ABA 问题
ABA问题解决方案
数据库有个锁称为乐观锁,是一种基于数据版本实现数据同步的机制,每次修改一次数据,版本就会进行累加。同样,Java也提供了相应的原子引用类AtomicStampedReference
5/3
是今天问题是:
聊聊synchronized加锁解锁的实现原理?
答案在“并发锁机制之深入理解synchronized(一)” 查找
参考答案:
这块一定要理解管程(Monitor)模型之MESA模型。synchronized是基于Monitor机制实现的,jvm层面一个Object对应一个Monitor,多线程竞争获取锁失败的线程会进入同步队列cxq的头部(后进先出),如果获取锁的线程调用wait()会进入条件队列waitSet。唤醒逻辑比较复杂,存在不同的唤醒策略。释放锁时,默认策略(QMode=0)是:如果EntryList为空,则将cxq中的元素按原有顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取锁。EntryList不为空,直接从EntryList中唤醒线程。
今天是5/4号
是今天问题是:
synchronized锁对象是如何记录锁状态的?
答案在“并发锁机制之深入理解synchronized(一)” 查找
参考答案:
需要了解对象的内存布局。锁标记是记录在Mark Word中的:
001 无锁
101 偏向锁
00 轻量级锁
10 重量级锁
5/5
今天问题是:
synchronized自旋发生在哪个阶段?为什么要设计自旋操作?
答案在“并发锁机制之深入理解synchronized(二)” 查找
参考答案:
自旋发生在重量级锁阶段。轻量级锁阶段没有自旋操作,cas获取锁失败,直接开始膨胀逻辑,获取monitor对象后进入重量级锁阶段。在重量级锁获取锁期间,如果cas失败,会进入自适应自旋尝试获取锁,如果一直失败,会park当前线程。
重量级锁park涉及系统调用,挂起开销太大。
5/6
是今天问题是:
偏向锁、轻量级锁、重量级锁分别应用于什么场景?
答案我也不知道在哪
参考答案:
偏向锁: 锁对象偏向于当前线程,适用于只有一个线程获取锁的场景,不存在竞争
轻量级锁: 适用于多个线程交替获取锁的场景,不能出现竞争,如果存在竞争就会升级为重量级锁(轻量级锁一次cas失败就会膨胀获取monitor)
重量级锁:使用于多个线程同时获取锁,存在竞争。
晚上的问题
如何自定义类加载器
参考答案
自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。
5/7
今天问题是:
聊聊你对AQS的理解?
答案请在“深入理解AQS之独占锁ReentrantLock源码分析”查找
参考答案:
java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这些行为的抽象就是基于AbstractQueuedSynchronizer(简称AQS)实现的,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。
AQS是JDK层面对管程(Monitor)的实现,实现了两种队列:同步队列和条件队列
AQS内部维护属性volatile int state ,表示资源的可用状态,提供了两种资源共享方式:独占和共享。
晚上问题
简述 Java 内存分配与回收策略以及 Minor GC 和 Major GC。
参考答案:• 对象优先在堆的 Eden 区分配
• 大对象直接进入老年代
• 长期存活的对象将直接进入老年代
当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次 Minor GC。Minor GC 通常发生在新生代的 Eden 区,在这个区 的对象生存期短,往往发生 Gc 的频率较高,回收速度比较快; Full GC/Major GC 发生在老年代,一般情况下,触发老年代 GC 的时候不会触发 Minor GC,但是通过配置,可以在 Full GC 之 前进行一次 Minor GC 这样可以加快老年代的回收速度。
5/8
今天是5月8日,今天问题是:
聊聊synchronized和ReentrantLock的区别
答案请在“深入理解AQS之独占锁ReentrantLock源码分析”查找
参考答案:
synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;
synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;
synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以被中断的;
在发生异常时synchronized会自动释放锁,而ReentrantLock需要开发者在finally块中显示释放锁;
ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;
synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(回顾一下sychronized的唤醒策略),而ReentrantLock对于已经在等待的线程是先来的线程先获得锁;
晚上问题
请聊聊方法区是什么?
参考答案:
方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。 方法区还有一块内存,运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。 在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。 异常情况: 1.方法区调用递归,内存会溢出,报OutOfMemoryError; 2.当常量池无法再申请到内存时OutOfMemoryError;
今天是5/9号,今天问题是:
CountDownLatch的应用场景
答案请在“深入理解AQS之Semaphorer&CountDownLatch&Cyclic详解”查找
参考答案:
让多个线程等待:模拟并发,让并发线程一起执行
让单个线程等待:多个线程(任务)完成后,进行汇总合并
晚上问题
描述一下 JVM 加载 Class 文件的原理机制?
参考答案:
Java 语言是一种具有动态性的解释型语言,类(Class)只有被加 载到 JVM 后才能运行。当运行指定程序时,JVM 会将编译生成 的 .class 文件按照需求和一定的规则加载到内存中,并组织成为 一个完整的 Java 应用程序。这个加载过程是由类加载器完成,具 体来说,就是由 ClassLoader 和它的子类来实现的。类加载器本 身也是一个类,其实质是把类文件从硬盘读取到内存中。 类的加载方式分为隐式加载和显示加载。隐式加载指的是程序在使 用 new 等方式创建对象时,会隐式地调用类的加载器把对应的类 加载到 JVM 中。显示加载指的是通过直接调用 class.forName() 方法来把所需的类加载到 JVM 中。 任何一个工程项目都是由许多类组成的,当程序启动时,只把需要 的类加载到 JVM 中,其他类只有被使用到的时候才会被加载,采 用这种方法一方面可以加快加载速度,另一方面可以节约程序运行 时对内存的开销。此外,在 Java 语言中,每个类或接口都对应一 个 .class 文件,这些文件可以被看成是一个个可以被动态加载的 单元,因此当只有部分类被修改时,只需要重新编译变化的类即可, 而不需要重新编译所有文件,因此加快了编译速度。 在 Java 语言中,类的加载是动态的,它并不会一次性将所有类全 部加载后再运行,而是保证程序运行的基础类(例如基类)完全加 载到 JVM 中,至于其他类,则在需要的时候才加载。
类加载的主要步骤:
• 装载,根据查找路径找到相应的 class 文件,然后导入。
• 链接,链接又可分为 3 个小步:
• 检查,检查待加载的 class 文件的正确性。
• 准备,给类中的静态变量分配存储空间。
• 解析,将符号引用转换为直接引用(这一步可选)
• 初始化。对静态变量和静态代码块执行初始化工作。
5/10
今天问题是:
CountDownLatch与Thread.join的区别
答案请在“深入理解AQS之Semaphorer&CountDownLatch&Cyclic详解”查找
参考答案:
CountDownLatch的作用就是允许一个或多个线程等待其他线程完成操作,看起来有点类似join() 方法,但其提供了比 join() 更加灵活的API。
CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减一操作,也可以在一个线程里调用n次执行减一操作。
而 join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活。
晚上问题
什么是类加载器,类加载器有哪些?
参考答案:
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类 加载器。
主要有一下四种类加载器:
• 启动类加载器(Bootstrap ClassLoader)用来加载 Java 核 心类库,无法被 Java 程序直接引用。
• 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类 加载器在此目录里面查找并加载 Java 类。
• 系统类加载器(system class loader):它根据 Java 应用 的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。
• 用户自定义类加载器,通过继承 java.lang.ClassLoader 类 的方式实现。
今天是5月11日,问题是:
CyclicBarrier与CountDownLatch的区别
答案请在“深入理解AQS之CyclicBarrier&ReentrantReadWriteLock”查找
参考答案:
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次
CyclicBarrier还提供getNumberWaiting(可以获得CyclicBarrier阻塞的线程数量)、isBroken(用来知道阻塞的线程是否被中断)等方法。
CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同。CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后,再执行。CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行。
CyclicBarrier 还可以提供一个 barrierAction,合并多线程计算结果。
CyclicBarrier是通过ReentrantLock的"独占锁"和Conditon来实现一组线程的阻塞唤醒的,而CountDownLatch则是通过AQS的“共享锁”实现
晚上问题:
JVM的分代年龄为什么要设计为15?
参考答案:
因为Object Header采用4个bit位来保存年龄,4个bit位能表示的最大数就是15!
今天是5月12日,问题是:
条件队列到同步队列的转换实现逻辑
答案请在“深入理解AQS之CyclicBarrier&ReentrantReadWriteLock”查找
参考答案:
调用await()的时候会释放锁,然后线程会加入到条件队列,调用signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁
结合CyclicBarrier源码理解
晚上问题
串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?
参考答案
吞吐量收集器使用并行版本的新生代垃圾收集器,它用于中等规模 和大规模数据的应用程序。 而串行收集器对大多数的小应用(在 现代处理器上需要大概 100M 左右的内存)就足够了。
今天是5月13日,今天问题是:
读写锁ReentrantReadWriteLock是怎样实现分别记录读写状态的?
答案请在“深入理解AQS之ReentrantReadWriteLock详解”查找
参考答案
读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,用一个变量state维护读写状态:高16为表示读,低16为表示写。
Java内存模型和JVM内存模型
参考答案:
这两者是不一样的,经常有人把它们搞混,Java内存模型简称JMM是一种符合计算机内存模型规范的,屏蔽了各种硬件和操作系统的访问差异,保证了java程序在各种平台下对内存的访问都有一致性效果的一种机制及规范,目的是解决多线程并发情况下数据共享的原子性、可见性、有序性问题。常说的 JVM 内存结构指的就是JVM的运行时数据区,其中堆、方法区被线程共享,程序计数器、栈、运行时常量池被线程独享。它描述的是,在运行时,字节码和代码数据存储的位置。
今天是14号,今天问题是:
ReentrantReadWriteLock存在什么缺陷?如何改进?
答案请在“深入理解AQS之ReentrantReadWriteLock详解”查找
参考答案
ReentrantReadWriteLock有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。为了进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock。StampedLock和ReentrantReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!在原先读写锁的基础上新增了一种叫乐观读(Optimistic Reading)的模式。该模式并不会加锁,所以不会阻塞线程,会有更高的吞吐量和更高的性能。
晚上问题
堆里面的分区:Eden,survival (from+ to),老年代,各自的特点?
参考答案:
堆里面分为新生代和老生代(java8取消了永久代,采用了Metaspace),新生代包含Eden+Survivor区,survivor区里面分为from和to区,内存回收时,如果用的是复制算法,从from复制到to,当经过一次或者多次GC之后,存活下来的对象会被移动到老年区,当JVM内存不够用的时候,会触发Full GC,清理JVM老年区 当新生区满了之后会触发YGC,先把存活的对象放到其中一个Survivor区,然后进行垃圾清理。因为如果仅仅清理需要删除的对象,这样会导致内存碎 片,因此一般会把Eden 进行完全的清理,然后整理内存。那么下次GC 的时候, 就会使用下一个Survivor,这样循环使用。如果有特别大的对象,新生代放不下, 就会使用老年代的担保,直接放到老年代里面。因为JVM 认为,一般大对象的存 活时间一般比较久远。
5/15
今天问题是:
聊聊你对阻塞队列的理解
答案请在“阻塞队列BlockingQueue实战及其原理分析”查找
参考答案:
BlockingQueue 继承了 Queue 接口,是队列的一种。Queue 和 BlockingQueue 都是在 Java 5 中加入的。阻塞队列(BlockingQueue)是一个在队列基础上又支持了两个附加操作的队列,常用解耦。两个附加操作:
支持阻塞的插入方法put: 队列满时,队列会阻塞插入元素的线程,直到队列不满。
支持阻塞的移除方法take: 队列空时,获取元素的线程会等待队列变为非空
BlockingQueue 是线程安全的,我们在很多场景下都可以利用线程安全的队列来优雅地解决我们业务自身的线程安全问题。
晚上问题:Java 类加载过程是怎么样的?
参考答案:Java 类加载需要经历一下 7 个过程: 1. 加载 加载是类加载的第一个过程,在这个阶段,将完成一下三件事情: • 通过一个类的全限定名获取该类的二进制流。 • 将该二进制流中的静态存储结构转化为方法去运行时数据结 构。 • 在内存中生成该类的 Class 对象,作为该类的数据访问入口。 2. 验证 验证的目的是为了确保 Class 文件的字节流中的信息不回危害到 虚拟机.在该阶段主要完成以下四钟验证: • 文件格式验证:验证字节流是否符合 Class 文件的规范,如 主次版本号是否在当前虚拟机范围内,常量池中的常量是否 有不被支持的类型. • 元数据验证:对字节码描述的信息进行语义分析,如这个类是 否有父类,是否集成了不被继承的类等。 • 字节码验证:是整个验证过程中最复杂的一个阶段,通过验 证数据流和控制流的分析,确定程序语义是否正确,主要针 对方法体的验证。如:方法中的类型转换是否正确,跳转指 令是否正确等。 • 符号引用验证:这个动作在后面的解析过程中发生,主要是 为了确保解析动作能正确执行。 3. 准备 准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些 内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的 内存,实例变量将会在对象实例化时随着对象一起分配在 Java堆中。 public static int value=123;//在准备阶段 value 初始值为 0 。在初 始化阶段才会变为 123 。 4. 解析 该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一 定在初始化动作完成之前,也有可能在初始化之后。 5. 初始化 初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段 用户应用程序可以通过自定义类加载器参与之外,其余动作完全由 虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。 6. 使用 7. 卸载
5/16
晚上问题:Java 中垃圾收集的方法有哪些?
参考答案:
标记 - 清除:这是垃圾收集算法中最基础的,根据名字就可以知 道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种 方法很简单,但是会有两个主要问题: 1. 效率不高,标记和清除的效率都很低; 2. 会产生大量不连续的内存碎片,导致以后程序在分配较大的 对象时,由于没有充足的连续内存而提前触发一次 GC 动作。
复制算法:为了解决效率问题,复制算法将可用内存按容量划分为 相等的两部分,然后每次只使用其中的一块,当一块内存用完时, 就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块 内存,再将第二块上的对象复制到第一块。但是这种方式,内存的 代价太高,每次基本上都要浪费一般的内存。 于是将该算法进行了改进,内存区域不再是按照 1:1 去划分,而是将内存划分为 8:1:1 三部分,较大那份内存交 Eden 区,其余 是两块较小的内存区叫 Survior 区。每次都会优先使用 Eden 区, 若 Eden 区满,就将对象复制到第二块内存区上,然后清除 Eden 区,如果此时存活的对象太多,以至于 Survivor 不够时,会将这 些对象通过分配担保机制复制到老年代中。(java 堆又分为新生 代和老年代)
标记 - 整理:该算法主要是为了解决标记 - 清除,产生大量内存 碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。 它的不同之处就是在清除对象的时候现将可回收对象移动到一端, 然后清除掉端边界以外的对象,这样就不会产生内存碎片了。
分代收集:现在的虚拟机垃圾收集大多采用这种方式,它根据对象 的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生 存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。 老年代里的对象存活率较高,没有额外的空间进行分配担保。
5/17
今天问题是:
SynchronousQueue公平和非公平实现原理是什么
答案请在“阻塞队列BlockingQueue实战及其原理分析(2)”查找
参考答案:
公平模式:底层使用TransferQueue, 队尾匹配(判断模式),队头出队,先进先出
非公平模式(默认策略):底层使用TransferStack,栈顶匹配,栈顶出栈,后进先出
结合阻塞队列二课上画的图进行理解
parNew的参数有哪些
-XX:SurvivorRatio 设置伊甸园空间大小与幸存者空间大小之间的比率。默认情况下,此选项设置为8
-XX:PreTenureSizeThreshold 大对象到底多大,大于这个值的参数直接在老年代分配
-XX:MaxTenuringThreshold升代年龄,最大值15, 并行(吞吐量)收集器的默认值为15,而CMS收集器的默认值为6。
-XX:+ParallelGCThreads 并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同
-XX:+UseAdaptiveSizePolicy 自动选择各区大小比例