同步操作将从 edgevagrant/JAVA-000 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
所有的东西放在一个文件感觉太多了,不好看,将学习记录和总结分散在文件中
在三周的学习中,进行了Java背景知识,Java内存模型、Java字节码、GC相关知识、GC调优等模块的学习。其中Java内存模型、GC等相关知识是需要掌握的重点,在工作中排除问题、优化性能的时候会用到这部分知识。其次是字节码相关知识,对于编译相关的,有个大概的了解即可,感觉不必太深入,但其中的类加载机制及应用需要掌握,这个在工作中也会用到;其他的知识有个大概的了解即可。关于调试工具的时候和GC日志分析,自己需要多动手实践。
涉及内容大致如下:
这部分设计Java技术体系、发展史,做一个了解即可。老师在课上也做了相关的介绍,这里推荐扩展阅读:《深入理解Java虚拟机:JVM高级特性与最佳实践》的第一部分。
这部分知识主要设计Java机器码的相关和Java类加载机制的应用。Java机器码的相关有个大概的了解即可,感觉在工作中不做底层这部分知识相对而言应用的比较少,如果用到了,回过头来深入,掌握。
大致的内容如下
类加载中需要掌握的内容有:双亲委派机制和自定义类加载器。类加载机制这个在工作中应用还是比较多了,可能写的比较少,但明里暗里打交道却不少,下面是一个举例
在开发中我们接触到的主流Java Web服务器,如Tomcat、Jetty、WebLogic、WebSphere等服务器,都实现了自己的多个类加载器,相应的需求如下:
上面举例的这个可详细参考:《深入理解Java虚拟机:JVM高级特性与最佳实践》的第9章类加载及执行子系统的案例与实战。
课程中涉及到的大致内容如下:
这个是要重点掌握的,核心点就是一个函数:defineClass。这个函数读取class的字节流,生成对应的类。核心的使用方法就是,读取转化class为字节流,调用函数,生成。掌握了这个就对类的动态生成加载有了大致的了解。
import java.io.*;
import java.lang.reflect.InvocationTargetException;
/**
* 自定义类加载
* 关键点是defineClass,读取字节码的字节流,生成class,思路就是转化class文件为字节流,传入defineClass中
* 思路:
* 1.继承ClassLoader,重写findClass方法
* 2.从文件中读取转化成字节流
* 3.传入defineClass进行加载
* 4.生成实例,调用方法
*/
public class HelloClassLoader extends ClassLoader {
@Override
public Class findClass(String name) {
byte[] b = new byte[0];
try {
b = loadClassFromFile(name);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// name 就是加载的类名称,这里注意要填写正确
return defineClass("Hello", b, 0, b.length);
}
/**
* 读取class文件,转化内容为字节流
* @param fileName 文件路径
* @return
* @throws FileNotFoundException
*/
private byte[] loadClassFromFile(String fileName) throws FileNotFoundException {
System.out.println(fileName);
File file = new File(fileName);
FileInputStream inputStream = new FileInputStream(file);
byte[] buffer;
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue = 0;
try {
while ( (nextValue = inputStream.read()) != -1 ) {
// 注意字节还原
byteStream.write(255 - nextValue);
}
} catch (IOException e) {
e.printStackTrace();
}
buffer = byteStream.toByteArray();
return buffer;
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
// 文件读取,字节流转换,加载
String path = "C:\\Users\\12439\\Documents\\Code\\Java\\JAVA-000\\Week_01\\code\\src\\main\\resources\\Hello.xlass".replace("\\", "/");
HelloClassLoader loader = new HelloClassLoader();
Class hello = loader.loadClass(path);
System.out.println(hello.getName());
// 生成实例,调用方法
Object instance = hello.newInstance();
System.out.println(hello.getMethod("hello").invoke(instance));
}
}
这个也是要重点掌握的,了解各个类加载器的关系,特别是父类加载器持有的类是能被子类看见的这一特性。
了解如上图所示的生命周期,其中有两个点需要注意:
一个实例如下:
// 代码
public class InitExample {
public static int var = 1;
static {
var = 2;
System.out.println("static print");
}
InitExample() {
System.out.println("init print");
}
public static void main(String[] args) {
InitExample example = new InitExample();
System.out.println(InitExample.var);
}
}
// 输出结果
static print
init print
2
从中可以看到,静态先于构造函数,而静态代码块执行先于静态变量语句。
这部分的内容可以说是重中之重了,有过C/C++开发工作的人应该知道内存管理的重要性和难度。虽然Java自己实现了内存管理,不用开发人员去操心,但其内存管理还是有不足之处,常常也会出内存泄漏和内存溢出等问题。当我们进行这些问题排查的时候,没有掌握相关的JVM内存管理知识,那就是盲目,没有方向,掌握这部分知识在解决问题的时候才能有所依据。
这部分内存主要涉及两块,一个是内存模型,内存模型是基础,影响了后面的内存管理,其中还涉及到了多线程竞争的问题,并发这块也是一个大问题,这里就不详述。第二个是垃圾回收的基础知识和各种GC算法,了解掌握GC算法有助于问题的分析和性能调优,这块也是相当重要,但感觉也不用深入到GC算法代码细节部分,了解其主要的算法思想即可,有需要在去深入。
GC工具的使用和日志分析就需要自己多动手实践了。
上图是Java虚拟机运行时的数据库,需要掌握其五个部分:方法区、堆、虚拟机栈、本地方法栈、程序计数器
还有非运行时的数据区:直接内存。这部分也会被频繁使用
上面这些哪些会有OOM现象:Java虚拟机栈(变量不断存放)、本地方法栈(不断递归)、堆(对象不断存放)、方法区(类信息等不断存放)、直接内存
OOM示例这块可以参考:深入理解Java虚拟机:JVM高级特性与最佳实践》的2.4 实战:OutOfMemoryError异常
垃圾回收这块有些前置知识需要了解:回收的判断标准(可达性分析算法)、分代收集理论、垃圾收集的三个基本算法
可达性分析算法:主要是通过枚举GC Roots作为根节点出发,能够遍历到的就是任然可以存活的对象
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
重要的分代假设里面,在ZGC之前,GC算法都有围绕这个假设进行算法设计
这个是相当重要的,目前为止的GC算法设计中都起码有其中一个算法的思想
如它的名字一样,分为标记和清除两个阶段:首先标记出需要清除的对象,标记完成后进行清除
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
G1及前面的算法的年轻代都是使用这个思想来进行垃圾的回收。因为存活对象少,复制所消耗的时间少
标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存,“标记-整理”算法的示意图如上图所示。
整理还有灵活调整空间,可以及时整理,也可以当碎片化到一定程度再进行整理
如上图所示,Serial收集其是一个单线程的,在其进行垃圾回收时,必须停止业务代码,让其专心进行回收,书中有个形象的比喻:“你妈妈在给你打扫房间的时候,肯定也会让你老老实实地在椅子上或者房间外待着,如果她一边打扫,你一边乱扔纸屑,这房间还能打扫完?”
其新生代使用标记-复制算法,老年代使用标记-整理算法
如上图所示,ParNew是基于Serial的并行版本,其他的基本没有任何改变,其思想就是利用多线程加快垃圾回收速度
其新生代使用标记-复制算法,老年代使用标记-整理算法
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值
这个收集器设计目标是达到设定吞吐量,还能智能调节
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
CMS是一个突破的节点,其突破点在于将垃圾回收的节点进行细分,达到了缩短STW时间,部分回收过程可以和业务代码一起运行。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求。
CMS是基于标记-清楚算法的,如上图所示,一共分为四个步骤:
CMS有如下优缺点:
G1 GC说是里程牌式的成果,其突破点在于对于内存布局的重新设计(Region),可控的最大停顿时间。可以说这些设计和改进,从CMS开始都是针对追求高响应的服务端的。
G1大致也分为下面四个阶段:
关于G1算法是否有STW停顿,这有一个小偷团伙作案的例子:
G1的优缺点:
shennadoah感觉是在G1的基础上最进一步的改进,大致的步骤如下,这里就不详细分析了:
ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
首先从ZGC的内存布局说起。与Shenandoah和G1一样,ZGC也采用基于Region的堆内存布局,但与它们不同的是,ZGC的Region(在一些官方资料中将它称为Page或者ZPage,本章为行文一致继续称为Region)具有动态性——动态创建和销毁,以及动态的区域容量大小。在x64硬件平台下,ZGC的Region可以具有如图3-19所示的大、中、小三类容量:
接下来是ZGC的核心问题——并发整理算法的实现。染色指针技术
其大致步骤如下:
在GC算法的发展的,个人感觉始终围绕着一个点:减少单次的STW时间。
如使用多线程加速回收,以减少串行GC的整个一次回收的STW时间;将回收过程细化,分阶段,更长的一次STW被分成了小部分,分散在各个回收处理阶段,也达到了减少单次STW时间。
而且GC算法的演进方向是为了满足服务端的快速响应。
算法演进图大致如下:
此部分仅个人想法,感觉还是实践有点少,经验不足,有待完善
感觉调优思路可以分为一条线,三个点
一条线:减少STW时间。因为代码运行总时间=GC STW时间 + 业务运行时间,减少STW时间,业务时间相对多了,系统就能处理更多的请求
三个点:内存占用、吞吐量、延迟,这三个构成了不可能三角(有点类似CAP理论),开发者需要在其中进行权衡,看当前系统需要的是什么,针对性的选择GC或者进行调优。当然,如今大部分开发人员都是WEB服务端了,基本上延迟是必须的了,内存这块好像也不缺,所以可以说后面基本是G1类GC的天下了吧。但肯定也有其他应用方面,这个时候还是需要权衡的。
分配速率(Allocation rate)表示单位时间内分配的内存量。通常 使用MB/sec作为单位。上一次垃圾收集之后,与下一次GC开 始之前的年轻代使用量,两者的差值除以时间,就是分配速率。 分配速率过高就会严重影响程序的性能,在JVM中可能会导致巨 大的GC开销。
提升速率(promotion rate)用于衡量单位时间内从年轻代提 升到老年代的数据量。一般使用MB/sec 作为单位, 和分配速率 类似。 JVM会将长时间存活的对象从年轻代提升到老年代。根据分代假 设,可能存在一种情况,老年代中不仅有存活时间长的对象,, 也可能有存活时间短的对象。这就是过早提升:对象存活时间还 不够长的时候就被提升到了老年代。 major GC 不是为频繁回收而设计的,但major GC 现在也要清 理这些生命短暂的对象,就会导致GC暂停时间过长。这会严重 影响系统的吞吐量。
一般来说过早提升的症状表现为以下形式:
解决这类问题,需要让年轻代存放得下暂存的数据,有两种简单 的方法:
至于选用哪个方案,要根据业务需求决定。在某些情况下,业务 逻辑不允许减少批处理的数量,那就只能增加堆内存,或者重新 指定年轻代的大小。如果都不可行,就只能优化数据结构,减少 内存消耗。 但总体目标依然是一致的:让临时数据能够在年轻代存放得下。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。