JVM-Java虚拟机
Java程序运行时,编译器将Java文件编译成平台无关的Java字节码文件(.class)。对应平台JVM对字节码文件进行解释,翻译成对应平台匹配的机器指令并运行。
- JVM内存区域(内存结构)
JVM内存区域粗略划分为堆和栈。
按虚拟机规范划分为五部分,包括程序计数器、虚拟机栈、本地方法栈、堆、方法区(永久代)。
在JDK1.8中变成了程序计数器、虚拟机栈、本地方法栈、堆、元空间。
(1)程序计数器:也叫PC寄存器,是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器,字节码解释器依次读取指令,实现流程控制。
(2)Java虚拟机栈:是线程私有的,为JVM执行Java方法服务,用来存储局部变量、操作数栈、动态连接等。
(3)本地方法栈:和Java虚拟机栈类似,但差别本地方法栈是为JVM使用到的本地native方法服务。
(4)Java堆:所有线程共享的一块内存区域,在虚拟机启动时创建,存放的是对象实例。
(5)方法区:所有线程共享的一块内存区域,存放已被虚拟机加载的类型信息、常量、静态变量、字符常量池、即时编译器编译后的代码(class)等。
(6)元空间:JDK1.8中,元空间代替了方法区。
- Java堆的内存分区
Java堆又分为年轻代(新生代)和老年代。新生代存放存活时间短的对象,达到年龄后的对象会被移到老年代,老年代还存放大对象。
新生代又分为eden、survivors,survivors分两个区域:from、to,这三者的比例是8:1:1。
- 堆、栈区别
(1)堆内存是不连续的;栈是连续的。
(2)堆是所有线程共享的;栈是线程私有的。
(3)堆存放的是对象实例和数组;栈存放的是局部变量、操作数栈等。
- JVM类加载过程
JVM类加载过程分为五个阶段:加载->链接->初始化->使用->卸载;链接阶段又包括验证->准备->解析。
(1)加载:通过类加载器加载Class文件字节码,在内存中生成class对象
(2)链接:
验证:确保加载的class的正确性、安全性。
准备:为类变量分配存储空间并设置类变量初始值。
解析:JVM将常量池内的符号引用转换为直接引用。
(3)初始化:执行类变量赋值和静态代码块。
(4)使用
(5)卸载:执行了System.exit()方法,程序正常结束、程序在执行过程中出现异常/错误而终止、操作系统出错导致JVM进程终止。
- 对象创建的过程
就是先执行类加载,再为新生对象分配内存并对象内存初始化,最后设置对象头。
(1)new指令
(2)检查类是否被加载:检查这个符号引用代表的类是否能在常量池中定位到一个类的符号引用。
(3)类加载、解析、初始化检查:如果没有被加载,则先执行相应的类加载过程。
(4)对象内存分配:虚拟机为新生对象分配内存。
(5)对象内存初始化:内存分配完成后,虚拟机将分配到的内存空间都初始化为零值,不包括对象头。
(6)设置对象头,请求头包含了对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等。
- 内存溢出和内存泄漏
内存泄漏会导致内存溢出。
(1)内存溢出:申请的内存超过了可用内存,内存不够用了。
(2)内存泄漏:申请的内存空间没有被正常释放,导致内存被占用的浪费。
- 说几种内存泄漏的场景
(1)定义了静态集合类,因为静态集合类的生命周期和JVM一样,所以集合引用的对象不能被释放啊
(2)IO没有正常被close啊,导致持续占用内存,不能被GC回收。
(3)变量的作用域不合理啊,或者不再使用的对象没有及时设置为null。
(4)ThreadLocal使用不当啊,用完记得要调用remove()方法啊。
(5)hash值发生变化啊,想使用HashMap、HashSet时,对象修改后的hash值和存储到容器时的hash值不同,就无法找到存入的对象单独删除啦。
- 对象有哪几种引用
(1)强引用
(2)软引用
(3)弱引用
(4)虚引用
- 逃逸分析技术了解吗?
逃逸分析:某个方法之内创建的对象除了在方法体内被引用外,还在方法体外被其他变量引用,导致在该方法执行完后,其创建的对象无法被GC回收。
方法逃逸:当一个对象被new出来后,如果对象是作为参数传递到外部了,这种是方法逃逸。
线程逃逸:一个对象被外部线程访问到,如对象赋值给可以在其他线程中访问的实例变量,这种叫线程逃逸。
逃逸分析的好处:栈上分配、同步消除、标量替换。
如果确定一个对象不会逃逸到线程之外,这个对象可以在栈上分配,对象占用的内存就会随着栈帧出栈销毁。这种情况,对象就不是分配在堆中的,减少GC压力。
- 双亲委派
Java类加载器主要有四类:启动类加载器、扩展类加载器、系统类加载器、用户自定义类加载器。
双亲委派的工作过程:
(1)如果一个类加载器收到类加载请求,它是先把这个请求委派给父类加载器去完成,一层层向上委派至顶层的启动类加载器;
(2)从启动类加载器开始,判断自己能否完成加载请求;如果不能则父加载器再一层层向下判断自己能否完成,能完成就加载;
(3)如果最后都无法完成,则抛出异常。
- Young GC、Full GC什么时候触发?
(1)Young GC:新创建的对象优先在新生代Eden区进行分配,如果Eden区没有足够的空间时,就会触发Young GC来清理新生代。
(2)Full GC:老年代空间不足;方法区由永久代实现,而永久代空间不足;System.gc()、jmap -dump等命令触发Full GC。
老年代空间不足判断又分为:
(1)Young GC之前检查发现本次Young GC后可能升入老年代的对象大小要超过老年代当前可用内存空间,就会触发Full GC。
(2)Young GC之后有一批对象需放入老年代,这时如果没有足够内存空间,则立马触发Full GC。
(3)老年代内存使用率达到一定比例,也会触发Full GC。
(4)新生代对象GC年龄达到阈值,需要晋升到老年代,老年代空间不足,就会触发Full GC。
- 对象什么时候会进入老年代?
(1)长期存活的对象移区年龄到达阈值后会进入老年代:
对象头信息中存储着对象的迭代年龄,每次YoungGC后对象的移区年龄加1,当到了设置的最大年龄15后,对象就会被移到老年代。
(2)大对象直接进入老年代。
(3)动态对象年龄判定:如果Survivor空间中相同年龄的对象总和内存占用超过Survivor空间的一半,则大于等于该年龄的对象直接移到老年代。
(4)空间分配担保:Young GC之后发现新生代还有大量对象存活,可能要把Survivor无法容纳的对象直接移到老年代。
- JVM调优
频繁Young GC(minor GC):通常是由于新生代空间较小,可以通过增加新生代空间-Xmn来降低Young GC的频率。
频繁Full GC:先分析原因,可能原因有:
(1)系统一次性加载了过多数据到内存,大对象进入了老年代。
(2)频繁创建了大量对象,但无法被回收,导致内存泄漏,会先引发Young GC,再引发Full GC。
(3)长生命周期的对象,到了一定的阈值后被移到老年代。
(3)BUG。
(4)JVM设置参数不合理。
公司一般有全方位监控JVM各项指标的监控系统,可以上去看看分析:
平均YGC周期
Heap使用峰值,可以参考YGC周期中的峰值
FGC前后的Heap谷值
一周内FGC次数
元空间使用的峰值
- JVM参数
Xms:分配内存最小值,4G、8G按需设置
Xmx:分配内存最大值
MaxNewSize:新生代最大值
MaxMetaspaceSize:元空间,一般512M
NewRatio:新生代与老年代的比例
新生代和老年代的比例一般设置为1:2
新生代占堆空间的1/3,老年代占2/3。