JVM 的运行时数据区

类的装载器 ClassLoader

通过编程显示有哪些ClassLoader

public class ClassLoaderDemo {
    public static void main(String[] args) {
        Class<?> classLoaderDemoClazz = new ClassLoaderDemo().getClass();
        //sun.misc.Launcher$AppClassLoader@18b4aac2 ,系统的 ClassLoader
        System.out.println(classLoaderDemoClazz.getClassLoader());
        //sun.misc.Launcher$ExtClassLoader@4554617c ,扩展的 ClassLoader
        System.out.println(classLoaderDemoClazz.getClassLoader().getParent());
        //null,BootStrapClassLoader
        System.out.println(classLoaderDemoClazz.getClassLoader().getParent().getParent());

        System.out.println("=====================================");

        //null ,因为 Object 和 String 都是 rt.jar 里面的,对应的都是BootStrapClassLoader
        System.out.println(new Object().getClass().getClassLoader());
        System.out.println(new String().getClass().getClassLoader());
    }
}
  • BootStrap ClassLoader : rt.jar
  • Extension ClassLoader : lib / *.jar
  • APP ClassLoader : classPath下
  • Customer ClassLoader

类的生命周期

  • 装载
    • 找到类文件交给 JVM
  • 链接
    • 验证
    • 准备
      • 为类的静态变量分配内存空间,并将其的值初始化为默认值
    • 解析
      • 将类的符号引用转换为直接引用
  • 初始化
    • 为静态变量赋值为真正的值。

运行时数据区

在这里插入图片描述

本地方法栈(Native Method Stack)

存放 native 的方法指令信息

程序计数器(PC寄存器)

每个线程都有一个程序计数器,是线程私有的,用来存储指向下一条指令的地址

Java 栈
每个方法执行时都会创建一个栈帧,用来存储局部变量表、操作数栈、动态链接、方法返回地址

局部变量表存储了编译器可知的各种基本类型、对象引用,对于 Long 和 double 的基本数据类型 占用2个局部变量空间,其他占用1个局部变量空间。可以理解为是 弹夹 ,LIFO

方法区

线程共享的,存放类的信息(构造方法、接口定义)、静态变量、常量和运行时常量池

运行时常量池
它是方法区的一部分,用于存放编译期生成各种字面量和符号引用,这部分内容将在类加载后存放到常量池中 ,String 类就在常量池中。 会发生 OOM
具有动态性,运行期间也可能将新的常量池放入池中。

  1. 比如 String.intern() 编译器会将字符串添加到常量池中,并返回常量池的引用

当 String a = new String(“aa”) ; 是在堆上创建aa字符串, 再 String b = new String(“aa”) ; 还是在堆上创建aa字符串。

但是 a.intern() 等于 b.intern(); 因为他们两个的常量池是相同的。

但是 a ≠ b, 因为堆的地址不同。

  1. 通过字面量赋值创建字符串(如:String str=”twm”)时,会先在常量池中查找是否存在相同的字符串,若存在,则将栈中的引用直接指向该字符串;若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串

如果 String c = “aa”; String d = “aa”; 则 c 和 d 是共用的堆地址和 常量池数据。

  1. 常量字符串的 “+” 操作,编译阶段直接会合成为一个字符串。

如 String e = “a” + “a”,在编译阶段会直接合并成语句String e = “aa”,于是会去常量池中查找是否存在 “aa”,从而进行创建或引用。

  1. 对于final字段,编译期直接进行了常量替换。非final 字段的不可以,因为非final 是通过 StringBuilder.append()在堆上创建新的对象。

final String f =“a”;

final String g = “a”;

String h = f + g;

h == c // true

JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池

方法区、永久代、元空间
方法区可以理解为接口,永久代和元空间是实现,jdk1.8 没有永久代,取而代之的是元空间

Heap 堆

用来存放对象的实例。一个JVM实例只有一个堆空间
堆内存分为三部分

  • Young/New 新生代

    • Eden :新创建的对象都是在 Eden区,
      • 可以配置参数,将大对象放到老年代中
    • Form/S0
    • To/S1
  • Old 老年代

  • Perm 永久区 1.7

  • Meta 元空间 1.8

堆内存 逻辑上 分为三部分 :新生 +老年 +方法区(永久代和元空间是实现方法区)

在这里插入图片描述

永久代和元空间

JDK 1.6 及之前:有永久代,常量池在方法区

JDK 1.7 : 有永久代,但已经逐步“去永久代”,常量池在堆

JDK 1.8 及以后:无永久代,常量池在元空间

Java7

在这里插入图片描述

Java8
在这里插入图片描述

栈、堆、方法区的交互关系

在这里插入图片描述

创建对象的几种方法

public class CreateObject implements Cloneable ,Serializable{
    public static void main(String[] args) throws Exception {
        // new 关键字
        CreateObject o = new CreateObject();
        // Class类的newInstance
        CreateObject o1 = CreateObject.class.newInstance();
        // Constructor类的newInstance
        Constructor<CreateObject> constructor = CreateObject.class.getConstructor();
        CreateObject o2 = constructor.newInstance();
        // clone
        CreateObject o3 = (CreateObject) o.clone();
        //序列化
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("createObj"));
        out.writeObject(o);
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("createObj"));
        CreateObject o4 = (CreateObject) in.readObject();
        System.out.println(o);
        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o3);
        System.out.println(o4);
    }
}

空间担保

对象分配到老年代

当eden区内存不够用时
当大对象存在时,超过设置的 MaxTenuringThreshold
长期存活的对象,回收年龄超过默认15
当Survivor 区中相同年龄的所有对象大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。

空间担保

对象首先分配到 eden 区,当内存不够用或回收年龄超过默认15时会进入老年代。
当进行一次垃圾回收时,会将edn区清空,将存活对象移动到s0 或 s1 中, 当 s0 或 s1 的内存不够用时,会进行空间担保。在不同的GC机制下,也就是不同垃圾回收器组合下,担保机制也略有不同。在Serial+Serial Old的情况下(客户端),发现放不下就直接启动担保机制;在Parallel Scavenge+Serial Old(服务端)的情况下,却是先要去判断一下要分配的内存是不是**>=Eden区大小的一半**,如果是那么直接把该对象放入老生代,否则才会启动担保机制。 担保机制是指将s0或 s1 的对象移动到老年代。 如果没有触发空间担保机制的话,在创建对象会存入old 区。

在发生Minor GC 之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果这个成立,那么 Minor GC是安全的。

如果不成立,则会查看是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,则尝试进行一次 Minor GC ,(如果GC 之后新生代存活的对象比较多,大于历次的平均值,会担保失败, 失败后会重新发起一次 Full GC )

JDK 6 Update 24 之后的规则变为只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小就会进行 Minor GC ,否则进行 Full GC。

内存分配策略

  • 优先分配Eden区

  • 大对象直接分配到老年代

    • -XX:PretenureSizeThreshold
  • 长期存活的对象分配到老年代

    • -XX:MaxTenuringThreshold=15
  • 空间分配担保

    • -XX:HandlePromotionFailure

    • 检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小

    • public class SpaceGuarantee {
          public static void main(String[] args) {
              //-verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M
              byte[] d1 = new byte[2 * 1024 * 1024];
              byte[] d2 = new byte[2 * 1024 * 1024];
              byte[] d3 = new byte[2 * 1024 * 1024];
              byte[] d4 = new byte[4 * 1024 * 1024];
              System.gc();
          /*
          [GC (Allocation Failure) 
          	[PSYoungGen: 6605K->952K(9216K)] 6605K->5056K(19456K), 0.0061140 secs] 
          	[Times: user=0.03 sys=0.00, real=0.01 secs]
          [GC (System.gc()) --
          	[PSYoungGen: 7335K->7335K(9216K)] 11439K->13487K(19456K), 0.0245714 secs]
              [Times: user=0.00 sys=0.00, real=0.02 secs]
          [Full GC (System.gc()) 
          	[PSYoungGen: 7335K->4825K(9216K)] 
          	[ParOldGen: 6152K->6145K(10240K)] 13487K->10970K(19456K), 
          	[Metaspace: 3245K->3245K(1056768K)], 0.0138269 secs] 
          	[Times: user=0.00 sys=0.00, real=0.01 secs]
          Heap
           PSYoungGen      total 9216K, used 5126K 
           	[0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
            eden space 8192K, 62% used 
            	[0x00000000ff600000,0x00000000ffb01b00,0x00000000ffe00000)
            from space 1024K, 0% used 
            	[0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
            to   space 1024K, 0% used 
            	[0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
           ParOldGen       total 10240K, used 6145K 
           	[0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
            object space 10240K, 60% used 
            	[0x00000000fec00000,0x00000000ff200548,0x00000000ff600000)
           Metaspace       used 3265K, capacity 4496K, committed 4864K, reserved 1056768K
            class space    used 356K, capacity 388K, committed 512K, reserved 1048576K
                class space    used 359K, capacity 388K, committed 512K, reserved 1048576K
      
            todo 从上面可以看出来 Eden 区占有 62%,old区占有60%,并触发了GC
            todo 因为新生代是10M,Eden :s0:s1=8:1:1,所以,Eden=8M,s0,s1=1M,
            todo 当Eden分配了三个2M的内存后,再分配4M的内存将不够分配,则会触发GC,
            todo GC的时候,会将Eden区的数据分配到s0,但是s0只有1M,无法存储Eden的数据,则会将已分配到Eden区的6M内存移动到老年代,在分配4M的数据到Eden区
           */
          }
      }
      
  • 动态对象年龄对象

    • -XX:TargetSurvivorRatio
    • 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代

逃逸分析 与 栈上分配

public class StackAllocation {
    public StackAllocation obj;

    //逃逸
    public StackAllocation getInstance(){
        //逃逸分析是指,在方法中,使用return new出的对象或者将new出来的对象赋值给一个全局的变量
        return  obj == null?new StackAllocation():obj;
    }
    //逃逸
    public void setObj() {
        this.obj = new StackAllocation();
    }
    //没有逃逸
    public void useStackAllocation(){
        //栈上分配是指在没有逃逸的方法中,在栈上分配内存
        StackAllocation stackAllocation = new StackAllocation();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值