方法区是一个规范区,元空间是他的实现
线程私有:
-
程序计数器
-
虚拟机栈
线程共享
- 堆
- 方法区
不再使用的对象就可以直接进行垃圾回收
cpu只能认识机器码,不认识字节码
new出来的都只能放在堆里面
同一行代码
内存溢出:
除了程序计数器:其他的都会
OOM
-
堆内存耗尽, 对象越来越多,又一直在使用,不能被垃圾回收
-
方法区(用于存储已经被虚拟机加载的类型信息,常量,静态变量,及时编译器后的代码缓存等数据)内存耗尽,加载的类越来越多,很多框架都会在运行期间动态的产生新的类。用的物理内存
-
虚拟机栈的累积–一个线程创建一个栈帧,用于存储局部变量表,操作数栈,动态连接,方法出口了每一个方法调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。每个线程最多只会占据1M 的内存,线程的个数越来越多,而长时间有运行不销毁的时候,就会占据虚拟机栈导致内存溢出了
stcakOverFlowError
虚拟机内部,方法调用次数过多导致的 递归 无限递归
方法区与永久代,元空间之间的关系?
方法区 存储类的相关信息,加的一些注解啊 他只是一个定义,具体的怎么实现并不管
方法区和永久代以及元空间是什么关系呢? 方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。
-
方法区是JVM规范中定义的一块内存区域,用来存储元数据,方法,字节码,即时编译器所需要的信息
-
永久代是Hotspot虚拟机对JVM的规范实现(1.8之前)
-
元空间是Hotspot虚拟机对JVM的规范实现(1.8之前),本地内存对这些信息的存储空间
创建类加载器–>放入到元空间
只有类加载器,对象实例,等等,被回收了,它对应的元空间的数据就可以被回收了
运行时常量池、方法区、字符串常量池这些都是不随虚拟机实现而改变的逻辑概念,是公共且抽象的,Metaspace、Heap 是与具体某种虚拟机实现相关的物理概念,是私有且具体的。
创建对象
对象的创建
-
类加载的检查:如果遇到一个新的指令的时候,首先去检查这个指令的参数能否在常量池中定位到这个类的符号引用
-
分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配并发问题
在创建对象的时候一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机,必须要保证线程安全的,虚拟机采用两种方式保证线程安全
- CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
3.初始化零值:
内存分配后,虚拟机需要将分配的空间都初始化为0值,保证对象的实例对象字段在java代码中不初始化就直接使用
4.设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC年代年龄等信息,这些信息要存放在对象头中,另外,根据虚拟机当前的运行状态的不同,是否使用偏向锁,对象头有不同的设置方式
5.执行Init方法
在上面的工作完成之后,虚拟机角度来看,对象已经构建完成了,但是从java的程序来看,对象创建才刚刚开始,方法还没有执行,所有的字段还是为0,所以一般来说,执行New指令之后会接着执行Init 的方法,对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算创建出来
对象的内存布局
在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。
Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
-Xmx10240m -Xms10240m -Xmn5120m --XX:SurvivorRatio = 3最小的内存值和Survivor区总大小分别是
这里都指的是堆内存
Xmx 最大内存
Xms 最小内存
Xmn 新生代
伊甸园区 幸存区 = 新生代
SurvivorRatio 默认值 8 : 1
NewRatio:新生代和老年代 新生代占一份,老年代占两份
基本信息(classSpace)+类的字节码和注解(non-class space)元空间根物理内存来的
代码缓存,在程序计数器,使用的这个CodeCache
虚拟机栈有关的东西
大对象直接进入老年代(大对象指的是需要连续内存空间的对象)
为什么要这样呢?
为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
总结:
针对 HotSpot VM 的实现,它里面的 GC 其实准确分类只有两大种:
部分收集 (Partial GC):
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个 Java 堆和方法区。
对象已经死亡?
使用引用计数法!给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。
可达性算法
GC Roots
哪些对象可以作为 GC Roots 呢?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
对象可以被回收,就代表一定会被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize
方法。当对象没有覆盖 finalize
方法,或 finalize
方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
JVM垃圾回收算法
标记清除
根对象:一定不能回收的对象
使用的是程序计数法,可达性算法
局部变量正在引用的对象
静态变量引用的对象
字符串常量池引用的对象
本地方法栈即native方法引用的对象
synchronized引用的对象
缺点:因为不连续,内存碎片问题,空闲的内存依然很小
产生大量的碎片化内存,会产生这些问题
标记清除算法,CMS标记清除算法,
标记整理
效率低,解决碎片化问题
老年代
标记复制
新生代,将可以继续保存的,放到from to 使用的就是空间换时间
占用了一个额外的内存
新生代的存活对象比较少,所以几次复制就可以完成好,但是不适合老年代的话,存活对象比较多,垃圾对象比较少,复制的比较多
GC和分代回收算法
GC:找到无用的对象的自动释放,减少内存碎片,加快分配速度,内存规整的就行了
- 回收的区域就是堆内存,就是回收对象啊,不包括虚拟机栈,方法调用结束后自动释放方法占用内存
- 判断无用对象,使用可代性算法,三色标记法标记存活对象,回收未标记对象
- GC具体的实现成为垃圾回收器
- GC大多都使用了分代回收思想,理论依据是大部分对象朝夕生灭,用完就立刻回收,另外少部分对象会长时间存活,每次都很难回收,根据这两类对象的特性将回收区域分为新生代和老年代,不同区域应用不同的回收策略
- 根据GC来分成是Minior GC ,Mixed GC ,Full GC
分代回收与GC规模
- 分代回收
- 伊甸园eden,最初的对象都分配到这里,与幸存区一起称为新生代
- 幸存区survivor,当伊甸园内存不足,回收到幸存对象到这里,分成from和to,采用标记复制算法
- 老年代old,当幸存区对象熬过几次回收(15次回收之后),晋升到老年代的时候(幸存区内存不足或大对象会导致提前进行晋升)
GC规模
stop the world
- Minor GC 发生在新生代的垃圾回收,暂停时间比较短
- Mixed GC 新生代+ 老年代部分区域的垃圾回收,G1收集器特有
- Full GC 新生代+老年代的完整的部分的垃圾回收,暂停时间比较长,应该尽量避免
三色标记和并发漏标问题
-
用三种颜色记录对象的标记状态
- 黑色-已标记
- 灰色-标记中
- 白色-还未标记
白色就可以直接作为垃圾扔掉了
垃圾回收的同时,也可以产生一些东西
漏标问题 - 记录标记过程中的变化
- Incremental Update 只要赋值发生,被赋值的对象就会被记录
- Snapshot At The Beginning SATB
- 新加对象会被记录
- 被删除引用关系的对象也会被记录
垃圾回收器
Parallel GC
- Eden内存不足发生Minor GC,标记复制STW
- Old内存不足发生Full GC,标记整理STW
- 注重吞吐量
ControlcurrentMarkSweep GC
- Old并发标记,重新标记时需要STW,并发清除
- Failback Full GC
- 注重响应时间
采用的是标记清除算法,会有内存碎片
并发运行是比较快的
CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
G1 GC
伊甸园会跟新生代之间是有限制的
可预测的停顿
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
- 兼顾响应时间和吞吐量
- 可以划分多个区域,每个区域都可以充当Eden,survivor,old ,humongous
- 新生代回收:Eden内存不足,标记复制STW
- 并发标记:old并发标记,重新标记的时候需要STW
- 混合收集:并发标记完成,开始混合收集,参与复制的有eden,survivor,old 其中old会根据暂停时间的目标.选择部分回收价值高的区域,复制时STW
- Failback Full GC (你并发的能力已经没有用了)
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
项目什么时候会出现内存溢出
- 误用线程池导致的内存溢出
使用线程池的时候,需要重写线程池,不能使用newFixedThreadPool 因为他使用的就是new LinkedBlockingQueue 进行直接装配,如果使用的话会不知道有没有上限,导致了出现线程出的问题
模拟同一时刻创建大量的线程池
newCachedThreadPool
有界的队列
将来对线程的数量限制是没有上线的,如果出现了很多线程也会造成问题
- 查询数据太大导致的内存溢出
findAll 方法 后面的条件可能失效,或者使用limit,来进行限制
- 动态生成类导致的内存溢出
METASPACE 内部的类加载器,这个类使用的一些东西,static根对象很强,他的类加载器,不能回收,再怎么做也不能释放掉
不能将静态变量,使用局部变量,这样就不会一直产生类加载器,不停的使用
类加载过程,双亲委派
方法区(存储一些关于字节码的东西,已经被虚拟机编译的东西,用于存储已经被虚拟机加载的类型信息,常量,静态变量)
类.class对象(堆中)
整个类加载分为三个阶段
-
加载
- 将类的字节码载入方法区,并且创建类.class对象
- 如果此类的父类没有加载,先加载父类(接口也是一样的)
- 加载是懒惰执行
-
链接
- 验证-验证类是否符合Class规范,合法性,安全性检查
- 准备-为Static变量分配空间,设置默认值 (如果静态变量是需要赋值的话,赋值的话只能在初始化的阶段才能执行,而不是在类加载)
- 解析-将常量池的符号引用解析为直接引用
-
初始化
- 执行静态代码开与非final的静态变量的赋值(如果是final的话,是在链接的阶段就开始赋值了)
- 初始化是懒惰执行的
类加载是懒惰的,首次用到的时候才加载(下面初始化条件满足也会导致类加载)
- 使用类.class
- 用类加载器的loadClass方法加载类
类的初始化是懒惰的, 满足的条件有:
- main方法所在的类
- 首次访问静态方法或静态变量(非final)
- 子类初始化,导致父类的初始化
- class.forName(类名,true,loader)或Class.forName
验证类的对象是不是占用的内存空间是在堆中的,看看内存是不是在范围之内
静态的变量是放在了类的尾部
类对象都是先放在eden区
使用的inspect 来查看详情
找出的东西需要进行区分是一个类的New对象,一个是类对象,两者之间是不一样的
访问静态的基本数据类型,并不会触发类加载,为什么呢?编译器做的事情,基本类型,用这个类会复制一下,放在自己里面,写死在这个main方法中,在准备阶段就已经赋值了,数值比较小直接写进去,数值比较大会写进常量池中
访问M的时候,对final的基本数据类型,并不会触发类加载
访问n的时候,类已经被初始化,所以会被执行
解析–过程不是一步到位的,先看常量池 需要嗯对一些常量数据
这是创建了一个对象,但是这个对象的引用还没有过来,但是如果之前已经使用过了就会直接进行形成
双亲委派
何为双亲委派,就是优先委派上级类加载器进行加载,如果上级类加载器
- 如果能找到这个类,则由上级加载,加载后该类也对下级加载器可见
- 找不到这个类,则由下级加载器才有资格执行加载
先问上面,之后开始找,踢皮球的加载器
上级对下级是可见的, 但是下级对上级是不可见
由下而上的扫描,由上到下的加载
双亲委派好处
- 避免类的重复加载,确保一个类的全局唯一性。Class与加载它的类加载器一起具备了一种带有优先级的层次关系,当父亲已经加载了该类时,就没有必要子ClassLoader 再加载一次;
- 保护程序安全,防止核心API被随意篡改。
能不能自己写一个类叫java.lang.System?
假冒类替换新的类。
不可以
-
首先如果使用双亲委派去做的,那么优先使用启动类加载器加载真正的java.lang.System 自然不会加载假冒的
-
假设你的自己的类加载器不使用双亲委派,那么你的类加载器加载假冒的java.lang.system,他需要先加载父类的java.lang.Object,而你没有使用委派,找不到java.alng.Object 所以会加载失败
-
在你使用自定义的加载器是java开头,直接会报安全异常,包名和模块都已经绑定了,特别是JDK9以上的
双亲委派的目的有两点
- 让上级的类加载器对下级的类加载器共享,反之不行,让你类能够依赖到jdk能够提供的核心类
- 让类的加载有优先次序,保证核心类优先加载
第一次破坏
JDK1.2之后的 java.lang.ClassLoader 中添加了一个新的 protected 方法 findClass() ,并引导用户编写类加载逻辑时,尽可能去重写这个方法,而不是在 loadClass() 中编写代码。
第二次破坏
线程上下文加载器(Thread Context ClassLoader)
这个类加载器可以通过 java.lang.Thread 类的setContextClassLoader() 方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
逆向使用类加载器,实际违背了双亲委派模型的一般性原则
第三次破坏
SGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
将java.*开头的类委派给父类加载器加载;
否则,将委派列表名单内的类委派给父类加载器加载;
否则,将 Import 列表中的类委派给 Export 这个类的 Bundle 的类加载器加载;
否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载;
否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器加载;
否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载;
否则,类加载器失败。
破坏双亲委派原则
使用SPI机制;
自定义类继承 ClassLoader,作为自定义类加载器,重写 loadClass() 方法,不让它执行双亲委派逻辑,从而打破双亲委派。但是遇到自定义类加载器和核心类重名或者篡改核心类内容,jvm会使用沙箱安全机制,保护核心类,防止打破双亲委派机制,防篡改,如果重名的话就报异常。
破坏双亲委派模型的例子
SPI
spi机制是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在JDBC中就使用到了SPI机制。
原生的JDBC中 Driver 驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库厂商去实现的。原生的JDBC中的类是放在 rt.jar 包的,是由启动类加载器进行类加载的,在JDBC中的 Driver 类中需要动态加载不同数据库类型的 Driver 类,而 mysql-connector-.jar 中的 Driver 类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,于是乎,这个时候就引入SPI,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。
Tomcat
Tomcat为什么不使用默认的双亲委派模型?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CD44XMOM-1656988113160)(%E9%87%91%E4%B9%9D%E9%93%B6%E5%8D%81%E9%9D%A2%E8%AF%95/image-20220705102548430.png)]
首先tomcat 是一个web容器:
- 部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同的版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离的
- 部署在同一个web容器中的相同的类相同的版本是可以共享的
- 容器也有自己依赖的类库,不能与应用程序的类库混淆,基于安全考虑,应让容器的类库和程序类库进行隔离
- 要支持 jsp 的热部署(jsp 文件最终也是编译成 class 文件才能在虚拟机中运行)。
对于第一种和第三种场景,如果使用默认的类加载器机制,是无法加载两个相同类库的不同版本的,默认的类加载器只关注全限定类名,不关注是什么版本的。
第二种场景,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
第四种场景,要实现 jsp 文件的热更新(jsp 文件其实也就是 class 文件),使用默认类加载器,如果修改了,但类的全限定名还是一样,类加载器会直接取方法区中已经存在的,修改后的 jsp 是不会重新加载的。
CommonLoader:Tomcat最基本的类加载器,加载路径中的 class 可以被Tomcat容器本身以及各个 Webapp 访问;
CatalinaLoader:Tomcat容器私有的类加载器,加载路径中的 class 对于 Webapp 不可见;
SharedClassLoader:各个 Webapp 共享的类加载器,加载路径中的 class 对于所有 Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个 Webapp 私有的类加载器,加载路径中的 class 只对当前 Webapp可见;
JspClassLoader:每一个JSP文件对应一个Jsp类加载器。
从图中的委派关系中可以看出:
CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,从而实现了公有类库的共用;
CatalinaClassLoader 和 Shared ClassLoader 自己能加载的类则与对方相互隔离;
WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个WebAppClassLoader 实例之间相互隔离;
JasperLoader 的加载范围仅仅是这个JSP文件所编译出来的那一个 .Class 文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 JasperLoader 来实现JSP文件的热插拔功能。
对象引用类型分为哪几类?
强引用:
- 普通变量赋值即为强引用,A a = new A();
- 成员变量也同样需要这个规则
- 通过GC Root的引用链,如果强引用不到该对象,该对象才能被回收
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nuWfQNWs-1656988113161)(%E9%87%91%E4%B9%9D%E9%93%B6%E5%8D%81%E9%9D%A2%E8%AF%95/image-20220703151753637.png)]
软引用:
- SoftReference a = new SoftReference(new A( ));
- 如果仅有软引用该对象的时候,首次垃圾回收不会回收该对象,如果内存仍然不足的时候,会再次回收释放该对象
- 软引用自身需要引用队列才能进行释放
- 典型的例子就是反射数据
弱引用:
- WeakReference a =new WeakReferencez(new A());
- 如果只有弱引用引用该对象的时候,只要发生垃圾回收,就会被释放掉
- 弱引用自身需要引用队列才能进行释放
- 典型的例子是TreadLocalMap中的Entry对象
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
虚引用:
- 例如PhantomReference a = new PhantomReference(new A());
- 必须配合引用队列一起使用,当虚引用引用的对象被回收时,会将引用对象入队,由Reference Handler 线程释放其关联的外部资源
- 典型例子是Cleaner释放DirectByteBuffer占用的直接内存
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
如果使用new的话就说明这个对象就在堆里面,如果不使用New 来创建的话,他的不仅会在堆里面还会在字符串常量池中,就不会被回收了
ThreadLocalMap的内存泄露问题可以使用这个来做的,可以使用这个引用队列问题来解决
cleaner
使用这个api来解决垃圾回收了
守护线程,还有就是使用System.in.read();
释放清理,使用的就是这个cleaner
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
无用的类和无用的常量
无用的常量:假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池了。
无用的类:
-
该类的所有的实例都已经被回收,也就是java堆中不存在任何类的实例
-
加载该类的ClassLoader已经被回收
-
对应的java.lang.class对象没有在任何地方被引用,无法在任何地方可以反射访问该类的对象.
finazlize
- 他是Object的一个方法,子类重写他,垃圾回收时这个方法会被调用,可以在其中的一些资源进行释放和清理工作
- 将资源释放和资源清理放在finalize这个方法中是非常的不好,非常影响性能,严重的时候会引起OOM,从Java9就开始标注为@Deprecated,不建议使用了.
为什么不建议使用?
1.垃圾回收顺序有关的,单向链表
- 线程
- 守护线程,其他的非守护线程结束了,这个守护线程也要结束(finazlize 的优先级是8还是很高的)
4.出现异常。中间出现异常,他就会吞掉所有的异常
守护线程
主要就是为了跟踪
父类
finalize得断开