JVM的位置
JVM体系架构
类加载器
- 类加载器收到类加载的请求
- 将这个请求委托给父类加载器,一直向上委托,直到启动类加载器
- 启动类加载器检查是否能加载这个类,能加载就结束,使用当前的加载器,否则就抛出异常,通知子加载器进行加载
- 重复步骤3
- 若都没找到抛出ClassNotFind异常
双亲委派机制:保护安全 (APP -> EXC -> BOOT)
eg.如自己创建了一个和jdk库中类名相同的类,会一直向上找,最终在最上层运行
Native
凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层C语言的库!
会进入本地方法栈,调用本地方法接口(JNI)
例如:new Tread().start() 源码中的 start0()方法。
private native void start0();
使用场景:Java驱动打印机,主要用于调用硬件,一般用不到
程序计数器(PC计数器)
一个线程一个虚拟机栈,每个线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址,也即将要执行的指令代码),占内存非常小可以忽略不计
方法区
- 方法区是被所有线程共享
- 静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池在方法区中,但是实例变量存在堆中。static、final、Class、常量池
栈
虚拟机栈又叫栈内存,主管程序的运行,生命周期和线程同步
一个线程一个虚拟机栈,线程结束,栈内存也就释放,对于栈来说不存在垃圾回收问题
主要存放 8大基本类型 + 对象引用地址 + 实例的方法
栈运行原理:栈帧
栈满了:StackOverflowError(递归循环调用)
栈、堆、方法区的调用关系:
堆
Heap。一个JVM只有一个堆内存,堆内存的大小是可以调节的。
- 类加载器读取了类文件后,一般会把类、方法、常量、变量、保存我们所有引用类型的真是对象放在堆中
JDK8以前,堆中分为三个区域
-
新生区
- 类:诞生和成长的地方,甚至死亡
- 所有的对象都是在伊甸园区被new出来的
- 幸存者区(有0和1区)
-
老年区
- 经过GC之后仍然存活的对象
-
永久区
这个区域常驻内存,用来存放JDK自身携带的Class对象、Interface元数据,存储的是Java运行时的环境。这个区域不存在垃圾回收。关闭虚拟机就会释放这个区域的内存。
以下情况会出现OOM:大量动态生成的反射类不断被加载、Tomcat部署了太多应用、一个启动类加载了大量第三方Jar包- jdk1.6之前:永久代,常量池在方法区
- jdk1.7:永久代,常量池在堆中
- jdk1.8之后:元空间,常量池在元空间中。
GC垃圾回收主要在新生区和老年区
- 新生区中的伊甸园区满了之后会发生轻量级GC
- 轻GC一次后未回收的对象在幸存区,达到15或20次之后到达养老区
- 老年区满了之后会发生重量级GC(Full GC)
假设内存满了,会报错OOM。(例如String一直+=)
堆内存OOM解决:
- 尝试扩大堆内存看结果:在Edit Configuration界面VM options参数中填写: -Xms1024m -Xmx1024m。(默认情况下分配的总内存是电脑内存的1/4,而初始化的内存是1/64)
- 分析内存,看哪个地方出现了问题(一般是内存泄露)
- 使用JProfiler
新生区的内存大小+老年区的内存大小=总内存大小,说明了元空间逻辑上存在物理上不存在。
使用JProfiler工具分析OOM原因
idea下载JProfiler插件,百度下载JProfiler软件
在Edit Configuration界面VM options参数中填写:
-Xms1m -Xmx8m -Xx:+HeapDumpOnOutOfMemoryError)
其中(OutOfMemoryError)可替换,-Xms设置初始化内存分配大小, -Xmx设置最大分配内存,-Xx:+PrintGCDetails打印GC垃圾回收具体信息,-Xx:+HeapDumpOnOutOfMemoryError打印OOM Dump信息
运行后会在src同级目录自动生成 .hprof文件,双击打开后可在Biggest Objects中查看哪些类占用较大
在左侧Heap walker中可查看哪行代码出了问题
GC垃圾回收算法
只在堆中进行GC,
- 引用计数法(基本不会使用)
- 复制算法
from和to区是随时变换的,但to区一定是空的。
将幸存区from中的对象全部移动到to中。
- 好处:没有内存的碎片
- 坏处:浪费了内存空间:永远都会有一块是空的(to区)。当from全部存活时,复制算法将会造成非常大的浪费。
- 使用场景:对象存活度较低的时候(新生区)
-
标记清除算法
缺点:两次扫描,严重浪费时间,会产生内存碎片
有点:不需要额外空间 -
标记整理(标记压缩)
在标记清除的步骤中多加了一步扫描:整理
- 标记清除整理(标记清除压缩算法)
在标记清除5次后,再进行整理
总结:
内存效率(时间复杂度):复制算法 > 标记清除算法 > 标记整理(压缩)算法
内存整齐度:复制算法 = 标记整理(压缩)算法 > 标记清除算法
内存利用率:标记清除算法 = 标记整理(压缩)算法 > 复制算法
没有最优的算法,只有最合适的算法------>GC分代收集算法
GC分代收集算法:
年轻代:存活率低,使用复制算法
老年代:区域大、存活率高:标记清楚(内存碎片不是太多) + 标记整理混合实现
轻GC(Minor GC)和重GC(Full GC)
Minor GC
当新对象去伊甸园区(Eden)申请内存失败的时候,就会进行Minor GC,对伊甸园区(Eden)回收非存活对象,而没有被回收的对象,会进入幸存区(Survivor),这种GC只发生在伊甸园区(Eden),不会影响到老年区。因为新对象分配内存大部分都在伊甸园区(Eden),所以伊甸园区(Eden)GC比较频繁。
注意:在GC之后,还存活的对象,进入幸存区(Survivor),谁空谁是to,可以交换位置,当一个对象经历了15次GC(可以配置次数:-XX:+MaxTenuringThreshold=15),还存活,就进入老年区。
Full GC
清理整个堆,因为Full GC需要对整个堆进行回收,所以比Minor GC慢,因为我们要尽可能的减少Full GC的次数。我们所说的JVM调优,很大一部分就是对Full GC的优化。
以下情况会造成 Full GC:
老年区满了:年轻区的对象转入或创建大对象才会满。
持久区满了(jdk7及之前版本)
方法区满了(jdk8及之后版本):系统中要加载的类过多。
System.gc() 被显示调用
通过Minor GC后进入老年代的平均大小大于老年代的可用内存:第一次Minor GC之后,有2MB的对象转入老年区,然后在下一次Minor GC的时候就会判断老年区的空间是否有2MB,如果没有就进行Full GC。
拓展
JMM
- 什么是JMM
Java Memory Model :Java内存模型 - 它干嘛的?
缓存一致性协议,用于定义数据读写的规则(遵守)
JMM中定义了线程工作内存与主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
解决共享对象可见性的问题:volatile