JVM
JVM核心一
JVM的认识
可能遇到的问题
1.谈谈对jvm的理解? java8虚拟机和之前的变化更新?
1)jvm的作用:首先java语音最重要的特点就是跨平台运行,而这特点就是使用了JVM实现的。
2)JVM俗称Java虚拟机,虚拟机是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
3)jvm的内部结构,分成三部分,类装载器,运行时数据区,执行引擎。
4)JVM垃圾回收机制(GC)
java8虚拟机和之前的变化是:删除永久区变为元空间。
2.什么是OOM?
程序申请内存过大,虚拟机无法满足我们,然后自杀了。
类似于Android系统的APP每个进程或者虚拟机有最大内存限制,一旦超过这个限制系统就会抛出OOM错误。
3.什么是栈溢出Stack Overflow Error? 怎么分析?
## 出现栈内存溢出的常见原因有2个:
1> 函数调用层次过深,每调用一次,函数的参数、局部变量等信息就压一次栈。
2> 局部静态变量体积太大
## 解决办法大致说来也有两种:
1> 增加栈内存的数目
2> 使用堆内存
3.JVM常用的调优参数有哪些?
详情看《堆内存调优》
4.内存快照如何抓取?怎么分析Dump文件?
1)获取jvm进程的dump快照文件
## 执行下面的jmap命令可以导出dump内存快照:
jmap -dump:live,format=b,file=dump.hprof pid
2)使用MAT分析内存快照
详情
5.谈谈JVM中,类加载器你的认识?
此问题主要是聊一下类加载器运行过程。
JVM体系结构
类加载器
类加载流程:
- 比如有一个 A.java ,当要使用A类时;
- 类加载器会先通过 双亲委派机制 找到对应的类:依次从启动类加载器(Bootstrap),扩展类加载器,应用程序类加载器中找,如果在启动类加载器找到,就不往下找了。如果都没有找到,会报classNotFund Exception的异常。
- 然后这个类成加载class文件,并且将class文件字节码内容加载到 内存 中,;
- 类装载器只负责class文件的加载, 至于它是否可以运行,则由 执行引擎 决定;
Java类加载的步骤(细节)
装载:把二进制形式的Java类型读入Java虚拟机中。
连接:把装载的二进制形式的类型数据合并到虚拟机的运行时状态中去。
验证:确保Java类型数据格式正确并且适合于Java虚拟机使用。
准备:负责为该类型分配它所需内存。
解析:把常量池中的符号引用转换为直接引用。
初始化:为类中的静态变量变量赋适当的初始值,执行静态代码块。
4种类加载器
- 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。(非常特殊: 无法被java程序直接引用)
- 扩展类加载器(extensions class loader)它用来加载Java的扩展库。Java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java类。
- 系统类加载器(system class loader)也叫应用类加载器,它根据Java应用的类路径(CLASSPATH)来加载Java类。一般来说,Java应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader0来获取它。
- 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
双亲委派机制的好处:
- 因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
- 从安全角度考虑,如果不使用这种委托模式,我们自己定义一个Map来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患。
- 而双亲委托的方式,就可以避免这种情况。因为Map在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader不会在被加载一次。
- 通过双亲委派机制,类的加载永远都是从 启动类加载器开始,依次下放,保证你所写的代码,不会污染Java自带的源代码,所以出现了双亲委派机制,保证了 沙箱安全。
JVM在搜索类的时候,又是如何判定两个class是相同的呢?
- JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同的class。
java栈(栈内存)
概念:
- 栈也叫栈内存,是一种只能从表的一端存取数据且遵循 “先进后出” 原则的线性存储结构。(类似像向枪弹盒里压下子弹,我们最后压进去的子弹,总是第一个打出)
- 堆和栈区别?栈管运行,堆管存储。且8种基本类型的变量 + 对象的引用变量 + 实例方法都是在函数的 栈内存 中分配。而对象在 堆内存 中分配(存放所有new出来的对象)。
java栈生命周期:
- java栈主管java程序运行,是在线程创建时创建,结束时释放。
java实现栈
栈可以分为静态栈(数组实现)和动态栈(链表实现)
数组实现
public class ArrayStatck {
private Object[] data; //数组
private int top;//节点
private int maxSize;//栈大小
public ArrayStatck(int maxSize) {
this.maxSize = maxSize;
this.top = -1;
data = new Object[maxSize];
}
public void addStack(Object value) throws Exception {
if(top == maxSize-1)throw new Exception("栈满异常");
data[++top] = value;
}
public void delStack()throws Exception{
if(top == -1)throw new Exception("栈空异常");
Object value = data[top--];
}
public void getStack(){
while (top != -1)
System.out.println(""+data[top--]);
}
public static void main(String[] args) throws Exception {
ArrayStatck arrayStatck=new ArrayStatck(10);
for (int i=1;i<=10;i++){
System.out.println(i);
arrayStatck.addStack(i);
}
arrayStatck.delStack();
arrayStatck.getStack();
}
}
链表实现
略。分享链接: 小码过河.
栈存储什么?
栈帧中主要保存 3 类数据
① 本地变量 (Local Variables) : 入参和出参,以及方法内的变量;
② 栈操作 (Operand Stack) : 记录出栈和入栈的操作;(可理解为pc寄存器的指针)
③ 栈帧数据 (Frame Data) : 包括类文件、方法等。
堆内存
堆内存
概念:java堆中存放的是访问类元数据的地址
作用:存放所有new出来的对象
堆内存主要分成三个区域(新生区,老年区,元空间)
- 新生区
类:诞生,成长甚至死亡的地方。(活的足够长的类,可能会进入老年区,但是大部分在新生区就玩完了。) - 老年区
新生区的对象满了,就放入老年区。统计上:绝大部分的对象都是临时对象,很少有对象能活到老年区。 - 元空间(java8之后 无永久区)
这个区域是常驻内存的,用来存放JDK自身携带的Class对象,Interface原数据。简单理解为java运行时的环境或类信息。这个区域不存在垃圾回收!关闭JVM虚拟机就会释放这个区域的内存。 - 其他(伸缩区)
概念:如果空间不足,在可变的范围之内扩大内存空间,当一段时间之后发现内存空间没有这么紧张的时候,再将可变空间进行释放。所以在整个调整过程之中
引入其他概念:
元空间:逻辑上存在,物理上不存在,这也是将其称为“非堆的原因”!
这个区域的空间会不会被占满呢?可以的。比如
一个启动类,加载大量的第三方的jar包
tomcat部署了太多的应用
大量动态生成的反射类
关于永久区的变化
jdk1.6之前:永久代,常量池在方法区中
jdk1.7:去永久代,常量池在堆中
jdk1.8,:无永久代,常量池在元空间
其他
GC 垃圾回收,主要是在新生区和老年区
内存爆满,会产生OOM(OutOfMemoryError)错误。
JDK8以后,永久存储区的名字变为元空间。
堆内存调优
引入一个比较常见的问题,例如OOM 内存爆满问题。
解决方案:1.调整JVM的内存空间大小。
1 尝试扩大内存,看结果
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
-Xms:设置初始分配大小,默认为物理内存的“1/64”
-Xmx:最大分配内存,默认为物理内存的“1/4”
-XX:+PrintGCDetails :输出详细的gc处理日志 Runtime.getRuntime().
System.out.println("初始化内存:Total_memory="+Runtime.getRuntime().totalMemory()/(double)1024/1024+"M");
System.out.println("默认最大内存:Max_memory="+Runtime.getRuntime().maxMemory()/(double)1024/1024+"M");
2 分析内存,查看问题,借助JProfiler
3.默认的情况下分配的内存是总内存的1/4;而初始化内存为“1/64”;
那么也就是说整个内存的可变范围(伸缩区):123.0M ~ 1820.5M 之间,那么现在就可能造成程序性能下降;
所以,我最好能让伸缩区的大小为0;即让 Max_memory 和 Total_memory 保持一致(-Xms 和 -Xmx 调成一样大小);
为啥说虚拟机的初始化内存和最大内存(-Xms和-Xmx)设置相同比较好?
- 这里的话,就得引入一个JVM垃圾回收概念了。当我们虚拟机初始化内存不足时,就会直接扩大内存?不是的,虚拟机初始化的内存不足时,会先调用JVM垃圾回收GC,GC操作是需要耗时的,而且Full GC会引起“Stop the World”,也就是说会引起线程停止,不可避免就会引起性能问题等等。
- 另外-Xmx和-Xms设置相等的话。避免在生产环境由于heap内存扩大或缩小导致应用停顿,降低延迟,同时避免每次垃圾回收完成后JVM重新分配内存。
Native 关键字 (Native Interface 本地接口)
概念:
- 在Java中是一个关键字,有声明,无实现。作用是融合不同的编程语言。native 方法需要在native method stack(本地方法栈) 中登记,在Execution Engine(执行引擎) 执行时加载本地方法库。
作用:
- 以线程为例,线程其实不属于Java的,其实它是属于操作系统底层的。Java中通过Thread类的start() 类启动一个线程。之后运行的是一个 private native void start0()方法。说明,线程方法是交由操作系统去处理的。
程序计程器
概念:也叫程序计数器。用于存储只想下一条指令的地址,即将要执行的指令代码。是最快的存储区。
每个线程都有一个程序计数器,是线程私有的,就是一个指针,只想方法区中的方法字节码。由执行引擎读取下一条指令。
方法区
概念:供各线程共享的运行时内存区域。