JVM-java虚拟机

文章目录

      • 1.内存区域划分
      • 2.垃圾回收GC*****
          • 如何判断对象是否存活:
          • 对象的缓刑阶段--finalize()
          • GC算法--分代收集
      • JDK内置的JVM工具
      • volatile
        • Double-Check-Singleton:双重单例加锁模式。

在JVM中一般会遇到以下俩个概念:

1.JVM 内存区域划分
物理的区域划分
2.JVM 内存模型(JMM)
表示多线程的逻辑结构,工作内存与主内存的关系。
所以一般当提到JVM内存怎么划分,一定要搞清楚要问的是哪一个。

1.内存区域划分

1.线程私有内存

  • 程序计数器
    记录当前线程正在执行的字节码行号指示器,简单来说就是当前线程跑到哪行代码了,这样子当线程重复调度的时候,就可以知道从哪开始恢复执行。
  • 虚拟机栈
    方法的执行模型,每个方法的调用以及执行完毕对应的栈帧在栈中的入栈和出栈。
    -Xss:设置栈的深度。
  • 本地方法栈
    本地方法栈就是本地方法,虚拟机栈是java方法。

2.线程共享内存


  • 所有的数组元素和对象
    -Xms:设置堆的最小值
    -Xmx:设置堆的最大值
    -Xmn:设置新生代内存大小
  • 方法区
    已加载的类信息以及常量和静态变量。
    已加载类信息:类的权限,属性,方法信息。
  • 运行时常量池
    方法区的一部分,存符号引用和自变量。
    例:
class Person{
   private String name;
   private Integer age;
   public void print(){} 
}
当Person per = new Person();后
per和俩个属性的值都放在堆中。
类的权限,属性和方法的信息都放在方法区。

10, “abc”----在方法区

符号引用:包名.类名  定位一个类的信息

2.垃圾回收GC*****

1.如何判断对象是否存活。(知道哪些对象要被回收)
2.如果把对象认为不再存活,标记位需要回收后,还有一个缓刑阶段(Object类的finalize()方法)。
3.如果对象通过finalize()方法判定为彻底不再存活,要进行回收。
4.如何回收:GC算法。

如何判断对象是否存活:

1.引用计数
给每个对象加一个引用计数器,当有一个地方引用这个对象,引用计数器+1,当不再引用,也就是引用等于null,该对象计数器-1。任意时刻当计数器等于0,判定这个对象不再存活。
无法解决循环引用问题。循环引用:我中有你你中有我。
test类中有一个Object属性,Object又指了test2,test2中的Object又指向test1。
2.可达性分析算法–Java
通过一系列称为GC Roots的对象开始向下寻找,若从GC Roots到对象有路可走,把这个路叫引用列,认为这个对象存活。无路可走就认为不存活。
有路可走,无路可走

哪些对象可以作为GC Roots:4个
1.本地方法栈、虚拟机栈中的变量。–》局部变量
2.类中的常量和静态变量。
类中的成员变量一定不是GC Roots,所以循环引用无效。

JDK1.2 后关于引用的扩充。很重要!

  • 强引用
    Person per = new Person()。
    –效果:只要对象被任意一个强引用指向,无论是否发生内存溢出异常(OOM),都不能回收被强引用指向的对象。

  • 软引用
    –JDK1.2后一个类–SoftReference类描述软引用。
    —语义:若对象只被软引用指向,当内存够用时不回收此对象,当内存不够用时(即将抛出OOM异常)时,会回收所有仅被软引用指向的对象。
    –软引用对象为有用但不必须对象。(如缓存对象:自动登录)。
    —如何实现一个线程安全缓存(Map)?
    ①HashMap+ReentrantReadWriteLock。
    ②使用ThreadLocal(线程的本地变量,线程隔离)

  • 弱引用:
    WeakReference类描述弱引用。
    当对象那个只被弱引用指向,当下次DC开始时,无论内存是否够用,都会回收掉只被弱引用指向的对象。

  • 虚引用
    —PhatomReference类描述虚引用。虚引用不对生存周期产生任何影响,并且也无法通过虚引用取得一个对象。
    —作用:被虚引用指向的对象,在进行GC之前会收到一个回收信息。需要知道类的对象有没有被回收,就给对象打上虚引用,当垃圾回收开始的时候,如果对象被回收了,JVM会发一个回调信息,告诉你这个对象被回收了。
    ----用到哪里:调优。想知道那个对象被回收了。
    ---- 轻度依次递减。-----

对象的缓刑阶段–finalize()

final finally finalize区别
final:终接器,常量,不可变类,无法被覆写的方法。
finally:用在异常体系中,保证finally代码块中的语句一定被执行。
考点:带不带return语句,return在哪
finalize():用在JVM垃圾回收的对象是否存活里面。
语义:
当一个对象被标记为不可达时,GC线程在回收时需要看:
1.若此对象所在的类没有覆写finalize()方法,认为此对象不再存活,可以回收。
2.若此对象所在的类覆写了finalize方法()
2.1且未被JVM调用,由JVM调用finalize()方法,若在finalize()中与任何一个GC Roots挂钩,就认为此对象存活下来了。
2.2已被JVM调用过,认为此对象不再存活。

GC算法–分代收集

堆分为新生代和老年代。
新生代:对象存活率非常低。
老年代:对象存活率较高。

分代收集:新生代采用复制算法,老年代采用标记-整理方法。
1.新生代
java新的复制算法:将新生代分为一块较大的Eden区和俩块较小的Survivor区,每次使用Eden和其中一块Survivor。Eden:Survivor=8:1:1(有俩个Survivor)
与原来的复制算法相比:利用率由原来的百分之50到现在的百分之90。
复制算法步骤
新生代的俩块Survivor区一个称为From区,另一块称为To区。
step1:当Eden区第一次满时,将Eden区的存活对象复制到From区,清空From区。
step2:当Eden区第二次满时,将Eden区和From区的存活对象复制到To区(对象存放是挨着的),一次清空Eden区与From区。
…这是一个来回重复的动作。
若干对象会在From与To区来回复制,默认复制了15次的时候,会将此对象移动到老年代。

2.老年代的标记-清除
老年代的清除:先把存活对象按一端移动,然后把存活空间以外的空间干掉,避免空间碎片。
为何老年代不采用复制算法:老年代对象存活率较高,如果采用复制算法,大部分对象都要复制,开销很大效率较低。

MinorGC:发生在新生代的垃圾算法,采用复制算法,效率较高,发生频率较高。
FullGC(Major GC):发生在老年代的垃圾回收,采用标记-整理算法,速度一般比MinorGC满10倍以上,发生频率较低。一般来说,发生FullGC至少会伴随一次MinorGC。
老年代什么时候得回收:Eden存活对象空间太大,无法放入From/To区,会直接进入到老年代,老年代满了后就必须回收。

JDK内置的JVM工具

Linux下如何查看进程id?
PID+grep
jsp -l:可以输出包名.类名。
1.jps:使用频率最高。
查看当前操作系统下所有JVM进程,返回进程ID。
2.jstack
查看指定JVM进程的线程堆栈情况(一般多线程卡死使用此工具定位问题)
jstack pid -l
3.jmap
查看指定JVM的内存情况。

使用以上三个工具分析JVM运行情况。

volatile

  • 保证变量的可见性。
    —可见性:synchronized(Lock)、final、volatile
    —先写后读
  • 内存屏障。
    –CPU指令重排:
int i = 1int y = 2;
boolean flag = true;
i = i+3;
y=y+4;
第三行代码与其他代码无关,JVM可能会推迟或提前第三行代码的执行。
1.加了volatile后就可保证第三行代码不可能被提前延后执行。
2.当CPU运行到第三行代码,可以保证前面的代码一定执行完毕,且结果可以被后续可见,并且之后的代码还未开始执行。内存屏障仅仅保证第三行禁止重排,其他四行不可保证:有可能先21,先54.

Double-Check-Singleton:双重单例加锁模式。

1.单例:
一个类的对象有且只有一个。
构造方法私有化。
唯一一个对象在类的内部,被私有化,提供一个返回对象的方法。

饿汉单例:上来就new。
懒汉单例:用时就new。线程不安全,如何安全:new的时候加锁,然后在锁内部再次判断对象是否为null。

懒汉式单例缺陷:,SINGLETON有可能被初次化了俩次甚至多次。
假设现在有三个线程同时调用getInstance(),他们三个都同时走到了判断对象是否为null这一步,那么SINGLETON至少被new了三次,因为走到这一步的时候这三个线程看到的SINGLETON都为null,所以至少new了三次,所以三个线程拿到的SINGLETON都不是一个。
**如何保证new是一个线程安全?**在new的那块加锁synchronized(类名.class),这就是所谓的一重加锁,在这里一定可以保证对象new了一次。但是他有一个问题:如果此时线程1拿到锁进去了,线程2和3卡在了获取锁外面,线程1new完之后,线程2和3恢复执行的地方在这个锁之外,当锁被释放的时候,线程2和3还是会进去,因此需要在拿到锁后再进行对象是否为空的判断,此时就可以保证不管有多少个线程只产生了一个对象。
Double-Check:为什么检查俩次null?
第一个:保证此时确实是第一次调用。
第二个:保证多线程场景下即便加锁也保证new只会执行一次。
此时,Double-Check检查了俩个但是只加了一次锁,另外一次锁在对象的volatile关键字上。
那么为什么要加volatile关键字?
假设此时线程1拿到了sychronized锁,正在进行初始化操作。此时线程2准备调用getInstance()方法,想取得一个SINGLETON对象。那么,由于new操作不是一个原子性操作,new有三个操作:(SINGLETON = new Single())
a.在堆上开空间。
b.SINGLETON 指向堆空间。
c.构造方法完成属性初始化。
当执行第二步以后,SINGLETON已经不等于null,假设线程1此时走到了第2步。线程2看到SINGLETON已经不等于null了,然后返回了SINGLETON,但是此时返回的SINGLETON可能还未用构造方法初始化,拿到了一个不完整的对象。那么用volatile保证我拿到的单例一定是初始化之后的。在return SINGLETON上有一个屏障,走到这行代码,一定能保证前面的所有代码都已经执行完。
第二层锁保证我拿到的单例对象一定是属性全部初始化过后的单例对象。
俩次加锁:
synchronized:保证线程安全,new的时候只有一个线程在操作。
volatile:保证不会指令重排,return的SINGLETON一定是所有属性全部初始化完成的单例对象。

你可能感兴趣的:(java)