String StringBuffer 和 StringBuilder

String、StringBuffer 和 StringBuilder

可变性

String:

简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以 String 对象是不可变的。

StringBuilder 与 StringBuffer:

而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,
在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

线程安全性

String 中的对象是不可变的,也就可以理解为常量,线程安全。

AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,
如 expandCapacity、append、insert、indexOf 等公共方法。

StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。

StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。 

对于三者使用的总结:

操作少量的数据: 适用String

单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder

多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer

装箱与拆箱

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

JVM 3.0

Java虚拟机的内部体系结构

在 Java虚拟机规范中,一个虚拟机实例的行为是分别按照子系统、内存区、数据类型和指令来描述的,
这些组成部分一起展示了抽象的虚拟机内部体系结构。

image

运行时数据区

Java虚拟机在执行Java程序的时候会把它管理的内存划分为若干个不同的数据区域,这些区域有各自的用途以及创建
和销毁的时机,有的区域随着虚拟机进程的启动而存在(线程共享),有的区域则随着用户线程的启动和结束而建立
和销毁(线程私有)。Java虚拟机所管理的内存包括以下几个运行时数据区域:

程序计数器

对于一个运行中的Java程序而言,每一个线程都有它的程序计数器,也叫PC寄存器,可以看做当前线程所执行的字节
码的行号指示器。在虚拟机的概念模型里,字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,
分支,循环,跳转,异常处理,线程恢复等都需要依赖这个计数器。

程序计数器既能持有一个本地指针,也能持有一个returnAddress。当线程执行某个Java方法时,程序计数器的值总是
下一条被执行指令的地址。这里的地址可以是一个本地指针,也可以是方法字节码中相对该方法起始指令的偏移量。
如果该线程正在执行一个本地方法,那么此时程序计数器的值是“undefined”。
程序计数器属于线程私有的内存,也就是说,每当创建一个线程,都将得到该线程自己的一个程序计数器。
Java虚拟机的多线程是通过线程的轮换并且分配处理器的执行时间来实现的,在一个确定的时刻,一个处理器都只会执行一条
线程中的指令。

为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。

Java虚拟机栈(Java栈)

Java虚拟机栈也是线程私有的,它的生命周期和线程相同。Java虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行时会创建一个栈帧,用于存放局部变量表,操作数栈,动态链接,方法出口等信息。一个Java方法从调用到执行结束的过程,相当于一个栈帧在Java虚拟机栈中从入栈到出栈的过程。

局部变量表

局部变量表用于存放编译时期可知的各种基本数据类型,对象的引用(不等同于对象,可能是指向对象的起始地址的指针,也可
能是指向一个代表对象的句柄)以及returnAddress类型(指向了一条字节码地址)。局部变量在方法执行时被创建,
在方法执行结束时销毁。字节码指令通过从0开始的索引使用其中的数据。类型为int, float, reference和returnAddress的值
在数组中占据一项,而类型为byte, short和char的值在存入数组前都被转换为int值,也占据一项。但类型为long和double的值
在数组中却占据连续的两项。

操作数栈

和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。它通过标准的栈操作访问–压栈和出栈。由于程序计数器
无法被程序指令直接访问,Java虚拟机的指令是从操作数栈中取得操作数,所以它的运行方式是基于栈而不是基于寄存器。
虚拟机把操作数栈作为它的工作区,因为大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。

帧数据区

除了局部变量区和操作数栈,Java栈帧还需要帧数据区来支持常量池解析、正常方法返回以及异常派发机制。
每当虚拟机要执行某个需要用到常量池数据的指令时,它会通过帧数据区中指向常量池的指针来访问它。除了常量池的解析外,
帧数据区还要帮助虚拟机处理Java方法的正常结束或异常中止。如果通过return正常结束,虚拟机必须恢复发起调用的方法的
栈帧,包括设置程序计数器指向发起调用方法的下一个指令;如果方法有返回值,虚拟机需要将它压入到发起调用的方法的操作数栈。
为了处理Java方法执行期间的异常退出情况,帧数据区还保存一个对此方法异常表的引用。

本地方法栈

任何本地方法接口都会使用某种本地方法栈,本地方法栈与Java虚拟机栈发挥的作用类似。当线程调用Java方法时,
虚拟机会创建一个新的栈帧并压入Java栈。当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新
的栈,虚拟机只是简单地动态连接并直接调用指定的本地方法。

堆(Heap)

Java程序在运行时创建的所有类实例或数组(数组在Java虚拟机中是一个真正的对象)都放在同一个堆中,堆是虚拟机管理的
内存最大的一块,被所有线程所共享,在虚拟机启动的时候创建。堆内存的唯一目的就是存放对象实例,几乎所有的对象实例
都在堆中分配内存。(大对象)

方法区(Method Area)

方法区同Java堆一样,是各个线程共享的内存区域,用于存储已经被虚拟机加载的类信息,常量,静态变量以及
及时编译器(JIT)编译后的代码等信息。

当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件并将它传输到虚拟机中,接着虚拟机
提取其中的类型信息,并将这些信息存储到方法区。方法区也可以被垃圾回收器收集,因为虚拟机允许通过用户定义的类装载器
来动态扩展Java程序。

方法区中存放了以下信息:

• 这个类型的全限定名(如全限定名java.lang.Object)
• 这个类型的直接超类的全限定名
• 这个类型是类类型还是接口类型
• 这个类型的访问修饰符(public, abstract, final的某个子集)
• 任何直接超接口的全限定名的有序列表
• 该类型的常量池,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项是常量池,用于存放编译时生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放
• 字段信息(字段名、类型、修饰符)
• 方法信息(方法名、返回类型、参数数量和类型、修饰符)
• 除了常量以外的所有类(静态)变量
• 指向ClassLoader类的引用(每个类型被装载时,虚拟机必须跟踪它是由启动类装载器还是由用户自定义类装载器装载的)
• 指向Class类的引用(对于每一个被装载的类型,虚拟机相应地为它创建一个java.lang.Class类的实例存于堆中。比如你有一个到java.lang.Integer类的对象的引用,那么只需要调用Integer对象引用的getClass()方法,就可以得到表示java.lang.Integer类的Class对象)

类加载子系统

虚拟机把Java描述类的信息加载到内存,并对数据进行校验,转换解析和初始化,形成可以被Java虚拟机直接使用的Java类型,这就是Java虚拟机的类加载机制。类从加载到虚拟机开始,到卸载出内存为止,它的整个生命周期包括:

加载,验证,准备,解析,初始化,使用和卸载7个阶段。

image

加载

加载是类加载的一个阶段,在该阶段,Java虚拟机主要完成以下三件事情:

• 根据此类的全限定名来确定该类的二进制字节流;
• 将这个字节流所代表的静态数据存储结构转换为方法区的运行时数据结构;
• 在堆内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

验证

验证是连接的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息是否符合该虚拟机的要求。

准备

准备阶段正式为类变量在方法区中分配内存并且赋初始值,初始值一般为该类变量类型的零值。

解析

解析阶段虚拟机将常量池内的符号引用替换为直接引用。
符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。
直接引用:是指能直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。

初始化

初始化是类加载过程的最后一个阶段,在前面的类加载过程中,除了在加载阶段用户可以自定义类加载器参与之外,其余动作完全由虚拟机主导和控制,到了初始化阶段,才真正开始执行类中定义的Java代码(字节码)。准备阶段为类变量分配内存并且赋初始值,赋值操作在初始化阶段执行。

JVM 4.0

Class文件

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,
中间没有添加任何分隔符号。Class文件采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只包含两种数据类型,
无符号数和表。无符号数属于基本的数据类型,以u1,u2,u4,u8分别代表1个字节,2个字节,4个字节和8个字节的无符号数,
可以用来描述数字、索引引用、数量值或者按照utf-8编码构成字符串值。表是由多个无符号数或者其他表作为数据项构成的
复合数据类型,所有表都习惯性地以“_info”结尾,用来描述有层次关系的复合结构数据。

Class文件的内容包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ClassFile {

u4 magic; //魔数:0xCAFEBABE,用来判断是否是Java class文件
u2 minor_version; //次版本号
u2 major_version; //主版本号
u2 constant_pool_count; //常量池大小
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags; //类和接口层次的访问标志(通过|运算得到)
u2 this_class; //类索引(指向常量池中的类常量)
u2 super_class; //父类索引(指向常量池中的类常量)
u2 interfaces_count; //接口索引计数器
u2 interfaces[interfaces_count]; //接口索引集合
u2 fields_count; //字段数量计数器
field_info fields[fields_count]; //字段表集合
u2 methods_count; //方法数量计数器
method_info methods[methods_count]; //方法表集合
u2 attributes_count; //属性个数
attribute_info attributes[attributes_count]; //属性表

}
  • 访问标志:类还是接口;是否定义为public类型,abstract类型;若为类,是否声明为final。
  • 字段:用来描述接口或类中的变量,但不包括在方法内部的变量。
  • 字面量:文本字符串,声明为final的常量值等。
  • 符号引用:类和接口的全限定名;字段的名称和描述符;方法的名称和描述符。
  • 类索引,父类索引,接口索引:用来确定类的继承关系。

JVM 2.0

#Java虚拟机

作用:

Java虚拟机的主要任务是装载class文件并且执行其中的字节码。
Java虚拟机包含一个类装载器(class loader),它可以从程序和API中装载class文件,
Java API中只有程序执行时需要的类才会被装载,字节码由执行引擎来执行。

当Java虚拟机由主机操作系统上的软件实现时,Java程序通过调用本地方法和主机进行交互。
Java方法由Java语言编写,编译成字节码,存储在class文件中。
本地方法由C/C++/汇编语言编写,编译成和处理器相关的机器代码,存储在动态链接库中,格式是各个平台专有。
所以本地方法是联系Java程序和底层主机操作系统的连接方式。(跨平台)

image

清除eclipse launch configuration

清除eclipse launch configuration

eclipse里面export runnable jar之前要先run一下才行, 但是之后launch configuration里面的记录一直存在, 非常难看, 清除的方法是:
清空workspace文件夹下的里的内容。

注意是删掉文件夹里面的内容, 而不是把文件夹删了

1
2
3
4
.metadata/.plugins/org.eclipse.debug.core/.launches

eg:
E:\JavaWorkSpace\.metadata\.plugins\org.eclipse.debug.core\.launches

IO复用技术

IO复用技术

多进程方式实现的服务器端,一次创建多个工作子进程来给客户端提供服务。其实这种方式是存在问题的。

可以打个比方:如果我们先前创建的几个进程承载不了目前快速发展的业务的话,是不是还得增加进程数?我们都知道系统创建进程是需要消耗大量资源的,所以这样就会导致系统资源不足的情况。

那么有没有一种方式可以让一个进程同时为多个客户端端提供服务?

接下来要讲的IO复用技术就是对于上述问题的最好解答。

对于IO复用,我们可以通过一个例子来很好的理解它。(例子来自于《TCP/IP网络编程》)

某教室有10名学生和1名老师,这些学生上课会不停的提问,所以一个老师处理不了这么多的问题。那么学校为每个学生都配一名老师,

也就是这个教室目前有10名老师。此后,只要有新的转校生,那么就会为这个学生专门分配一个老师。

如果把以上例子中的学生比作客户端,那么老师就是负责进行数据交换的服务端。则该例子可以比作是多进程的方式。

后来有一天,来了一位具有超能力的老师,这位老师回答问题非常迅速,并且可以应对所有的问题。而这位老师采用的方式是学生提问前必须先举手,确认举手学生后在回答问题。则现在的情况就是IO复用。

目前的常用的IO复用模型有三种:select,poll,epoll。

select模型

说的通俗一点就是各个客户端(连接的文件描述符)套接字,都被放到了一个集合中,调用select函数之后会一直监视这些文件描述符中有哪些可读,如果有可读的描述符那么我们的工作进程就去读取资源。

poll模型

poll 和 select 的实现非常类似,本质上的区别就是存放 fd 集合的数据结构不一样。select 在一个进程内可以维持最多 1024 个连接,poll 在此基础上做了加强,可以维持任意数量的连接。

但 select 和 poll 方式有一个很大的问题就是,我们不难看出来select是通过轮训的方式来查找是否可读或者可写,打个比方,如果同时有100万个连接都没有断开,而只有一个客户端发送了数据,所以这里它还是需要循环这么多次,造成资源浪费。所以后来出现了 epoll系统调用。

epoll模型

epoll是 select 和 poll 的增强版,epoll 同 poll 一样,文件描述符数量无限制。

epoll是基于内核的反射机制,在有活跃的 socket 时,系统会调用我们提前设置的回调函数。而 poll 和 select 都是遍历。

但是也并不是所有情况下 epoll 都比 select/poll 好,比如在如下场景:

在大多数客户端都很活跃的情况下,系统会把所有的回调函数都唤醒,所以会导致负载较高。既然要处理这么多的连接,那倒不如 select 遍历简单有效。

内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。

Java多线程相关问题

Java多线程相关问题

1.线程池的原理,为什么要创建线程池?创建线程池的方式?

2.线程的生命周期?什么时候会出现僵死线程?

3.说说线程安全问题?什么是线程安全?如何实现线程安全?

4.创建线程池有哪几个核心参数?如何合理的配置线程池的大小?

5.volatile、ThreadLocal的使用场景和原理;

6.ThreadLocal什么时候会出现OOM情况?为什么?

7.sychronized、volatile区别?synchronized锁粒度、模拟死锁场景、原子性和可见性

Java 字节码

在 Java 中,JVM可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。
Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。

Java程序从源代码到运行一般一下三步:

(1).java文件

JDK中javac编译

(2).class文件(JVM能理解的Java字节)

JVM

(3)机器可以执行的二进制文件

需要格外注意的是 .class->机器码 这一步:
在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。
而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而JIT 属于运行时编译。
当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。
而我们知道,机器码的运行效率肯定是高于 Java 解释器的。
这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。

总结:

Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码
,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。

堆和栈

堆和栈

栈是运行时的单位,而堆是存储式的单位。

栈解决程序运行的问题,解决程序如何运行的问题,如何处理数据。

堆解决数据存储问题,数据存哪,怎么存。

Java中,一个线程,就会有一个线程栈,不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。
而堆,是所有线程共有的。栈,是运行单元,里面存的信息都是与当前程序(线程)相关的。包括局部变量,程序运行状态,方法返回值等。堆只负责存储对象信息。

为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?

第一,从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。

第二,堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。

第三,栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。

第四,面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。

堆和栈中,栈是程序运行最根本的东西。程序运行可以没有堆,但是不能没有栈。而堆是为栈进行数据存储服务,说白了堆就是一块共享的内存。不过,正是因为堆和栈的分离的思想,才使得Java的垃圾回收成为可能。

栈溢出
Java中,栈的大小通过-Xss来设置,当栈中存储数据比较多时,需要适当调大这个值,否则会出现java.lang.StackOverflowError异常。常见的出现这个异常的是无法返回的递归,因为此时栈中保存的信息都是方法返回的记录点。

Java 字节码

JDK 和 JRE

JDK:

JDK是Java Development Kit,它是功能齐全的Java SDK。它拥有JRE所拥有的一切,还有编译器(javac)和工具(如javadoc和jdb)。
它能够创建和编译程序。

JRE:
Java运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java虚拟机(JVM),Java类库,java命令和其他的一些基础构件。
但是,它不能用于创建新程序。