上一篇已经学习到了集合类,Callable等基础回顾等信息,接下来复习一下我们的JVM相关互到JUC的基础回顾。
一、JVM
(一)JVM架构图
- 记住一句话:栈管运行,堆管存储,程序寄存器管指针。
- 橙色的占的内存比较多和线程共享,而灰色的比较少和线程私有。
(二)类装载器
- 上一步的类装载子系统
1、类装载器的作用
- 看看底层的关系【我们的小class,装载后转化为大Class,模板装配到了方法区】
以后你实例化这个类对象,都是从方法区的模板刻出来的 - 为什么你的xxx.class文件能被JVM识别呢【是因为我们编译后的文件都存在CAFEBABEI前缀】
2、类装载器类型
(1)BootStrap
-
看代码启动测试
BootStrap:是启动类加载器【根加载器】
打出null,是因为我们底层用C++写的,你调用的时候是得不到的。
这个rt.jar里面就有我们的比如String.class,Object.class的压缩包等,启动的时候,这些就默认的加载了,所以我们能直接进去就调用。
-
启动了会加载哪些类呢
最顶端的BootStrap就是进行java,javax,sun等开头的核心类库进行加载
(2)Extension
- Extension它加载的类型时
对我们的jre下的lib下的ext类进行加载 - 有它的原因:其实刚开始java出来,设计的观念在现在看来都不行,会被替换,所以有扩展的加载器,专门去加载扩展。
(3)Application
- Application:加载的是我们自定义的类型
- 它能加载的类型:系统类加载器我们自定义类的加载器
(4)自定义类加载器
- 自定义类加载器:自己定义的【基本不会,除非很大公司,就是继承ClassLoader】
(5)类加载顺序说明
https://blog.csdn.net/weixin_46635575/article/details/122578677
双亲委派机制好处【不会破坏它原有的系统】
(三)本地接口
1、Native上面东西
-
调用多线程启动方法多次(会报错)
-
这个start0就是native定义的
相当于你的程序要去调用操作系统底层的接口了(这个接口给你提供了功能:比如完成计算) -
这里的逻辑梳理一下:我们在我们的程序里面用native定义的方法,它会去调用底层的本地方法接口,它的定义的方法会放在本地方法栈里面。(我们非native定义的方法就放在方法栈里面)
-
为什么有native呢
其实java在95年诞生的时候,你java要发行,而C语言是老大呀,你就要跟我一点,所以它会有这样的方法,但是也有一点功能实现的目的(因为当时底层是系统用C写的) -
目的
2、PC寄存器(Program Counter Register)
- 不会发生内存溢出
3、小总结
- 类被类加载器加载到内存里面(并不是所有的都会被加载,而且是按需加载,而且存在双亲委派机制(它的理由是沙箱安全机制))
- Native讲的过程中(我们的新建new Thread()并不是线程马上启动,现在知道原因了吧,我们创建了后,它要调用底层,底层是未知的,要看CPU才知道,CPU说什么启动再启动)
- PC寄存器(它就是一个指针,指向内存地址,指向马上要执行的地址(比如A方法的内部有个for循环内调用了B方法,执行到这个后,它的寄存器就会指向了B方法))
(四)方法区
1、是模板等
- 存了:类的结构信息(我们new 某个类的对象,都是根据这个模板来生成的对象),比如构造方法,字段,方法数据等信息。
- 方法区是一个规范:在不同的虚拟机是不一样的,最典型的是永久代(PermGen Space)和元空间(Metaspace)
2、元空间和永久代
具体可以看此文:https://blog.csdn.net/weixin_46635575/article/details/122740245
(五)栈(stack)
复习一句话:栈观运行,堆管存储
1、第一部分(大致介绍)
-
看一下这里理解(只要报错,我们打印的是栈轨迹)
简单来说就是比较重要,好好学习。 -
去复习一下队列(先进先出)和栈(后进先出)数据结构
2、第二部分(存储了哪些东西)
(1)有关介绍
-
栈更多的装载方法的
-
java栈
- 首先基本类型都是存储在栈里面
- 第二所有的引用类型也是(执行new了一个对象在堆里面的对象)都是存在栈里面
- 实例方法也是存在这里面
- 方法输入和输出参数和方法内的参数都是
- 记录入栈和出栈的操作
-
分析一下执行流程
上面的停着,下面的也不能动。
(2)栈运行原理
3、第三部分(测试递归调用:这里有一个错误)
-
StackOverflowError:它是一个错误,
-
错误和异常关系
错误和异常都继承于这个类
而我们的StackOverflowError就是继承于Error的 -
图
(六)堆+栈+方法区
- 再次复习:我们的栈里面存了基本数据类型的变量,和引用对象,方法(栈帧),入栈和出栈的操作。
(七)堆(Heap)
1、概念
1)名称
- java7
逻辑上分为:新生,养老,永生代
物理上分为:新生代和老年代
2)GC发生时间
-
一直new对象:
我们new了对象,它是伊甸园里面存在,当你死循环在无限的new后,那么它会发生GC,此时发生过后,我们的Eden里面的对象几乎都死完了。 -
对于上面new的对象,经过GC后,它会被移动到幸存者0区或1区(全部都是移动到一块地区),移动到的幸存者区的这个区此时叫做from区,对应的另外一个幸存者区叫做to区。
-
经过一次GC后,活下来的对象都被移动到了某一个幸存者区里面,另外一个幸存者区是空着的(因为我们采用的复制引用算法),请记住,我们的幸存者0或1区是不会触发youngGc的,仅仅是当伊甸园区的不能再装了,才会触发,当Eden第二次触发YoungGC后,我们的幸存者区此时也会被同时进行垃圾回收,会把当前的from区里面能活下来的对象移动到to区,此时对象的年龄值就会被增加1,当如此循环,对象的年龄到了15后,就会被移动到永生代里面去了。
-
如果移动过程中,我们的养老区间,也满了,会触发Full GC = FGC。如果把养老区都整理了,也装满了(就是经过Full GC也还是不能装下的情况下),就会报OOM了。
3)问题分析
(1)第一种情况输出什么呢
答案是输出20,为什么呢,我们的基本数据类型,是传的复印件(就是没有引用)
首先肯定是我们的main方法入栈,接着去调用changeValue方法(它也会入栈),它执行完后,出栈
(2)第二种情况输出什么呢
引用类型时传的引用,传一个引用给他它就会修改值。
(3)第三种情况输出什么呢
它为什么特殊的输出了hello呢
其实就是我们的常量池在做鬼。
- 当我们的String str = "xxx"的格式中,它会去常量池汇总找这么一个xxx的字符串,如果找到了那就传递这引用给他,如果没有找到,哪就创建一个。
- 而我们的String str = new String(“xxx”)的时候,会在堆内存中new和常量池中new
这里两个的区别可以看这篇文章:https://blog.csdn.net/weixin_46635575/article/details/122782447
2、对象生命周期和GC
3、永久代
永久代几乎不会发生垃圾回收
4、堆参数的设置
(1)理论
(2)测试
-
怎么查看CPU的参数呢
- 获取CPU核数
- 其实这里可以补充一点为什么用Runtime类,它可以理解为运行时数据区的抽象(Runtime DataArea)
- 获取CPU核数
-
看内存
public class test {
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors());//获取CPU核数
long maxMemory = Runtime.getRuntime().maxMemory();//返回java虚拟机试图使用的最大内存
long totalMemory = Runtime.getRuntime().totalMemory();//返回java虚拟机中的内存总量
System.out.println("试图使用内存大小 = " + maxMemory + "字节" + "=" + (maxMemory / (double)1024/1024) + "MB");
System.out.println("虚拟机的内存总量" + totalMemory);
}
}
大概就是我们的4分之一和上线内存也差不多的是64分支一
-
那我们可以修改的参数方式
对于Xms和Xmx在工作开发中必须一样,测试环境随便改(避免GC和应用程序争抢内存,理论峰值忽高忽低)
-
证明我们的堆内存是分三个区呢
而且我们的PSYoungGen + ParOldGen刚好等于我们的479.5M,证明Metaspace在我们的直接内存里面。 -
测试OOM(OutOfMemoryError)
首先出现了YoungGC,后来又出现了FullGC了,最后出现了OutOfMemoryError:java heap space(堆报错了)
还有另外一种情况
(3)GC日志分析
[GC (Allocation Failure) [PSYoungGen: 105038K->17264K(149504K)] 105038K->50056K(491008K), 0.0118032 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 118092K->792K(149504K)] 347588K->230288K(491008K), 0.0009803 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 792K->760K(149504K)] 230288K->230256K(491008K), 0.0008719 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 760K->0K(149504K)] [ParOldGen: 229496K->131800K(341504K)] 230256K->131800K(491008K), [Metaspace: 3248K->3248K(1056768K)], 0.0315704 secs] [Times: user=0.14 sys=0.03, real=0.03 secs]
[GC (Allocation Failure) [PSYoungGen: 2570K->32K(149504K)] 265506K->262968K(491008K), 0.0007913 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 32K->32K(149504K)] 262968K->262968K(491008K), 0.0006594 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 32K->0K(149504K)] [ParOldGen: 262936K->197368K(341504K)] 262968K->197368K(491008K), [Metaspace: 3248K->3248K(1056768K)], 0.0284284 secs] [Times: user=0.11 sys=0.00, real=0.03 secs]
[GC (Allocation Failure) [PSYoungGen: 2508K->32K(154112K)] 331013K->328536K(495616K), 0.0016326 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 32K->0K(154112K)] [ParOldGen: 328504K->131800K(341504K)] 328536K->131800K(495616K), [Metaspace: 3248K->3248K(1056768K)], 0.0295789 secs] [Times: user=0.19 sys=0.00, real=0.03 secs]
[GC (Allocation Failure) --[PSYoungGen: 131136K->131136K(154112K)] 262936K->394072K(495616K), 0.0311449 secs] [Times: user=0.09 sys=0.00, real=0.03 secs]
[Full GC (Ergonomics) [PSYoungGen: 131136K->0K(154112K)] [ParOldGen: 262936K->262936K(341504K)] 394072K->262936K(495616K), [Metaspace: 3248K->3248K(1056768K)], 0.0305174 secs] [Times: user=0.17 sys=0.01, real=0.03 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(153088K)] 262936K->262936K(494592K), 0.0008972 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(153088K)] [ParOldGen: 262936K->262918K(341504K)] 262936K->262918K(494592K), [Metaspace: 3248K->3248K(1056768K)], 0.0676979 secs] [Times: user=0.24 sys=0.02, real=0.07 secs]
Heap
PSYoungGen total 153088K, used 5403K [0x00000000f5980000, 0x0000000100000000, 0x0000000100000000)
eden space 136704K, 3% used [0x00000000f5980000,0x00000000f5ec6d40,0x00000000fdf00000)
from space 16384K, 0% used [0x00000000fdf00000,0x00000000fdf00000,0x00000000fef00000)
to space 14848K, 0% used [0x00000000ff180000,0x00000000ff180000,0x0000000100000000)
ParOldGen total 341504K, used 262918K [0x00000000e0c00000, 0x00000000f5980000, 0x00000000f5980000)
object space 341504K, 76% used [0x00000000e0c00000,0x00000000f0cc1858,0x00000000f5980000)
Metaspace used 3279K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
-
我们的FullGC执行结果:要么OK,要么就OOM了
-
这里还要多一嘴,我们的Young GC会在Eden满了后触发,而我们的OldGC说的是当你Young收拾了,发现还是无法存了,此时会触发OldGC,但是我们的Full GC都是混合使用的,触发了OldGC,一般就是触发FUllGC,对我们的老年代,新生代都进行回收。
(九)GC常用算法
1、引用计数算法
(1)复习概念
此方法只需要了解,不用掌握,因为我们的java不采用这种方法(Python是采用的这种)
这种例子比如我们的链表,几个数据相互连接,没有任何外界对它引用了,而他们是互联的,此时就会是非死对象
(2)看main方法
此时补充看我们的:public static void main(String[] args)方法的理解(自行脑补):我们后台启动两个线程main线程和GC线程
(3)补充我们的Thread.start()和System.gc()方法
记得当我们的System.gc()启动垃圾回收和我们的Thread.start()一样的,我们启动了,并不是即可启动,而且是会由于操作系统来说(因为他们的底层都是Native处理,这些都是未知的)
2、复制算法(Coping)
(1)复习概念
其实就是将我们的from复制到to区(也就我们的minor GC采用的这种算法)
(2)原理
(3)劣势
- 劣势:空间浪费
3、标记清除(Mark-Sweep)
- 概念很好理解:就是我们的标记,和清除
- 老年代一般是用的标记清除或者标记整理和标记清除混合使用
(1)原理
(2)劣势
4、标记压缩(Mark-Compact)
- 其实就是标记,清除,压缩。三步
(1)原理
(2)劣势
其实就印证了那句话,慢工出细活。
- 其实有了另外的一种(但是这一种具体在工作中也没有很说明情况,等工作了再来回答)
5、分代收集算法
(1)情况
- 分代收集算法即没有最好的算法
不同的代,用不同的算法使用于不同的状态的情况。
(2)总结
- 看下面的另外一种情况(在java9默认垃圾回收器采用的G1)
二、JMM(java内存模型)
(一)概念理解
1、Volatile是java虚拟机提供的轻量级的同步机制
-
三存(CPU > 内存 > 硬盘):我们之前数据在硬盘里面存起来的,后来感觉写在程序里面,慢了,就有了数据库来方便进行处理,后来发现数据库也慢了,就有了像Redis样的缓存结构的No-SQL数据库,后来感觉还是慢了,现在有了CPU的多级缓存。
-
保证可见性,不保证原子性,禁止指令重排。
2、内存快照和JMM
再次提醒在我们的CPU和内存之间有了缓存。
-
内存快照
内存快照就是比如我们的A线程和B线程通知想去修改公共数据age,此时不能直接进行修改,它们各自都要对数据进行复制,然后操作各自的数据,然后再提交给物理内存上值进行修改。 -
我们的JMM
3、特征
- 可见性(就比如我写的文章,那么都可以见;对于程序理解起来就是,A和B线程对我们的数据都是可见的):其实就是一种通知机制,我修改了,其他的就要知道,你就要通知它。
- 原子性(这个和数据库的原则性一样的:和数据库的事务扯上关系(不可分割的部分))即不可分割的就是原子性。
- 有序性:正常情况他们想的怎么写,就是按着你写的顺序去执行(但是不一定,就比如上面写到的native定义的方法,交给了操作系统来处理,我们java无法预测)
(二)代码实操
- 看第一种情况
package cn.mldn.Juc.jvm;
class MyNumber {
int age = 29;
public void setAge() {
age = 129;
}
}
public class JVMHello {
public static void main(String[] args) {
MyNumber myNumber = new MyNumber();
new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myNumber.setAge();//此时将29修改为129
System.out.println(Thread.currentThread().getName() + myNumber.age);
},"AAAA").start();
while (myNumber.age == 29) {
}
System.out.println(Thread.currentThread().getName());
}
}
此时情况是无法自己结束的,当我们AAAA线程修改后,我们的main线程一直在傻傻的等着,它也不知道个什么,看下面的对比版本更好理解
- 第二个版本
package cn.mldn.Juc.jvm;
class MyNumber {
volatile int age = 29;
public void setAge() {
age = 129;
}
}
public class JVMHello {
public static void main(String[] args) {
MyNumber myNumber = new MyNumber();
new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myNumber.setAge();//此时将29修改为129
System.out.println(Thread.currentThread().getName() + myNumber.age);
},"AAAA").start();
while (myNumber.age == 29) {
}
System.out.println(Thread.currentThread().getName());
}
}
就相当于我们的用volatile定义后,我A线程修改后,它就给其他线程说名情况。
三、代码加载顺序练习
class Code {
public Code() {
System.out.println("执行构造方法1111");
}
{
System.out.println("执行代码块222");
}
static {
System.out.println("执行静态代码块333");
}
}
public class CodeTest {
{
System.out.println("执行test代码块444");
}
static {
System.out.println("执行test静态代码块555");
}
public CodeTest() {
System.out.println("执行构造方法666");
}
public static void main(String[] args) {
System.out.println("-------------------------");
new Code();
System.out.println("----------------------------");
new Code();
System.out.println("----------------------------");
new CodeTest();
}
}
一定要记得静态先行(而且只加载一下),其次是构造代码块,其次是是构造方法