本学习笔记学习内容来自尚硅谷周阳老师
类加载器
ClassLoader是类加载器,负责加载class文件,class文件必须具有特定的文件内容标识:字符cafe babe(原因,java创作者来灵感时正在喝咖啡),类加载器只负责将类的内容(运行时数据结构)加载到方法区。
java自带的加载器
1.启动类加载器(根加载器):Bootstrap(C++编写)
启动时自动将java JDK自带的包里面rt.jar(运行时)包里面的类加载到虚拟机中
2.拓展类加载器:Extension (java编写)
加载JDK除了rt.jar以外其它的升级版本的包拓展出的类javax开头的类
3.应用程序类加载器:AppClassLoader(java编写)
也加系统类加载器,加载当前应用的classpath下的所有类,既非java JDK自带的类
用户自定义类加载器
java.lang.ClassLoader的子类,用户可以定制类的加载方式
是个抽象类,需要被继承
类加载遵从双亲委派机制和沙箱安全机制
双亲委派
双亲委派机制:每一个类的加载使用的加载器顺序都是Bootstrap(爷爷辈)->Extension(父亲辈)->AppClassLoader(应用加载器)的使用顺序,一旦加载到就不再向后加载;保证了类的安全性,不会被恶意重写方法攻击(当一个类收到了类加载请求他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一层次的类加载器都是如此),保证了使用不通的类加载器最终得到的是同一个类。
沙箱安全
java为了系统的安全性,防止恶意类访问关键资源,通过沙箱机制来保证安全,沙箱的具体内容阳哥木有将,通过查资料学习了,内容太多,就不总结来,具体内容看https://blog.youkuaiyun.com/qq_30336433/article/details/83268945
本地方法接口(native Interface)
一旦方法加了native修饰符,表示这个方法的内容已经不是java代码,而是调用本地方法接口(操作系统和java的关联兴许是使用c或c++实现的);这个方法在JVM的本地方法库。
本地接口的作用是融合不同的编程语言为java所用,她的初衷是融合C/C++程序。诞生之初为了能发行屈服于C/C++的淫威做的妥协;它的做法是开在Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies.
PC寄存器(Register)
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也就是即将要执行的指令代码),由执行引擎读取下一条指令。
这块内存所占空间非常小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
如果执行的是native方法,那么这个计数器是空的。
用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不发发生内存溢出错误(Outofmemory=OOM)。
方法区
非线程私有,属于程序共有,存在gc回收。供各线程共享的运行时内存区域。它存储了每一个类的结构信息(模板),例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。它是规范,在不同虚拟机里头实现是不一样的,最典型的是永久代(PermGen space)和元空间(Metaspace)。
java栈
栈管运行,堆管存储。
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命周期是跟随线程的生命周期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
方法就是栈帧,栈帧中主要保存3类数据:
本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
栈操作(Operand Stack):记录出栈、入栈的操作;
栈帧数据(Frame Data):包括类文件、方法等等.
栈运行原理:栈中的数据以栈帧的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并压入栈中,A方法又调用了方法B,于是产生栈帧F2也被压入栈中,B方法又调用方法C,于是产生了栈帧F3也被压入栈中······
执行完毕后先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧······
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的·大小和具体JVM的实现有关,通常在256K~756K之间,约等于1Mb左右。
引用reference当new一个对象的时候就是引用,指向堆里面对象实例数据,堆里面的对象类型数据的指针指向方法区的对象类型数据。
HotSpot是使用指针的方式来访问对象:Java堆中会存放访问类元数据的地址,reference存储的就是对象的地址。
heap堆
java8和7的区别,java8将永久存储区换成了元空间。
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
堆内存逻辑上分为三部分:新生区+养老区+永久存储区(8的元空间)。物理上新生和养老区。新生区有(伊甸园Eden Space、幸存0区Survivor 0 Space、幸存1区Survivor 1 Space)。
经研究表明,约98%对象是临时对象。而堆存储着对象。
写程序时,死循环创建对象,堆空间(此时在伊甸园区,也叫young区)有限,用完空间就会触发GC(YGC/轻GC),杀死接近98%的对象,剩下的会进入幸存者0区(也叫from区);下一次堆内存满了再触发GC(YGC),二次存活的会在幸存0区和幸存1区(也叫to区)互换位置(换之前将from区的内容复制到to区并且年龄+1,清空from区,然后他们的名字互换),直到大约15次互换(即存活15次),就会进入养老区;当养老区也满了时,就会触发Full GC(FGC/重GC);如果多次FGC之后无法腾出空间,则触发OOM异常,堆内存溢出异常。
java7的永久代,java8的元空间都是用来存储永久变量量的,也就是JDK里面的rt.jar包里面的类。
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进次区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放次区域所占用的内存。
方法区补充
实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
方法参数传值还是传引用问题
分三中情况分析:
1、基本类型
基本类型是传值,复制一个值传到调用的方法里面,调用的方法将参数值改变后本方法里面的值不会受到影响,还是原来的值,即基本类型值不会被调用的方法改变;
2、引用对象
对象传递是引用,调用的方法参数是对象时,传递的是引用的地址,当调用的方法改变对象的值时,当前方法对象的内容会一起改变,因为他们指向同一个地址;
3、字符串String
String对象比较特殊,JVM会维护一个字符串常量池,当常量池没有提供的新字符串内容时会创建一个新的,保留原来的,所以当调用的方法改变值时,调用的方法里面的对象指向一个新的对象地址,而本方法的对象引用地址并没有被改变。
对象生命周期
from区和to区不是固定的,每次GC之后都会交换一次,谁空谁是to。
操作流程为:第一次GC幸存者进入幸存者0区(from)区;第二次GC,from区的幸存者复制到to区,年龄+1,from区清空,然后身份互换,新的幸存者进入from区;再次GC,重复第二次······
最终理解,覆盖上面的操作流程:说白了就是每次GC的时候都会对Eden区和from区同时进行回收操作,存活下来的被一起复制到to区并将年龄+1,然后清空Eden区和from区,清空之后from区和to区名称互换。依次进行······
堆空间比例:
伊甸园区:幸存者0区:幸存者1区=8:1:1
(Eden+From+To):养老区=1:2
堆参数调优
参数 | 说明 |
---|---|
-Xms | 设置初始分配大小,默认为物理内存的“1/64‘ |
-Xmx | 最大分配内存,默认为物理内存的”1/4“ |
-XX:+PrintGCDetails | 输出详细的GC处理日志 |
-XX:MaxTenuringThreshold | 设置对象在新生代中的存活次数 |
GC日志格式:
[区域名称:GC前内存占用->GC后内存占用(该区域内存总大小)]
GC算法
JVM在进行GC时,并非每次都对新生代区、养老区、元空间三个内存区域一起进行回收,大部分时候回收的都是指新生代。(频繁GC新生区,偶尔GC养老区,极少GC元空间)
因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major 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倍左右。
GC四大算法:1、引用计数法 2、复制算法(Copying) 3、标记清除(Mark-Sweep) 4、标记压缩(Mark-Compact)
引用计数法(了解既可,JVM不用):
GC引用计数器记录每一个对象的引用状况,在一定时间内引用一次加1,过了没引用时间减1,直到减为0,回收。
缺点:每次对象赋值之后都要维护一个引用计数器,且计数器本身也要消耗一定的栈空间;较难处理循环引用(类似死锁)(a.B=b;b.A=a;a和b是同一个类,两个语句互相等待赋值)
赋值算法(Copying)
年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)
Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old generation中,也即一旦收集后,Eden是就变成空的了。
当对象在Eden(包括一个Survivor区域,这里表示from区域)出生后,在经过一次Minor GC后,如果对象还存活,并且能够被另一块Survivor所容纳(这里表示to区,即to区有足够的空间来存储Eden和from区域中存活的对象),则使用复制算法将这些仍然还存活的对象复制到另一块Survivor区域(即to区域)中,然后清理所使用过的Eden以及Survivor区域(from区域),并且将这些对象的年龄设置为1,以后对象在Survivor区每熬过一次Minor GC,就将对象的年龄+1,当对象的年龄达到某个值时(默认是15岁,通过-XX:MaxTenuringThreshold来设定参数),这些对象就会成为老年代。
-XX:MaxTenuringThreshold 设置对象在新生代中的存活次数
优点:不会产生内存碎片;缺点:耗空间
详细解析可以看:
标记清除(Mark-sweep)
老年代一般是由标记清除或者标记清除和标记整理混合实现的。
标记清除算法分标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象。
特点:不需要额外空间,节约空间;两次扫描(一次标记,一次清除)耗时严重;会产生内存碎片。这个算法在GC时会停止程序,处理完再开始
标记压缩(Mark-Compact)
老年代一般是由标记清除或者标记清除和标记整理混合实现的。
标记压缩就是标记清除然后整理的算法,也叫标记整理算法。算法的原理是先扫描一遍进行标记,然后扫描一遍进行清除,最后扫描一遍进行压缩,将不联系的空闲区域连续起来。
工作中可以折中使用,即多次标记清除(Mark-sweep)之后进行一次标记压缩(Mark-Compact)算法的GC。这个算法在GC时会停止程序,处理完再开始
内存效率:复制算法>标记清除算法>标记整理算法(此处的效率是指时间复杂度对比,实际情况不完全如此)
内存整齐度:复制算法=标记清除算法>标记清除算法
内存利用率:标记整理算法=标记压缩算法>复制算法
JMM java内存模型
volatile关键字是java虚拟机提供的轻量级的同步机制
可见性,原子性,有序性
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规范或规则,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
1.线程解锁前,必须把共享变量的值刷新回主内存
2.线程加锁前,必须读取主内存的最新值到自己的工作区
3.加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈内存),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
package geovis.demo; /** * @author wucq * @date 2021-01-06 18:23:16 * @desc 验证JMM内存模型:当变量不加volatile时,每个线程的变量拷贝主内存的值到自己的内存空间, * 当前变量改变值以后主内存也会被改变为同样的值,但其它已经拷贝了的线程 * 不知道主内存已经改变,还是原来的值; * 当变量加了volatile之后,每个线程变量拷贝主内存的值到自己的内存空间, * 当前变量改变值以后主内存也会被改变为同样的值并通知其它线程,更新为 * 相同的值 * 本代码验证方案是Number类加与不加volatile分别执行,查看main线程会不会被通知值的变化 */ class Number { volatile int number = 10; public void setNumber(){ this.number = 6688; } } public class Jmm { public static void main(String[] args) { Number number = new Number(); new Thread(()->{ try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } number.setNumber(); System.out.println(Thread.currentThread().getName()+" 改变了值\t" + number.number); },"AA").start(); while (number.number==10){ } System.out.println(Thread.currentThread().getName()+"被通知修改属性值:"+number.number); } }
java程序的运行是先编译后运行,所以所有的程序都是静态先行,就是先执行静态的。而IDEA编辑器在某种程度上减少了编译的过程,所有有很多原理性的看不出来。(静态先行,加载一次)
package geovis.demo; /** * @author wucq * @date 2021-01-07 10:02:34 * @desc java语言是先编译后运行的,所以先有类的加载,加载时优先级:静态资源>构造块资源>构造方法 * (静态先行,只加载一次),然后再开始执行程序。所以,一下程序输出顺序为: * 555->777->222>333->111->333->111->444->666 */ public class LoadLearn { { System.out.println("loadlearn的构造块444"); } static{ System.out.println("loadlearn的静态代码块555"); } public LoadLearn(){ System.out.println("loadlearn的构造方法666"); } public static void main(String[] args) { System.out.println("===========loadlearn的main方法777"); new CodeY(); System.out.println("----------------------"); new CodeY(); System.out.println("----------------------"); new LoadLearn(); } } class CodeY{ { System.out.println("codeY的构造块333"); } static { System.out.println("codeY的静态代码块222"); } public CodeY(){ System.out.println("codeY的构造方法111"); } }