类加载:
- 示例:
public class ServerA{
public static int flushInterval;
public static ServerB serverB = new ServerB();
}
public class ServerB{
}
-
Java代码运行在JVM上的,JVM想要执行Java代码,首先需要将代码编译成class文件;
-
然后再使用类加载器将class字节码文件加载到JVM内存中,类加载器遵循双亲委派机制;
- 启动类加载器(Bootstrap ClassLoader):
主要是负责加载我们在机器上Java安装目录下的“lib”目录中的核心类库; - 扩展类加载器(Extension ClassLoader):
主要是负责加载我们在机器上Java安装目录下的“lib\ext”目录中的类; - 应用程序类加载器:
这类加载器就负责去加载“ClassPath”环境变量所指定的路径中的类其实你大致就理解为去加载你写好的Java代码吧,这个类加载器就负责加载你写好的那些类到内存里; - 自定义类加载器:
除了上面那几种之外,还可以自定义类加载器,去根据你自己的需求加载你的类; - 双亲委派机制:
JVM的类加载器是有亲子层级结构的,就是说启动类加载器是最上层的,扩展类加载器在第二层,第三层是应用程序类加载器,最后一层是自定义类加载器;
假设你的应用程序类加载器需要加载一个类,他首先会委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载,但是如果父类加载器在自己负责加载的范围内,没找到这个类,那么就会下推加载权利给自己的子类加载器;
- 启动类加载器(Bootstrap ClassLoader):
-
类加载过程分几步,分别是加载,验证,准备,解析,初始化
- 加载
加载的话就是刚才说的用类加载器去加载类,将class文件加载至java虚拟机,并存储在方法区。方法区存储类信息、常量、静态变量; - 验证
这一步就是根据Java虚拟机规范,来校验你加载进来的“.class”文件中的内容,是否符合指定的规范,假如说,你的“.class”文件被人篡改了,里面的字节码压根不符合规范,那么JVM是没法去执行这个字节码的,所以把“.class”加载到内存里之后,必须先验证一下,校验他必须完全符合JVM规范,后续才能交给JVM来运行; - 准备
主要是给类分配一定的内存空间,然后给他里面的类变量(也就是static修饰的变量)分配内存空间,来一个默认的初始值,比如上面的示例里,就会给“ServerA.flushInterval”这个类变量分配内容空间,给一个“0”这个初始值; - 解析
主要是给符号引用变成直接引用,就是把一些变量什么temp,直接换成物理地址,不然执行的时候JVM也不认识temp是啥 - 初始化
在初始化阶段时,才会执行类的初始化代码,主要是给类变量赋值,准备阶段只是设置了初始值,这个是核心阶段,执行类的初始化,如果发现这个类的父类没有初始化,会先暂停,然后去初始化父类,也是走类加载的一套流程,直到父类加载完了,再执行子类的初始化
以上就是类大概的加载的过程,加载的类是放在JVM的元空间(Metaspace),在JDK 1.8以前的版本里叫方法区,如下图
- 加载
字节码执行过程:
- 示例:
public class ServerA{
public static void main(String[] args) {
ServerB serverB = new ServerB();
serverB.test();
}
}
public class ServerB{
public String test(){
String temp = "test";
return temp;
}
}
- 类加载到永久代之后,会使用字节码执行引擎去执行我们所写的代码编译出来的字节码指令;
我们写好的Java代码会被翻译成字节码,对应各种字节码指令,所以当JVM加载类信息到内存之后,实际就会使用自己的字节码执行引擎,去执行我们写的代码编译出来的代码指令,那么在执行字节码指令的时候,JVM里就需要一个特殊的内存区域了,那就是“程序计数器”,这个程序计数器就是用来记录当前执行的字节码指令的位置的; - 在执行字节码指令时,需要一个特殊的内存区域程序计数器去记录当前执行的字节码指令的位置; 由于JVM是支持多线程的,代码可能会被多线程并发执行,因此每个线程都会有自己的一个程序计数器,专门记录当前这个线程目前执行到了哪一条字节码指令了;
- 每个线程都有自己的Java虚拟机栈,当具体某个线程执行到某个方法时,首先会给这个方法创建一个栈帧,将栈帧压入线程的虚拟机栈中;栈帧里就有这个方法的局部变量表 、操作数栈、动态链接、方法出口等东西;
- 局部变量表: 是一组变量值的存储空间,用呀存放方法参数和局部变量,虚拟机通过索引定位的方式使用局部变量表;
- 操作数栈: 是一个后入先出栈。方法执行中进行算术运算或者是调用其他的方法进行参数传递的时候是通过操作数栈进行的;
- 动态链接: 在虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成是每个方法的间接引用,如果代表栈帧A的方法想调用代表栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,但是因为符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用才可以访问到真正的方法,这时候就有一点需要注意,如果符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析,如果是在运行期间转换为直接引用,那么这种转换就成为动态连接;
- 方法出口:
1、当方法正常结束时,即执行方法返回的字节码指令,有可能会返回值给该方法的调用者,此时的调用者的程序计数器值可以为该方法的返回地址;
2、当方法执行遇到没有捕获的异常时,返回地址有异常处理器来决定;
字节码执行过程如下图:
- 当方法执行完毕,栈帧出栈,里面的局部变量直接就从内存里清理掉了,局部变量指向堆内存的引用也随之失效;
- 这里的栈帧如果没有执行完时,其实都是GC Root,垃圾回收时,就是根据这里的局部变量的引用和永久代的引用来判断对象是否存活;
- 当引用失效,堆内存实例则会变成"垃圾"对象,JVM本身是有垃圾回收机制的,他是一个后台自动运行的线程;当触发垃圾回收机制,垃圾对象就会被清理掉,释放内存;如图:
对象创建
- 对象创建的方式有如下几种
- 使用new关键字创建对象
ServerB serverB = new ServerB();
- 使用Class类的newInstance方法(反射机制)
ServerB serverB = (ServerB)Class.forName("ServerB类全限定名").newInstance(); 或者: ServerB serverB = ServerB.class.newInstance();
- 使用Constructor类的newInstance方法(反射机制)
Constructor<ServerB> constructor = ServerB.class .getConstructor(); ServerB serverB = constructor.newInstance();
- 使用Class类的newInstance方法(反射机制)
ServerB serverB = (ServerB)Class.forName("ServerB类全限定名").newInstance(); 或者: ServerB serverB = ServerB.class.newInstance();
- 使用Clone方法创建对象
Constructor<ServerB> constructor = ServerB.class .getConstructor(); ServerB serverB = constructor.newInstance(); ServerB serverB1 = (ServerB)serverB.clone();
- 使用(反)序列化机制创建对象
Constructor<ServerB> constructor = ServerB.class .getConstructor(); ServerB serverB = constructor.newInstance(); // 写对象 ObjectOutputStream output = new ObjectOutputStream( new FileOutputStream("serverB.bin")); output.writeObject(serverB); output.close(); // 读对象 ObjectInputStream input = new ObjectInputStream(new FileInputStream( "serverB.bin")); ServerB serverB1 = (ServerB) input.readObject();
- 使用new关键字创建对象
- 对象创建过程
-
类加载检查
JVM遇到一条new指令时,首先检查这个指令的参数是否能在方法区(元空间)的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类的加载过程; -
对象分配内存
对象所需内存的大小在类加载完成后便完全确定(对象内存布局),为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来;- 根据Java堆中是否规整有两种内存的分配方式
Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定;- 规整,指针碰撞(Bump the pointer)
Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。例如:Serial Old、ParNew Old等收集器(标记压缩算法); - 不规整,空闲列表(Free List)
Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。例如:CMS这种基于Mark-Sweep算法的收集器
- 规整,指针碰撞(Bump the pointer)
- 并发问题
在分配内存的过程中,如果在给对象a分配内存的同时指针还没来得及修改,对象b同时也使用了原来的这个指针来分配内存。就会出现一块内存分配给了两个对象,即并发问题; - 解决方案
- CAS同步
对象在进行分配内存之前会将该线程期望的指针值与内存上的原有的指针值进行比较,如果相同,则先修改内存上的指针值,即向后移动该对象大小的空间,然后存放对象。如果不相同,则返回现在的指针值,然后重试以上操作; - 本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)
把内存分配的动作按照线程划分为在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定,这样可以避免线程同步,提高对象分配的效率;虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定;
- CAS同步
- 根据Java堆中是否规整有两种内存的分配方式
-
内存空间初始化
即给对象赋默认值(零值),如果使用了TLAB,这一工作过程也可以提前至TLAB分配时进行 -
设置对象头
对象由对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)三部分组成;- 对象头:包含了该对象的必要信息,不仅包括该对象的一些类元信息地址指针,还包含其他信息,比如对象的分代年龄,锁信息等;
- 实例数据:对象中各个实例字段的数据;
- 对齐填充:其中为了保持对象内存的整齐性,还包括了对齐指针来进行对对象的内存大小的填充
-
执行init()方法
为对象赋用户自己的值,执行构造方法; -
引用对象
对象实例化完毕后,把栈中的变量引用地址指向堆内存中的地址;
以上就是对象大概创建的一个过程,如下图:
-
内存分配策略
- JVM堆内存分新生代和老年代,默认比例1:2,新生代又分为Eden、Survivor0(Survivor From)、Survivor1(Survivor To)三个区域,默认比例8:1:1,如下图;
- 当对象被实例化,会根据内存分配策略将对象分配到具体的区域
-
优先分配到Eden区
对象优先都会在Eden区进行分配,如果Eden区内存不够,则会触发Minor GC,使用复制算法将Eden区和Survivor From区存活的对象复制到Survivor To区,再清空Eden区和Survivor From区;GC过后,“From” 和 “To”会交换他们的角色,保证名为To的Survivor区域是空的; -
躲过15次GC之后进入老年代
每次Minor GC过后,存活对象年龄都会+1,默认设置下,当对象年龄达到15岁时,也就是躲过15次GC就会移动到老年代(年龄阈值可以通过-XX:MaxTenuringThreshold来设置); -
动态对象年龄判断
JVM并不是要求对象年龄必须达到MaxTenuringThreshold才能晋升老年代,当一批对象总大小大于Survivor From区域的50%时,此时大于等于这批对象年龄的对象,就可以直接进入老年代(年龄1+年龄2+…+年龄n,这样多个年龄对象累加总和大于Survivor From区域50% 年龄n以上的对象直接进入老年代);- 动态年龄判断时机
新生代GC过后,要转移存活对象进入survivor区之前
GC过后,在转移存活对象到survivor区之前,会根据动态年龄规则判断survivor from区当前存活对象是否大于survivor from区的50%空间,然后将大于等于这个年龄的对象分配到老年代,然后把本次GC后存活的对象和当前使用的那块survivor from中剩余的存活对象放入那块空闲的survivor to区,最后将eden区和当前使用的survivor from区清空并交换survivor from区、survivor to区角色,保证survivor to区是空的;
- 动态年龄判断时机
-
大对象直接进入老年代
虚拟机提供了一个参数:-XX:PretenureSizeThreshold,当对象大小大于该值时,该对象就会直接被分配到老年代中(该参数只对Serial和ParNew垃圾收集器有效); -
老年代空间分配担保
任何一次Minor GC发生之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间;- 如果大于:则此次Minor GC是安全的;
- 如果小于:则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则触发一次Full GC;
要是Full GC之后,老年代还是没有足够空间存放Minor GC过后的剩余存活对象,那么此时就会导致"OOM"内存溢出; - 空间担保的目的
如果Minor GC后存活对象太多无法放入Survivor区域,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间;
-
逃逸分析与栈上分配
栈上分配是jvm的一个优化技术,对于那些线程私有的对象,可以将它们分配在栈上,而不是堆上。栈上分配的好处是不需要GC介入去回收这个对象,出栈即释放资源,可以提高性能;
注意: 使用栈上分配策略除了需要开启标量替换,还需要开启逃逸分析;- 标量替换(-XX:+DoEscapeAnalysis):比如我们的实例user中有两个字段,就把这个实例认作它内部的两个字段以局部变量的形式分配在栈上也就是打散,这个操作称为:标量替换
- 逃逸分析(-XX:+EliminateAllocations):分析对象是否发生逃逸,只要是被多个线程共享的对象就是逃逸对象;
总结:
1、小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上;
2、直接分配在栈上,可以自动回收,减轻GC压力;
3、大对象或者逃逸对象无法栈上分配;以上就是内存分配策略过程如下图:
-
垃圾回收-什么情况下JVM内存中的一个对象会被垃圾回收
-
确定垃圾对象
-
引用计数法:
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值就减1;计数器为0,说明对象没有被使用,但是JVM一般不采用引用计数法进行GC,引用计数法会存在循环依赖问题;
如下示例:public class ServerA { private Object ref; public static void main(String[] args) { ServerA obj1 = new ServerA(); ServerA obj2 = new ServerA(); obj1.ref = obj2; obj2.ref = obj1; obj1 = null; obj2 = null; } }
-
可达性分析算法(GC ROOT)
可达性分析算法可以解决循环依赖问题;
其算法思路就是通过GC Root的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Root间没有任何引用链相连,则证明此对象是不可用的;- 哪些是GC ROOT
线程栈变量、静态变量、常量池、JNI指针(native方法用到的对象)
- 哪些是GC ROOT
-
-
java中对象不同的引用类型
- 强引用
只要是强引用的类型,那么垃圾回收的时候绝对不会去回收这个对象;ServerA serverA = new ServerA();
- 软引用
将"ServerA"实例对象用"SoftReference"软引用类型包裹起来,此时"serverA "变量对"ServerA"对象就是软引用类型, 可使用get()获取对象;SoftReference<ServerA> serverA = new SoftReference(new ServerA()); ServerA obj = serverA .get();
正常情况下垃圾回收是不会回收软引用对象的,但是如果在进行垃圾回收之后发现内存空间还是不够存放新的对象,内存都快溢出了,此时就会把软引用对象给回收掉,哪怕他被变量引用了,因为他是软引用类型,所以依然会被回收掉; - 弱引用
将"ServerA"实例对象用"WeakReference"软引用类型包裹起来,此时"serverA "变量对"ServerA"对象就是弱引用类型, 可使用get()获取对象;WeakReference<ServerA> serverA = new WeakReference(new ServerA()); ServerA obj = serverA .get();
如果发生垃圾回收,就会把弱引用对象回收掉; - 虚引用
虚引用是每次垃圾回收的时候都会被回收ServerA serverA = new ServerA();
与软引用,弱引用不同,虚引用指向的对象十分脆弱,不可以通过get方法来得到其指向的对象。它的唯一作用就是当其指向的对象被回收之后,自己被加入到引用队列,用作记录该引用指向的对象已被销毁;
- 强引用
-
finalize()方法
在讲完上述GC ROOT 和 引用类型之后,基本上可以知道哪些对象可回收,哪些对象不可回收;
- 有GC ROOT 引用的对象不能回收;
- 没有GC ROOT引用的对象可回收;
- 如果有GC ROOT引用,但是是软引用或者弱引用也有可能被回收掉;
但是,没有被GC ROOT引用的对象,不一定立马会被回收,如下代码所示:public class ServerA { private static ServerA instance; @Override protected void finalize() throws Throwable { ServerA.instance = this; } }
重写finalize()方法可以进行自我拯救;
- finalize自我拯救
- 一旦垃圾收集器准备释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存;
- 如果对象没有被 GC Roots 引用,那他将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法;对象没有覆盖 finalize 方法,或者 finalize 方法已经被虚拟机调用过,虚拟机将这两种情况都视为 “没有必要执行”。也就是说,finalize 方法只会被执行一次;
- 如果对象被判定为有必要执行 finalize 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个虚拟机自动建立的低优先级为 8 的 Finalizer 线程去执行它;
- finalize 方法是对象逃脱死亡命运的最后一道关卡;稍后 GC 将对队列中的对象进行第二次规模的标记,如果对象要在 finalize 中 “拯救” 自己,只需要将自己关联到引用上即可,通常是 this。如果这个对象关联上了引用,那么在第二次标记的时候他将被移除出 “即将回收” 的集合;如果对象这时候还没有逃脱,那基本上就是真的被回收了;
- finalize自我拯救