JVM知识


六、JVN

1、谈谈对Java的理解

  • 平台无关性(一次编译,到处运行)
  • GC(垃圾回收机制,释放堆内存)
  • 语言特性(泛型、反射,lamada表达式)
  • 面向对象(封装,继承,多态)
  • 类库(Java本生自带的集合,并发库,网络库,IO)
  • 异常处理

2、平台无关性如何实现?

Compile Once, Run Anywhere如何实现?

  • 编译时
  • 运行时

示例演示:

javap是jdk自带的反汇编译器,可以查看编译器返回的字节码,了解编译器内部工作的机制


在liunx平台下运行:


Java文件运行流程:

平台无关性流程:

Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。

为什么JVM不直接将源码解析成机器码去执行?

  • 准备工作:每次执行都需要各种检查
  • 兼容性:也可以将别的语言解析成字节码

3、JVM如何加载.class文件

JVM是内存中的虚拟机

JVM架构:

JVM体系架构

  • Class Loader :依据特定格式,加载class文件到内存
  • Execution Engine :对命令进行解析
  • Native Interface :融合不同开发语言的原生库为Java所用
  • Runtime Data Area : JVM内存空间结构模型

4、什么是反射?

定义:JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

代码示例:

(1)Robot

package com.interview.javabasic.reflect;

public class Robot {
    // 私有变量
    private String name;
    // 公共方法
    public void sayHi(String helloSentence){
        System.out.println(helloSentence + " " + name);
    }
    // 私有方法
    private String throwHello(String tag){
        return "Hello " + tag;
    }
    static {
        System.out.println("Hello Robot");
    }
}

(2)反射示例主类

package com.interview.javabasic.reflect;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * 反射示例
 */
public class ReflectSample {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
        // 获取类
        Class rc = Class.forName("com.interview.javabasic.reflect.Robot");
        // 获取类实例
        Robot r = (Robot) rc.newInstance();
        // 打印类名称
        System.out.println("Class name is " + rc.getName());
        // 通过类获取方法 getDeclaredMethod获取该类的所有方法,不能获得该类实现的或者继承的方法
        Method getHello = rc.getDeclaredMethod("throwHello", String.class);
        // 私有变量或方法必须设置
        getHello.setAccessible(true);
        // 获取方法内容
        Object str = getHello.invoke(r, "Bob");
        System.out.println("getHello result is " + str);
        // getMethod只能获得该类的公共方法,也能获得该类继承或者实现的方法
        Method sayHi = rc.getMethod("sayHi", String.class);
        sayHi.invoke(r, "Welcome");
        Field name = rc.getDeclaredField("name");
        name.setAccessible(true);
        name.set(r, "Alice");
        sayHi.invoke(r, "Welcome");
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println(System.getProperty("java.class.path"));
    }
}

5、ClassLoader

类从编译到执行的过程:

  • 编译器将Robot.java源文件编译为Robot.class字节码文件
  • ClassLoader将字节码转换为JVM中的Class < Robot>对象
  • JVM利用Class 对象实例化为Robot对象

谈谈ClassLoader?

ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的。ClassLoader负责通过将Class 文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。

ClassLoader的种类

  • BootStrapClassLoader : C+ +编写,加载核心库java.*

  • ExtClassLoader : Java编写,加载扩展库javax.*

    System.out.println(System.getProperty("java.ext.dirs"));
    
    // 打印结果
    /Users/cyh/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
  • AppClassLoader : Java编写,加载程序所在目录

    System.out.println(System.getProperty("java.class.path"));
    // 打印结果
    /Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/tools.jar:/Users/cyh/Downloads/javabasic/out/production/javabasic:/Users/cyh/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/192.5728.98/IntelliJ IDEA.app/Contents/lib/idea_rt.jar
  • 自定义ClassLoader : Java编写,定制化加载

自定义ClassLoader类的实现方法:(不太明白6-5)

protected Class<?> findClass (String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null) ;
}

自定义ClassLoader类示例:

package com.interview.javabasic.reflect;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class MyClassLoader extends ClassLoader {
    private String path;
    private String classLoaderName;

    public MyClassLoader(String path, String classLoaderName) {
        this.path = path;
        this.classLoaderName = classLoaderName;
    }

    //用于寻找类文件
    @Override
    public Class findClass(String name) {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }

    //用于加载类文件
    private byte[] loadClassData(String name) {
        name = path + name + ".class";
        InputStream in = null;
        ByteArrayOutputStream out = null;
        try {
            in = new FileInputStream(new File(name));
            out = new ByteArrayOutputStream();
            int i = 0;
            while ((i = in.read()) != -1) {
                out.write(i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
                in.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return out.toByteArray();
    }
}

主类:

package com.interview.javabasic.reflect;

public class ClassLoaderChecker {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader m = new MyClassLoader("/Users/cyh/Desktop/", "myClassLoader");
        Class c = m.loadClass("WaLi");
        System.out.println(c.getClassLoader());
        System.out.println(c.getClassLoader().getParent());
        System.out.println(c.getClassLoader().getParent().getParent());
        System.out.println(c.getClassLoader().getParent().getParent().getParent());
        c.newInstance();
    }
}

输出:


ClassLoader的双亲委派机制

图片来源:https://blog.nowcoder.net/n/0597892d34ae4023add7406cbbe388d0

为什么要使用双亲委派机制去加载类?

  • 避免多份同样字节码的加载

查看ClassLoader下的关于native的方法:

打开openjdk中查看c源码;https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/777e3d4780b8/src/share/native/java/lang/ClassLoader.c

类的加载方式

  • 隐式加载: new
  • 显式加载: loadClass , forName等

loadClass和forName的区别

  • Class.forName得到的class是已经初始化完成的
  • Classloder.loadClass得到的class是还没有链接的

代码示例:

package com.interview.javabasic.reflect;

public class LoadDifference {
    public static void main(String[] args) throws ClassNotFoundException {
        // 还没有链接,不能执行静态代码块,所以什么也打印不出来
        ClassLoader cl = Robot.class.getClassLoader();
        // 已经初始化完成,可以直接打印出"Hello Robot"
        Class r = Class.forName("com.interview.javabasic.reflect.Robot");
//        Class.forName("com.mysql.jdbc.Driver");
    }
}

6、Java内存模型

内存简介

JVM内存模型-JDK8

Java内存模型之线程私有部分

(1)程序计数器( Program Counter Register )

  • 当前线程所执行的字节码行号指示器(逻辑)

  • 改变计数器的值来选取下一条需要执行的字节码指令

  • 和线程是一对一的关系即“线程私有”

  • 对Java方法计数,如果是Native方法则计数器值为Undefined

  • 不会发生内存泄露

    (2)JAVA虚拟机栈

局部变量表和操作数栈

  • 局部变量表:包含方法执行过程中的所有变量
  • 操作数栈:入栈、出栈、复制、交换、产生消费变量

示例演示:

package com.interview.javabasic.jvm.model;

public class ByteCodeSample {
    public static int add(int a, int b) {
        int c = 0;
        c = a + b;
        return c;
    }
}

执行及反编译命令:

javac ByteCodeSample.java
javap -verbose ByteCodeSample.class

递归为什么会引发java.lang.StackOverflowError异常?

  • 递归过深,栈帧数超出虚拟栈深度

示例代码:

package com.interview.javabasic.jvm.model;

public class Fibonacci {
    //F(0)=0,F(1)=1,当n>=2的时候,F(n) = F(n-1) + F(n-2),
    //F(2)=F(1) + F(0) = 1, F(3) = F(2) + F(1) = 1+1 = 2
    //F(0)-F(N) 依次为 0,1,1,2,3,5,8,13,21,34...
    public static int fibonacci(int n){
        if(n == 0) {return 0;}
        if(n == 1) {return 1;}
        return fibonacci(n - 1) + fibonacci(n - 2);
    }

    public static void main(String[] args) {
        System.out.println(fibonacci(1000000));
    }
}

运行结果:

虚拟机栈过多会引发java.lang.OutOfMemoryError异常

(3)本地方法栈

  • 与虚拟机栈相似,主要作用于标注了native的方法

Java内存模型之线程共享部分(不太明白)

(1)元空间( MetaSpace )

元空间( MetaSpace )与永久代( PermGen )的区别

  • 元空间使用本地内存,而永久代使用的是jvm的内存(java.lang.OutOfMemoryError : PermGen space)

MetaSpace相比PermGen的优势

  • 字符串常量池存在永久代中,容易出现性能问题和内存溢出
  • 类和方法的信息大小难易确定,给永久代的大小指定带来困难
  • 永久代会为GC带来不必要的复杂性
  • 方便HotSpot与其他JVM如Jrockit的集成

(2)Java堆(Heap)

  • 对象实例的分配区域

  • GC管理的主要区域

7、JVM三大性能调优参数-Xms -Xmx -Xss的含义

示例:
java -Xms128m -Xmx128m -Xss256k -jar xxx.jar

  • -Xss:规定了每个线程虚拟机栈(堆栈)的大小(一般情况下256k足够,此配置将会影响此进程中并发线程数的大小)
  • -Xms:堆的初始值(即该进程刚创建出来时专属Java堆的大小,一旦对象容量超过Java堆的初始容量,将会自动扩容到-Xmx大小)
  • -Xmx:堆能达到的最大值(一般将-Xms和-Xmx设置成同样的大小,因为当k不够用要发生扩容时,会发生内存抖动,影响程序运行时的稳定性)

Java内存模型中堆和栈的区别-内存分配策略

  • 静态存储:编译时确定每个数据目标在运行时的存储空间需求
  • 栈式存储:数据区需求在编译时末知,运行时模块入口前确定
  • 堆式存储:编译时或运行时模块入口都无法确定,动态分配

Java内存模型中堆和栈的联系

  • 联系:引用对象、数组时,栈里定义变量保存堆中目标的首地址


Java内存模型中堆和栈的区别

  • 管理方式:栈自动释放,堆需要GC
  • 空间大小:栈比堆小
  • 碎片相关:栈产生的碎片远小于堆
  • 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
  • 效率:栈的效率比堆高

元空间、堆、线程独占部分间的联系—内存角度

示例代码:

package com.interview.javabasic.jvm.model;

public class HelloWorld {
    private String name;
    public void sayHello() {
        System.out.println("hello" + name);
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        int a = 1;
        HelloWorld hw = new HelloWorld();
        hw.setName("test");
        hw.sayHello();
    }
}

分析:

不同JDK版本之间的intern()方法的区别一JDK6 VS JDK6+

String S = new String( original: "a");
s. intern() ; 

JDK6:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。

JDK6+:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中.
不存在,则在池中创建该字符串并返回其引用。

永久代错误测试代码:

package com.interview.javabasic.jvm.model;

import java.util.Random;

public class PermGenErrTest {
    public static void main(String[] args) {
        for(int i=0; i <= 1000; i++){
            //将返回的随机字符串添加到字符串常量池中
            getRandomString(1000000).intern();
        }
        System.out.println("Mission Complete!");
    }

    //返回指定长度的随机字符串
    private static String getRandomString(int length) {
        //字符串源
        String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for ( int i = 0; i < length; i++){
            // Returns a random number between 0 (inclusive) and 61 (exclusive),str一共62个字符
            int number = random.nextInt(62);
            // 表示获取str字符串中序号对应的字符追加到sb中
            sb.append(str.charAt(number));
        }
        return sb.toString();
    }
}

JVM参数配置(Run->EditConfigurations):

-XX:MaxPermSize=6M -XX:PermSize=6M

运行结果:

  • jdk6

  • jdk6+(jdk8)

不同jdk版本intern()方法代码测试:

package com.interview.javabasic.jvm.model;

public class InternDifference {
    public static void main(String[] args) {
        String s = new String("a"); // new的string对象在java堆中
        s.intern();
        String s2 = "a"; // “”中声明的字符串直接在常量池中创建
        System.out.println(s == s2); // ==比较地址

        String s3 = new String("a") + new String("a");
        s3.intern();
        String s4 = "aa";
        System.out.println(s3 == s4);
    }
}

JDK6版本的intern()方法:

  • 运行结果:
    false
    false
  • 解析:

JDK6+版本的intern()方法:

  • 运行结果:
    false
    true
  • 解析:jdk6+是能把字符串的引用放入常量池中的

8、指针压缩

1、指针压缩是如何实现的?如何证明开启指针压缩节省了内存?

默认情况下,Java虚拟机中对象的起始地址需要对齐至8的倍数(即为内存对齐,对应虚拟机选项 -XX:ObjectAlignmentInBytes,默认值为 8)。如果一个对象用不到8字节,那么空白的那部分空间就白白浪费掉了。这些浪费掉的空间我们称之为对象之间的填充。默认情况下,Java虚拟机中32位的指针可以寻址到2的35次方,也就是32GB的内存空间(超过32位会关闭压缩指针)。在对压缩指针解引用时,我们需要将其左移3位,再加上一个固定的偏移量,便可以寻址到32GB地址空间为64位指针了。此外,我们可以配置刚刚提到的内存对齐选项(-XX:ObjectAlignmentInBytes)来进一步提升内存寻址范围。但是,这也可能增加对象填充,导致压缩指针没有打到节省空间效果。

在64位的虚拟机中,对象头的标记字段占64位,而类型指针又占64位。也就是说一个对象额外占用的字节就是16个字节。以Integer对象为例,它仅有一个int类型的私有字段,占4个字节。因此,每个Integer的额外开销至少400%,这也就是Java为什么要引入基本数据类型的原因之一。为了减少内存开销,64位Java虚拟机引入了压缩指针概念(对应虚拟机选项-XX:+UseCompressedOops,默认开启),将堆中原本64位的Java对象指针压缩成32位的。这样一来,对象头的类型指针也会被压缩成32位,使得对象头大小从16字节降低为12字节。压缩指针不仅可以作用对象头的类型指针,还可以作用引用类型的字段,引用类型的数组。

2、开启指针压缩后,一个oop所能表示的最大堆空间是多少?不开呢?

开启指针压缩后,一个oop所能表示的最大堆空间是32 GB,当最大堆大小超过32 GB时,JVM将自动关闭oop压缩。不开启指针压缩后,一个oop所能表示的最大堆空间是4GB。

3、如何扩展开启指针压缩后一个oop所能表示的最大堆空间?

当Java堆大小大于32GB时,也可以使用压缩指针。虽然默认对象对齐是8个字节,但可以使用 -XX:ObjectAlignmentInBytes配置字节值。指定的值应为2的幂,并且必须在8和256的范围内。例如,当对象对齐为16个字节时,通过压缩指针最多可以使用64 GB的堆空间。请注意,随着对齐值的增加,对象之间未使用的空间也会增加。

常用命令

启用指针压缩:-XX:+UseCompressedOops

禁止指针压缩:-XX:-UseCompressedOops


评论
评论
  目录