java虚拟机部分
垃圾回收算法
GC效率
内存效率:复制算法>标记压缩算法
内存整齐度:复制算法=标记压缩算法
内存利用率:标记压缩=标记清楚算法
– 算法一:引用计数法。
这个方法是最经典点的一种方法。具体是对于对象设置一个引用计数器,每增加一个变量对它的引用,引用计数器就会加1,没减少一个变量的引用,引用计数器就会减1,只有当对象的引用计数器变成0时,该对象才会被回收。可见这个算法很简单,但是简单往往会存在很多问题,这里我列举最明显的两个问题,
一是采用这种方法后,每次在增加变量引用和减少引用时都要进行加法或减法操作,如果频繁操作对象的话,在一定程度上增加的系统的消耗。
二是这种方法无法处理循环引用的情况。再解释下什么是循环引用,假设有两个对象 A和B,A中引用了B对象,并且B中也引用了A对象,
那么这时两个对象的引用计数器都不为0,但是由于存在相互引用导致无法垃圾回收A和 B,导致内存泄漏。
- 算法二:标记清除法
这个方法是将垃圾回收分成了两个阶段:标记阶段和清除阶段。
在标记阶段,通过跟对象,标记所有从跟节点开始的可达的对象,那么未标记的对象就是未被引用的垃圾对象。
在清除阶段,清除掉所以的未被标记的对象。
这个方法的缺点是,垃圾回收后可能存在大量的磁盘碎片,准确的说是内存碎片。因为对象所占用的地址空间是固定的。对于这个算法还有改进的算法,就是我后面要说的算法三。
- 算法三:标记压缩清除法(Java中老年代采用)。
在算法二的基础上做了一个改进,可以说这个算法分为三个阶段:标记阶段,压缩阶段,清除阶段。标记阶段和清除阶段不变,只不过增加了一个压缩阶段,就是在做完标记阶段后,将这些标记过的对象集中放到一起,确定开始和结束地址,比如全部放到开始处,这样再去清除,将不会产生磁盘碎片。但是我们也要注意到几个问题,压缩阶段占用了系统的消耗,并且如果标记对象过多的话,损耗可能会很大,在标记对象相对较少的时候,效率较高。
- 算法四:复制算法(Java中新生代采用)。
核心思想是将内存空间分成两块,同一时刻只使用其中的一块,在垃圾回收时将正在使用的内存中的存活的对象复制到未使用的内存中,然后清除正在使用的内存块中所有的对象,然后把未使用的内存块变成正在使用的内存块,把原来使用的内存块变成未使用的内存块。很明显如果存活对象较多的话,算法效率会比较差,并且这样会使内存的空间折半,但是这种方法也不会产生内存碎片。
- 算法五:分代法(Java堆采用)。
主要思想是根据对象的生命周期长短特点将其进行分块,根据每块内存区间的特点,使用不同的回收算法,从而提高垃圾回收的效率。
比如Java虚拟机中的堆就采用了这种方法分成了新生代和老年代。然后对于不同的代采用不同的垃圾回收算法。
新生代使用了复制算法,老年代使用了标记压缩清除算法。
JVM
类加载过程与双亲委派机制
//Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
//ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
//AppClassLoader:主要负责加载应用程序的主函数类
public static void main(String[] args) {
Test0106 test0106 = new Test0106();
System.out.println(test0106.getClass());
System.out.println(test0106.getClass().getClassLoader());
System.out.println(test0106.getClass().getClassLoader().getParent());
System.out.println(test0106.getClass().getClassLoader().getParent().getParent());
}
class javapra.Test0106
sun.misc.LauncherAppClassLoader@18b4aac2
sun.misc.LauncherExtClassLoader@2280cdac
null
0.当一个Test0106.class这样的文件要被加载时。不考虑我们自定义类加载器
1.类加载器收到类加载请求;
2.将这个请求向上委托父类加载器完成,一直向上委托直到启动类加载器;
3.启动类加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则抛出异常,通知子类加载器进行加载;
4.重复第三步
如果加载不到这个类则class not found
- 为什么要有这种设计
如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了
堆溢出与栈溢出
栈:先进后出;栈内存主管程序运行,生命周期,线程同步,线程结束,栈内存释放,一个方法就是一个栈帧,栈中存放八种基本数据类型+对象引用+实例的方法;如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常
public static void main(String[] args) {
a();
}
public static void a(){
b();
}
public static void b(){
a();
}
Exception in thread "main" java.lang.StackOverflowError
at javapra.Test0106.a(Test0106.java:14)
at javapra.Test0106.b(Test0106.java:17)
堆内存溢出:当扩展时无法申请到足够的内存时会抛出OutOfMemoryError 异常
static class OOMObject {
}
public static void main(String[] args) throws InterruptedException {
List<OOMObject> list = new ArrayList<>();
while(true) {
// TimeUnit.MILLISECONDS.sleep(1);
list.add(new OOMObject());
}
}
一个对象在内存中的实例化过程
public class Test0106{
String name; // 定义一个成员变量 name
int age; // 成员变量 age
void sing(){
System.out.println("人的姓名:"+name);
System.out.println("人的年龄:"+age);
}
public static void main(String[] args) {
String name; // 定义一个局部变量 name
int age; // 局部变量 age
Test0106 people = new Test0106() ; //实例化对象people
people.name = "张三" ; //赋值
people.age = 18; //赋值
people.sing(); //调用方法sing
}
}
1.类中的成员变量和方法体会进入到方法区
2.程序执行到 main() 方法时,main()函数方法体会进入栈区
,这一过程叫做进栈(压栈),定义了一个用于指向 Person 实例的变量 person
3.程序执行到 Person person = new Person(); 就会在堆内存
开辟一块内存区间,用于存放 Person 实例对象,然后将成员变量和成员方法放在 new 实例中都是取成员变量和成员方法的地址值
4.对 person 对象进行赋值, person.name = “张三” ; person.age = 18;先在栈区找到 person,然后根据地址值找到 new Person() 进行赋值操作
5.当程序走到 sing() 方法时,先到栈区找到 person这个引用变量,然后根据该地址值在堆内存中找到 new Person() 进行方法调用;
6.在方法体void sing()被调用完成后,就会立刻马上从栈内弹出(出栈 ),最后,在main()函数完成后,main()函数也会出栈
本地方法栈
带native关键字的说明Java作用范围无法达到,需要调用底层C语言,那么就会调用本地方法接口JNI,从而调用本地方法库中的方法;本地方法接口的作用就是拓展Java代码,融合不同的编程语言。
public class Test0106 {
public static void main(String[] args) {
new Thread(()->{},"mystread").start();
new Thread().start();
}
private native void start0();
}
start方法源码
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
OOM解决方案
默认情况下:分配的总内存是电脑内存的四分之一,初始化内存为电脑内存的六十四分之一
public static void main(String[] args){
//jvm试图使用最大的内存
long max = Runtime.getRuntime().maxMemory();
//返回JVM初始化总内存
long total = Runtime.getRuntime().totalMemory();
System.out.println(max/(double)1024/2024+"MB");
System.out.println(total/(double)1024/2024+"MB");
}
1371.8260869565217MB
93.0909090909091MB
- 如何改变分配的内存大小
初始堆大小1024MB 最大堆大小1024MB 打印GC信息
-Xms1024m Xmx1024m -XX:+PrintGCDetails
496.5691699604743MB
496.5691699604743MB
Heap
PSYoungGen total 305664K, used 26214K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 262144K, 10% used [0x00000000eab00000,0x00000000ec499bc0,0x00000000fab00000)
from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
Metaspace used 3552K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 381K, capacity 388K, committed 512K, reserved 1048576K
定位具体导致内存溢出代码位置可使用JPofiler
- 程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置
,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError 情况的区域。
- Java 虚拟机栈
与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧
(Stack Frame ①)用于存储局部变量表、操作栈、动态链接、方法出口
等信息。
每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。经常有人把Java 内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java 内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。而所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中的局部变量表部分。
局部变量表
存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。
其中64 位长度的long 和double 类型的数据会占用2 个局部变量空间(Slot),其余的数据类型只占用1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。在Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常
;如果虚拟机栈可以动态扩展(当前大部分的Java 虚拟机都可动态扩展,只不过Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError 异常
。
- Java 堆
对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的
一块。Java 堆是被所有线程共享的一块内存区域
,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例
,几乎所有的对象实例都在这里分配内存。这一点在Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配①,但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换②优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。Java 堆是垃圾收集器管理的主要区域
,因此很多时候也被称做“GC 堆”(GarbageCollected Heap,幸好国内没翻译成“垃圾堆”)。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代;再细致一点的有Eden 空间、From Survivor 空间、To Survivor 空间等。如果从内存分配的角度看,线程共享的Java 堆中可能划分出多个线程私有的分配缓冲区(Thread LocalAllocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
根据Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。
- 方法区
方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域
,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而
已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory 来实现方法区的规划了。
Java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内
存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出现过的若干个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。
根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。
GC针对什么对象
-Xms8m -Xmx8m -XX:+PrintGCDetails
查看GC过程
[GC (Allocation Failure) [PSYoungGen: 1536K->492K(2048K)] 1536K->836K(7680K), 0.0016970 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2010K->492K(2048K)] 2354K->1360K(7680K), 0.0116310 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 2028K->504K(2048K)] 2896K->1862K(7680K), 0.0052206 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2040K->504K(2048K)] 3398K->2302K(7680K), 0.0011732 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2040K->504K(2048K)] 3838K->2874K(7680K), 0.0031236 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1729K->504K(2048K)] 4099K->3920K(7680K), 0.0141194 secs] [Times: user=0.05 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 1500K->512K(2048K)] 4916K->5322K(7680K), 0.0026002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 512K->0K(2048K)] [ParOldGen: 4810K->4233K(5632K)] 5322K->4233K(7680K), [Metaspace: 3536K->3536K(1056768K)], 0.0684661 secs] [Times: user=0.23 sys=0.00, real=0.07 secs]
[Full GC (Ergonomics) [PSYoungGen: 1536K->0K(2048K)] [ParOldGen: 5171K->5580K(5632K)] 6707K->5580K(7680K), [Metaspace: 3540K->3540K(1056768K)], 0.0521595 secs] [Times: user=0.16 sys=0.00, real=0.05 secs]
[Full GC (Ergonomics) [PSYoungGen: 1196K->662K(2048K)] [ParOldGen: 5580K->5594K(5632K)] 6777K->6257K(7680K), [Metaspace: 3546K->3546K(1056768K)], 0.1232137 secs] [Times: user=0.37 sys=0.00, real=0.12 secs]
[Full GC (Allocation Failure) [PSYoungGen: 662K->662K(2048K)] [ParOldGen: 5594K->5565K(5632K)] 6257K->6228K(7680K), [Metaspace: 3546K->3546K(1056768K)], 0.0961531 secs] [Times: user=0.19 sys=0.00, real=0.10 secs]
Heap
PSYoungGen total 2048K, used 914K [0x00000000ffd80000, 0x0000000100000000, 0x0000000100000000)
eden space 1536K, 59% used [0x00000000ffd80000,0x00000000ffe64b30,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 5632K, used 5565K [0x00000000ff800000, 0x00000000ffd80000, 0x00000000ffd80000)
object space 5632K, 98% used [0x00000000ff800000,0x00000000ffd6f5c8,0x00000000ffd80000)
Metaspace used 3709K, capacity 4540K, committed 4864K, reserved 1056768K
class space used 396K, capacity 428K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.add(ArrayList.java:462)
at javapra.Test0106.main(Test0106.java:23)
当一个对象通过一系列根对象(比如:静态属性引用的常量)都不可达时就会被回收。简而言之,当一个对象的所有引用都为null。循环依赖不算做引用,如果对象A有一个指向对象B的引用,对象B也有一个指向对象A的引用,除此之外,它们没有其他引用,那么对象A和对象B都需要被回收
Java语法
四个访问修饰符的权限
访问修饰符\访问权限 | 同一个类 | 同一个包 | 子类 | 不同包 |
---|---|---|---|---|
public | ✔ | ✔ | ✔ | ✔ |
protected | ✔ | ✔ | ✔ | |
default | ✔ | ✔ | ||
private | ✔ |
char 型变量中能不能存储一个中文汉字,为什么
可以存储,char存储大小2字节16位
抽象的(abstract)方法是否可同时是静态的(static), 是否可同时是本地方法 (native),是否可同时被 synchronized
都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由 本地代码(如 C 代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized 和方法的实现细节有关, 抽象方法不涉及实现细节,因此也是相互矛盾的
通过下面这代码理解抽象类和接口:门本身有开和关的动作,但不一定都具备有响铃的功能;抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。
interface Alram {
void alarm();
}
abstract class Door {
void open();
void close();
}
class AlarmDoor extends Door implements Alarm {
void oepn() {
}
void close() {
}
void alarm() {
}
}
1)抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;
2)抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
3)接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
4)一个类只能继承一个抽象类,而一个类却可以实现多个接口。
和equals 的区别
equals 和 最大的区别是一个是方法一个是运算符。 = =如果比较的对象是基本数据类型,则比较的是数值是否相等;如果比较的是引用数据类型,则比较的是对象的 地址值是否相等。 equals():用来比较方法两个对象的内容是否相等。 注意:equals 方法不能用于基本数据类型的变量,如果没有对 equals 方法进行重写,则比较的是引用类型的 变量所指向的对象的地址。
String s = “Hello”;s = s + ” world!”;这两行代码执行后,原始的 String 对象中的内容到底变了没有?
那么 s 所指向的那个对象是否发生了 改变呢? 答案是没有。这时,s 不指向原来那个对象了,而指向了另一个 String 对象,内容为”Hello world!”,原来 那个对象还
存在于内存之中,只是 s 这个引用变量不再指向它了。
我们还可以知道,如果要使用内 容相同的字符串,不必每次都 new 一个 String。例如我们要在构造器中对一个名叫 s 的 String 引用变量进行初 始化, 把它设置为初始值
private static String s;
public static void main(String[] args) {
s="aaa";
System.out.println(s);
}
不可变类有一些优点,比如因为 它的对象是只读的,所以多线程并发访问也不会有任何问题。当然也有一些缺点,比如每个不同的状态都要一个对象 来代表,可能会造成性能上的问题。所以 Java 标准类库还提供了一个可变版本,即 StringBuffer
try catch finally 执行过程
public static int getNum(){
try {
int a=1/0;
System.out.println("aaaa");
return 1;
}catch (Exception e){
System.out.println("vvvv");
return 2;
}finally {
System.out.println(111);;
}
}
---------
vvvv
111
2
switch 是否能作用在 byte 上,
Java5 以前 switch(expr)中,expr 只能是 byte、short、char、int。从 Java 5 开始,Java 中引入了枚举类型, expr 也可以是 enum 类型。 从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的
String 、StringBuilder 、StringBuffer 的区别
String:不可变字符串;
StringBuffer:可变字符串、效率低、线程安全;
StringBuilder:可变字符序列、效率高、线程不安全;
初始化上的区别,String可以空赋值,后者不行,报错
short s1 = 1; s1 = s1 + 1; 有错吗?short s1 = 1; s1 += 1 有错吗;
对于 short s1 = 1; s1 = s1 + 1;
由于 1 是 int 类型,因此 s1+1 运算结果也 是 int 型, 需要强制转换类型才能赋值给 short 型。
而 short s1 = 1; s1 += 1;可以正确编译,
因为 s1+= 1;相当于 s1 = (short)(s1 + 1);其中有隐含的强制类型转换。
Integer 类型的数值比较输出的结果为
Integer s=111,s1=111,s2=150,s3=150;
System.out.println(s==s1);
System.out.println(s2==s3);
true
false
装箱的本质是什么呢?当我们给一个 Integer 对象赋一个 int 值的时候,会调用 Integer 类的静态方法 valueOf,查看源码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
为什么要有封装类
是为了在各种类型间转化
简单的说,如果整型字面量的值在-128 到 127 之间,那么不会 new 新的 Integer 对象,而是直接引用常量 池中的 Integer 对象
集合相关的知识
List 的三个子类的特点,List 和 Map、Set 的区别
ArrayList
底层结构是数组,底层查询快,增删慢。 LinkedList 底层结构是链表型的,增删快,查询慢。 voctor 底层结构是数组 线程安全的,增删慢,查询慢
List 和 Set 是存储单列数据的集合,Map 是存储键和值这样的双列数据的集合;List 中存储的数据是有顺序, 并且允许重复;Map 中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的,Set 中存储的数据是无 序的,且不允许有重复,但元素在集合中的位置由元素的 hashcode 决定,位置是固定的(Set集合根据hashcode来进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说set中的元素还是无序的);
List 接口
有三个实现类(LinkedList:基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还 存储下一个元素的地址。链表增删快,查找慢;ArrayList:基于数组实现,非线程安全的,效率高,便于索引,但不便 于插入删除;Vector:基于数组实现,线程安全的,效率低)。
Map 接口
有三个实现类(HashMap:基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null 键;HashTable:线程安全,低效,不支持 null 值和 null 键;两者的的 key 值均不能重复,若添加 key 相同的键值对,后面的 value 会自动覆盖前面的 value,但不会报错LinkedHashMap:是 HashMap 的一个子类,保存了 记录的插入顺序;SortMap 接口:TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序)。
Set 接口
有两个实现类(HashSet:底层是由 HashMap 实现,不允许集合中有重复的值,使用该方式时需要重写 equals()和 hashCode()方法;LinkedHashSet:继承与 HashSet,同时又基于 LinkedHashMap 来进行实现,底层使 用的是 LinkedHashMp)。
数组和链表分别比较适合用于什么场景,为什么?
- 概念
当内存空间中有足够大的连续空间时,可以把数据连续的存放在内存中;
另外一种,不影响别人的数据存储方式是把数据集中的数据分开离散地存储到这些不连续空间中,这时为了能把数据集中的所有数据联系起来,需要在前一块数据的存储空间中记录下一块数据的地址,这样只要知道第一 块内存空间的地址就能环环相扣地把数据集整体联系在一起了。
因此,数据的物理存储结构就有连续存储和离 散存储两种,它们对应了我们通常所说的数组和链表
- 数组的特点
数组是将元素在内存中连续存储的;它的优点:因为数据是连续存储的,内存地址连续,所以在查找数据的时候效率 比较高;它的缺点:在存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好它的空间的大小。在运 行的时候空间的大小是无法随着你的需要进行增加和减少而改变的,当数据两比较大的时候,有可能会出现越界的情况,数据 比较小的时候,又有可能会浪费掉内存空间
- 链表的特点
在改变数据个数时,增加、插入、删除数据效率比较低链表是动态申请内存空间, 不需要像数组需要提前申请好内存的大小,链表只需在用的时候申请就可以,根据需 要来动态申请或者删除内存空间,对于数据增加和删除以及插入比数组灵活。还有就是链表中数据在内存中可以在任 意的位置,通过应用来关联数据(就是通过存在元素的指针来联系)
- 用面向对象的方法求出数组中重复 value 的个数
public static void main(String[] args){
int[] arr = {1,4,1,4,2,5,4,5,8,7,8,77,88,5,4,9,6,2,4,1,5};
Map<Integer, Integer> map = new HashMap<>();
for (int value : arr) {
////判断是否包含指定的键值
if (map.containsKey(value)) {
map.put(value, map.get(value) + 1);
} else {
map.put(value, 1);
}
}
for (Map.Entry<Integer, Integer> entry : map.entrySet()){
System.out.println(entry.getKey() + "出现:" + entry.getValue() + "次");
}
}
list相关
ArrayList 和 Linkedlist 区别
ArrayList 和 Vector 使用了数组的实现,可以认为 ArrayList 或者 Vector 封装了对内部数组的操作,比如 向数组中添加,删除,插入新的元素或者数据的扩展和重定向。 LinkedList 使用了循环双向链表数据结构。与基于数组的 ArrayList 相比,这是两种截然不同的实现技术,这也 决定了它们将适用于完全不同的工作场景。 LinkedList 链表由一系列表项连接而成。一个表项总是包含 3 个部分:元素内容,前驱表和后驱表,无论 LikedList 是否为空,链表内部都有一个 header 表项,它既表示链表的开始,也表示链表的结尾。表项 header 的后驱表项便是 链表中第一个元素,表项 header 的前驱表项便是链表中最后一个元素。
1. ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。
2. 如果集合数据是对于集合随机访问 get 和 set,ArrayList 绝对优于 LinkedList,因为 LinkedList 要移动指针。
3. 如果集合数据是对于集合新增和删除操作 add 和 remove,LinedList 比较占优势,因为 ArrayList 要移动数 据
List a=new ArrayList()和 ArrayList a =new ArrayList()的区别
//这句创建了一个 ArrayList 的对象后把上溯到了 List。
//此时它是一个 List 对 象了,有些ArrayList 有但是 List 没有的属性和方法,它就不能再用了
List list = new ArrayList();
//创建一对象则保留了ArrayList 的所有属性。
//所以需要用到 ArrayList 独有的方法的时候不能用前者。
ArrayList arl = new ArrayList();
//ArrayList 独有的方法
arl.trimToSize();
多线程
线程和进程的区别
进程
:具有一定独立功能的程序关于某个数据集合上的一次运行活动,是操作系统进行资源分配和调度的一个独 立单位。
线程
:是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的可以独立运行的基本单位。 特点:线程的划分尺度小于进程,这使多线程程序拥有高并发性,进程在运行时各自内存单元相互独立,线程之间内存 共享,这使多线程编程可以拥有更好的性能和用户体验 注意:多线程编程对于其它程序是不友好的,占据大量 cpu 资源。
请说出同步线程及线程调度相关的方法
wait()
: 使 一 个 线 程 处 于 等 待 ( 阻 塞 ) 状 态 , 并 且 释 放 所 持 有 的 对 象 的 锁 ; sleep()
:使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
notify()
:唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
notityAll()
:唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁 的线程才能进入就绪状态;
注意:java 5 通过 Lock 接口提供了显示的锁机制,Lock 接口中定义了加锁(lock()方法)和解锁(unLock() 方法),增强了多线程编程的灵活性及对线程的协调
启动一个线程是调用 run()方法还是 start()方法
启动一个线程是调用 start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由 JVM 调度并 执行,这并不意味着线程就会立即运行。 run()方法是线程启动后要进行回调(callback)的方法。
线程的转换状态
多线程实现方式
Java多线程实现方式主要有四种:继承Thread类、实现Runnable接口、实现Callable接口通过FutureTask包装器来创建Thread线程、使用ExecutorService、Callable、Future实现有返回结果的多线程。
//继承Thread类
public static void main(String[] args){
Test te=new Test();
Test te1=new Test();
te.start();
te1.start();
}
@Override
public void run() {
System.out.println("我是run方法"+new Thread().getName());
}
我是run方法Thread-2
我是run方法Thread-3
//实现Runnable接口
public class Test implements Runnable {
public static void main(String[] args){
Test te=new Test();
Thread thread=new Thread(te);
thread.start();
}
@Override
public void run() {
System.out.println("我来了"+new Thread().getName());
}
}
线程互斥与同步
在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用同一台打 印机,会使打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性。
当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系。
1. 间接相互制约。
一个系统中的多个线程必然要共享某种系统资源,如共享 CPU,共享 I/O 设备,所谓间接 相互制约即源于这种资源共享,打印机就是最好的例子,线程 A 在使用打印机时,其它线程都要等待。
2. 直接相互制约。
这种制约主要是因为线程之间的合作,如有线程 A 将计算结果提供给线程 B 作进一步处理, 那么线程 B 在线程 A 将数据送达之前都将处于阻塞状态。 间接相互制约可以称为互斥,直接相互制约可以称为同步,对于互斥可以这样理解,线程 A 和线程 B 互斥访问 某个资源则它们之间就会产个顺序问题——要么线程 A 等待线程 B 操作完毕,要么线程 B 等待线程操作完毕,这其 实就是线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步。
并发队列-阻塞队列
常用的并发队列有阻塞队列和非阻塞队列,前者使用锁实现,后者则使用 CAS 非阻塞算法实现
BlockingQueue 阻塞队列
BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。
一个线程往里边放,另外一个线程从里边取的一个 BlockingQueue。 一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限 的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中, 直 到负责消费的线程从队列中拿走一个对象。负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个 空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
需求:在多线程操作下,一个数组中最多只能存入 3 个元素。多放入不可以存入数组,或等待某线程对数组中某个元素取走才能放入,要求使用 java 的多线程来实现
public class Test {
public static void main(String[] args) {
final BlockingQueue queue = new ArrayBlockingQueue(3);
for(int i=0;i<2;i++){
//负责插入的线程
new Thread(){
public void run(){
while(true){
try {
Thread.sleep((long)(Math.random()*1000) );
System.out.println(Thread.currentThread().getName() + "准备放数据!");
queue.put(1);
System.out.println(Thread.currentThread().getName() + "已经放了数据," + "队列目前有" + queue.size() + "个数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
} //负责移除的线程
new Thread(){
public void run(){
while(true){
try {
//将此处的睡眠时间分别改为 100 和1000,观察运行结果
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "准备取数据!");
System.err.println(queue.take());
System.out.println(Thread.currentThread().getName() + "已经取走数据," + "队列目前有" + queue.size() + "个数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
hread-1准备放数据!
Thread-1已经放了数据,队列目前有1个数据
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有2个数据
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有1个数据
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有2个数据
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有3个数据
公平锁
:就是在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线 程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO 的规则从队列中取到自己。 非公平锁
:比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式
线程池
- 是什么
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可, 节省了开辟子线程的时间,提高的代码执行效率
1. 线程池的作用
线程池作用就是限制系统中执行线程的数量。 根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造 成系统拥挤效率不高。用线程池控制线程数量,其他线程 排队等候。一个任务执行完毕,再从队列的中取最前面 的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程 池 中有等待的工作线程,就可以开始运行了;否则进入等待队列。
- 选用的意义
减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务 器累趴下(每个线程需要大约 1MB 内存,线程开的越多,消耗的内存也就越大,最后死机)
- 线程池创建线程大致可以分为下面三种:
//创建固定大小的线程池
ExecutorService fPool = Executors.newFixedThreadPool(3);
//创建缓存大小的线程池
ExecutorService cPool = Executors.newCachedThreadPool();
//创建单一的线程池
ExecutorService sPool = Executors.newSingleThreadExecutor();
网络通信
TCP和UDP
TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。每一条TCP连接只能是点到点的。因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用,实现DOS、DDOS、CC等攻击。
UDP支持多对多,一对多,UDP是一个无状态的传输协议,所以它在传递数据时非常快。没有TCP的这些机制,DP较TCP被攻击者利用的漏洞就要少一些。但UDP也是无法避免攻击的,比如:UDP Flood攻击…… UDP的缺点: 不可靠,不稳定 因为UDP没有TCP那些可靠的机制
HTTP 与 HTTPS 的区别
HTTP(HyperText Transfer Protocol:超文本传输协议)默认80端口
HTTPS(Hypertext Transfer Protocol Secure:超文本传输安全协议)默认443端口