视频
100个P的视频,内容重复的一堆,我只能找出这么多感觉有用的,大家自己看的时候注意一下。
随看随记
jvm如何认定两个对象同属于一个类型,必须同时满足下面两个条件:
- 都是用同名的类完成实例化的。
- 两个实例各自对应的同名的类的加载器必须是同一个。比如两个相同名字的类,一个是用系统加载器加载的,一个扩展类加载器加载的,两个类生成的对象将被jvm认定为不同类型的对象。
能不能自己写个类叫java.lang.System?
- 答案:通常不可以,但可以采取另类方法达到这个需求。
- 解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。
- 但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器加载一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。
ThreadLocal的内存泄漏问题
- 每次使用完ThreadLocal都调用它的remove()方法清除数据
- 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
G1什么时候触发FullGC
- 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
- 从老年代分区转移存活对象时,无法找到可用的空闲分区
- 分配巨型对象时在老年代无法找到足够的连续分区
GC
程序的栈
- 每个线程对应一个栈,每个栈中都有很多个栈帧,每个方法对应一个栈帧。
JVM的GC历史
- 如何判断对象是否是个垃圾
- (JVM不用,Python使用)引用计数法,通过对被检查的对象的引用进行计数,如果为0,则表示该对象是垃圾。但是这个方法对于循环引用不起作用。
- 根可达算法,GCroot,通过从变量的根开始向下指向,直到结果,对于已经没有的引用的,被视为垃圾。
- 可以当作GC根的有:线程栈变量,静态变量,运行时常量池,JNI变量。
算法
- 标记清除:标记以后,进行清除,会产生碎片
- 复制:一分为二,有用的复制到空闲空间。浪费空间
- 标记整理:标记以后,进行清除,同时向前整理效率低
垃圾回收器
管理模型
- 仅仅在新生代进行的GC成为MinGC,老年代的内存满了会触发FullGC
- 分代模型:年轻代,老年代。
- 分区模型:分成一个一个的小格。
分代类型垃圾回收器(Stop The World)
- JDK1.8 默认PS + PO
Serial + Serial Old
- Serial 用于年轻代,单线程,时停(STW)的拷贝算法。
- Serial Old 用于老年代,单线程、时停(STW)的使用标记整理算法算法
- Serial串行回收器(单CPU效率最高,虚拟机是Client模式的默认垃圾回收器)。
- Stop The World 当进行垃圾回收的时候,会让所有的进程停止,仅仅进行垃圾回收,回收之后,在继续业务。
Parallel Scavenge + Parallel Old(多个回收线程并行清理)
- 和Serial + Serial Old 一样 ,只不过在垃圾回收时采用多线程的形式。
ParNew
- 和Parallel Scavenge 类似,仅仅是帮助CMS进行更好的适配。
三色标记算法(用在并发标记阶段)
- mark word 会有 三色标记算法颜色标记
- 三种颜色
- 白色的表示没看到。
- 灰色表示自己的检测到了,但是子类或者对象等还没有检测。
- 黑色表示全都结束了。
- 新的线程每次从灰色的开始就向下遍历就可以了
- 会有原来标记到,但是后来有不需要标记导致的误删除,在多线程的情况下,进行下面的操作。例如下面的图片中,B和D断开连接,A和D建立连接,这时就会变成下面的图。由于A是黑色的,并不会从A开始进行遍历,因此会导致D会一直为白色的,成为垃圾被回收了。
CMS(Concurrent Mark Sweap)(并发,业务线程和回收线程能够同时执行)
- 初始标记仅仅会标记GC根,因此会很快。
- 在并发阶段使用三色标记算法
- 原来被引用的指针,在并发时没有引用了,这时会产生浮动垃圾。
- 原来认定是垃圾的对象,在并发时发现有被其他对象引用了,会导致错删。
- 初始标记和并发标记没有标记到业务进行过程中产生的垃圾,必须在下一次垃圾清理时进行清理。
解决方案(Incremental Update)
- 增加修改
- CMS的操作是把A的颜色置为灰色
- 在最初时也会有问题,如果A对象的1引用已标记结束,2没有标记,但是当业务逻辑把上述的操作放到这里时,CMS不会去再次从1属性开始标记,而是从2开始。这样也会导致D被漏标,会导致D漏删。
- 所以,CMS的remark阶段,必须从头把刚才修改为灰色的标记重新扫描一遍。因此,CMS在remark阶段的具有较长的stw时间。
G1(物理上不分代,逻辑上分代)(摒弃了分代模型,采用了分区模型)
SATB解决方案(Snapshot At the Beginning)
- 如果B对D的引用取消了,这时G1会记录被取消的对象(D),然后在下次回收的时候,会判断是否被黑色的引用了,如果被引用了,则不会把D当成垃圾,继续标记,如果没被黑色引用,则被当成垃圾,回收。
- 对于如何寻找D是否被其他的对象引用,是通过在D的对象分区表(Rset)的前列会存储被其他块的引用表(Rset),如果有则表示D的对象被其他对象引用了,如果没有,则表示没有被引用,当成垃圾。
- 图中的每一个小块就是一个小分区。每一个小块的头部都会有Rset。
- G1效率高的原因就是,对每一个小块进行GC操作,而不会像以前那样对整个分区进行操作。
ZGC(Zero Pause GC)
颜色指针(后面再说)
GC调优
- -X 是非标准参数
-Xms<size> 设置初始 Java 堆大小
-Xmx<size> 设置最大 Java 堆大小
-Xss<size> 设置 Java 线程堆栈大小
GC真题
JVM(loading)
- Java从编码到执行
- 首先Javac将Java编译成字节码,字节码在JVM中先使用类加载器将我们写的Java文件,和类库的文件进行加载,使用字节码解释器,对类加载器中的文件进行解释,然后到硬件中去执行。
- 对于频繁使用的类,JVM会使用JIT及时编译器,将其编译成本地代码,减轻解释器的压力,提高Java的运行效率。
- JVM : Java虚拟机,在解释字节码文件。
- JRE : Java运行时环境,包括JVM + 核心类库
- JDK : Java开发工具包,包括JRE + 开发工具包。
Class文件的内容
class文件的加载
- 类加载的流程
- 装载字节码
- 连接
- 校验:校验字节码(检查魔数是否正确)
- 准备:将静态变量赋初值,赋值为0
- 解析:将逻辑地址转化为内存的物理地址。
- 初始化,将静态变量赋初始值,是开发者提供的数值。
双亲委派的类加载模式
- 父加载器是查找关系,并不是继承关系
- 根类加载器:BootStrap :加载核心类,由C++实现的核心类库
- 拓展类加载器:Extension:加载jre/lib/ext/*.jar内的所有jar包
- 项目路径类加载器:App:加载classpath下的jar包
- 自定义类加载器:Custom ClassLoader,加载自定义的classloader
- 下图中的输出为null的表示是从根类加载器中加载的,因为使用C++写的,Java识别不出来,因此是null。
- 对于获取类加载器的类加载器,都是从BootStrap类加载其中加载出来的,因此是null。
双亲委派流程程
- 双亲委派过程是一个一去一回的流程。
- 在检查类是否被加载时,Custom ClassLoader向上到根加载器(BootStrap)询问是否有该类的加载器,如果有则返回结果。如果没有则从BootStrap向下到Custom ClassLoader尝试寻找字节码,如果找到字节码,则加载对应的加载器,到缓存中。如果没找到,则返回Class Not Found异常
- 如果我们假装写了一个java.lang.String类,如果没有双亲委派的话,就会直接加载类加载器,然后加载对象到内存中,对于使用者来说是不会被发现的,这已经不是系统的String了,如果在假的String上做手脚,就会导致安全问题。
- 还有一个好处就是不必要重复加载,能够节省资源。
父加载器
编译
- Java采用混合模式,既有解释器(bytecode intepreter),又有编译器(JIT)。
- 混合使用解释器+热点代码编译。
- 起始阶段采用解释器解释,如果发现热点代码,则将其编译到本机能执行的语言,提高运行速率。
- JVM支持三种模式混合模式:
- -Xmixed 先解释,针对重点代码编译。
- -Xint 解释器解释,启动很快,执行很慢
- -Xcomp:使用纯编译,启动很慢,执行很快。
懒加载(JVM没有规定什么时候加载,一般采用懒加载模式,当需要的时候再加载)
小结
JVM(linking)
准备状态的赋初值问题
输出3
输出2
- 对于第一个图来说,在类加载时,count会赋初值0,t赋初值null,然后count执行< cinit > ,初始化为2,count = 2,t 执行new语句,count++,结果为3
- 对于第而个图来说,在类加载时,t赋初值null,count会赋初值0,t 执行new语句,count++,count = 2然后count执行< cinit > ,初始化为2,结果为2
对象初始化问题
- 队形初始化也分为三个步骤
- 分配空间,同时对象赋默认值。
- 对象赋初始值。
- 指针引用该内存位置。
DCL(Double Check Loading)
- 双重检查的单例写法,如果不加volatile就会导致CPU内部指令重排,导致上述的顺序为1、3、2,当赋初始值的动作还没有做,但是有其他线程来的时候,会导致该线程使用的是默认值而不是初始值,称为对象的半初始化,这样会导致bug。
JMM
- 每个线程都有自己的虚拟机栈,虚拟机栈会拷贝堆中的对象到栈中,这样就会导致,当拷贝过的栈中数据被修改时,不同的线程在处理数据时,就会导致数据的不一致,引发并发问题,因此需要缓存一致性协议。
- MESI协议:Modified修改的,Excluasive独享的,Shared共享的,Invaild无效的
- 现代CPU采用缓存锁+总线锁的方式进行数据一致性协议。
MESI
缓存行
- CPU在读取内存数据时,不会一次仅读取一个数据,而是直接读取一行,放到缓存中,这一行数据被称作缓存行。目前一般时64个字节。
- 伪共享问题,在于同一个缓存行中的数据,如果被两个不同的CPU锁定,会导致这整个缓存行依次交替上锁,交替从内存中更新缓存行,影响效率。
- 解决方案时像下面的图一样,采用对齐的方式,因为一个缓存行64字节,一个long是8个字节,因此仅仅使用缓存行中的最后一个字节,或者第一个字节,其他都是空字节,来保证效率。
CPU的乱序执行问题
- CPU为了提高效率,会在一条指令的执行过中,去同时执行另一条数据,前提是这两条指令没有依赖关系。对于费时去找内存的指令,CPU会在同时执行其他的指令,来提高执行效率。
如何解决
- 指令重排和内存屏障
- 指令重排的两个原则:
- as-if-serial:不论怎么进行重排序,单线程的程序执行结果不能发生改变。因此有数据依赖关系的操作不会进行重排。
- happens-before:其中的volatile原则:volatile变量的写,先发生于读,这保证了volatile变量的可见性,简 单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的 线程总是能够看到该变量的最新值。
volatile
- 可见性:被volatile修饰的变量对所有的县城都是可见的。
- 禁止重排:
- volatile无法保证原子性,因为volatile仅仅能保证一个变量的可见性,而原子性需要一段操作的原子性,因此仅仅通过synchronized来完成。
读写屏障的五层实现
硬件层的内存屏障
- lfence:loadfence 读屏障,lfence之前的读操作必须在,lfence之后的读操作先完成。(仅对读施加屏障)
- sfence:savefence 写屏障,sfence之前的写操作必须在,sfence之后的写操作先完成。(仅对写施加屏障)
- mfence:mfence之前的读写操作必须在,mfence之后的读写操作先完成。(对读写都加屏障)
JMM
- 每个线程栈都会有自己的工作内存,工作内存会从主内存中拷贝自己的线程需要用到的数据,如果用到volatile会加屏障
JVM虚拟机提供的四种屏障
- volatile写之前所有的操作都要结束(不许重排,增加XXXXStoreBarrior),volatile读之后所有的操作都要重新开始(不许重排LoadXXXXBarrior),
- 一个另外先volatileStore 后volatileLoad也会使用了StoreLoadBarrior.
- 因为volatile要实现同步,因此在volatile写操作之前,所有的写操作都不许重排。volatile在读操作之前,所有的读操作都不许重排。
Object o = new Object();
- 开辟内存空间,同时赋值默认值,0、0.0、null。
- 对象赋初始值
- 栈对象引用
对象在内存中的存储布局
- 一个对象包括4个部分:第1和2也叫做对象头。
- MarkWord(8个字节)
- 类型指针(4个字节)(这个对象属于哪个类)
- 对象实例这个对象的实例数据
- 对齐占位(凑位数用要凑满至少16个字节,多个往16的整数倍上凑)
- 一个数组对象包括5个部分:
- MarkWord(8个字节)
- 类型指针(4个字节)(这个对象属于哪个类)
- 数组长度(4个字节)
- 对象实例这个对象的实例数据
- 对齐占位(凑位数用)
- MarkWord 中包含的信息有锁信息,HashCode(),GC相关信息,锁信息就是锁的升级,从无锁到偏向锁到轻量级锁到重量级锁的过程,
- 类型指针,指向的是T.class对象。
对象如何定位
- 间接定位:对象小,GC时不需要频繁改动栈中的指针指向,缺点是需要两次方法访问。
- 直接定位:访问对象快速,但是GC时会比较麻烦
对象如何分配
- 流程
- 首先会尝试在栈中能否进行分配,及西宁逃逸分析和标量替换,如果可以,就直接在栈中分配,如果不可以,在堆中分配。
- 如果对象过于大们无法在年轻代进行分配,就直接在老年代进行分配。
- 如果年轻代够,就先在伊甸区进行分配,然后不断的去判断该对象是否存货,如果存活,采用复制算法,将活下来的对象复制到S1或S2中,当年龄足够大时,放到老年代中。