JVM类加载过程
JVM架构模型
由于跨平台性的设计,Java的指令集都是根据栈来设计的,优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
JVM生命周期
1.虚拟机的启动
java虚拟机的启动是通过引导类加载器,bootstrap class loader创建一个初始类来完成的initial class来完成的,这个类是由虚拟机的具体实现来指定的;
2.虚拟机的执行
虚拟机的任务就是执行java程序,
程序开始时,它才执行,程序结束时,它才停止。
执行java程序时,其实执行的就是Java虚拟机
3.虚拟机的退出
程序执行结束
程序遇到了异常或者错误而终止
由于操作系统出现错误导致jvm进程终止
某线程调用了runtime类或者System类的exit方法,或者Runtime类的halt方法,且Java安全管理允许此次操作
JNI卸载java虚拟机时
JVM产品
classical Jrockiet J9 Hotspot
JVM加载过程
加载:
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
链接:
-
验证
1.确保Class文件的字节流符合虚拟机的要求,保证类的正确性,保证不会危害虚拟机
2.主要包括四种验证:文件格式验证,字节码验证,元数据验证,符号引用验证
-
准备
1.为类变量分配内存并且设置变量的默认初始值,int默认值0,引用类型默认为null
类变量:被static修饰的变量
2.这里不包含用final修饰的static,因为final在编译时候就会分配了,准备阶段会显示初始化;
3.这里不会为实例变量分配初始化,类变量会分配到方法区中,而实例变量会随着对象分配到堆中
实例变量 ,没有被static修饰的成员变量,不是类中的局部变量
-
解析
1.解析动作主要针对类或者接口,字段,类方法,接口方法,方法类型等
初始化
- 初始化阶段就是执行类的构造方法()的过程。这个构造方法只是为了初始化类变量存在的,如果没有类变量那么就不会调用此方法。
- 此方法不需要定义,是java编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按照语句在源文件中出现的顺序执行
- ()不同于类的构造器。
- 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕
- 虚拟机必须保证一个类的()方法在多线程下被同步加锁
类加载器
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
JVM把所有派生于抽象类的Boostrap CLassLoader的类加载器都认为是自定义加载器;
Bootstrap ClassLoader
这个类的加载使用c/c++实现的,嵌套在JVM内部
它用来加载Java的核心库,用于提供JVM自身需要的类
并不继承java.lang.classLoader,没有父加载器
加载扩展类和应用程序类加载器,并指定为他们的父类加载器
出于安全考虑,启动类加载器只加载java,javax,sun等开头的类
扩展类加载器 Extension
java语言编写
派生于ClassLoader
父类加载器为启动类加载器
从java.ext.dirs系统所指定的目录中加载,或者从jre/lib/ext中加载,如果用户创建的jar放在此目录下,也会被加载
应用程序类加载器 AppClassLoader
派生于ClassLoader
父类加载器为扩展类加载器
负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库
该类加载是程序中的默认的类的加载器,一般来说java应用的类都是由它完成加载
通过ClassLoader.getSystemClassLoader()方法可以获取到此类
双亲委派机制
java虚拟机对class文件采用按需加载的方式,当使用到该类时才会将它的class文件加载进内存生成class对象
加载class文件时,采用的是双亲委派机制,即把请求交给父类处理,它是一种任务委派模式
所以,如果自定义了一个String类,在调用String时,使用的依然是系统的String,因为应用类加载器在加载String时会向上委托,直到引导类加载器加载核心StringAPI。
优势:
- 避免类的重复加载
- 保护程序安全,防止核心api被篡改
类的主动使用和被动使用
JVM运行时数据区及线程
程序计数器
作用
用来存储指向下一条指令的地址,由执行引擎读取下一条指令;
介绍
它是一块很小的内存空间,也是运行速度最快的存储区域。
每个线程都有自己的程序计数器 ,是线程私有的,生命周期与线程的生命周期保持一致
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法,程序计数器指向当前线程正在执行的java方法的jvm地址,如果是在执行native方法,则是未指定值
它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复都需要依赖计数器。
字节码解释器工作时,就是通过改变程序计数器的值来选取下一条字节码指令。
唯一一个java虚拟机规范中没有规定任何outotmemoryError情况的区域。
虚拟机栈
介绍:
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧;
栈中的数据都是以栈帧的格式存在。
栈帧是一个区块,里面是一个数据集合
线程上正在执行的每个方法都对应一个栈帧
是线程私有的,生命周期和线程一致
作用:
运行时单位,解决程序的运行问题,程序的执行和数据的处理。
保存方法的局部变量,部分结果,并参与方法的调用和返回
优点:
-
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
-
JVM直接对java栈的操作只有两个:
每个方法的执行,伴随着进栈,入栈,压栈
执行结束后的出栈操作
-
栈不存在垃圾回收问题
异常:
Java栈的大小可以是固定的或者动态的
如果采用固定大小,那么栈的大小会在线程创建的是时候选定,如果线程请求的容量超出了栈的容量,那么会出现StackOverflowError
如果采用动态大小,那么当线程请求的容量超出栈的容量,栈会自动扩展,如果栈无法扩展到足够的内存,会出现OutOfMemory异常
线程在最初创建时,如果内存不足够分配虚拟机栈,也会出现OutOfMemory异常
栈的运行原理
在一条活动线程中,一个时间点上,只有一个栈帧是活动的,即正在被执行方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧,与之相对应的方法被称为当前方法,定义这个方法的类被称为当前类。
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
如果在该方法中调用了其他方法,就会创建一个新的栈帧对应此方法,称为新的栈顶栈帧。
不同线程中的栈帧是不能共享的,不可能在一个栈帧中引用其他线程的栈帧。
java有两种返回函数的方式,一种是return另一种是抛出异常,他们都会导致栈帧弹出
栈帧的内部结构
每个栈帧中都包含:
- 局部变量表
- 操作数栈(或表达式栈)
- 动态链接(或指向运行时常量池的引用)
- 方法返回地址(或方法正常退出或者异常退出的定义)
- 一些附加信息
局部变量表
局部变量表也被称为局部变量数组或本地变量表。
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,对象引用,以及returnAddress类型。
由于局部变量表是建立在线程的栈上,是线程的私有数据,应此不存在数据安全问题。
局部变量表所需的容量大小是在编译期确定下来的,并保存在Code属性的maximum local variables数据项中,在方法运行期间,大小不会变化
slot
- 局部变量表的基本存储单元是slot(变量槽)
- 参数值的存放总是在局部变量数组的0下标位开始,到数组长度为-1的索引结束
- 在局部变量表里,32位以内的类型只占用一个solt,64位类型占用两个solt
- byte,short,char在存储前被转化为int,Boolean也转为int false变为0 true变为非0
- long和double占据两个slot
- jvm会为局部变量表中的每一个solt分配一个访问索引 ,64位的局部变量值时,只需要访问前一个索引
- 如果当前帧是由构造方法或者实例方法创建的,那么该对象的引用this会存放在index为0的slot处,其余参数按照参数表顺序继续排列
- solt的位置是可以重复利用的,即如果局部变量表中的一个变量已经过了作用域,那么在这个变量作用域之后声明的变量会重新占据该位置,达到节省资源的效果,因为局部变量表一旦生成,不能动态的减少,那么之前过期的变量就可以重复利用。
操作数栈
介绍
栈帧中的元素之一,在方法的执行过程中,根据字节码指令,往栈中写入数据或者提取数据,即入栈和出栈
某些字节码指令将值压入操作数栈,其余字节码指令将操作数取出栈,使用他们后再把结果压入栈
- 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
- 操作数栈就是jvm执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会被创建出来,这个方法的操作数栈是空的。
- 每一个操作数栈都拥有一个明确的栈深度用于存储数值,其所需最大的深度在编译器就定义好了,保存在方法的code属性中
- 栈中的元素可以是任意的数据类型
- 32位占用一个深度
- 64位占用两个深度
栈顶缓存技术
栈顶元素将全部缓存在物理cpu的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率
动态链接
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码实现动态链接。比如invokedynamic指令。
- 在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
虚方法和非虚方法
如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
静态方法,私有方法,final方法,构造方法,父类方法都是非虚的
其他方法称为虚方法
方法返回地址
存放调用该方法的pc寄存器的值
一个方法的结束有两种方式
正常执行完成,
出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到调用该方法的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法指令的下一条指令地址。而通过异常退出的,返回地址通过异常表来确定,栈帧一般不保存这部分信息。
本质上方法的退出就是当前栈帧的出栈过程。此时需要将此方法的返回值压入调用者栈的操作数栈,并且返回到调用者调用此次方法后面的位置,设置pc寄存器值等,让方法继续下去 。
异常退出的方法不会给调用者返回方法的返回值
本地方法
一个本地方法就是一个java调用非java代码的接口。一个本地方法的实现由非java语言实现
-
与Java环境外交互
有时java应用需要与java外面的环境交互,这是本地方法存在的主要原因。比如java和底层底层系统或者系统硬件之间的交互。
-
与操作系统交互
-
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动的打印机等
本地方法栈
虚拟机栈用于管理java方法的调用,而本地方法栈用于管理本地方法的调用
本地方法栈是线程私有的
允许被实现成固定或者是可动态扩展内存大小,内存溢出原理和虚拟机栈类似
当某个线程调用了一个本地方法,它就进入了一个全新的并且不受虚拟机限制的的世界,它和虚拟机拥有同样的权限。
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
- 它可以直接使用本地处理器中的寄存器
- 直接从本地内存的堆中分配任意数量的内存
并不是所有的jvm都支持本地方法。因为java虚拟机规范中没有明确必须使用本地方法栈的使用语言,具体实现方式,数据结构等
再hotspot jvm中,直接将本地方法栈和虚拟机栈合二为一
堆
- 一个jvm实例只存在一个堆区,堆是java内存管理的核心区域
- java堆区在jvm启动的时候就被创建,其空间大下也被确定,是jvm管理的最大一块内存空间
- 堆的内存大小是可以调节的
- 堆可以是物理上不连续的一块区域,但是逻辑上应该是连续的
- 所有线程共享堆区的内容,这里还可以划分线程的私有缓冲区(Thread Local Allocation buffer,TLAB)
- 几乎所有的对象实例以及数组都应当在运行时分配在堆上
- 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
- 堆是gc执行的重点区域
堆的内部结构
- 逻辑上有永久区或者元空间,实际上堆空间没有给他们分配空间
堆空间大小的设置
年轻代与老年代
- 存储在堆中的对象一般分为两类
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
- 另外一类对象的生命周期很长,在极端情况下可能和jvm生命周期一致
- java堆区进一步细分可以分为年轻代和老年代
- 其中年轻代又分为Eden空间,survivor0和survivor1空间,也叫做from区 to区
对象分配过程
- survivor区满了不会触发YGC,等到下次Eden满了触发gc时,会将survivor一并处理
- 每次gc操作,eden区一定被清空,其中一个survivor区为空
- 关于垃圾回收,频繁在新生区收集,很少在养老区,几乎不在永久区/元空间收集
- 有可能survivor区不到15次gc直接跳到老年区,也可能从Eden直接跳到老年区
MinorGC,Major GC,FUll GC
MinorGCM
触发条件,新生代的Eden区满,特点执行频繁,回收速度快,会引发STW
MajorGC
当对象从老年代清除,那么就发生了MajorGC或者FullGC
出现了MajorGC,经常会伴随着一次MinorGC
MajorGC的执行时间比MinorGC慢10被以上,STW时间会很长
如果MajorGC后,内存还是不足,就会报oom
FullGC
触发条件
- 调用System.gc()时,系统建议执行FullGC但是不是必然执行
- 老年代空间不足
- 方法区空间不足
- 通过MinorGC后 进入老年代的大小大于老年代的可用空间
- 对象由年轻代向老年代转换时,老年代内存空间不足
要尽量避免full gc
内存分配策略
- 优先分配到Eden
- 大对象直接分配到老年代
- 尽量避免程序中出现过多的大对象
- 长期存活的对象分配到老年代
- 动态对象年龄判断
- 如果Survivor 区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到要求的年龄
TLAB
- 堆区是线程的共享区域,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建非常频繁,因此在并发环境下从堆区中划分内存是线程不安全的
- 为避免多个线程操作同一个地址,需要使用枷锁等机制,进而影响分配速度
- JVM为每个线程分配了一个私有的缓存区,它包含在Eden空间内
- 多线程同时分配内存时,使用TLAB可以避免一系列的线程安全问题
在Eden中为线程分配的一小块空间,约占总Eden空间的1%,不是每个对象实例都能够在TLAB中分配内存,但是JVM把TLAB作为内存分配的首选,如果对象在TLAB分配失败,那么JVM会通过使用加锁的机制,确保数据操作的原子性
小结堆空间的参数设置
空间担保机制
逃逸分析
同步省略
如果方法中的对象只能被一个线程访问,那么编译器在编译这个同步块的时候会取消这部分的代码同步,也叫锁消除
分离对象或标量替换
有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在cpu寄存器中
逃逸分析总结
逃逸分析的目的是经过一系列算法,判断对象是否逃逸,尽量让对象在栈上存储,但是这项技术并不成熟,不能保证逃逸分析后的结果所造成的消耗一定小于不进行逃逸分析的消耗,因为逃逸分析本身也具有消耗,另一个例子经过逃逸分析这项耗费时间的操作却发现对象是逃逸的,那么这个分析过程就白白浪费了
hotspot虚拟机目前并没有实现栈上分配,不过使用了标量替换
方法区
java虚拟机规范中明确说明方法区在逻辑上是堆的一部分,但是对于hotspotJVM而言,方法区还有一个别名叫做non-heap,所以,方法区看作是独立于堆的一部分空间
- 方法区和堆一样也是线程共享的区域
- 方法区在JVM启动的时候被创建,并且它的实际物理内存和堆空间一样都可以是不连续的
- 方法区的大小和堆空间的大小一样,可以选择固定或者扩展
- 方法区的大小决定了JVM可以保存多少个类,如果定义的类过多,那么会出现oom错误
- 关闭JVM就会释放掉方法区的资源
设置方法区大小
jdk7以前-xx:PermSize来设置永久代初始空间,默认20.75
-xx:MaxPermSize设置永久代最大空间,32位机器默认64m,64位机器默认82m
当超出最大空间会报异常oom:PermGen space
jdk8以后改为MetaspaceSize(元空间)
默认值依赖于平台,windows下默认值是21m左右,最大值是-1,即没有限制,对于64位的服务器端的JVM来说,默认值就是21M,这就是初始的高水位线,一旦触及这个水位线,Full gc就会被触发卸载没用的类,然后重置水位线,重置水位线的高低取决于gc释放元空间的多少,如果释放的多,那么水位线可能调低,如果释放的少,那么水位线可能相应的调高些(但不超过设置的最大值),为了避免水位线频繁调高,建议一开始把水位线调的稍微高点
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有能用的系统内存,如果元数据发生溢出,虚拟机会抛出异常oom:Metaspace
方法区内部结构
方法区存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等
类型信息
- 对每个加载的类型包括类,接口,枚举,注解,JVM必须在方法区中存储一下信息
- 这个类型的完整有效名称(全名=包名.类名)
- 这个类型直接父类的完整有效名对于interface或是java.lang.Object都没有父类
- 这个类型的修饰符,public,abstract,final
- 这个类型直接接口的一个有序列表
域信息
保存类型的所有域的相关信息,包括域的声明顺序
域的相关信息包括:域名称 ,域类型,域修饰符(public,private,static,final…)
方法信息
JVM必须包含所有方法的以下信息
- 方法名称
- 方法的返回类型或void
- 方法参数的数量和类型
- 方法的修饰符
- 方法的字节码,操作数栈,局部变量表大小(abstract和native方法除外)
- 异常表(abstract和native方法除外)每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址,被捕获异常类的常量池索引
non-final类变量
- 静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
- 类变量被类的所有实例所共享,即使没有类实例时你也可以访问它
运行时常量池和常量池
- 方法区,内部包含了运行时常量池
- 字节码文件,内部包含了常量池
- 要弄清除方法区,需要理解清楚classFIle,因为加载类信息都在方法区
- 要弄清楚方法区的运行时常量池,需要理解清楚ClassFIle中的常量池
常量池相当于一张表,存放编译期的各种字面量和符号引用,这部分内容经过加载后存放到方法区的运行时常量池中
JVM为每个已加载的类型(类或者接口)都维护一个常量池,池中的数据项和数组一样是通过索引访问的。
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行解析期后才能过获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
运行时常量池是具备动态性的,而常量池则不具备
运行时常量池
方法区演进过程
永久代为什么被元空间所替代
永久代和元空间的一大不同就是元空间使用本地内存,元空间的最大可分配空间就是系统的可用内存空间,这是很有必要的因为永久代的空间大小很难确定,尤其是动态加载很多类的时候,而且对永久代的调优也很困难
结论
引用的对象实体始终存放在堆空间,我们说的是它的引用
static byte[] by1=new byte[1024*1024];//虽然是静态变量,但是实体1m大小的数组是放在堆空间的
byte[] by2=new byte[1024*1024];//非静态也是放在堆空间
但是引用名的位置并不是固定的,上面的by1在jdk6中放在永久代的静态变量中,在jdk7及以上放在堆空间中
by2是放在栈中
方法区的垃圾回收
java虚拟机规范中对方法区的约束非常宽松,提到可以不要求虚拟机在方法区中进行垃圾收集,比如ZGC收集器就不进行类卸载。
一般来说,这个区域的回收效果比较难以令人满意,尤其时类卸载,条件相当苛刻,但是这个区域的回收是很有必要的
方法区的垃圾收集主要回收两部分内容,常量池中废弃的常量和不再使用的类型
判定一个类是否废弃的三个条件
- 该类和该类子类的实例全都已经被回收
- 该类的类加载器已经被回收
- 该类的class对象,就是类本身没有任何引用,比如类变量的引用
当三个条件都满足时,虚拟机才可能回收,和堆中的对象不一样,堆中的对象没有引用是一定被回收
对象实例化的方式
- new对象包括单例模式的getInstance()静态方法,静态工厂模式的xxxFactory.get();
- Class的newInstance();反射的方式,不建议使用因为只能调用空参的构造器,权限必须是public
- Constructor的newInstance(xxx);反射的方式,可以调用无参也可以调用有参的构造器权限没有要求
- 使用clone();不调用任何构造器,但是当前的类需要实现Cloneable接口,实现clone
- 使用反序列化:从文件或者网络直接获取对象
- 第三方库Objenesis
创建对象的步骤
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kA4Py0jS-1615038451881)(C:\Users\东瀛初代目带佐\AppData\Roaming\Typora\typora-user-images\1597244732826.png)]
堆中对象的组成部分
对象访问定位
对象访问的主要方式有两种,句柄访问,直接指针(hotspot)
句柄访问
直接指针就是Hotspot的默认访问方式,即栈中存放对象的直接地址,对象的类型指针又指向方法区的元类型,
直接指针的效率更高
直接内存
不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域
直接内存是Java堆外的,直接向系统空间申请的一块内存区域
执行引擎
解释器和JIT编译器
解释器:当java虚拟机启动时,会根据预定义的规范对字节码采用逐行解释的方法执行,将每条字节码文件中的内容翻译为对应的本地机器指令执行
JIT编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言
-
编译执行:顾名思义,先编译再执行,所以就需要编译器现将代码编译成机器码,然后在进行执行,因为是整体编译,所以会产生编译后的机器码文件
-
解释执行:解释器会将代码一句一句的进行解释,每解释一句就运行一句,在这个过程中,不会产生机器码文件
解释运行优缺点分析
-
从启动效率来看解释运行不需要进行编译操作,所以启动效率更快
-
从运行效率角度来看,编译运行只需要编译一次,而解释运行每次都需要经过解释的过程,所以编译执行的效率更高
-
从内存角度来看,编译执行需要生成编译后的字节码文件,而解释运行则不需要
编译器
热点代码及探测方式
一个被多次调用的方法,或者是一个方法体内循环次数过多的循环体都可以被称为热点代码。
JIT编译器会把热点代码编译为本地机器指令存在内存中。
热点代码的查找,目前Hotspot的热点探测方式是基于计数器的热点探测,即为每个方法建立两个不同类型的计数器,分别为方法调用计数器(统计方法的调用次数),回边计数器(统计循环体的执行次数)
方法调用计数器
设置Hotspot虚拟机执行方式
String
jdk8底层用char型数组存放,到了8以后改用byte[]存放,主要原因是因为char是占用两个字节,而一些编码方式只用一个字节就足够,比如ISO-8859-1等,这样会造成空间的浪费,改为用字节数组存放可以节省空间
String类型的常量池比较特殊,主要使用方法有两种:
-
直接使用双引号声明出来的对象会直接存储在常量池中
String s=“qwe";
如果不是用双引号声明的String对象,可以直接用String声明的intern()方法
字符串的拼接
- 常量与常量的拼接结果在常量池,原理是编译期优化(字符串对象默认为常量比如"123" ”qwr“)
- 常量池中不会存在相同内容的常量
- 只要其中有一个是变量,结果就在堆中。变量的拼接原理是StringBuilder
- 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入常量池,并返回对象地址
String a = "123";
如上定义的为常量;
String b = a +"456";
如上 b 为变量,为啥? 难道常量拼接常量得到的是变量吗?
不是,常量拼接常量得到的依旧是常量。
但是此时将 a 作为引用,a 已经不再是常量了,是变量了,所以得到的 b 自然就是变量。
String b = "123" + "456";
此时 b 为常量。
如果给 a 加上修饰符 final ,那么 a 就是个常量,那么 b 就为常量了
final String a = "123";
String b=a+"456";
此时等号两边都是常量或者常量引用,所以b也是常量
append和字符串”+“
StringBuilder的append效率远高与字符串的”+“操作
String s = "hello";
s = s + "java";
这个操作相当于新建一个StringBuilder 然后 append("hello") 再 append("java") , 最后把值再通过StringBuilder的toString方法返回给s 也就是创建了两个对象,一个StringBuilder,一个String
StringBuilder s="hello";
s=s.append("java");
只创建了一个对象,效率更高;
另外,StringBuilder最初是16长度,会随着内容而扩容,所以在知道长度的情况下,建议使用构造器指定长度,避免重复扩容
intern()
String str=new String("a")+new String("b");//创建了几个对象?
/*
1.new String("a")
2.常量池中的”a"
3.new Stirng("b")
4.常量池中的“b"
5.因为是变量之间的相加,因为是new出来的所以是变量,变量之间的”+“运算底层调用StringBuilder
6.最后StringBuilder底层创建了一个new String(),把相加的结果返回,也就是创建了一个new String(char[ab])
这也印证了之前的结论,只要字符串拼接,有一个是变量,那么这个字符串就不会被存入字符串常量池中,而是堆中
*/
离谱的题目
总结
关于String创建在内存中产生情况大致分为三种
-
第一种只在堆中创建对象而在常量池中不创建对象,其实就是变量不会放入常量池
char [] a={'1','1'}; String s=new String(a);//借助了数组,也是字符串拼接中一方为变量时StringBuilder toString()方法不会在常量池中放入字符串常量的底层原理 String s=new Stirng("1")+new Stirng("1");//运用了StringBuilder toString()方法
如果在此种情况下调用了intern()方法那么在jdk7以前,需要在方法区的常量池中创建一个字符串常量
char [] a={'1','1'}; String s=new String(a); s.intern();//此时会在方法区中的常量池中创建一个11的字符串常量 String ss="11"; s==ss//false
如果是jdk7及之后的版本,因为字符串常量池已经被转移到堆中,现在已经在堆中创建了字符串11的对象,如果再在常量池中创建一个,会浪费空间,所以设计为此时调用intern()方法时,常量池中放的是指向堆中此对象的地址,但是只有在调用intern()方法时会采取这种节省空间的策略,如果是直接创建了一个常量,那么依然会在常量池中创建一个新的常量而不会使用堆中的地址。
char [] a={'1','1'}; String s=new String(a); s.intern();//此时会在常量池中创建一个指向堆中”11“对象的地址 String ss="11";//这时新创建的字符串常量用的就是之前堆中的”11“对象,因为常量池指向的就是它的地址 s==ss;//true char [] a={'1','1'}; String s=new String(a); String ss="11";//会在常量池中重新创建一个”11“,不会使用上面的策略,即现在堆中和常量池中都有”11“ s==ss;//false
-
第二种在堆区中创建对象引用,也在常量池中创建常量
String s=new String("11");//在常量池中放入了11,并且在堆中创建了对象指向11
-
第三种 只在常量池中创建字符串常量
String s="11";//在常量池中创建了11
垃圾收集
垃圾是指程序中没有任何指针指向的对象
相关算法
- 标记阶段:引用计数算法
- 标记阶段:可达性分析算法
- 清除阶段:标记-清除算法
- 复制算法
- 标记-压缩算法
- 分代收集算法
- 增量收集算法,分区算法
引用计数算法(python)
目的:判断对象是否存活,即一个对象没有任何引用指向
对每个对象保存一个整型的引用计数器,记录对象被引用的数量,当引用计数器为0时,就是此对象没有引用指向它,即是垃圾
优点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延时4性
缺点:需要单独的空间存储计数器,增加了空间的开销
每次赋值都需要更新计数器,时间开销也增加
无法处理循环引用,导致java没有使用这个算法
可达性分析算法(java c#)
相对于引用计数算法,可达性分析算法同样具备简单和高效的特点,而且可以解决循环引用避免内存泄漏
这种类型的垃圾收集器也被称为追踪性垃圾收集器
- 可达性分析算法是以根对象集合(GC ROOTS)为起始点,按照从下至上的方式搜索被根对象所链接的目标 对象是否可达
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或者间接连接着,搜索走过的路径被称为引用链
- 如果目标对象没有任何引用链,则意味着该对象为垃圾
- 在可达性分析算法中,只有能够被根对象直接或者间接连接的对象才是存活对象
GC ROOTS 类型
- 虚拟机栈中引用的对象
- 本地方法栈内引用的对象
- 方法区中类静态引用的对象
- 方法区中常量引用的对象
- 所有被同步锁所持有的对象
- 虚拟机内部的引用
- 基本数据类型对应的Class对象,常驻异常对象NullPointException等
- 反应虚拟机内部情况的JMXBean,JVMTI中的注册回调,本地代码缓存等
对象的finalization机制
finalize()不应该被主动调用,应该由gc进行调用,而且只会被调用一次。
对象的回收至少要经历两次标记
- 判断对象是否有引用链
- 判断finalize() 方法的状态
- 没有重写,或者之前已经被虚拟机调用过,那么为不可触及状态,可以回收
- 如果重写了,并且未被执行,那么虚拟机会执行这个方法,如果这个方法里使这个对象和任何一个引用链建立了关系,那么它将复活,即移除待回收区域
对象的三种状态
- 可触及的:从根节点开始,可以到达该对象,即有引用链
- 可复活的:对象的所有引用都已经释放,但是对象可能在finalize()中复活
- 不可触及的:对象的finalize()被调用但是没有复活,那么就会进入不可触及的状态,只有此状态的对象可以被回收