一.类加载和创建对象的过程
1.类加载
1.编译 : 将源码文件(.java)编译成JVM可以解释的.class文件 . 语法分析>语义分析>注解处理 , 生成class文件
2.加载 :
装载 : 字节码本来存储在硬盘上 , 需要运行时 , 有类加载系统负责将类的信息加载到内存中(方法区) , 使用的是类加载器进行加载 , 充当的是一个快递员的身份. (双亲委派机制)下面会有:
连接 : 效验class信息 , 为类的静态变量分配内存 , 初始化为默认值
初始化 : 为变量赋值为正确的初始值
3.解释 : 将字节码转换成操作系统可识别的执行指令.
字节码解释器(直接解释) : jvm运行程序时 , 逐行对字节码指令进行翻译 , 效率低
即时编译器(JIT)(编译解释) : 对某段代码整体编译后执行 , 效率高 , 但是编译需要耗费一定的时间.
4.执行 : OS(操作系统)识别解释阶段生成的指令 , 调用系统的硬件执行最终的程序指令.
2.创建对象的过程(在我们使用new创建对象的时候)
1. jvm会先去加载这个对象的类的字节码文件 , 并把其中static的属性放到方法区中保存 , 此时这些属性还是其类型对应的默认值 , (static final的变量除外 , jvm会直接对其进行赋值)
2. 紧接着jvm会对这些方法区中的static变量进行赋值
3. 在方法区中static属性赋完值后 , new关键字就会在堆中去是申请一块空间用来存储对象 , 此时对象中的成员属性就在内存中存在了 , 但会被赋予其默认值
4. 在new关键字在堆中开辟空间后 , jvm就会调用对象的构造方法 , 在构造方法中对成员属性开始赋值 , 当构造方法执行完毕后 , 成员属性也随之完成了赋值 , 到这里 , 整个对象的创建过程才算完成
3.双亲委派机制
把请求交由父类处理 , 是一种任务委派模式
加载一个类时 , 委托给父类加载器进行加载 , 如果父类加载器没有找到 , 就向上级委托 , 直到引导类加载器 , 父类加载器找到就直接返回 , 如果没有找到就委托给自定义加载器 , 如果还没有找到 , 就直接抛异常ClassNotFoundException
JVM内存结构
1.运行时数据区
线程的私有区域 : 程序计数器 , 虚拟机栈 , 本地方法栈
线程的共享区域 : 堆 , 方法区
线程的私有区域的生命周期与线程相同 , 随着线程的启动而创建 , 随着线程的结束而销毁
共享区域随着虚拟机的启动而创建 , 随着虚拟机的关闭而销毁
1.程序计数器
可以看做是当前线程所执行字节码的行号指示器 , 用来指向下一个将要执行的指令代码 , 有执行引擎来读取下一条指令 , 是唯一一个没有内存溢出的区域
一个线程的执行 , 就是通过字节码解释器来改变当前计数器的值 , 来获取下一条将要执行的字节码指令 , 从而保证线程的正常执行.
2.虚拟机栈
就是一个执行java方法的内存模型 , 每个方法在执行的同时都会创建一个栈帧 , 栈帧中有:
局部变量表(存储一些方法内部的变量) , 操作栈(用于计算方法内部的值) , 动态链接(有时候要用到类中的一些变量 , 所以需要一个引用地址指向运行时常量) , 返回地址(用来返回之前调用它的方法)
栈帧就是用来记录方法执行的过程 , 方法执行的过程中 , 虚拟机会创建一个与之对应的栈帧 , 方法的执行与返回对于栈帧在虚拟机中的入栈和出栈
线程1在cpu1上面运行,线程2在cpu2上面运行,在cpu资源不足时其他线程将处于等待状态。在线程内部,每个方法的执行和返回都对应一个栈帧的入栈和出栈,每个运行中的线程当前只有一个栈帧处于活动状态
3.本地方法栈
用来管理本地方法的调用
4.堆
在JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是线程共享的内存区域,也是垃圾收集器进行垃圾回收最主要的内存区域。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据.
堆解决的是数据存储的问题,即数据怎么放,放在哪儿
一个 JVM 实例只存在一个堆内存,堆也是 Java 内存管理的核心区域
java8 及之后堆内存分为:新生区(新生代)+老年区(老年代)
新生区分为 Eden(伊甸园)区和 Survivor(幸存者)区
为什么要进行分区(分代)?
1.将对象根据存活的概率进行分类 , 对存货时间长的对象 , 放到老年区 , 从而减少扫描垃圾的时间以及GC频率
2.针对分类进行不同的垃圾回收算法 , 可以扬长避短
分代的收集思想Minor GC、Major GC、Full GC
JVM 在进行 GC 时,并非每次都新生区和老年区一起回收的,大部分时候回收的都是指新生区.针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类型:一种是部分收集,一种是整堆收集.
部分收集:不是完整收集整个 java 堆的垃圾收集.其中又分为:
新生区收集(Minor GC/Yong GC):只是新生区(Eden,S0,S1)的垃圾收集.
老年区收集(Major GC / Old GC):只是老年区的垃圾收集.
整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集.
整堆收集出现的情况:
1.调用System.gc()时会进行full GC
2.当堆空间中对象的数量已经达到了预设的阈值
3.当堆空间中的对象已经被分配到了连续的内存区域,无法再分配更多的内存时。
在进行Full GC时 , java虚拟机会停止掉所有的正在运行的线程 , 而且,Full GC的执行时间通常比局部回收(Partial GC)要长得多,因为它需要扫描整个堆空间以确定哪些对象可以被回收。因此,应该尽可能地避免Full GC的发生,以减少应用程序的停顿时间。
5.方法区
方法区也是被共享的 , 用于存储已被虚拟机加载的类的信息 , 常量 , 静态变量 , 即时编译器编译后的代码 , 还包括一个特殊的区域"运行时常量池"
方法区看做是一块独立于 java 堆的内存空间
常量池的好处?
常量池避免了频繁的创建和销毁对象从而影响了系统的性能 , 其也实现了对象的共享
Integer常量池:
Integer 的 valueOf 方法很简单,它判断变量是否在 IntegerCache 的最小值(-128)和最大值(127)之间,如果在,则返回常量池中的内容,否则 new 一个 Integer 对象。
字符串常量池 :
第一种使用 new 创建的对象,存放在堆中。每次调用都会创建一个新的对象。
第二种:先在栈上创建一个 String 类的对象引用变量 str,然后通过符号引用去字符串常量池中找有没有 “abcd”,如果没有,则将“abcd”存放到字符串常量池中,并将栈上的 str 变量引用指向常量池中的“abcd”。如果常量池中已经有“abcd”了,则不会再常量池中创建“abcd”,而是直接将 str 引用指向常量池中的“abcd”。
JDK7 及以后的版本中将字符串常量池放到了堆空间中。因为方法区的回收效率很低,我们开发中会有大量的字符串被创建, 回收效率低。放到堆里,能及时回收内存
为什么方法区的回收效率低呢?
方法区的回收在 Full GC 的时候才会执行永久代的垃圾回收,而 Full GC 是老年代的空间不足、方法区不足时才会触发。这就导致字符串常量池回收效率不高
2.垃圾标记算法
就是用来判断该对象是否是垃圾 , 是否有引用指向该对象
(1)引用计数算法
给堆内存中的每一个对象记录一个引用个数 , 引用个数为0的认为是垃圾 , 但是无法解决循环引用的问题.
(2)可达性分析算法
在内存中 , 从根对象(GC root , 一组活跃的对象) 向下一直找引用 , 找到的对象就不是垃圾, 没找到的就是垃圾.
哪些可以作为GC root呢?
在虚拟机栈中 , 当前栈顶的栈帧是活跃的 , 当前栈帧里面的局部变量锁指向堆内存中对象的就可以作为GC root
对象的finalization机制
只要记住对象在被回收之前会调用一次finalize方法, 它就是用来执行一些清理操作 , 比如释放资源 , 但是它的执行时间是不固定的, 也不能保证一定会被执行。这是因为垃圾收集器的行为是不可预测的,可能会出现一些意外的情况,例如在finalize()方法执行过程中发生了异常等。因此,程序员不能依赖finalize()方法来进行资源的释放和清理操作,而应该使用try-with-resources或finally块等机制来确保及时释放资源。
垃圾回收算法
1.标记清除算法
最简单残暴的算法, 直接将垃圾干掉 , 但存在内存碎片的问题
内存碎片:可能有10M的空余内存,但程序申请9M内存空间却申请不下来(10M的内存空间是垃圾清除后的,不连续的)。
2.标记复制算法
把存活的对象复制这样没有了内存碎片的问到另一块空间 , 复制完成后 , 把原有的整块空间干掉, 题 , 但是内存的利用率很低 , 得有一块新的区域给复制过去
3.标记压缩算法
把存活的对象一道一边, 垃圾移到另一边 , 再将垃圾一起清理掉
stop the world(应用停止访问)
回收垃圾的时候 , 程序有短暂的时间是不能正常运行的 , 又由于垃圾回收是会导致stop the world,所以分代的一个原因是为了使stop the world持续的时间尽可能短。
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁,适合标记复制算法。
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记- 清除或者是标记-清除与标记-整理的混合实现。
总结:不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法, 以提高垃圾回收的效率,同时使stop the world持续的时间尽可能短。
垃圾回收器
回答思路:单线程-->多线程-->CMS-->G1
单线程缺点(新生代Serial,老年代Serial Old):只有一个线程进行垃圾回收,效率低
多线程(新生代Paraller,老年代Paraller Old):多线程垃圾回收器内部提供多个线程进行垃圾回收,在多 cpu 情况下大大提升垃圾回收效率。
两者的共同缺点:会暂停其他用户线程,也就是Stop the World
所以垃圾回收器的优化思路就是停顿的时间能不能尽可能的短,用户线程和 GC 线程能不能去并发执行,让垃圾收集过程中用户也不会感到明显的卡顿。
1.CMS
老年代推出了一款垃圾回收器CMS,采用的是标记清除算法:
1.初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
2.并发标记:垃圾回收线程,与用户线程并发执行。此标记阶段标记出间接关联的对象。
3.重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
4.并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。 这个过程非常耗时。
所以整个思路就是:将耗时时间长的阶段与用户线程一起并发工作,因此,总体上说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
CMS中的问题:
1、CPU敏感(在多核的情况下使用最好,如果没有多少核使用这个没有多少意义,还要不停的上下文切换)
2、浮动垃圾和内存碎片:有可能重写标记阶段完后,又产生了一些垃圾,但是这些垃圾只能等下次垃圾回收在清除,同时标记清除算法的缺点也非常明显,会产生内存碎片,申请大对象(例如数组要求连续空间)可能申请不下来。
2.G1
G1垃圾回收器是在 Java7 之后引入的一个新的垃圾回收器,对老年代和新生代都可以进行回收。
以前学习堆的分区时候,了解到堆的分区主要为:伊甸园、幸存者区以及老年区的知识。
学习G1之前要忘记之前堆的分区的相关知识,因为 G1 把堆内存分割为很多不相关的区域(Region)
使用不同的 Region 来表示 Eden、幸存者 0 区,幸存者 1 区,老年代等。G1 GC 有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各 个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时 间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
-
并行与并发:G1能充分利用多CPU,多核环境下的硬件优势。GC线程并行执行,与用户线程并发执行
-
分代收集:能够采用不同的方式去处理新创建的对象和已经存活了一段时间的对象,不需要与其他收集器进行合作。
-
空间整合:G1从整体上来看基于“标记-整理”算法实现的收集器,从局部上看是基于复制算法实现的(也就是一个Region是标记清除,两个region之间是复制),因此G1运行期间不会产生空间碎片。
优点:能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配,不会产生内存碎片,有利于长时间运行。同时可以根据用户所期望的 GC 停顿时间来指定回收计划,尽可能的满足用户期望。
排查JVM的问题
对于还在正常运行的系统来说
1.可以使用jmap来查看JVM中各个区域的使用情况
2.可以使用jstack来查看线程运行的情况 , 比如哪些线程阻塞 , 是否出现了死锁
3.可以通过jstat命令来查看垃圾回收的情况 , 特别是full GC , 如果发现full GC比较频繁 , 那么就得进行调优
对于已经发生了OOM的系统
1.⼀般生产系统中都会设置当系统发⽣了OOM时,生成当时的dump文件
2.我们可以利⽤jsisualvm等⼯具来分析dump⽂件
3.根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
4.然后再进行详细的分析和调试
一个独享从进入到JVM , 再到GC清除的经历
1.首先把字节码文件内容加载到方法区
2.根据类的信息在堆中创建对象
3.对象首先会在堆中的Eden区 , 经过一次Minor GC后 , 对象如果存活 , 就会进入到Suvivor区 , 在后续的每次的MinorGC中 , 如果对象一直存活 , 就会在Suvivor区来回移动 , 每移动一次年龄就会增长1
4.当年龄超过15后 , 如果对象仍然存活 , 就会进入到老年代
5.如果经过Full GC , 被标记为垃圾对象 , 那么就会被GC清除掉.