JVM---超详细介绍
1.定义:
java virtual machine -Java程序的运行环境(Java二进制字节码的运行环境)
2.好处:
- 一次编写,到处运行
- 自动内存管理,垃圾回收功能
- 数组下标越界检查(c的话会抛异常,但是可能会覆盖已有数组内存内容)
- 多态
3.比较:
JVM(Java虚拟机),JDK(Java 开发者工具), JRE(Java运行环境)JavaSE(纯Java工具) JavaEE(多服务器部署Java应用)
4.学习JVM有什么用:
- 最真实的 面试 最近失业就面的比较多
- 理解底层的实现原理
- 中高级程序员必备技能(常用来定位现场实际问题,如OOM,程序缓慢)
5.常见的JVM:(本文已hotSpot为准)
6.学习进阶路线:
- 通过类加载器加载编译的Java字节码
- 类放在方法区
- 类中的实例在堆中创建
- 堆中的实例在调用方法时又会用到栈,程序计数器,本地方法栈
- 解释器解释每行代码
- 方法中的热点代码(执行较多的)由即时编译器解释运行
- GC进行垃圾回收
- 本地方法接口肯能会与操作系统产生一些调用
7.JVM的内存结构:
- 类加载器
- 程序计数器
- 方法区
- 堆
- 栈
- 本地方法栈
类加载器:
特点:使用双亲委培机制,一般我们自己创建的类采用app加载器加载,app有一个父类未Ext类,ext的非直接父类为bootstrap类,其中bootstrap类主要加载jdk自身的class,既Java本身的语法方法,ext为jre环境下的一个class类,最后才是我们的自己的class.
程序计数器:(主要用于记录下一条jvm指令地址,底层使用寄存器来存取(速度快))
特点:(程序展示:java程序报错时定位的行数就是通过程序计数器实现的)
1.线程私有,每个线程都有自己的程序计数器,cpu根据时间片分配方式,随机调度多个线程中的一个运行,若没运行完则下次就是根据程序计数器来运行
2.唯一一个Java虚拟机中不会存在内存溢出的区
虚拟机栈:
线程运行需要的内存空间,一个线程需要一个虚拟机栈,一个虚拟机栈中由多个栈帧组成,栈帧类比于每个方法运行时需要的内存。栈帧包括:参数,局部变量,返回地址等信息
- 每个线程运行时需要的内存称为虚拟机栈
- 每个栈由多个栈帧组成,对应每次方法调用时所占的内存
- 每个线程只能有一个活动栈帧,对应线程当前正在执行的那个方法
实际演示:在idea中通过单步调试可类比方法入栈和出栈的场景
问题辨析:
- 垃圾回收是否涉及栈内存:不需要,栈内存不受垃圾回收处理
- 栈内存分配越大越好么:可通过-Xss size 来制定大小 一般系统默认为1M ,栈内存越大只是扩大方法调用层次,不会加快速度,扩张栈内存会减少最大线程数,IDEA可通过如下设置
- 方法内的局部变量是否是线程安全的:判断线程安全一般是判断变量是私有的还是公有的,
由于每个线程都有自己的栈帧,局部变量是方法栈帧的子元素,所以得定位变量是否有逃离栈帧来判断线程安全
线程栈帧的局部变量是线程安全的多个线程共享的则是线程不安全的
示例:
栈内存溢出:
可能原因:
1.栈帧过多栈内存溢出,类似递归无释放的情况 ,JSON解析类无限调用(@JsonIgnore)
2.栈帧过大导致占栈内存溢出,不易出现
8.线程运行诊断
案例1:CPU占用过多
1.定位进程号 通过top查看最高pid 这里以25536 pid作为举例
2.ps H -eo pid,tid,%cpu | grep 25536 定位tid 这里以25537 tid作为举例
H -代表打印进程数
-eo -代表要输出的字段 后面跟字段名,多个逗号隔开
pid 进程号
tid 线程号
3.使用JDK提供的工具jstack 查看pid
jstack 25536 可看到所有pid进程的线程信息
把tid 转换成16进制 通过查询定位到具体的线程信息,可查看对应的代码部分来定位问题、
案例2:程序运行很长时间没有结果
1.同上定位到jstack
2.查看jstack最后部分,可定位到具体的死锁线程以及代码段
具体示例代码如下:
本地方法栈:
- Java本身难以实现,通过调用第三方语言写的类库实现的功能,需要的内存都在本地方法栈。
- 例如Object类中的clone,wait,方法 返回的是native Object ,此类方法基本都是调用非Java写的三方类库代码实现。
注:总结,虚拟机栈,程序计数器,本地方法栈都是线程私有的,当虚拟机栈的栈帧变量未逃离栈帧作用域时,则线程是安全的,否则则是不安全的。
堆和方法区是线程共享的。
堆:
- 特点:通过new关键字,创建对象都会使用堆内存
- 它是线程共享的,堆中的对象都需要考虑线程安全问题
- 有垃圾回收机制
堆分配:
1.Java的堆内存主要分为新生代(大小占2/3)和老年代(大小占2/3),其中新生代又分为Eden区和Survivor区,Survivor区又分为两个1区和二区,根据对象流向,可对对应的区命名为From区和to区,两者直接的对象是互相流动的。当新生代内存满了后就会回收一部分对象至老年代
堆优化:一般我们说jvm虚拟机优化即指堆内存优化,主要的内容其实就是一些参数的调优,以及一些代码的约束和规范,这里列举一些常用的调优参数
堆内存溢出:(OOM一般出现流程:新生代已满,老年代已满,fullGc,若没有内存则报OOM)
演示程序:通过-Xmx 可调整堆内存,一般堆内存比较大,和设备内存相关,排查堆内存问题一般可以把堆内存设置小一点,把相关可能的问题先定位出来
7
堆内存诊断:
工具:
- jps工具:查看当前系统中有哪些Java进程
- jmap工具:查看堆内存占用情况,主要参数 -heap
- jconsole工具:图形界面多功能监测工具,可以连续监测
- jvisualvm 工具:类似jsonsole图形化,并且有堆转储功能即堆dump 查看某一时刻具体的堆情况详细信息
这里以一个演示程序说明:
/** * 演示堆内存 */ public class Demo1_4 { public static void main(String[] args) throws InterruptedException { System.out.println("1..."); Thread.sleep(30000); byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb System.out.println("2..."); Thread.sleep(20000); array = null; System.gc(); System.out.println("3..."); Thread.sleep(1000000L); } }
1.通过jps查看Java进程:
2.jmap使用
演示程序分三个输出点,分别是程序初始化时输出1,创建10M数组完成输出2,垃圾回收数组输出3
我们通过控制台jmap -heap 18756分别抓取三次输出的结果查看堆内存变化:
命令会输出两部分,
第一步分为堆内存配置
第二部分为当前堆内存信息,这里我直接将三次的堆信息截图出来:
3.jconsole工具使用:
还是用jmap演示程序演示,在运行时控制台输入jconsole查看
1.选择对应程序
2.查看堆内存变化
总结:jconsole工具目前比较强大,可以定位很多问题,并且是图形化展示,也可用于定位线程死锁信息,类似于我们的jstack tid
常见案例:
1.垃圾回收后,内存占用任然很高.
示例:
/** * 演示查看对象个数 堆转储 dump */ public class Demo1_13 { public static void main(String[] args) throws InterruptedException { List<Student> students = new ArrayList<>(); for (int i = 0; i < 200; i++) { students.add(new Student()); // Student student = new Student(); } Thread.sleep(1000000000L); } } class Student { private byte[] big = new byte[1024*1024]; }
上面已经讲过三种工具了,这里我们用jvisualvm来定位程序中内存占用问题,运行测速程序,在控制台输入jvisualvm连接对应的Java进程
使用线程中的dump功能查看最高堆内存的一些对象来定位问题,这里我们可以看到前两个的内存占用最严重 我们点进去可以进行具体的查看
我们可以看到具体的内容类型及大小来分析该list属于哪里间接程序定位问题、
9.方法区
- jdk1.8中对方法区的定义:所有Java虚拟机中线程共享的,存储相关类的信息,包括运行时常量池,字段,方法和构造方法等,方法区在jvm启动时创建,逻辑上是堆的一部分,具体实现上不一定。原来的永久代在1.8后换成了元空间,不占用jvm内存,占用的是本地内存即操作系统内存,同时Stringtable移到了堆中。这里的概率我只是摘取了部分,具体的大家可自行找资料
- 方法区申请内存不足时也会有内存溢出错误
- 概率草图如下
方法区内存溢出演示代码:(1.8需通过设置元空间大小 一般本地内存较大)
/** * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace * -XX:MaxMetaspaceSize=8m */ public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码 public static void main(String[] args) { int j = 0; try { Demo1_8 test = new Demo1_8(); for (int i = 0; i < 10000; i++, j++) { // ClassWriter 作用是生成类的二进制字节码 ClassWriter cw = new ClassWriter(0); // 版本号, public, 类名, 包名, 父类, 接口 cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null); // 返回 byte[] byte[] code = cw.toByteArray(); // 执行了类的加载 test.defineClass("Class" + i, code, 0, code.length); // Class 对象 } } finally { System.out.println(j); } } }
实际场景中:
Spring,mybatis中应用了很多的类加载机制,如cglib 其asm包底层就是动态生成字节码,所以在运行期间还是很容易造成内存溢出的,但1.8后移步到了元空间,所以就基本不考虑这个了
10.垃圾GC说明:
垃圾回收算法:1.复制算法 2.标记清除算法 3.标记整理算法
JVM垃圾回收机制:分代垃圾回收算法,即不同堆内存使用不同的垃圾回收算法。
新生代的GC又成为Minor Gc,通过可达性分析对象引用来进行对象回收,采用的是复制算法 ,将存活的对象放入to,并且开始Survival区From和to区的交换,同时标记存活的年龄+1,如果Survival中的from区进行垃圾回收时发现年龄已经达到15岁(<=15不一定达到15移动),则会将该对象放入老年代。
老年代的Gc它会进行全局的清理,包括新生代(MinorGc)和老年代的清理。堆已满,新生代Gc后内存任然不足,那么就会触发FullGc(耗时更长)。老年代的算法可能是标记清除或者标记整理算法。