由于Java语言将自己的内存控制权交给了虚拟机,所以需要了解虚拟机的运行机制
(主要用于回顾JVM)
Java对于内存的管理是采取的分区管理,根据不同的区域,管理上也是大有不同。
其中主要分为线程私有区域、线程共享区域
就是在Java虚拟机中当前线程所执行的字节码的信号指示器,标示了下一条需要执行的字节码指令。也就是说整个线程都是通过程序计数器根据下一条需要执行的字节码指令不断地推进,从而让线程不断地运行
(因为整条线程都是程序计数器不断地推进向前的,当程序计数器停止,则线程也就停止了)
若线程正在执行的是Java方法,程序计数器记录的是JVM字节码指令地址
若线程正在执行的是native本地方法,程序计数器就未指定值underfined
native方法:(C / C++方法,因为虚拟机的底层是由C++方法写的)
也称Java栈,每个线程在创建时都会创建一个虚拟机栈,内部保存的每一个栈帧对应着Java方法的每一次调用
调用一个新方法时,就构建一个栈帧压入到Java虚拟机栈中
当一个方法执行完毕时,就将其对应的一个栈帧出战
线程调用一个方法的执行和退出就对应着一个栈帧的入栈和出栈。栈顶部的第一个栈帧叫做当前栈帧,对应线程需要执行的最新方法。
栈帧内部包括
是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表所需要的容量大小在编译期就能确定下来,且在整个方法运行期间都不会发生容量空间的改变。
局部变量表的容量以变量槽为最小单位
通过索引定位的方式使用局部变量表,索引值的范围从0开始至局部变量表最大的变量槽数量
若访问的是32位数据类型的变量,索引N就代表使用第N个变量槽
若访问的是64位数据类型的变量,则会同时使用第N和N+1两个变量槽
两个相邻的共同存放一个64位数据的两个变量槽,虚拟机不允许采用任何方式单独访问其中的某一个
若执行实例方法,则局部变量表的第0位索引的变量槽默认是用于传递方法所属对象实例的引用 this
为节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的。
当字节码程序计数器的值已经超出了某个变量的作用域,则这个变量对应的变量槽就可以交给其他变量来重用
this
用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。当一个方法执行过程中,会有各种字节码指令堆操作数栈出栈和入栈操作
每个栈帧都包含一个指向运行时常量池中该 栈帧所属方法
的引用,持有该引用就是为了支持方法调用过程中的动态连接
存放了被调用方法的实际地址或指向该地址的指针(当需要调用另一个方法时,会从栈帧中获取动态连接信息,跳转到该地址执行另一个方法)
在Java源文件被编译到字节码文件时,所有的变量和方法引用都作为符号引用保存在Class文件的常量池中
一个方法调用另外一个方法时,通过常量池中指向方法的符号引用来表示
动态连接的作用是为了将这些符号引用转换为调用方法的直接引用
栈帧中允许携带Java虚拟机实现相关的一些附加信息,比如对程序调试提供支持的信息,但具体的信息取决于具体的虚拟机实现
以上两种方式都将导致栈帧被弹出
程序计数器通过下一条字节码指令推动着线程不断地执行,而每个线程都会在创建时创建一个Java虚拟机栈,当线程调用Java方法时,在Java虚拟机栈中通过栈帧的入栈和出栈来存取或计算Java方法内部的相关数据
是被所有线程共享的一块内存区域,几乎(除了基本类型、直接内存、final、static修饰的常量)所有的对象实例都是在堆中进行分配内存的
Java堆是垃圾收集器管理的内存区域,以G1收集器的出现为分解,往前的收集器基本是采用分代收集理论进行设计
垃圾分代的唯一目的是优化GC性能
Java中的堆可以处于物理上不连续的内存空间,只要逻辑上是连续的就行
-XX:PermSize
和 -xx:MaxPermSize
来设置永久代参数-XX:MetaspaceSize
和 -xx:MaxMetaspaceSize
来设置元空间参数方法区被各个线程共享
只是一个逻辑部分,各个厂商有不同的实现
Java7(永久代)和Java8(元空间)的方法区存储类型不一样
既不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范定义的内存区域
JDK8将方法区(永久代)移除,使用元数据区(元空间)来代替,并将元数据区从虚拟机运行时数据区移除,转移到了本地内存中,受本机物理内存的限制
当申请的内存超过了本机物理内存时,才会抛出OutOfMemoryError异常
Java虚拟机在执行的时候会把管理的内存分配到不同的区域,这些区域称为虚拟机内存;
对于虚拟机没有直接管理的物理内存,也会有一定的利用,被利用但不在虚拟机内存的地方称为本地内存。
无论是对象的访问定位,还是对象是否可以被回收的判断等,都离不开引用。
Java中虚拟机HotSpot通过直接引用来访问Java对象的,直接引用就是说指针是直接指向对象实例的
若想要获取对象的类型数据信息,则需要再调用对象里维护的类型数据指针
Object obj = new Object()
,这种new产生的引用就是强引用引用计数法会为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1
优点:实现简单
缺点:
每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响
存在循环引用问题,所谓循环引用就是当A引用B,B同时引用A时会出现对象无法回收的问题
A、B实例对象在栈上已经没有变量引用了,由于计数器还是1无法回收,出现了内存泄漏
可达性分析算法指的是如果从某个普通对象到GC Root对象是可达的(也就是普通对象能通过引用链找到GC Root对象),对象就不可被回收;若引用链不存在就可以被回收。
(一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,即GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的)
优点:
可达性分析算法将对象分为两类,且对象与对象之间存在引用关系:
JM XBean
、JVM TI
中注册的回调、本地代码缓存等当对堆进行部分内存区域回收的时候,就会存在跨区域引用的问题。
若存在跨区域的引用关系,那么这种引用即便不是固定GC Roots范畴,也应该被纳入GC Roots集合的补充,一起进行可达性分析判断
(例如所有堆内存被划分为(A、B、C、D、E)五个区,当我们这次只对A、B进行回收时,就需要判断C、D、E中是否有引用A、B中的对象)
查找跨区的引用关系:
全区域扫描
记忆集
列出了从外部指向本块的所有引用,这种引用记录会在引用关系创建,更改时进行维护。
当需要进行这种外部引用关系分析时,直接读取记忆集中的内容就行
一般来说
检查对象是否被引用,若对象被引用了,则说明该对象还在使用,不允许被回收
具体是否需要执行垃圾回收Java虚拟机会自行判断
优点:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象
缺点:碎片化问题,内存可能会出现细小的可用内存单元;
分配速度慢,由于内存碎片的存在,需要维护空闲链表,极有可能每次需要遍历到链表的最后才能获得合适的内存空间
优点:吞吐量高,只需要遍历存活对象并复制到To空间
没有碎片化,在复制对象后按顺序放入To空间,不存在碎片化内存空间
缺点:内存使用效率低:每次只能让一半的内存空间来为创建对象使用
性能比标记-整理算法好,但是不如标记-清除算法
优点:内存使用效率高,整个堆内存都可用,不会像复制算法一样只使用半个堆内存
没有碎片化,在整理阶段将对象向内存的一侧移动,剩下的空间都是可以分配对象的有效空间
缺点:整理阶段效率不高,整理算法有多种,但是有的整理算法性能不高,可以使用较为高效的整理算法
采用分代收集算法,根据对象存活周期将内存划分为几块,不同块采用适当的收集算法
一般分为新生代和老年代
新生代每次在垃圾收集时都发现有大批对象死去,而每次会收回存活的少量对象,将会逐步晋升到老年代中存放
新生代:
老年代:
分代回收时,创建出来的对象,首先会被放入Eden伊甸园区
随着对象在Eden区越来越多,若Eden区满,新创建的对象已经无法放入,就会触发年轻代的GC,称为Minor GC或者Young GC
Minor GC会把需要Eden和From需要回收的对象回收,把没有回收的对象放入To区
接下来,S0会变成To区,S1会变成From区,当Eden区满时再往里放入对象,依然会发生Minor GC
此时会回收Eden区和S1(from)中的对象,并把eden和from区中剩余的对象放入S0
注意:每次Minor GC都会为对象记录他的年龄,初始值为0,每次GC完加1
如果Minor GC后对象的年龄达到阈值(最大15,默认值和垃圾回收器有关),对象就会被晋升至老年代
当老年代中空间不足,无法放入新的对象时,现场时Minor GC如果还是不足,就会触发Full GC,Full GC会对整个堆进行垃圾回收
若Full GC依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory异常
Serial是一种单线程串行回收年轻代的垃圾回收器
回收年代及算法:年轻代-复制算法
优点:单CPU处理器下吞吐量非常出色
缺点:多CPU下吞吐量不如其他垃圾回收器,堆若偏大会让用户线程处于长时间的等待
适用场景:Java编写的客户端程序或硬件配置有限的场景
(运行在客户端模式下的默认新生代收集器)
-XX:+UseSerialGC
新生代、老年代都使用串行回收器-XX:+UseParNewGC
新生代使用ParNew回收器,老年代使用串行回收器CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程等待的时间
XX:+UseConcMarkSweepGC
回收年代及算法:老年代-标记-清除算法
优点:系统由于垃圾回收出现的停顿时间较短,用户体验好
缺点:
内存碎片问题
CMS会在Full GC时进行碎片的整理,但是会导致用户线程暂停,可以使用
-XX:CMSFullGCsBeforeCompaction=N 参数(默认为0)调整N次Full GC之后再整理
退化问题:在某些特定的情况下会退化成Serial Old的单线程垃圾回收器
若老年代内存不足无法分配对象,CMS就会退化成Serial Old单线程回收老年代
浮动垃圾问题:在清理过程中,有些垃圾可能会回收不掉
无法处理在并发清理过程中产生的”浮动垃圾“,不能做到完全的垃圾回收
适用场景:大型的互联网系统中用户请求数据量大、频率高的场景,比如订单接口、商品接口等
-XX:MaxGCPauseMillis=n
设置每次垃圾回收时的最大停顿毫秒数-XX:GCTimeRatio=n
设置吞吐量为n(用户线程执行时间 = n / n+1)-XX:+UseAdaptiveSizePolicy
设置可以让垃圾回收器根据吞吐量和最大停顿的毫秒数自动调整内存大小-XX:+UseParallelGC
或 -XX:+UseParallelOldGC
可以使用Parallel Scavenge+Parallel Old这种组合JDK9之后默认的垃圾回收器是G1(Garbage First)垃圾回收器
Parallel Scavenge关注吞吐量,允许用户设置最大暂停时间,但是会减少年轻代可用空间的大小
CMS关注暂停时间,但是吞吐量方面会下降
G1则是将两种垃圾回收器的优点融合
支持巨大的堆空间回收,并有较高的吞吐量
支持多CPU并行垃圾回收
允许用户设置最大暂停时间
G1的整个堆会被划分成多个大小相等的区域,称之为区Region(以Region作为单位),区域不要求是连续的
分为Eden、Survivor、Old区
Eden区和Survivo区结合起来就是年轻代
所有的Old区结合在一起就是老年代
Region的大小通过堆空间大小/2048计算得到,也可以通过参数 -XX:G1HeapRegionSize=32m
指定(其中32m指定region大小为32M),Region size必须是2的指数幂,取值范围从1M到32M
Region的跨区域引用使用记忆集的方式,通过记录别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内
如何保证收集线程与用户线程互不干扰地运行
回收过程中改变对象的引用关系,必须保证其不能打破原本的对象图结构,导致标记结果出现错误
G1收集器是通过原始快照算法SATB(标记时对象之间的引用状态不变,即使有改变也不在本轮进行垃圾回收)来实现的
回收过程中新创建对象,G1为每一个Region设计了两个名为TAMS的指针,将Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置的范围内
G1垃圾回收的两种方式
年轻代回收(Young GC),回收Eden区和Survivor区中不用的对象。会导致STW,G1中可以通过参数
-XX:MaxGcPauseMillis=n(默认200)
设置每次垃圾回收时的最大暂停时间毫秒数,G1垃圾回收器会尽可能地保证暂停时间
混合回收(Mixed GC)
新创建地对象会存放在Eden区,当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行Young GC
标记处Eden 和 Survivor区域中的存活对象
根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域
G1在进行Young GC的过程中会去记录每次垃圾回收时每个Eden区和Survivor区的平均耗时,以作为下次回收时的参考依据。这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域
后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬到另一个Survivor区
当某个存活对象的年龄到达阈值(默认15)将会被放入老年代
部分对象若大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就会被放入Humongous区,若对象过大会横跨多个Region
多次回收之后,会出现很多Old老年代区,此时总堆占有率达到阈值时(-XX:InitiatingHeapOccupancyPercent默认45%)会触发混合回收MixedGC。回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法完成
G1堆老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是G1名称的由来
最后清理阶段使用复制算法,不会产生内存碎片
注意:
Class文件主要包括如下的数据类型
文件无法通过文件扩展名来确定我呢见类型,文件扩展名可用随意修改,不会影响文件的内容
软件是使用文件的字节头(文件的前几个字节)去校验文件的类型,若软件不支持该种类型就会出错
文件类型 | 字节数 | 文件头 |
---|---|---|
JPEG(jpg) | 3 | FFD8FF |
PNG(png) | 4 | 89504E47 |
bmp | 2 | 424D |
XML(xml) | 5 | 3C3F786D6C |
AVI(avi) | 4 | 41564920 |
Java字节码文件(.class) | 4 | CAFEBABE |
在Java字节码文件中,将文件头称为Magic魔数
主副版本号指的是编译字节码文件的JDK版本号
主版本号用来标识大版本号,JDK1.0-1.1使用了45.0-45.3,JDK1.2是46之后每升级一个大版本就加1;
副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号(主版本号-44 = 当前的JDK版本)
版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容
高版本JDK可以执行低版本文件,但是低版本JDK不能执行高版本文件
紧接着主、次版本号之后是常量池入口
常量池是class文件中关联最多的数据类型,也是占用class文件最大的数据项之一
也是第一个出现的表类型数据项目
主要存放两类常量
主要是确定该类型的继承关系,即继承了哪些父类,实现了哪些接口等
类索引和父类索引都是一个u2数据类型(存储无符号的2字节整数),而接口索引是一个u2类型的数据集合
(一个类可以继承一个父类,但是可以实现多个接口)
描述了一个类从加载–>使用–>卸载的整个过程
**文件格式验证,**比如文件是否以0xCAFEBABE开头,主次版本号是否满足当前Java虚拟机版本要求
**元信息验证,**比如类必须有父类,默认有父类Object
**验证程序执行指令的语义,**比如方法内的指令执行中跳转到其他方法总
**符号引用验证,**比如是否访问了其他类中private的方法等
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
将使用符号描述所引用的目标转为直接引用目标的地址
使用一组符号来描述所引用的目标,与虚拟机实现的内存布局无关,各种虚拟机实现的内存布局可以各不相同,但是能接受的符号引用必须都是一致,需要符合Java虚拟机规范
是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,若有了直接引用,那引用的目标必定已经在虚拟机的内存中存在
初始化阶段是为类的静态变量赋予正确的初始值(准备阶段是将变量赋零值)
JVM负责对类进行初始化,主要是对类变量进行初始化
对类变量进行初始值的设定,主要有两种方式:
声明类变量是指定初始值
public static int value = 250
使用静态代码块是为类变量指定初始值
public static int value;
static {
value=250
}
通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性
双亲委派机制让一个类只能被同一个类加载器加载,就可以避免同一个类被多次加载,减少加载过程中的性能开销
当类加载器发现一个类在自己的加载路径中就会去加载这个类,
若没有发现,则从下往上继续查找是否被加载过,
若发现被加载了则直接将这个类进行加载,
若一直到最顶层的类加载器都没有被加载,则由顶向下进行加载。
若所有类加载器都无法加载该类,则会抛出类无法找到的错误
//获取main方法所在类的类加载器,应用程序类加载器
ClassLoader classLoader = Demo.class.getClassLoader();
System.out.println(classLoader);
//使用应用程序类加载器加载 com.aaa.bbb.CCC
Class<?> clazz = classLoader.loadClass("com.aaa.bbb.CCC");
System.out.println(clazz.getClassLoader());
每个java实现的类加载器中保存类一个成员变量叫"父"(Parent)类加载器,可以理解为是其上级而不是继承关系
应用程序类加载器的父类加载器是扩展类加载器
扩展类加载器的父类因为启动类加载器使用C++编写,获取不到,所以为null
启动类加载器使用C++编写,没有父类加载器
JDK8及之前的版本中
JDK9引入了module
启动类加载器使用java编写
扩展类加载器被替换成了平台类加载器
-Xmx4g -Xms4g
-XX :+UserG1GC -XX:MaxGCPauseMillis=50
-XX :ParallelGCThreads=4
-XX :+PrintGCDetails -XX: +PrintGCDateStamps
-Xloggc : gc.log
-XX: MaxMetaspaceSize=2g
-Xss1m
-XX :+HeapDumpOnOutOfMemoryError
-XX :HeapDumpPath=/usr/local/