JVM详解+String类详解

面试Java时必须要搞懂的基础知识点2.0

  • JVM
    • 类加载子系统
      • 类的加载?
      • 类的生命周期
    • JVM内存结构(运行时数据区)
      • 堆内存
      • 方法区
      • JVM栈
      • 本地方法栈
      • 程序计数器
  • 再谈字符串String
    • String不可变性
    • 字符串常量池
    • StringBuffer和StringBuilder类
  • 参考

JVM

VM:虚拟机就是用软件的方式来模拟一个计算机系统,这个计算机系统具有完整的硬件系统功能,并且运行在一个完全隔离的环境中,也就是物理机的软件实现
JVM:就是采用虚拟化技术,隔离出一块独立的子操作系统,使java软件不受任何影响在虚拟机中运行
JVM实现了java平台的无关性
JVM是运行在操作系统之上,,与硬件没有直接的交互

JVM主要由三个子系统组成:
1.类加载子系统:装载具有适合名称的类或者接口
2.运行时数据区:包含方法区、java堆、java栈、本地方法栈、指令计数器、其他隐含寄存器
3.执行引擎:负责执行包含在已装载的类或接口中的指令

java程序运行的过程:
java源代码—>java编译器----->形成.class字节码文件---->类加载系统----->加载到运行时数据区(内存空间)—>通过JVM执行引擎执行

下面分别看一下JVM三个子系统

类加载子系统

类的加载?

类的加载是将.class文件中的二进制数据读到内存中,将其放在运行时数据区的方法内,
然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
类的加载最终获得了堆中的Class对象,Class对象封装了类在方法区内的数据结构,并且向java程序员提供了访问方法区内的数据结构的接口

类的生命周期

类的生命周期:加载---->连接----->初始化----->使用---->卸载
加载:查找并加载类的二进制字节流,将字节流所代表的静态数据结构转化为方法区中的运行时数据结构,然后在java堆中创建一个java.lang.Class对象
加载方式:可以根据类的全路径名找到class文件,也可以从jar文件中读取,或者运行时生成,动态代理技术
加载的时机:有些虚拟机在预期一个类要被使用时,会提前对这个类加载,有些虚拟机在真正用到一个类的时候才会加载
连接:这个阶段的主要工作就是做一些加载后的验证工作、一些初始化前的准确工作,具体步骤:验证–>准备–>初始化
1.验证:当一个类被加载后,要验证这个类是否合法,例如合格类是否符合字节码的格式,变量与方法是否重复、数据类型是否有效、继承与实现是否符合标准等,这个阶段的目的就是保证加载的类能够被JVM所运行
2.准备:为类的静态变量分配内存并设为JVM默认的初始值
对于非静态变量不会分配内存
注意:此时静态变量的初始值不是我们设置的初始值,而是JVM默认的初始值:
基本类型的默认值是0,引用类型的默认值是null,常量的默认值是程序中设定的
例如:final static a =100;//此时在准确阶段,a的初始值就是100
3.解析:这一阶段的任务就是把常量池中的符号引用转为直接引用,也就是将所有的类或接口名、字段名、方法名转换为具体的内存地址

初始化:类加载的最后一步,就是将静态变量赋值的过程

使用:对象是实例化---->垃圾收集—>对象终结
对象实例化:执行类中构造函数的内容
垃圾收集:当对象不再被引用的时候,就会被虚拟机标上特别的垃圾记号,在堆中等待GC回收
对象的终结:对象被GC回收后,对象就不再存在,对象的生命就走到尽头
卸载:类的class对象被GC
卸载类满足三个条件:
1.该类的所有的对象都已被GC,堆中不存在该类的实例化对象
2.该类没有在其他任何地方被引用
3.该类的类加载器的实例已被GC

JVM内存结构(运行时数据区)

JVM详解+String类详解_第1张图片
一个类加载到内存后会分为5部分:方法区、栈内存、堆内存、本地方法栈、程序计数器
其中方法区和堆内存是可以所以线程共享的内存区域
栈内存、本地方法栈和程序计数器是运行时线程私有的内存区域

->堆和栈的区别:
堆:用来存储java对象,无论是成员变量、局部变量还是类变量,他们指向的对象都存在堆内存中
栈:以栈帧的方式存储 方法调用的过程,并存储 方法调用过程中 的基本数据类型以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放

->私有线程和线程共享区域:
栈内存属于单个线程,每个线程都有一个栈内存,其存储的变量只能在其所属的线程中可见,即栈内存可以理解成线程的私有内存
堆内存中的对象对所有线程可见,也就是说堆内存中的对象可以被所有线程访问

堆内存

java堆是JVM中最大的区域,这块区域是线程共享的,也是gc主要的回收区
类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以便执行器执行
堆内存基于Generation算法,划分为新生代、老年代、永久代
新生代又分为伊甸区+幸存区
幸存区又分为幸存1区和幸存0区
分代收集,是基于一个事实:不同的对象生命周期不一样,因次,可以将不同生命周期的对象分代,不同的代采用不同的回收算法进行垃圾回收,提高回收效率
JVM详解+String类详解_第2张图片

1.新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器回收,结束生命
新生区又分为两部分:伊甸区+幸存区
所有的类都是在伊甸区被new出来的,
幸存区有两个:0区+1区
当伊甸区的空间用完时,程序有需要创建对象,JVM的垃圾回收器将对伊甸区进行垃圾回收,把伊甸园中剩余的对象移动到幸存0区
若幸存0区也满了,则对该区进行垃圾回收,然后移动到幸存1区
若幸存1区也满了,就移动到养老区

2.养老区中存放在新生区经历了N次垃圾回收依然存在的对象,可认为这里放着一些生命周期较长的对象
一般来说,需要大量连续存储空间的对象会被分配到这里

3.永久区用于存放JDK自身所携带的类,接口的元数据。是存储运行环境必须的类信息,被装载进此区域的数据不会被垃圾回收,关闭JVM才会释放次区域所占用的内存

JDK不同版本JVM的内存模型区别:
对比与JDK6(运行时常量池在方法区中),JDK7将运行时常量池移到了java堆内存中
对比与JDK6,JDK8直接将方法区去掉,在本地内存中新增元数据空间,运行时常量仍在堆中

方法区

方法区是所有线程都可以访问的共享区域,类的所有字段和方法的字节码,以及一些特殊方法如构造方法、接口代码在此定义
静态变量+常量+类信息(接口定义/构造方法)+运行时常量都在方法区

JVM栈

栈是线程私有的,生命周期与线程相同。
基本数据类型的变量和引用变量的内存都是在此处分配

本地方法栈

线程私有,为虚拟机使用的到native方法服务
当一个JVM创建的线程调用了native方法后,JVM不再为其在虚拟机栈中创建栈帧,而是执行引擎通过本地方法接口,利用本地方法库(c语言库)执行

程序计数器

当前线程执行的字节码的行号指示器,各线程之间独立存储,互不影响

再谈字符串String

前面1.0已经说到一点关于String类的知识点,现在再说的更详细一点

String不可变性

首先看一下String类实现的源码:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
{
     
    /** The value is used for character storage. */
    private final char value[];

    /** The offset is the first index of the storage that is used. */
    private final int offset;

    /** The count is the number of characters in the String. */
    private final int count;

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    ........
}

得出结论:
1.String类是final类型的,不能被继承。其成员变量也都是final类型。
这样定义一是为了安全性,因为String类比较底层,和操作系统接触频繁
二是为了效率,设计成final,JVM不用在虚函数表中查询相关方法,而直接定位到String类的相关方法上,提高了执行效率
2.String类是通过char数组来保存字符串的

String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响原对象,相关的任何操作(replace、sub、concat等)都会生成新的对象

那如果我们非要改变String对象该怎么办呢?
我们已经知道String类是通过char数组 value来保存字符串的,那么value这个成员变量就是一个引用,指向真正的数组内存地址,我们不能改变它的引用指向,我们能不能直接改变数组中的内容呢?那么就需要获取到value,而value是私有的,这里用到了反射

public static void reflectString() throws Exception{
     
    
    String s = "ABCDEF";
    System.out.println("s = " + s);
    
    Field valueField = s.getClass().getDeclaredField("value");
    valueField.setAccessible(true);
    
    char[] value = (char[]) valueField.get(s);
    value[0] = 'a';
    value[2] = 'c';
    value[4] = 'e';
    
    System.out.println("s = " + s);
}

打印结果:
s = ABCDEF
s = aBcDeF

字符串常量池

每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用
如果字符串不在常量池中,就会在堆中实例化字符串并且将其放入常量池中
由于String字符串的不可变性---->常量池中不存在两个相同的字符串
Java中的常量池分为两种形态:静态常量池和运行时常量池
静态常量池:即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串字面量,还包含类、方法的信息
运行时常量:jvm在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量

创建字符串的方式:
1.使用“”创建的字符串都是常量,在编译时期已经存储到常量池中
2.使用new创建的字符串对象会存储到堆中,是运行时期创建的。
当使用new创建字符串时,会先到常量池中检查是否有值相同的字符串,若没有,则在堆中和常量池中都新建一个。若有,拷贝一份到堆中,返回引用
3.当使用‘+’来创建字符串时,‘+’两边都是都是常量,例如"hellow"+“world”,编译器就能确定,若两边包含变量例如"aa"+str1,则在运行时才创建,存储在堆中

StringBuffer和StringBuilder类

String类对象一旦确定就不能被修改,那么java提供了一个可更改的字符串操作类:StringBuffer
使用StringBuffer时,每次都会对StringBuffer对象本身进行操作,而不是新生成对象
若要频繁对字符串进行修改,可选择这个类
StringBuffer的主要方式是append和insert
线程安全

StringBuilder类是线程不安全的,没有对方法加同步锁

参考

https://zhuanlan.zhihu.com/p/29629508
https://blog.csdn.net/qq_29966203/article/details/90578433
https://blog.csdn.net/zhengzhb/article/details/7517213
https://blog.csdn.net/qq_29966203/article/details/90756633

你可能感兴趣的:(java,jvm,字符串)