Java虚拟机的秋招面试总结

JVM内的守护线程Daemon

  守护线程的生命周期:随着程序在JVM中运行,守护线程第一时间被启动,并且一直处于运行态,。当所有用户线性都执行完毕后,程序就会杀死守护线程,离开JVM,终止程序。

用户线程转换为守护线程的注意:

  1. thread.setDaemon(true)必须在thread.start()之前设置,不能把正在运行的用户线程设置为守护线程。 
  2. 在Daemon线程中产生的新线程也是Daemon的。
  3. 守护线程不能用来及逆行JVM中文件,数据库大的读写或者进行计算任务,因为当所有用户线程完成后程序会强行杀死所有守       护线程,而若在守护线程中进行读写或计算的话有可能因为程序的退出而导致操作不能完成。

 

 

JVM—字节码增强技术

Java字节码增强是指在Java字节码生成之后,对其进行修改,增强其功能,Java字节码增强主要是为了减少冗余代码,提高性能等。

 

 

 

对象是咋样分配在堆中

  1. 对象首先分配在线程的本地分配缓冲区。

每个线程可以在堆中预先分配到一块区域,作为本地线程分配缓冲区(TLAB),当该线程执行的时候,有对象创建的话,就在该线程的TLAB中分配内存,当该线程的TLAB用完了才申请堆中空闲区域。

  1. 堆中优先分配Eden

大多数情况下,对象都在新生代的Eden区分配内存,而因为大部分的对象都是朝生夕死的,所以新生代又会频繁的进行垃圾回收。

  1. 大对象直接进入老年代

需要大量连续空间的对象,如:长字符串、数组等,会直接在老年代分配内存,为了避免在新生区频繁的GC时发生大量的内存复制。

  1. 长期存活的对象“晋升”老年代

新生代中经历了多次GC后仍然存活的对象,当年零达到一定程度(默认15)时就是晋升为老年代。

  1. 空间分配担保

在MinorGC(新生代GC)之前,会先检查老年代中最大可用空间是否可以容纳新生代的所有对象,如果可以容纳,则MinorGC可以完全执行,否则,检查是否允许担保失败,是则检查老年代最大可用空间是否大于历次晋升到老年代的对象的平均大小,是则尝试进行MinorGC,小于或则MinorGC失败,则会发起一次FullGC清理老年代。

 

 

 

 

Java 内存模型

JVM中规定了所有的变量都存在在主内存中,然后每条线程都有自己的工作内存,线程的工作内存保存了该线程需要用到的变量发的拷贝值,线程在CPU上运行时都是堆自己工作线程中的数据进行读写操作,运行结束之后才将数据同步到到主内存中,JVM需要线程同步机制来达到多线程同一内存区域的读写控制。
 

 

主内存与工作内存的数据交互

  1. lock:把主内存变量表示为一条线程独占,此时不允许其它线程对此变量进行读写。
  2. unlock:解锁一个主内存变量
  3. Read :把一个主内存值保存成到线程工作内存中作为变量副本,强调的是读入的这个过程
  4. Load :把read到变量值保存到线程工作内存中作为变量副本,强调的是读入的值的保存过程。
  5. Use:线程执行期间,把工作内存中的变量值传递给字节码执行引擎。
  6. Assign(赋值):字节码执行引擎把运算结果传递回工作内存,复制给工作内存中的结果变量
  7. Store:把工作内存中的变量值传递主内存,强调的是传递的过程
  8. Write:把store传递进来的变量值传递到主内存,强调传送的过程。

JVM要求以上8个操作都具有原子性,即对数据的读写操作具有原子性,但也有例外,即:long、double、的非原子性协议,这两个64位类型的数据的读写操作各需要两次进行,一次读写32位,两次不能保证原子性。

 

原子性:基本数据类,更大范围的原子性可以用lock、unlock操作实现,表现到代码层面就是使用同步代码块型的读写操作都是原子性的

可见性:当一个线程修改了主内存的变量的时候,其他线程就立刻知道这个修改。

  可以使用Volatile关键字修改的变量,一旦在工作内存中被修改,则立刻同步回主内存,并且其他使用了这个变量的线程的工作内存和立刻从主内存读取新值。而Synchronized关键字修饰的变量由于一次只能由一个线程使用,所以一次也只能有一个工作线程读写它,所以只能纵向地实现可见性。

有序性:多线程之间对共享数据地操作的有序性,可以通过volatile和Synchronized关键字来保证,Volatile关键字禁止了指令重排,而synchronized关键字规定了多个线程每次只能由一个线程对共享数据进行操作。

一个volatile变量由两种特性,可见性、禁止指令重排。但是volatile不具备原子性,原因是volatile变量的值可以被多线程交替修改,而修改包含read、load、use、store、write等过程,这些过程不能保证是原子执行的。

可见性:关键字修饰的变量,一旦在工作内存中被修改,则立刻同步回主内存,并且其他使用了这个变量的线程的工作内存回立刻从主内存读取新值。

禁止指令重排:volatile变量在赋值后创建一个内存屏障,指令重排时,位于后面的指令不重排到内存屏障之前。

final (一旦初始化完成,其他线程可见)

 

指令重排:

int a=1;int b=2;int c=a+b;

生成机器指令以后的执行步骤为:

1、对a赋值1

2、对b赋值2

3、取a的值

4、取b的值

5、将取到的值相加之后存放在c

以上这些指令在执行的时候不一定是按照1-5 的指令来执行的,指令重排是发生在单线程里面的,因为在操作的时候操做的都是这些变量的副本。

指令重排在单线程里面是执行顺序可能不一样,但是在多线程里面从其他的线程看来是这单个线程的执行是有序的。

内存屏障:

内存屏障又叫内存栅栏,是一组机器指令,用于实现对内存操作的顺序限制

就是让一个CPU处理单元中的内存状态对其他处理单元可见的一项技术。

内存屏障提供了连个功能,首先,他们通过确保从另一个CPU来看屏障两边的所以指令都是正确的程序顺序,而保证程序顺序的外部可见性,其次它们可以实现内存数据的可见性,确保内存数据回会同步到CPU的缓存子系统。

Java中的内存屏障主要有Load和Store

对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存中加载数据。

对store 来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回主内存

 实际应用中分为四类:

LoadLoad屏障

序列:Load1、loadload 、Load2

确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。通常能执行预加载指令或支持乱序处理的处理器中需要显示声明LoadLoad屏障,因为在这些处理器中正在等待的加载指令能够绕过正在等待存储的指令。 而对于总是能保证处理顺序的处理器上,设置该屏障相当于无操作。

Store Store指令

序列:Store1StoreStoreStore2

确保Store1的数据在Store2以及后续的Store指令操作相关数据之前对其它的处理器可见,通常情况下,处理器不能保证从写缓冲向其它处理器和主存中按顺序刷新数据,那么它需要使用Store、Store屏障。

LoadStore

序列:Load1、LoadStore 、Store1

确保Load1的数据在Store1和后续的store指令被刷新前读取,在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障

StoreLoad 屏障

序列:Store1 ,Store Load、Load1

   确保Store1的数据在被Load1和后续的Load指令之前对其他的处理器可见,StoreLoad屏障可以防止一个后续的Load指令不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新的数据。

Java中的线程调度是指系统为处理器分配线程的过程,主要有两种调度方式,分别是协同式调度和抢占式调度。

协同式调度:

 线程的执行时间是由线程本身控制,线程工作执行完了以后,要主动通知系统切换到另一个线程上,最大的好处是没有线程的同步问题。

 

 

 

JVM类加载机制与对象的生命周期

 虚拟机 把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终成为被虚拟机直接使用的Java对象,这就是类的加载机制。

  1. 类的声明周期

类的生命周期包括7个部分:加载--验证准备解析初始化使用--卸载

其中,验证准备解析 称为连接阶段。除了解析外,其他阶段是顺序发生的,而解析可以于这些阶段交叉进行,因为Java支持动态绑定,需要运行时才能确定具体类型。

  1. 类的初始化触发

累的加载机制没有明确的触发条件,但是又5种情况下必须对类进行初始化,那么加载验证准备就必须在此以前完成了。

  1. new ,getstatic pubstatic ,invokestatic这四个字节码指令时对类进行初始化(即:实例化对象,读写静态对象,调用静态方法时,进行类的初始化)
  2. 使用反射机制对类进行调用时,进行类的初始化
  3. 初始化一个类,其父类没有被初始化时,先初始化其父类,
  4. 虚拟机启动时,初始化一个执行的主类。
  5. 读写静态对象或者调用静态方法,则初始化句柄对应类。

一般,以上五种最常见的是前三种:实例化对象,读写静态方法,调用静态方法,反射机制调用类,调用子类触发父类初始化,

  1. 类的加载过程
    1. 加载

加载阶段,虚拟机需要完成三件事:通过类名获取类的二进制字节流——将字节流的内容转存到方法区在内存中生成同一个Class 对象作为该类方法区的数据访问入口。

           通过类名获取类的二进制字节流通过类加载器来完成的,其中加载过程使用的是“双亲委派模型”

启动类加载器:加载系统环境变量下JAVA_HOME/lib目录下的类库。

扩展类加载器:加载JAVA_HOME/ext目录下的类库

应用程序加载器:加载用户类路径classpath指定的类库。

自定义加载器:如果需要自定义加载时的规则,可以自己是现实类加载器

双亲委派模型是指:当一个类加载器收到类加载请求时,不会直接加载这个类,而是把这个加载请求委派给自己父类加载器去完成。如果父类加载器无法加载时,子加载器才会去尝试加载。

采用双亲委派模型的原因,避免一个类被多个类加载器重复加载。

2、验证:

 确保class文件的二进制字节流中包含的信息符号虚拟机要求,包括:文件格式验证、元数据验证(数据语义分析)、字节码验证(数据流语义合法性)、符号引用验证(符号引用的匹配性校验,确保解析能正确执行)

3准备:

为类变量在方法区中分配内存,并设置零值。这里是类变量不是实例变量,实例变量是对象分配到堆内存时根据运行时动态生成的。

4解析:

把常量池中的符号引用解析为直接引用,根据符号引用所在的描述,在内存中找到符合描述的目标并把目标指针返回。

5初始化:

       真正开始执行Java程序代码,该步执行<clinit>方法根据代码赋值语句,对 类变量和其他资源  进行初始化赋值。

<clinit>方法:编译器自动收集类中所有类变量的赋值语句和静态语句合并而成,手机的顺序是在程序代码出现的顺序,所以,静态语句中只能访问到定义在静态语句块之前的变量,在其之后的变量可以赋值,但不可以访问,

向上转型的例子时程序代码的运行顺序了:父类静态内容---子类静态内容父类构造器---子类构造器---子类构造方法。

在经历了上面5加载阶段后,才真正地可以使用class对象或者使用实例对象。使用过后,不再需要用到该类的class对象或者实例对象时,就会把类卸载掉(发生在方法区的垃圾回收:无用类的卸载)。

在初始化类加载的最后阶段,这里所有的静态变量会被赋值初始化,并且静态块将被执行。

  1. 对象的生命周期:

对象是由类创建出来的,所以对象的生命周期就包含在类的生命周期里面:

类加载---创建累的实例对象使用对象对象回收类卸载

 

 

 

 

 

线程安全和锁优化

Java 中将各种操作共享的数据分为5类,不可变类型、绝对线程安全、相对线程安全、线程兼容和线程对立

  1. 不可变

如果共享数据是一个基本类型,那么只要在声明变量的时候加上final关键字他就是不可变的。如果共享数据是一个对象,那就要保证对象的行为不会对其状态产生任何影响才行,Java.lang.String 就是一个 不可变的对象,调用它的substring(),这些方法都不会影响他的值,会返回一个新的对象。除了String类型之外,还有枚举类型,以及Java.lang.Number的部分子类,但那时Number的子类型AtomicInteger和AtomicLong则并非是不可变的。

 

 

 

Java 中的内存泄露问题:

 

判断一个内存空间是否符合垃圾回收的标准有两个:

第一:给对象赋予了空值,以后再没有使用过

第二:对对象赋予了新值,重新分配了内存空间。

内存泄露发生的两种情况:

 1 在堆中申请的内存没有释放

 2 对象不在使用,但是还没有释放

因为Java具有垃圾回收机制,所以内存泄露主要发生在第二种情况

造成内存泄露的情况:

  1. 静态集合类
  2. 各种连接
  3. 监听器
  4. 变量不合理的作用域
  5. 单例模式可能会造成内存泄露

 

s==null ,其中s是一个字符串类型的引用,他不指向任何一个字符串。而赋值语句s=”“,中s是一个字符穿类型的引用,它指向另一个字符串。

new String(“abc”)创建了几个对象

答案:一个或者两个。如果常量池中原来有abc,那么只创建一个对象,如果没有创建两个。

 

为何使用父类委托机制?

父类委托机制的优点是能够提尕松软件系统的安全性,假设我自己定义一个类加载器,这个类不符合JVM规范,里面有不安全的代码,如果不适用委托机制,这个类就会被直接加载到内存里面了,如果使用父类委托,那么就被父加载器加载,会按照jvm的规范来加载,不符合规范就不会加载。

 

猜你喜欢

转载自blog.csdn.net/ddhsea/article/details/84339836