赞
踩
2.5.2 ParNew 收集器
可以认为是 Serial 收集器的多线程版本。
并行:Parallel
指多条垃圾收集线程并行工作,此时用户线程处于等待状态
并发:Concurrent
指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,而垃圾回收线程在另一个 CPU 上运行。
2.5.3 Parallel Scavenge 收集器
这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。
CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。
作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。
2.5.4 Serial Old 收集器
收集器的老年代版本,单线程,使用
标记 —— 整理
。
2.5.5 Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用
标记 —— 整理
2.5.6 CMS 收集器
CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于
标记 —— 清除
算法实现。
运作步骤:
初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
并发标记(CMS concurrent mark):进行 GC Roots Tracing
重新标记(CMS remark):修正并发标记期间的变动部分
并发清除(CMS concurrent sweep)
缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记 —— 清除
算法带来的空间碎片
2.5.7 G1 收集器
面向服务端的垃圾回收器。
优点:并行与并发、分代收集、空间整合、可预测停顿。
运作步骤:
初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(Final Marking)
筛选回收(Live Data Counting and Evacuation)
2.6.1 对象优先在 Eden 分配
对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲区,将线程优先在 (TLAB) 上分配。少数情况会直接分配在老年代中。
一般来说 Java 堆的内存模型如下图所示:
新生代 GC (Minor GC)
发生在新生代的垃圾回收动作,频繁,速度快。
老年代 GC (Major GC / Full GC)
发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。
2.6.2 大对象直接进入老年代
2.6.3 长期存活的对象将进入老年代
2.6.4 动态对象年龄判定
2.6.5 空间分配担保
3. Java 内存模型与线程
屏蔽掉各种硬件和操作系统的内存访问差异。
3.1.1 主内存和工作内存之间的交互
| 操作 | 作用对象 | 解释 |
| — | — | — |
| lock | 主内存 | 把一个变量标识为一条线程独占的状态 |
| unlock | 主内存 | 把一个处于锁定状态的变量释放出来,释放后才可被其他线程锁定 |
| read | 主内存 | 把一个变量的值从主内存传输到线程工作内存中,以便 load 操作使用 |
| load | 工作内存 | 把 read 操作从主内存中得到的变量值放入工作内存中 |
| use | 工作内存 | 把工作内存中一个变量的值传递给执行引擎,
每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行这个操作 |
| assign | 工作内存 | 把一个从执行引擎接收到的值赋接收到的值赋给工作内存的变量,
每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 |
| store | 工作内存 | 把工作内存中的一个变量的值传送到主内存中,以便 write 操作 |
| write | 工作内存 | 把 store 操作从工作内存中得到的变量的值放入主内存的变量中 |
3.1.2 对于 volatile 型变量的特殊规则
关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。
一个变量被定义为 volatile 的特性:
如果不符合
运算结果并不依赖变量当前值,或者能够确保只有单一的线程修改变量的值
和变量不需要与其他的状态变量共同参与不变约束
就要通过加锁(使用 synchronize 或 java.util.concurrent 中的原子类)来保证原子性。
通过插入内存屏障保证一致性。
3.1.3 对于 long 和 double 型变量的特殊规则
Java 要求对于主内存和工作内存之间的八个操作都是原子性的,但是对于 64 位的数据类型,有一条宽松的规定:允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作划分为两次 32 位的操作来进行,即允许虚拟机实现选择可以不保证 64 位数据类型的 load、store、read 和 write 这 4 个操作的原子性。这就是 long 和 double 的非原子性协定。
3.1.4 原子性、可见性与有序性
回顾下并发下应该注意操作的那些特性是什么,同时加深理解。
由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write。大致可以认为基本数据类型的操作是原子性的。同时 lock 和 unlock 可以保证更大范围操作的原子性。而 synchronize 同步块操作的原子性是用更高层次的字节码指令 monitorenter 和 monitorexit 来隐式操作的。
是指当一个线程修改了共享变量的值,其他线程也能够立即得知这个通知。主要操作细节就是修改值后将值同步至主内存(volatile 值使用前都会从主内存刷新),除了 volatile 还有 synchronize 和 final 可以保证可见性。同步块的可见性是由“对一个变量执行 unlock 操作之前,必须先把此变量同步会主内存中( store、write 操作)”这条规则获得。而 final 可见性是指:被 final 修饰的字段在构造器中一旦完成,并且构造器没有把 “this” 的引用传递出去( this 引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final 字段的值。
如果在被线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句指“线程内表现为串行的语义”,后半句是指“指令重排”现象和“工作内存与主内存同步延迟”现象。Java 语言通过 volatile 和 synchronize 两个关键字来保证线程之间操作的有序性。volatile 自身就禁止指令重排,而 synchronize 则是由“一个变量在同一时刻指允许一条线程对其进行 lock 操作”这条规则获得,这条规则决定了持有同一个锁的两个同步块只能串行的进入。
3.1.5 先行发生原则
也就是 happens-before 原则。这个原则是判断数据是否存在竞争、线程是否安全的主要依据。先行发生是 Java 内存模型中定义的两项操作之间的偏序关系。
天然的先行发生关系
| 规则 | 解释 |
| — | — |
| 程序次序规则 | 在一个线程内,代码按照书写的控制流顺序执行 |
| 管程锁定规则 | 一个 unlock 操作先行发生于后面对同一个锁的 lock 操作 |
| volatile 变量规则 | volatile 变量的写操作先行发生于后面对这个变量的读操作 |
| 线程启动规则 | Thread 对象的 start() 方法先行发生于此线程的每一个动作 |
| 线程终止规则 | 线程中所有的操作都先行发生于对此线程的终止检测
(通过 Thread.join() 方法结束、 Thread.isAlive() 的返回值检测) |
| 线程中断规则 | 对线程 interrupt() 方法调用优先发生于被中断线程的代码检测到中断事件的发生
(通过 Thread.interrupted() 方法检测) |
| 对象终结规则 | 一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始 |
| 传递性 | 如果操作 A 先于 操作 B 发生,操作 B 先于 操作 C 发生,那么操作 A 先于 操作 C |
3.2.1 线程的实现
使用内核线程实现
直接由操作系统内核支持的线程,这种线程由内核完成切换。程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口 —— 轻量级进程(LWP),轻量级进程就是我们通常意义上所讲的线程,每个轻量级进程都有一个内核级线程支持。
使用用户线程实现
广义上来说,只要不是内核线程就可以认为是用户线程,因此可以认为轻量级进程也属于用户线程。狭义上说是完全建立在用户空间的线程库上的并且内核系统不可感知的。
使用用户线程夹加轻量级进程混合实现
直接看图
Java 线程实现
平台不同实现方式不同,可以认为是一条 Java 线程映射到一条轻量级进程。
3.2.2 Java 线程调度
协同式线程调度
线程执行时间由线程自身控制,实现简单,切换线程自己可知,所以基本没有线程同步问题。坏处是执行时间不可控,容易阻塞。
抢占式线程调度
每个线程由系统来分配执行时间。
3.2.3 状态转换
五种状态:
创建后尚未启动的线程。
Runable 包括了操作系统线程状态中的 Running 和 Ready,也就是出于此状态的线程有可能正在执行,也有可能正在等待 CPU 为他分配时间。
出于这种状态的线程不会被 CPU 分配时间,它们要等其他线程显示的唤醒。
以下方法会然线程进入无限期等待状态:
1.没有设置 Timeout 参数的 Object.wait() 方法。
2.没有设置 Timeout 参数的 Thread.join() 方法。
3.LookSupport.park() 方法。
处于这种状态的线程也不会分配时间,不过无需等待配其他线程显示地唤醒,在一定时间后他们会由系统自动唤醒。
以下方法会让线程进入限期等待状态:
1.Thread.sleep() 方法。
2.设置了 Timeout 参数的 Object.wait() 方法。
3.设置了 Timeout 参数的 Thread.join() 方法。
4.LockSupport.parkNanos() 方法。
5.LockSupport.parkUntil() 方法。
线程被阻塞了,“阻塞状态”和“等待状态”的区别是:“阻塞状态”在等待着获取一个排他锁,这个时间将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
已终止线程的线程状态。
4. 线程安全与锁优化
// 待填
5. 类文件结构
// 待填
有点懒了。。。先贴几个网址吧。
1. Official:The class File Format
2.亦山: 《Java虚拟机原理图解》 1.1、class文件基本组织结构
6. 虚拟机类加载机制
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
在 Java 语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。
类的生命周期( 7 个阶段)
其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。解析阶段可以在初始化之后再开始(运行时绑定或动态绑定或晚期绑定)。
以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):
遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
使用 java.lang.reflect 包的方法对类进行反射调用的时候。
当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。
前面的五种方式是对一个类的主动引用,除此之外,所有引用类的方法都不会触发初始化,佳作被动引用。举几个例子~
public class SuperClass {
static {
System.out.println(“SuperClass init!”);
}
public static int value = 1127;
}
public class SubClass extends SuperClass {
static {
System.out.println(“SubClass init!”);
}
}
public class ConstClass {
static {
System.out.println(“ConstClass init!”);
}
public static final String HELLOWORLD = “hello world!”
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
/**
output : SuperClass init!
通过子类引用父类的静态对象不会导致子类的初始化
只有直接定义这个字段的类才会被初始化
*/
SuperClass[] sca = new SuperClass[10];
/**
output :
通过数组定义来引用类不会触发此类的初始化
虚拟机在运行时动态创建了一个数组类
*/
System.out.println(ConstClass.HELLOWORLD);
/**
output :
常量在编译阶段会存入调用类的常量池当中,本质上并没有直接引用到定义类常量的类,
因此不会触发定义常量的类的初始化。
“hello world” 在编译期常量传播优化时已经存储到 NotInitialization 常量池中了。
*/
}
}
6.2 类的加载过程
6.2.1 加载
通过一个类的全限定名来获取定义次类的二进制流(ZIP 包、网络、运算生成、JSP 生成、数据库读取)。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法去这个类的各种数据的访问入口。
数组类的特殊性:数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建的,数组创建过程如下:
如果数组的组件类型是引用类型,那就递归采用类加载加载。
如果数组的组件类型不是引用类型,Java 虚拟机会把数组标记为引导类加载器关联。
数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。
内存中实例的 java.lang.Class 对象存在方法区中。作为程序访问方法区中这些类型数据的外部接口。
加载阶段与连接阶段的部分内容是交叉进行的,但是开始时间保持先后顺序。
6.2.2 验证
是连接的第一步,确保 Class 文件的字节流中包含的信息符合当前虚拟机要求。
文件格式验证
是否以魔数 0xCAFEBABE 开头
主、次版本号是否在当前虚拟机处理范围之内
常量池的常量是否有不被支持常量的类型(检查常量 tag 标志)
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
Class 文件中各个部分集文件本身是否有被删除的附加的其他信息
……
只有通过这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面 3 个验证阶段全部是基于方法区的存储结构进行的,不再直接操作字节流。
元数据验证
这个类是否有父类(除 java.lang.Object 之外)
这个类的父类是否继承了不允许被继承的类(final 修饰的类)
如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
类中的字段、方法是否与父类产生矛盾(覆盖父类 final 字段、出现不符合规范的重载)
这一阶段主要是对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。
字节码验证
保证任意时刻操作数栈的数据类型与指令代码序列都鞥配合工作(不会出现按照 long 类型读一个 int 型数据)
保证跳转指令不会跳转到方法体以外的字节码指令上
保证方法体中的类型转换是有效的(子类对象赋值给父类数据类型是安全的,反过来不合法的)
……
这是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段对类的方法体进行校验分析,保证校验类的方法在运行时不会做出危害虚拟机安全的事件。
符号引用验证
符号引用中通过字符创描述的全限定名是否能找到对应的类
在指定类中是否存在符方法的字段描述符以及简单名称所描述的方法和字段
符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
……
最后一个阶段的校验发生在迅疾将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,还有以上提及的内容。
符号引用的目的是确保解析动作能正常执行,如果无法通过符号引用验证将抛出一个 java.lang.IncompatibleClass.ChangeError 异常的子类。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。
6.2.3 准备
这个阶段正式为类分配内存并设置类变量初始值,内存在方法去中分配(含 static 修饰的变量不含实例变量)。
public static int value = 1127;
这句代码在初始值设置之后为 0,因为这时候尚未开始执行任何 Java 方法。而把 value 赋值为 1127 的 putstatic 指令是程序被编译后,存放于 clinit() 方法中,所以初始化阶段才会对 value 进行赋值。
基本数据类型的零值
| 数据类型 | 零值 | 数据类型 | 零值 |
| — | — | — | — |
| int | 0 | boolean | false |
| long | 0L | float | 0.0f |
| short | (short) 0 | double | 0.0d |
| char | ‘\u0000’ | reference | null |
| byte | (byte) 0 | |
特殊情况:如果类字段的字段属性表中存在 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 1127。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。
如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。
体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-0gjae42E-1711920787523)]
[外链图片转存中…(img-LyRUFmWs-1711920787523)]
[外链图片转存中…(img-534Fxbd7-1711920787524)]
[外链图片转存中…(img-Fytihkpe-1711920787525)]
[外链图片转存中…(img-6Bf8QnLy-1711920787525)]
[外链图片转存中…(img-hH8lyfpT-1711920787526)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-taNzR0c3-1711920787526)]
总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。
如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。
[外链图片转存中…(img-PaoLy2KG-1711920787527)]
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。