最小栈设计并实现一个minstack类_JVM01——类加载、内存模型

(01)类加载过程 bf9f4f93056e33b486d128b0b36d6786.png       启动tomcat,首先会把**.java文件进行编译(javac),转换成JVM可识别的二进制字节码文件(**.class); JVM对class文件中的类描述的数据加载到内存,并对数据进行 验证(包含文件格式验证、元数据验证、字节码验证、符号引用验证)、 准备(为类变量 分配内存且设置该变量的默认初始值,即零值 。这里不包含final static修饰的 常量 ,因为final在编译的时候就会分配给方法区,准备阶段会显示初始化;也 不会为实例变量分配初始化 ,实例变量是会随着对象一起分配到java堆中,而 类变量 会分配在方法区中。)、解析、初始化,最终转化为JVM可以直接使用的Java类型。 类加载三大阶段: eb94b7a3b96d79952a801691c75a1cfa.png*类加载过程中,遵循双亲委派机制 (类加载器收到请求,不会自己加载,而是委托给父加载器) 1f95408e90ae693f00772b137e9286c7.png 引导类加载器(启动类加载器,Bootstrap) 这个类加载器使用C/C++语言实现,嵌套在JVM内部。用来加载Java的核心类库(JRE/lib/rt.jar),用于提供JVM自身需要的类。 并不继承自java.lang.ClassLoader,没有父加载器。出于安全考虑,Bootstrap启动类加载器只加载包名为 java,javax,sun 等开头的类。 扩展类加载器 扩展类加载器是虚拟机自带的加载器。 java语言编写,由sun.misc.Launcher$ExtClassLoader实现,派生于ClassLoader类,父类加载器为启动类加载器。 加载JDK目录下的jre/lib/ext/*.jar。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。 系统类加载器 负责加载 应用程序classpath目录(tomcat中的webapps下的/WEB-INF) 下的所有jar和class文件。 自定义类加载器

为什么要自定义类加载器呢?

  • 隔离加载类:某些类路径一样,类名也相同,需要做类的仲裁,就需要自定义加载器,来隔离加载类。
  • 修改类的加载方式
  • 扩展加载源
  • 防止源码泄露

用户自定义类加载器实现步骤

  • 可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类的加载器,满足一些特殊的需求
  • 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义类的类加载器。但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法。而是建议把自定义类加载逻辑写在fingClass()方法中
  • 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及获取字节码流的方式,使自定义类加载器编写更加简洁
优点:     (1)避免类重复加载;    (2)保护程序安全,防止核心API被随意篡改 (02)虚拟机组成(Jconsole监控线程) 535a7e17195f748c978cf0c3e3ba3d27.png

字节码执行引擎是java虚拟机中最核心的部分之一。

输入的是字节码文件,处理过程是对字节码文件解析的等效过程,输出的是执行结果。

a228e546a2d7b7ac5610c97593cd79a0.png

1).虚拟机栈(JVM Stacks)栈属于线程独有的区域,顺序为先进后出,存储 局部变量表(编译时确定大小)、操作数栈、动态链接、方法返回地址和一些附加信息信息; fb08ff6e76da4e36a3932c37f7549334.png 我们试着按上图中的程序来简单画一下代码执行的内存活动。 49e01e21c6c73ebf3f500a9c582e049e.png执行main方法中的第一行代码是,栈中会分配main()方法的 栈帧,并存储math局部变量,接着执行compute()方法,那么栈又会分配compute()的 栈帧区域。 当compute()方法执行完之后,就会出栈被释放。 *栈帧: 每个方法在运行的时候都会分配一块独立的栈帧内存区域,主要有四个部分组成。 21e844d89b302a513940ee9b2fa7d3ec.png使用jdk自带的命令  javap -c **.class  可以将class文件转化为一种更可读的class文件。
Compiled from "Math.java"public class com.example.demo.test1.Math {  public static int initData;  public static com.example.demo.bean.User user;  public com.example.demo.test1.Math();    Code:       0: aload_0       1: invokespecial #1                  // Method java/lang/Object."":()V       4: return  public int compute();    Code:       0: iconst_1       1: istore_1       2: iconst_2       3: istore_2       4: iload_1       5: iload_2       6: iadd       7: bipush 10       9: imul      10: istore_3      11: iload_3      12: ireturn  public static void main(java.lang.String[]);    Code:       0: new           #2                  // class com/example/demo/test1/Math       3: dup       4: invokespecial #3                  // Method "":()V       7: astore_1       8: aload_1       9: invokevirtual #4                  // Method compute:()I      12: pop      13: getstatic #5                  // Field java/lang/System.out:Ljava/io/PrintStream;      16: ldc #6                  // String test      18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V      21: return  static {};    Code:       0: sipush 666       3: putstatic #8                  // Field initData:I       6: new           #9                  // class com/example/demo/bean/User       9: dup      10: invokespecial #10                 // Method com/example/demo/bean/User."":()V      13: putstatic #11                 // Field user:Lcom/example/demo/bean/User;      16: return}
此时的jvm指令码就清楚多了, 体结构是可以看懂的,类、静态变量、构造方法、compute()方法、main()方法。我们以 compute()方法进行详细解释: 0. 将int类型常量1压入操作数栈
0: iconst_1
这一步就是1压入操作数栈 e228b9613d6fe970c8c754d4f9083e69.png 1.  将int类型值存入局部变量1 
1: istore_1
先给局部变量1,也就是代码中的第一个局部变量a在 局部变量表中分配内存,然后将int类型的值,也就是目前唯一的一个1存入局部变量a 0210bac765064a46e4437a33a91483d9.png 2. 将int类型常量2压入操作数栈
2: iconst_2 
3.  将int类型值存入局部变量2 
3: istore_2
603102a78ad08d4c49a62d782e7e1fa8.png 4. 从局部变量1中装载int类型值
4: iload_1
5. 从局部变量2中装载int类型值 
 5: iload_2
这两个代码是将局部变量1和2,也就是a和b的值装载到操作数栈中 22c7358f6255d99bfa88b90ce9908c24.png 6. 执行int类型的加法
6: iadd
iadd指令一执行,会将操作数栈中的1和2依次从栈底弹出并相加,然后把运算结果3再压入操作数栈底。 4f70d74d749f7b37f2e715b6dfaf0afd.png 7. 将一个带符号整数压入栈
7: bipush 10
50744e8afe405db826a79f076cffb013.png 9. 执行int类型的乘法
9: imul
这里就类似上面的加法了,将3和10弹出栈,把结果30压入栈 4364f184dd59f9b1c7fbfdd6f362f99c.png 10. 将将int类型值存入局部变量3  
10: istore_3
将30存入局部变量3,也就是c df269a75d3babd1e0514dbd5a84bf013.png 11. 从局部变量3中装载int类型值
11: iload_3
da348812497be20f939f78527e3f7c71.png 12. 返回int类型值
12: ireturn
将操作数栈中的30返回 说白了赋值号=后面的就是操作数,在这些操作数进行赋值、运算的时候需要内存存放,那就是存放在操作数栈中,作为临时存放操作数的一小块内存区域。 方法出口 说白了就是方法执行完了之后要到哪里,那么我们知道compute()方法执行完之后应该回到main()方法第三行,那么当main()方法调用compute()的时候, compute()栈帧中的方法出口 就存储了 当前要回到的位置 ,那么当compute()方法执行完之后,会根据方法出口中存储的相关信息回到main()方法的相应位置。 main()方同样有自己的栈帧,在这里有些不同的地方我们讲一下。 我们上面已经知道 局部变量会存放在栈帧中的局部变量表中,那么main()方法中的math会存入其中,但是这里的math是一个对象,我们知道new出来的 对象是存放在堆中的 4fdc70f1f30c66fc9ec20d561c1db5d0.png 那么这个math变量和堆中的对象有什么联系呢?是同一个概念么?当然 不是的,局部变量表中的math存储的是堆中那个 math对象在堆中的内存地址动态链和运行时常量池有关联 运行时常量池位于方法区(注意:JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池)

47134c19942cb12401fdfee2a865e04f.png

当一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用最终转换为调用方法的直接引用。

2).程序计数器 (Program Counter Register) 程序计数器也叫PC寄存器,是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。 在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果 正在执行的是本地(Native)方法 ,这个 计数器值则应为空 (Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。 PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码,由执行引擎读取下一条指令。 3aabb9d088d2dfcf603ec259bd2e7253.png 举类说明程序计数器:
package PCRegister;public class PCRegisterTest {    public PCRegisterTest() {    }    public static void main(String[] args) {        int i = 10;        int j = 20;        int k = i + j;        System.out.println(k);    }}
图解代码: 9b4b5117773dd883c24dc495391c14b7.png 3).方法区 在jdk1.8之前,有一个名称叫做 持久代/永久代(Permanent) ,在jdk1.8之后,oracle官方改名为 元空间(Meta space) 。存放常量 ( 运行时常量池,也就是class常量池, 字符串常量池在1.7以后移到了堆里 ) 、静态变量( 1.7以后移到了堆里 )、类元信息。 方法区是 逻辑概念 ,所有JVM都必须遵循。 永久代是 HotSpot 虚拟机基于JVM规范对方法区的一个落地实现 ,并且 只有 HotSpot 才有 PermGen space,并且在JDK1.8之后被移除了。 JDK1.8之后 方法区的落地实现为 元空间 ,它和永久代的最大 区别 是 元空间不在虚拟机,而在本地内存。
public static int initData = 666;
这个initData就是静态变量,毋庸置疑是存放在方法区的
public static User user = new User();
那么这个user就有点不一样了, user变量放在方法区new的User是存放在堆中的到这里我们就能意识到栈,堆,方法区之间都是有联系的。 46891df90aeeaef909d803bdc94b6948.png 栈中的局部变量,方法区中的静态变量,如果是对象类型的话都会指向堆中new出来的对象,那么红色的联系代表什么呢?我们先来了解一下对象。 对象组成 对象在内存中存储的布局可以分为3块区域: 对象头(Header)、 实例数据(Instance Data)和 对齐填充(Padding)。下图是普通对象实例与数组对象实例的数据结构: 6c5fdefc66018b54ac4fbbe62ea98bdb.png 对象头HotSpot虚拟机的对象头包括两部分信息: Mark Word        第一部分markword,用于存储 对象自身的运行时数据,如哈希码(HashCode)、 GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“MarkWord”。 Klass Pointer        对象头的另外一部分是klass类型指针,即 对象指向它的类元数据的指针,虚拟机通过这个指针来 确定这个对象是哪个类的实例。 数组长度(只有数组对象有)        如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。 实例数据        实例数据部分是对象真正存储的有效信息,也是 在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。 对齐填充        对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 其中的klass类型指针就是那条红色的联系,那是怎么联系的呢? 897901ce2716e5b333c4b3623d267acd.png 类加载其实最终是以 类元信息 的形式 存储在方法区 中的,math和math2都是由同一个类new出来的,当对象被new时,都会在对象头中存储一个指向类元信息的指针,这就是Klass  Pointer。 4).本地方法栈(Native Method Stacks) 我们常见的线程类
new Thread().start();
它在底层调用了一个start0()的方法
private native void start0();
这个方法没有实现,但又不是接口,是使用 native修饰 的,是属于本地方法,底层通过C语言实现的。 本地方法始终也是方法,每个线程在运行的时候,如果有运行到本地方法,那么必然也要产生局部变量等,那么就需要存储在本地方法栈了。如果没有本地方法,也就没有本地方法栈了。 5).堆(Heap) 7da05fc17c55e4e19e5a98423b2ad77a.png我们new出来的对象是放在堆中,那 具体放在堆中的哪个位置呢?其实new出来的对象一般都放在 Eden区(伊甸园区),但是当Eden区满了之后呢?假设我们给 堆分配600M内存,这个是 可以通过参数调节的,我们后文再讲。那么老年代默认是占2/3的,也就是差不多400M,那年轻代就是200M,其中Eden区160M,Survivor区40M。 堆参数详解

        查看所有参数的默认初始值:

            -XX:+PrintFlagsInitial

        查看所有参数的最终值(可能会存在修改,不再是初始值)

            -XX:+PrintFlagsFinal

        初始堆空间内存(默认为物理内存的1/64),比如说我设置600m

            -Xms:600m

        最大堆空间内存(默认为物理机的1/4),比如说我设置600m

            -Xmx:600m

        设置新生代的大小(初始值及最大值)。比如说我设置80m

            -Xmn:80m

        设置新生代与老年代在堆结构的占比,比如我设置的:表示新生代占1,老年代占2,新生代占整个堆的1/3

            -XX:NewRatio=2

        设置新生代中Eden和S0/S1空间的占比,表示Eden:S0:S1 = 8:1:1

            -XX:SurvivorRatio=8

        设置新生代垃圾的最大年龄

            -XX:MaxTenuringThreshold

        输出详细的GC处理日志

            -XX:+PrintGCDetils

        打印GC简要信息

  1. -XX:PrintGC

  2. -verbose:gc

        是否设置空间分配担保(JDK6 Update24之后,不再影响空间分配担保策略,其规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行MinorGC,否则进行FullGC。)

            -XX:HandlePromotionFailure

cfb82653f9798f041901d8bf0c9484bf.png

字符串常量池(String Table): 在JDK 1.7中,intern()方法不会再复制实例,只是会把首次遇到的字符串 实例的引用 添加到常量池中,并返回此引用。
public class Ex{    public static void main(String[] args){//      String st="javac";      String s1="jav";      s1=s1+"ac";//相当于是new了一个字符串对象"javac",放入到堆内存中。      //s1="jav"+"ac"是等价于s1="javac"的,并没有在堆内存中new一个对象。      System.out.println(s1.intern()==s1);            String s2="ja";      s2=s2+"va";      //会把java当作常量放到常量池里      System.out.println(s2.intern()==s2);            String s3=new String("abc");      System.out.println(s3.intern()==s3);    }}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值