对于JVM、GC、类加载,很多人摸不清楚头绪,不知道他们之间的关系。误以为GC和类加载还有JVM区分统称垃圾回收,实则他们包含的东西很多,很细,完整的了解正个JVM的加载过程,就需要全面理解这些东西。
我认为的理解相互关联步骤:
类的加载过程
通过类的加载延伸到gc的编译原理
根据gc得到解决方案并拓展锁知识
根据这些完全熟悉gc的生态链
下面开始开始简述相关知识。
1.内存区域类型
*.寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制;
*. 栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(对象可能在常量池里)(字符串常量对象存放在常量池中。);
*. 堆:存放所有new出来的对象;
*. 静态域:存放静态成员(static定义的);
*. 常量池:存放字符串常量和基本类型常量(public static final)。有时,在嵌入式系统中,常量本身会和其他部分分割离开(由于版权等其他原因),所以在这种情况下,可以选择将其放在ROM中 ;
*. 非RAM存储:硬盘等永久存储空间
2.堆栈空间分配
栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表
3.堆栈缓存方式
栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。
堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
4、数据结构中的堆和栈
堆和栈在数据结构中是两种不同的数据结构。 两者都是数据项按序排列的数据结构。
栈:像是装数据的桶或者箱子
栈是大家比较熟悉的一种数据结构,它是一种具有后进先出的数据结构,也就是说后存放的先取,先存放的后取,这就类似于我们要在取放在箱子底部的东西(放进去比较早的物体),我们首先要移开压在它上面的物体(放入比较晚的物体)。
堆:像是一颗倒立的大树
堆是一种经过排序的树形数据结构,每个节点都有一个值。通常我们所说的堆的数据结构是指二叉树。堆的特点是根节点的值最小(或最大),且根节点的两个树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意的,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。
一.类加载
1.类加载的过程
(1)将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。
(2)分为三个阶段:
1)验证阶段:验证文件的安全性,防止对jvm本身造成危害;
2)准备阶段:正式为类变量(static变量)分配内存并设置类变量初始值
3)解析阶段:虚拟机常量池的符号引用替换为字节引用过程
(3)执行类构造器初始化类对象;
总结:初始化-加载-销毁
2.由类加载过程延伸的问题
(1)类加载器?
虚拟机提供了3种类加载器:
启动类加载器:主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分;
扩展类加载器:负责加载JAVAHOME路径下的jdk中的一些类库;
系统类加载器:负责加载CLASSPATH路径下的应用程序。
(2)jvm以什么样的方式加载一个类
采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式。 那么,一起来理解下双亲委派模式:
双亲委派:如图,根据自定义类加载器-再到系统类-然后拓展类-最后引导类加载器。如果你在最下层系统类加载器中,编写了一个StringUtil,但是在引导类加载器也有一个相同类,默认只加载系统存在的StringUtil,主要原因是防止修改核心类,保证安全性, 双亲委派模式的好处是避免了一个类会被重复加载。
3.jvm什么时候决定加载一个类?
1)创建对象就会触发加载;
2)引用了该类的静态属性、方法都会触发类的加载;
3)初始化子类也会导致父类被加载;
4.java中创建对象的几种方式?
1) new对象
2) 用反射创建对象(代理)
3) 克隆对象
4) 序列化
二.JVM
JVM定义:指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。
线程私有区域:程序计数器、Java虚拟机栈、本地方法栈(与线程生命周期一致)
线程共享区域:Java堆、方法区、运行时常量池
jvm主要知道它各个模块与作用即可。(小型独立运行的虚拟机)
三.GC
1.GC本质上就是对JVM内存管理的一个名词(垃圾回收)。
栈:和唯一的一个线程强绑定,线程出现就需要这块内存,线程结束,这块内存就可以回收了,这块内存的分配/回收时机是明确的,就不需要 GC 来操心。
常量池+方法区:首先,这两块内存占比较小,其次,里面的数据很少“失去作用”。回收这块内存性价比不高,也不是重点考虑的。
堆:GC 的重点就是如何管理堆上的内存。堆上的内存是以对象为单位进行管理的,分配与回收的都是一个完整的对象。因此,回收转换成了回收死对象的问题。
2.如何判断对象已死
死对象:没有引用指向的对象。
1)引用计数法ReferenceCount:无法解决对象的循环引用问题
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。 引用计数法实现简单,判定效率也比较高。
2)可达性分析算法
核心思想: 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。
GC Roots的对象包含下面几种:
虚拟机栈(栈祯中的本地变量表)中引用的对象
方法区中的静态属性不回收,所以有引用指向的对象必须活着,常量引用的对象
常量池中引用指向的对象
如果在GC过程中,可达性分析后对象的引用关系发生了变化,有可能造成对象回收错误,所以通常在GC中,会通知应用线程的执行(STW:Stop The World)
3.引用分类
1)强引用:被引用的对象必需活着
2)弱引用:gc就回收
3)虚引用:和引用无关,但是回收会通知
4)软引用:被引用的对象尽可能活着,但是内存紧张可以回收
4.垃圾回收算法
1)标记清除:标出所有需要回收的对象回收。
2)复制算法(新生代回收算法):把空间分2区域,把使用的对象复制到新区域,便于区分回收。缺点:需要翻倍内存。
3)标记整理(老年代回收算法):第一阶段标记被引用对象,第二阶段遍历堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。(清除和复制的结合)
4)分代收集算法:根据对象存活周期分块,分为新生代和老年代。新生代采用复制,老年代采用标记整理
对象在Eden区分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
看看 Minor GC和Full GC 有什么不同呢?
Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
Major GC/Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。
Eden与Survivor区默认8:1:1 大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活 的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回 收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所 以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可