深入理解jvm虚拟机总结

我跟你讲一下我对于java的理解吧
jvm内存模型
java最大的特点就是平台无关性一次编译,到处运行
编译过程
Java源码首先被编译成字节码,再由不同平台的JVM进行解析,JAVA语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。
其中导致这个特性最主要的原因就是java中的jvm虚拟机(引出jvm虚拟机教育他!)
JVM是内存中的虚拟机,可以理解为,JVM的存储就是内存,我们所有写的常量,变量,方法都在内存中。
Jvm主要是由类加载器(ClassLoader),运行数据区域(Runtime Data Area),执行引擎(Execution Engine)还有本机接口(Native Interface)组成
其中的类加载器嘛,他主要的作用就是根据特定的格式,把.class文件加载进jvm内存中,他的种类有四种
引导类加载器(bootstrapclass loader):它用来加载 Java 的核心库,他的底层是用是用c++编写的。(java.
扩展类加载器(extensionsclass loader)(依科斯坛熏):它用来加载 Java 的扩展库。(javax.

系统类加载器(systemclass loader或 App class loader)他用来加载我们自己写的代码的
还有一种的自定义的类加载器 我们可以通过继承ClassLoader 类的方式实现自己的类加载器,以满足一些特殊的需求。
从1.2版本开始,Java引入了双亲委托机制,从而更好的保证Java平台的安全
双亲委派机制这个我特意研究过 他先是调用自定义类加载器,appClassloader, extensionsclass loader,bootstrapclass loader 检查文件是否被加载,再尝试用 bootstrapclass loader,extensionsclass loader,appClassloader, 自定义类加载器 加载文件进jvm内存中,他这么做的主要目的是为了 避免多份同样字节码的加载(使用委托机制逐层去父类查找是否加载)

内存模型

接下来我跟你讲一下runtime data area(jvm的内存模型)
Jvm的内存模型主要分为
程序计数器,虚拟机栈 ,本地方法栈,堆跟方法区
程序计数器的话 他是一块小的内存空间,线程私有的
可以理解为一个记录着当前线程所执行的字节码的行号指示器,他有着命令指向的作用他的执行流程我打个比方跟你说吧

比如说 有这么一串代码 int a =0;a=1+2;

程序计数器第一步会把0存入操作栈 ,第二步 把a存入局部变量表 ,
第三步会把 0赋值给a ,第四步再把 1放入 操作栈 ,5.会把 2也放入操作栈 ,6把1+2的合 算出来 放在操作栈 ,7把3赋值给a 就是这么一个流程,就像电影里的一帧帧一样 ,指向哪一帧就执行哪一个,这就是他的执行流程

内存模型中还有虚拟机栈本地方法栈他们两是 差不多的 只不过虚拟机栈存的是java中的多个栈帧,本地机栈存的是Native方法的栈帧
栈帧是用来存储我刚刚说的局部变量表操作栈动态链接返回地址的地方
方法的参数和方法中定义的局部变量就存放在局部变量表中;
方法内语句的操作数存放在操作数栈。
虚拟机栈有个挺好玩地方就是他是执行先进后出原则,而且方法调用结束后,栈帧的释放时自动的不受GC管理。
栈

方法区

方法区是一个被线程共享的内存区域
方法区里面存储了类信息静态变量即时编译器编译后的代码
Jdk1.7的时候已经移除了方法区中的字符串常量池。
Jdk1.7 以前方法区是HotSpot使用“永久代(permanent generation)”作为实现的
Jdk1.8以后移除了永久代并使用metaspace(元空间)作为替代实现
我跟你讲下元空间跟永久代的区别
元空间使用本地内存,永久代使用的是JVM内存,jdk1.7之前字符串常量池存在永久代中,容易出现性能问题和内存溢出。最重要的是
永久代是在堆内存中保存的,但是永久代不会被回收,当永久代空间不足的时候就会触发fullGc

内存模型中还有一个特别重要的区域 堆
堆存储的主要是对象(数组)
堆内存分为新生代(Young Generation)、老年代(Old Generation)
新生代:主要是用来存放新生的对象
老年代:主要存放应用程序中生命周期长的内存对象。
新生代又分为Eden和Survivor区。Survivor区由FromSpace(from区)和ToSpace(to 区)成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1

堆也是GC 收集垃圾的主要区域
(理解GC机制就从:“GC的区域在哪里”,“GC的对象是什么”,“GC的时机是什么”,“GC做了哪些事”几方面来分析。)
说起这个gc我对他的理解还是挺深刻的
1、 需要GC的内存区域
GC的区域主要是在 java 堆和方法区中

2、GC的对象
需要进行回收的对象就是已经没有存活的对象,判断一个对象是否存活常用的有两种办法:引用计数和可达分析。
(1)引用计数算法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
(2)可达性分析算法(Reachability Analysis):从GC Root开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
3、什么时候触发GC
(1)程序调用System.gc时可以触发(但是不必然执行)
(2)系统自身来决定GC触发的时机(根据Eden区和From Space区的内存大小来决定。当内存大小不足时,则会启动GC线程并停止应用线程)

GC又分为 minor GC 和 Full GC
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
a.调用System.gc时,系统建议执行Full GC,但是不必然执行
b.老年代空间不足
c.方法去空间不足
d.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
e.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

4、GC做了什么事
主要做了清理对象,整理内存的工作。Java堆分为新生代和老年代,采用了不同的回收方式。(回收方式即回收算法详见后文)

二、GC常用算法
GC常用算法有:标记-清除算法标记-压缩算法复制算法分代收集算法。
目前主流的JVM(HotSpot)采用的是分代收集算法。
1、标记-清除算法
1.标记:从根集合出发,将所有活动对象及其子对象打上标记(使用刚才讲的可达性分析算法进行标记)
2.清除:遍历堆,将非活动对象(未打上标记)的连接到空闲链表上
缺点:标记清除之后,使内存中出现N个不连续的碎片块,分配速度不理想,每次分配都需要遍历空闲列表找到足够大的分块,导致可能出现很多碎片空间无法利用的情况

2、 标记-整理算法【标记-清除算法 升级版】

  1. 标记:从根集合进行扫描,对存活的对象进行标计
  2. 清除: 移动所有存活的对象,且按照内存地址次序依次排列,然后末端内存地址以后的内存全部回收
    优点:自带整理功能,这样不会产生大量不连续的内存空间,适合老年代的大对象存储。

3、复制算法
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。
他的优点是 简单高效 ,适用于对象存活率低的场景
缺点也是挺明显的:他需要浪费50%的内存,对于那些对象存活率高的需要频繁的复制

4、分代收集算法
当前商业虚拟机的垃圾收集都采用分代收集。此算法没啥新鲜的,就是将上述三种算法整合了一下
它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。

他的具体执行流程是这样的
1.当系统创建一个对象的时候,总是在Eden区操作,当这个区满了,那么就会触发一次minor GC,也就是年轻代的垃圾回收。一般来说这时候不是所有的对象都没用了,所以就会把还能用的对象复制到From区,并且年龄加1
2. 这样整个Eden区就被清理干净了,可以继续创建新的对象,当Eden区再次被用完,就再触发一次minor GC,然后呢,注意,这个时候跟刚才稍稍有点区别。这次触发YoungGC后,会将Eden区与From区还在被使用的对象复制到To区,并且年龄加1
3.再下一次minor GC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区。年龄继续加1

4.年龄大于15,进入到老年代中,此参数可以通过 -XX:MaxTenuringThreshould进行设置

如果遇到特别大的对象,需要分配较大区域来装载对象,可以直接进入老年代中

5.当老年代也满了的话,就会触发Full GC进行清理 ,如果Full GC使用太频繁的话,无疑会对系统性能产生很大的影响。所以要合理设置年轻代与老年代的大小,尽量减少Full GC的操作。
默认每小时执行一次Full GC

GC进行垃圾回收是通过垃圾收集器的

三、垃圾收集器
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现
新生代收集器
1.Serial收集器(-XX:+UseSerialGC ,复制算法)
单线程收集,进行垃圾收集时,必须暂停所有的工作线程,会停顿10ms-100ms之间
简单高效,Client模式下默认的新生代收集器

2.PerNew收集器(-XX:+UseParNewGC,复制算法)
1.这是对单线程的Serial的一种改进,ParNew收集器是多线程的,如果CPU数量为1个或者少于4个时,该种收集器的性能并不会比Serial要好。
2.可以和CMS收集器配合

3.Parallel Scavenge(潘蓉斯甘venge)收集器(-XX:+UseParallelGC ,复制算法)
1.也是复制算法,也是多线程
2.比起关注用户线程停顿时间,更关注系统的吞吐量(高效率利用CPU时间,尽可能快的完成运算任务)
3.在多核下执行才有优势,Server模式下默认的新生代收集器
(可以使用-XX:+UseAdaptiveSizePolicy 配合自适应调节策略,会把内存管理的调优任务交给虚拟机去完成)
老年代收集器
1.Serial Old 收集器(-XX:++UseSerialOldGC,标记-整理算法 )
1.单线程收集,进行垃圾收集时,必须暂停所有的工作线程
2.简单高效,Client模式下默认的老年代收集器
2.Parallel Old收集器(-XX:+UseParallelOld GC ,标记-整理算法)
多线程,吞吐量优先,和Parallel Scavenge收集器一起配合,可以实现对Java堆内存的吞吐量优先的垃圾收集策略。
3.CMS收集器(-XX:+UseConcMarkSweepGC ,标记-清除算法)
划时代的收集器,垃圾回收线程【几乎】可以和用户线程同时工作,还是必须Stop-the-World才可以
垃圾回收过程:

  1. 初始化标记:Stop-the-World
  2. 并发标记:并发追溯标记,程序不会停顿
  3. 并发预清理:查找执行并发标记阶段从新生代晋升到老年代的对象
  4. 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象 Stop-the-World
  5. 并发清理*********:清理垃圾对象,程序不会停顿
  6. 并发重置:重置CMS收集器的数据结构

对象引用

从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用软引用弱引用和虚引用。
java中的强引用,软引用,弱引用,虚引用有什么用
强引用:即使抛出OutOfMemoryError终止程序,也不会回收具有强引用的对象,可以通过将对象设置为null来弱化引用,使其被回收
String str = new String(“abc”);//强引用
软引用:(可以做缓存)对象处在有用,但非必须的状态,只有当内存空间不足时,GC才会回收该引用的对象的内存,可以用来实现高速缓存
String str = new String(“abc”);//强引用
SoftReference softStr = new SoftReference<>(str);//软引用
弱引用:(可以做缓存,这个缓存,生命周期更短)非必须的对象,比软引用更弱一些; GC时会被回收 ;被回收的概率不大,因为GC线程优先级比较低;适用于偶尔使用且不影响垃圾收集的对象
String str = new String(“abc”);//强引用
WeakReference weakStr = new WeakReference<>(str);//弱引用
虚引用:不会决定对象的生命周期,任何时候都可能被垃圾收集器回收,跟踪对象被垃圾收集器回收的活动,起哨兵作用【必须和引用队列ReferenceQueue联合使用】
String str = new String(“abc”);//强引用
ReferenceQueue queue = new ReferenceQueue();
//ReferenceQueue无实际的存储结构,存储逻辑依赖于内部节点的关系来表达【类似链表】并且可以监控队列里面是否有对象来判断对象是否被回收
PhantomReferencep = new PhantomReference(str, queue);
//虚引用
四种引用的级别由高到低:强引用>软引用>弱引用>虚引用

常用的性能优化参数:
-XX:SurvivorRatio:Eden和Survivor的比例是8:1
-XX:NewRatio:新生代内存容量与老生代内存容量的比例 1 :2
-XX:MaxTenuringThreshould:对象从新生代晋升到老年代经过GC次数的最大阀值 。。。。。。。堆相对于栈来讲
管理方式:栈自动释放,堆需要GC进行垃圾回收
空间大小:栈比堆小
碎片相关:栈产生的碎片远小于堆 (内存不能及时释放产生的碎片,因为GC不是实时的)
分配方式:栈支持静态和动态分配,而堆仅支持动态分配
效率:栈的效率比堆高

拓展:
编译器
在 JVM 中有三个非常重要的编译器,它们分别是:前端编译器、JIT 编译器、AOT 编译器。
前端编译器,最常见的就是我们的 javac 编译器,其将 Java 源代码编译为 Java 字节码文件。
JIT 即时编译器,最常见的是 HotSpot 虚拟机中的 Client Compiler 和 Server Compiler,其将 Java 字节码编译为本地机器代码。
AOT 编译器则能将源代码直接编译为本地机器码。这三种编译器的编译速度和编译质量如下:
编译速度上,解释执行 > AOT 编译器 > JIT 编译器。
编译质量上,JIT 编译器 > AOT 编译器 > 解释执行。
而在 JVM 中,通过这几种不同方式的配合,使得 JVM 的编译质量和运行速度达到最优的状态。

发布了8 篇原创文章 · 获赞 27 · 访问量 451

猜你喜欢

转载自blog.csdn.net/liuguang212/article/details/104672829