1,类装载器ClassLoader介绍
1.1,类装载器的种类,包含启动类加载器(Bootstrap),扩展类加载器(Extension),应用程序类加载器(AppClassLoader)
注意:
Classloader有多种,可以说三个,也可以说是四个(第四个为自己定义的类加载器,继承ClassLoader),系统自带的三个分别为:
1,启动类加载器(Bootstrap) ,C++所写
2,扩展类加载器(Extension) ,Java所写
3,应用程序类加载器(AppClassLoader)
我们new自己对象的时候创建的是应用程序类加载器(AppClassLoader)。
1.2,实例验证类装载器的种类
public class Demo {
public static void main(String[] args) {
Object object = new Object();
Demo demo = new Demo();
System.out.println(object.getClass().getClassLoader());
System.out.println(demo.getClass().getClassLoader());
System.out.println(demo.getClass().getClassLoader().getParent());
}
}
输出:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@76ed5528
Process finished with exit code 0
1.3,类加载顺序的双亲委派机制
双亲委派机制:“我爸是李刚,有事找我爹”。
例如:需要用一个A.java这个类,首先去顶部Bootstrap根加载器去找,找得到你就用。
找不到再下降一层,去Extension加载器去找,找得到就用。
找不到再降一层,去AppClassLoader加载器去找,找得到就用。这时还没找到就会报"CLASS NOT FOUND EXCEPTION"。
1.4,实例验证双亲委派机制
package java.lang;
public class String {
public static void main(java.lang.String[] args) {
System.out.println("双亲委派机制验证代码");
}
}
输出:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
Process finished with exit code 1
上面代码证明了类加载器的顺序:首先加载的是Bootstrap加载器,由于JVM中有java.lang.String这个类,
所以会首先加载这个类,而这个类中并无main方法,所以会报“在这个类 java.lang.String 中找不到 main 方法”错误。
这个问题就涉及到,如果有两个相同的类,那么java到底会用哪一个?如果使用用户自己定义java.lang.String,
那么用这个类的程序会出错,所以,为了保证用户写的源代码不污染java出厂自带的源代码,
而提供了一种“双亲委派”机制,保证“沙箱安全”。即先找到先使用。
2,方法区的介绍Method Area
注意:
方法区:绝对不是存放方法的地方,他是存放类的描述信息(模板)的地方,存储的每一个类的结构信息(比如static)
永久代和元空间的解释:
方法区是一种规范,类似于接口定义的规范:List list = new ArrayList();
把这种比喻用到方法区则有:
1,java 7中:方法区 f = new 永久代();
2,java 8中:方法去 f = new 元空间();,
Classloader只是负责class文件的加载,相当于快递员,这个“快递员”并不是只有一家,Classloader有多种
加载之前是“小class”,加载之后就变成了“大Class”,这是按照java.lang.Class模板生成了一个实例。“大Class”就装载在方法区,模板实例化之后就得到n个相同的对象
JVM并不是通过检查文件后缀是不是.class来判断是否需要加载的,而是通过文件开头的特定文件标志-cafe babe
3,Execution Engine执行引擎的介绍
4,本地方法接口(Native Interface)与本地方法栈(Native Method Stack)的介绍
注意:平时我们所说的栈指的是Java栈,本地方法栈(Native Method Stack)里面装的都是native方法。
见线程类Thread中start0()方法
5,PC寄存器的介绍
注意:本地native方法不归java管,所以计数器是空的
上图中亮色部分有2个特点
1,所有线程共享(灰色是线程私有)
2,亮色地方存在垃圾回收
6,Java栈的介绍
6.1,代码模拟栈内存溢出
public class Demo {
private static void outStack() {
outStack();
}
public static void main(java.lang.String[] args) {
outStack();
}
}
输出:
Exception in thread "main" java.lang.StackOverflowError
at jvm.Demo.outStack(Demo.java:6)
...
Process finished with exit code 1
注意:
栈管运行,堆管存储
栈是线程私有的,不存在垃圾回收
栈帧的概念:java中的方法被扔进虚拟机的栈空间之后就成为“栈帧”,比如main方法,是程序的入口,被压栈之后就成为栈帧。
7,堆+栈+方法区的交互关系的介绍
8,堆的介绍
注意:
Java 7之前和图上一模一样,Java 8把永久区换成了元空间
堆逻辑上由”新生+养老+元空间“三个部分组成,物理上由”新生+养老“两个部分组成
当执行new Person();时,其实是new在新生区的伊甸园区,然后往下走,走到养老区,但是并未到元空间。
注意:
GC发生在伊甸园区,当对象快占满新生代时,就会发生YGC(Young GC,轻量级GC)操作,伊甸园区基本全部清空
幸存者0区(S0),别名“from区”。伊甸园区没有被YGC清空的对象将移至幸存者0区,幸存者1区别名“to 区”
每次进行YGC操作,幸存的对象就会从伊甸园区移到幸存者0区,如果幸存者0区满了,就会继续往下移,如果经历数次(默认15次)YGC操作对象还没有消亡,最终会来到养老区
如果到最后,养老区也满了,那么就对养老区进行FGC(Full GC,重GC),对养老区进行清洗
如果进行了多次FGC之后,还是无法腾出养老区的空间,就会报OOM(out of Memory)异常
from区和to区位置和名分不是固定的,每次GC过后都会交换,GC交换后,谁空谁是to区
注意:
整个堆分为新生区和养老区,新生区占整个堆的1/3,养老区占2/3。新生区又分为3份:伊甸园区,幸存者0区(from区),幸存者1区(to区) = 8:1:1
每次从伊甸园区经过GC幸存的对象,年龄(代数)会+1
8.1,堆的永久代/元空间
注意:
临时对象说明,其在伊甸园区生,也在伊甸园区死。
堆逻辑上由”新生+养老+元空间“三个部分组成,物理上由”新生+养老“两个部分组成,元空间也叫方法区
永久代(方法区)几乎没有垃圾回收,里面存放的都是加载的rt.jar等,让你随时可用
注意:
上面的图展示的是物理上的堆,分为两块,新生区和养老区。
堆的参数主要有两个:-Xms,Xmx:
1,-Xms堆的初始化的大小
2,Xmx堆的最大化大小
Young Gen(新生代)有一个参数-Xmn,这个参数可以调新生区和养老区的比例。但是,这个参数一般不调。
永久代也有两个参数:-XX:PermSize,-XX:MaxPermSize,可以分别调永久带的初始值和最大值。
Java8 后没有这两个参数啦,因为Java8后元空间不在虚拟机内啦,而是在本机物理内存中
8.2,代码验证JVM堆默认初始化内存大小&可使用最大内存大小
public static void main(java.lang.String[] args) {
long totMemory = Runtime.getRuntime().totalMemory();
long maxMemory = Runtime.getRuntime().maxMemory();
System.out.println("堆总共内存(M)-" + totMemory/(1024*1024));
System.out.println("堆最大内存(M)-" + maxMemory/(1024*1024));
}
输出:
堆总共内存(M)-119
堆最大内存(M)-1744
Process finished with exit code 0
8.3,代码验证JVM堆内存可调节
IDEA—Run—Edit Configurations
设置堆内存初始化,最大化内存大小,打印GC日志:
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
public static void main(java.lang.String[] args) {
long totMemory = Runtime.getRuntime().totalMemory();
long maxMemory = Runtime.getRuntime().maxMemory();
System.out.println("堆总共内存(M)-" + totMemory/(1024*1024));
System.out.println("堆最大内存(M)-" + maxMemory/(1024*1024));
}
输出:
堆总共内存(M)-981
堆最大内存(M)-981
Heap
PSYoungGen total 305664K, used 15729K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 262144K, 6% used [0x00000000eab00000,0x00000000eba5c420,0x00000000fab00000)
from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
Metaspace used 3015K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 330K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
8.4,模拟堆内存溢出OOM异常
1,首先把上述堆内存调成10M后,再new一个10M的对象,导致Full GC也无法处理,直至撑爆堆内存,查看堆溢出错误(OOM),程序及结果如下:
public static void main(java.lang.String[] args) {
byte[] bytes = new byte[10*1024*1024];
}
输出:
[GC (Allocation Failure) [PSYoungGen: 1532K->496K(2560K)] 1532K->531K(9728K), 0.0030234 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 496K->480K(2560K)] 531K->515K(9728K), 0.0027392 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [PSYoungGen: 480K->0K(2560K)] [ParOldGen: 35K->453K(7168K)] 515K->453K(9728K), [Metaspace: 3007K->3007K(1056768K)], 0.0088867 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 453K->453K(9728K), 0.0082019 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 453K->435K(7168K)] 453K->435K(9728K), [Metaspace: 3007K->3007K(1056768K)], 0.0151079 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at jvm.Demo.main(Demo.java:10)
Heap
PSYoungGen total 2560K, used 57K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0e6a0,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 435K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 6% used [0x00000000ff600000,0x00000000ff66cf78,0x00000000ffd00000)
Metaspace used 3039K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 333K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 1
8.5,GC各个参数讲解
[GC (Allocation Failure) [PSYoungGen: 1532K->496K(2560K)] 1532K->531K(9728K), 0.0014412 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 464K->0K(2560K)] [ParOldGen: 35K->453K(7168K)] 499K->453K(9728K), [Metaspace: 3007K->3007K(1056768K)], 0.0101167 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
提问下:
下面程序中,有几个线程在运行
答:有两个线程,一个是main线程,另一个是后台GC线程
面试题:
1,GC是什么?
要答分代收集算法:
次数上频繁收集Young区
次数上较少收集Old区
基本不动元空间
知识点:
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC or Young GC),一种是全局GC(major GC or Full GC)
Minor GC和Full GC的区别
普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。
全局GC(major GC or Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上 (因为养老区比较大,占堆的2/3)
2,GC有哪四大算法
1,引用计数法
2,复制算法(Copying)
3,标记清除(Mark-Sweep)
4,标记压缩(Mark-Compact)
8.5,GC四大算法的介绍
8.5.1,引用计数法(现在一般不用)
引用计数法一般不用的另一个缺点也很明显,就是当涉及到相互引用的场景时,代码说话
public class RefCountGC {
private Object instance;
public RefCountGC() {
instance = null;
}
public static void main(String[] args) {
RefCountGC objA = new RefCountGC();
RefCountGC objB = new RefCountGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
结论:
虽然objA和objB都置空,但是他们之前曾发生过相互引用,所以调用system.gc(手动版唤醒GC,后台也开着自动档)并不能进行垃圾回收。
并且,system.gc执行完之后也不是立刻执行垃圾回收
8.5.2,复制算法(Coping)
年轻代中使用的是Minor GC(YGC),这种GC算法采用的是复制算法(Copying)。
Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old generation中,也即一旦收集后,Eden是就变成空的了。
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄+1,当对象的年龄达到某个值时 ( 默认是15岁,通过-XX:MaxTenuringThreshold 来设定参数),这些对象就会成为老年代。
8.5.2.1,新生区为什么考虑用复制算法,简单说用它有什么好处
因为新生代中的对象基本都是朝生夕死的(90%以上),简言之就是因为Eden区对象一般存活率较低,需要复制的对象少。
使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的from活动区间与另外80%中存活的eden对象转移到10%的to空闲区间,接下来,将之前90%的内存全部释放,以此类推。
复制算法的优点是不会产生内存碎片。
上面动画中,Area空闲代表to,Area激活代表from,绿色代表不被回收的,红色代表被回收的。
8.5.2.2,有优点必然有缺点,复制算法的缺点是什么呢
如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,
并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。 所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,
而且最重要的是,我们必须要克服50%内存的浪费。
8.5.3,标记清除(Mark-Sweep)
老年代一般是由标记清除或标记清除+标记整理的混合实现。
8.5.3.1,标记清除优缺点
动画演示:
8.5.4,标记压缩(Mark-Compact)
标记压缩(Mark-Compact)又叫标记清除压缩(Mark-Sweep-Compact),或者标记清除整理算法。
老年代一般是由标记清除或标记清除+标记整理的混合实现
动画演示:
面试题:请说出各个GC回收算法优缺点
内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
内存整齐度:复制算法=标记整理算法>标记清除算法。
内存利用率:标记整理算法=标记清除算法>复制算法。
可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程
整理不易,请作者喝杯咖啡呗~
完