1.基本概念
-
java代码执行
- 代码编译class:javac
- 源文件通过编译器产生字节码文件,
- 字节码文件通过jvm的解释器编译成机器上的机器码
- 装载class:ClassLoader
- 执行class:
- 解释执行
- 编译执行
- client compiler
- server compiler
- 代码编译class:javac
-
内存管理
- 内存空间
- 方法区
- 堆
- 方法栈
- 本地方法栈
- pc寄存器
- 内存分配
- 堆上分配
- TLAB分配
- 栈上分配
- 内存回收
- 算法
- copy
- mark-sweep
- mark-compact
- sun jdk
- 分代回收
- 新生代可用的GC
- 串行copying
- 并行回收copying
- 并行copying
- Minor GC触发机制及日志格式
- 旧生代可用的GC
- 串行Mark -Sweep-compact
- 并行compacting
- 并发mark-sweep
- Full GC触发机制以及日志格式
- 新生代可用的GC
- GC参数
- G1
- 分代回收
- 算法
- 内存状况分析
- jconsole
- visuaivm
- jstat
- jmap
- mat
- 内存空间
-
线程资源同步和交互机制
- 线程资源同步
- 线程资源执行机制
- 线程资源同步机制
- Synchronized实现机制
- lock/unlock的实现机制
- 线程交互机制
- Object.wait/notify/notifyAll-Double check pattem
- 并发包提供的交互机制
- semaphore
- countdownLatch
- 线程状态及分析方法
- jstack
- tda
2.jvm内存
- 线程资源同步
-
线程私有
- 程序计数器
- 指向虚拟机字节码指令的位置
- 唯一一个无oom的区域
- 虚拟机栈
- 虚拟机栈和线程的生命周期相同
- 一个线程中,每调用一个方法创建一个栈帧
- 栈帧
- 本地变量表
- 操作数栈
- 对运行时常量池的引用
- 异常
- 线程请求的栈深度大于jvm所允许的深度StackOverflowError
- 若JVM允许动态扩展,若无法申请到足够内存OutOfMemoryError
- 本地方法栈
- 异常
- 线程请求的栈深度大于jvm所允许的深度StackOverflowError
- 若jvm允许动态扩展,若无法申请到足够内存OutOfmemeryError
- 异常
- 程序计数器
-
线程共享
- 方法区(永久代)
- 运行时常量池
- 类实例区(java堆-运行时数据区)
- 创建的对象和数组都保存在java堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。
- 新生代:eden;from survior;to survivor
- 老生代
- 异常
- 方法区(永久代)
-
直接内存
- 不受jvm gc管理
3.jvm运行时内存
- 新生代:占据堆的1/3空间,会频繁触发MinorGC进行垃圾回收
- Eden区:java对象出生地,当Eden区不够会触发minorGC,对新生代进行一次垃圾回收
- ServivorFrom:上一次GC的幸存者,作为这一次GC的被扫描者
- ServivorTo:保留了一次MinorGC过程中的幸存者
- MinorGC的过程(复制->清空->互换)
- eden、servivorFrom复制到ServivorTo,年龄+1
- 清空eden,servivorFrom
- servivorTo和servivorFrom互换
- 老年代
- 存放应用程序中生命周期长的内存对象 。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
- MajorGC(标记清除算法):扫描一次所有老年代,标记存活对象,回收没有标记的对象,会产生内存碎片,为减少内存损耗,一般需要进行合成或者标记出来方便下次直接分配,当老年代也装不下了,抛出oom异常。
- 永久代:内存的永久保存区域,存放Class和Meta(元数据)的信息。Class被加载时被放到永久区域,和存放实例区域不同,GC不会在主程序运行期对永久区域进行清理,导致永久代区域会随着加载class的增多而胀满,最终抛出oom异常
- java8与元数据:元空间不在虚拟机中,而是使用本地内存,元空间大小受本地内存的限制,类的元数据放入native memory 字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由maxpermSize控制,而由实际可用空间来控制。
4.垃圾回收算法
- 总结
- gc
- 哪些内存需要回收
- 什么时候回收
- 怎么回收
- 哪些对象死亡
- 引用计数法:循环引用的问题
- 根搜索算法
- GC roots:需要经过两次标记,两次标记后仍然是可回收对象,则将面临回收
- Gc roots(可达性分析)
- VM栈中的引用
- 方法区中的静态引用
- JNI中的引用
- 垃圾收集算法
- 标记清除:
- 标记需回收对象,回收标记的
- 效率低;内存碎片多
- 复制:可用内存被压缩到了原本的一半
- 标记整理:标记后存活对象移向内存一端,清除端边界外的对象
- 分代收集:根据对象存活的不同生命周期将内存分为不同的域。
- 新生代与复制算法:eden,from space,to space 8:1:1
- 老年代和标记复制算法:每次只回收少量对象
- 标记清除:
- 垃圾收集器
- serial;parNew;parallel scavenge;serial old;parallel old;cms
- 参数
- Xms;Xmx;Xmn;-XX。。。
- gc
5.java四种引用类型
- 强引用:把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的
- 软引用:需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中
- 弱引用:需要用weakReference类实现,比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管jvm的内存空间是否足够,总会回收该对象占用的内存
- 虚引用:需要用phantomRerence类来实现,不能单独使用,必须和引用队列联合使用,主要作用是跟踪对象被垃圾回收的状态。
6.垃圾收集器
- Serial垃圾收集器(单线程,复制算法)
- ParNew垃圾回收器:多线程复制算法,默认开启的cpu数目相同的线程数
- Parallel scavenge收集器:多线程复制算法,高效。适用于在后台运算二不需要太多交互的任务。
- Serial old收集器:单线程标记整理算法
- 与新生代的parallel scavenge收集器搭配使用
- 作为老年代中使用cms收集器的后备垃圾收集方案
- parallel old收集器:多线程标记整理算法
- cms收集器:多线程标记清除算法,主要目标是获取最短垃圾回收停顿时间,多线程清除算法。
- 初始标记:标记和gc roots 直接关联对象,速度快。
- 并发标记:进行gc roots 跟踪过程,和用户线程一起工作
- 重新标记:修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录
- 并发清除:清除gc roots不可达对象,和用户线程一起工作。
- G1收集器
- 基于标记整理算法,不产生内存碎片
- 精确控制停顿时间,不牺牲吞吐量前提下,实现低停顿垃圾回收。
- 内存划分为大小固定的几个独立区域,并跟踪这些区域的垃圾收集进度
- 每次根据所允许的收集时间,优先回收垃圾最多的区域,区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获得最高的垃圾收集效率
7.Java IO/NIO
- 阻塞IO模型:读写数据过程中会发生阻塞现象。
- 用户线程发出 IO 请求之后,内核会去查看数据是否就绪,
- 没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出 CPU。
- 当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除 block 状态。
- 典型的阻塞 IO 模型的例子为: data = socket.read();如果数据没有就绪,就会一直阻塞在 read 方法
- 用户线程发出 IO 请求之后,内核会去查看数据是否就绪,
- 非阻塞IO模型:
- 当用户线程发起一个 read 操作,不需要等待,马上就得到了一个结果。
- 如果结果是一个error 时,数据还没有准备好,可以再次发送 read 操作。
- 内核中的数据准备好了,并又再次收到了用户线程的请求,它马上就将数据拷贝到了用户线程,然后返回。
- 非阻塞 IO 模型中,用户线程需要不断地询问内核数据是否就绪,非阻塞 IO不会交出 CPU,一直占用 CPU。
- 问题: while 循环中需要不断地去询问内核数据是否就绪,这样会导致 CPU 占用率非常高,因此一般情况下很少使用 while 循环这种方式来读取数据。
- 当用户线程发起一个 read 操作,不需要等待,马上就得到了一个结果。
- 多路复用IO模型:
- 有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。
- 通过 selector.select()去查询每个通道是否有到达事件,没有事件,则一直阻塞在那里,这种方式会导致用户线程的阻塞。只有当socket 真正有读写事件发生才会占用资源来进行实际的读写操作。
- 多路复用 IO 比较适合连接数比较多的情况。
- 注意:多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用 IO 模型来说, 一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询
- 信号驱动IO模型:
- 当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函
数,用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作
- 当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函
- 异步IO模型:
- 用户线程发起 read 操作之后,立刻就可以开始去做其它的事。从内核的角度,当它受到一个 asynchronous read 之后,会立刻返回,说明 read 请求已经成功发起了,因此不会对用户线程产生任何 block。内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它 read 操作完成了。
- 用户线程只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了。
- 异步 IO 模型中, IO 操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用 IO 函数进行具体的读写。
- 在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用 IO 函数进行实际的读写操作;
- 在异步 IO 模型中,收到信号表示 IO 操作已经完成,不需要再在用户线程中调用 IO 函数进行实际的读写操作。
- java NIO
- 三大核心部分: Channel(通道)、Buffer(缓冲区)、Selector
- 数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
- NIO 和传统 IO 之间区别是, IO 是面向流的, NIO 是面向缓冲区的
- 缓冲区
- Java IO 面向流:每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据, 需要先将它缓存到一个缓冲区。
- NIO :数据读取到一个稍后处理的缓冲区,需要时可在缓冲区中前后移动。增加了处理过程中的灵活性。但是,需检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据
- 非阻塞
- IO 的各种流是阻塞的。当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
- NIO 的非阻塞模式,使一个线程从某通道发送请求读取数据,但仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞 IO 的空闲时间用于在其它通道上
执行 IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
- channel:Channel 和 IO 中的 Stream(流)是差不多一个等级的。
- Stream 是单向的: InputStream, OutputStream,
- Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。主要实现有:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel分别可以对应文件 IO、 UDP 和 TCP(Server 和 Client)。
- Buffer:缓冲区,一个连续数组,channel提供从文件、网络读取数据的渠道,但读取或写入的数据必须经由Buffer。
- 客户端发送数据先将数据存到Buffer,然后将Buffer中的内容写入通道
- 服务端接收数据通过Channel将数据读入到Buffer中,然后再从Buffer中取出数据来处理。
- Selector:能够检测多个注册的通道上是否有事件发生,如果有时间发生,便获取事件然后针对每个事件进行相应的响应处理。一个单线程管理多个通道,管理多个连接。
8.JVM类加载机制
- 类加载机制:加载,验证,准备,解析,初始化
- 加载:会在内存中生成一个代表这个类的 java.lang.Class 对象, 作为方法区这个类的各种数据的入口。可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
- 验证:主要目的是为了确保class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身安全
- 准备:正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间
- 解析:虚拟机将常量池中的符号引用替换为直接引用的过程
- 符号引用:引用的目标并不一定要已经加载到内存中。 各种虚拟机实现的内存布局可以各不相同,
- 直接引用:指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在
- 初始化:类加载的最后阶段,真正执行类中定义的java程序代码
- 执行类构造器方法的过程。
9.类加载器
- 启动类加载器(Bootstrap ClassLoader): 负责加载 JAVA_HOME\lib 目录中的, 或通过-Xbootclasspath 参数指定路径中的, 且被虚拟机认可(按文件名识别, 如 rt.jar) 的类。
- 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
- 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。JVM 通过双亲委派模型进行类的加载, 也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。
10.双亲委派
- 类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,
- 只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class), 子类加载器才会尝试自己去加载。
- 好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象