java
2022-12-26 16:10:29 0 举报
AI智能生成
java学习笔记
作者其他创作
大纲/内容
JVM
JDK1.8=JVM内存划分
Java运行时数据区概括图
共享区域
堆
对象实例,数组
类初始化生成的对象
基本数据类型的数组也是对象实例
基本数据类型的数组也是对象实例
字符串常量池
字符串常量池原本存放于方法区,从jdk7开始放置于堆中。
字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张string table
字符串常量池存储的是string对象的直接引用,而不是直接存放的对象,是一张string table
静态变量
静态变量是有static修饰的变量,jdk7时从方法区迁移至堆中
逃逸分析技术
概述:
是目前Java虚拟机中比较前沿的优化技术,它与类型继承关系分析一
样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术
样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术
原理:
基本原理是:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部
方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;
甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;
从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度
方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;
甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;
从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度
如果能证明一个对象不会逃逸到方法或线程之外,则可能为这个对象实例采取不同程度的优化
逃逸分析为对象实例的优化策略
栈上分配
如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,
对象所占用的内存空间就可以随栈帧出栈而销毁
对象所占用的内存空间就可以随栈帧出栈而销毁
标量替换
如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,
这个过程就称为标量替换。
这个过程就称为标量替换。
同步消除
如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,
对这个变量实施的同步措施也就可以安全地消除掉
对这个变量实施的同步措施也就可以安全地消除掉
方法区
类信息
版本号
字段
方法
接口
常量池表
运行时常量池
1,运行时常量池(Runtime Constant Pool)是方法区的一部分。
2,存放<b>编译期生成的各种字面量与符号引用</b>,这部分内容将在类加载后存放到方法区的运行时常量池中。
3,运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
2,存放<b>编译期生成的各种字面量与符号引用</b>,这部分内容将在类加载后存放到方法区的运行时常量池中。
3,运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
私有区域
虚拟机栈
栈帧
局部变量表
操作数栈
动态连接
方法返回地址
本地方方法栈
本地方法栈则是为虚拟机使用到的本地(Native)方法服务
程序计数器:程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的
字节码的行号指示器
字节码的行号指示器
对象的创建
类加载的过程
类加载时机: 加载->连接(验证->准备->解析)->初始化
加载
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
连接
验证
确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,
保证这些信息被当作代码运行后不会危害虚拟机自身的安全
保证这些信息被当作代码运行后不会危害虚拟机自身的安全
准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初
始值的阶段
始值的阶段
解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
初始化
初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程 序。
对象创建步骤:1,分配内存空间2,将对象指向这个内存空间3,初始化对象
创建对象的过程可能是123,也有可能是132
创建对象的过程可能是123,也有可能是132
类加载器
启动类加载器
扩展类加载器
引用程序类加载器
双亲委派模型
双亲委派模型的工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,
每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反
馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反
馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,
并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应
用程序将会变得一片混乱
并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应
用程序将会变得一片混乱
如何破坏双亲委派模型?
子主题
Java内存模型与线程
Java内存模型
主内存
工作内存
每条线程还有自己的工作内存
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成
线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内
存中进行,而不能直接读写主内存中的数据
存中进行,而不能直接读写主内存中的数据
主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域
对于volatile型变量的特殊规则
当一个变量被定义成volatile之后,它将具备两项特性:
第一项是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,
新值对于其他线程来说是可以立即得知的
新值对于其他线程来说是可以立即得知的
禁止指令重排序优化
普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。
因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点
因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点
原子性
一组指令操作要么全部执行成功,要么全部执行失败
可见性
可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改
除了volatile之外,Java还有两个关键字能实现可见性,它们是synchronized和final
有序性
如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的
前半句是指“线程内似表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象
前半句是指“线程内似表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本
身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对
其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入
身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对
其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入
先行发生原则(Happens-Before)
概述:是判断数据是否存在竞争,线程是否安全的非常有用的手段
先行发生是Java内存模型中定义的两项操作之间的偏序关系,比如说操作A先行发生于操作B,其实就是说在发生操作B之前,
操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等
操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等
线程安全与锁优化
线程安全
因为如果根本不存在多线程,又或者一段代码根本不会与其他线程共享数据,那么
从线程安全的角度上看,程序是串行执行还是多线程执行对它来说是没有什么区别的
从线程安全的角度上看,程序是串行执行还是多线程执行对它来说是没有什么区别的
TODO
垃圾回收
如何判断对象是否是可回收?
引用计数法
概述:在对象上用一个标记字段计数,每次对象被引用就加1,
如果该标记字段为0则对象可被回收(Netty的ByteBuf就是用的引用计数法)
如果该标记字段为0则对象可被回收(Netty的ByteBuf就是用的引用计数法)
优点:引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,
但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法
但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法
缺点:单纯的引用计数就很难解决对象之间相互循环引用的问题。
可达性分析算法
概述:基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,
搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用
图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用
图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
GCRoot对象
方法区中常量引用的对象,如运行时常量池里的引用,字符串常量池里的引用
方法区中的类静态属性引用的对象,如java类的引用类型静态变量
本地方法栈中引用的对象(通常所说的Native方法)
虚拟机栈(栈帧中的局部变量表)中引用的对象,如:方法堆栈中使用的参数,局部变量,临时变量
被同步锁(synchronized)关键字持有的对象
三色标记
白色:
表示对象尚未被垃圾收集器访问过。
在可达性分析刚刚开始的阶段,所有的对象都是白色的
若在分析结束的阶段,仍然是白色的对象,即代表不可达
黑色:
表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过
黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。
黑色对象不可能直接(不经过灰色对象)指向某个白色对象
黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象
灰色:
表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过
如何解决并发标记时的对象的消失问题?
增量更新
当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,
再将这些记录过的引用关系中的黑色对象为根,重新扫描一次
再将这些记录过的引用关系中的黑色对象为根,重新扫描一次
这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了
CMS是基于增量更新来做并发标记的
原始快照
当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,
再将这些记录过的引用关系中的灰色对象为根,重新扫描一次
再将这些记录过的引用关系中的灰色对象为根,重新扫描一次
这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
G1、Shenandoah则是用原始快照来实现。
java对象引用概念分类
强引用:
概述:发生 gc 的时候不会被回收
例如:我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM
宁愿抛出OutOfMemory错误也不会回收这种对象。
宁愿抛出OutOfMemory错误也不会回收这种对象。
软引用:
概述:有用但不是必须的对象,在发生内存溢出之前会被回收。
例如:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会
回收这些对象的内存。
SoftReference<String> softRef=new SoftReference<String>(str); // 软引用
回收这些对象的内存。
SoftReference<String> softRef=new SoftReference<String>(str); // 软引用
用处:
软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容
是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了
是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了
弱引用:
概述:有用但不是必须的对象,在下一次GC时会被回收
具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦
发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
例如:
虚引用:
概述:随时可能被回收,无法通过虚引用获得对象,用 PhantomReference 实现虚引用,
虚引用的用途是在 gc 时返回一个通知
虚引用的用途是在 gc 时返回一个通知
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚
引用主要用来跟踪对象被垃圾回收器回收的活动。
引用主要用来跟踪对象被垃圾回收器回收的活动。
垃圾收集算法
分代收集理论:
概述
1,一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域
2,新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放
3,老年代中,每次垃圾收集时,都有大批对象存活,回收的对象少
2,新生代中,每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放
3,老年代中,每次垃圾收集时,都有大批对象存活,回收的对象少
部分收集分为
新生代收集(Minor GC/Young GC)
只对新生代进行垃圾回收
老年代收集(Major GC/Old GC)
只对老年代进行垃圾回收,目前只有CMS收集器有单独收集老年代的行为
混合收集(Mixed GC)
对整个新生代和部分老年代进行垃圾回收,目前只有G1是这种行为
整堆收集(Full GC)
收集整个堆和方法区的垃圾
标记清除算法(Mark-Sweep)
概述:标记清除算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,
统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
最基础的收集算法,是因为后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的
统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
最基础的收集算法,是因为后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的
缺点:
1,执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,
导致标记和清除两个过程的执行效率都随对象数量增长而降低
导致标记和清除两个过程的执行效率都随对象数量增长而降低
2,内存空间碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中
需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
标记复制算法
概述
为了解决标记-清除算法面对大量可回收对象时执行效率低的问题
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着
的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,
这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,
而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,
按顺序分配即可。这样实现简单,运行高效,
的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,
这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,
而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,
按顺序分配即可。这样实现简单,运行高效,
优点:
实现简单,运行高效
缺点:
复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费太多。
标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低
优化:
半区复制分代
概述
新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。
发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,
然后直接清理掉Eden和已用过的那块Survivor空间。
发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,
然后直接清理掉Eden和已用过的那块Survivor空间。
HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新
生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会
被“浪费”的。
生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会
被“浪费”的。
空间担保
如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,
这些对象便将通过分配担保机制直接进入老年代,这对虚拟机来说就是安全的
这些对象便将通过分配担保机制直接进入老年代,这对虚拟机来说就是安全的
HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局
应用:
一般应用于年轻代,需要回收的对象多,存活的对象少的情况
标记整理算法
概述
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动
式的
式的
缺点:
如果存活的对象少,需要回收的对象多,会导致垃圾回收的效率低下
应用:
一般应用于老年代,需要回收的对象少,存活的对象多的情况
垃圾回收器
新生代收集器
Serial收集器
单线程垃圾回收器,它在来及回收时必须暂停其他所有工作
线程,直到他收集结束(STW)
线程,直到他收集结束(STW)
ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本
ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程 交互的开销,
该收集器在通过超线程(Hyper-Threading)技术实现的伪双核处理器环境中都不能百分之百保证超越Serial收集器。
该收集器在通过超线程(Hyper-Threading)技术实现的伪双核处理器环境中都不能百分之百保证超越Serial收集器。
它默认开启的收集线程数与处理器核心数量相同
Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是
能够并行收集的多线程收集器
能够并行收集的多线程收集器
老年代收集器
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器
CMS收集器是基于标记-清除算法实现的
收集过程
初始标记
仅仅标记一下GC Roots能直接关联到的对象,速度很快
需要“StopThe World”
需要“StopThe World”
并发标记
从GC Roots的直接关联对象开始遍历整个对象图,过程耗时长,
但不需要停顿用户线程,可以和用户线程一起并发运行
但不需要停顿用户线程,可以和用户线程一起并发运行
重新标记
为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那
一部分对象标记记录
需要“Stop The World”
一部分对象标记记录
需要“Stop The World”
并发清除
清理删除标记阶段判断已经死亡的对象,不需要移动存活对象,多
以可以和用户线程同时并发执行
以可以和用户线程同时并发执行
要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不
得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了
得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了
空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找
到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况
到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况
混合收集器
Garbage First收集器(简称:G1)
G1收集器才被Oracle官方称为“全功能的垃圾收集器”(Fully-Featured Garbage Collector)。
虚拟机性能监控,故障处理工具
JVM自带操作指令
jps:虚拟机进程状况工具
jstat:虚拟机统计信息监视工具
jinfo:Java配置信息工具
jmap:Java内存映像工具
jhat:虚拟机堆转储快照分析工具
jstack:Java堆栈跟踪工具
可视化工具
jconsole
VisualVM
arthas
收藏
收藏
0 条评论
下一页