1、JVM的位置
-
JVM运行在操作系统上,在操作系统之上,JVM虽然是一个环境,其实就相当于一个软件,Java程序都在JVM上跑,
-
JVM是有限制的,JVM与操作系统交互,操作系统也与JVM交互
-
JVM是用C语言写的,所以Java又叫C++--
-
JVM是包含在JRE(Java运行时环境)中的
-
操作系统也相当于一个软件,操作系统之下是硬件体系
2、JVM的体系结构
这个图因为是由两个图拼在一起的,所以JVM架构图是一整个大图,包含执行引擎这些
-
一共5个:堆、元空间(方法区)、虚拟机栈、本地方法栈、程序计数器
-
栈里面不可能会有垃圾的,因为栈用完就弹出去了,栈里面要是有垃圾程序就崩了,
-
main方法压在栈最底下,每执行一个变量产生一个栈中的引用,这个引用用完就弹出去了,要是有垃圾的话,根据栈的先进后出性质,栈里面要是有垃圾的话,main方法就死了,所以栈里面不可能有垃圾的
-
所以:Java栈、本地方法栈、程序计数器 这三块不可能会有垃圾回收
-
所以JVM调优就是在堆里面调,方法区属于特殊的堆
-
所以JVM调优基本都是在调 堆和方法区,堆的JVM调优是最重要的调优
3、类加载器
3.1、作用
-
加载class文件
-
过程:new Student(); new完后变成了具体实例,这个具体的实例的引用放在栈里,这个具体的人放在堆里面 #
-
就Lombok这个插件来说,它需要在我们程序执行期间多态地给我们生成getter、setter方法,原理?
-
它在程序加载的时候,它会解析成这样一棵树的结构,lombok在语法树上解析,它在语法树上新增动态增加代码
-
只要Lombok能在Execution Engine(执行引擎)、类加载器上挂一个钩子函数,大多数都是在执行引擎上挂钩子,就能实现;但是它不能在运行时挂钩子
3.2、类加载器分类
-
将自己写的类放进去的方法:修改rt.jar包、放到jre/lib/ext里面 虚拟机自带的加载器
-
启动类(根)加载器(BootStrap)
-
是扩展类加载器的父类,但是底层是C、C++写的,扩展类加载器通过.getParent()方法获取到的是null,即Java获取不到
-
在rt.jar包里面,rt即root包
-
扩展类加载器(Extension)
-
是应用程序加载器的父类,应用程序加载器可通过.getParent()方法获取到其父类扩展类加载器
-
在/jre/lib/ext这个文件夹里
-
应用程序(系统)加载器(App)
-
就是一个抽象类,在lang包下,其实就是我们写的
3.3、Java来源
Java本来是想在C++的基础上,去掉繁琐的东西:指针、内存管理,于是早期的名字叫 C++--
3.4、类加载的生命周期
3.4.1、步骤顺序
-
加载、验证、准备、初始化、卸载 这5个阶段的顺序是确定的,但解析阶段不一定
-
某些情况下,可以在初始化阶段之后再开始,这是为了支持Java的运行时绑定(动态绑定)
3.4.2、类加载的过程
-
加载:加载分为3步:
-
通过类的全限定性类名(权限类名),获取该类的二进制流
-
将该二进制流的静态存储结构,转为方法区的运行时数据结构
-
在堆中为该类生成一个class类
-
验证:验证该class文件中的字节流信息,符合虚拟机的要求,不会威胁到虚拟机的安全
-
准备:为class对象的静态变量分配内存,初始化其初始值
-
解析:该阶段主要完成,符号引用转换成直接引用
-
初始化:开始执行类中定义的java代码,初始化阶段就是调用类构造器的过程
4、双亲委派机制(保证安全)
-
类加载器收到类加载的请求
-
将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动(根)类加载器
-
启动类加载器检查是否能加载当前类,能加载就使用当前的加载器,否则抛出ClassNotFound异常,通知子加载器进行加载
-
重复步骤3
-
目的:保证安全
-
就是要运行(加载)一个类的时候,它会先向上找,App -> Ext -> Boot Class Loader,如果Boot无法加载,就会一级级返回运行,Boot -> Ext -> App,直到找到能加载的Class Loader
-
假设你自己写了一个String类,你在里面写了main方法,但是运行的时候它会向上找,找到Boot里面的String,然后执行这个,所以会报错
4.1、四种类加载器其实是各司其职
-
Bootstrap ClassLoader ,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
-
Extention ClassLoader,主要负责加载目录:%JRE_HOME%\lib\ext目录下的jar包和class文件。
-
Application ClassLoader,主要负责加载当前应用的classpath下的所有类
-
User ClassLoader:用户自定义类加载器,可加载指定路径的class文件
4.2、使用双亲委派机制的原因
-
避免类的重复加载
-
保证安全性,因为有优先级
4.3、父加载器 与 子加载器 之间不是继承关系
-
双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码
4.4、双亲委派机制的实现原理
-
实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中
-
先检查类是否已经被加载过
-
若没有加载,则调用父加载器的loadClass()方法进行加载
-
若父加载器为空,则默认使用启动类加载器作为父加载器
-
如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载
4.5、主动破坏双亲委派机制
-
因为他的双亲委派过程都是在loadClass方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可
-
因为双亲委派机制的类加载范围是有限的,有些情况加载不到,所以就需要主动破坏双亲委派机制
-
比如:JDBC、tomcat 就都破坏了双亲委派机制,JDBC需要加载SPI接口实现类,而不是API,即使用根加载器加载第三方的类;tomcat是web容器,会部署多个应用程序,那么它们可能依赖相同的第三方类库的不同版本,但是双亲委派机制是不允许加载多个相同类的,所以tomcat要破坏双亲委派机制,提供隔离机制
4.6、loadClass()、findClass()、defineClass()的区别
-
loadClass() 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。
-
findClass() 根据名称或位置加载.class字节码
-
defineclass() 把字节码转化为Class
-
如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候,就可以继承ClassLoader,并且重写findClass方法,实现自己的加载逻辑 #
5、沙箱安全机制(Sandbox)(了解)
沙箱就是一个限制程序运行的环境,沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,
-
因为早期本地代码、远程代码,远程代码不安全
-
代码签名,即用Java生成认证文件,
-
比如我们的代码不能访问https,tomcat不能访问https,但Java生成证书就能访问https了
-
最新的引入了域(Domain)的概念,JVM会把不同的代码加载到不同的域
5.1、沙箱的基本组件
-
字节码校验器(bytecode verifier):确保java类文件遵循Java的语言规范,可以帮助Java程序实现内存保护,但是不是所有类文件都会经过字节码校验,比如核心类,以java、javax开头的
-
类加载器(Class Loader):虚拟机为不同的类加载器载入的类提供不同的命名空间,并且类加载器在三个方面对Java沙箱起作用
-
他防止恶意代码去干涉善意代码 (双亲委派机制)
-
它守护了被信任的类库边界
-
它将代码归入保护域,确定了代码可以进行哪些操作
-
存取控制器(access controller):控制核心API对操作系统的存取权限,而这个控制的策略设定可以由用户指定
-
安全管理器(security manager):是核心API和操作系统之间的主要接口,实现权限控制,比存取控制器优先级高,(我们很难控制)
-
安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,比如安全提供者、消息摘要、数字签名(keytools)、加密、鉴别
6、Native
-
凡是带了native关键字的,说明Java的作用范围达不到了,会去调用底层C语言的库,
-
会进入本地方法栈,然后调用本地方法接口(JNI:Java Native Interface)
-
JNI的作用:扩展Java的使用,融合不同的编程语言,为Java所用,最初的目的是为了:C、C++
-
Native在内存区域中专门开辟了一块标记区域:本地方法栈(Native Method Stack),登记native方法,在最终执行的时候(即执行引擎执行的时候),通过本地方法接口(JNI)加载本地方法库中的方法
6.1、使用场景
在企业级应用中很少见
-
Java程序驱动打印机
-
管理系统
-
Robot类
7、程序计数器
-
即程序计数器(Program Counter Register)
-
每一个线程都有一个程序计数器,是线程私有的,其实就是一个指针,指向方法区中方法字节码(用来存储指向下一条指令的地址,也就是即将要执行的指令代码),在执行引擎读下一条指令,是一个非常小的内存空间,几乎可以忽略不计 #
8、方法区(即元空间)
-
现在的方法区,就是元数据区,即元空间,元空间使用本地内存,与堆没关系了
-
方法区是被所有线程共享的,所有的字段、方法字节码、一些特殊方法(构造函数、接口代码)在此定义,
-
简单说就是所有定义的方法的信息都保存在该区域,此区域属于共享区间
-
方法区逻辑上是属于堆的,因为方法区是共享的,但是方法区其实不在堆里面,在永久区里面,所以为了区分,叫方法区或非堆,然后JDK1.8之后就出了一个元空间,方法区就在元空间这个分代概念里面
-
方法区里面有常量池
8.1、方法区存放的东西
-
静态变量(static)、常量(final)、类信息(构造方法、接口定义),运行时的常量池存在方法区中,但是实例变量存在堆内存
-
即static、final、Class、常量池 放在方法区中
9、栈
-
程序 = 数据结构 + 算法 (框架容易淘汰)
-
栈:先进后出,后进先出,要执行就压入栈,运行完就弹出栈
-
队列:先进先出(FIFO:first input first output)
-
所以main方法最先执行,最后结束
-
栈内存,主管程序的运行,栈的生命周期和线程同步,线程结束,栈内存也就释放了
-
对于栈而言,不存在垃圾回收问题(因为如果栈中有垃圾,main线程就不可能弹出去了,程序也永远不会停止)
9.0、栈帧
-
栈是运行时的单位,而堆是存储的单位
-
即栈解决程序的运行问题,即程序如何执行
-
栈里面保存着一个个栈帧(Stack Frame),每个线程创建时都会创建一个虚拟机栈,是私有的
9.0.1、生命周期
-
生命周期与线程一致
9.0.2、虚拟机栈的作用
-
主管Java程序的运行,它保存方法的局部变量、部分结果,参与方法的调用和返回
9.0.3、栈帧中存储的5个东西
-
局部变量表(Loacl Variables):
-
操作数栈(Operand Stack)(或表达式栈):
-
动态链接(Dynamic Linking)(或执行运行时常量池的方法引用):
-
方法返回地址(Return Address)(或方法正常退出或异常退出的定义):
-
一些附加信息
9.1、栈中存放的东西
-
八大基本类型
-
对象引用(存个地址)
-
实例的方法
9.2、栈的运行原理
-
栈帧
-
程序正在执行的方法,一定在栈顶
-
栈如果放满了,就会有错误,StackOverflowError
9.3、栈 + 堆 + 方法区 间的交互关系
10、三种JVM
-
Sun公司:HotSpot Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.2+9-LTS, mixed mode)
-
BEA JRockit
-
IBM J9 VM
11、堆(Heap)
-
一个JVM只有一个堆内存,堆内存的大小是可调节的,
11.0、新生代、老年代、元空间内存占比
-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M
-
新生代和老年代在堆内存占比:新生代是1/3,老年代是2/3
-
元空间:使用本地内存,取代了永久代,以前的永久代不好回收
11.1、类加载器读取了类文件后,一般会把什么东西放在堆中?
-
类实例(类具体的实例)、方法、常量(常量值是在方法区的常量池里面拿的),即保存所有引用的真实实例
11.2、堆内存中细分为三个区域与GC的关系
-
一般的GC都是轻量级的GC:Minor GC
-
幸存区是伊甸园区与老年区之间的过渡,所以主要的GC垃圾回收就发生在伊甸园区和老年区
-
当新生区、老年区都满了,即堆内存满了就会报一个错误OOM(OutOfMemoryError),堆内存不够,特别严重
-
JDK8之后,把永久区改了个名字叫:元空间,但也有不同
11.2.1、新生区(伊甸园区) Young/New
-
新生区又细分为:伊甸园区、幸存区0区、幸存区1区
-
幸存区0区和幸存区1区两个就是From和To,两个会不停地动态交换
11.2.1.1、新生区的经历
-
student1一开始是在伊甸园区,因为新出来的都在伊甸园
-
当伊甸园区满了后触发轻GC,它经历第一次的GC回收时没有被回收时,那么它就会进入幸存区
-
当它在幸存区再经历过多次的GC回收后还没有被回收,并且此时伊甸园区也满了,幸存区也满了,就会触发一次重GC,它如果活下来就会进入老年区
-
一般当对象经历15次GC还存活,会进入老年区
11.2.2、老年区(养老区) Old Gen
-
假如老年区快满了,就说明新生区+老年区已经快没有内存了,就会触发另一种GC回收:重GC回收(Full GC)(重量级的垃圾回收)
11.2.3、永久区 Perm(Permanent),JDK8之后改名为:元空间
-
永久存储区存放:JVM内置的东西等
-
现在的元空间独立出来了,
12、新生区、老年区
12.1、新生区
-
类:诞生、成长,甚至死亡的地方
-
分为伊甸园区、幸存区(0,1两个区)
12.1.1、伊甸园区
-
所有对象都在 伊甸园区 new出来
-
当伊甸园区满了后触发轻GC,在伊甸园区活过第一次的GC回收后,就会进入幸存区
12.1.2、幸存区
-
当它在幸存区再经历过多次的GC回收后还没有被回收,并且此时伊甸园区也满了,幸存区也满了,就会触发一次重GC,它如果活下来就会进入老年区
-
一般当对象经历15次GC还存活,就会进入老年区
12.2、老年区(Old Gen)
-
当老年区也满了,就会触发重GC,如果重GC后还是满了,就报OOM错误了
13、永久区(现元空间,内置方法区,使用的是本地内存,不在虚拟机中)
-
这个区域是常驻内存的,用来存放JDK自身携带的Class对象,包括一些接口元数据(interface元数据)
-
所以存储的是Java运行时的环境或类信息
-
这个区域不存在垃圾回收
-
关闭虚拟机的时候,就会释放这个区域的内存
-
当一个启动类:加载了大量的第三方jar包、或tomcat部署了太多的应用、或存放大量动态生成的反射类,这个时候永久区就崩了,直到内存满了,就报OOM错误
-
元空间是独立的,不在堆内,元空间的内有方法区,方法区内有常量池
13.1、JDK1.6之前
-
永久代、常量池是在方法区
13.2、JDK1.7
-
永久代,常量池在堆中,但是永久代慢慢地退化了,(当初提出退永久代概念)
13.3、JDK1.8之后
-
无永久代,常量池在元空间
14、堆内存调优
-
默认情况下,分配的总内存是电脑的1/4,而初始化的内存是电脑的1/64,可以手动调参解决OOM,扩大堆内存
14.1、遇到OOM解决调优
-
尝试扩大堆内存,看结果
-
分析内存,查看出错的位置(专业工具)
14.2、遇到OOM故障,如何排除?
-
查看代码第几行出错,使用内存快照分析工具
-
早些年eclipse里面的MAT
-
Jprofiler
-
Debug,一行行代码分析(对于上线项目来说不现实)
14.3、内存快照分析工具的作用
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
-
-Xms:设置初始化内存分配大小,默认1/64
-
-Xmx:设置最大分配内存,默认1/4
-
-XX:+:选择要打印什么,比如-XX:+PrintGCDetails、 -XX:+HeapDumpOnOutOfMemoryError 等
-
分析Dump内存文件,快速得内存泄漏
-
分析堆中是数据
-
获得大的对象
-
...等等
14.4、JVM调优的命令
JVM启动时可指定参数
6.3.1、以 - 开头的参数
-
以 - 开头的是 标准参数,所有版本的JVM都支持
6.3.2、以 -X 开头的参数
-
以 -X 开头的是 非标准参数,不同版本的JVM有些许不同
6.3.3、以 -XX 开头的参数(调优常用)
-
java -XX:+PrintFlagsFinal -version:打印所有,可以设置的参数,的最终值。即查看那些,可调优的参数,大概728个左右,使用java -XX:+PrintFlagsFinal -version | wc -l 能看见,还能使用java -XX:+PrintFlagsFinal -version | grep BiasedLocking,查看关于偏向锁的参数
15、GC(也可以叫分代收集算法)
-
垃圾回收只有在堆+方法区,但方法区其实也属于堆,所以GC只有在堆
-
JVM进行GC时,并不是对新生代、幸存区、老年代这三个区域统一回收,大部分都是回收新生代
-
GC通过:引用计数法、根可达算法 判断对象是否存活
15.1、GC的两种类型
-
Minor GC(普通GC):只针对新生代的GC
-
Full GC(全局GC):针对整个堆,包括:新生代、老年代、元空间
15.1.1、触发GC的时机
-
Minor GC触发条件:Eden区满的时候
-
Full GC触发条件:
-
System.gc时,系统建议执行Full GC,但是不是一定会执行的
-
由Eden区、From区向To区复制时,对象大小 > To可用内存大小,就把对象转到老年代,但是老年代的内存也小于对象大小,就触发Full GC
-
老年代的空间不够分配新的内存,就触发Full GC
-
GC担保失败:发生Minor GC前,先检查老年代是否有连续空间,如果有,就执行Minor GC,如果没有则根据设置: -XX:-HandlePromotionFailure,查看参数是否允许担保失败,然后继续检查,当前老年代最大可用连续空间 是否大于 平均历次晋升到老年代大小,如果大于,则进行MinorGC,否则进行FullGC,如果未设置HandlePromotionFailure,则直接进行FullGC
15.2、各种算法
15.2.1、引用计数法
-
被引用一次就将计数器+1,没人用就清除掉,以下所有的算法都是使用根可达算法
15.2.2、复制算法
-
幸存区分为from、to区,这两个分区会不断交换,谁空谁是to区
-
每次GC都会把从Eden区幸存的对象移到幸存区中,被移入的区就叫它from区,空的就算to区,所以每次GC后Eden就是空的,然后再是 from区到 to。
-
然后复制算法就是当GC时,会把Eden中幸存的对象复制一份放到to区中,然后from中所有幸存的对象也复制一份放到to区中,然后清理并整合空间,然后旧to区变成新from区,旧from区变成新to区
-
当对象经历15次GC还存活,就进入老年区-XX:MaxTenuringTheshold=5,设定进入老年代的GC次数
-
所以Eden、from、to区主要用的就算复制算法
15.2.2.1、优缺点
优点:没有内存碎片
缺点:浪费了内存空间,一半的空间永远是空的
15.2.2.2、复制算法最佳使用场景
对象存活率较低的情况
15.2.3、标记清除法、标记整理(压缩)法
标记:扫描对象,对活着的对象进行标记
15.2.3.1、标记清除法
扫描两次,第一次扫描进行标记,第二次扫描将没有标记的对象清除掉
优点:不浪费内存空间
缺点:扫描严重浪费时间、有内存碎片
15.2.3.2、标记压缩(整理)法(标记清除压缩法)
扫描三次,第一次扫描进行标记,第二次扫描将没有标记的对象清除掉,第三次扫描将存活的对象全部移动到同一端
优点:不浪费内存空间、没有内存碎片
缺点:扫描严重浪费时间,比标记清除更慢
15.3、分代收集算法
-
也不算是一个独立的算法,而是在:复制算法、标记清除、标记整理的基础上实现的,根据内存对象的存活周期,分为新生代、老年代
-
在新生代使用:复制算法,因为对象存活率低,所以很适合使用一小块to幸存区去存放Eden、from区的存活对象
-
在老年代使用:标记清除和标记整理,一般使用标记清楚,当内存碎片较多时使用一次标记整理
15.4、对于算法的总结
-
内存效率(即使用内存的次数,时间复杂度): 复制算法>标记清除算法>标记压缩算法
-
内存整齐度:复制算法=标记压缩算法>标记清除算法
-
内存利用率:标记压缩算法=标记清除算法>复制算法
15.4.1、有无最优的算法?
没有最好的算法,只有最适合的算法
15.4.2、年轻代、老年代的算法分析
年轻代:对象存活率低,所以使用复制算法
老年代:对象存活率高,所以使用标记清除(内存碎片不多时使用)+ 标记压缩(内存碎片多时使用)混合实现
16、JMM(Java Memory Model)Java内存模型
Java内存模型,是一串规则,并不真实存在。是一种虚拟机规范,Java Memory Model
主要描述了三种规范:原子性、可见性、有序性
java 内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 java程序在各种平台下都能达到一致的并发效果,
内存与内存访问
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。
每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。
线程不能直接读写主内存中的变量。不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成
每个线程的工作内存都是独立的,线程操作数据只能在工作内存中进行,然后刷回到主存。
主内存与工作内存间的8种操作和规则
lock 和 unlock 一般针对的也是主内存,比较容易控制。4对操作是成对的
-
线程加锁前,必须读取主存中的最新值到工作内存中,
-
线程解锁前,必须把共享变量立刻刷回主存,
8种操作
-
lock(锁定):作用于主内存中的变量,把变量标识为线程独占的状态。
-
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
-
read(读取):作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的 load操作使用。
-
write(写入):作用于主内存中的变量,它把 store操作从工作内存中得到的变量的值放入主内存的变量中,
-
load(加载):作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
-
store(存储):作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
-
use(使用):作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
-
assign(赋值):作用于工作内存的变量,它把一个从执行引擎中接收到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
关于操作的规则
-
操作必须成对出现,且必须遵循操作顺序。
-
不允许线程丢弃他最近的 assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
-
不允许线程将没有 assign 的数据从工作内存同步到主内存。
-
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
-
如果对一个变量进行 lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新 load 或 assign操作初始化变量的值。
-
lock 与 unlock 次数相等,如果一个变量没有被 lock,就不能对其进行 unlock操作。也不能 unlock一个被其他线程锁住的变量。违反的话就是加解锁中会抛出的异常。
-
一个线程对一个变量进行 unlock操作之前,必须先把此变量同步回主内存,
Happens-Before规则
简而言之就是所有的操作必须遵循顺序,不可乱序。
-
单线程规则:一个线程中的每个动作都 happens-before 该线程中后续的每个动作
-
监听器锁定规则:监听器的 解锁 动作 happens-before 后续对这个监听器的 锁定 动作
-
volatile变量规则:对volatile字段的写入动作 happens-before 后续对这个字段的读取动作
-
线程start规则:线程start()方法的执行 happens-before 一个启动线程内的任意动作
-
线程join规则:一个线程内的所有动作 happens-before 任意其他线程join()成功返回之前(即join了,但join还未返回,那么该线程就一直执行动作)
-
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
final 引起的 this引用逃逸
final:通过禁止 构造函数初始化 和 给 final 字段赋值 这个两个动作的重排序,保证可见性。
在构造器构造还未彻底完成前(即实例初始化阶段还未完成),将自身 this 引用向外抛出并被其他线程复制(访问)了该引用,可能会问到该还未被初始化的变量,甚至可能会造成更大严重的问题。
this逃逸可能发生的场景
-
在构造器中很明显地抛出 this引用提供其他线程使用(如上述的明显将this抛出)。
-
在构造器中内部类使用外部类情况:内部类访问外部类是没有任何条件的,也不要任何代价,也就造成了当外部类还未初始化完成的时候,内部类就尝试获取为初始化完成的变量。
17、线程共享、私有的区间
17.1、线程共享
-
堆
-
元空间(方法区)
17.2、线程私有
-
本地方法栈
-
程序计数器(PC计数器)
-
虚拟机栈
18、根搜索算法(根可达算法)
18.1、根可达算法的根的对象有哪些?
-
线程栈变量
-
静态变量,方法区类静态属性引用的变量
-
常量池对象,方法区常量池引用的对象
-
JNI指针,本地方法栈JNI引用的对象
18.2、根可达算法如何判断对象需要被回收?
-
一个对象满足上述条件时,不会马上被回收,还需要进行两次标记;
-
第一次标记:判断对象是否有finalize()方法,并且方法未被执行过,若不存在则标记为垃圾对象,等待回收;
-
第二次标记:将当前对象放入F-Queue队列,并生成一个finalize线程去执行该方法,虚拟机不保证该方法一定会被执行,因为如果线程执行缓慢或进入死锁,会导致回收系统崩溃;
-
如果执行了 finalize方法后仍然没有与 GC Roots(即根)有直接或间接的引用,则该对象会被回收
19、四种常量池
-
Class文件常量池
-
运行时常量池
-
全局字符串常量池
-
基本类型包装类对象常量池
对象的生命周期
对象一共有 7个阶段:创建阶段、应用阶段、不可见阶段、不可达阶段、收集阶段、终结阶段、空间重分配阶段。