目录
一、JVM内存划分
二、JVM类加载
1、什么是类加载
2、类加载的过程
2.1 加载
2.2 连接
2.3 初始化
3、何时触发类加载
4、双亲委派模型
4.1 什么是双亲委派模型
4.2 双亲委派模型的优点
三、JVM的垃圾回收机制
1、什么是GC
2、GC回收哪部分内存
3、判定垃圾的算法
3.1 引用计数
3.2 可达性分析
4、垃圾回收算法
4.1 标记清除
4.2 复制算法
4.3 标记整理
4.4 分代回收
一、JVM内存划分
堆区:存放new出来的对象,所有线程共享堆区
栈区:存放方法之间的调用关系,每个线程对应有一个栈区
方法区:存放类对象(加载好的类),所有线程共享方法区
程序计数器:存放下一个要执行的指令的地址,每个线程对应有一个程序计数器
注意: 方法区在JDK8之后被替换为元数据区/元空间
JVM的内存划分一般会结合代码来考,举个例子:
class Test2{
private int z;
}
public class Test {
//x是成员变量,在堆区
public int x;
//y是静态成员变量,类变量,在方法区
public static int y;
//test2也是静态成员变量,在方法区,但是它new的对象在堆区,所以test2中的z也在堆区
public static Test2 test2 = new Test2();
public static void main(String[] args) {
//test是一个局部变量,在栈区,但是它new的对象在堆区
Test test = new Test();
}
}
注意:变量存在于哪个区域,和变量类型无关,和变量的形态(局部/成员/静态)有关!
二、JVM类加载
1、什么是类加载
java程序在运行之前,需要先进行编译,把.java文件编译为二进制字节码的.class文件,当程序运行的时候,JVM就会读取对应的.class文件,并且解析文件中的内容,在内存中构造一个类对象并初始化。
2、类加载的过程
类加载的过程大体可分为以下几个步骤:
2.1 加载
JVM找到对应的.class文件,读取文件内容,并按照.class规范的格式来解析。
注意:“加载”阶段只是整个“类加载”过程中的一个阶段,不要搞混了~
2.2 连接
连接过程又可以细分为三个步骤:
(1) 验证
检查当前的.class文件的字节流中包含的信息是否符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害JVM自身的安全。
(2) 准备
给类里的静态变量分配内存空间,初始值设为0。
(3) 解析
将常量池内的符号引用替换为直接引用,即初始化字符串常量的过程。
一个.class文件中包含很多字符串常量,比如代码里有一个字符串常量:String s = "hello",但是在类加载之前,"hello"还没有分配到内存空间,s里也就无法保存"hello"的真实地址,只能先使用一个占位符(即符号引用)来标记一下(这里存的是"hello"的地址),等到真正给"hello"分配内存空间后,就能使用真正的内存空间地址替代占位符~
2.3 初始化
执行类中的构造方法,对类进行初始化,加载父类、初始化静态变量、执行静态代码块……
3、何时触发类加载
(1) 第一次创建类的实例时;
(2) 使用了类的静态方法或静态属性;
(3) 使用了类的子类。
4、双亲委派模型
4.1 什么是双亲委派模型
JVM加载类,是由类加载器来负责的,JVM中自带了多个类加载器(程序猿也可以自己实现)。
描述这几个类加载器相互配合的工作过程就是双亲委派模型。
如果一个类加载器收到了类加载的请求, 它不会自己去尝试加载这个类,而是先把这个请求交给父类加载器去完成,每一个类加载器都是这样,因此所有的请求最终都会先交给最顶层的类加载器去完成,只有当父类加载器无法完成这个请求时(它的搜索范围中没有找到所需的类),子类加载器才会尝试自己去完成。
如果父类加载器无法完成这个请求,就会交还给子类加载器去完成。
4.2 双亲委派模型的优点
安全性:使用双亲委派模型可以保证Java的核心API不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己负责的类就可能会带来一些问题,比如程序猿自己写了一个全限定名称为“java.lang.Object”的类,那么程序运行的时候,就会加载程序猿自己写的Object类,而不是标准库中的Object类,因此安全性就不能得到保证。
三、JVM的垃圾回收机制
1、什么是GC
由于申请内存资源后,需要手动释放内存资源,否则会造成内存泄漏问题,但是手动释放内存资源最大的问题就是容易忘记释放,GC(Garbage Collection)是一种自动释放资源的机制,程序猿只需要负责申请内存,释放内存的工作,由JVM完成。
2、GC回收哪部分内存
GC主要针对堆区来进行内存回收。
栈区的变量,释放时机确定(出了作用域生命周期就结束),不必回收;程序计数器是固定内存空间,不必回收;方法区中的静态属性,在类加载之后一般是不会卸载的,所以也不需要进行处理。
3、判定垃圾的算法
在Java中,如果一个对象没有被任何引用指向的时候,就说明这个对象无法再被使用了,那么这个对象就是垃圾。
两种典型的判定对象是否存在引用的方法:
3.1 引用计数
给每个对象都加上一个引用计数器,每当有一个引用指向这个对象时,计数器就+1;每当一个有引用不再指向这个对象时,计数器就-1,当计数器为0为,就说明这个对象不再被使用了。
但是,这种方法并不是JVM采取的方法,因为引用计数法无法解决对象的循环引用问题。
循环引用的代码示例:
public class Demo {
public Demo demo;
public static void demoGC(){
Demo demo1 = new Demo();
Demo demo2 = new Demo();
demo1.demo = demo2;
demo2.demo = demo1;
demo1 = null;
demo2 = null;
//强制jvm进行垃圾回收
System.gc();
}
public static void main(String[] args) {
demoGC();
}
}
3.2 可达性分析
约定一些特定的对象——GC Roots作为起始点,每隔一段时间,就从GC Roots出发进行遍历,能够被访问到的对象就被称为“可达”,否则就是“不可达”。
可达性分析是JVM中采用的判断对象是否存在引用的方法。
在Java中,可作为GC Roots的对象有以下几种:
(1) 栈上的引用类型变量;
(2) 常量池中的对象;
(3) 方法区中,静态的引用类型变量。
4、垃圾回收算法
4.1 标记清除
算法分为“标记”和“清除”两个阶段:首先标记出所有可回收的对象,在标记完成后统一回收所有被标记的对象。
标记清除算法的缺点:
(1) 效率问题:标记和清除这两个过程的效率都会带来额外的时间开销;
(2) 空间问题:标记清除后会产生大量的内存碎片,内存碎片太多的话,可能会导致程序在后续运行中需要给一个较大对象分配空间时,无法找到足够连续的内存。
4.2 复制算法
复制算法将可用内存按容量分为大小相等的两块空间,每次只使用其中的一块。当内存1需要进行垃圾回收时,会将内存1上还存活的对象复制到内存2上,然后再把内存1上的对象全部清理掉。这样做就不会产生大量的内存碎片。
复制算法的缺点:
(1) 空间利用率更低,只有一半的内存被使用,另一半的内存空着。
(2) 如果内存上的存活对象比较多,可回收的对象比较少时,复制算法的效率就会大大降低。
4.3 标记整理
标记整理算法的标记过程和标记清楚算法的标记过程一样,而整理过程类似于顺序表中的删除元素操作,但搬运对象的过程也比较耗时。
4.4 分代回收
分代算法是根据对象存活的周期而把内存划分为几个部分。Java堆中分为新生代和老年代。在新生代中,每次垃圾回收都会有大量对象被回收,只有少数对象存活,因此在新生代中使用“复制算法”;而老年代中对象存活率高,一般使用标记整理法。
哪些对象会进入新生代/老年代?
新生代:一般刚创建的对象会进入新生代的伊甸区,如果经过一轮GC还存活的话会进入到新生代中的生存区;
老年代:刚创建的比较大的对象和经历了N次(一般是15次)GC后还存活的对象对象会从新生代进入到老年代。