总结:
1、静态代码块+static变量在链接-准备中赋0值,在初始化clinit()中赋初始值
2、final static变量在链接-准备极端就通过constantvalue标注,进行赋值了
3、非静态变量 跟随实例对象创建过程中赋值,通过init()初始化
双亲委派
拓展类加载器 --> 启动类加载器
当父类没有加载器,才让子类提供加载器。
启动类中都是核心API,则不会加载自定义的string
并且在自定义的string中写main()方法,也不会执行,由于类加载双亲委派给了启动类,启动类中string类没有main()方法,找不到main()方法。
举例子:加载第三方API接口
SPI接口由引导类加载器加载,接口实现类由反向委派给系统加载器加载,通过ContextClassLoader获取加载器 进行加载。
双亲委派优势:
避免了类的重复加载(上交给父类加载,父类加载后,子类就不会加载)
保护程序,防止核心API被篡改。
沙箱安全机制
保护Java核心源代码的安全
运行时数据区
1、方法区、2、堆 ------ 一个进程对应
(大部分的JVM垃圾回收都在堆,小部分在方法区(永久代、元空间、堆外内存))
3、程序计数器、4、本地方法栈、5、虚拟机栈 ------ 一个线程线程对应一个
方法区和堆是公用的,程序计数器、虚拟机栈、本地方法栈 一个线程对应一个
如果线程只剩下守护线程,JVM可以关闭
PC寄存器
用来存储指向下一条指令的地址,即将要执行的代码,由执行引擎读取下一条指令
OutOfMemoryError
PC寄存器和栈,不会有GC垃圾回收,但栈可能出现溢出,PC寄存器则不会出现OOM溢出
并行:多线程执行
并发:看着像是并行,实际是切换着执行
虚拟机栈
栈桢内部结构
内存中的栈和堆 (栈管运行,堆管存储)
Java虚拟机栈是什么
Java虚拟机栈,早期也叫Java栈,每个线程创建时都会创建一个虚拟机栈,内部保存一个个栈帧,对应着一次次的Java方法调用
一个线程创建时,就创建一个虚拟机栈,内部有多个栈桢,一个栈桢对应一个方法调用
生命周期 = 线程生命周期
作用
优点
栈中可能出现的异常
超过了整个内存空间,OOM
StackOverflowError
调整虚拟机栈大小
设置线程的最大栈空间
https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE 官网
栈存储单位 栈桢(stack Frame)--虚拟机栈
方法和栈桢是对应关系,一个方法执行,一个栈桢入栈
一个活动线程中,只有一个活动栈桢,即当前栈桢=当前方法,当前类
执行引擎运行的字节码指令,只针对当前栈桢
寄存器存放的也是当前栈桢的字节码指令的操作地址
栈运行原理
1、正常执行return
2、抛出异常
对out内的class反编译 javap -v xxx.class (-p显示私有方法)
方法返回值为int、double 反编译后,为iretrun dreturn
栈桢结构
局部变量表+操作数栈 大小主要占据了栈桢的大小
局部变量表
栈 > 栈帧 > 局部变量表 > 局部变量
使用idea jclasslib查看字节码文件
重新编译Recompile 在插件jclasslib中查看
最基本的存储单元 slot
如果一个局部变量占两个slot,访问起始索引。
当前帧是由构造方法和非静态方法(实例方法)创建的 ,那么对象应用this 会存放在index为0的slot处
非静态的局部变量名索引为0处存放的 this
this是需要放入局部变量表中(静态的方法没有this)
Slot 重复利用
b变量在c创建之前就销毁了,c就重复利用了b的槽位
静态变量和局部变量对比
操作数栈 (oprand stack)
栈:数组结构或链表结构 (操作数栈是用数组结构实现的,但不能用索引访问,需要入栈出栈)
局部变量是存在局部变量表中,当作云算时移到操作数栈中临时存储,由执行引擎处理后再返回给局部变量表
javap反编译后得到的 stack------- 为 操作数栈深度 locals --------为局部变量表的大小
push、load_1、load_2、add为操作操作数栈;
push:将数值入操作数栈
load_n:将局部变量表中的索引n处的值,加载到操作数栈中
store_1、store_2、store_3为保存在局部变量表中
15 入操作数栈,然后存入局部变量表
局部变量表从1开始,0为this
8入操作数栈,然后存入局部变量表
将局部变量表 的 1、2 处 的 15 、8取出放入 操作数栈
iadd 由执行引擎 将字节码指令转为机器指令后得到23,将23入操作数栈,然后存入局部变量表
局部变量表最后空间为4,操作数栈深度2
push、load_1、load_2、add为操作操作数栈;
push:将数值入操作数栈
load_n:将局部变量表中的索引n处的值,加载到操作数栈中
store_1、store_2、store_3为保存在局部变量表中
aload_0表示从局部变量表中加载this到操作数栈中,
invokevirtual 然后invokevirtual执行this变量的getSum方法,得到返回值存入操作数栈中
istore_1 表示将操作数栈的返回值保存到局部变量表中
栈顶缓存技术(Tos Top-of-stack cashing)(还需测试)
寄存器:指令少,执行速度更快
动态链接(指向运行时常量池的方法引用)
每一个栈桢都有一个 指向运行时常量池的引用方法
字节码文件中的常量池
每个栈帧中动态链接到运行常量池中的方法引用,方便方法调用,不用每个栈帧都直接进行方法引用占用空间。
一个类生成字节码文件,需要的类、变量、方法调用,不能全部放入字节码文件(太大),需要通过符号引用的方式标明,从常量池中取出。
常量池的作用,提供一些符号和常量,便于指令的识别。
常量池可以通过javap -v反编译字节码查看,常量池再字节码文件中,运行时常量池在运行时的方法区中。
对动态链接的理解 https://www.cnblogs.com/tommaoxiaoqi/p/13063340.html
简单来说就是
1.方法区中字节码已经有直接引用为什么还要在栈帧里放?
Child类继承Father类,并重写doSomething方法:
在静态解析时候,看到变量father和child的静态类型都是Father,所以,doSomething方法都解析成Father类里的方法。而child变量的实际类型是Child,应该调用Child类里的doSomething方法,因此方法区里静态解析式不完全正确,要在运行期间栈帧进栈的时候动态连接到真实的类和方法。
2. 已经创建了栈帧,还存一个指向自己的地址多余吗?
动态连接发生在栈帧完全入栈之前,也在局部变量表等形成之前。
以Test类最后一条语句child.doSomething为例。执行到该条语句之时,找到doSomething的直接引用,即在方法区里的地址。形成了指向Child类doSomething方法的动态连接,找到了该方法的入口,也就找到了对应得字节码指令,有了局部变量表和操作数栈的出入栈,然后形成了doSomething这个方法的栈帧。字节码指令指行完后,doSomething方法的栈帧出栈,根据返回地址,返回到test方法继续执行。
动态连接是找到正确方法的入口,然后才有后来的进栈出栈的执行。
3. 静态多分派
由1中的例子可知,编译期是根据对象声明的类型来选择方法。调用过程中首先确定调用者的静态类型是什么,即child的静态类型Father,然后根据要调用的方法参数的静态类型(声明类型)确定所有重载方法中要调用哪一个(方法重载)。两者共同决定所以是多分派。
方法的调用
符号引用:
直接引用:执行时,需要把符号引用对应到直接引用上,去常量池中找到指定的位置
测试方法中参数为父类或接口,编译后,调用方法时,未指定具体的子类方法或具体的实现类方法。即为晚期绑定
晚期绑定:编译期间不确定的方法绑定,运行期才确定。
一个是invokevirtual 一个是invokeinterface
早期绑定:编译期间就确定了的方法绑定。运行期间不变
虚方法和非虚方法(编译期间就确定具体调用的方法,不能被重写的方法)
例子:
父类:构造、静态、final、普通方法
子类:super构造、this有参构造、父类同名的静态、私有方法
invokestatic、invokespecial 非虚方法 invokevirtual中final非虚方法,其他全是虚方法
静态动态语言区别:对类型的检查在编译期还是运行期
方法重写的本质
虚方法表
重写了就指向当前类,否则就会指向父类的父类。
方法返回地址
存放调用该方法的PC寄存器的值,PC寄存器存放的是方法执行完后要执行的下一条指令的值,之后交给执行引擎
异常处理表----方法的class文件中
出现异常在字节码4-8中,按11行处理
栈的面试题
栈溢出的情况:
1、StackOverflowError 栈内存超过了固定值------通过-Xss设置栈的大小
2、OOM 当动态设置栈的大小时,超过了系统内存,报错
stringbuffer 线程安全 有synchronized
stringbuilder 线程不安全
1、方法内的stringbuilder是安全的:因为stringbuilder在方法中,只有当线程来调用方法才会被操作,所以只会有一个线程操作该数据。(当前方法栈帧压入线程栈中,不能被其他线程调用),并且stringbuilder在方法内产生,在的方法内消亡。不会对其他线程造成不安全。
2、成员变量被多个线程调用不安全,主线程和从线程都调用
3、方法参数stringbuilder不安全,stringbuilder会被多个线程操作,不止属于该方法。
4、方法内部stringbuilder,但将其作为返回值,是不安全的。虽然stringbuilder在方法内部,但作为返回值,会造成其他操作该值的线程不安全。
5、方法内部stringbuilder,但将tostring作为返回值,是安全的,因为stringbuilder在方法内消亡了,返回的是tostring,因为tostring是new的string对象,string是安全的
--->
本地方法接口
java.lang.object
java.lang.Thread
本地方法栈
方法区:JRocket J9没有 ; java8后hotspot也没有方法区了,考虑使用元空间存储类的整体结构
堆
一个进程对应一个JVM实例,一个JVM实例中有一个运行时数据区,其中只有一个方法区一个堆,多个线程共享堆空间、方法区
设置不同的连个程序的堆内存
jdk bin下jvisualvm
堆空间划分
eden伊甸园区
1.7和1.8之间的区别:主要是 永久区变为元空间
-XX: +PringGCDetails :显示垃圾回收细节
其中from 和 to区只用一个,另一个为空(算新生代总量total也只用了一个)
jps 查看内存进程情况 jstat -gc 进程号 查看进程gc情况
堆空间溢出异常OutOfMemoryError
新生代和老年代
young:old 1:2
Eden:From:To 8:1:1
实际6:1:1,因为有自适应
-XX:-UseAdaptiveSizePolicy:-表示关闭自适应,实际没有用。直接用Ratio分配即可 (Parallel垃圾收集器的参数)
通过设置SurvivorRatio = 8来达到eden:from:to = 8:1:1
-XX:SuriviorRatio默认值为8,但测试的时候为6:1:1,需要重新设置一次
-XX:NewRatio默认2,--- 新:老 1:2
查询SurvivorRatio的值:jinfo -flag SurvivorRatio 进程号
设置新生代最大内存 (一般不设置)
-Xmn:洗面奶,设置新生代最大内存大小
如果同时设置了新生代比例与此参数冲突,则以此参数为准。
(600m的新生代内存,设置-XX:NewRatio为2:200:400和设置-Xmn为100:100:500,最后以-Xmn为准:100:500)
对象分配过程
1、eden满的时候,触发Minor垃圾回收(年轻代垃圾回收YGC),还有用的进入幸存者0区
2、第二次eden满的时候,触发Minor垃圾回收(survivor进行被动回收),并将s0剩余的进入幸存者1区,age+1.
3、当某次触发垃圾回收时,幸存者区的age达到阈值,进入老年代区
survivor区放不下,不会触发垃圾回收:放不下就直接进入老年代区
老年代放不下,进fullGC(majorGC)
对象分配特殊情况:
元空间进行类加载
常用的调优工具:
javap jps jmap jinfo
按内存回收区域分
垃圾回收:Minor GC、Major GC、Full GC
部分收集: Minor Major Mixed GC
整堆收集: Full GC
GC线程:用户线程:STW:stop the world
STW 会暂停其他用户的线程,等垃圾回收结束,用户线程才回复运行
方法区的回收一般是,不用的类和加载器
尽量避免fullGC 和 Major GC
GC日志分析
为什么分代?
内存分配策略
1、一般策略:
2、不同年龄段的对象分配策略:
动态对象年龄判断:同一年龄的所有对象大小综合的大于survivor0+survivr1的一半,就讲大于或等于该年龄的对象直接进入老年代。
-----------该年龄的对象太多,在survivor复制过去复制过来,增大消耗。
前端没有GC日志,说明没有进行垃圾回收
对象分配过程:TLAB (Thread Local Allcation Buffer) :在Eden区为每个线程分配一个独立的内存空间
查看是否开启TLAB
jvm测试堆空间常用参数
jdk7后不再用HandlePromotionFailure参数进行空间担保,-------只要老年代连续剩余大小大于生新生代对象总大小或历次晋升平均大小,就会进行Minor GC,否则进行Full GC
堆外分配
逃逸分析
使用逃逸分析,编译器对代码优化
一、栈上分配
测试栈上分配 (默认开启)
1、Xms 1G Xmx 1G 开与不开 逃逸分析(未逃逸的对象,栈上分配)
2、Xms 256m Xmx 256m 开与不开 逃逸分析(未逃逸的对象,栈上分配)
不开逃逸分析,对象分配在堆中,会出现GC垃圾回收
开启逃逸分析,未逃逸的对象分配在栈中,不会出现GC
--------------
---------------
注意:
1、对象未逃逸 + 标量替换 = 栈上分配 ,此处开启了逃逸分析,默认也开启了标量替换,才能栈上分配。
并且User对象没有属性,就不需要标量替换(没有给出来而已)
2、如果开启逃逸分析,但关闭标量替换,则还是会在队中分配
二、同步省略 (锁消除)
同步省略、栈上分配 是字节码文件加载到内存以后,才进行的执行判断,在运行时才考虑去掉
三、分离对象或标量替换
分离对象或标量替换例子:
注意:
1、对象未逃逸 + 标量替换 = 栈上分配 ,此处开启了逃逸分析,默认也开启了标量替换,才能栈上分配。
并且User对象没有属性,就不需要标量替换(没有给出来而已)
2、如果开启逃逸分析,但关闭标量替换,则还是会在队中分配
逃逸分析是在服务端能开启
逃逸分析并不成熟
堆小结:
方法区
方法区、栈、堆的交互关系
如果person在方法中创建
当创建对象new在方法中没有逃逸,则person在方法局部变量表中---栈上分配
如果person在类中创建
则为成员变量,随该类一起存放在堆中
设置方法区(元空间)大小
元空间内存溢出OOM
解决OOM
方法区结构
方法区存储的类型信息
常量池再字节码文件中
打开字节码文件
jclasslib
1、加载的类型信息
2、域信息
3、方法信息
非静态方法有一个this对象参数 args_size = 1
静态方法没有this对象参数
异常表
字节码文件没有 ClassLoader 当字节码文件加载到内存中,方法区内就会记录有该字节码文件被加载的加载器名
另外ClassLoader加载到方法区也会记录加载过谁
4、non-final
5、全局常量(static final)
对字节码文件 open in terminal
final修饰的全局常量,在字节码编译过程中就被赋值了2
静态的变量,在加载的 链接 的 准备环节才进行默认初始化0,在 初始化阶段才被赋值为1 (方法赋值)
运行时常量池 VS 常量池
运行时 ,将常量池加载到方法区 ,就成了运行时常量池
常量池
6、运行时常量池
在常量池中方法或字段引用,指向常量池中的符号地址,在运行期解析后,进入运行时常量池中,转换为真实地址。
方法区的使用
字节码指令在常量池中找
方法区的演变
jdk7时,将静态变量和字符串常量池放入堆空间中
jdk8将元空间用在本地内存上,静态变量和字符串常量池还在堆空间中
1.8 元空间(方法区)使用直接内存(类型信息、域信息、方法信息、JIT代码缓存、运行时常量池)静态变量+字符串常量池还在堆中(方便GC,之前只有FullGC,但触发频率低)
直接内存(NIO Channl和buffer的IO方式,native函数库直分配堆外外内存,存储在java堆的DirectByteBuffer对象中,作为这一块内存的引用------显著提高性能---避免了堆和直接内存切换)
为什么元空间 替代 永久区
1、永久代设置空间大小难确定,容易产生OOM
2、对永久区调优很困难(full GC消耗大,方法区垃圾回收,类的回收检验复杂,消耗大)
永久代触发GC的概率很低,只有full GC才回收,开发中大量字符串被创建,但回收效率低,会导致永久代内存不足,放到堆里能及时回收。
jdk1.6 1.7 字节数组对象放在老年代
静态变量对象,和静态变量放在哪
staticObj 是 类的静态成员变量 ------------ 存在方法区(jdk7后在堆中)(静态成员变量)(jdk7后 和StringTable在堆中)
instanceObj 是 类的非静态成员变量(实例变量) ----------- 存在堆内(跟随Test类实例对象) 跟栈上分配区别开
localObj 是 方法内局部变量 ------------ 方法栈桢局部变量表内
如果person在方法中创建
当创建对象new在方法中没有逃逸,则person在方法局部变量表中---栈上分配
如果person在类中创建
则为成员变量,随该类一起存放在堆中
总结:
成员变量:
1、静态成员变量:在方法区
2、非静态成员变量:跟随对象在堆中
方法中的new对象: P p = new P();
1、如果没有逃逸:p在方法栈桢的局部变量表中。并且对象被标量替换打散为变量。
方法区垃圾回收有必要吗(可回收也可不回收)
方法区堆类的卸载很苛刻
方法区的回收:1、常量池中废弃的常量
2、类的实例被回收+类加载器被回收+该类.class未被任何地方调用------该类信息才允许被回收
方法区中类信息被大量使用于反射、动态代理等字节码框架,通常需要对方法区进行类卸载
总结
运行时数据区
虚拟机对象
类对象创建过程:
1.类加载检查 2.分配内存 3.初始化零值 4.设置对象头 5.执行Init()方法
1、创建对象所属的类
如果堆内存规整,使用指针碰撞分配内存
如果堆内存不规整,使用空闲列表记录空闲内存
2、内存当前有所属对象的类,再进行内存分配
标记压缩整理算法
标记清除算法---CMS(并发垃圾收集器)---空闲列表
一般不会发生分配内存并发,因为有每个线程都有TLAB,只有用完后,才开始需要同步锁定
-----------给对象属性赋值默认初始化值(用来TLAB,就在TLAB分配内存前初始化零值)
----------堆空间new出来的对象都是有对象头的
init()构造器对应的方法
clinit()构造器方法
java层面的初始化
Person p = new Person(); new 只是创建对象,Person()才是完成整的创建对象及初始化。
对象的内存布局
对象头
对象的访问定位 (句柄 、直接指针)
句柄方式
直接指针(hotspot采用)
直接内存
IO、NIO:非阻塞式IO
直接对进程进行内存分配
ByteBuffer.allocateDirect(size);
当开启进程,会通过堆中的DirectByteBuffer对象,直接从内存中划分指定大小。
优点:访问内存的速度比访问JAVA堆快;NIO库支持java使用直接内存
直接内存OOM
缺点:分配回收成本较高;不受JVM内存回收管理
设置直接内存的大小
可以设置直接内存大小 -XX:MaxDirectMemorySize
进程中的内存空间 = 堆空间 + 本地内存(元数据区+直接内存)
执行引擎
解释器、即时编译器、GC
HotSpot 执行引擎模拟
.java代码编译成.class字节码文件---前端编译器
.class字节码编译为机器语言---后端编译器(即时编译器+解释器)
.java代码直接便以为本地机器代码---静态提前编译器(AOT)
字节码:跨平台的通用契约(CAFE BABY开头)
执行过程
从PC寄存器中取出指令地址,找到对应的字节码指令 执行
JIT能在方法区缓存代码
nodepad++插件,查看字节码文件
现状
jconsole工具
解释器
JIT编译器
为什么同时用解释器 和 即时编译器
触发JIT编译执行的标准
HotSpot 热点探测方式:基于计数器的热点探测 = 方法调用计数器 + 回边计数器 (真对多次调用的方法 和 多次调用的方法内循环体)
方法调用计数器
-XX:-UseCounterDecay 关闭热度衰减
-XX:CounterHalfLigeTime 设置衰减周期时间
热度衰减:长时间未达到衰减阈值,计数器值减半(防止系统运行足够长时间,大部分都被编译成本地代码)
设置程序执行方式 (解释执行、JIT编译执行、混合执行)
默认混合执行
改为解释执行 Java -Xint -version
改为编译执行 Java -Xcomp -version
测试 执行模式切换
类似goto
结论:混合模式最好
HotSpot JIT分类
64 bit 默认 -server
StringTable
针对某个模块修改jdk依赖
jdk8及之前String底层使用的char数组;jdk8以后,String底层都是用的byte数组
jdk8--------
jdk11--------
修改动机
String的不可变性
笔试题:
替换String内容 和 替换char数组内容 (证明String不可变性)
ex.str为 ”good“指向的成员变量,
而ex.change(ex.str,ex.ch); ===》 String str = ex.str = "good"(此str为change方法内部变量,方法结束就销毁),此时”good“也指向该str
而该方法内的str并不是成员变量,即使做了str = "test ok",也指向给该方法局部变量新开空间”test ok“,原本的成员变量str并没有改变
想要改变,则需在方法内直接给成员变量赋值,即新开空间”test ok“也指向成员变量
jdk6中StringTableSize = 1009
查看进程的StringTableSize
设置StringTableSize
jdk7后 StringTableSize = 60013
jdk8时 StringTableSize 最小值为 1009
String的内存分配
创建String对象使用字符串常量池
jdk7及之后字符串常量池在堆中
由于在堆中,调优时仅需调整堆大小
1、永久代设置空间大小难确定,容易产生OOM
2、对永久区调优很困难(full GC消耗大,方法区垃圾回收,类的回收检验复杂,消耗大)
测试付出常量池在堆中
String的基本操作
idea debug看内存情况
后面的字符串不会被加载,内存中不会增加新的,只会指向
方法调用中参数传递过程
tostring会创建新string对象在常量池中
字符串拼接
1、常量与常量拼接
从常量池中取的就是"abc"
2、拼接中有一个是变量,结果在堆中(常量池外),原理StringBuilder (线程不安全)
变量字符串拼接 底层原理
3、加了final修饰的变量,变为常量引用,用于字符串拼接,不会使用StringBuilder
总结:
1、字符串常量拼接,还是同一个地址,在编译期就优化了
2、字符串变量拼接,使用stringbuilder创建新的string对象,地址不同
3、final修饰的字符串为常量,当字符串常量或常量引用拼接,还是使用编译器优化,而非stringbuilder
String字符串拼接 和 StringBuilder的append() 添加字符串 执行效率
直接用字符串变量进行字符串拼接,会导致每次拼接都创建一个StringBuilder和String对象(底层还是append) 内存占用大,GC会花额外的时间
使用StringBuilder对象的append方法进行拼接,只会创建一次StringBuilder
new StringBuilder底层是一个char[16] 长度16,当存满,就需要扩容(创建更大的数组,将原本的复制进新的数组)
改进空间:
在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highLevel的情况下,建议使用构造器实例化:
StringBuilder s = new StringBuilder(highLevel); //new char[highLevel] -------- 直接设置char[]数组的大小,后续不需要新建、复制数组
intern()
调用equals判断字符串常量池中是否有相同,没有则在常量池中生成
字符串String面试题1
对于 String str = new string("ab"),1、new的String对象;2、字符串常量池中”ab“对象
对于string str = new string("a")+new string("b"),
1、new 的stringbuilder对象;2、string对象;3、字符串常量池中”a“对象;4、string对象;5、字符串常量池中”b“对象;6、stringbuilder.tostring()中的string对象
关键:string str = new string("a")+new string("b") ------ 字符串常量池中不会出现”ab“---------因为"ab"是stringbuilder通过char[]数组传递给new string(char[])的。
Tostring字节码中也只有char[] value
含变量的字符串拼接,使用的是stringbuilder,在最后会调用stringbuilder的Tostring方法,经stringbuilder中的字符传入char数组
面试题2
new string("1");会在队中创建包含”1“的string对象,并在字符串常量池中生成”1“
此时s3是指向的堆中”11“的地址,s4指向堆中的”11“,即s3s4都是指向堆中"11"对象的地址,所以s3==s4
字符串常量池中存放堆中创建的new string("ab")的地址
对于s、s2
结论:
1、
string str = new string("ab") ---------- 在堆中创建”ab“对象,也在字符串常量池中创建”ab“对象
string str = new string("a") + new string("b") ----------- 在堆中创建”ab“对象,在字符串常量池中没有创建”ab“对象,”ab“存在char数组中
而如果此时调用str.intern则会在字符串常量池中创建一个指向堆中有的”ab“的地址
2、
s.intern():在字符串常量池中存在s的字符串,返回true,不做任何操作
在字符串常量池中不存在s的字符串,返回false,jdk6中-----直接在字符串常量池中生成一个新的s字符串对象
jdk7、8中------在字符串常量池中创建一个指向堆中new string(s)的地址(对象的引用地址)
正常情况下出现 堆中有对象,字符串常量池中没有对象的情况 只有在 ----含变量字符串拼接 导致
+拼接符: 字符串常量拼接,就存在常量池中
字符串变量拼接,使用stringbuilder创建对象,并tostring创建string对象,在堆中存在拼接后的字符串对象,常量池中没有,此时如果调用intern,会在常量池中生成对象
intern()效率测试
创建字符串对象时,后面跟上intern()可以减少内存空间
StringTable的垃圾回收
开启StringTable垃圾回收
-XX:+PrintStringTableStatistice
了解
垃圾回收
C++内存需要自己分配,指向也是真实的地址,C++没有垃圾收集技术 Java = (C++)--
为什么要进行GC
GC作用区域(方法区、堆)
垃圾回收算法 (什么是垃圾? 怎么回收?)
垃圾标记 + 垃圾清除
标记阶段:引用计数算法 (解决:手动解除 / 使用软引用 )
(Java没使用---无法处理循环引用的情况)
跟survivor的计数器age不一样
证明Java未使用引用计数算法的例子:
使用GC会进行回收
解决引用计数缺点:循环引用使用 1、手动解除 2、软引用
标记阶段:可达性分析算法 --- 能解决循环引用的问题
(根搜索算法、追踪性垃圾收集算法)
以根对象为起始点,按照从上到下的方式搜索根对象集合锁链接的目标对象是否可达
哪些能作为GC roots
临时GC roots : 当对新生代垃圾回收时,非新生代的区域都刻考虑作为GC roots
判断GC rootsd 的小技巧
注意:可达性分析算法,需要保证一致性的快照内进行
STW(stop the world),CMS收集器枚举根节点也必须要停顿
finalization机制(一般较少重写)
永远不会主动调用finalize()方法,应该交给垃圾回收机制调用,需要在对象被回收之前作什么,就重写finalize()一般较少重写
虚拟机的对象可能的三种状态(可触及的、可复活的、不可触及的)
只有进入了finalize()方法,并在方法内与引用链建立联系,才能不被移除
例子:测试Object类中finalize()方法,即对象的finalization机制
当前对象的静态变量可以作为GC roots,创建当前对象指向给静态变量,再取消指向(静态变量为null),进行GC,期间执行finalize(),将当前对象指向静态变量(救活该对象)
再取消指向(静态变量为null),进行GC
jvisualvm 查看程序的Finalizer线程
MAT和jprofiler GC roots溯源
1、jmap获取dump文件
jmap -dunp:format -b,live,file-test1.bin 14036
jmap -dunp:format -b,live,file-test2.bin 14036
2、jvisualvm生成Dump文件
mat打开Dump文件,并查看GC roots
-------》得到可能成为GC roots的对象
可以看到,Dump1主线程中有参数,在Dump2中由于参数赋值null,就没有参数了。
jprofiler GC roots溯源
标记当前值,可以动态看到当前内存值的变化
针对某一个类型,进行show selection in heap walker,就可以进行单独查看 分配、对象、引用的情况(References用的比较多)
Outgoing references定位代码的位置,Incoming references判断和那个GC roots相关联的
jprofiler分析OO
查看当前程序的对象,看到出现超大对象
线程查看,出错线程的出错位置
垃圾收集算法
清除阶段:标记-清除算法(Mark-Sweep)--- 较为简单的算法
当堆内存被耗尽才进行标记清除,并STW
从根节点开始,标记有被引用的对象,对堆内存线性遍历,没有标记的对象清除。
在为对象分配内存时,内存不连续,需要创建一个空闲列表记录空闲内存
清除:并不是真的清除,而是把需要清除的对象地址保存在空闲列表中,下次新对象进入,直接覆盖
清除阶段:复制算法(Copying)--- 复制算法建立在存活对象少,垃圾对象堆的情况下
类似survivor0、survivor1 GC算法
即如果发现对象都是存活的,那么都需要复制,非常耗时
清除阶段:标记-压缩算法 (标记-整理)(Mark-Compact)
在标记清除的基础上优化,对碎片进行整理,因此可以称为 标记-清除-压缩算法(Mark-Sweep-Compact)
分代收集算法
年轻代使用 复制算法(Copying)
老年代使用 标记清除+标记压缩 (整理)结合的方式
CMS回收器一般使用Mark-Sweep,当老年代内存回收不佳,出现Full GC时,使用Mark-Compact的 Serial Old回收器进行Full GC内存整理
增量收集算法
每次只收集一小片区域的内存空间,借还到用户进程
分区算法
垃圾回收相关概念
System.gc()
注意:
1、方法中代码块中创建对象,在作用域外进行GC,创建的对象不会进行回收
----------------------------对象不会被回收(内存的对象还指向着槽的位置,槽保留着关于buffer的数组对象引用)
即虽然出了代码块,但没有给buffer置空,相当于引用还存在,局部变量表中的slot槽还存在,该数组对象还有引用指向,所以不会被GC回收
运行后,局部变量表中只有this,但此时buffer并未被回收,因为局部变量表的slot为1的槽还好留着buffer的数组对象引用,并被内存中的对象地址指向,所以对象不会被回收。
说明buffer占用过槽(没有被下一个变量占用,槽就还会指向内存地址)
未进行回收
2、方法中代码块中创建对象,在作用域外再有变量被创建,并进行GC,代码块内创建的对象会进行回收
---------------------------对象会被回收,因为slot为1的槽被占用,数组对象引用被打断,内存的对象没有指向了,引用不存在
此时代码块后又创建了方法局部变量,即原本未被清除的buffer所占用的槽被占用了,引用被打断了,在GC就能回收了
进行了回收
局部变量表与java内存回收https://blog.csdn.net/knxw0001/article/details/9185969
内存溢出(OOM) 和 内存泄漏
内存溢出(OOM)
没有空闲内存,垃圾收集器也无法提供更多内存
内存指的是虚拟内存
内存泄漏(Memory Leak)
对象不再被程序用到,但GC不能回收
举例:
Stop The World (STW)自动发生
(类似:GC roots是不停在变的,进行可达性分析需要保证一致性快照)
CMS ZGC都还是会有STW
开发中不要使用System.gc();会导致STW发生
安全点 和 安全区
强、软、弱、虚 引用
都是可触及的
强引用:不回收
软引用:内存不足才回收 SoftReference ----------- 可作为缓存使用
高速缓存
先强引用,再软引用,再关闭强引用
也可直接传递对象建立软引用
在报OOM之前,垃圾回收器会回收软引用的可达性对象
注意:不一定报OOM才会回收软引用:
查看老年代空闲内存
放一个跟老年代空闲内存差不多大的对象,迫使新生代进行GC
老年代刚好放满,并进行过一次full GC
弱引用:发现即回收 WeakReference
虚引用:对象回收跟踪 PhantomReference ----- 可在需要GC前添加到引用队列,来判断对象是否将要进行GC,来提前做行动
一个强引用对象、一个引用队列、一个虚引用对象(强引用、引用队列)、断开强引用
终结期引用 FinalReference
垃圾回收器
串行垃圾收集器
并行垃圾收集器
按照工作模式分
按照碎片处理方式
按个工作内存区间分
切换频率多----暂停时间短----内存小----吞吐量低
切换频率少----暂停时间长----内存大----吞吐量高
频繁切换会导致无用时间增多-----------缺:吞吐量降低--------------优但暂停时间低
7款经典垃圾回收器
组合关系(jdk14)
当CMS移除后,只剩 Serial GC+Serial Old GC Parallel Scavenge GC+Parall Old GC G1
查看默认垃圾回收器
jdk8中没有使用G1
jdk9之后默认G1
垃圾回收器详情
Serial:串行
可以替换G1 为 SerialGC
ParNew:并行 (很多JVM server模式下 新生代默认垃圾收集器)
年轻代:复制算法、STW
在jdk9后就不能用了
Parallel:吞吐量优先
Parallel 和 Parallel Old在吞吐量优先的 server 模式下内存回收性能不错
jdk8中默认Parallel --- 老年代 Parallel old
jdk9中默认G1
参数设置
在jdk9中可以通过设置参数使用Parallel 和 Parallel Old
停顿时间 和 垃圾收集占比时间
= 1/(99+1)
自适应调节
CMS回收器:低延迟
初始标记---并发标记---重新标记---并发清除
为什么不用标记整理? --- 用户进程还在运行
CMS优缺点
对资源敏感,即原本并发线程,会被一个垃圾回收线程占用,导致程序变慢
参数设置
老年代设置为CMS,新生代会自动设置为ParNew
该选项能有效降低Full GC次数
对执完回收后的内存进行压缩整理
垃圾回收线程的个数
(CPU+3)/4
年轻代ParallelGCThreads 个数 默认 = CPU个数
G1区域分代化(Garbage First)
在延迟可控下,更高的吞吐量 --- 分代垃圾回收算法Region
相较于其他垃圾回收器,G1要多10~20%的额外负载
参数设置
启动G1、设置Region大小、设置最大停顿时间 STW线程数、并行垃圾收集线程数、触发GC堆占用率阈值
Region
逻辑上连续
>0.5个Region
指针碰撞
TLAB
记忆集(Remembered Set)
当进行YGC时,如果某个新生代对象被来年代对象引用,这需要全局扫描--------而在新生代设置记忆集可以直接保存老年代引用信息,YGC搜索GCroots时就会涵盖记忆集中的范围。而不必要全局搜索
GC回收过程一:年轻代
清空的eden会存入链表中linklist,空闲链表
G1回收过程二:并发标记过程 (主要针对老年代)
并发标记,如果发现区域对象全是垃圾,就不用等到混合回收,直接立即回收
再次标记STW 初始快照算法(snapshot-at-the-beginning SATB)
G1回收过程三:混合回收
G1可选回收过程四:full GC