艾瑞
艾瑞
新闻资讯
中心动态 学习技巧
java知识点整理(一)
2020-05-18

、 Java运行时数据区
1.程序计数器:当前线程所执行的字节码行号的指示器。
 java虚拟机多线程是通过线程间轮流切换来分配给处理器执行时间;在确定时间节点,一个处理器(一核)只会执行一个线程的指令;为保证 线程切换 回来后能恢复到原执行位置,各个线程间计数器互相不影响,独立存储
如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。
2.栈:Java方法执行的内存模型。
Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法。当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈
栈帧中包括局部变量表、操作数栈、指向当前方法所属类的运行时常量池的引用、方法返回地址和一些额外的附加信息。




 
局部变量表:就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及方法形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。
操作数栈:最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
指向运行时常量池的引用: 因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量池。
方法返回地址: 当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
3.本地方法:栈是执行Java方法的,而本地方法栈是用来执行native方法的
4.堆:主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,是垃圾收集器管理的主要区域
5.方法区:主要用于存储已被虚拟机加载的类信息(包括类的名称、方法信息、字段信息)、常量、静态变量、编译器编译后的代码等.
在方法区中存在一个叫运行时常量池的区域,它主要用于存放编译器生成的各种字面量和符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);这些内容将在类加载后存放到运行时常量池中,以便后续使用。它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。
 
一个指向A的class对象,一个指向加载自己的classLoader
栈是编译时分配空间,而堆是动态分配(运行时分配空间),所以栈的速度快cpu有专门的寄存器(esp,ebp)来操作栈,堆都是使用间接寻址的。栈快点
栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要 在运行时动态分配内存,存取速度较慢。

二、
Java类加载
1.类加载子系统的作用:根据给定的全限定名类名(如java.lang.Object)来装载class文件的内容到运行时数据区中的方法区
2.双亲委派机制:当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
3.类加载过程:加载、链接(验证、准备、解析)、初始化
加载阶段,虚拟机需要完成以下3件事情:
1.通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
验证阶段大致会完成4个阶段的检验动作:
1.文件格式验证
2.元数据验证
3.字节码验证:
4. 符号引用验证
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化阶段,则根据程序猿通过程序制定的主观计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器()方法的过程。

三、
JVM调优
栈的内存要远远小于堆内存
-Xss选项设置栈内存的大小。
-Xms选项可以设置堆的开始时的大小,-Xmx选项可以设置堆的最大值。

四、锁(lock、synchronized)
在Java中,每一个对象都拥有一个锁标记(锁),也称为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。
 
Lock和synchronized有以下几点不同:
  1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  2)采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完后,系统会自动让线程释放对锁的占用;synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  3)响应中断Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  4)Lock可以是否获取锁,synchronized不行。
    5)Lock可以提高多个线程进行读操作的效率。
    6) 公平锁Lock可以设置为公平锁,synchronized就是非公平锁
 
单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的(线程B调用threadB.interrupt()方法能够中断线程B的等待过程
)。
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

五、
线程
线程共有5中状态:新建、可运行(runnable)、运行(running),阻塞和死亡。
当使用new操作符创建新线程时,线程处于“新建”状态。
当调用start()方法时,线程处于可运行状态。
阻塞状态是指线程因为某种原因放弃了cpu 使用权,即让出了cpu分片时间,暂时停止运行。直到线程进入可运行状态,才有机会再次获得cpu时间 转到运行状态。
阻塞的情况分三种:  
    (一). 等待阻塞:运行的线程执行o.wait()方法,JVM会把该线程放入等待队列中。
    (二). 同步阻塞:运行的线程在获取对象的同步锁时,该锁正被其他线程拥有,则JVM会把该线程放入锁池(lock pool)中。
    (三). 其他阻塞:运行的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
当run()方法运行完毕或出现异常时,线程处于终止状态。

 
 
1)start方法:start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。
2)run方法:run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。
3)sleep方法:sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,即使调用sleep方法,其他线程也无法访问这个对象。
注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。
4)yield方法:调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
  注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。
sleep() 方法在指定的睡眠时间内一定不会再得到运行机会,直到它的睡眠时间完成;而 yield() 方法让出控制权后,还有可能马上被系统的调度机制选中来运行,比如,执行yield()方法的线程优先级高于其他的线程,那么这个线程即使执行了 yield() 方法也可能不能起到让出CPU控制权的效果,因为它让出控制权后,进入排队队列,调度机制将从等待运行的线程队列中选出一个等级最高的线程来运行,那么它又(很可能)被选中来运行。
5)join方法:假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的时间。
当调用thread1.join()方法后,main线程会进入等待,然后等待thread1执行完之后再继续执行。
  实际上调用join方法是调用了Object的wait方法,这个可以通过查看源码得知。join方法同样会让线程释放对一个对象持有的锁。
wait方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。
6)interrupt方法:单独调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,也就说,它可以用来中断一个正处于阻塞状态的线程;另外,通过interrupt方法和isInterrupted()方法来停止正在运行的线程。
interrupt方法可以中断处于阻塞状态的线程。不能中断正在运行中的线程。但是如果配合isInterrupted()能够中断正在运行的线程,因为调用interrupt方法相当于将中断标志位置为true,那么可以通过调用isInterrupted()判断中断标志是否被置位来中断线程的执行。
wait()和sleep
1.sleep是Thread类的方法,是线程用来控制自身流程的;wait是Object类的方法,用来线程间的通信,这个方法会使当前拥有该对象锁的线程等待直到其他线程调用notify方法时再醒来,不过你也可以给他指定一个时间,自动醒来。这个方法主要是用走不同线程之间的调度的。
2.调用某个对象的wait()方法,相当于让当前线程交出此对象的锁,然后进入等待状态,等待后续再次获得此对象的锁;Thread类中的sleep方法使当前线程暂停执行一段时间,从而让其他线程有机会继续执行,但它并不释放对象锁;
调用sleep方法不会释放锁,调用wait方法会释放当前线程的锁(其实线程间的通信是靠对象来管理的,所有操作一个对象的线程是这个对象通过自己的wait方法来管理的,就好像这个对象是电视机,三个人是三个线程,那么电视机的遥控器就是这个锁,假如现在A拿着遥控器,电视机调用wait方法,那么A就交出自己的遥控器,由jVM虚拟机调度,遥控器该交给谁。)
 
wait()、notify()和notifyAll()方法
利用wait()来让一个线程在某些条件下暂停运行。例如,在生产者消费者模型中,生产者线程在缓冲区为满的时候,消费者在缓冲区为空的时候,都应该暂停运行。如果某些线程在等待某些条件触发,那当那些条件为真时,你可以用 notify 和 notifyAll 来通知那些等待中的线程重新开始运行。
 
wait应该永远在被synchronized的背景下和那个被多线程共享的对象上调用
用wait和notify解决生产者消费者问题。对在多线程间共享的那个Object来使用wait。在生产者消费者问题中,这个共享的Object就是那个缓冲区队列。希望上锁的对象就应该被synchronized,即那个在多个线程间被共享的对象。在生产者消费者问题中,应该被synchronized的就是那个缓冲区队列。

 
 
1)wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。为了调用wait()或者notify(),线程必须先获得那个对象的锁。也就是说,线程必须在同步块里调用wait()或者notify()。一个线程如果没有持有对象锁,将不能调用wait(),notify()或者notifyAll()。否则,会抛出IllegalMonitorStateException异常。
2)调用某个对象的wait()方法能让当前线程阻塞
3)调用某个对象的notify()方法能够唤醒一个正在等待这个对象的锁的线程,如果有多个线程都在等待这个对象的锁,则只能唤醒其中一个线程;
4)调用notifyAll()方法能够唤醒所有正在等待这个对象的锁的线程;
一个线程被唤醒不代表立即获取了对象的锁,只有等调用完notify()或者notifyAll()并退出synchronized块,释放对象锁后,其余线程才可获得锁执行。
为何这三个不是Thread类声明中的方法,而是Object类中声明的方法?
由于每个对象都拥有锁,所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作了。而不是用当前线程来操作,因为当前线程可能会等待多个对象的锁,如果通过线程来操作,就非常复杂了。