浅析JVM

JVM架构

图1位JVM的架构图。下面的各个章节将以此为基础分别叙述。

图1: JVM架构图
JVM架构图
重点是运行时数据区(Runtime Date Area),RDA包括:

  • 方法区:存放类的描述信息,也就是说放模板的信息
  • Java栈
  • 本地方法栈
  • 程序计数器

Class Loader

负责加载class文件,class文件开头有特定的文件标示(不只是看文件扩展名)。将class文件字节码加载到内存中。
在这里插入图片描述

类加载器包括:

  • 虚拟机自带的装载器
    • 启动类加载器(Bootstrap)【C++有的】
    • 扩展类加载器(Extention)【Java扩展的】。
    • 应用程序加载器(AppClassLoader),也叫系统类加载器,加载在当前的classpath的所有类
  • 用户自定义加载器
    • Java.lang.ClassLoader的子类,用户可以自定义类的加载方式
      在这里插入图片描述

ClassLoader的双亲委派机制

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

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

当需要用到某个Class的时候,从上往下找。如下面的程序自定义class为String,在程序加载时去找用哪个Class Loader加载String,从上往下依次BootStrap Class Loader, Extention Class Loader…在Bootstrap中找的时候就有String,所以不会再往下找,但是java.lang.String(rt.jar中)类没有main方法,所以下面的程序会报错。

public class String {
    public static void main(String[] args){
        System.out.println("hello");
    }
}

再来看个例子:

public class classLoaderTest {
    public static void main(String[] args){
        //JDK自带的走的是BootStrap Class Loader
        Object obj = new Object();
        //用户自定义的走的时候AppClassLoader
        classLoaderTest test = new classLoaderTest();
        //System.out.println(obj.getClass().getClassLoader().getParent().getParent());//exception
        //System.out.println(obj.getClass().getClassLoader().getParent());//exception
        System.out.println(obj.getClass().getClassLoader());//null

        System.out.println(test.getClass().getClassLoader().getParent().getParent());//null
        System.out.println(test.getClass().getClassLoader().getParent());//sun.misc.Launcher$ExtClassLoader@74a14482
        System.out.println(test.getClass().getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2
    }
}

为什么Object可以直接调用。这是因为java的运行时环境(jre)已经装载了Object.class。打开jre中的rt.jar(runtime)包就可以看到这个class,如下图所示。
在这里插入图片描述

在这里插入图片描述
sun.misc.Launcher$AppClassLoader@18b4aac2
在这里插入图片描述

运行时内存区

Native

本地方法接口Native Interface

//Thread.java -> start0()
private native void start0();

这里native表示这个方法的实现无法由Java完成,需要借助C/C++或者底层的接口完成工作。

本地方法栈Native Method Stack

在执行引擎(execution Engine)执行时加载本地方法库。

PC寄存器,程序计数器

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

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

用以完成分支(switch),循环(while,for),跳转,异常处理,线程恢复等基础功能。不会发生内存溢出错误(Out Of Memory)。

方法区Method Area

方法区是供各线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。

上面讲的是规范,在不同虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)。但是实例变量存在堆内存中,和方法区无关

Java栈(Stack)

1 什么是Java栈

首先记住一句话,很重要:

  • 栈管运行
  • 堆管存储。

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

栈存储以下数据:

  • 本地变量(Local Variables):输入参数输出参数以及方法内的变量
  • 栈操作(operand stack):记录出栈、入栈的操作
  • 栈帧数据(Frame Data):即Java方法。包括类文件、方法等等

2 Java栈运行原理

栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集。

  • 当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中,
  • A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈,
  • B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈,
    ……
    遵循“先进后出”/“后进先出”原则。执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……

每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体JVM的实现有关,通常在256K~756K之间,约等于1Mb左右。

下图示在一个栈中有两个栈帧:

  • 栈帧 2是最先被调用的方法,先入栈,
  • 然后方法 2 又调用了方法1,栈帧 1处于栈顶的位置,
  • 栈帧 2 处于栈底,执行完毕后,依次弹出栈帧 1和栈帧 2,
  • 线程结束,栈释放。

每执行一个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕 后会自动将此栈帧出栈。

在这里插入图片描述
在这里插入图片描述

栈溢出Stack Overflow
public class MyStack {
    public static void test(){
        test();
    }

    public static void main(String[] args){
        System.out.println("11111111");
        test();
        System.out.println("22222222");

    }
}

//111111111
//Exception in thread "main" java.lang.StackOverflowError

StackOverflowError 是错误不是异常。

堆(Heap)

堆结构

  • 新生区
    • eden space
    • supervisor 0 space(from)
    • supervisor 1 space(to)
  • 养老区
  • 永久存储区(8之后称为元空间)

在这里插入图片描述

新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。

当伊甸园的空间用完,而程序又需要创建对象,那么会触发以下行为:

  • JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区
  • 若幸存 0区也满了,再对该区进行垃圾回收,然后移动到 1 区。
  • 如果1 区也满了,那就再移动到养老区。
  • 若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。
  • 若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”

如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

在这里插入图片描述

MinorGC的过程(复制->清空->互换)

1:eden、SurvivorFrom 复制到 SurvivorTo,年龄+1
首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区(只要产生GC,一定要把eden区清空),当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1

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

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

永久带 or 元空间

永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。像rt.jar就存储在永久带

实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。

对于HotSpot虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)” ,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。
在这里插入图片描述

在这里插入图片描述

  • -Xmm: 新生代的分配大小
  • -Xms: JVM Heap初始分配大小,默认为物理内存的1/64
  • Xmx: JVM Heap最大分配内存,默认为物理内存的1/4
  • -XX: printGCDetails: 输出详细的GC日志
public static void main(String[] args){
    //返回 Java 虚拟机试图使用的最大内存量。
    long maxMemory = Runtime.getRuntime().maxMemory() ;
    //返回 Java 虚拟机中的内存总量。
    long totalMemory = Runtime.getRuntime().totalMemory() ;
    System.out.println("MAX_MEMORY = " + maxMemory + "(字节)=" +
            (maxMemory / (double)1024 / 1024) + "MB");
    System.out.println("TOTAL_MEMORY = " + totalMemory + "(字节)=" +
            (totalMemory / (double)1024 / 1024) + "MB");
}

//MAX_MEMORY = 1886912512(字节)=1799.5MB
//TOTAL_MEMORY = 128974848(字节)=123.0MB

在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。

元空间与永久代之间最大的区别在于:

  • 永久带使用的JVM的堆内存
  • java8以后的元空间并不在虚拟机中而是使用本机物理内存

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

IDEA调堆内存参数和GC日志分析

Run -> edit configuration -> VM options
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
在这里插入图片描述

MAX_MEMORY = 1029177344(字节)=981.5MB
TOTAL_MEMORY = 1029177344(字节)=981.5MB
Heap
 PSYoungGen      total 305664K, used 20971K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
  eden space 262144K, 8% used [0x00000000eab00000,0x00000000ebf7afb8,0x00000000fab00000)
  from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
  to   space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
 ParOldGen       total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
  object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
 Metaspace       used 3206K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 345K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0
public static void main(String[] args){
		// VM options: -Xms4m -Xmx4m -XX:+PrintGCDetails
        byte[] temp = new byte[10 * 1024 * 1024];
    }

[GC(Allocation Failure) [PSYoungGen: 511K->488K(1024K)] 511K->504K(3584K), 0.0009078 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1000K->488K(1024K)] 1016K->592K(3584K), 0.0009197 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 991K->488K(1024K)] 1095K->656K(3584K), 0.0014451 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 599K->488K(1024K)] 767K->688K(3584K), 0.0017793 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 488K->504K(1024K)] 688K->736K(3584K), 0.0020608 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC(Allocation Failure) [PSYoungGen: 504K->0K(1024K)] [ParOldGen: 232K->617K(2560K)] 736K->617K(3584K), [Metaspace: 3213K->3213K(1056768K)], 0.0082679 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1024K)] 617K->617K(3584K), 0.0006540 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
[PSYoungGen: 0K->0K(1024K)] [ParOldGen: 617K->599K(2560K)] 617K->599K(3584K), [Metaspace: 3213K->3213K(1056768K)], 0.0082581 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
at com.jian8.basic.test.main(test.java:9)
Heap
PSYoungGen total 1024K, used 34K [0x00000000ffe80000, 0x0000000100000000, 0x0000000100000000)
eden space 512K, 6% used [0x00000000ffe80000,0x00000000ffe88888,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 2560K, used 599K [0x00000000ffc00000, 0x00000000ffe80000, 0x00000000ffe80000)
object space 2560K, 23% used [0x00000000ffc00000,0x00000000ffc95db8,0x00000000ffe80000)
Metaspace used 3244K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 353K, capacity 388K, committed 512K, reserved 1048576K

在这里插入图片描述
[GC(Allocation Failure) [PSYoungGen【GC类型】: 511K【Young GC 前新生代大小】->488K【Young GC 后新生代大小】(1024K)【新生代总大小】] 511K【Young GC前 JVM堆内存占用】->504K【Young GC后 JVM堆内存占用】(3584K)【JVM堆总大小】, 0.0009078 secs] [Times: user=0.00【Young GC用户耗时】 sys=0.00【Young GC系统耗时】 , real=0.00 secs【Young GC实际耗时】 ]

栈 + 堆 + 方法区的关系

在这里插入图片描述

为什么full GC比GC慢?
因为full GC要清理的空间更大。就像打扫家里一样,面积越大自然也就越慢。

GC四大算法

引用计数法

在这里插入图片描述

复制算法(Copying)

在这里插入图片描述

在这里插入图片描述

特点:

  • 不会产生内存碎片
  • 但是耗费空间

Eden区的对象一般存活率比较低,

标记清除(Mark-Sweep)

在这里插入图片描述
特点:

  • 不需要复制拷贝,节省了内存空间
  • 但是导致了内存碎片。
  • 需要先标记,后清除。所以需要两次扫描,耗时

标记压缩(Mark-Compact)

步骤:标记-》清除-》压缩
相当于在标记清除中多了步压缩,将不连续的内存压缩至连续。这会导致更耗时。
在这里插入图片描述
工作中可以改进,多次GC后才Compact,减少移动对象的成本。
在这里插入图片描述

GC算法总结

GC四大算法包括:

  • 引用计数法
  • 复制算法
  • 标记清除算法
  • 标记压缩/标记整理算法

Java9之后有G1垃圾回收算法,效率高且耗时低。

四大算法的特点是:

  • 内存效率:复制算法 > 标记清除算法 > 标记压缩算法
  • 内存整齐度:复制算法 > 标记压缩算法 > 标记清除算法
  • 内存利用率:标记压缩算法 > 标记清除算法 > 复制算法

这里再思考一个问题,是否有最好的GC算法。

答:没有最好的算法,只有最合适的算法。按照不同代的特点,采用分代收集算法。

  • 年轻代
    • 年轻代的特点是区域相对较小且对象的存活率低。
    • 采用复制算法回收整理,速度是最快的。复制算法的效率只和当前存货对象的大小有关,因此使用于年轻代的垃圾回收。
  • 老年代
    • 老年代的特点是区域较大且对象存活率高。存在大量存活率高的对象的时候,采用复制算法不合适。
    • 一般是采用标记清除标记压缩的混合实现(mark-sweep-compact)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值