赞
踩
有三种类加载器
双亲委派机制是当一个类收到类加载请求后,它首先不会自己尝试加载这个类,而是把这个请求交给父类,每一层加载类都是这样,会一直从自定义类加载器往上找,只有当父类加载器反馈自己无法完成的时候,子类加载器才会去加载。
例如:因为java自带String类,如果自己写了个父类的话,类加载器会找到java自带的String类,为了保证User编写的代码不污染java自带的代码,因此就有了双亲委派机制,保证了安全,确保使用不同的类加载器最终得到的都是同一个object对象。
防止多份同样字节码的加载
Java中的类加载机制指虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用、卸载七个阶段。类加载机制的保持则包括前面五个阶段。
加载
加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
验证
验证的作用是确保被加载的类的正确性,包括文件格式验证,元数据验证,字节码验证以及符号引用验证。
准备
准备阶段为类的静态变量分配内存,并将其初始化为默认值。假设一个类变量的定义为public static int val = 3;那么变量val在准备阶段过后的初始值不是3而是0。
解析
解析阶段将类中符号引用转换为直接引用。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。
初始化
初始化阶段为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
JVM中的内存主要划分为5个区域,即方法区,堆内存,程序计数器,虚拟机栈以及本地方法栈
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ruJt43hx-1605091706617)(/Users/jiangcheng/Java-Notes/图片/JVM图/Jvm结构图.png)]
VM的内存可以分为堆内存和非堆内存,堆内存分为新生代和老年代。年轻代又可以进一步划分为一个Eden(伊甸)区和两个Survivor(幸存)区组成。
JVM初始分配的堆内存由**-Xms指定,默认是物理内存的1/64。JVM最大分配的堆内存由-Xmx**指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。因此我们一般设置-Xms和-Xmx相等以避免在每次GC 后调整堆的大小。
通过参数**-Xmn2G** 可以设置年轻代大小为2G。通过**-XX:SurvivorRatio**可以设置年轻代中Eden区与Survivor区的比值,设置为8,则表示年轻代中Eden区与一块Survivor的比例为8:1。注意年轻代中有两块Survivor区域。
JVM使用**-XX:PermSize** 设置非堆内存初始值,默认是物理内存的1/64。由**-XX:MaxPermSize**设置最大非堆内存的大小,默认是物理内存的1/4。
创建的对象会优先在Eden分配,如果是大对象(很长的字符串数组)则可以直接进入老年代。虚拟机提供一个**-XX:PretenureSizeThreshold参数**,令大于这个参数值的对象直接在老年代中分配,避免在Eden区和两个Survivor区发生大量的内存拷贝。
另外,长期存活的对象将进入老年代,每一次MinorGC(年轻代GC),对象年龄就大一岁,默认15岁晋升到老年代,通过**-XX:MaxTenuringThreshold设置晋升年龄。**
管理方式:栈自动释放,堆要GC
空间大小:栈比堆小
碎片相关:栈产生的碎片远小于堆
分配方式:栈支持静态和动态分配,而堆是支持动态分配
效率:栈的效率比堆高
字符串常量池在jdk1.6的时候放在方法区,由于在方法区,内存回收效率不高,容易导致内存溢出问题。因此在jkd1.8的时候把字符串常量池从方法区移出,移动了堆。字符串常量池物理上存放在堆,但是逻辑上还是在方法区。
如果字符串常量池放在方法区中,那么每次修改字符串都会产生一个新的对象,而方法区的垃圾回收效率很低,产生过多字符串对象很容易发生OOM错误,因此把字符串常量池放到堆中,方便垃圾回收字符串对象。
java8永久代移除了,被元空间取代了,但是元空间本质和永久代差不多。
元空间和永久代最大区别是:永久代使用jvm堆内存,但是java8以后元空间不在虚拟机,而是使用本机的物理内存,元空间仅受本地内存大小限制
字符串在永久代中,容易出现性能问题和内存溢出的问题。类和方法的信息等比较难确定大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出。使用元空间则使用了本地内存。
判断一个对象是否应该被回收,主要是看其是否还有引用。判断对象是否存在引用关系的方法包括引用计数法以及root根搜索方法。
是一种比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理循环引用的问题,如A引用B,B引用A,双方计数都是1,导致无法回收。
root搜索方法的基本思路就是通过一系列可以做为root的对象作为起始点,从这些节点开始向下搜索。当一个对象到root节点没有任何引用链接时,则证明此对象是可以被回收的。以下对象会被认为是root对象:
如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表一个引用。JDK1.2以后将引用分为强引用,软引用,弱引用和虚引用四种。
可以通过引用计数法和可达性分析算法判断对象是否已经死亡
原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理循环引用的问题,如A引用B,B引用A,双方计数都是1,导致无法回收。
根对象(即GC Root)作为起始点集,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Root没有引用链相连、则证明此对象是不可能再被使用的。
可以为作为GC Root对象的是
要判断一个对象是否真正死亡,要经历两次标记过程
对象和引用链的任何对象建立联系即可,如改对象被其他类或者对象成员变量引用。
当内存不足,JVM开始垃圾回收,对于强引用的对象,就算出现OOM也不会对该对象进行回收
public class jinfo {
public static void main(String[] args) throws InterruptedException {
Object o1=new Object();
Object o2=o1;
System.gc();
System.out.println(o1);
System.out.println(o2);
}
}
当系统内存充足 不会被回收, 当系统内存不充足 会被回收,一般用于高速缓存
package JVM; import java.lang.ref.SoftReference; public class jinfo { public static void softRef_Memory_Enough() { Object o1=new Object(); SoftReference<Object> softReference = new SoftReference<>(o1); System.out.println(o1); System.out.println(softReference.get()); //内存够用,不会被回收 o1=null; System.gc(); System.out.println(o1); System.out.println(softReference.get()); } public static void softRef_Memory_NotEnough() { Object o1=new Object(); SoftReference<Object> softReference = new SoftReference<>(o1); System.out.println(o1); System.out.println(softReference.get()); //内存不够用,会被回收 o1=null; try { byte[] bytes = new byte[30 * 1024 * 1024]; } catch (Throwable throwable) { throwable.printStackTrace(); }finally { System.out.println(o1); System.out.println(softReference.get()); } } public static void main(String[] args) throws InterruptedException { // softRef_Memory_Enough(); softRef_Memory_NotEnough(); } }
只要垃圾回收机制一运行,不管内存多少 一定会回收
package JVM; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; public class jinfo { public static void main(String[] args) throws InterruptedException { Object o1 = new Object(); WeakReference<Object> weakReference = new WeakReference<>(o1); System.out.println(o1); System.out.println(weakReference.get()); o1=null; System.gc(); System.out.println(o1); System.out.println(weakReference.get()); } }
原来的HashMap key为空也不会被gc回收
但是WeakHashMap,当key为空时,gc后,会回收这个map
package JVM; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.util.WeakHashMap; public class jinfo { public static void main(String[] args) throws InterruptedException { WeakHashMap<Integer, String> map = new WeakHashMap<>(); Integer key = new Integer(2); String value = "WeakHashMap"; map.put(key, value); System.out.println(map); key = null; System.out.println(map); System.gc(); System.out.println(map); } }
虚引用形同虚设,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
虚引用要配合引用队列来操作
package JVM; import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.util.WeakHashMap; public class jinfo { public static void main(String[] args) throws InterruptedException { Object o1 = new Object(); ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); //虚引用可以用 弱引用另一外一种调用方法代替 // WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue); //虚引用 PhantomReference<Object> phantomReference = new PhantomReference<>(o1, referenceQueue); System.out.println(o1); System.out.println(phantomReference.get()); System.out.println(referenceQueue.poll()); o1=null; System.gc(); Thread.sleep(500); System.out.println(); System.out.println(o1); System.out.println(phantomReference.get()); //调用gc后,虚引用进入 引用队列 System.out.println(referenceQueue.poll()); } }
垃圾回收算法可以分为引用计数式垃圾收集和追踪式垃圾收集。
Minor GC 目标只是新生代的垃圾回收
Major GC 目标只是老年代的垃圾回收,目前只有CMS收集器会有单独回收老年代
Mixed GC 目标是整个新生代和部分老年代的垃圾回收,目前只有G1收集器有这种行为
Full GC 收集整个堆和方法区
原理:首先标记出所有需要回收的对象,在标记完成后,统一回收所有被标记的对象。或者标记出所有存活的对象,在标记完成后,统一回收所有没有被标记的对象。
缺点
原理:将可用内存分为大小相等的两块,每次只使用其中一块,当这一块内存使用完后,则把内存还存活的对象复制到另外一块内存空间,把已使用的那一块清理掉。如果内存中多数对象都可以回收,那么算法只需要复制少量对象。每次分配都是针对整个半区进行内存回收,分配时,不需要考虑内存空间碎片化问题。分配内存的时候只需要移动栈顶指针,按需分配。
缺点
原理:标记过程和标记清除算法一样,标记完成把存活对象都行内存空间一端移动,然后清理掉边界以外的内存。与标记清除算法本质区别是,标记清除是一种非移动式的回收算法,标记整理是一种移动式的回收算法。
缺点:不管是否移动对象都存在弊端,移动则内存回收更复杂,不移动则内存分配的时候会更复杂,虽然不移动对象,则停顿时间更短,但是吞吐量会降低。移动对象吞吐量会提升,但是要耗费大量时间。总体来说内存分配比内存访问频率要高很多,因此移动对象总体效率更高。
主要有两种垃圾回收方式
常量池回收(废弃常量,比如一个常量不在任何地方被引用,则被标记为废弃常量,这个常量可以回收)
对无用类的回收(类卸载)
大多数情况下,对象在新生代Eden区分配,当在Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
可以查看正在运行的虚拟机进程
用于监视虚拟机各种运行状态,可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即使编译等运行数据。命令为 jstat [option vmid [interval[s|ms] [count]]]
例子 jstat -gc 200 100 20 代表每100毫秒查询一次进程id为200的垃圾收集状况,一共查询20次。如果省略
100和20,代表只查询一次。
实时查看和调整虚拟机各项参数。
jinfo [option]
用于生成堆转储快照 jmap [option]
option取值
选项 | 作用 |
---|---|
-dump | 生产java转储快照,格式为-dump:[live,]format=b,file=,其中live子参数说明是否只dump出存活对象。 |
-heap | 显示java堆详细信息,如使用哪种回收器,参数配置,分代状况。只在Linux/Solaris有效 |
-histo | 显示堆中对象统计信息,包括类,实例数量,合集容量 |
用来分析jmap生成的堆转储快照。现在一般不用jhat,而用VisualVM
用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机每一条线程正在执行的方法堆栈的集合,生成线程快照的目的是定位线程出现长时间停顿的原因,如线程间思索,死循环等
命令格式 jstack [option] vmid
选项 | 作用 |
---|---|
-F | 当正常输出的请求不被响应时,强制输出线程堆栈 |
-l | 除堆栈外,显示关于锁的附加信息 |
-m | 如果调用到本地方法的话,可以显示c/c++堆栈 |
初始标记
这个过程要"Stop the World",标记速度很快,只是标记GC Roots能直接关联到的对象
并发标记
就是从GC Roots的直接关联对象开始遍历整个图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
重新标记
这个过程要"Stop the World",这个阶段是为了修正并发标记期间,因用户程序继续运行而导致的标记产生变动的那一部分对象记录,这个阶段停顿时间比初始标记时间长一些,但是也比并发标记阶段时间短。
并发清除
清理删除掉标记阶段判断已经死亡的对象,由于不需要移动存活对象,所以可以和用户线程同时并发执行。
缺点
使用的是Mixed GC模式,基于Region堆内存布局,不以固定大小以及固定数量的分代区域划分,而是把连续的java堆划分为多个大小相等的独立区域每一个Region都可以根据需求,扮演新生代的Eden空间,Survivor空间或者老年代空间,收集器可以扮演不同的角色,也可以根据不同的扮演的不同角色采用不同的策略去处理。
Region有一类特殊的Humongous区域,专门用来存在大对象,如果一个对象大小超过了Region容量的一半的对象就可以大对象,每个Region大小可以通过-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。如果一个大对象超过Region容量,则会被存放在N个连续的Humongous Region中,G1大多数行为会把Humongous Region当作老年代一部分看待。每次垃圾回收,G1都会跟踪各个Region里面的垃圾堆价值大小,优先处理回收价值最大的Region。
初始标记
仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的只,让下一阶段用户线程并发运行时能正确地在可用Region中分配对象,这个阶段要暂停线程,但是借用在MinorGC的时候同步完成,所以耗时很短。
并发标记
从GC Roots开始与堆中对象进行可行性分析,递归扫描整个堆里面的对象图,找出要回收的对象,耗时较长,可与用户程序并发执行。当扫描完对象图后还要重新处理STAM记录下的在并发时有引用变动的对象。
最终标记
暂停用户线程,然后处理并发阶段后仍然遗留下来的最后那少量SATB。
筛选回收
负责更新Region的统计数据,根据Region的回收价值和成本进行排序,然后根据用户所期望的停顿时间来制定回收计划。可以选择任意多个Region构成回收集,然后把回收那一部分的Region复制到空Region中,再清理整个旧Region全部空间。这里要移动对象,所以要暂停用户线程
若删除没被访问过的对象的引用,则把删除的引用记录下来,在并发扫描结束后,将这些被删除引用的对象设置根,进行搜索。
已经被标记为的对象一旦新插入了指向没被标记的对象引用之后,把这个新的引用记录下来,等并发扫描结束后,再将这些记录过的引用对象设为根,重新扫描。
一般情况下我们通过new指令来创建对象,当虚拟机遇到一条new指令的时候,会去检查这个指令的参数是否能在常量池中定位到某个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。如果没有,那么会执行类加载过程。通过执行类的加载,验证,准备,解析,初始化步骤,完成了类的加载,这个时候会为该对象进行内存分配,也就是把一块确定大小的内存从Java堆中划分出来,在分配的内存上完成对象的创建工作。
指针碰撞
假设Java堆中的内存是绝对规整的,用过的内存在一边,未使用的内存在另一边,中间有一个指示指针,那么所有的内存分配就是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
空闲列表方式
如果Java堆内存中不是规整的,已使用和未使用的内存相互交错,那么虚拟机就必须维护一个列表用来记录哪块内存是可用的,在分配的时候找到一块足够大的空间分配对象实例,并且需要更新列表上的记录。需要注意的是,Java 堆内存是否规整是由所使用的垃圾收集器是否拥有压缩整理功能来决定的。
主要可以划分为三个部分对象头,实例数据,对齐填充。
有两类信息
用于存储对象自身运行的数据,如哈希吗,GC分代年龄,锁状态标志,线程持有锁,偏向线程ID、时间戳等,被称为Mark Word。
存储内容 | 标识位 | 状态 |
---|---|---|
对象哈希码,对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀 |
空,不需要记录 | 11 | GC标记 |
偏向线程ID,偏向时间戳 | 01 | 可偏向 |
类型指针,即对象指向它的类型元数据的指针,虚拟机可以通过这个指针来确定该对象是哪个类的实例。
java程序会通过栈上的reference数据来操作堆上的具体对象,访问堆上的对象主要方式有使用句柄和直接指针两种
java堆中会划出一块内存作为句柄池,reference中存储的就是对象句柄地址,而句柄中包含了对象的实例数据和类型数据各自具体地址信息,然后可以利用该对象地址去访问对象和数据,总共要两次访问。
reference中存储的就是对象地址和对象类型数据的指针,如果直接访问对象,只需要一次访问,避免了间接访问带来的开销(注意,如果是访问对象类型数据,还是需要两次访问,因为reference中存储的是对象类型数据的指针)
使用直接指针方法访问速度比使用句柄快,由于对象访问在java中非常频繁,因此这节省了一次指针定位的时间开销,
用于监视虚拟机各种运行状态,可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即使编译等运行数据。命令为 jstat [option vmid [interval[s|ms] [count]]]
例子 jstat -gc 200 100 20 代表每100毫秒查询一次进程id为200的垃圾收集状况,一共查询20次。如果省略
100和20,代表只查询一次。
实时查看和调整虚拟机各项参数。
jinfo [option]
用于生成堆转储快照 jmap [option]
option取值
选项 | 作用 |
---|---|
-dump | 生产java转储快照,格式为-dump:[live,]format=b,file=,其中live子参数说明是否只dump出存活对象。 |
-heap | 显示java堆详细信息,如使用哪种回收器,参数配置,分代状况。只在Linux/Solaris有效 |
-histo | 显示堆中对象统计信息,包括类,实例数量,合集容量 |
可以使用VisualVM可视化工具
JNDI服务可以使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器请求子类加载器完成类加载的行为,这种行为是为了打通双亲委派模型的层次结构来逆向使用类加载器。java涉及SPI的加载基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB,JBI等。目前是利用java.utils.ServiceLoader类,以META-INF/service中的配置信息,辅以责任链模式,来执行SPI加载。
破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。
当创建一个对象的时候,在栈内存中会有一个引用变量,指向堆内存中的某个具体的对象实例。Java虚拟机规范中并没有规定这个引用变量应该以何种方式去定位和访问堆内存中的具体对象。目前常见的对象访问方式有两种,即句柄访问方式和直接指针访问方式,分别介绍如下。
回收条件
1、该类所有的实例都已经被回收,也就是说Java堆中不存在该类的任何实例;
2、加载该类的ClassLoader已经被回收;
3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾回收主要是完成清理对象,整理内存的工作。GC经常发生的区域是堆区,堆区还可以细分为新生代、老年代。新生代还分为一个Eden区和两个Survivor区。垃圾回收分为年轻代区域发生的Minor GC和老年代区域发生的Full GC。
对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快。
Full GC是指发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC。
如果Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,那么年龄大于等于该对象年龄的对象即可晋升到老年代,不必要等到-XX:MaxTenuringThreshold。
发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小。如果大于,则进行一次Full GC(老年代GC),如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,那只会进行一次Minor GC,如果不允许,则改为进行一次Full GC。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。