java 虚拟机

2018/11/11

运行时数据区域


程序计数器

记录正在执行的虚拟机字节码指令的地址(如果是正在咨询过的本地方法则为空)

java虚拟机栈

每个java方法在执行的同时会创建一个栈帧用于存储局部变量表,操作数栈,常量池等引用信息。从方法的调用直至执行完成过程,就对应这一个栈帧在java虚拟机中入栈出栈的过程


可以通过 -Xss这个虚拟机参数来指定每个线程的java虚拟机栈内存的大小

java -Xss512M HackTheJava

该区域可能抛出以下异常:

  • 当线程请求的栈的深度超过最大值,会抛出StackOverflowError 异常
  • 栈进行动态扩展时如果无法申请到足够内存,会抛出OutOfMemoryError异常

本地方法栈

本地方法栈与虚拟机栈类似,他们之间的区别就是本地方法栈为本地方法服务

本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。

所有的对象都在这里分配内存,是垃圾回收的主要区域(GC堆)

现代的垃圾回收算法基本都是采用分代回收算法,其主要是针对不同的对象采用不同的回收算法,可以将堆分成两块:

  • 新生代(Young Generation)
  • 老年代 (Old Generation) 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出OutOfMemoryError异常。 可以通过 -Xms 和 -Xms两个虚拟机参数来指定一个程序的内存大小,第一个参数设置初始值,第二个参数设置最大值。
    java -Xms1M -Xmx2M HackthenJava
    

    方法区

    用于存放已被加载类的信息,常量,静态变量,即时编译器编译后的代码等数据。 和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出OutOfMemoryError异常

对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现

HotSpot虚拟机把它当成永久代来进行垃圾回收,但是很难确定永久代的大小,因为他受到很多因素的影响,并且每次Full GC之后的永久代的大小都会改变,所以经常会抛出OutOfMemoryError异常。 为了更容易管理方法区,从jdk8开始,移除永久代,并把方法区移至元空间,它位于本地方法中,而不是虚拟机内存中

运行时常量池

运行时常量池也是方法区中的一部分,Class文件中的常量池(编译器生成的各种字变量和符号引用)会在类加载后被放进这个区域。这样能在一些场景中显著的提高性能,因为避免了在Java堆和Native堆中来回复制数据

直接内存

在jdk4中加入了NIO类,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里的DirectByteBuffer 对象作为这块内存的引用进行操作。

垃圾回收

垃圾收集主要针对堆和方法区进行

程序计数器,虚拟机栈和本地方法区这三个区域属于线程私有的,只存在线程的生命周期内,线程结束后也会自动消失,因此不需要对这三个区域进行垃圾回收

判端一个对象是否可被回收

1,引用计数法

给对象添加一个引用计数器,当对象增加一个引用计数器加1,引用失效是减一,引用计数为0是对象可被回收。 两个对象出现循环引用的情况下,此时引用计数器永远不会为0,导致无法对它们进行垃圾回收 正因为循环引用的存在,所以java不使用引用计数法

public class ReferenceCountingGC {

    public Object instance = null;

    public static void main(String[] args) {
        ReferenceCountingGC objectA = new ReferenceCountingGC();
        ReferenceCountingGC objectB = new ReferenceCountingGC();
        objectA.instance = objectB;
        objectB.instance = objectA;
    }
}

2, 可达性分析算法

通过GC Roots作为起始点搜索,能够达到的对象都是存活的,不可达的对象可被回收.

java虚拟机使用该算法来判断对象是否可被回收,在Java中GC Roots一般包含以下内容:

  • 虚拟机栈中局部变量表中引用的对象
  • 本地方法中JNI引用的对象
  • 方法中类静态属性引用的对象
  • 方法区常量引用的对象


3 方法区的回收 因为方法区主要存放的是永久代对象,而永久代对象的回收率比新生代低很多,因为在方法区上进行回收性价比不高 主要是对常量池的回收和对类的卸载

在大量使用反射,动态代理,CGlib等ByteCode框架,动态生成JSP以及OSGI这类频繁自定义Classloader的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。

类的卸载条件很多,需要满足一下三个条件,并且满足了也不一定会被卸载

  • 该类的所有实例已经被回收,并且堆中不存在该类的任何实例
  • 加载该类的Classloader已经被回收
  • 该类对应得Class没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。 可以通过 -Xnoclassgc 参数来控制是否对类进行卸载

4 finalize() finalize类型C++的析构函数,用来关闭外部资源等工作。但是try-finally等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用循序,因此最后不要使用。

当一个对象被回收时,如果需要执行对象finalize()方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用过finalize()自救,后面回收时不会调用finalize()方法。

## 引用类型 无论是通过引用计数法来判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判断对象是否可被回收都与引用有关。

java四种不同程度的引用类型

### 1,强引用 被强引用关联的对象不会被回收。 使用new一个新的对象的方式来创建引用。

 Object obj = new Object();

### 2,软引用 被软引用的对象只有在内存不够的情况下才会被回收

使用SoftReference类来创建软引用

 Object obj = new Object();
 SoftReference<Object> sf = new SoftReference<Object>(obj);
 obj = null;  // 使对象只被软引用关联

### 3,弱应用 被弱引用关联的对象一定会被回收,也就是说他只能存活到下一次垃圾回收发生之前 使用WeakReference类来实现弱引用

 Object obj = new Object();
 WeakReference<Object> wf = new WeakReference<Object>(obj);
 obj = null;

### 4,虚引用

又称为幽灵引用和幻影引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过一个虚引用取得一个对象

为一个对象设置虚引用关联的唯一目的就是在这个对象被回收时得到一个系统通知。 使用 PhantomReference 来实现虚引用

 Object obj = new Object();
 PhantomReference<Object> pf = new PhantomReference<Object>(obj);
 obj = null;

## 三,内存分配与回收策略

### Minor GC 和 Full GC

  • Minor gc:发生在新生代上,因为新生代存活的对象很短,因此Minor GC会频繁执行,执行的速度一般也比较快
  • Full GC:发生在老年代上,老年代对象存活时间比较久,因此Full GC很少执行,执行速度比MinorGC慢很多

### 内存分配策略 #### 1,对象忧先在Edn上分配 大多数情况下,对象在新生代Edn区分配,当Edn区空间不够时,发起Minor GC

#### 2,大对象直接进入老年代 大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。 经常出现大对象会提前触发垃圾收集以获取足够的空间分配给大对象 -XX:PretenceSizeThreshold, 大于此值的对象直接在老年代分配,避免在Edan区和Survivor区之间的大量内存复制。

#### 3,长期存活的对象进入老年代 为对象定义年龄计数器,对象在Edn出生并且在Minor GC仍然存活,将移动到Survivor中,年龄就增加一岁,增加到一定年龄则进入老年代 -XX:MaxTenuringThreshold 用来定义年龄的阈值。

#### 4,动态年龄判定 虚拟机并不是永远的要求对象达到MaxTenuringThreshold 才能晋升到老年代,如果在Survivor中相同年龄所有对象的大小的总和大于Survivor空间的一半,则年龄大于等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄

#### 5,空间分配担保,在发生Minor GC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么Minor GC可以确认安全的。 如果不成立的话虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于晋升老年代对象的平均大小,如果大于,将尝试进行一次Minor GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那么就进行一次Full GC

## 4,类的加载机制 类在运行时第一次使用动态加载的,而不是编译时一次性加载。如果在编译时一次性加载,会占用很多内存

### 类的生命周期

包括以下7个阶段

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)
  • 使用(Using)
  • 卸载(Unloading) ### 类加载过程 包含了加载,验证,准备,解析和初始化这五个阶段

#### 1,加载 加载是类加载的一个阶段,注意不要混淆

加载过程完成以下三件事

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构
  • 在内存中生成一个代表这个类的Class对象,作为方法区这个类各种数据的访问接口

其中二进制字节流可以从以下方式获取

  • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础
  • 从网络中获取,最典型的应用是 Applet。
  • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流
  • 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。

### 2,验证 确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

### 3,准备 类变量是被static修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存 实例变量不会这个阶段分配内存,它将会在对象实例化是随着对象一起分配在堆中 注意:实例化不是类加载的一个过程,类加载是在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次 初始值一般为0,例如下面的类变量value被初始化为0而不是123

 private int value = 123

如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为0.

 private static final value = 123

4,解析

将常量池的引用替换为直接引用的过程 其中解析过程在某些情况下可以在初始化之后才开始,这是为了支持java的动态绑定

5,初始化

初始化阶段才真正开始执行类中定义的java程序代码,初始化阶段就是虚拟机执行类构造器clinit() 方法的过程。 注意:init()是对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行init方法,() 是类构造器 clinit()方法具有以下特点:

  • 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
 public class Test {
     static {
         i = 0;                // 给变量赋值可以正常编译通过
         System.out.print(i);  // 这句编译器会提示“非法向前引用”
     }
     static int i = 1;
 }
  • 与类的构造函数(或者说实例构造器 ())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 () 方法运行之前,父类的 () 方法已经执行结束。因此虚拟机中第一个执行 () 方法的类肯定为 java.lang.Object。
  • 由于父类的 () 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:
 static class Parent {
     public static int A = 1;
     static {
         A = 2;
     }
 }
 
 static class Sub extends Parent {
     public static int B = A;
 }
 
 public static void main(String[] args) {
      System.out.println(Sub.B);  // 2
 }
  • ()方法对于类或者接口并不是必须的,如果一个类中不包含静态语句块,也就没有对类变量的赋值操作,编译器可以不为类生成() 方法
  • 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法。但接口与类不同的是,执行接口的 () 方法不需要先执行父接口的 () 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 () 方法。
  • 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都会阻塞等待,直到活动线程执行 () 方法完毕。如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

类的初始化时机

1,主动引用

虚拟机规范中并没有明确规定何时加载,但是规范严格规定了只有且只有一下五种情况必须对类进行初始化(加载,验证,准备都会随之发生)

  • 遇到new,getStatic,putStatic,invokeStatic这四条字节码指令时,如果类没有进行初始化,则必须先触发使其初始化。最常见的生成这四条指令的场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰),已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化
  • 当初始化一个类时,如果发现父类没有初始化,则先对其父类进行初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类;
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

    2.被动引用

以上五种场景中的行为称为一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化。称为被动引用。被动引用的常见例子包括:

  • 通过子类引用父类的静态字段,不会导致子类初始化
 System.out.println(SubClass.value);  // value 字段在 SuperClass 中定义
  • 通过定义数组来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是由虚拟机自动生成的,直接继承Object子类,其中包含了数组的属性和方法
     SuperClass[] sca = new SuperClass[10];
    
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类, 因此不会触发定义常量的类的初始化
     System.out.println(ConstClass.HELLOWORLD);
    

## 类与类加载器 两个类相等需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。 这里的相等,包括类的class对象的equals()方法,isAssignableForm()方法,isInstance()方法返回的结果为true,也包括要用instance关键字做对象所属关系判断结果为true。 ## 类加载器分类 从java虚拟机的角度来讲,只存在以下不同的类加载器,

  • 启动类加载器(Bootstrap Classloader),这个类加载器用C++的一部分,是虚拟机自身的一部分。
  • 所有其他类的加载器,这些类由java实现,独立于虚拟机外部,并且全部都继承抽象类ClassLoader. 从java开发人员的角度看,类加载器可以划分的更细致一些:
  • 启动类加载器(Bootstrap Classloader)此类加载器负责将存放在JRE_HOME\lib目录中的,或者被-XBootClasspath参数所指定的路径中的。并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的即使放在lib中也未必会加载)类库加载在虚拟机内存中。启动类加载器无法被java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用null代替即可
  • 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 /lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型

应用程序是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。

下面展示的类加载器之间的层级关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的类加载器外,其余的类加载器都应有自己的父类加载器。这里的类加载器之间的关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系

1.工作过程

一个类加载器首先将类加载请求传送到父类加载器,只有当父加载器无法完成类加载请求时才会尝试加载

2,好处

使得java类随着它的类加载器一起带有一直优先级的层次关系,从而使得基础类得到统一。

例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。

3.实现

以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。

 public abstract class ClassLoader {
     // The parent class loader for delegation
     private final ClassLoader parent;
 
     public Class<?> loadClass(String name) throws ClassNotFoundException {
         return loadClass(name, false);
     }
 
     protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
         synchronized (getClassLoadingLock(name)) {
             // First, check if the class has already been loaded
             Class<?> c = findLoadedClass(name);
             if (c == null) {
                 try {
                     if (parent != null) {
                         c = parent.loadClass(name, false);
                     } else {
                         c = findBootstrapClassOrNull(name);
                     }
                 } catch (ClassNotFoundException e) {
                     // ClassNotFoundException thrown if class not found
                     // from the non-null parent class loader
                 }
 
                 if (c == null) {
                     // If still not found, then invoke findClass in order
                     // to find the class.
                     c = findClass(name);
                 }
             }
             if (resolve) {
                 resolveClass(c);
             }
             return c;
         }
     }
 
     protected Class<?> findClass(String name) throws ClassNotFoundException {
         throw new ClassNotFoundException(name);
     }
 }

## 自定义类加载器 FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。

java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,因此自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。

  public class FileSystemClassLoader extends ClassLoader {
  
      private String rootDir;
  
      public FileSystemClassLoader(String rootDir) {
          this.rootDir = rootDir;
      }
  
      protected Class<?> findClass(String name) throws ClassNotFoundException {
          byte[] classData = getClassData(name);
          if (classData == null) {
              throw new ClassNotFoundException();
          } else {
              return defineClass(name, classData, 0, classData.length);
          }
      }
  
      private byte[] getClassData(String className) {
          String path = classNameToPath(className);
          try {
              InputStream ins = new FileInputStream(path);
              ByteArrayOutputStream baos = new ByteArrayOutputStream();
              int bufferSize = 4096;
              byte[] buffer = new byte[bufferSize];
              int bytesNumRead;
              while ((bytesNumRead = ins.read(buffer)) != -1) {
                  baos.write(buffer, 0, bytesNumRead);
              }
              return baos.toByteArray();
          } catch (IOException e) {
              e.printStackTrace();
          }
          return null;
      }
  
      private String classNameToPath(String className) {
          return rootDir + File.separatorChar
                  + className.replace('.', File.separatorChar) + ".class";
      }
  }

参考链接


扫码关注有惊喜

(转载本站文章请注明作者和出处 火花的博客-dbbaxbb

Show Disqus Comments

Post Directory