JVM详解

JVM位置
在这里插入图片描述

JVM是运行在操作系统上的,它与硬件没有直接的交互

JVM体系结构概述
在这里插入图片描述
类装载器ClassLoader
负责加载class文件,class文件在文件开头有特定的文件标识,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
类装载器ClassLoader2

虚拟机自带的加载器

  • 启动类加载器(Bootstrap)++
  • 拓展类加载器(Extension ) java
  • 应用程序加载器(AppClassLoader)java也叫系统类加载器,加载当前应用的classpath的所有类

用户自定义加载器
Java.lang.ClassLoader的子类,用户可以制定类的加载方式(继承自java.lang包下的ClassLoader抽象类)

类装载器的双亲委派机制
当一个类收到了类加载请求,它首先不会尝试自己去加载这个类。而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需要加载的Class),子类加载器才会尝试自己加载。

采用双亲委派机制的一个好处是比如加载位于rt.jar包中的类java.langObject,不管是哪个加载器加载这个类,最终都会委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终都是同一个Object对象。

程序计数器(PC寄存器)
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。

这块内存区域很小,它是当前线程执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。

如果执行的是一个native方法,那这个计数器是空的。

用以完成分支、循环、跳转、异常处理、线程恢复等基础功能,不会发生内存溢出(OutOfMermory = OOM)错误

Method Area 方法区

供各线程共享的运行时内存区域。它储存了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。上面讲的是规范,在不同虚拟机里头实现不一样的,最典型的就是永久代(PermGen space)和元空间()Matespace)。But 实例变量存在堆内存中,和方法区无关

Stack栈
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命周期是跟随线程的生命周期,线程结束,栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就over,生命周期和线程一致,是线程私有的。8种基本数据类型的变量+对象的引用变量+实例方法在函数的栈内存中分配。

  • 栈存储什么?
    栈侦中主要保存3类数据:
    本地变量(Local Variables):输入参数和输出参数以及方法内的变量。
    栈操作(Operand Strack):记录出栈、入栈的操作;
    栈侦数据(Frame Data):包括类文件、方法等。

堆(heap)
在这里插入图片描述
在这里插入图片描述
MinorGC的过程(复制——>清空——>互换)

  • 1:eden、SurvivorFrom复制到SurvivorTo,年龄+1
    首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和from区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到to区域(如果对象的年龄已经达到了老年的标志(默认是15岁),则赋值到老年区),同时把这些对象的年龄+1

  • 2:清空eden、SurvivorFrom
    然后,清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to

  • 3:SurvivorTo和SurvivorFrom互换
    最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个默认参数是15),最终如果还是存活,就存入到老年代

Java 8 之后永久代被元空间取代,元空间的本质和永久代类似
在这里插入图片描述

  • 元空间与永久代之间最大的区别在于:
    永久代使用的JVM的堆内存,单java8之后的==元空间并不在虚拟机中而是使用本机物理内存。 ==

因此,在默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串常量池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermsize控制,而由系统的实际可用空间来控制。

堆内存调优

-Xms设置初始分配大小,默认为物理内存的 1/64
-Xmx最大分配内存,默认为物理内存的1/4
-XX:+PrintGCDetails输出详细的GC处理日志

编码测试

package com.haiyang.jvm;

public class Test01 {
    public static void main(String[] args) {
        System.out.println(Runtime.getRuntime().availableProcessors());//得到处理器的数
        long maxMemory = Runtime.getRuntime().maxMemory();//返回java虚拟机试图使用的最大内存量
        long totalMemory = Runtime.getRuntime().totalMemory();//返回java虚拟机中的内存总量

        System.out.println("-Xmx:MAX_MEMORY:"+maxMemory+"(字节)、"+(maxMemory / (double)1024/1024)+"MB");
        System.out.println("-Xms:TOTAL_MEMORY:"+totalMemory+"(字节)、"+(totalMemory / (double)1024/1024)+"MB");
    }
}

输出:
在这里插入图片描述

在Idea里修改参数

JVM参数:-Xms1024m -Xmx1024m -XX:+PrintGCDetails

在这里插入图片描述

测试

在这里插入图片描述
实际生产中,-Xmx 和 -Xms 配一样大:
理由:避免GC和应用程序争抢内存,理论是峰值和峰谷忽高忽低。

GC

JVM在进行GC时,并非每次都对整个内存区域一起回收的,大部分时候回收的都是指新生代。
因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(Full GC or major GC)

Minor GC和Full GC的区别

  • 普通GC(minor GC ):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以minorGC非常频繁,一般回收速度也比较快。
  • 全局GC (major GC or Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的minor GC (但并不是绝对的)。Major GC 的速度一般要比Minor GC慢上10倍以上。

四算法
1、引用计数法
2、复制算法(Copying)
3、标记清除(Mark-Sweep)
4、标记压缩(Mark-Compact)

- 引用计数法
(应用:微软的COM/ActionScrip3/Python…)

在这里插入图片描述
缺点:
1、每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗;
2、较难处理循环引用
JVM的实现一般不采用这种方法

- 复制算法(Copying)
新生代中使用的是Minor GC,这种GC算法采用的是复制算法(Coping)

复制算法的基本思想:就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

原理 :

  • 从根集合(GC Root)开始,通过Tracing从From中找到存活对象,拷贝到To中;
  • From、To交换身份,下次内存分配从To开始;

优点:1、没有标记和清除的过程,效率高
2、没有内存碎片,可以利用bump-the-pointer实现快速内存分配

缺点:1、它浪费了一半的内存。
2、复制算法要使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。

- 标记清除(Mark-Sweep)
老年代一般是由标记清除或者是标记清除与标记整理的混合实现

标记清除算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象。
在这里插入图片描述
优点:不需要额外空间
缺点:1、两次扫描,耗时严重。2、会产生内存碎片。

- 标记压缩(Mark-Compact)
标记压缩原理:
1、标记(标记的是存活对象) 2、压缩:再次扫描,并往一端滑动存活对象。然后直接清除边界以外的内存。
可以看到,标记的存活对象将会被整理,按照内存地址,依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表少了许多开销。

优点:没有内存碎片
缺点:慢工出细活,耗时太大,不仅要标记所有的存活对象,还要整理所有存活对象的引用地址。

总结

内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是比较简单的时间复杂度,实际情况并非如此);
内存整齐度:复制算法 = 标记整理算法>标记清除算法
内存利用率:标记整理算法=标记清除算法>复制算法

以上可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费 了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程。

有没有最好的算法:没有;
没有最好的算法,只有最合适的算法。==========>分代收集算法

年轻代(Young Gen)
年轻代的特点是区域相对于老年代较小,对象存活率低。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象的大小有关,因而很适用于年轻代的回收。而复杂算法内存利用率不高的问题,通过hotspot中的两个Survivor的设计得到缓解。

老年代(Tenure Gen)
老年代的特点是区域较大,对象的存活率高
这种情况,存在大量存活率高的对象,复制算法明显不合适。一般是由标记清除或者是标记清除和标记整理混合使用实现。

==Mark阶段的开销与存活对象的数量成正比。==这点上说来,对于老年代,标记清除或者标记整理有一些不符,但可以通过多核/线程利用,对并发、并行的形式标记效率。

==Sweep阶段的开销与所管理区域的大小形成正相关,==但Sweep“就地处决”的特点,回收的过程没有对象的移动。使其相对于其他有对象移动步骤的回收算法,仍是效率最好的。但是需要解决内部碎片问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值