JVM学习笔记
-
Sun公司HotSpot Java Hotspot™ 64-Bit Server VM (build 25.181-b13,mixed mode)
-
BEA JRockit
-
IBM J9VM
我们学习和使用的大都是: Hotspot
2、JVM体系结构
1.JVM的位置
2.JVM的体系结构
-
方法区:
-
方法区存储虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据;
-
是jvm规范中的一部分,并不是实际的实现,在实际实现上并不相同(HotSpot在1.7版本以前和1.7版本,1.7后都有变化)。
-
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError(OOM)异常
-
-
Java堆:
-
仅有一个堆,Java堆用于存放new出来的对象的内容。
-
是垃圾收集器管理的主要区域。可细分为:新生代和老年代;新生代又可分为 Eden,from Survivor,to Survivor。
-
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。
-
-
Java虚拟机栈:
-
存放的东西:八大基本类型 + new出来的对象引用地址 + 实例方法的引用地址。
-
每一条java虚拟机线程都有自己私有的java虚拟机栈,这个栈和线程同时创建,用于存储栈帧。
-
Java虚拟机栈是Java方法执行的内存模型,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
-
栈帧(Stack Frame)存储局部变量表,操作数栈,动态链接,方法出口等信息,随着方法的调用而创建,随着方法的结束而销毁。
-
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
-
如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
-
-
本地方法栈:
-
本地方法栈和虚拟机栈非常相似,不同的是虚拟机栈服务的是【Java方法】,而本地方法栈服务的是【Native方法】。
-
HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
-
-
程序计数器:
-
java虚拟机可以支持多个线程同时运行,每个java虚拟机线程都有自己的程序计数器(PC寄存器)。
-
在任一时刻,一个java虚拟机的线程,只会执行一个方法的代码。
-
程序计数器记录【当前线程所执行的Java字节码的地址】。
-
当执行的是Native方法时,程序计数器为空。程序计数器是JVM规范中唯一一个没有规定会导致OOM(OutOfMemory)的区域。
-
3、类加载的过程
1.概述
1、如图所示:
-
Java源代码文件会被Java编译器编译为字节码文件(.class后缀)
-
然后由JVM中的类加载器加载各个类的字节码文件
-
加载完毕之后,交由JVM执行引擎执行。
2、百分之99的JVM调优都是在堆中调优,Java栈、本地方法栈、程序计数器是不会有垃圾存在的。
2.流程
其中加载、验证、准备、初始化、卸载这五个阶段的过程是固定的,在类加载过程中必须按照这种顺序按部就班的进行,而解析阶段则不一定,可以在初始化以后进行,是为了支持java语言的运行时绑定
1.加载
-
通过一个类的全限定名获取定义此类的二进制字节流
-
将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
-
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据结构的访问入口
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据
2.验证
这一阶段主要是为了确保Class文件的字节流中包含的信息符合虚拟机的要求,并且不会危害虚拟机自身的安全。
四个校验动作
-
文件格式验证:验证字节流是否符合Class文件格式的规范
-
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
-
字节码验证:通过数据流和控制流分析。确定程序语义是合法的、符合逻辑的
-
符号引用验证:确保解析动作能正确执行
3.准备
是正式为类变量分配内存并设置初始值的阶段,这些变量所使用的内存都将在方法区分配
进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在java堆中, 初始值通常情况下是数据类型默认的零值
4.解析
是将虚拟机常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定附
符号引用:与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。虚拟机能接收的符号引用必须是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的Class文件格式中。
直接引用:可以是指向目标的指针,相对偏移量或者是一个能间接定位到目标的句柄,如果有了直接引用那么引用目标必定已经在内存中存在
5.初始化
类初始化时类加载的最后一步,处理加载阶段,用户可以通过自定义的类加载器参数,其他阶段都完全由虚拟机主导和控制。到了初始化阶段才真正执行Java代码
类初始化的主要工作时为了静态变量赋程序设定的初值
static int a=100; 在准备阶段a被赋默认值0,在初始化阶段就会被赋值为100
java虚拟机规范中严格规定了有且只有五种情况必须对类进行初始化:
-
使用new创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行初始化。
-
通过java.lang.reflect包的方法对类进行反射调用的时候,要是类没有进行过初始化,则要首先进行初始化
-
当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化
-
当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类
-
使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
4、类加载器
作用:加载.class文件。
新建的对象放入堆里面,引用(地址)放到栈,其中引用指向堆里面对应的对象。
-
注:
-
这个 Car Class , 就是反射里头的那个,每个类独一份的 Class 模板
-
Class<car> carClass = Car.class : 这个 Class 用来实例化具体的对象出来
-
类是模板,对象是具体的
-
1)启动类(根)加载器 Bootstrap ClassLoad er 2)扩展类加载器 Extension ClassLoader 3)应用程序(系统类)加载器 Application ClassLoader
-
Car car = new Car();
Class<? extends Car > aClass = car.getClass();
ClassLoader classLoader = aClass.getClassLoader(); // Application 加载器
classLoader.getParent(); // ExtClassLoader 扩展类加载器
classLoader.getParent().getParent(); // null 1.不存在 2. java 获取不到(可能是c++写的)
1-启动类加载器,负责加载jre\lib目录下的rt.jar包
2-扩展类加载器:负责加载jre\lib\ext目录下的所有jar包
3-应用程序类加载器:负责加载用户类路径上所指定的类库,如果应用程序中没有自定义加载器,那么次加载器就为默认加载器。
5、双亲委派机制
面试回答顺序:
-
工作过程
-
设计初衷
-
实际体验中,不能获取到 ExtClassLoader 往上,原因是其属于 c++ 语言管理的范畴了
双亲委派机制的工作过程:
-
类加载器收到类加载的请求;
-
把这个请求委托给父加载器去完成,一直向上委托,直到启动类(根)加载器;
-
启动类加载器检查能不能加载(使用findClass()方法),能加载就结束;否则抛出异常,通知子加载器进行加载;
-
重复步骤三.
举例
大家所熟知的String类,String默认情况下是启动类加载器进行加载的。假设我也自定义一个String,并且制定加载器为自定义加载器。现在你会发现自定义的String可以正常编译,但是永远无法被加载运行。
因为申请自定义String加载时,总是启动类加载器加载,而不是自定义加载器,也不会是其他的加载器。
为什么要设计这种机制
-
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),
-
所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
可以改 jdk 中的 jar 包,但尽量别改 rt.jar
6、沙箱安全机制
1.概述
-
Java安全模型的核心就是Java沙箱(sandbox)
-
什么是沙箱?
-
沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,
-
通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
-
-
沙箱主要限制系统资源访问,那系统资源包括什么?
-
CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
-
所有的Java程序运行都可以指定沙箱,可以定制安全策略。
-
2.历史
-
在Java中将执行程序分成本地代码和远程代码两种
-
本地代码默认视为可信任的
-
而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。
-
而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 Sandbox 机制。如下图所示JDK1.0安全模型
-
-
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。
-
因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。
-
在Java1.2版本中,再次改进了安全机制,增加了代码签名。
-
不论本地代码或是远程代码,都会按照用户的安全策略设定,
-
由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。
-
当前最新的安全机制实现,则引入了 域(Domain) 的概念。
-
虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。
-
虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。
-
存在于不同域中的类文件就具有了当前域的全部权限,
-
下图所示为最新的安全模型(jdk 1.6)
3.组成沙箱的基本组件
-
字节码校验器(bytecode verifier) :
-
确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。
-
但并不是所有的类文件都会经过字节码校验,比如核心类。如:new String();
-
-
类裝载器(class loader) :
-
其中类装载器在3个方面对Java沙箱起作用
-
它防止恶意代码去干涉善意的代码;
-
它守护了被信任的类库边界;
-
它将代码归入保护域,确定了代码可以进行哪些操作。
-
-
虚拟机为不同的类加载器载入的类提供不同的命名空间。
-
命名空间由一系列唯一的名称组成, 每一个被装载的类将有一个名字,
-
这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
-
-
类装载器采用的机制是双亲委派模式。
-
1.从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
-
2.由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
-
存取控制器(access controller) :存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
-
安全管理器(security manager) : 是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
-
安全软件包(security package) : java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
-
安全提供者
-
消息摘要
-
数字签名
-
加密
-
鉴别
-
-
-
-
7、Native、本地方法栈
1.概述
-
native :凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库!
-
会进入本地方法栈,然后通过本地接口 (JNI)( Java Native Interface ),调用本地方法库
-
JNI作用:开拓Java的使用,融合不同的编程语言为Java所用,Java诞生的时候C、C++横行,想要立足,必须要有调用C、C++的程序
-
它在内存区域中专门开辟了一块标记区域: Native Method Stack,登记native方法
-
在最终执行的时候,通过本地接口 (JNI),加载本地方法库中的方法
-
如private native void start0();
-
-
2. Method Stack & JNI
本地方法栈(Native Method Stack)
-
它的具体做法是 Native Method Stack 中登记 native方法,
-
在执行引擎执行的时候通过本地接口 (JNI),加载本地方法库(Native Libraies)。
本地接口(Native Interface)JNI
-
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序
-
Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了块区域处理标记为native的代码,
-
它的具体做法是在Native Method Stack 中登记native方法,在( Execution Engine )执行引擎执行的时候加载Native Libraies。
-
目前native方法使用的越来越少了,除非是与硬件有关的应用,比如通过 Java程序驱动打印机 或者 Java系统管理生产设备,在企业级应用中已经比较少见。
-
现在的异构领域间通信很发达,比如可以使用 Socket通信,也可以使用 Web Service 等等,不多做介绍!
8、方法区、栈、堆
1.PC寄存器
-
程序计数器: Program Counter Register
-
每个线程都有一个程序计数器,是线程私有的
-
就是一个指针, 指向方法区中的方法字节码(用来存储指向一条指令的地址——将要执行的指令代码,执行引擎读取下一条指令
-
是一个非常小的内存空间,几乎可以忽略不计
2.方法区Method Area
-
方法区存放被所有线程共享的所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义
-
简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
这句话需要背下!:
/*
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量和数组的内容存在堆内存中,和方法区无关
*/
方法区就存:static final Class信息 运行时常量池
3.栈 stack
为什么 main 方法最先执行,但最后结束?——栈
-
栈:栈内存,主管程序的运行,生命周期和线程同步;
-
线程结束,栈内存也就是释放,
-
对于栈来说,不存在垃圾回收问题
一旦线程结束,栈就Over!
栈有可能放的东西:8大基本类型 + 对象的引用 + 实例的方法
栈运行的原理:每执行一个方法,都会产生一个栈帧
栈如果满了,就会 StackOverFlowError
4.堆 Heap
一个JVM仅有一个堆内存,堆内存大小可以调节
-
JVM内存划分为堆内存和非堆内存,
-
堆内存分为年轻代(Young Generation)、老年代(Old Generation),
-
非堆内存就一个永久代(Permanent Generation)。(这个非堆,严格意义上来说也是堆,但逻辑操作上又不是堆......)
-
-
年轻代又分为Eden和Survivor区。
-
Survivor区由FromSpace和ToSpace组成。
-
Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
-
Eden满了就触发轻GC,
-
经过轻GC存活下来的就到了幸存者区,
-
幸存者区满之后意味着新生区也满了,则触发重GC,
-
经过重GC之后存活下来的就到了老年代。
-
-
-
堆内存用途:
-
存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
-
-
老年代:
-
在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。
-
老年代中的对象生命周期较长,存活率比较高,
-
在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
-
-
非堆内存用途:永久代,也叫方法区存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。
1.分代概念
-
新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。
-
老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。
-
Minor GC : 清理年轻代
-
Major GC : 清理老年代
-
Full GC : 清理整个堆空间,包括年轻代和永久代
-
所有GC都会停止应用所有线程。
-
2.元空间
在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。
-
元空间有注意有两个参数:
-
MetaspaceSize :初始化元空间大小,控制发生GC阈值
-
MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存
-
3.移除永久代原因:
为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。有了元空间就不再会出现永久代OOM问题了!
9.堆(扩展)
-
Heap
-
一个JVM只有一个堆内存,堆内存的大小是可以调节的。
-
-
类加载器读取了类文件后,一般会把什么东西放到堆中?
-
类, 方法,常量,变量~,保存我们所有引用类型的真实对象;
-
-
堆内存中还要细分为三个区域:
-
新生区:Eden , From , To
-
养老区 Old
-
永久区 Perm
幸存区:某次垃圾回收中,幸存下来的,又被判定为新生区
-
图 堆内存详细划分
-
GC垃圾回收,主要是在伊甸园区和养老区~
-
假设内存满了,OOM,堆内存不够——java.lang.OutOfMemoryError:Java heap space
-
永久存储区里存放的都是Java自带的,例如lang包中的类,如果不存在这些,Java就跑不起来了
-
在JDK8以后,永久存储区改了个名字(元空间)
图 堆内存溢出
1.新生区、老年区
-
类:诞生和成长的地方,甚至死亡;
-
伊甸园,所有的对象都是在伊甸园区new出来的!
-
幸存者区(0,1)
-
图 重GC和轻GC
-
GC处理流程:
-
伊甸园满了就触发轻GC,经过轻GC存活下来的就到了幸存者区,
-
幸存者区满,意味着新生区也满了,
-
则触发重GC,对整个新生区进行清理
-
经过重GC之后存活下来的就到了养老区。
-
连养老区也满了,说明整个内存都满了
-
-
真理:经过研究,99%的对象都是临时对象
2.永久区
概述
-
这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境~
-
这个区域不存在垃圾回收,关闭虚拟机就会释放内存
-
存放内容
-
jdk1.6之前:永久代,常量池是在方法区;
-
jdk1.7: 永久代,但是慢慢的退化了,去永久代,常量池在堆中
-
jdk1.8之后:无永久代,常量池在元空间
-
图 JVM探究
2.探究
1.什么时候在永久区就崩了呢?
一个启动类,加载了大量的第三方jar包。
Tomcat 部署了太多的应用
大量动态生成反射类,不断地被加载
直到内存满,就会出现 OOM
2.元空间
逻辑上存在,物理上不存在 (因为存储在本地磁盘内) 所以最后并不算在JVM虚拟机内存中
10.堆内存调优
1.概览
System.out.println(“最大内存Max_memory=”+Runtime.getRuntime().maxMemory()/(double)1024/1024+”M”);
System.out.println(“初始化内存大小Total_memory=”+Runtime.getRuntime().totalMemory()/(double)1024/1024+”M”);
虚拟机参数打印信息:
1.JVM内存分析
默认情况下:
分配的总内存,是电脑内存的 1/4
而初始化 JVM 的内存是 1/64
2.JVM内存排错
遇到OOM:堆空间错误
1.尝试扩大堆内存(设置 VM Options),查看结果: -Xms1024m -Xmx1024m -XX:+PrintGCDetails
如果解决,说明是默认分配的内存不够
2.分析内存,看一下哪里出现了问题(专业工具)
//改成小内存执行Jvm
// -Xms8m -Xmx8m -XX:+PrintGCDetails
// -Xms 设置初始化内存分配大小 1/64
// -Xmx 设置最大分配内存,默认 1/4
// -XX:+PrintGCDetails :控制台打印 GC 的回收信息
// -XX:+HeapDumpOnOutOfMemoryError :输出 OOM dump 信息
public static void main(String[] args){
// String 这种类型,是在堆空间中的
String str = "adsadasdasdsa";
while(true){
str += str + new Random().nextInt(888888888)+new Random().nextInt(99999999);
}
}
在一个项目中,如果出现了 OOM 故障,如何排除
内存快照分析工具: Jprofile、 MAT 是eclipse集成使用 在这里不学)
工具作用:
分析 Dump 内存文件,快速定位内存泄露
探知堆中的数据,获得大的对象
......
工具使用:
安装插件,安装完重启IDEA
安装路径,需保证无空格无中文!
测试程序
报错,但无法定位错误
参数设置
再次执行,得到错误信息文件
用 JProfile打开
分析
11.垃圾回收算法GC
-
新生代
-
幸存区(form区,to区)
-
老年区
JVM在进行GC时,并不是对这三个区域(jdk1.8以后不存在永久区,改名元空间,不在JVM,是在本地内存中的)统一回收。 大部分时候,回收都是新生代~
GC两种类:轻GC (普通的GC), 重GC (全局GC)
● JVM的内存模型和分区~详细到每个区放什么?
● 堆里面的分区有哪些? Eden, form, to, 老年区,说说他们的特点!
● GC的算法有哪些? 标记清除法,标记整理,复制算法,引用计数器
● 轻GC和重GC分别在什么时候发生?
1.GC四大算法
1.引用计数算法
在JVM中几乎不用,每个对象在创建的时候,就给这个对象绑定一个计数器(有消耗)。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,该对象死亡,计数器为0,这时就应该对这个对象进行垃圾回收操作。
优点:
-
简单
-
计算代价分散
-
“幽灵时间”短(幽灵时间指对象死亡到回收的这段时间,处于幽灵状态)
缺点:
-
不全面(容易漏掉循环引用的对象)
-
并发支持较弱
-
占用额外内存空间(计数器消耗)
2.复制算法
-
每次轻 GC 之后,Edan区是空, To 区是空
将可用内存划分为两块,每次只是用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空间一次性清理掉。
这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就行,实现简单,运行高效。
优点: 空间连续,没有内存碎片,运行效率高。
缺点: 每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半。复制收集算法在对象存活率高的时候,效率有所下降, 所以复制算法主要用在新生代幸存者区中的from区和to区,因为新生代对象存活率低。
3.标记-清除算法
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC
操作。
优点:
-
实现简单,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。
-
此外,这个算法相比于引用计数法更全面,在指针操作上也没有太多的花销。更重要的是,这个算法并不移动对象的位置。
缺点:
-
需要进行两次动作,标记获得的对象和清除死亡的对象,所以效率低。
-
死亡的对象被GC后,内存不连续,会有内存碎片,GC的次数越多碎片越严重。
4.标记-压缩/整理算法
标记-整理法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段不是进行直接清理,而是令所有存活的对象向一端移动,然后直接清理掉这端边界以外的内存。
-
优点:不会像标记-清除算法那样产生大量的碎片空间。
-
缺点: 如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。
-
两次扫描,严重浪费时间,会产生内存碎片
-
-
再优化:与标记清除结合
-
甚至再优化:先标记清除几次,再进行压缩
-
期待后生
2.总结
多指标比较:
-
内存效率(时间复杂度):复制算法 > 标记清除算法 > 标记压缩算法
-
内存整齐度:标记压缩算法 = 复制算法 > 标记清除算法
-
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
没有最好的算法,但有当下最合适的算法
GC:分代收集算法
-
年轻代特点存活率低,所以适合于复制算法;
-
老年代存活率高,适合于标记清除+标记压缩混合实现
12.JMM
1.初探
1、什么是JMM?
Java内存模型(Java Memory Model)
2、它干嘛用的?
-
JMM是用来定义一个一 致的、跨平台的内存模型,是缓存一致性协议,用来定义数据读写的规则。(找到这个规则! )
3、它改如何学习?
-
去搜博客,去搜面试题
-
JMM:抽象的概念
-
2.内存可见性
在Java中,不同线程拥有各自的私有工作内存,当线程需要读取或修改某个变量时,不能直接去操作主内存中的变量,而是需要将这个变量读取到线程的工作内存的变量副本中。
那怎样保持这变量之间的一致性呢?
-
当该线程修改其变量副本的值后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值。
-
白话:当我在修改 A 时,其他的线程都读不了 A,得等我改好并重新放回去。
解决共享对象可见性这个问题:volatile关键字
上图右侧每个线程都有自己的工作区域,可以改变变量的值,所以就存在共享对象可见性不一致的问题,这时就可以使用关键字volatile,保证共享对象可见性的问题,只要右边的线程变量的值改变就会立即被刷新到主内存中。
那 volatile 是怎么解决的呢?得先知道:指令重排序
3.指令重排序
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序,指令重排序使得代码在多线程执行时会出现一些问题。
其中最著名的案例便是在 初始化单例时 由于 可见性 和 重排序 导致的错误。
单例模式案例一
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
//有可能多个线程同时进入该判断
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
-
以上代码是经典的 懒汉式 单例实现,
-
但在多线程的情况下,多个线程有可能会同时进入
if (singleton == null)
, -
从而执行了多次
singleton = new Singleton()
,单例被破坏。
单例模式案例二
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
-
以上代码在检测到
singleton
为 null 后,【在同步块中再次判断】, -
可以保证同一时间只有一个线程可以初始化单例。
-
但仍然存在问题,原因就是Java中
singleton = new Singleton()
语句并不是一个原子指令,而是由三步组成:-
1.为对象分配内存
-
2.初始化对象
-
3.将对象的内存地址赋给引用
-
-
但是当经过 指令重排序 后,会变成:
-
1.为对象分配内存
-
2.将对象的内存地址赋给引用(会使得singleton != null)
-
3.初始化对象
-
-
所以就存在一种情况,当线程A已经将内存地址赋给引用时,但 实例对象并没有完全初始化,
-
此时线程B判断
singleton
已经不为null,就会导致B线程 访问到未初始化的变量 从而产生错误。
单例模式案例三:使用 volatile
public class Singleton {
//此处添加了 volatile 关键字 ↓
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
以上代码对singleton
变量添加了volatile
修饰,可以【阻止局部指令重排序】。
那么为什么 volatile 可以保证变量的可见性和阻止指令重排序?
4.volatile
原理
-
规定 线程每次修改变量副本后 立刻同步到主内存 中,用于保证其它线程可以看到自己对变量的修改
-
规定 线程每次使用变量前,先从主内存中 刷新最新的值 到工作内存,用于保证能看见其它线程对变量修改的最新值
-
为了实现可见性内存语义,编译器在生成字节码时,会在指令序列中插入 内存屏障 来 防止指令重排序 。
注意
-
volatile只能保证基本类型变量的内存可见性
-
对于引用类型,无法保证引用所指向的实际对象内部数据的内存可见性。
-
关于引用变量类型:
-
-
volilate只能保证共享对象的可见性,不能保证原子性:
-
假设两个线程同时在做 i++(该操作不具有原子性),在线程A修改共享变量从0到1的同时,线程B已经正在使用值为0的变量,
-
所以这时候可见性已经无法发挥作用,线程B将其修改为1,所以最后结果是1而不是2
-
更多:https://blog.csdn.net/qq_32534441/article/details/86416015
-
-