JVM
1.JVM内存区域
OOM了解吗?什么情况下会出现OOM?
对栈来说,如果没有空间来为新的栈帧开辟空间就会产生OOM
对永久代如果空间不够会抛出OOM
对堆:如果空间不够也GC没用的情况下也会抛出OOM
一般可能是内存泄漏或者是内存溢出照成的。
StackOverflow出现的场景?
对栈来说,如果线程请求的栈深度大于虚拟机所能接受的栈深度会抛出SOF
1.JVM内存区域分为五大部分:
虚拟机栈
java线程的栈帧,线程的每一个方法会生成一个栈帧,用于存放该线程的局部变量表、操作数栈、动态连接、方法出口等信息。随着方法的调用与结束,对应着入栈与出栈;局部变量表中存放着方法参数和内部定义的局部变量,基本存储单位是方法槽。
动态连接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,这个引用是为了支持方法调用过程中的动态连接(DynamicLinking) 这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析 另外一部分符号引用将在每一次运行期间都转化为直接引用,这部分就称为动态连接
操作数栈:java虚拟机栈中的一个用于计算的临时数据存储区
本地方法栈 Native方法的栈帧 和虚拟机栈差不多,只不过是调用ni方法的栈帧
堆 几乎所有的对象都在这里分配内存
堆分为老年代、新生代、永久代;
新生代分为Eden FromSurvivors To Survivors 8:1:1
这两个Survivors有什么用呢?
在分配内存时,每次只使用一块Eden和 Survivor然后使用标记复制算法将其复制到另一块Survivor上。当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实 际上大多就是老年代)进行分配担保(Handle Promotion)。
方法区 :
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
程序计数器:
当前线程字节码文件的行号指示器,通过改变该计数器的值来获得下一步执行的字节码指令;程序控制流的指示器,程序当中的分支、循环、线程恢复、跳转、异常处理都要由它指示。由于Java虚拟机通过时间片轮转实现多线程,所以每一个线程都需要一个程序计数器记录当前执行的位置。
3.对象访问定位的方式:
句柄池:每一个句柄 数据结构中包括对象数据的指针以及类的指针
直接指针 在对象数据中存放类指针
5堆内存中对象分配的基本策略:2种
碰撞指针、空闲表
并发问题如何解决
CAS乐观锁+失败重试 先创建失败了就重试
TLAB 为每个线程在Eden初设一点内存,现在TLAB分配 当TLAB用完后,用CAS+失败重试
2.垃圾回收
一个长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,就会造成内存泄漏。
ThreadLocalMap中存在内存泄漏。
如何判断对象是否死亡:
如果没有任何一个对象引用该对象则判断为死亡(可达性方法)
(引用计数法)没引用一次加一取消引用减一;不好解决互相引用的问题;
GCRoots有哪些?
虚拟机栈中引用的对象,本地方法栈中引用的对象
方法区中常量引用的对象,方法区中类静态属性引用的对象
7.如何判断常量是否无用,类是否无用?
常量无用:没有引用即可
类无用:该Class类没有被引用、没有该类的对象还在存活、该类的ClassLoader已经销毁
分代收集理论
如今的垃圾回收器大都是遵循分代收集。建立在两个分代假说:弱分代假说,强分代假说。垃圾回收器应该将Java堆分为不同的存储区域,根据其年龄进行收集。
跨代引用假说,跨代引用相对于同代引用仅占用极少数。
如何解决跨代引用的问题?
在新生代维护一个记忆集,将老年代分为若干块,标识出哪一块会存在跨代引用,在mingc的时候将它加入到GCroot中。
MinorGC MajorGC FullGC 新生代、老年代、整堆收集
正是因为垃圾回收器每次只回收某一个或者某些部分的区域,才有了这三个回收类型的划分。然后根据不同的区域采取不同的回收算法
什么时候会FullGC?
- System.gc()方法的调用
- 老年代空间不足(新生代转入、创建大对象)
- CMS无法处理浮动垃圾、有可能出现并发清理失败进而导致另一次完全“Stop The World”的Full GC的产生
垃圾回收算法?各有什么优缺点
引用计数算法和追踪式算法(引用式算法实现比较简单但是不好解决循环引用的问题)
追踪式算法基于分代假说理论,分别在各自的内存区域回收各自的无用对象,关于如何解决跨代引用,需在新生代上建立一个全局的数据结构(该结构被称 为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会 存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
标志清除 产生内存碎片 /执行效率不稳定:可能需要回收的对象占大部分
标记复制 解决了内存碎片 可用空间减为一半,改进后在新生代使用。
标记整理 慢,适合老年代
什么是STW?
必须全部暂停用户线程
什么是安全点?
可达性分析算法中通过根节点枚举来查找引用链,然后通过OopMap来得到对象的引用地址,因为Oopmap变化是实时的不能为每一条指令都生成oopmap,只在安全点生成oopmap,然后让所有的线程跑到最近的安全点 停下来即可完成STW并且进行根节点枚举。
记忆集与卡表
—有其他分代区域中对象引用了本区域对象时,其对应的 卡表元素就应该变脏,
通过写屏障(Write Barrier)技术维护卡表状态的
垃圾回收器
三个年轻代 Serial ParNew Parrllel Scavenge
三个老年代 CMS SerialOld Parallel Old
然后是G1 SGC ZGC
Serial/Serial Old
这两个的运行过程一样的
在单核处理器下很好,他说所有收集器占用额外内存最小的。
然后是ParNew,Serial的多线程版本
然后是Parrllel Scavenge 和ParNew擦不多
重点关注吞吐量
然后是ParallelOld
PS的老年代版本 ,标记整理算法/
老年代的CMS 2次stop the world 详细看一下
一种以获取最短回收停顿时间为目标的收集器,并发收集、低停顿。到这里就不一样了,运行过程变成了四个。
四个阶段
初始标记(STW)、并发标记、重新标记(STW)、并发清除
单线程 、单线程与用户并发、多线程、单线程并发
缺点:1.在核数小于四个的情况下咱用核心数多,吞吐量降低
2.无法处理浮动垃圾、有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。是CMS运行期间预留的内存无法满 足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不 得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,
3.标记清除会产生碎片,在无法为新对象分配时会FullGC或者进行整理。但是是单线程的
G1 3次 stop the world
开创了收集 器面向局部收集的设计思路和基于Region的内存布局形式。
且其内存区域是按Region来划分,每一个Region都可以作为Eden From To 或者是老年代,它可以面向堆内存任 何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而 是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的 Region采用不同的策略去处理。
可以建立起停顿时间模型的收集器,每次收集停顿时间都少于某个时间。
四个阶段(三次STW)
初始标记(STW)、并发标记、最终标记(STW)、筛选回收(STW)
单线程 、单线程与用户并发、多线程、 多线程
Region引用对象如何解决?解决的思 路我们已经知道(见3.3.1节和3.4.4节):使用记忆集避免全堆作为GC Roots扫描,
·初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要 停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿。
·并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。
·最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。
·筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。
两个低延迟的垃圾回收器
ZGC 染色指针 Region大小可变
shenandoahGC 对G1的增强
内存分配与回收策略
对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起 一次Minor GC。
大对象直接进入老年代
长期存活的对象将进入老年代,每经过一次minorGC年龄增加一岁,增加到一定程度就会晋升至老年代
动态对象年龄判定:当Survivor空间中相同年龄所有对象占用的空间大于Survivor空间的一半,大于等于该年龄的对象就会直接进入老年代。
空间分配担保:每次MinorGC之前都要检查老年代的连续可用空间是否能够容纳当前新生代所有对象的大小,如果不能容纳就需要FullGC,如果设定了允许担保失败可以不进行FullGC
会不会发生内存泄漏?
https://www.cnblogs.com/gaopeng527/p/5258413.html
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露
静态集合,容器中的对象在程序结束前不能释放(但是还是可能有用,如果没用了就是泄漏)
HashMap中对象的hash值被改变
各种连接,比如数据库的连接,如果没有close会泄漏
内部类持有外部类,外部类无法GC
ThreadLocalMap中会发生内存泄漏
3.类加载
何时加载类
使用new ,获取类的static,
子类加载的时候发现父类还没有加载
JVM启动的时候main类
使用反射
当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解 析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句 柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有 这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
.类加载过程/Java对象的创建过程:(每一步详细实现)
加载(加载进内存、转换成Class实例对象)、
连接(验证、准备:分配空间设置0值、解析)
验证:是确保Class文件的字节流中包含的信息符合《Java虚 拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
准备:准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初 始值的阶段。
解析::符号引用->直接引用
直接引用就是直接指向目标的指针、相对偏移量或者是一个能 间接定位到目标的句柄。
初始化:(执行静态变量复制、静态代码块)
执行类构造器·clinit()方法
创建对象的过程
检查类是否加载
分配内存,在堆中分配内存
初始化 为变量赋默认值
设置对象头
执行
类加载器
启动类加载器 由C++实现
是虚拟机自身的一部分
加载
扩展类加载器
加载
应用程序加载器
自定义加载器
双亲委派模型
当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委托给它的父类加载器去执行,每一层的类都采用相同的方式,直至委托给最顶层的启动类加载器为止。如果父类加载器无法加载委托给它的类,便将类的加载任务退回给下一级类加载器去执行加载。
双亲委托模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。
为什么要使用双亲委托模型?
使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委托给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种加载器环境中都是同一个类。相反,如果没有使用双亲委托模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果自己去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。
双亲委托模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委托的代码都集中在java.lang.ClassLoader的loadClass()方法中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass方法进行加载。
破坏双亲委派模型
自定义一个类加载器,不让他委托给父类
JDK9的模块
4.Java 内存模型 JMM
工作内存与主内存
所有变量都存在主内存,每个线程都还有自己的工作内存,工作内存通过load和store将变量取出或放入。
什么是内存屏障?
大多数现代计算机为了提高性能而采取乱序执行,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。
java的内存屏障通常所谓的四种即LoadLoad
,StoreStore
,LoadStore
,StoreLoad
实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;
DCL为何需要加volatile
使用了它的禁止重排序,关键变化在于有volatile修饰的变量,赋值后(前面mov%eax,0x150(%esi)这句便 是赋值操作)多执行了一个“lock addl$0x0,(%esp)”操作,这个操作的作用相当于一个内存屏障 (Memory Barrier或Memory Fence,指重排序时不能把后面的指令重排序到内存屏障之前的位置
volatile如何实现的内存可见性?
addl$0x0,(%esp)”的空操作可以将本地处理器的缓存写入内存,该写入动作也会引起处理器或者别的内核缓存无效化,实现了可见性。
volatile如何实现的禁止重排序?
关键变化在于有volatile修饰的变量,赋值后(前面mov%eax,0x150(%esi)这句便 是赋值操作)多执行了一个“lock addl$0x0,(%esp)”操作,这个操作的作用相当于一个内存屏障 (Memory Barrier或Memory Fence,指重排序时不能把后面的指令重排序到内存屏障之前的位置
volatile 关键字
volatile 变量对所有线程是立即可见的,对volatile变量所有的写操作都能立刻反映到其他线程之中。从物理存储的角度看,各个线程的工作内存中volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看 不到不一致的情况,因此可以认为不存在一致性问题。但是由于java中的操作不是原子性的,所以多线程是不安全的。
指令重排序是指处理器采用了允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元进行处理。Volatile修饰的变量可以禁止指令重排序。
在操作前后加一个内存屏障
线程的状态转换
·新建(New):创建后尚未启动的线程处于这种状态。
·运行(Runnable):包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可 能正在执行,也有可能正在等待着操作系统为它分配执行时间。
·无限期等待(Waiting):处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线 程显式唤醒。以下方法会让线程陷入无限期的等待状态: ■没有设置Timeout参数的Object::wait()方法; ■没有设置Timeout参数的Thread::join()方法; ■LockSupport::park()方法。
·限期等待(Timed Waiting):处于这种状态的线程也不会被分配处理器执行时间,不过无须等待 被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状 态: ■Thread::sleep()方法; ■设置了Timeout参数的Object::wait()方法; ■设置了Timeout参数的Thread::join()方法; ■LockSupport::parkNanos()方法; ■LockSupport::parkUntil()方法。
·阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到 一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时 间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
·结束(Terminated):已终止线程的线程状态,线程已经结束执行。
5.JVM调优
设置堆大小
Xms初始堆大小
Xmx最大堆大小
PermSize 初始分配的非堆内存
MaxPermSize 最大非堆内存