1.1 基础总结

StringBuilder和StringBuffer主要不同在于,StringBuffer的append、delete、replace、length等方法前都加了synchronized关键字保证线程安全,而StringBuilder没有,另外的区别在于StringBuffer有一个toStringCache的char数组,是用于记录最近一次toString()方法的缓存,任何时候只要StringBuffer被修改了这个变量会被赋值为null
JVM(Java Virtual Machine)Java虚拟机
一。三大体系:类加载器,运行时数据区,执行引擎
二。JRE/JDK/JVM的关系
JDK(Java Development Kit):Java程序开发工具包,开发时必不可少,包含jre
JRE(JavaRuntimeEnvironment):Java运行环境,只要安装jre就可以运行java程序,包含bin和lib目录,bin就是jvm,lib就是jvm工作所需要的类库
JVM(JavaVirtualMachine):Java虚拟机,Windows、linux、unix都有对应的jvm,实现java的跨平台
三。类加载器
1.启动类加载器(Bootstrap ClassLoader):加载rt.jar(runtime.jar)
2.扩展类加载器(Extension ClassLoader):加载ext目录的类
3.应用程序类加载器(Application ClassLoader):加载应用程序类,classpath下的类
JVM在加载类时默认采用的是双亲委派机制,首先检查类有没有被加载过,如果没有被加载则依次递归向上委托,最终达到顶层的启动类加载器,当父类加载器无法完成加载任务,子类才会尝试加载
四。类的生命周期(类加载顺序)
1. 加载:将类的.class文件读入到内存
2. 链接:
校验:校验加载进来的.class字节码文件是否符合规范
准备:为类的静态变量分配内存空间,并附默认值
解析:符号引用转直接引用
3. 初始化:静态变量赋初始值,执行静态代码块
4. 使用
5. 卸载
五。运行时数据区
线程私有的:
1. java虚拟机栈:存放8种基本数据类型,局部变量,以及指向堆的地址
2. 本地方法栈:指向native修饰的本地方法
3. 程序计数器(PC寄存器):当前线程所执行的字节码的行号指示器,保证线程切换后能够回到正确的位置
线程共享的:
4: 堆:存放对象和数组
5: 方法区:存放类的信息、常量、静态变量
六。Jvm的生命周期:
1. 启动:main()方法启动时,jvm就启动了
2. 运行:jvm内部有守护线程和非守护线程,main()方法是非守护线程,jvm的gc就是守护线程
3. 退出:所有的非守护线程都终止时,jvm才退出
七。执行引擎
1. 解释器:一条一条地解释和执行字节码指令,所以可以很快地解释字节码,但是执行起来会比较慢。对于重复的代码也会重复解释执行
2. 即时编译器:首先按照解释执行的方式来执行,然后将重复的代码编译成本地代码,提高效率
八。堆内存划分
年轻代:新创建的对象一般放在年轻代,内存不足时会触发minorgc
Eden伊甸园区:
Survivor From 幸存者区:
Survivor To区
年老代:多次GC后依然存活的对象会被放到年老代。年老代内存不足会触发full gc(major gc)
持久代:可以理解为方法区。但是方法区也有可能发生GC,当一个类的对象全都被GC了,同时它的类加载器也被GC了,就会触发这个类的GC
九。判断对象是否可以被回收 :https://blog.youkuaiyun.com/qq_36795474/article/details/79401113
1. 引用计数法:每个对象都有一个引用计数器,当对象被引用一次计数器就加1;当引用失效时计数器就减1。当对象的计数器为0时,对象就是要被回收的。
缺点是无法解决对象之间相互循环引用的问题。
2. 可达性分析算法:以GC Roots节点作为起始点向下搜索,当一个对象到GC Roots没有任何引用相连时,则证明此对象是不可用的。
十。垃圾收集算法
1. 标记清除算法:标记被引用的对象,清除没有被标记的对象。缺点是会产生内存碎片,如果创建一个较大的对象时可能无法找到可以分配的连续内存。
2. 复制算法:将内存分为等大小的两块,每次仅使用其中的一块,标记可达的对象,将可达的对象复制到空闲区,将原本的活动区清除掉。
3. 标记整理算法:标记可达的对象,将标记的对象放到内存的一端,对区域外的内存进行清除
4. 分代收集算法:大部分JVM的垃圾收集器采用的算法。年轻代每次垃圾回收都要回收大部分对象,因此使用复制算法;老年代每次回收都只回收少量对象,因此使用标记整理算法。
十一。新生代和老年代的区别
所谓的新生代和老年代是针对于分代收集算法来定义的,新生代又分为Eden和Survivor两个区。加上老年代就这三个区。数据会首先分配到Eden区 当中(当然也有特殊情况,如果是大对象那么会直接放入到老年代(大对象是指需要大量连续内存空间的java对象)。),当Eden没有足够空间的时候就会 触发jvm发起一次Minor GC。如果对象经过一次Minor GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空 间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,当年龄达到一定的程度(默认为15)时,就会被晋升到老年代 中了,当然晋升老年代的年龄是可以设置的。如果老年代满了就执行:Full GC 因为不经常执行,因此采用了 Mark-Compact算法清理;只要FULL GC JVM(stop world) 会把所有的用户线程停止。
十二。jvm垃圾回收器
1. Serial收集器:单线程的收集器,串行,简单高效,但是会stop the world停止其他线程
应用场景:客户端模式下的虚拟机
2. ParNew收集器:Serial的多线程版本
应用场景:服务器模式的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。
3. Parallel Scavenge(平行线清道夫)收集器:新生代收集器,采用复制算法,并行的多线程收集器,又称为吞吐量优先收集器
4. Serial Old(串行老年代)收集器:Serial收集器的老年代版本,单线程收集器,采用标记-整理算法。
5. Parallel Old(平行线老年代)收集器:Parallel Scavenge收集器的老年代版本,多线程,采用标记-整理算法。
应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器
6. CMS收集器(Concurrent Mark Sweep-并发标记扫描):以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现。并发收集、低停顿。年老代收集器。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
运行过程:初始标记、并发标记、重新标记、并发清除
缺点:对CPU资源非常敏感;无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生;标记-清除算法会存在空间碎片
7. G1收集器:并行与并发,分代收集。建立可预测的停顿时间模型,因此可以预测垃圾收集的停顿时间。
应用场景:要求尽可能可控GC停顿时间的场景
运行过程:初始标记、并发标记、最终标记、筛选回收
jdk1.7 和jdk1.8 使用的是相同的收集器
jdk1.7 采用的是 . Parallel Scavenge 和 Parallel Old
jdk1.8 采用的是 . Parallel Scavenge 和 Parallel Old
十三。并行、并发、吞吐量
1. 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
2. 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
3. 吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
十四。JVM调优工具
jps:查看虚拟机启动的所有进程,启动参数
jconsole:分析内存使用量、线程数、类加载数和CPU占用率
jstack:查看进程内的线程堆栈信息
ps -ef | grep jetty:查找进程
十五。JVM调优指令
-Xms2048m -Xmx2048m 堆得初始值和最大值,最好设置一样,防止内存抖动
-Xss2m 栈的深度
-XX:PermSize=256m -XX:MaxPermSize=512m 持久代的初始值和最大值
-XX:NewSize=512m -XX:MaxNewSize=512m 年轻代的初始值和最大值
-XX:NewRatio=2 年轻代占内存的三分之一
-XX:SurvivorRatio=5 survivor区占内存的1/7,eden区占5/7
-XX:+UseConcMarkSweepGC -XX:+UseParallelGC 设置使用CMS收集器/Parallel收集器
-XX:ParallelCMSThreads=1 -XX:ParallelGCThreads=3 设置CMS收集器/Parallel收集器线程数
-XX:MaxGCPauseMillis=100 垃圾回收最大停顿时间
-XX:GCTimeRatio=99 垃圾回收占程序运行的1/100, 1/(1+n)
-XX:+PrintGCDetails -Xloggc:/app1/log/gc.log 打印垃圾回收信息和位置设置
十六。Java内存模型
1. java定义内存模型的目的是:为了屏蔽各种硬件和操作系统的内存访问之间的差异。
2. java内存模型规定了所有的变量都存储在主内存中,每条线程拥有自己的工作内存,工作内存保存了主内存中变量的副本。
3. 线程对变量操作只能在工作内存中进行,不能直接读写主内存的变量。
4. 不同线程之间的变量访问需要通过主内存来完成。
java内存模型和java运行时数据区域的关系:主内存对应着java堆,工作内存对应着java栈。
volatile关键字,保证修饰的变量对所有线程的可见性,禁止指令重排序
十七。volatile和synchronized的区别
1. volatile的本质是告诉编译器当前变量的值在工作内存中是不确定的,需要直接从主内存中读取;而synchronized是锁定当前变量,只有当前线程可以访问,其他线程被阻塞住
2. volatile只能修饰变量;synchronized则可以修饰变量、方法、类
3. volatile仅能实现变量的修改可见性,并不能保证原子性;而synchronized可以保证变量的修改可见性和原子性
4. volatile不会造成线程阻塞;而synchronized可能会造成线程的阻塞
5. volatile修饰的变量不会被编译器指令重排序优化;而synchronized修饰的变量可以被编译器优化
十八。页面展示堆栈信息、GC信息
堆栈信息:Thread.getAllStackTraces()
GC信息:ManagementFactory.getGarbageCollectorMXBean()
十九。Class.forName和ClassLoader的区别
Class.forName(className)方法,内部实际调用的方法是 Class.forName(className,true,classloader); // boolean initialize默认为true,进行初始化
ClassLoader.loadClass(className)方法,内部实际调用的方法是 ClassLoader.loadClass(className,false); // boolean resolve默认为false,不进行链接
1. Class.forName会将类的.class文件加载到jvm中,并且进行链接(校验、准备、解析)和初始化,执行类中的static块
2. ClassLoader只会将.class文件加载到jvm中,不会执行static静态代码块,只有在newInstance才会去执行static块
二十。内存泄漏和内存溢出
内存泄漏是指申请的内存空间没有被释放
内存溢出是指存储的数据超过了内存容量
Java中不存在内存泄漏(可能存在,GC回收不了的时候),因为Java中有gc垃圾回收,不可达的对象会被回收
设计模式
一。设计模式的目的
设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总和。总结起来,就是多用接口和抽象类,从而增加代码的可扩展性,降低模块间的依赖和联系。
二。设计模式分类
1. 创建型模式:关注对象的创建,解耦对象的实例化过程
2. 结构型模式:关注类和对象之间的组合
3. 行为型模式:关注类和对象之间的通信
三。设计模式六大原则:
1. 开闭原则:对扩展开放,对修改关闭
2. 里氏替换原则:任何父类出现的地方,子类都可以出现。也就是子类可以替换父类的任何功能
3. 依赖倒转原则:针对接口编程,依赖于抽象而不依赖于实现
4. 接口隔离原则:使用多个隔离的接口,比使用单个接口要好。降低类之间的耦合度
5. 最少知道原则:一个实体应当尽量少的与其他实体发生相互作用,使得系统功能模块相对独立
6. 合成复用原则:尽量使用合成/聚合的方式,而不是使用继承
四。常用的设计模式:
单例模式:保证一个类只有一个实例,并提供一个访问它的全局访问点
工厂模式:定义一个创建对象的接口,让子类自己决定实例化哪个类
抽象工厂模式:创建相关或依赖对象的家族,而无需明确指定具体类
代理模式:给某对象提供一个代理,来控制对该对象的访问
模板模式:定义一个算法结构,将一些步骤延迟到子类中实现
五。工厂模式与抽象工厂模式的区别
工厂模式针对的是一个产品等级结构,抽象工厂模式针对的是面向多个产品等级结构
六。JDK库中使用的设计模式
单例模式:RunTime.getRunTime()
工厂模式:Class.forName("")
代理模式:动态代理
迭代器模式:Iterator
七。Spring中使用的设计模式
工厂模式:BeanFactory
多线程
一。进程和线程
进程是资源分配的最小单位,进行内部可以创建多个线程
线程是CPU调度的最小单元,也叫轻量级进程,每个线程都拥有各自的程序计数器、Java虚拟机栈和本地方法栈,并且共享内存中的变量,线程中的通信更方便
二。线程的生命周期
1. 新建:使用new方法,new出来的线程
2. 就绪:调用的线程的start()方法后进入就绪状态,等待CPU资源
3. 运行:就绪的线程被调度并获得CPU资源时,便进入运行状态
4. 阻塞:sleep()、wait()之后线程就处于了阻塞状态。调用notify或者notifyAll()方法唤醒线程后进入就绪状态
5. 死亡:线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束
三。多线程的接口和类
1. Executor接口:定义了一个execute(Runnable command)方法
2. ExecutorService接口:继承了Executor接口,扩展了Future submit(Callable/Runnable)方法
3. AbstractExecutorService抽象类:实现了ExecutorService接口,实现了大部分方法
4. ThreadPoolExecutor类:继承了AbstractExecutorService抽象类,提供方法创建线程池并返回一个ExecutorService对象
5. Executors类:一般用来创建指定的线程池
四。ThreadPoolExecutor构造函数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {……}
corePoolSize:核心池线程数量。线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务
maximumPoolSize:最大线程数。只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉
unit:时间单位。TimeUnit.MILLISECONDS
workQueue:阻塞队列。用于保存任务的队列,当工作线程数大于corePoolSize时,新进来的任务会被放到阻塞队列中
threadFactory:建线程的工厂类。默认使用Executors.defaultThreadFactory()
handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略
线程池合适的线程数量是多少?
1.CPU密集型
第一种是 CPU 密集型任务,比如加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务。
最佳线程数 = CPU 核心数的 1~2 倍
如果设置过多的线程,实际上并不会起到很好的效果。此时假设我们设置的线程数是 CPU 核心数的 2 倍以上,因为计算机的任务很重,会占用大量的 CPU 资源,所以这是 CPU 每个核心都是满负荷工作,而设置过多的线程数,每个线程都去抢占 CPU 资源,就会产生不必要的上下文切换,反而会造成整体性能的下降。
2.IO密集型
第二种任务是耗时 IO 型,比如数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特别消耗 CPU 资源,但是 IO 操作很耗时,总体会占用比较多的时间。
对于这种情况任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。
3.通用公式
线程数 = CPU 核心数 * (1+ IO 耗时/CPU 耗时)
通过这个公式,我们可以计算出一个合理的线程数量,如果任务的 IO 耗时时间长,线程数就随之增加,而如果CPU 耗时长,也就是对于我们上面的 CPU 密集型任务,线程数就随之减少。
太少的线程数会使得程序整体性能降低,而过多的线程也会消耗内存等其他资源,所以如果想要更准确的话,可以进行压测,监控 JVM 的线程情况以及 CPU 的负载情况,根据实际情况衡量应该创建的线程数,合理并充分利用资源。
4.结论
综上所述我们就可以得出以下结论:
线程的 CPU 耗时所占比例越高,就需要越少的线程
线程的 IO 耗时所占比例越高,就需要越多的线程
针对不同的程序,进行对应的实际测试就可以得到最合适的选择
线程数 >= CPU 核心数
五。阻塞队列
1. 无界队列:LinkedBlockingQueue。如果没有设置初始容量则取Integer.MAX_VALUE队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务处理速度造成请求堆积)可能导致内存占用过多或OOM
2. 有界队列:ArrayBlockingQueue,LinkedBlockingQueue。队列长度受限,当队列满了就需要创建多余的线程来执行任务需要设置初始容量
3. 同步移交队列:SynchronousQueue。不会保存提交的任务,而是将直接新建一个线程来执行新来的任务阻塞队列,
每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,一进一出,避免队列里缓冲数据,这样在系统异常关闭时,就能排除因为阻塞队列丢消息的可能
大家有兴趣可以关注一下微信订阅号“向测试媛出发”,会不定时的发布一些测试学习过程中的心得以供大家参考:

1157

被折叠的 条评论
为什么被折叠?



