原理篇划重点如下:
Java程序是如何运行起来的
Java类加载过程
双亲委托模型
类加载器的类型
JVM运行时数据区
堆里有哪些区域,Java8做了哪些改变
对象是如何分配和流转的
对象什么时候进入老年代
对象的引用类型
对象何时被回收
垃圾回收算法有哪些
新生代的回收机制和流程
什么时候出发老年代的回收
一、类加载
1.Java程序是如何运行起来的
Java程序是如何运行起来的呢?简单来讲,就是写好了java类,通过编译后变成class文件,再打包成jar或者war,然后部署到了线上服务器。此时在通过一个java –jar
的命令去启动一个jvm进程。
[root@LOCAL~]#java –jar hello.jar
JVM拿到的都是class文件,首先会有一个类加载器,会把那些编译好的class文件加载到jvm中,然后jvm就能拿到这个类,基于自己的字节码引擎去执行程序。
2.Java类加载过程
什么时候加载一个类:一般来说,发生在实例化一个对象的时刻,会触发类的加载到一个初始化的全过程。
类加载1、加载:类什么时候加载,用到这个类的时候;加载什么?Class文件,通过类的全限定名称去找到这个类。
2、验证:class文件是否符合规范,确保Class文件的字节流中包含信息符合当前虚拟机要求。
3、准备:给class类分配空间,类变量(static修饰)赋默认值,譬如int型赋值为0(不包含final,final在编译阶段分配)
4、解析:符号引用(字面量)替换为直接引用(内存地址,偏移量)
5、初始化:执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。如果发现父类则还会优先去初始化父类。
3.双亲委托模型
JVM去加载一个类,首先会委派给自己的父类加载器去加载,没有找到则会继续向上委托,直到顶层的引导类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
好处有二:
- 具备了一种带有优先级的层次关系,避免类的重复加载。
- 防止核心API库被随意篡改。
说明一下:比如Object超类,无论在哪个类加载器中按照这个规则最终都会委托给启动类加载器来负责加载,不会被其他加载器重复加载;如果用户也写一个java.lang.System
类,放到classpath下,根据双亲委托模型而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载
4.类加载器
虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)。
- 启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,负责将 <JAVA_HOME>/lib路径下的核心类库加载到内存中。
- 扩展类加载器由Java语言实现的,它负责加载<JAVA_HOME>/lib/ext的类库。
- 系统(System)类加载器,负责加载系统类路径-classpath路径下的类库,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器。
5.如何实现一个自己的类加载器
继承ClassLoader,覆盖核心方法findClass,定义私有方法loadClass将其转化成二进制数据流,从而加载到Class类。
二、JVM内存模型
1.运行时数据区
- 方法区,存放类信息(就是class文件加载进来的类),1.8以前是方法区,1.8以后叫元空间-MetaSpace。
- 程序计数器,jvm会通过字节码引擎去执行我们的字节码指令,但java是多线程语言,当cpu轮转时,就需要一块这样线程独占的空间去记录字节码执行的现场和位置。
- 虚拟机栈,写代码的时候会写函数或者方法,运行这个方法会生产一些局部变量,虚拟机栈就是存放局部变量的地方;同时,栈是一个先进后出的数据结构,每个线程都有一个自己的栈,执行一个方法先入栈,并在这个栈帧上保存该方法的局部信息,如果执行的时候又进入到一个方法,则又继续入栈,方法执行完出来出栈。
- 本地方法栈,完全类似的,只不过上面是给java程序用,这里是给native方法用的。
- 堆空间,存放对象和数组数据的地方,也是JVM调优和分析的重点。
线程隔离和共享的空间要清楚!
2.堆里有哪些区域,Java8做了哪些改变。
堆空间所有线程共享,存放数组与对象;堆划分为新生代和老年,新生代分为Eden和Survivor,Survivor区又可以FromSurvivor和ToSurvivor。Java8以后方法区的规范中,移除了永久代,用元空间替代。
Java7及以前版本的Hotspot中方法区位于永久代中。同时,永久代和堆是相互隔离的,但它们使用的物理内存是连续的。永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。
在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。所以java8元空间,基本不存在元空间的OOM内存溢出;但是java7的永久代位于JVM内存中,会存在永久代的OOM错误;
好处:内存隔离,当Java Heap空间不足时会触发GC,但Native memory空间不够却不会触发GC。
三、对象的生命周期
1.对象是如何分配和流转的
- 对象优先分配在新生代的Eden区。
- 新生代如果对象满了,会触发minor GC回收掉没人引用的垃圾对象,然后在分配。
- 如果对象躲过了15次垃圾回收,就会放入老年代里。
- 如果触发动态年龄判断,大于阈值的N岁对象也会进入老年代。
- 如果老年代也满了,就会触发FullGC,把老年代里没人引用的垃圾对象清理掉,然后再分配。
- 大对象数组直接进入老年代
2.对象什么时候进入老年代
- 躲过15次MinorGC后进入老年代,可以通过设置
-XX:MaxTenuringThreshold=15
- 动态对象年龄判断,当前S区的对象的总大小>当前S区的50%,可直接进入老年代。
- 大对象直接进入老年代,大对象阈值可以设置:
-XX:PretenureSizeThreshold
。 - MinorGC后对象太多无法放入S区。
3.对象的引用类型
Java语言为程序员提供了4个不同级别对象引用类型,按照级别从高到低分别为:强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)和虚引用(PlantomReference)。
4.如何判断对象是否可以被回收
a、引用计数法
在对象上添加一个引用计数器,每当有一个对象引用它时,计数器加1,当使用完该对象时,计数器减1,计数器值为0的对象表示不可能再被使用。引用计数法简单高效,但不能解决对象之间相互引用的问题。
b、可达性分析法
通过一系列称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,搜索路径称为 “引用链”,以下对象可作为GC Roots:
- 方法的局部变量
- 类变量,
static
修饰 - 常量引用,
final static
修饰
当一个对象到 GC Roots 没有任何引用链时,意味着该对象可以被回收。
5.对象何时被回收
回收时机:当对象永久地失去引用后,系统会在合适的时候回收它所占的内存。但是真正的触发回收是自动的,不可预期的。
三、垃圾回收
1.垃圾回收算法有哪些
JVM 提供了不同的回收算法来实现这一套回收机制,通常垃圾收集器的回收算法可以分为以下几种:
- 分代回收是主流垃圾回收器的一致设计:堆划不同区,根据对象年龄来存储。
- 新生代主要采用复制算法;
- 老年代主要采用标记-清除/整理算法;
- 老年代采用的标记整理法比新生代的复制算法速度慢10倍以上!;
2.新生代的回收机制
1.刚开始对象都分配在Eden区,运行一段时间Eden逐渐饱和。
2.Eden区满了触发MinorGC,复制存活对象到S1区。Eden区再次清空
3.Eden区再次满了,触发MinorGC,复制Eden+S1存活对象到S2。
4.Eden区和S1区清空,再次开始分配对象。
新生代划分出Eden、S1和S2区,GC过程中,在新生代的回收过程中始终保持一块S区空着,循环往复!
3.老年代的回收时机
- MinorGC前,JVM自我检查发现Minor后进入老年代的对象太大,老年代放不下,此时提前触发FullGC;
- MinorGC后,剩余对象太大,老年代装不下。
但是如果FullGC后仍然无法存储对象,JVM抛出OOM内存溢出异常!
4.垃圾回收的整个流程回顾:
- 代码里创建的对象,优先会在新生代里分配,随着方法执行的结束,对象无人引用就变成了垃圾对象。随着时间的拉长,垃圾对象越来越多,一旦新生代不够用了,就会触发一次MinorGC,把没有引用的垃圾对象回收掉,腾出一大部分空间出来。
- 如果是那些长周期存活的对象(比如注解了
@Service和@Controller
等的Spring Bean对象),在新生代里会持续躲过多次垃圾回收,躲过一次,年长一岁,当它长大到15岁的时候就会转移到老年代里去! - 当老年代里的对象越来越多,新进入的老年代的对象无法装下的时候就会触发OldGC或是FullGC,如果GC解决不了只能触发OOM到应用层。
网友评论