JVM面试题
常见的面试题
1、请你谈谈你对JVM的理解?java8虚拟机有什么更新?
java8中移除了方法区的永久代实现,使用Metaspace(元空间)实现,元空间不在jvm中,仅受本地内存限制,可通过参数调整大小。
2、什么是OOM,请你说说OOM产生的原因?如何分析?
内存溢出OutOfMemeoryError(OOM),当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error。利用jprofile dump文件分析OOM
最常见的OOM情况有以下三种:
- java.lang.OutOfMemoryError: Java heap space ——>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
- java.lang.OutOfMemoryError: PermGen space ——>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。加载大量的第三方包,也有可能会出现永久区溢出
- java.lang.StackOverflowError ——> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。
3、JVM的常用调优参数有哪些?
-
-XX:+HeapDumpOnOutOfMemoryError:让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用
- -Xms2g:初始化推大小为 2g;
- -Xmx2g:堆最大内存为 2g;
- -XX:MetaspaceSize=128m 设置元空间大小为128m
- -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
- -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
- -XX:MaxTenuringThreshold=15 设置新生代进入老年代的存活年限(默认是15)
- –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
- -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
- -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
- -XX:+PrintGCDetails:打印 gc 详细信息
- -XX:+TraceClassLoading 追踪类的加载信息并打印出来
- -XX:+HeapDumpOnOutOfMemoryError dump堆内存溢出的异常分析文件
4、内存快照抓取,如何分析,命令是什么?
使用Jconsole 和 Jprofiler 性能分析工具,dump内存快照JVM参数 -XX:+HeapDumpOnOutOfMemoryError
5、堆里面分区?Eden(伊甸园区)、Surival(幸存者区,细分为from 区 和 to 区)、养老区
逻辑上分为:新生区->养老区->永久区(jdk1.8中叫元空间),jdk1.8后,将永久区移除JVM,使用元空间的概念替代,元空间是在本地内存上的,不属于JVM,所以物理上堆分区只有:新生区、养老区。
新生区,又细分为Eden(伊甸园区)、Surival(幸存者区,细分为from 区 和 to 区)
6、GC垃圾收集算法有那几个?谈谈利弊?
-
引用计数法,每次对象都会使用计算器,成本高,内存消耗大,一般不采用
-
复制算法,内存效率整齐度高,适合新生区使用,好处是没有内存碎片,缺点是浪费一倍的内存空间
-
标记清除整理算法,内存利用率高,没有内存碎片,缺点是耗时也可能比较严重,适合老年区使用,该算法分3步:
-
标记,扫描全部对象,标记能够存活的对象
- 清除没有被标记的对象
- 整理,压缩空间,将存活的对象滑动到一侧
-
7、栈内存和堆内存的区别
栈内存:栈是管理程序运行的,存放函数中定义的一些基本类型的变量(int…double等) + 对象的引用变量(真实的对象信息在堆里面)
堆内存:存放着由new创建的对象和数组,类、方法、常量、保存了类型引用的真实信息;
方法区存储:类信息、常量池、静态变量、符号引用、方法代码
BAT 难度的面试题
1、JVM垃圾回收的时候如何确定,GC Roots?
可达性分析算法,假设一个根能连到的对象都被判断是活着的对象,就是可达到,相反不可达的对象就会被回收,也就是与根对象没有关系的对象都会被回收
这里的根有4种情况,它可以是:
- JVM栈中引用的对象(就是引用变量的真实对象)
- 类中静态属性引用的对象
- 方法区中的常量(常量池就是放在方法区中)
- 本地方法栈中 Native 方法引用的对象!
2、-X,-XX参数你用过哪些?
# 查看帮助
java -X --help
xjwdeMacBook:~ xjw$ java -X --help
-Xmixed 混合模式执行 (默认)
-Xint 仅解释模式执行
-Xbootclasspath:<用 : 分隔的目录和 zip/jar 文件>
设置搜索路径以引导类和资源
-Xbootclasspath/a:<用 : 分隔的目录和 zip/jar 文件>
附加在引导类路径末尾
-Xbootclasspath/p:<用 : 分隔的目录和 zip/jar 文件>
置于引导类路径之前
-Xdiag 显示附加诊断消息
-Xnoclassgc 禁用类垃圾收集
-Xincgc 启用增量垃圾收集
-Xloggc:<file> 将 GC 状态记录在文件中 (带时间戳)
-Xbatch 禁用后台编译
-Xms<size> 设置初始 Java 堆大小
-Xmx<size> 设置最大 Java 堆大小
-Xss<size> 设置 Java 线程堆栈大小
-Xprof 输出 cpu 配置文件数据
-Xfuture 启用最严格的检查, 预期将来的默认值
-Xrs 减少 Java/VM 对操作系统信号的使用 (请参阅文档)
-Xcheck:jni 对 JNI 函数执行其他检查
-Xshare:off 不尝试使用共享类数据
-Xshare:auto 在可能的情况下使用共享类数据 (默认)
-Xshare:on 要求使用共享类数据, 否则将失败。
-XshowSettings 显示所有设置并继续
-XshowSettings:all
显示所有设置并继续
-XshowSettings:vm 显示所有与 vm 相关的设置并继续
-XshowSettings:properties
显示所有属性设置并继续
-XshowSettings:locale
显示所有与区域设置相关的设置并继续
# 输出此帮助消息
xjwdeMacBook:~ xjw$ java -?
3、你常用的项目,发布后配置过JVM调优参数吗?怎么设置元空间大小,怎么样设置幸存者区大小
# 设置元空间大小
-XX:MetasapceSize10m
4、引用,强引用,弱引用,虚引用都是什么,请你谈谈
5、GC垃圾回收器和GC算法的关系?分别有哪些?
GC垃圾回收器:串行垃圾回收器、并行垃圾回收器、并发垃圾回收器、G1垃圾回收器
-
串行(STW:Stop the World)单线程
-
并行垃圾回收器(多线程工作,也会导致 STW)
-
并发垃圾回收器
在回收垃圾的同时,可以正常执行线程,并行处理,但是如果是单核CPU,只能交替执行!
-
G1垃圾回收器
将堆内存分割成不同的区域,然后并发的对其进行垃圾回收
6、谈谈默认的垃圾回收器?
# 打印jvm的垃圾回收详情信息
java -XX:+PrintGCDetails -version
Heap(堆)
- PSYoungGen(新生区),细分为eden(伊甸园区)、from区、to区
- ParOldGen(养老区)
- Metaspace(元空间) ,逻辑上属于堆,物理上不属于堆
# 打印命令行的一些参数
java -XX:+PrintCommandLineFlags -version
可以发现InitialHeapSize 初始化堆内存,MaxHeapSize 最大堆内存,-XX:+UseParallelGC 使用的垃圾回收器ParallelGC
7、G1垃圾回收器的特点?
8、OOM你看过几种?
内存溢出OutOfMemeoryError(OOM),当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error。利用jprofile dump文件分析OOM
最常见的OOM情况有以下三种:
- java.lang.OutOfMemoryError: Java heap space ——>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
- java.lang.OutOfMemoryError: PermGen space ——>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
- java.lang.StackOverflowError ——> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。
Java 混合模式(默认)
修改为解释执行模式
java -Xint -version
修改为编译执行模式
java -Xcomp -version
深入学习
一本书《深入理解JAVA虚拟机》
其他面试题
-
JVM 内存区域?
-
三大常量池了解吗?
-
堆内存结构?
物理上由年轻代和老年代组成
-
GC 算法-复制算法?
-
GC 算法-标记整理?
-
GC 算法-标记清除?
-
三种算法优缺点比较?
内存效率:复制算法 > 标记清除 > 标记整理
内存整齐度:复制算法=标记整理>标记清除
内存利用率:标记整理 = 标记清除 > 复制算法
年轻代经常发生GC且对象存活率较低,使用复制算法;
老年代不经常GC,使用标记整理算法
-
老年代担保是什么
-
Full GC 触发条件
老年代空间不足
-
OOM 问题怎么排查
dump文件
-
Dump日志分析工具
0、JVM的位置
每一个学习 JVM 的人,都渴望成功!
JVM 会了但是你也看不出来多少效果!
高级程序员甚至架构师,都必须要了解JVM !
Java无法直接操作内存(硬件)
1、JVM体系架构图
简化一下:
自己必须能够闭着眼睛想到这个图,方法在java1.7(含)前使用永久代实现,java1.8后使用元空间实现,元空间不在jvm中,仅受本地内存限制。
兴趣才是最好的老师,我老了也会敲代码,因为他是一辈子的兴趣
2、类加载器Classloader
类加载结构图
一个类加载到JVM的基本结构过程
- 正向实例化
- 反向,反射获取类模版,创建实例
public class Demo01 {
public static void main(String[] args) {
Object o = new Object(); // jdk 自带的
Demo01 demo01 = new Demo01(); // 正向创建,实例化一个自己定义的对象
Class<? extends Demo01> aClass = demo01.getClass(); // 反射得到对象的Class模板
ClassLoader classLoader = aClass.getClassLoader();// 得到类加载器
System.out.println(classLoader);
}
}
执行结果:类加载器AppClassLoader
类的加载、连接和初始化(了解)
运行期间完成
第一步加载:查找并加载类的二进制数据
第二步连接:
-
验证:保证被加载的类的正确性;
-
准备:给类静态变量分配内存空间,赋值一个默认的初始值:int 0;new() 不是一个原子性的操作
-
解析:把类中的符号引用转换为直接引用
在把java编译为class文件的时候,虚拟机并不知道所引用的地址;助记符:符号引用(代替真实的地址)! 转为真正的直接引用,找到对应的直接地址!
第三步初始化:给类的静态变量赋值正确的值(静态变量是基本类型或者字符串,被java编译器标记成常量值,初始化就直接被JVM完成);
public class Test{
// public int a = 1; // 类实例化(new)才有的a
public static int a = 1; // 类初始化(类加载的时候),就初始化变量a,赋值
// public static main (){....} 类初始化(类加载的时候),就初始化该方法
}
Test.java文件进入类加载器的过程(原理):
// 1、加载:
编译文件为.class文件,通过类加载,加载到JVM
// 2、连接
验证(1) 保证Class类文件没有问题
准备(2) 给类静态变量分配内存空间,赋值一个默认的初始值,这里给int类型的变量 a分配内存空间并赋值 a = 0;
解析(3) 符号引用转换为直接引用
// 3、初始化:
给类的静态变量赋值正确的值,把1赋值给变量a;
所以变量a的值一开始是0,后来才被赋值为1
类的加载static
package com.coding.classloader;
// JVM 参数:
// -XX:+TraceClassLoading // 用于追踪类的加载信息并打印出来
// 分析项目启动为什么这么慢,快速定位自己的类有没有被加载!
// rt.jar jdk 出厂自带的,最高级别的类加载器要加载的!(最优先)
public class Demo02 {
public static void main(String[] args) {
System.out.println(MyChild1.str2);
}
}
class MyParent1{
public static String str = "hello,world";
static {
System.out.println("MyParent1 static");
}
}
class MyChild1 extends MyParent1{
public static String str2 = "hello,str2";
static {
System.out.println("MyChild1 static");
}
}
执行结果:
MyParent1 static
MyChild1 static
hello,str2
执行,输出类加载的信息,首先加载的是rt.jar包 的内容
往下翻,最终加载我们自己的类Demo02 -> MyParent1 -> MyChild1,执行输出
以后分析项目启动为什么这么慢时候,通过追踪类加载信息,快速定位自己的类有没有被加载!
类加载final常量池
package com.coding.classloader;
// 常量,如果编译期间就确定的值就会在调用类加载的时候,放到调用类的常量池中,就不会去加载常量所在的类
public class Demo03 {
public static void main(String[] args) {
System.out.println(MyParent02.str); // 已确定的值,加载类Demo03的时候把它放进自己的常量池
}
}
class MyParent02{
public static final String str = "hello world";
static {
System.out.println("MyParent02 static"); // 这句话会输出吗?
}
/*
final 常量在编译阶段的时候放入常量池,并不会加载类;
这个代码中将常量放到了Demo03的常量池中。之后 Demo03与MyParent02 就没有关系了,
Demo03从自己的常量池拿值,根本不需要去触发加载Myparent02,除非str是要运行才能确定的
值,那么他就不会放进Demo03的常量池
*/
}
执行结果:
并没有打印“MyParent02 static”,因为加载类的Demo03的时候就已经将常量str放进了自己的常量池,因为str已经确定值,就不用去加载类MyParent02,前面我们使用JVM参数-XX:+TraceClassLoading 看到类加载的整个过程,现在我们也追踪一下Demo03的加载过程
发现只加载了Demo03就执行了输出,因为常量str的值在Demo03常量池就已经确定,就不需要去触发加载类MyParent02,完成Main主进程。
编译期间不确定的常量就不会放入调用类的常量池中
package com.coding.classloader;
import java.util.UUID;
/**
* 当一个常量的值并非编译期间可以确定的,那这个值就不会被放入调用类的常量池中!
* 程序运行期间的时候,回主动使用常量所在的类
*/
public class Demo04 {
public static void main(String[] args) {
System.out.println(MyParent04.str);
}
}
class MyParent04{
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("MyParent04 static"); // 这句话会输出吗?
}
}
同样我们加上JVM参数-XX:+TraceClassLoading 追踪类加载的过程 ,运行
可以看到加载类Demo04 -> MyParent04 -> UUID,既然加载了MyParent04,自然就会执行静态代码块,打印“MyParent04 static”
ClassLoader类加载分类4个
1、java虚拟机自带的加载器(先天)
-
BootStrap 根加载器(加载系统的包,JDK 核心库中的类 rt.jar)
-
Ext 扩展类加载器 (加载一些扩展jar包中的类)在java的/ext目录下如下图,一些大的公司如腾讯,自己写一些类打成jar包放入ext中,优先级比自己写的java代码高,可以把类放入ext下避免被覆盖,它会优先加载
-
Sys/App 系统(应用类)加载器 (我们自己编写的类)
2、用户自己定义的加载器(后天)
- ClassLoader,只需要继承这个抽象类即可,自定义自己的类加载器
package com.coding.classloader;
// Demo01
public class Demo01 {
public static void main(String[] args) {
Object o = new Object(); // jdk 自带的
Demo01 demo01 = new Demo01(); // 实例化一个自己定义的对象
// null 在这里并不代表没有,只是Java触及不到!它的底层是C++写的,所以java触及不到
System.out.println(o.getClass().getClassLoader()); // null
System.out.println(demo01.getClass().getClassLoader()); // AppClassLoader
System.out.println(demo01.getClass().getClassLoader().getParent()); // ExtClassLoader
System.out.println(demo01.getClass().getClassLoader().getParent().getParent()); // null
// 思考:为什么我们下面自己定义的java.lang.String 没有生效?
// 因为:
// jvm 中有机制可以保护自己的安全;
// 双亲委派机制: 一层一层的让父类去加载,如果顶层的加载器不能加载,然后再向下类推
// Demo01 04
// AppClassLoader 03
// ExtClassLoader 02
// BootStrap (最顶层) 01 java.lang.String 对应rt.jar包
// 双亲委派机制 可以保护java的核心类不会被自己定义的类所替代,你自己定义的java.lang.String这个类是不生效的
}
}
双亲委派机制
双亲委派机制 可以保护java的核心类不会被自己定义的类所替代
大概就是如果一个类加载器收到了类加载的请求,首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成。通过双亲委派机制就能保证同样一个类只被加载一次
一层一层的让父类去加载,如果顶层的加载器不能加载,然后再向下类推
// 自己定义一个类与java的核心类重复,看看会不会被覆盖
package java.lang
public class String{
public static void main(String[] args){
System.out.println("a");
}
}
执行报错,因为真正运行的不是你这个类,是rt.jar包的java.lang.String,里面确实没有main方法
在类加载最顶层BootStrap,也就是rt.jar包里,我们发现有个类java.lang.String,这个类里面根本没有main方法,所以执行的是这个类不是你自己定义的类,就报错没有main方法 ,这是双亲委派机制保护Java的核心类不会被自己定义的类锁替代
生活中的例子
比如说:公司部门有位程序员 A 发现如果做一个数据系统的话,来把公司各部门的数据打通,这样就可以减少很多交流成本,那么他可能就会和老大去说,申请去做这个系统,老大一看,这个方案完全可以抽成公共的呀,就自己去写了(父类加载公共方法),也可能老大一看,你就自己去写吧(父类不加载时,子类再进行加载),更巧的是,程序员 B 也发现了,他也去找老大说,这个时候老大会说什么呢?这个事情 A 去做了,就不用太担心了
那如果程序员 A 和 B 发现了之后没有和老大交流,都自己闷头去做了,这样的话,同样的系统做了两遍,还浪费了两个人的时间精力,由此造成的资源浪费太大了
我觉得双亲委派的机制类似于这样,因为这个机制的存在,让资源浪费的现象大大减少了。
tomcat 打破双亲委派机制
我们都知道 tomcat 是个 web 容器,那么它应该:
-
支持部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,就比如两个应用程序,其中一个依赖的是一个类库的 v1.0 ,另外一个依赖的是同样一个类库的 v2.0 ,那么 tomcat 是不是应该允许这个类库的 1.0 和 2.0 版本都存在?
- 部署在同一个 web 容器中相同的类库相同的版本是应该可以共享的。就比如,服务器上有 100 个应用程序,这些程序依赖的都是相同的类库,那 tomcat 总不能把这 100 份相同的类库都加载到虚拟机里面去吧,要是非要加载进去,那服务器不得分分钟炸了
- web 容器需要支持 jsp 文件的修改,也就是说,当程序运行之后,我对 jsp 文件进行了修改,那么 tomcat 是不是也应该支持?如果不支持的话,那我修改一次就不能用了,不合适吧
基于上面三点,就能看到 tomcat 其实是打破了双亲委派机制的
-
第一个问题,第三方类库就是同样一个资源,在双亲委派机制中,同样一个资源是不应该加载两次的,但是在 tomcat 里面却被允许了;
- 第二个问题好像又在说双亲委派的机制,正是因为双亲委派机制的存在,所以第二个问题就不是问题了嘛;
- 第三个问题又打破了双亲委派机制,因为如果不打破的话,原来的 jsp 文件已经加载进来了,现在对它进行了修改,那么应该还会加载原来的 jsp 文件,这样的话修改岂不是无效了?
所以, tomcat 打破了双亲委派机制,但并不是完全打破。
3、Native方法
public class Test {
public static void main(String[] args) {
// java 真的可以开启线程吗?不能,是调用native 本地方法,调用底层C 去实现的
// private native void start0();
new Thread().start();
}
}
点进start方法,底层调用的是native start0()
加了native关键字的方法,一个native 方法就是一个java调用非java代码的接口,该方法的实现由非java语言实现,比如C。很多其他的编程语言都有这一机制,并非java特有,比如python会调用很多其他语言写的类库去实现功能。
native : 只要是带了这个关键字的,说明 java的作用范围达不到,只能去调用底层 C 语言的库!Robot类
闲谈:java.awt.Robot 按键精灵 ! 默认鼠标键盘操作!java可以通过这个类实现一个简单的自动化脚本!它底层也是使用了native,通过本地方法接口操做鼠标
do
mouse.move()
loop
import java.awt.*;
try {
Robot robot = new Robot();
//robot.mouseMove(500,1000);
Color color = robot.getPixelColor(500, 1000);
System.out.println(color.toString());
robot.delay(3000);
} catch (AWTException e) {
e.printStackTrace();
}
JNI
JNI : Java Native Interface (Java 本地方法接口)
为什么会有 Native 这个东西?
为什么有javascript,它跟java没有半毛钱关系,因为当时java很火,javascript为沾点关系所以改这个名字
故事
1995年,全世界都在写c、c++、vb,java刚出来要生存下来那么必须可以去调用 c、c++的库,所以说Java就在内存中专门开辟了一块区域标记为 native 方法
本地方法栈:null,因为java触及不到了,调用的是c、c++的库
现在的通信:Socket、WebService…,企业开发中很少用到native去开发了
4、程序计数器
每个线程都有一个程序计数器,是线程私有的。看JVM的体系架构图
程序计数器就是一块十分小的内存空间(几乎可以不计大小)。
作用:看做当前字节码的行号指示器
下图左边部分是字节码文件,通过javap -c 命令 class文件得到代码语句的执行顺序,如下图
javap -c Calc.class
代码解析:分支、循环、跳转、异常处理!都需要依赖于程序计数器来完成
bipush 将 int、float、String、常量值推送值栈顶;
istore 将一个数值从操作数栈存储到局部变量表;
iadd 加
imul 乘
当一个线程的cpu时间片用完后,程序计数器会记录线程的运行代码位置,等待下次该线程重新获得cpu时间片,会从上次记录的位置开始运行。
5、方法区的前世今生
逻辑上属于堆,Java 1.8后物理上不属于堆,使用了元空间实现,不属于JVM
Method Area 方法区 是 Java虚拟机规范中定义的运行是数据区域之一,和堆(heap)一样可以在线程之间共享!
天上飞的理念都会有落地的实现!元空间和永久代,都是对JVM规范中方法区的实现。
JDK1.7之前
方法区的实现就是永久代
永久代:用于存储一些JVM加载类信息,常量,字符串、静态变量等等。。。。这些东西都会放到永久代中;
永久代大小空间是 有限的:如果满了就报异常 OutOfMemoryError:PermGen
JDK1.8之后
方法区在JVM(Hotspot)的实现就是元空间
彻底将永久代移除jvm(HotSpot),Java Heap 中或者 Metaspcace(别名 Native Heap)元空间;算出来的堆空间大小是不包含元空间的,所以说它物理上不属于堆
方法区存储:类信息,常量,字符串、静态变量、符号引用、方法代码。。。。。。
元空间和永久代,都是对JVM规范中方法区的实现。
元空间和永久代最大的区别:永久代在JVM中,受JVM限制,元空间并不在Java虚拟机中,使用的是本地内存!
# JVM参数,设置元空间大小
-XX:MetasapceSize10m
如果元空间满了就报异常 OutOfMemoryError:MetaSpace
6、栈Stack
什么是栈?栈中存哪些东西?
栈就是管理程序运行的,存储一些基本类型(8大基本类型)的值,对象的引用(引用变量),方法等….
栈与队列
程序 = 数据结构 + 算法(IT行业流传的术语)
程序 = 业务逻辑 + 框架(真的IT人员做的)
业务逻辑->框架原理 -> 数据结构 -> 算法
栈和队列都是基本的数据结构,队列:FIFO(First Input First OutPut)
吃多了拉就是队列,喝多了吐就是栈
程序的运行本质其实就是压栈的过程,栈空了,线程就结束了
栈是什么
**栈就是管理程序运行的**,存储一些基本类型的值(局部变量),对象的引用(引用变量),方法等….
栈的优势:存取速度比堆快!仅次于寄存器,栈的数据是不可以共享的,运行的线程独享该栈数据;
StackOverFlow 栈溢出
public class Demo01 {
public static void main(String[] args) {
a();
}
// main a a a a a a a a a a a 满
// Exception in thread "main" java.lang.StackOverflowError
private static void a() {
a();
}
}
递归调用a(),最终导致栈溢出
执行结果:
所以说,栈里面是一定不会存在垃圾回收的问题的,只要线程一旦结束,该栈就Over了。栈的生命周期和线程一致;
栈原理
java栈的组成元素–栈帧
例子:我们看的动画24帧,就是1秒有24个画面
解释一下这幅图:先将F2压栈,再将F1压栈,F1的父帧是F2
栈(存什么)+ 堆 + 方法区(1.7就是永久代实现,1.8后就是元空间实现)的交互图,如下:
我们的这个栈主要是 HotSpot JVM (指针)
假设你的公司用的 JVM 不是HotSpot,上面的图不是这样的
3种类型的JVM
问题:请你谈谈你认识几种 JVM? (3种)
- SUN 公司 HotSpot (掌握即可)
- BEA 公司 JRockit
- IBM 公司 J9VM
7、堆Heap(掌握)
Java7之前
Heap 堆,一个JVM实例中只存在一个堆,堆的内存大小是可以调节的。
存储内容:类、方法、常量、保存了类型引用的真实信息;
分为三个部分:
- 新生区:YoungGen(Eden-from-to)
- 老年区:OldGen
- 永久区:Perm
堆内存在逻辑上分为三个部分:新生、老年、永久(JDK1.8以后,叫元空间)
物理上只有新生区、老年区;元空间在本地内存中,不在JVM中!
GC 垃圾回收主要是在 新生区和老区,又分为 普通GC 和 Full GC(重GC),如果堆满了,就会爆出 OutOfMemory;
# 打印jvm的垃圾回收详情信息
java -XX:+PrintGCDetails -version
新生区(Eden-from-to)
新生区 就是一个对象诞生、成长、消亡的地方!
新生区细分: Eden(伊甸园区)、Surival(幸存者区,细分为s0 幸存1区 和 幸存2区 s1,又叫做 from区 和 to区),所有的对象在Eden被 new 出来的,慢慢的当 Eden 满了,程序还需要创建对象的时候,就会触发一次轻量级GC;清理完一次垃圾之后,会将活下来的对象,会放入Surival(幸存者区),……. 清理了 20次之后,出现了一些极其顽强的对象,有些对象突破了15次的垃圾回收!这时候就会将这个对象送入养老区!运行了几个月之后,养老区满了,就会触发一次 Full GC;假设项目1年后,整个空间彻彻底底的满了,突然有一天系统 OOM,排除OOM问题,或者重启;
Sun HotSpot JVM 虚拟机中,内存管理(分代管理机制:不同的区域使用不同的算法!)
Eden(伊甸园区)、Surival(幸存者区,细分 from区 和 to区)
99% 的对象在 Eden 都是临时对象;
老年区
15次都幸存下来的对象从新生区进入老年区,老年区满了之后,触发 Full GC
默认是15次,可以修改!
永久区
放一些 JDK 自身携带的 Class、Interface的元数据;
几乎不会被垃圾回收的;
OutOfMemoryError:PermGen在项目启动的时候永久代不够用了?例如引入加载大量的第三方包!
JDK1.6之前: 有永久代、常量池在方法区;
JDK1.7:有永久代、但是开始尝试去永久代,常量池在堆中;
JDK1.8 之后:永久代没有了,取而代之的是元空间;常量池在元空间中;
闲聊:方法区和堆一样,是共享的区域,是JVM 规范中的一个逻辑的部分,但是记住它的别名 非堆
元空间和永久代,都是对JVM规范中方法区的实现。
元空间:它是本地内存!
元空间满了也会溢出:OutOfMemoryError:MetaSpace
8、JVM调优参数
三种参数类型:标配参数、X参数,XX参数
标配参数、X与XX参数
在各种版本之间都很稳定,很少有变化
java -version
java -help
java -showversion
# 查看运行的java进程
xjwdeMacBook:~ xjw$ jps -l
614
681 org.jetbrains.idea.maven.server.RemoteMavenServer
683 org.jetbrains.jps.cmdline.Launcher
684 com.coding.oom.Demo04
685 sun.tools.jps.Jps
X与XX参数
java -X -help
-Xint # 解释执行
-Xcomp # 第一次使用就编译成本地的代码
-Xmixed # 混合模式(Java默认)
XX参数布尔型
-XX: + 或者 - 某个属性值, + 代表开启某个功能,- 表示关闭了某个功能!;
public class Demo04 {
public static void main(String[] args) throws InterruptedException {
System.out.println("Hello,World");
TimeUnit.SECONDS.sleep(MAX_VALUE);
}
}
运行程序,通过jps 查看运行的java进程
通过jinfo -flag 功能 线程ID 查看运行中的java 程序,它的某个参数是否开启
# 查看是否开启打印gc详情信息
xjwdeMacBook:~ xjw$ jinfo -flag PrintGCDetails 684
-XX:-PrintGCDetails # - 表示关闭了该功能
开启打印gc详情
重新运行程序,jinfo查看是否开启参数打印gc信息, + 代表开启某个功能
XX参数之key = value型;
1、查看元空间大小,是key=value型的
# 修改元空间大小
-XX:MetaspaceSize=128m
2、查看进入养老区的存活年限(默认是15),该参数控制对象在新生代需要经历多少次GC后晋升到老年代的最大阀值,在JVM中用4个bit(位)存储(放在对象头中),所以最大值是15
-XX:MaxTenuringThreshold=15
默认值
查看所有默认值
jinfo -flags
jinfo -flags 3304 # 3304是使用jps -l 查出来运行的java程序ID
查看到初始的堆内存大小-XX:Initi,最大的堆内存大小等信息,-XX:+UseParallelGC,默认使用并行垃圾回收
Command line: 我们通过idea运行程序是给它传了个jvm 参数 -XX:MetaspaceSize=128m,
经典面试题:-Xms, -Xmx,怎么解释呢?考察你到底研究过没有!
1、-Xms初始堆的大小,等价:-XX:InitialHeapSize,一般是物理内存的 1/64
2、-Xmx最大堆的大小 ,等价:-XX:MaxHeapSize,一般是物理内存的 1/4
最常用的东西都是有语法糖的!
初始默认值
初始的默认值到底有多少?
java -XX:+PrintFlagsInitial
# 查看 java 环境初始默认值;这里面只要显示的值,我们都可以手动赋值,不建议修改,了解即可!
java -XX:+PrintFlagsInitial
= 默认值
:= 就是被修改过的值
java -XX:+PrintFlagsFinal -Xss128k GCDemo
public class GCDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("Hello,World");
TimeUnit.SECONDS.sleep(MAX_VALUE);
}
}
使用java命令行执行程序
# 查看被修改过的值!启动程序的时候判断,修改线程栈大小为128k
java -XX:+PrintFlagsFinal -Xss128k GCDemo
java -XX:+PrintCommandLineFlags
# 打印出用户手动选项的 XX 选项
java -XX:+PrintCommandLineFlags -version
常用参数
-XX:+HeapDumpOnOutOfMemoryError # 让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用
-Xms2g # 初始堆内存大小2g,一般是物理内存的 1/64
-Xmx2g # 最大堆内存大小2g,一般是物理内存的 1/4
-Xss200m # 线程栈大小设置,默认 512k~1024k
-Xmn # 设置新生区的大小,一般不用动!
-XX:MetaspsaceSize=128m # 设置元空间的大小,这个在本地内存中!OOM
-XX:MaxMetaspaceSize=128m # 最大的元空间大小
-XX:+PrintGCDetails # 打印垃圾回收的详细信息
-XX:+TraceClassLoading # 用于追踪类的加载信息并打印出来
-XX:SurvivorRatio=8 # 设置新生代中的 s0/s1 空间的占比,默认就是8,s0又叫from区,s1又叫to区
uintx SurvivorRatio = 8 那么新生代中 Eden:s0:s1 = 8:1:1
uintx SurvivorRatio = 4 那么新生代中 Eden:s0:s1 = 4:1:1
-XX:NewRatio=2 # 设置新生代(Eden:Surival(from:to))与老年代的占比:
NewRatio = 2 新生代1,老年代是2,默认新生代是整个堆的 1/3; 1:2
NewRatio = 4 新生代1,老年代是4,默认新生代是整个堆的 1/5; 1:4
-XX:MaxTenuringThreshold=15 # 对象从新生代进入老年代的存活阈值(默认15)15次GC后对象还存在新生代,那么下次GC就会进入老年代
-XX:+UseParNewGC # 指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC # 指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC # 指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGCDetails # 打印 gc 详细信息
-XX:+TraceClassLoading # 用于追踪类的加载信息并打印出来
-XX:MaxDirectMemorySize=128m # 最大的基础缓冲区大小
-Xms8m -Xmx8m -XX:+PrintGCDetails
初始堆内存大小8m,最大堆内存大小8m,打印gc信息
使用java -XX:+PrintFlagsInitial 查看参数默认值
调优实例参考
测试堆内存=新生区+老年区内存
package com.coding.oom;
/**
* 默认情况:
* maxMemory : 1808.0MB (虚拟机试图使用的最大的内存量 一般是物理内存的 1/4)
* totalMemory : 123.0MB (虚拟机试图默认的内存总量 一般是物理内存的 1/64)
*/
// 我们可以自定堆内存的总量
// -XX:+PrintGCDetails; // 输出详细的垃圾回收信息
// -Xmx: 最大分配内存; 1/4
// -Xms: 初始分配的内存大小; 1/64
// -Xmx1024m -Xms1024m -XX:+PrintGCDetails
public class Demo01 {
public static void main(String[] args) {
// 获取堆内存的初始大小和最大大小
long maxMemory = Runtime.getRuntime().maxMemory();
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("maxMemory="+maxMemory+"(字节)、"+(maxMemory/1024/(double)1024)+"MB");
System.out.println("totalMemory="+totalMemory+"(字节)、"+(totalMemory/1024/(double)1024)+"MB");
}
}
执行结果:
可以看到堆的总内存大小 = PSYoungGen + ParOldGen
提升性能15%
转载来源地址:https://zhenbianshu.github.io
项目的基本情况:项目是一个高 QPS 压力的 web 服务,单机 QPS 一直维持在 1.5K 以上,由于旧机器的”拖累”,配置的堆大小是 8G,其中 young 区是 4G,old区就是8-4G,垃圾回收器用的是 parNew + CMS。
使用 jstat -gcutil pid 1000
每隔一秒打印一次 gc 统计信息。
可以看到,单次 gc 平均耗时是 60ms 左右,还算可以接受,但 YGC 非常频繁,基本上每秒一次,有的时候还会一秒两次,在一秒两次的时候,服务对业务响应时长的压力就会变得很大。
接着查看 gc log,打印 gc log 需要在 JVM 启动参数里添加以下参数:
-XX:+PrintGCDateStamps
:打印 gc 发生的时间戳。-XX:+PrintTenuringDistribution
:打印 gc 发生时的分代信息。-XX:+PrintGCApplicationStoppedTime
:打印 gc 停顿时长-XX:+PrintGCApplicationConcurrentTime
:打印 gc 间隔的服务运行时长-XX:+PrintGCDetails
:打印 gc 详情,包括 gc 前/内存等。-Xloggc:../gclogs/gc.log.date
:指定 gc log 的路径
单次 GC 方面并不能直接看出问题,但可以看到 gc 前有很多次 18ms 左右的停顿。
YGC频繁
轻GC频繁,借助可视化工具分析,把gc log 上传到gceasy网站 https://gceasy.io/,它可以帮助我们生成各个维度的图表帮助分析。
查看 gceasy 生成的报告,发现我们服务的 gc 吞吐量是 95%,它指的是 JVM 运行业务代码的时长占 JVM 总运行时长的比例,这个比例确实有些低了,运行 100 分钟就有 5 分钟在执行 gc。幸好这些 GC 中绝大多数都是 YGC,单次时长可控且分布平均,这使得我们服务还能平稳运行。
解决这个问题
-
减少对象的创建,不是一时半会儿能解决的,需要查找代码里可能有问题的点,分步优化
-
增大 young 区,虽然改一下配置就行,但以我们对 GC 最直观的印象来说,增大 young 区,YGC 的时长也会迅速增大,要清除的垃圾对象多了,所以需要更多的GC时间。
其实这点不必太过担心,我们知道 YGC 的耗时是由
GC 标记 + GC 复制
组成的,GC复制用在新生区,GC标记用在老年区。相对于 GC 复制,GC 标记是非常快的。而 young 区内大多数对象的生命周期都非常短,如果将 young 区增大一倍,GC 标记的时长会提升一倍,但到 GC 发生时被标记的对象大部分已经死亡, GC 复制的时长肯定不会提升一倍,所以我们可以放心增大 young 区大小。
由于低内存旧机器都被换掉了,我把堆大小调整到了 12G,young 区保留为 8G。
分代MaxTenuringThreshold年龄调整
除了 GC 太频繁之外,GC 后各分代的平均大小也需要调整。
我们知道 GC 的提升机制,每次 GC 后,JVM 存活代数大于 MaxTenuringThreshold
的对象提升到老年代。当然,JVM 还有动态年龄计算的规则:按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值,但看各代总的内存大小,是达不到 survivor 区的一半的。如下图:
所以这十五个分代的对象会一直在两个 survivor 区之间(from区和to区)来回复制,每ygc一次就会触发存活对象从eden区和from区复制到to区,同时对象的分代年龄+1。再观察各分代的平均大小,可以看到,四代以上的对象已经有一半都会保留到老年区了,所以将这些对象直接提升到老年代,以减少对象在两个 survivor 区之间复制的性能开销。
所以我把 MaxTenuringThreshold 的值调整为 4,将存活超过四代的对象直接提升到老年代。
偏向锁停顿
还有一个问题是 gc log 里有很多 18ms 左右的停顿,有时候连续有十多条,虽然每次停顿时长不长,但连续多次累积的时间也非常可观。
1.8 之后 JVM 对锁进行了优化,添加了偏向锁的概念,避免了很多不必要的加锁操作,但偏向锁一旦遇到锁竞争,取消锁需要进入 safe point
,导致 STW。
解决方式很简单,JVM 启动参数里添加 -XX:-UseBiasedLocking
使用偏向锁即可。
结果
调整完 JVM 参数后先是对服务进行压测,发现性能确实有提升,也没有发生严重的 GC 问题,之后再把调整好的配置放到线上机器进行灰度,同时收集 gc log,上传到gceasy 再次进行分析
由于 young 区大小翻倍了,所以 YGC 的频率减半了,GC 的吞量提升到了 97.75%。平均 GC 时长略有上升,从 60ms 左右提升到了 66ms,还是挺符合预期的。
由于 CMS 在进行 GC 时也会清理 young 区,CMS 的时长也受到了影响,CMS 的最终标记和并发清理阶段耗时增加了,也比较正常。
9、Dump内存快照
Jvm分析常用命令
点进Java的bin目录,内置工具命令你认识多少
javac # 编译.java源码文件为.class字节码文件
java -jar xxx.jar # 可带配置参数或者JVM参数启动java程序
java -jar test3.jar --spring.config.location=application.properties
# 动态参数启动
java -jar -Dserver.port=8081 sentinel-dashboard-1.7.0.jar
jps -l # 查看运行的java进程id
javap -c Vdemo02.class # 对字节码文件进行反汇编
# 常用的就是结合-dump参数导出内存信息文件,使用MAT 或者 jprofile 分析dump文件
jmap(java memory map) java内存映像工具
# 命令格式
jmap -dump:live,format=b,file=文件路径/文件名 pid
# 例子:
> jps
58837 GCTest
58838 Jps
>jmap -dump:live,format=b,file=dmp.hprof 58837
# 分析java进程ID或core file或远程调试服务的Java堆栈信息
jstack java进程id
# 一个java GUI监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视远程的服务器VM
# 建立java进程的连接后,可以查看内存、CPU、线程等使用情况
jconsole
请你说说在在工作如何排查OOM?我是通过Dump内存快照查看的,可以使用Jconsole和Jprofile工具
在java程序运行的时候,想测试运行的情况!
使用一些工具来查看;
1、Jconsole
2、idea debug
3、Eclipse(MAT插件)
4、IDEA(Jprofiler插件)
Jconsole
public class Demo05 {
public static void main(String[] args) throws InterruptedException {
System.out.println("start");
// 睡眠到死
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
}
}
启动程序,进入jdk安装目录,找到Jconsole
连接后
线程在干什么
加载的类
概要
Java VisualVM
Jprofiler
一款性能瓶颈分析插件
1、IDEA安装 JProfiler 插件
2、window上安装 JProfiler (无脑下一步即可:注意路径中不能有中文和空格,否则报错 )
我使用的是Mac 笔记本,在xclient.info搜索该软件
注册码分享
Name:rjsos
Company:rjsos
License key:A-J11-Everyone#admin-3v7hg353d6idd5#9b4
3、激活
# 注册码仅供大家参考
L-Larry_Lau@163.com#23874-hrwpdp1sh1wrn#0620
L-Larry_Lau@163.com#36573-fdkscp15axjj6#25257
L-Larry_Lau@163.com#5481-ucjn4a16rvd98#6038
L-Larry_Lau@163.com#99016-hli5ay1ylizjj#27215
L-Larry_Lau@163.com#40775-3wle0g1uin5c1#0674
4、在IDEA 中绑定 JProfiler
Jprofile的快速体验
package com.coding.oom;
import java.util.ArrayList;
import java.util.List;
// -Xmx10m -Xms10m -XX:+HeapDumpOnOutOfMemoryError
public class Demo03 {
byte[] bytes = new byte[1*1024*1024]; // 1M
public static void main(String[] args) throws InterruptedException {
// 泛型:约束!
List<Demo03> list = new ArrayList<Demo03>();
int count = 0;
try {
// Error 与 Exception是平级的关系,所以永远都不会捕获到Exception,要使用共同的父类Throwable,
// 不停的往里面加对象
while (true){
list.add(new Demo03());
count = count + 1;
}
} catch (Throwable e) { // Throwable 或者 Error
System.out.println("count="+count);
e.printStackTrace();
}
}
}
执行结果
7次就堆内存爆满了
怎么分析找到报错的代码?加上JVM参数 -XX:+HeapDumpOnOutOfMemoryError
执行结果:
可以发现我们Dump出一个内存快照文件,它被放在了当前项目的目录下
我们使用Jprofile打开它,这是离线分析模式
点击大对象,发现很明显有一个list对象有问题
展开该对象,精准定位到问题的代码行
在Idea直接点击jprofile运行
点击右上角的停止按钮停止jprofile运行程序
public class Demo04 {
public static void main(String[] args) throws InterruptedException {
System.out.println("sdsdfs");
TimeUnit.SECONDS.sleep(MAX_VALUE);
}
}
使用jprofile 运行,可以实时监控程序
分析dump出来的快照,查看异常对象;分析定位到具体的类和代码问题!dump出来的文件一般都比较大