From: JVM 系列阅读笔记:https://liudongdong1.github.io/tags/jvm/
.1. 简述垃圾回收机制
在 JVM 中,有一个垃圾回收线程,它是低优先级的 ,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时 ,才会触发执行,扫描那些没有被任何引用的对象 ,并将它们添加到要回收的集合 中,进行回收。
若 GC 一次之后仍不能满足内存分配的要求,JVM 会再进行两次 GC 作进一步的尝试,若仍无法满足要求,则 JVM 将报 “out of memory” 的错误,Java 应用将停止。
.2. 内存模型
Java 堆 是被所有线程共享的一块内存区域 ,在虚拟机启动时创建 。此内存区域的唯一目的就是存放对象实例 ,几乎所有的对象实例都在这里分配内存。- 方法区(Method Area)是
各个线程共享的内存区域 ,它用于存储已被虚拟机加载 的类信息、常量、静态变量、即时编译器编译后的代码 等数据。 - 程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是
当前线程所执行的字节码的行号指示器 。 - Java 虚拟机栈(Java Virtual Machine Stacks)也是
线程私有的 ,它的生命周期与线程相同 。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等 信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 - 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而
本地方法栈则是为虚拟机使用到的 Native 方法 服务。
.3. 如何判断对象死完:
堆中?乎放着所有的对象实例,对堆垃圾回收前的第?步就是要判断哪些对象已经死亡(即不能再被任何途径使? 的对象)
引?计数法 :给对象中添加?个引?计数器 每当有?个地?引?它,计数器就加 1 当引?失效,计数器就减 1 任何时候计数器为 0 的对象就是不可能再被使?的。 很难解决对象的相互循环引用 问题- 可达性分析算法:是通过?系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所?过 的路径称为
引?链 ,当?个对象到 GC Roots 没有任何引?链相连的话,则证明此对象是不可?的。
虚拟机栈(栈中的本地变量列表)/ 方法区静态属性 / 方法区常量引用 / 本地方法栈中 JNI 所引用的对象 都是可以作为 gc roots 的
.4. GC 回收机制
1. 标记清除法:
标记阶段 对所有存活的对象进行标记,标记完成后,再扫描整个空间未标记对象,直接回收不存活的对象 .
缺点是会造成内存碎片,对大内存的分配无足够空间 时而提前猝发一次垃圾回收动作;
2. 复制算法:
将可用内存将容量划分成大小相等的 2 块 , 每次清理时将其中 A 内存还存活的对象复制到 B 内存里面,然后再把 A 中清理掉;优点高效且并不产生碎片,缺点牺牲了一半的内存为代价。适用存活对象少,回收对象多
3. 标记整理算法
完成标记后它不是直接清理可回收对象,而是将存活对象都向一端移动 , 最后清理掉端边界意外的内存;
4. 分代收集算法
整合了复制算法 和标记整理算法 , 根据新生代和老年代 的不同特性采取上面的不同算法
- 新生代 生命周期短,每次回收时都有大量垃圾对象需要回收
复制算法 , 内存划分按照 8:1:1 分一个大的 Eden 区和两个小的 survivor 的空间,新生代中存活的对象,需要转移的 Survivor 的对象不多 , 算是缓解了复制算法的缺点 - 老年代 每次只有少量的对象需要回收
标记整理算法
当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代 和 永久代,如图所示:
新生代 中存在一个 Eden 区和两个 Survivor 区。新对象会首先分配在 Eden 中(如果新对象过大,会直接分配在老年代中)。在 GC 中,Eden 中的对象会被移动到 Survivor 中,直至对象满足一定的年纪(定义为熬过 GC 的次数),会被移动到老年代。
可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。参数 -XX:NewRatio 设置老年代与新生代的比例。例如 -XX:NewRatio=8 指定 老年代 / 新生代 为 8/1 . 老年代 占堆大小的 7/8 ,新生代 占堆大小的 1/8 (默认即是 1/8 )。
老年代: 对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的 GC 要比新生代要少得多。对象从老年代中消失的过程,可以称之为 major GC (或者 full GC )。
像一些类的层级信息,方法数据 和方法信息(如字节码,栈 和 变量大小),运行时常量池(JDK7 之后移出永久代),已确定的符号引用和虚方法表等等。它们几乎都是静态的并且很少被卸载和回收,在 JDK8 之前的 HotSpot 虚拟机中,类的这些 **“永久的”** 数据存放在一个叫做永久代的区域。
永久代一段连续的内存空间,我们在 JVM 启动之前可以通过设置 -XX:MaxPermSize 的值来控制永久代的大小。但是 JDK8 之后取消了永久代,这些元数据被移到了一个与堆不相连的称为元空间 (Metaspace ) 的本地内存区域。
.5.垃圾回收三色标记法:
?开始所有对象都染成??初始化完成后,会启动标记程序。在标记的过程中,是可以暂停标记程序执? Mutation,算法需要维护 3 个集合,??集合、??集合、灰?集合。3 个集合是互斥的,对象只能在?个集合 中。执?之初,所有对象都放???集合
- 第一次执行:算法将 Root 集合能直接引用的对象加入到灰色集合中
- 往后执行:不断从灰色集合中取出元素进行标记
- 如果对象在白色集合中,那么将白色对象放入到黑色集合中
- 然后遍历节点所有引用对象,并递归所有引用对象
- 当一个对象的所有引用都在灰色集合中,就把这个点放在黑色集合中
- 当算法执行完成,所有不需要 GC 的元素都会涂黑,白色集合中就是需要进行回收的对象
.6. CMS垃圾回收器介绍:
当垃圾回收开始清理资源时,其余的所有线程都会被停止。如果清理的时间过长,在我们的应用程序中就能感觉到明显的卡顿。
CMS(Concurrent Mark Sweep)收集器是?种以获取最短回收停顿时间为?标 的收集器。它?常符合在注重? 户体验的应?上使?。 是 HotSpot 虚拟机第?款真正意义上的并发收集器,它第?次实现了让 垃圾收集线程与?户线程(基本上)同时?作。
- 初始标记:暂停所有其他线程,并
记录下直接与root相连的对象 ,速度恒快 - 并发标记:同时开启 GC 和用户线程,用一个闭包结构去记录可达对象,因为用户线程可能会不停的更新引用域,无法保证可达性分析的实时性。
- 重新标记:为了修正并发期间因为用户程序继续运行而导致标记产生变动的那一部分标记记录
- 并发清除:开启用户线程同时 GC 线程开始对未标记的区域做清扫。
.7. JVM 俩个内存
1. 栈内存
在函数中 定义的一些基本类型的变量 和对象的引用变量 都在函数的栈内存 中分配。当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后 ,Java 会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
2. 堆内存
堆内存用来存放由 new 创建的对象和数组 。在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管理。
.8. 类加载器
启动类加载器 :Bootstrap ClassLoader,负责加载存放在 JDK\jre\lib (JDK 代表 JDK 的安装目录,下同) 下,或被 - Xbootclasspath 参数指定的路径中的,并且能被虚拟机识别的类库 扩展类加载器 :Extension ClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 DK\jre\lib`ext 目录` 中,或者由 java.ext.dirs 系统变量指定的路径中的所有类库(如 javax.* 开头的类),开发者可以直接使用扩展类加载器。应用程序类加载器 :Application ClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径 (ClassPath)所指定的类,开发者可以直接使用该类加载器
- 命令行启动应用时候由 JVM 初始化加载
- 通过 Class.forName () 方法动态加载, 将类的.class 文件加载到 jvm 中之外,还会
对类进行解释 ,执行类中的 static 块; - 通过 ClassLoader.loadClass () 方法动态加载, 是
将.class文件加载到jvm中 ,不会执行static中的内容,只有在newInstance才会去执行static块 。
.2. 加载机制
- 全盘负责: 当一个类加载器负责加载某个 Class 时,
该Class所依赖的和引用的其他Class也将由该类加载器负责载入 ,除非显示使用另外一个类加载器来载入。 - 缓存机制: 缓存机制将会保证
所有加载过的Class都会被缓存 ,当程序中需要使用某个 Class 时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区 。这就是为什么修改了 Class 后,必须重启 JVM,程序的修改才会生效。 - 双亲委派机制: 如果一个类加载器收到了类加载的请求,它
首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成 ,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
.9. 对象创建
HotSpot VM 遇到一条 new 类型()指令时 ,①首先在常量池中是否 有这个类的符号引用 (符号引用代表类是否已被加载、解析和初始化过 );②如果没有检测到此类的符号引用就必须先执行相应的类加载过程 ;3 类加载通过后从Java堆中分配确定大小的内存 (分配内存的过程是一个并发进行的过程);④内存分配完成后,将分配到的内存空间初始化为零值 (对象头不初始化–对象头在下面介绍);5 虚拟机对对象进行必要的设置,并将这些设置信息存放在对象头中 ;⑥此时就得到了从JVM视角看到的对象 ;⑦从java程序的角度来看对象才刚刚创建---<init>方法还没有执行,所有的字段都还为0,执行init方法后真正的可用对象产生 。
.10. 对象内存分布
目前主流访问方式有使用句柄和直接指针 两种。句柄 — 从 Java 堆中划分一块内存作为句柄池,refernece 中存储对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息,修改代价小;直接指针 —reference 中直接存储堆中对象地址 ,如图二所示,其访问速度更快,节省指针定位的时间开销,HotSpot VM 就是采用直接指针方式。
.11. OutofMemory错误
OutOfMemory 异常原因:根据报错信息确定出是那个区域发生OutOfMemory异常 ,然后分析是内存泄露还是内存溢出 。
内存泄漏 :与 GC Roots 相关联并导致 GC 无法自动回收。内存溢出 :众多对象确实还必须活着,导致大量内存被占用而无法 GC,当超出限制内存最大值时就抛出 OutOfMemory 异常。
.12. Full GC
**Full GC:** 无官方定义,通常意义上而言指的是一次特殊 GC 的行为描述,这次 GC 会回收整个堆的内存,包含老年代,新生代,metaspace 等。
- System.gc () 方法的调用
- 旧生代空间不足。旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行 Full GC 后空间仍然不足,则抛出错误:java.lang.OutOfMemoryError: Java heap space 。
- Permanet Generation 空间满了
.12. 拷贝
- 浅拷贝:如果属性是基本类型,拷贝的就是基本类型的值;如果
属性是内存地址 (引用类型),拷贝的就是内存地址 - 深拷贝:会拷贝所有的属性,并
拷贝属性指向的动态分配的内存 . 可以使用 Arrays.copyof ()
.13. 反射
- 通过外部类的
全路径名创建对象 ,并使用这些类,实现一些扩展的功能 。 - 反射让开发人员可以
枚举出类的全部成员 ,包括构造函数、属性、方法。以帮助开发者写出正确的代码。 - 测试时可以利用反射 API 访问类的私有成员,以保证测试代码覆盖率。
- 是 JVM 在运行时动态加载类,调用方法或者访问属性,不需要知道运行对象是谁
- 相关的类:
- class: 每一个类都有一个 class 对象(类实例),包含类的相关信息
- Field:使用 get/set 修改或者读取 Field 相关对象
- method:使用
invoke方法调用与method相关联的对象 - Constructor: 使用 newInstance () 构建对象
- Object 类:Object 是所有 Java 类的父类。所有对象都默认实现了 Object 类的方法。
.14. 如何避免内存泄漏
- 未对作废数据内存单元置为 null
- 尽早释放无用对象的引用,
- 使用临时变量时,让引用变量在推出活动域后自动设置为 null,暗示垃圾收集器收集;
- 程序避免用 String 拼接,用 StringBuffer,因为每个 String 会占用内存一块区域;
- 尽量少用静态变量(全局不会回收);
- 不要集中创建对象尤其大对象,可以使用流操作;
- 尽量使用对象池,不再循环中创建对象,优化配置;
- 创建对象到单例 getInstance 中,对象无法回收被单例引用;
- 服务器 session 时间设置过长也会引起内存泄漏。
.15. 检查异常&运行时异常
-
检查异常是在程序中最经常碰到异常,所有继承自 Exception 并且不是运行时异常的异常都是检查异常 ,比如咱们最常见的 IO 异常和 SQL 异常。这种异常都发生在编译的阶段,Java 编译器强制程序去捕获此类型的异常,即它会把可能会出现这些异常的代码放到 try 块中,把对异常的处理代码放到 catch 块中。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。 -
运行时异常不同于检查异常,编译器没有强制对其进行捕获并处理,如果不对异常进行处理,那么当出现这种异常的时候,会由 JVM 来处理,比如 NullPointerException 异常,它就是运行时异常。只要程序设计得没有问题通常就不会发生运行时异常。
|