序号 | 考点 | 链接 | 备注(公司、年份) |
---|---|---|---|
1. | classLoader 类加载的流程 | https://blog.csdn.net/qq_33907408/article/details/84898113 | 2018.11 招银网络java 1 面 |
2. | 加载器双亲委派模型及破坏 | https://blog.csdn.net/qq_33907408/article/details/85211420 | 阿里云2018.10 java2面 |
参考:深入JVM内核:原理、诊断与优化(葛一鸣)、《深入理解JV虚拟机》
【内容】
【补充】
- 常量池
- 元空间(Metaspace):从JDK 8开始,Java开始使用元空间取代永久代,元空间并不在虚拟机中,而是直接使用本地内存。
- 年轻代-老年代转移时机:①年龄计数器到达一定数量(可以设置)②动态判定:Survivor中相同年龄对象查过一半,大于这个年龄的对象进入老年区。③大对象(大小可以设置)
【内容】
1. 简介
jvm,和vmware visualbox不同,没有对应的硬件
jvm使用软件模拟java字解码指令集
使用最广泛的虚拟机hotspot,还有jRokit(oracle将二者合并到hotspot)
java和jvm是相对独立的 只要满足jvm规范,都能在jvm上面跑
scala,Groovy,clojure都可以在jvm上面运行
- 整数的表达:源码 反码 补码
将整数在jvm中的存储形式(二进制)打印出来
int a=-6;
for(int i=0;i<32;++i){
int t=(a & 0x80000000 >>> i) >>> (31-i);
System.out.println(t);
}
- 为什么需要用补码?(在jvm内部二进制没有歧义的表示0;符号位直接参与运算)
32 64位只有指针长度和long变了,其他不变!
相同的二进制串在jvm内部怎么知道是浮点数还是整数呢?这就需要看相关的操作,在汇编层面,如加法对于整数类型是add,对于浮点数类型是fadd,自然能够区分。
2. jvm运行机制
2.1 jvm启动流程
2.2 jvm基本结构
【pc寄存器】:
每个线程拥有一个pc寄存器,在线程穿件的时候创建
指向下一条指令的地址,如果下一个方法是本地方法,pc的值是undefined)
【方法区】
保存的内容:类型的常量池、字段方法信息、方法字解码
基本都属于永久代(可以理解成和方法区是同一个东西)
【java堆】
全局共享(所有线程共享的)
堆分代(见补充)
【java栈】
线程私有
由一系列的帧组成(帧可以含有多个帧片段,片段的大小是32位)
保存的是:常量池指针,局部变量、操作数、返回地址等等。
每一次的方法调用,就是将该方法的帧压到总的线程帧栈中,执行完毕就移出去。
- 局部变量表:如果是类的局部变量表就是各个成员变量,如果是方法,第一个是this指针
- 操作数栈:java中没有寄存器,参数传递都是栈实现
【栈上分配 vs 堆上分配】
- 栈分配(<1M):小对象,在没有逃逸的情况下可以直接分配在栈上;可以自动回收,减少GC压力;大对象和逃逸对象无法栈上分配
- 堆分配:相反
【堆、栈、方法区的交互例子】
public class AppMain{ public static void main(String[] args){ Sample test1=new Sample("测试1"); Sample test2=new Sample("测试2"); test1.printName(); test2.priintName(); } } public class Sample{ private name; public Sample(String name){ this.name=name; } public void printName(){ System.out.println(name); } }
【本地方法栈】
只为native方法提供服务,但是hotspot并不区分本地方法栈和虚拟机栈
2.3 内存结构
当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作
每一个read\load\store\write操作都是原子的,但是之间是有空隙的
每个线程对主内存(堆)的共享变量都保留一个副本,对其操作或者修改都具有时延,如果立即线程之间可见,需要加上volatile关键字(见补充)
2.4 编译运行 && 解释运行
bytecode执行的两种方式
解释执行:读一句执行一句
编译执行:将字解码编译成机器码,运行机器码(性能有数量级的提升)
3. jvm常用配置参数
3.1 trace跟踪参数 (主要是对GC的跟踪)
打开GC:
-verbose:gc
-XX:+printGC
打印详细信息:-XX:+PrintGCDetails
打印信息加上时间戳:-XX:+PirintGCTimeStamps
-Xloggc:log/gc.log:log输出到文件
-XX:_printHeapAtGC 每一次GC之后都打印出堆的信息
-XX:+TraceClassLoading监控类的加载(跟踪调试 第一个肯定是object)
-XX:printClassHistogram :打印类的直方图
3.2 堆分配参数
思考:java桌面级产品,需要打包jre进去,但是打包的部分只是产品用到的部分 怎么做优化呢?
官方推荐:新生代占整个堆的3/8,幸存代占整个新生代的1/10,oom的时候一定要将堆内存dump出来,不然没有办法排查现场的问题
就算堆空间没有用完也能抛出oom,原因是永久区的溢出(如cflib生成的hibernate等动态代理的对象的时候,永久区的溢出,没有更为永久的区域存放这些对象了)
-Xmx -Xms 指定最大堆空间 指定最小堆空间
System.out.println(Runtime.getRuntime().maxMemory()/1024.0/1024+"M")```
-XX:PermSize
- 例子:
新生代太小:全部分配到了老年代
3.3 栈分配参数
通常只有几百K 决定了函数调用的深度
每个线程都会分配一个空间,所以每一个不会大空间
m
4. GC算法和种类
GC的对象时堆空间(年轻代和老年代)和永久区
老年代的对象:可能是开始分配的时候空间不够,直接从新生代担保进来的,其他的应该都是长命对象
4.1 引用计数法
古老的垃圾回收算法
为每一个对象标记一个使用数量,用过引用数量作为参考是否进行回收(0)
伴随加加减减 影响性能
很难处理循环引用
4.2 标记清除法
是现代垃圾回收的思想基础
两个阶段:标记阶段 + 清除阶段
从根节点开始标记,能够遍历到的标记的都不会回收,其他的回收。
4.3 标记压缩法
适用于存活对象比较多的场合
在标记清除算法上面优化:清除的时候是将存活对象复制压缩到内存的一端,之后清理边界外的所有对象
4.4 复制算法
高效,但是不适合于对象多的场景(年老代)
改进:大对象尽量直接进老年 不要放到复制空间;老年对象直接进入老年代(计数一直都有,说明被长期使用);剩余对象,小的 年轻的 进入复制空间
4.5 可触及性
三种对象
可触及对象:从根节点可以触及到这个对象
可复活对象:当下阶段是不可触及的,但是在finalize()中可能复活的对象,也是不可以回收的
不可触及对象:回收的都是不可触及的对象(不可触及对象不能被复活)
- 新建对象首先处于[reachable, unfinalized]状态(A)
- 随着程序的运行,一些引用关系会消失,导致状态变迁,从reachable状态变迁到f-reachable(B, C, D)或unreachable(E, F)状态
- 若JVM检测到处于unfinalized状态的对象变成f-reachable或unreachable,JVM会将其标记为finalizable状态(G,H)。若对象原处于[unreachable, unfinalized]状态,则同时将其标记为f-reachable(H)。
- 在某个时刻,JVM取出某个finalizable对象,将其标记为finalized并在某个线程中执行其finalize方法。由于是在活动线程中引用了该对象,该对象将变迁到(reachable, finalized)状态(K或J)。该动作将影响某些其他对象从f-reachable状态重新回到reachable状态(L, M, N)
- 处于finalizable状态的对象不能同时是unreahable的,由第4点可知,将对象finalizable对象标记为finalized时会由某个线程执行该对象的finalize方法,致使其变成reachable。这也是图中只有八个状态点的原因
- 程序员手动调用finalize方法并不会影响到上述内部标记的变化,因此JVM只会至多调用finalize一次,即使该对象“复活”也是如此。程序员手动调用多少次不影响JVM的行为
- 若JVM检测到finalized状态的对象变成unreachable,回收其内存(I)
- 若对象并未覆盖finalize方法,JVM会进行优化,直接回收对象(O)
- 注:System.runFinalizersOnExit()等方法可以使对象即使处于reachable状态,JVM仍对其执行finalize方法
4.6 Stop-The-World
全局暂停,所有java代码都不能执行,native还可以执行(因为不和jvm交互)
只有全局停顿,才不会在清理的过程中产生新的垃圾
新生代的GC比较短,老年代的GC可能长达分钟级别,根据具体大小不同
全局暂停的原因:(基本是因为GC引起的)
- dump线程
- 死锁检查
- 堆dump
5. GC参数
5.1 串行收集器
最老 最稳定 效率最高
只是用一个线程,没有充分发挥好多核的优势
在新生代使用复制算法;在老年代使用标志-压缩算法
-XX:+UseSerialGC
5.2 并行收集器
【ParNew】
-XX:+UseParNewGC
-XX:ParallelGCThreads 指定回收线程的并行数量
新生代采用并行回收,老年代依旧是串行回收
【Parallel】
同样是新生代复制算法,老年代标志压缩算法
-XX:UseParallelGC 使用parallel收集器+老年代串行
-XX:UseParallelOldGC 使用parallel收集器+老年代并行
-XX:MaxGCPauseMills GC时候尽量不超过的时间,不能完全保证,只能作为一个目标值
-XX:GCTimeRatio::(0-100 default99) 1-垃圾回收占总时间的比例(默认1%的时间做GC)
上面两个参数是矛盾的,不能同时局部最优,只能整体更优
5.3 CMS收集器
Concurrnet Mark Sweep(并发标记清除)
-XX:+UseConcMarkSweepGC
清理不太彻底,因为没有STOP-THE-WROLD
标记清除算法(前面都是标记压缩):及时会产生碎片,但是由于是并发的,用户线程也在执行着业务,不能挪动内存的数据
注意并行和并发:这里是并发,表示回收和业务一起执行(不一定并行,可能串行)
停顿会对减少些,吞吐量相对会降低(因为cpu的总量是一定的)过程:(不能完全消除全局停顿)
5.4 tomcat实例
JMeter吞吐量测试工具
6. 类装载器classLoader???(不太懂)
6.1 class装载验证流程
加载—链接(验证 准备 解析)—初始化
【加载】
获取类的二进制流,转为方法区的数据结构,在java堆中生成对应的java.lang.Class对象
【链接】
验证:是为了保证Class流的格式是正确的(比如在方法区是都有对应的类啊什么的,final 被继承啊肯定是不行的)
准备:分配内存,并为类设置初始值(在方法区中)
public static int v=1;在准备阶段v=0,在接下来的初始化阶段clinit才是1
但如果是public static final int v=0,在准备阶段就已经变成1了解析:符号引用替换为直接引用
【初始化 】
clinit:class init
static变量赋值语句 static{}语句执行一次
子类的clinit调用之前父类的clinit必须已经调用了
clinit是线程安全的
java.lang.NoSuchFieldError?
6.2 什么是类装载器ClassLoader
是一个抽象类
classLoader的实例读入java字节码,将类装 载到JVM中,负责装载过程的加载阶段
6.3 JDK中ClassLoader默认设计模式
有哪些ClassLoader呢?
BootStrap ClassLoader(启动ClassLoader)启动rt jar包中的类
Extension ClassLoader(扩展CLassLoader)启动javahome/lib/ext下面的jar包中的类
App ClassLoader(引用ClassLoader、系统ClassLoader) 启动自己classPath下面的类
Custom ClassLoader(自定义ClassLoader)
每一个ClassLoader都有一个Parent作为父亲,启动的没有
在加载的时候 boot没有办法看到下一层的extension 因为每一个类里面只有一个Parent(去寻找父类的委托对象)
解决方式:Thread.setContextClassLoader
7 java堆分析
【补充】
-
常量池:
java虚拟机缓存了Integer、Byte、Short、Character、Boolean包装类在-128~127之间的值,如果取值在这个范围内,会从int常量池取出一个int并自动装箱成Integer,超出这个范围就会重新创建一个。 -
年轻代、年老代和永久代(持久代)
young generation \\ old(tenured) generation \\ perm area
对堆内存的一个划分,划分目的是:优化GC性能
在eden诞生,满了并还存活进入survivor1,满了并还存活进入survivor2,满了并还存活进入tenured。
生命周期比较长的能够存活到最后
perm:不属于堆空间。用于存放静态文件,java的类或者方法等等,基本不参与GC(例外:如热加载的类数据)
survivor0 survivor1之间会有个互斥算法(一般一个是from 一个是to)
-
逃逸对象:线程之间的公有对象
-
volatile关键字
线程对共享变量的修改,其他线程如果需要实时可见,需要对该共享变量加上volatile关键字
如果不加volatile,只在线程的自己的栈区进行查看值,没有办法进行更新。 -
oom和sof
oom:outOfMemoryException && sof:stackOutFlowException
https://blog.csdn.net/wenjieyatou/article/details/79131371OOM主要是内存泄漏和内存溢出
SOF主要是发生在递归过程中,线程请求的栈深度大于虚拟机所允许的深度
- 可见性、有序性、
【可见性】
方法往往是final,synchronized,volatile
【有序性 (指令重排、主内存同步延时)】
在线程内部都是有序的,在外部的线程观看,是无序的
可重排语句(读后读)如a=1,b=2编译器可以根据需要,和对性能的提升角度进行重排
但很多语句是不能重排(读后写,写后读,写后写)的,如a=1;b=a;
解决方式:在write函数前面加上synchronized关键字(性能不一定好,因为syn基本是串行)
- finalize()方法 && 对象复活
object中有finalize方法(protected):
protected void finalize() throws Throwable { }
java不保证finalize方法被及时执行,甚至不保证被执行,因为GC是不受程序控制的,可以用try_catch_finally代替
对象的finalize只能调用一次(不要理解对等于c++中的析构函数)
- GC参数整理