Java虚拟机整体架构祥图

类加载器
虚拟机自带的加载器
启动类(根、BootStrap)加载器
扩展类加载器
应用程序(系统)加载器
public static void main(String[] args) {
ClassLoader classLoader = OtherTest.class.getClassLoader();
//AppClassLoader,应用程序加载器 实现java.lang.abstract
System.out.println(classLoader);
//ExtClassLoader,扩展加载器 jre/lib/ext目录
System.out.println(classLoader.getParent());
//null,不存在或者java无法获取 rt.jar
System.out.println(classLoader.getParent().getParent());
}双亲委派机制
类加载器收到加载类的请求
将这个请求向上委托父类加载器去完成,一直向上委托到根加载器
根加载器检查是否能够加载此类,能加载就加载并结束;不能加载则抛出异常,通知子加载器进行加载
重复步骤3
示例代码:
package java.lang;
/*
* @Description
* @Author Doubly
* @Date 2021/10/8
*/
public class String {
public String toString(){
return "Hello";
}
public static void main(String[] args) {
String s = new String();
System.out.println(s.toString());
}
}运行结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application 为了保证程序的安全,首先在AppClassLoader中找String,虽然已经找到,但是会继续在ExtClassLoader中找,最后在BootStrapClassLoader中找,找到rt.jar下面的String。所以不会家在我们自己写的String,会报没有main方法的错误。
AppClassLoader -> ExtClassLoader -> BootstrapClassLoader(最终执行)
native关键字
凡是带了native关键字的,说明Java的作用范围达不到了。会进入本地方法栈,通过本地方法接口(JNI)调用本地方法库。比如Object中的hashcode()方法,就是调用的本地方法。
方法区
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存,和方法区无关
栈
8大基本类型+对象引用+实例的方法
堆
一个jvm只有一个堆内存,堆内存大小是可以调节的。
默认分配的总内存是电脑的1/4,初始化的内存是电脑的1/64.
代码示例:
public class Test {
public static void main(String[] args) {
//返回虚拟机试图使用的最大内存
long maxMemory = Runtime.getRuntime().maxMemory();
//返回虚拟机初始化使用的总内存
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("max:"+maxMemory + "字节 " + (maxMemory/(double)1024/1024) + "MB");
System.out.println("max:"+totalMemory + "字节 " + (totalMemory/(double)1024/1024) + "MB");
}
}结果:
max:1908932608字节 1820.5MB
max:128974848字节 123.0MB
堆内存细分三个区域:
新生区(伊甸园区+幸存区0区+幸存区1区)(幸存区也叫from区与to区)
养老区
永久区
新生区
伊甸园区
所有的对象都是在伊甸园区new出来的,伊甸园区满了会出发轻gc
幸存区(0区+1区)
gc触发后,还存在引用指向对象,对象就不会被回收,会进入幸存区。伊甸园区和幸存区都满了以后会今进行重gc。
养老区
幸存区满了之后,会进入养老区。一般99%的对象都会被清除,默认当一个对象经历15次后可以进入养老区。养老区满了之后就出现了OOM(Out Of Memory)。
查看JVM堆中每个区的情况,可使用如下参数
JVM参数:
-Xms1024m -Xmx2048m -XX:+PrintGCDetails 输出:
Heap
PSYoungGen total 305664K, used 20971K [0x0000000795580000, 0x00000007aaa80000, 0x00000007c0000000)
eden space 262144K, 8% used [0x0000000795580000,0x00000007969fafb8,0x00000007a5580000)
from space 43520K, 0% used [0x00000007a8000000,0x00000007a8000000,0x00000007aaa80000)
to space 43520K, 0% used [0x00000007a5580000,0x00000007a5580000,0x00000007a8000000)
ParOldGen total 699392K, used 0K [0x0000000740000000, 0x000000076ab00000, 0x0000000795580000)
object space 699392K, 0% used [0x0000000740000000,0x0000000740000000,0x000000076ab00000)
Metaspace used 3104K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 339K, capacity 388K, committed 512K, reserved 1048576K
OOM之后如何排查问题?
尝试扩大堆内存看结果
分析内存,看一下哪个地方出现了问题(专业工具)
内存快照分析工具:MAT(Eclipse最早继承),Jprofiler
MAT、JProfiler作用:
分析Dump内存文件,快速定位内存泄漏
获得堆中的数据
获得大的对象
如何获取JVM的dump文件?
-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryErrorpublic class Test {
//-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError
public static void main(String[] args) {
String str = "MyString";
while (true){
System.out.println(str);
str += new Random(999999999);
}
}
} 永久区/元空间
逻辑上存在,物理上不存在。新生区+老年区=jvm使用的堆内存
这个区域常驻内存的,用来存放jdk自身携带的Class对象、Interface元数据,存储的是Java运行的一些环境或类信息。这个区域不存在垃圾回收。关闭虚拟机就会释放这个区域的内存。
历史:
Jdk1.6之前:永久代,常量池在方法区重
JDK1.7:永久代,但是慢慢的退化了。去永久代,常量池在堆中
JDK1.8以后:无永久代,常量池在元空间
垃圾回收
垃圾回收主要是在伊甸园区和养老区
垃圾回收可分为两种:
轻量级垃圾回收(轻gc)
回收新生区和偶尔幸存区
重量级垃圾回收(重gc、full gc)
养老区满了,回收养老区
JVM如何判断对象需要回收?
- 引用计数法:每个对象引用次数做计数,每次被引用则计数+1,失去引用则-1。当计数为0的就清除。
- 可达性算法:从GC Roots开始查找引用,能够被找到的说明被引用可达,其余的则引用不可达,需要清除。
什么是GC Roots?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的native方法)中引用的对象
gc算法
标记清除法:扫描对象,对活着的对象进行标记;对没有标记的对象进行清除
优点:不需要额外空间
缺点:标记和清除两次扫描,严重浪费时间;会产生内存碎片
标记压缩:再对标记清除算法进行压缩整理
再次扫描,向一段移动存活对对象,防止内存碎片的产生
复制算法:新生区以及from区的存活对象复制到to区,to区永远是空的
优点:没有内存碎片
缺点:浪费空间,to区永远是空的
最佳使用场景:对象存活度较低,新生区
总结
内存效率:复制算法>标记清除>标记压缩(时间复杂度)
内存整齐度:复制算法=标记压缩>标记清除
内存利用率:标记压缩=标记清除>复制算法
没有最优的算法,只有最合适的算法。
所以GC使用分代收集算法
新生区:存活率较低,使用复制算法
老年区:存活率较高,使用标记清除+标记压缩算法