2022年12月18日 08:24 周日转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!
概述
#
- Java中,JVM可以理解的代码就叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机
- Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时效率极高,且由于字节码并不针对一种特定的机器。因此,Java程序无需重新编译便可在多种不通操作系统的计算机运行
- Clojure(Lisp 语言的一种方言)、Groovy、Scala 等语言都是运行在 Java 虚拟机之上。下图展示了不同的语言被不同的编译器编译成
.class文件最终运行在 Java 虚拟机之上。.class文件的二进制格式可以使用
WinHexopen in new window 查看。

.class文件是不同语言在Java虚拟机之间的重要桥梁,同时也是支持Java跨平台很重要的一个原因
Class文件结构总结
#
根据Java虚拟机规范,Class文件通过ClassFile定义,有点类似C语言的结构体
ClassFile的结构如下:
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类可以有多个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}

...
2022年12月17日 22:39 周六转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!
回顾一下类加载过程
#
开始介绍类加载器和双亲委派模型之前,简单回顾一下类加载过程。
- 类加载过程:加载->连接->初始化。
- 连接过程又可分为三步:验证->准备->解析。
[
加载是类加载过程的第一步,主要完成下面 3 件事情:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的
Class 对象,作为方法区这些数据的访问入口
类加载器
#
类加载器介绍
#
类加载器从 JDK 1.0 就出现了,最初只是为了满足 Java Applet(已经被淘汰) 的需要。后来,慢慢成为 Java 程序中的一个重要组成部分,赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。
根据官方 API 文档的介绍:
A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a “class file” of that name from a file system.
...
2022年12月16日 10:06 周五转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!
类的声明周期
#

类加载过程
#
- Class文件,需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些Class文件呢
- 系统加载Class类文件需要三步:加载->连接->初始化。连接过程又分为三步:验证->准备->解析

加载
#
类加载的第一步,主要完成3件事情
构造与类相关联的方法表
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构,转换为方法区的运行时数据结构
- 在内存中生成一个该类的Class对象,作为方法区这些数据的访问入口
虚拟机规范对上面3点不具体,比较灵活
- 对于1 没有具体指明从哪里获取、怎样获取。可以从ZIP包读取 (JAR/EAR/WAR格式的基础)、其他文件生成(JSP)等
- 非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的**loadClass()**方法
- 数组类型不通过类加载器创建,它由Java虚拟机直接创建
加载阶段和连接阶段的部分内容是交叉执行的,即加载阶段尚未结束,连接阶段就可能已经开始了
验证
#
验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证阶段主要由四个检验阶段组成:
- 文件格式验证(Class 文件格式检查)
- 元数据验证(字节码语义检查)
- 字节码验证(程序语义检查)
- 符号引用验证(类的正确性检查)

准备
#
2022年12月12日 15:58 周一转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!
前言
#
当需要排查各种内存溢出问题、当垃圾收集成为系统达到更高并发的瓶颈时,我们就需要对这些**“自动化”的技术实施必要的监控和调节**
堆空间的基本结构
#
Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。且Java自动内存管理最核心的功能是堆内存中的对象分配和回收
Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)
从垃圾回收的角度来说,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆被划分为了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法
JDK7版本及JDK7版本之前,堆内存被通常分为下面三部分:
- 新生代内存(Young Generation)
- 老生代(Old Generation)
- 永久代(Permanent Generation)

JDK8版本之后PermGen(永久)已被Metaspace(元空间)取代,且已经不在堆里面了,元空间使用的是直接内存。
内存分配和回收原则
#
对象优先在Eden区分配
#
多数情况下,对象在新生代中Eden区分配。当Eden区没有足够空间进行分配时,会触发一次MinorGC
首先,先添加一下参数打印GC详情:-XX:+PrintGCDetails
public class GCTest {
public static void main(String[] args) {
byte[] allocation1, allocation2;
allocation1 = new byte[30900*1024];//会用掉3万多K
}
}
运行后的结果(这里应该是配过xms和xmx了,即堆内存大小)
如上,Eden区内存几乎被分配完全(即使程序什么都不做,新生代也会使用2000多K)
注: PSYoungGen 为 38400K ,= 33280K + 5120K (Survivor区总会有一个是空的,所以只加了一个5120K )
假如我们再为allocation2分配内存会怎么样(不处理的话,年轻代会溢出)
allocation2 = new byte[900 * 1024];
在给allocation2分配内存之前,Eden区内存几乎已经被分配完。所以当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。GC期间虚拟机又发现allocation1无法存入空间,所以只好通过分配担保机制,把新生代的对象,提前转移到老年代去,老年代的空间足够存放allocation1,所以不会出现Full GC(这里可能是之前的说法,可能只是要表达老年代的GC,而不是Full GC(整堆GC) )
执行MinorGC后,后面分配的对象如果能够存在Eden区的话,还是会在Eden区分配内存
执行如下代码验证:
public class GCTest {
public static void main(String[] args) {
byte[] allocation1, allocation2,allocation3,allocation4,allocation5;
allocation1 = new byte[32000*1024];
allocation2 = new byte[1000*1024];
allocation3 = new byte[1000*1024];
allocation4 = new byte[1000*1024];
allocation5 = new byte[1000*1024];
}
}
大对象直接进入老年代
#
- 大对象就是需要连续空间的对象(字符串、数组等)
- 大对象直接进入老年代,主要是为了避免为大对象分配内存时,由于分配担保机制(这好像跟分配担保机制没有太大关系)带来的复制而降低效率。
2022年12月9日 08:48 周五转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!
原文地址:
https://juejin.im/post/5e1505d0f265da5d5d744050#heading-28 感谢原作者分享!!
JVM的基本介绍
#
- JVM,JavaVirtualMachine的缩写,虚拟出来的计算机,通过在实际的计算机上仿真模拟各类计算机功能实现
- JVM类似一台小电脑,运行在windows或者linux这些真实操作系统环境下,直接和操作系统交互,与硬件不直接交互,操作系统帮我们完成和硬件交互的工作

Java文件是如何运行的
#
场景假设:我们写了一个HelloWorld.java,这是一个文本文件。JVM不认识文本文件,所以需要一个编译,让其(xxx.java)成为一个JVM会读的二进制文件—> HelloWorld.class
类加载器
如果JVM想要执行这个.class文件,需要将其**(这里应该指的二进制文件)装进类加载器**中,它就像一个搬运工一样,会把所有的.class文件全部搬进JVM里面

方法区
类加载器将.class文件搬过来,就是先丢到这一块上
方法区是用于存放类似于元数据信息方面的数据的,比如类信息、常量、静态变量、编译后代码…等
堆
堆主要放一些存储的数据,比如对象实例、数组…等,它和方法区都同属于线程共享区域,即它们都是线程不安全的
栈
线程独享
栈是我们代码运行空间,我们编写的每一个方法都会放到栈里面运行。
名词:本地方法栈或本地方法接口,不过我们基本不会涉及这两块内容,这两底层使用C进行工作,和Java没有太大关系
程序计数器
主要就是完成一个加载工作,类似于一个指针一样的,指向下一行我们需要执行的代码。和栈一样,都是线程独享的,就是每一个线程都会自己对应的一块区域而不会存在并发和多线程问题。
小总结

- Java文件经过编译后编程.class字节码文件
- 字节码文件通过类加载器被搬运到 JVM虚拟机中
- 虚拟机主要的5大块:方法区、堆 都为线程共享区域,有线程安全问题;栈和本地方法栈和计数器都是独享区域,不存在线程安全问题,而JVM的调优主要就是围绕堆、栈两大块进行
简单的代码例子
#
一个简单的学生类及main方法:
public class Student {
public String name;
public Student(String name) {
this.name = name;
}
public void sayName() {
System.out.println("student's name is : " + name);
}
}
main方法:
public class App {
public static void main(String[] args) {
Student student = new Student("tellUrDream");
student.sayName();
}
}
★★ 执行main方法的步骤如下
...
2022年12月7日 13:49 周三转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
如果没有特殊说明,针对的都是HotSpot虚拟机
前言
#
- 对于Java程序员,虚拟机自动管理机制,不需要像C/C++程序员为每一个new 操作去写对应的delete/free 操作,不容易出现内存泄漏 和 内存溢出问题
- 但由于内存控制权交给Java虚拟机,一旦出现内存泄漏和溢出方面问题,如果不了解虚拟机是怎么样使用内存,那么很难排查任务
运行时数据区域
#
Java虚拟机在执行Java程序的过程中,会把它管理的内存,划分成若干个不同的数据区域
JDK1.8之前:
- 线程共享
堆,方法区【永久代】(包括运行时常量池)
- 线程私有
虚拟机栈、本地方法栈、程序计数器
- 本地内存(包括直接内存)

JDK1.8之后:
1.8之后整个永久代改名叫"元空间",且移到了本地内存中
规范(概括):
线程私有:程序计数器,虚拟机栈,本地方法栈
线程共享:堆,方法区,直接内存(非运行时数据区的一部分)
Java虚拟机规范对于运行时数据区域的规定是相当宽松的,以堆为例:
- 堆可以是连续,也可以不连续
- 大小可以固定,也可以运行时按需扩展
- 虚拟机实现者可以使用任何垃圾回收算法管理堆,设置不进行垃圾收集
程序计数器
#
是一块较小内存空间,看作是当前线程所执行的字节码的行号指示器
java程序流程

字节码解释器,工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令
分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器
而且,为了线程切换后恢复到正确执行位置,每条线程需要一个独立程序计数器,各线程计数器互不影响,独立存储,我们称这类内存区域为**“线程私有”**的内存
总结,程序计数器的作用
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制
- 多线程情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切回来的时候能够知道该线程上次运行到哪
程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随线程创建而创建,线程结束而死亡
Java虚拟机栈
#
- Java虚拟机栈,简称"栈",也是线程私有的,生命周期和线程相同,随线程创建而创建,线程死亡而死亡
- 除了Native方法调用的是通过本地方法栈实现的,其他所有的Java方法调用都是通过栈来实现的(需要和其他运行时数据区域比如程序计数器配合)
- 方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈,每一个方法调用结束后,都会有一个栈帧被弹出。
- 栈由一个个栈帧组成,每个栈帧包括局部变量表、操作数栈、动态链接、方法返回地址。
栈为先进后出,且只支持出栈和入栈

局部变量表:存放编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是一个指向一个代表对象的句柄或其他与此对象相关的位置)

操作数栈 作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。计算过程中产生的临时变量也放在操作数栈中
动态链接 主要服务一个方法需要调用其他方法的场景。
在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。
...
2022年12月6日 17:13 周二转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
Java8被引入的一个非常有用的用于异步编程的类【没看】
简单介绍
#
CompletableFuture同时实现了Future和CompletionStage接口
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
}
CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,还提供了函数式编程的能力。

Future接口有5个方法:
boolean cancel(boolean mayInterruptIfRunning) :尝试取消执行任务。boolean isCancelled() :判断任务是否被取消。boolean isDone() : 判断任务是否已经被执行完成。get() :等待任务执行完成并获取运算结果。get(long timeout, TimeUnit unit) :多了一个超时时间。

CompletionStage<T> 接口中的方法比较多,CompoletableFuture的函数式能力就是这个接口赋予的,大量使用Java8引入的函数式编程
常见操作
#
创建CompletableFuture
#
两种方法:new关键字或 CompletableFuture自带的静态工厂方法 runAysnc()或supplyAsync()
通过new关键字
这个方式,可以看作是将CompletableFuture当作Future来使用,如下:
我们通过创建了一个结果值类型为 RpcResponse<Object> 的 CompletableFuture,你可以把 resultFuture 看作是异步运算结果的载体
CompletableFuture<RpcResponse<Object>> resultFuture = new CompletableFuture<>();
如果后面某个时刻,得到了最终结果,可以调用complete()方法传入结果,表示resultFuture已经被完成:
// complete() 方法只能调用一次,后续调用将被忽略。
resultFuture.complete(rpcResponse);
通过isDone()检查是否完成:
public boolean isDone() {
return result != null;
}
获取异步结果,使用get() ,调用get()方法的线程会阻塞 直到CompletableFuture完成运算:
rpcResponse = completableFuture.get();
...
2022年12月5日 17:31 周一转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
本文来自一枝花算不算浪漫投稿, 原文地址:
https://juejin.cn/post/6844904151567040519open in new window。 感谢作者!
思维导图

目录
#
ThreadLocal代码演示
#
简单使用
public class ThreadLocalTest {
private List<String> messages = Lists.newArrayList();
public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);
public static void add(String message) {
holder.get().messages.add(message);
}
public static List<String> clear() {
List<String> messages = holder.get().messages;
holder.remove();
System.out.println("size: " + holder.get().messages.size());
return messages;
}
public static void main(String[] args) {
ThreadLocalTest.add("一枝花算不算浪漫");
System.out.println(holder.get().messages);
ThreadLocalTest.clear();
}
}
/* 结果
[一枝花算不算浪漫]
size: 0
*/
简单使用2
...
2022年12月5日 09:24 周一转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
文章开头先用例子介绍几种类型的api使用
package com.aqs;
import lombok.*;
import java.util.concurrent.atomic.*;
@Data
@Getter
@Setter
@AllArgsConstructor
@ToString
class User {
private String name;
//如果要为atomicReferenceFieldUpdater服务,必须加上volatile修饰
public volatile Integer age;
}
public class AtomicTest {
public static void main(String[] args) {
System.out.println("原子更新数值---------------");
AtomicInteger atomicInteger = new AtomicInteger();
int i1 = atomicInteger.incrementAndGet();
System.out.println("原子增加后为" + i1);
System.out.println("原子更新数组---------------");
int[] a = new int[3];
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(a);
int i = atomicIntegerArray.addAndGet(1, 3);
System.out.println("数组元素[" + 1 + "]增加后为" + i);
System.out.println("数组为" + atomicIntegerArray);
System.out.println("原子更新对象---------------");
User user1 = new User("ly1", 10);
User user2 = new User("ly2", 20);
User user3 = new User("ly3", 30);
AtomicReference<User> atomicReference = new AtomicReference<>(user1);
boolean b = atomicReference.compareAndSet(user2, user3);
System.out.println("更新" + (b ? "成功" : "失败"));
System.out.println("引用里值为"+atomicReference.get());
boolean b1 = atomicReference.compareAndSet(user1, user3);
System.out.println("更新" + (b1 ? "成功" : "失败"));
System.out.println("引用里值为"+atomicReference.get());
System.out.println("原子更新对象属性---------------");
User user4=new User("ly4",40);
AtomicReferenceFieldUpdater<User, Integer> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(User.class, Integer.class, "age");
boolean b2 = atomicReferenceFieldUpdater.compareAndSet(user4, 41, 400);
System.out.println("更新"+(b2?"成功":"失败"));
System.out.println("引用里user4值为"+atomicReferenceFieldUpdater.get(user4));
boolean b3 = atomicReferenceFieldUpdater.compareAndSet(user4, 40, 400);
System.out.println("更新"+(b3?"成功":"失败"));
System.out.println("引用里user4值为"+atomicReferenceFieldUpdater.get(user4));
System.out.println("其他使用---------------");
User user5=new User("ly5",50);
User user6=new User("ly6",60);
User user7=new User("ly7",70);
AtomicMarkableReference<User> userAtomicMarkableReference=new AtomicMarkableReference<>(user5,true);
boolean b4 = userAtomicMarkableReference.weakCompareAndSet(user6, user7, true, false);
System.out.println("更新"+(b4?"成功":"失败"));
System.out.println("引用里值为"+userAtomicMarkableReference.getReference());
boolean b5 = userAtomicMarkableReference.weakCompareAndSet(user5, user7, false, true);
System.out.println("更新"+(b5?"成功":"失败"));
System.out.println("引用里值为"+userAtomicMarkableReference.getReference());
boolean b6 = userAtomicMarkableReference.weakCompareAndSet(user5, user7, true, false);
System.out.println("更新"+(b6?"成功":"失败"));
System.out.println("引用里值为"+userAtomicMarkableReference.getReference());
System.out.println("AtomicStampedReference使用---------------");
User user80=new User("ly8",80);
User user90=new User("ly9",90);
User user100=new User("ly10",100);
AtomicStampedReference<User> userAtomicStampedReference=new AtomicStampedReference<>(user80,80);//版本80
//...每次更改stamp都加1
//这里假设中途被改成81了
boolean b7 = userAtomicStampedReference.compareAndSet(user80, user100,81,90);
System.out.println("更新"+(b7?"成功":"失败"));
System.out.println("引用里值为"+userAtomicStampedReference.getReference());
boolean b8 = userAtomicStampedReference.compareAndSet(user80, user100,80,90);
System.out.println("更新"+(b8?"成功":"失败"));
System.out.println("引用里值为"+userAtomicStampedReference.getReference());
}
}
/*
原子更新数值---------------
原子增加后为1
原子更新数组---------------
数组元素[1]增加后为3
数组为[0, 3, 0]
原子更新对象---------------
更新失败
引用里值为User(name=ly1, age=10)
更新成功
引用里值为User(name=ly3, age=30)
原子更新对象属性---------------
更新失败
引用里user4值为40
更新成功
引用里user4值为400
其他使用---------------
更新失败
引用里值为User(name=ly5, age=50)
更新失败
引用里值为User(name=ly5, age=50)
更新成功
引用里值为User(name=ly7, age=70)
AtomicStampedReference使用---------------
更新失败
引用里值为User(name=ly8, age=80)
更新成功
引用里值为User(name=ly10, age=100)
Process finished with exit code 0
*/
原子类介绍
#
- 在化学上,原子是构成一般物质的最小单位,化学反应中是不可分割的,Atomic指一个操作是不可中断的,即使在多个线程一起执行时,一个操作一旦开始就不会被其他线程干扰
- 原子类–>具有原子/原子操作特征的类
- 并发包java.util.concurrent 的原子类都放着
java.util.concurrent.atomic中

- 根据操作的数据类型,可以将JUC包中的原子类分为4类(基本类型、数组类型、引用类型、对象的属性修改类型)
2022年11月30日 14:48 周三转载自https://github.com/Snailclimb/JavaGuide (添加小部分笔记)感谢作者!
Semaphore [ˈseməfɔː(r)]
- 何为 AQS?AQS 原理了解吗?
CountDownLatch 和 CyclicBarrier 了解吗?两者的区别是什么?- 用过
Semaphore 吗?应用场景了解吗? - ……
AQS简单介绍
#
AQS,AbstractQueueSyschronizer,即抽象队列同步器,这个类在java.util.concurrent.locks包下面

AQS是一个抽象类,主要用来构建锁和同步器
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
}
AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7) 等等皆是基于 AQS 的。
AQS原理
#
AQS核心思想
#
面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来
AQS 核心思想是,如果被请求的共享资源(AQS内部)空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
[ 搜索了一下,CLH好像是人名 ]
在 CLH 同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。
CLH队列结构
