前言
前一阵子在公司内部做了一次技术分享,主要讲的就是JVM核心知识。由于JVM涉及的知识太多太广,所以我就以个人的经验把内容做了一下精简,只保留最核心的内容,并且把核心的内容都给抽出来,让大家记住最重要的部分。现在,我把分享的内容总结出来。(文中如有纰漏,还望您批评指正,谢谢)
什么是JVM?
JVM是Java Virtual Machine(Java虚拟机)的缩写,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域等组成。JVM屏蔽了与操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行的目标代码(字节码),就可在多种平台上不加修改的运行,这也是Java能够“一次编译,到处运行的”原因。
Java文件被加载到JVM的过程
Java类的加载
1.四大主动引用
调用类的构造方法 new Test();
调用类的静态变量、静态方法已经反射
初始化子类(java规定:初始化子类的时候会先初始化其父类,所以父类会被加载)
-
含有main方法类会被提前加载
注意:调用常量不会引起类的加载
2.类的加载过程
类加载过程都做好了什么?
准备:给静态变量开辟内存空间,同时设置初始化的值(注意:这里不是代码设置的初始化值,而是静态默认的缺省值,比如 int的默认值是0)
解析:把常量的符号引用改成直接引用(直接引用即为内存地址)
初始化:执行静态变量和静态代码块(当存在多个静态变量和静态代码块的时候,按照从上到下的顺序执行)
总结:当一个类被主动引用的时候,会进行类的加载,类的加载过程中会进行静态变量的初始化和静态代码块的执行,并且会把常量的符号引用替换成直接引用。整个类的加载过程网上一搜有一大堆,黑子加粗的是需要大家必须知道和记住。
为什么静态方法中不能引入非静态变量?
答:当类被加载的时候,非静态变量还没有得到初始化,而静态方法不需要拥有对象实例就可以得到执行,所以静态方法中不能引用非静态变量。
为什么可以使用 private static final Singleton instance = new Singleton();这种方式做单例模式?
答:因为类只会被初始化加载一次,当类被加载的时候静态变量会得到初始化,Java的枚举类就是利用这种方式,里面的枚举类型的数据就是静态常量
类加载器
启动类加载器: 负责加载存放在
<JAVA_HOME>\lib
目录中的类;被-Xbootclasspath
参数所指定路径中、并且是被虚拟机识别的类库扩展类加载器:负责加载
<JAVA_HOME>\lib\ext
目录中的类库;被java.ext.dirs
系统变量所指定的路径中的所有类库-
应用类加载器:负责加载 用户类路径(
ClassPath
)上所指定的类库(如无特殊指定,我们的写的在项目里写的java代码都是被这个类加载器进行加载的)
4.双亲委派模型
当一个类被加载的时候,所有的类加载器都会把加载任务交给父加载器,结合上这张图来看,也就说双亲委派模型最后都会交给启动类加载器进行加载,只有当父类加载器加载不了的时候才会交给子类进行加载。
protected Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(var1)) {
Class var4 = this.findLoadedClass(var1);
if (var4 == null) {
long var5 = System.nanoTime();
try {
if (this.parent != null) {
var4 = this.parent.loadClass(var1, false);//先让父类进行加载
} else {
var4 = this.findBootstrapClassOrNull(var1);//当parent为null的时候使用启动类加载器加载
}
} catch (ClassNotFoundException var10) {
}
if (var4 == null) {
long var7 = System.nanoTime();
var4 = this.findClass(var1);//当父类加载不了的时候才会交给子类进行加载
PerfCounter.getParentDelegationTime().addTime(var7 - var5);
PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
PerfCounter.getFindClasses().increment();
}
}
if (var2) {
this.resolveClass(var4);
}
return var4;
}
}
优点:保证了类的唯一性
注意:双亲委派模型是java推荐的一种技术模型,不是自定义ClassLoader加载器必须遵守的,另外这里的双亲指的就是父类,别问我我也不知道为啥非要叫双亲
JVM内存结构
运行时数据区
JVM的运行时数据区主要5个部分组成,分别是方法区、程序计数器、方法栈、本地方法栈以及堆内存。其中用红色标注的堆内存和方法区是线程共享,剩下的三个是线程私有的。
方法区
方法区,又叫永久代,jdk1.6以后也叫元空间。是JVM内存区域中比较稳定的一块,很少发生GC。在方法区中存放类信息、静态变量以及常量等信息。方法区是线程共享的,存在线程安全问题。
栈内存
也叫线程栈,每一个线程都有线程栈,每一个方法都是一个栈帧,在方法从被调用到结束的过程就是对应栈帧入栈以及出栈的过程。在栈帧中又有4个区域
- 局部变量表:存放方法中的局部变量
- 操作数栈:执行方法中的各种操作,例如:赋值之类的操作
- 动态链接:把方法中的符号引用转化为直接引用(直接引用即内存地址)
- 方法出口:记录返回值以
及程序当前执行的位置
本地方法栈
同方法栈类似,存放的是本地调用方法(即native修饰的,如public static native void yield();
程序计数器
线程私有,每个线程会有一个区域存放程序计数器
作用:记录所在线程执行到了哪一步
场景:当某个所有的线程的时间片被抢走时,过了一段时间再被恢复以后,该线程的代码不会从头执行,而是会按照程序计数器所记录的位置执行
堆内存
运行时数据区中栈内存最大,线程共线,是主要发生GC的位置,存放的是Java实例对象,也就是说所有通过new的对象都存在这里。在堆内存中,有划分了成了新生代和老年代,默认比例1:2,在新生代中又划分为了eden、sur0和sur1,默认的比例是8:1:1,所有刚new的出来的对象都会放到eden区域中。
总结
JVM内存结构是非常重要的一个部分,有几个关键的点一定要记住:
所有new的对象一定存在堆内存中
-
非静态成员变量存在堆内存中,局部变量都在栈内存中
通过代码再来加固一下
public class Test {
public static User user = new User();//static修饰 user这个实例存在方法区 new User()生成的对象在堆区,此时user引用了堆区的对象
private int a = 10;//a存在了堆区,引用了栈里面的整型变量10
private Date date = new Date();//date存在堆区,new Date()生成的对象也在堆区,date引用了生成的对象
private int compute() {
int a = 1;
int b = 2;
User user = new User();//注意此时user是局部变量,所以存在栈区,而new User()生成的对象在堆区,此时栈区的user引用了堆区生成的对象
int c = (a + b) * 10;
int d = com();
return c;
}
public static void main(String[] args) {
Test test = new Test();
System.out.println(test.compute());
}
}
垃圾回收
垃圾回收即GC,刚才讲堆内存的时候,提到了堆内存是存放的java对象,是主要发生GC的位置。
这里需要注意的是,垃圾回收器在判断一个对象是否存活时用的是可达性分析法。可达性分析法是判断一个对象有没有被GC ROOT直接或者间接持有,只要被GC ROOT直接或者间接持有,那么该对象就是存活状态,或者即为死亡。
GC ROOT:
1.方法栈(局部变量表)中引用的对象
2.本地方法栈 中 JNI引用的对象
3.方法区 中常量、类静态属性引用的对象
总结
本文主要讲了类的加载过程、JVM的内存结构以及垃圾回收。由于我个人读过很多JVM的文章都写得太多太详尽,反正读多了容易掌握不到重点,而本文就是对核心知识一个总结,虽然不够详尽,但是无论是工作还是面试当中应该都够用了。如果文章有错误的内容或者漏讲了一些重要的知识点,欢迎大家在评论区给我留言,谢谢~