JVM
类装载器ClassLoader
负责加载class文件,class文件在文件开头有特定的文件标示(cafe babe),将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。
类装载器ClassLoader的种类:
- 虚拟机自带的加载器
- 启动类加载器(BootStrap) C++
- 扩展类加载器(Extension)Java
- 应用程序加载器(AppClassLoader)Java,也叫系统类加载器,加载当前应用的classpath的所有类
- 用户自定义加载器 Java.lang.ClassLoader 的子类,用户可以定制类的加载方式。
package com.jms.jvm;
/**
* @author Jamison
* @version 1.0
* @date 2021/4/3 17:16
*/
public class MyObject {
public static void main(String[] args) {
Object object = new Object();
System.out.println(object.getClass().getClassLoader().getParent());//Exception in thread "main" java.lang.NullPointerException
System.out.println(object.getClass().getClassLoader());//null (bootstrapClassLoader)
MyObject myObject = new MyObject();
System.out.println(myObject.getClass().getClassLoader().getParent().getParent());//null (bootstrapClassLoader)
System.out.println(myObject.getClass().getClassLoader().getParent());//sun.misc.Launcher$ExtClassLoader@1b6d3586 (ExtClassLoader)
System.out.println(myObject.getClass().getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2 (AppClassLoader)
}
}
双亲委派机制:
当一个类收到类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的请求都应该传送到启动类加载其中,只有当父类加载器反馈无法完成这个请求的时候(在他的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
采用双亲委派机制的好处就是比如加载rt.jar包中的java.lang.Object,不管是那个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。双亲委派保证了沙箱安全。
Native Interface
本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须有调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行是加载native libraise。
目前该方法使用的越来越少了,除非是与硬件有关的,在企业级应用中比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等
Native Method Stack
它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。
程序计数器(Program Counter Register)—>PC寄存器
每一个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,即将要执行的指令代码),有执行引擎读取下一条指令,是一个非常小的内存孔家,几乎可以忽略不计。
这块内存区域很小,它是当前线程锁执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
如果执行的是native方法,那这个计数器是空的。
用以完成分支、循坏、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出(OutOfMemory==OOM)错误。
方法区Method Area (共享、有垃圾回收)
供各线程共享的运行时内存区域。它存储了每一个类的结构信息。例如运行时的常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。上面讲的是规范,在不同的虚拟机里头实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)。
But 实例变量存在堆内存中,和方法区无关。
Stack(栈管运行,堆管存储) Java Stack
栈也叫做栈内存,主管Java程序的运行,实在线程创建时创建,它的生命周期跟随线程的生命周期,线程结束栈内存也就释放。对于栈来说不存在垃圾回收问题。只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。**8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
栈帧:
方法(Java中)== 栈帧(虚拟机中)
栈帧中主要保存3类数据:
1.本地变量(Local Variable):输入参数和输出参数以及方法内的变量;
2.栈操作(Operand Stack):记录出栈、入栈的操作;
栈帧数据(Frame Data): 包括类文件、方法等等。
栈的运行原理:
栈中的数据都是以栈帧(Stack Frame)的格式存在的,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集。当一个方法A被调用时就产生一个栈帧F1,并被压入栈中,A方法有调用了B方法,于是产生栈帧F2,也被压入栈中,B方法有调用了C方法,于是产生栈帧F3,也被压入栈,…
执行完成后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧,…
遵循“先进后出”/“后进先出”原则。
学校:程序=算法+数据结构
工作:程序=框架+业务逻辑
Heap 堆
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取类文件后,需要把类、方法、常变量放到堆内存当中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分:
1.Young Generation Space 新生区 Young/New
a)、伊甸区 Eden Space
b)、幸存0区 (Survivor 0 Space)
c)、幸存1区 (Survivor 1 Space)
2.Tenure Generation Space 养老区 Old/Tenure
3.Pernanent Space 永久区 Perm
GC
产生GC的大致过程:
新生区是类诞生、成长、消亡的区域,一个类在这里产生、应用、最后被垃圾回收器手机,结束生命。新生区又分为两部分:伊甸园区(Eden Space)和幸存者区(Survivor Space) ,所有类都是在Eden区被new出来的。幸存区有两个:0区(Survivor 0 Space)和1区(Survivor 1 Space)。当Eden 区的空间被用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC) ,将伊甸园区中不再被其他对象所引用的对象进行销毁。然后将伊甸园区中的剩余的对象移动到幸存者0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生Major GC(Full GC),进行养老区的内存清理。若养老区在执行Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
产生OOM原因:
- Java虚拟机的对内存设置不够,可以通过参数-Xms、-Xmx来调整;
- 代码中创建了大量的大对象,并且长时间不能被拉架回收器回收(存在引用)。
Java对从GC的角度还可以细分为:新生代(Eden 区、Form Survivor 区和To Survivor 区)和老年代。
Minor GC的过程(复制->清空->交换)
- 首先,当Eden区满时会触发第一次的GC,把还存活的对象拷贝到Survivor From区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,进过这次回收之后还存活的对象,则直接复制到To 区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年区),同时把这些对象的年龄+1
- 然后,清空Eden和SurvivoFrom中的对象,也即赋值之后有交换,谁空谁是To
- 最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC是的SurvivorFrom区。部分对象会在From和To区域复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
获取JVM对内存空间的大小:
Runtime.getRuntime().maxMemory();
public static void main(String[] args) {
long maxMemory = Runtime.getRuntime().maxMemory(); //返回Java虚拟机试图使用的最大内存量,-Xmx MAX_MEMORY:3784310784字节、3609.0MB
long totalMemory = Runtime.getRuntime().totalMemory();//返回Java中内存总量,-Xms TOTAL_MEMORY:255328256字节、243.5MB
System.out.println("-Xmx MAX_MEMORY:" + maxMemory + "字节、" + (maxMemory / (double)1024 / 1024) + "MB");
System.out.println("-Xms TOTAL_MEMORY:" + totalMemory + "字节、" + (totalMemory / (double)1024 / 1024) + "MB");
}
JVM调优,VM Options设置
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
分代收集算法:
1.次数上频繁收集Young区
2.次数上较少收集Old区
3.基本不动元空间
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倍以上。
垃圾回收的四大算法:
- 引用计数法(JVM一般不采用这种方式),他有两大缺点:一是每次对对象赋值是均要维护引用计数器,且计数器本身也有一定的消耗,二是较难处理循环引用。
- 复制算法(年轻代中使用的是Minor GC,这种GC采用的就是复制算法(Copying )):Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被放到Old Genneration中,也即一旦收集后,Eden就变成空的了。当对象在Eden(包括一个Survivor区域,这里假设是From区域)出生后, 在经过一次GC后如果对象还存活,并且能够被另一块Survivor区域(To区)所容纳,则使用复制算法将这些仍然还存活的对象复制到To区,然后清除所有用过的Eden区和From区。复制算法不会产生内存碎片。之后From区和To区会相互交换角色
- 标记清除(Mark-Sweep)(老年代一般是用标记清除或者标记清除与标记整理的混合实现):算法分成标记和清除两个阶段,先标记要回收的对象,然后统一回收这些对象(遍历两遍)。虽然这个算法不会用到额外的内存空间,但是产生了两个问题:一是两次扫描,严重耗时,二是会产生内存碎片。
- 标记压缩(两个步骤标记和压缩):标记(和标记清除一样),压缩(再次扫描,并往一段滑动存活对象)特点:没有内存碎片,但是耗时严重。
JMM(Java内存模型Java Memory Model)
简述:JMM本身是一种抽象的概念并不真实存在,它描述的是椅子规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规范:
- 线程解锁前,必须把共享变量的值刷新会主内存中;
- 线程加锁前,必须 读取主内存的最新值到自己的工作内存;
- 加锁解锁使用的是同一把锁。
JVM工作的过程:
由于JVM运行程序的实体hi线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称之为栈空间),工作内存是每一线程的私欲数据区域,而Java内存模型中规定所有的变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的内存空间,然后对变量进行操作,操作完成后再将变量协会内存,不能直接操作主内存中的变量,各个线程的工作内存中存储这主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信必须通过主内存来完成。
JMM要求:
- 可见性
- 原子性
- 有序性
为了保证这些要求,java推出了Volatile 关键字来处理这些要求
Volatile 的特性:
- 保证了可见性
- 禁止指令重排
虽然Volatile可以保证JMM的可见性和原子性,但是它并不能保证原子性。例如下面的例子:
class MyData{
//添加了volatile关键字
volatile int number = 0;
public void addPlusPlus(){
number++;
}
}
/**
* @author Jamison
* @version 1.0
* @date 2021/4/5 13:38
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
}
}, String.valueOf(i)).start();
}
//等待前面的20个线程执行完成
while (Thread.activeCount() > 2){
Thread.yield();
}
//预期结果是20000,但是实际上的结果是小于20000,是不确定的。这就验证了volatile不保证原子性
System.out.println(myData.number);
}
}
解决原子性问题:
//java.util.concurrent.atomic
AtomicInteger atomicInteger = new AtomicInteger();
public void atomicAdd(){
atomicInteger.getAndIncrement();
}
getAndIncrement的原码:
指令重排
简介:计算机在执行程序是,为了提高性能,编译器和处理器常常会对指令做重排,一般分为以下三种:
- 编译器优化的重排
- 指令并行的重排
- 内存系统的重排
在单线程的环境里面确保程序最终执行结果和代码执行的结果一致。
处理器在进行重排序时必须考虑指令之间的数据依赖性。
多线程环境中线程交替执行,由于编译优化重排的存在,两个线程中使用的变量能否保证原子性是无法确定的,结果无法预测。
Volatile 实现了禁止指令重排优化,从而避免多线程环境下程序出现乱序的现象
先了解一下概念,内存屏障(Memory Barrier),又称内存栅栏,是一个CPU指令,它的作用有两个:
- 保证特定操作的顺序执行;
- 是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
DCL 单例模式
//DCL (Double Check Lock)双端检索机制,不一定线程安全,原因是有指令重排机制。加volatile禁止指令重排。
public static SingletonDemo getInstance() {
if (singletonDemo == null) {
synchronized (SingletonDemo.class) {
if (singletonDemo == null) {
singletonDemo = new SingletonDemo();
}
}
}
return singletonDemo;
}
CAS是什么? (compareAndSwap)比较并交换
AtomicInteger atomicInteger = new AtomicInteger(5);
// compareAndSet(int 期望值, int 更新值)
System.out.println(atomicInteger.compareAndSet(5, 1024));//true
System.out.println(atomicInteger.get());//1024
System.out.println(atomicInteger.compareAndSet(5, 99)); //false
System.out.println(atomicInteger.get());//1024
底层原理:unsafe类和自旋锁
CAS缺点
- 循环时间长,开销大。如果CAS长时间一直不成功,可能会给cpu带来很大的开销
- 只能保证一个共享变量的原子操作
- 引出ABA问题???
ABA问题
原因:CAS算法实现一个重要的前提是需要取出内存中某时刻的数据并在当下时刻比较并交换,那么在这个时间差内会导致数据的变化。比如说:一个线程one从内存中位置V取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作,将A改为了B并写回了主内存,然后线程two又将V位置的数据变回了A,这时候线程one进行CAS操作发现内存中V位置仍然是A,于是one操作成功了。尽管线程one操作成功,但是不代表这个过程就是没有问题的。
解决ABA问题:原子引用+修改版本号(类似时间戳)
原子引用是什么???
//自定义类
User user = new User("Jamison", 12);
User user1 = new User("zms", 21);
AtomicReference<User> reference = new AtomicReference<>();
reference.set(user);
System.out.println(reference.get()); //User{name='Jamison', age=12}
System.out.println(reference.compareAndSet(user, user1) + "\t" + reference.get());//true User{name='zms', age=21}
System.out.println(reference.compareAndSet(user, user1) + "\t" + reference.get());//false User{name='zms', age=21}
解决ABA问题
AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);
System.out.println("============一下是ABA问题的产生=================");
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
try {
//睡眠2秒,让t1线程完成一次ABA操作
TimeUnit.SECONDS.sleep(2);
boolean b = atomicReference.compareAndSet(100, 2021);
System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + b);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
TimeUnit.SECONDS.sleep(3);
System.out.println("============一下是ABA问题的解决=================");
new Thread(() -> {
try {
int stamp = stampedReference.getStamp();
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp);
stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第二次版本号:" + stampedReference.getStamp());
stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第三次版本号:" + stampedReference.getStamp());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t3").start();
new Thread(() -> {
try {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp);
TimeUnit.SECONDS.sleep(3);
boolean result = stampedReference.compareAndSet(100, 2021, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() +"\t修改成功否:" + result + "\t最新的版本号:" + stampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t修改后的最新值:" + stampedReference.getReference());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t4").start();
四大垃圾收集器
- 串行垃圾收集器(Serial)
- 并行垃圾收集器
- 并发垃圾收集器
- G1
查看默认的垃圾收集器
java -XX:+PrintCommandLineFlags -version
七种垃圾回收
- UseSerialOldGC(废弃)
- UserSerialGC
- UseParallelGC
- UseConcMarkSweepGC(并发标记清除GC)
- UseParNewGC
- UseParallelOldGC
- UseG1GC
七种垃圾收集的使用场景
- 新生代:
a)、串行(Serial/Serial Copying)1:1
b)、并行(ParNew)N:1
c)、并行回收GC(Parallel)/(Parallel Scavenge) N:N - 老年代
a)、串行(Serial Old/Serial CMS)(废弃,Java8后被优化掉)
b)、并行(Parallel Old/Parallel CMS) N:N
c)、并发标记清除GC(CMS)
并发标记清除GC(CMS)的四个步骤
1、Intial Mark
2、Concurrent Mark
3、ReMark
4、Concurrent Sweep