JVM01-堆、方法区、对象创建、执行引擎和StringTable

一、堆

概述

MinorGC、MajorGC和FullGC

JVM在进行GC时,并非每次都对上面的三个内存(新生代、老年代;方法区)区域一起回收,大部分时候回收的都是 新生代

针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC

  • 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:

    • 新生代收集(Minor GC / Young GC):只是新生代的垃圾收集
    • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。
      • 目前,只有CMS GC 会有单独收集老年代的行为。
      • 注意:很多时候Major GCFull GC 混淆使用,需要具体分辨是老年代回收还是整堆回收
    • 混合回收(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
      • 目前,只有 G1 GC 会有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

年轻代GC(Minor GC)触发机制:

  • 当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次 Minor GC会清理年轻代的内存。)
  • 因为Java 对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
  • Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。

老年代GC (Major GC/Full GC) 触发机制:

  • 指发生在老年代的GC,对象从老年代消失时,我们说Major GCFu11 GC发生了。
  • 出现了Major GC, 经常会伴随至少一次的Minor GC (但非绝对的,在Paral1el
    Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
    • 也就是在老年代空间不足时,会先尝试触发Minor GC 。如果之后空间还不足,则触发Major GC
  • Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。
  • 如果Major GC后,内存还不足,就报00M (OutOfMemoryError)了。

堆是分配对象存储的唯一选择吗?

随着JIT编译期的发展与 逃逸分析技术 逐渐成熟,栈上分配标量替换优化技术 将会导致一些微妙的变化,所有的对象都分配到堆上也变得不那么“绝对”了。

如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在对上分配内存,也无需进行垃圾回收了。这也就是堆外存储技术

逃逸分析概述

  • 如何将堆上的对象分配到栈,需要使用逃逸分析手段。
  • 这是一种可以有效减少Java程序中同步负载内存堆分配压力的跨函数全局数据流分析算法。
  • 通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。
  • 逃逸分析的基本行为就是分析对象动态作用域:
    • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
    • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。

逃逸分析:代码优化

如果一个对象不会发生逃逸,则可能为这个变量进行一些高效地优化:

  • 栈上分配:如果一个对象不会发生逃逸,可以让这个对象在栈上分配内存,对象所占用的内存会随着栈帧的出栈而销毁。减少了GC的时间消耗。
  • 同步消除:如果一个对象不会发生线程逃逸,那么对这个变量实施的操作可以不考虑同步。
  • 标量替换:如果一个对象不会被外部访问,并且这个对象可以被拆解(由聚合量标量),那么该对象就不需要在堆上创建,而是直接在栈上创建这个对象的成员变量。

二、方法区

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

运行时数据区结构图

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

方法区概述

  • 到了 JDK 8,就完全放弃了永久代的概念,改为与JRockit、J9一样在本地内存 (并非Java虚拟机的内存)中实现的元空间(Metaspace)来代替。

  • 元空间的本质与永久代类似,都是对JVM规范中方法区的实现。两者最大的区别是:元空间不在虚拟机设置的内存中,而是使用本地内存

  • 如果方法区无法满足新的内存分配需要时,将会抛出OOM异常。

方法区的内部结构

方法区存储内存

用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

方法区的演进细节

版本细节
jdk1.6及之前有永久代,静态变量存放在永久代中
jdk1.7有永久代,但已经逐渐“去永久代”,字节串常量池、静态变量移除,保存到堆中
jdk1.8及之后无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中

只有HotSpot才有永久代的说法,这样做的好处是:GC能够像管理堆一样管理这部分内存,减少工作。但是带来的缺点就是Java应用很容易遇到内存溢出的问题(如果是放在元空间中,是使用本地内存,不再使用虚拟机的内存,容量更大)。

字符串常量池为什么要调整位置?

jdk7中将StringTable放到了堆空间中。因为永久代的回收效率很低,在full gc的时候才会触发。而full gc是老年代的空间不足、永久代不足时才会触发。这就导致StringTable回收效率不高。

而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。

方法区的垃圾收集

方法区垃圾收集的“性价比”通常是比较低的。

方法区的垃圾收集主要回收两部分内容:

  • 常量池中废弃的常量,与Java堆中对象的回收类似,如果没有任何地方引用一个常量,那么这个常量就可以被系统清理出常量池。
  • 不再使用的类型,要判断一个类型是否属于“不再被使用的类”则是比较苛刻的,需要同时满足3个条件。

总结

image-20200820103808645

常见面试题

image-20200820104131223

image-20200820104047927

三、HotSpot虚拟机对象

对象的创建

image-20200820113840759

  1. 首先检查类是否被加载、解析和初始化过。如果没有,就必须先执行相应的类加载过程。

  2. 为新生对象分配内存(对象所需内存的大小在类加载完成后就可以完全确定),为对象分配空间就是从Java堆中划分一块空间。

    • 堆中内存是绝对规整的:所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞
    • 如果堆中的内存不是规整的:虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在需要时划分一块足够大的空间,这种分配方式称为“空闲列表”。
    • 说明:选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。
  3. 并发情况下,即使是指针碰撞 这种简单的方式,对象创建也并不是线程安全的。解决该问题的两种方案:

    • 采用CAS(Compare And Set)配上失败重试的方式来保证操作的原子性。
    • 每个线程在Java堆中分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。可以通过-XX:+/-UseTLAB参数来设定。
  4. 将分配到的内存空间(不包括对象头)都初始化为零值。这步操作保证了对象的实例字段在不赋初始值时就可以直接使用。

  5. 设置对象的对象头

  6. 执行Class文件中的()方法。

对象的内存布局

image-20200820163025927

image-20200820162031375

对象的访问定位

Java程序是通过reference数据来操作堆上的具体对象,主流的对象访问方式有两种:句柄访问直接指针

句柄访问

在Java堆中专门划分一块内存作为句柄池 ,句柄中包含了对象实例数据与类型数据各自具体的地址信息。

最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

image-20200820164807387

直接指针

Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。

HotSpot主要采用这种对象访问方式。

好处:

  • 速度很快,减少了一次指针定位的时间开销。

image-20200820164613043

四、执行引擎

概述

执行引擎是Java虚拟机核心的组成部分之一。

“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式

JVM的任务是负责装载字节码到其内部,但字节码不能直接运行在操作系统上,因为字节码指令并不等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。

那么,如果想要一个Java程序运行起来,执行引擎(Execution Engine)的工作就是将字节码指令解释/编译为对应平台的本地机器指令才可以

即时编译器

既然HotSpot VM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?

当Java虛拟器启动时,解释器可以首先发挥作用,而不必等待 即时编译器(JIT,just in time) 全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器把越来越多的代码编译成本地代码,开始发挥作用,获得更高的执行效率。

当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。
编译器要想发挥作用,要先把代码编译成本地代码,需要一定的执行时间。但编译为本地代码后,执行效率高。

五、StringTable

String的基本特性

字符串常量池是不会存储相同内容的字符串的。

  • String的String Pool 是一个固定大小的Hashtable,默认值长度是1009。
  • 使用-XX:StringTableSize 可以设置StringTable的长度
  • 在JDK6中StringTable的长度默认是1009
  • 在JDK7中,StringTable的长度默认是60013
  • JDK8后,设置StringTable的长度时,1009是可以设置的最小值。

String内存位置的变化

看上面方法区的演进细节 以及 字符串常量池为什么要调整位置?

字符串拼接操作

  1. 常量与常量的拼接结果是在常量池,原理是编译器优化。

  2. 常量池中不会存在相同结果的常量

  3. 只要其中有一个是变量,结果就是在堆中。变量拼接的原理是StringBuilder。

    //从生成的字节码中,也可以看出这一点	
    //下面测试测试的部分字节码如下:
      0 ldc #2 <Hadoop>
      2 astore_1
      3 ldc #3 <Hdfs>
      5 astore_2
      6 ldc #4 <HadoopHdfs>
      8 astore_3
      9 ldc #4 <HadoopHdfs>	//编译器优化:在编译阶段,常量与常量就拼接成HadoopHdfs
     11 astore 4
     13 new #5 <java/lang/StringBuilder>	//使用StringBuiler进行 s1 + "Hdfs"
     16 dup
     17 invokespecial #6 <java/lang/StringBuilder.<init>>
     20 aload_1
     21 invokevirtual #7 <java/lang/StringBuilder.append>
     24 ldc #3 <Hdfs>
     26 invokevirtual #7 <java/lang/StringBuilder.append>
     29 invokevirtual #8 <java/lang/StringBuilder.toString>
     32 astore 5
     34 new #5 <java/lang/StringBuilder>
     37 dup
     38 invokespecial #6 <java/lang/StringBuilder.<init>>
     41 ldc #2 <Hadoop>
     43 invokevirtual #7 <java/lang/StringBuilder.append>
     46 aload_2
     47 invokevirtual #7 <java/lang/StringBuilder.append>
     50 invokevirtual #8 <java/lang/StringBuilder.toString>
     53 astore 6
     55 new #5 <java/lang/StringBuilder>
     58 dup
     59 invokespecial #6 <java/lang/StringBuilder.<init>>
     62 aload_1
     63 invokevirtual #7 <java/lang/StringBuilder.append>
     66 aload_2
     67 invokevirtual #7 <java/lang/StringBuilder.append>
     70 invokevirtual #8 <java/lang/StringBuilder.toString>
     73 astore 7
    
    
  4. 如果拼接结果调用了 intern() 方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。

@Test
public void test1() {
    String s1 = "Hadoop";
    String s2 = "Hdfs";

    String s3 = "HadoopHdfs";
    String s4 = "Hadoop" + "Hdfs";//编译器优化
    //如果拼接符号的前后出现了变量,则相当于在堆空间中new String(), 
    //具体的内容为拼接的结果: HadoopHdfs
    String s5 = s1 + "Hdfs";
    String s6 = "Hadoop" + s2;
    String s7 = s1 + s2;

    System.out.println(s3 == s4);//true
    System.out.println(s3 == s5);//false
    System.out.println(s3 == s6);//false
    System.out.println(s3 == s7);//false
    System.out.println(s5 == s6);//false
    System.out.println(s5 == s7);//false
    System.out.println(s6 == s7);//false

    //intern():判断字符串常量池中是否存在HadoopHdfs值,如果存在,则返回常量池中HadoopHdfs的地址;
    //如果字符串常量池中不存在HadoopHdfs,则在常量池中加载一份HadoopHdfs, 并返回次对象的地址。
    String s8 = s6.intern();
    System.out.println(s3 == s8);//true
}

intern() 的使用

new String() 创建了几个对象

new String("abc") 会创建2个对象:

  • new 关键字在堆空间创建的对象
  • 字符串常量池中的对象,字节码指令:ldc
@Test
public void test3() {
    String s2 = new String("abc");
}

//上面代码的字节码为:
 0 new #15 <java/lang/String>	//new 一个String对象
 3 dup
 4 ldc #16 <abc>	//在字符串常量池中放入abc
 6 invokespecial #17 <java/lang/String.<init>>
 9 astore_1
10 return

new String("a") + new String("b") 会创建几个对象呢?

  1. 对象1:new StringBuilder
  2. 对象2:new String(“a”)
  3. 对象3:常量池中的a
  4. 对象4:new String(“b”)
  5. 对象5:常量池中的b

StringBuilder的toString() 方法:

尽管该方法体中就是new 了一个 String对象,但是和普通的 new 一个String对象不同,它只创建了一个对象。

@Override
public String toString() {
 // Create a copy, don't share the array
 return new String(value, 0, count);
}
0 new #80 <java/lang/String>
3 dup
4 aload_0
5 getfield #234 <java/lang/StringBuilder.value>
8 iconst_0
9 aload_0
10 getfield #233 <java/lang/StringBuilder.count>
13 invokespecial #291 <java/lang/String.<init>>
16 areturn

在字符串常量池中,并没有生成。

死坑题

public class StringIntern {

    public static void main(String[] args) {
        String s1 = new String("1");
        String intern1 = s1.intern();//调用此方法之前,字符串常量池中已经存在了"1"
        String s2 = "1";
        System.out.println(s1 == s2);   //jdk6:false;jdk7/8:false
        System.out.println(intern1 == s2);  //true

        //s3变量记录的地址为:new String("12")
        String s3 = new String("1") + new String("2");
        //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
        //在字符串常量池中生成"11"。
        //如何理解:jdk6:创建了一个新的对象"12",也就有新的地址。
                 //jdk7:此时常量中并没有创建"12",而是创建一个指向堆空间中new String("12")的地址,					//目的就是为了节省内存空间
        String intern2 = s3.intern();
        //s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"12"的地址
        String s4 = "12";
        System.out.println(s3 == s4);   //jdk6:false;jdk7/8:true
        System.out.println(intern2 == s4);  //true
    }
}

总结

总结String的intern()的使用:

  • Jdk1.6中,将这个字符串对象尝试放入串池。
    • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
    • 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
  • Jdk1.7起,将这个字符串对象尝试放入串池。
    • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
    • 如果没有,则会把对象的引用地址复制一份, 放入串池,并返回串池中的引用地址
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值