美文网首页小卜java
JAVA面试汇总(四)JVM(一)

JAVA面试汇总(四)JVM(一)

作者: 汤太咸啊 | 来源:发表于2022-01-18 22:49 被阅读0次
久违的重新写了一篇面试汇总的,关于JVM的一篇,一共三篇,今天写了第一篇,继续重新学习,重新卷起来,come on baby

1.什么情况下会触发类的初始化?

(1)首先是类未被初始化时,创建类的实例(new 的方式),访问某个类或接口的静态变量,或者对该静态变量赋值,调用类的静态方法。
(2)对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
(3)如果在初始化某一个类时候,其父类没有被初始化时候,则会触发父类的初始化。
(4)当咱们打的jar包,在执行完java -jar命令后,用户需要指定一个要执行的主类(包含 main() 方法的那个类,例如SpringBoot的那个启动类的main方法,非SpringBoot的,咱们自己手动打包的一般在MANIFEST.MF文件中指定),虚拟机会先初始化这个主类。
(5)JDK 1.7新增了一种反射方式java.lang.invoke.MethodHandle,通过实MethodHandle同样是访问静态变量,对该静态变量赋值,调用类的静态方法,前提仍然是该类未被初始化。

2.谈谈你对解析与分派的认识。

(1)解析调用是将那些在编译期就完全确定,在类加载的解析阶段就将涉及的符号引用全部转变为可以确定的直接引用,不会延迟到运行期再去完成。
(2)分派又分为静态分派和动态分派
(3)静态分派:同样是将编译期确定的调用,重载(Oveload)就是这种类型,在编译期通过参数的静态类型(注意不是实际类型)作为判断依据,找到具体的调用的方法。

public class TestOverLoad {
    public static void main(String[] args) {
        //静态类型都是Parent,实际类型分别是Sun和Daughter
        Parent sun = new Sun();
        Parent daughter = new Daughter();
        TestOverLoad test = new TestOverLoad();
        //输出结果按照静态类型执行
        test.testMethod(sun);
        test.testMethod(daughter);
    }
    static abstract class Parent { }
    static class Sun extends Parent { }
    static class Daughter extends Parent { }
    public void testMethod(Parent parent) {
        System.out.println("hello, Parent");
    }
    public void testMethod(Sun sun) {
        System.out.println("hello, Sun");
    }
    public void testMethod(Daughter daughter) {
        System.out.println("hello, Daughter");
    }
}

//输出
hello, Parent
hello, Parent

(4)动态分派:运行期根据实际类型确定方法执行版本的分派过程称为动态分派。重写(Override),在运行时期,通过判断实体的真实类型,判断具体执行哪一个方法。

public class TestOverride {
    public static void main(String[] args) {
        //静态类型都是Parent,实际类型分别是Sun和Daughter
        Parent sun = new Sun();
        Parent daughter = new Daughter();
        //这时候输出结果按照实际类型找到方法
        sun.testMethod();
        daughter.testMethod();
    }
    static abstract class Parent {
        public void testMethod() {
            System.out.println("hello, Parent");
        }
    }
    static class Sun extends Parent {
        @Override
        public void testMethod() {
            System.out.println("hello, Sun");
        }
    }
    static class Daughter extends Parent {
        @Override
        public void testMethod() {
            System.out.println("hello, Daughter");
        }
    }
}
//输出
hello, Sun
hello, Daughter

3.Java类加载器包括⼏种?它们之间的⽗⼦关系是怎么样的?双亲委派机制是什么意思?有什么好处?

(1)启动类加载器(Bootstrap ClassLoader),由C语言编写的。负责把<JAVA_HOME>\lib目录中的类库加载到虚拟机内存中。
(2)扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.LauncherExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。 (3)应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.LauncherApp-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
(4)自定义类加载器。下面是自定义类加载器的方式,这个有几点注意,TestDemo编译出来class,把class复制到idea或者eclipse生成target目录之外,因为需要删除掉TestDemo.java,这样target下的class可能也自动没有了,另外如果不删除TestDemo.java会导致一直输出默认的应用程序加载器,因为你运行环境里有,双亲委派的应用程序加载器能找TestDemo,所以默认用父类的了,所以必须删除掉。

package test;

public class TestDemo {
    private String name;
    public TestDemo()
    {
    }
    public TestDemo(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return name;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public String toString()
    {
        return "Demo name is " + name;
    }
}
package test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class TestMyClassLoader extends ClassLoader
{
    public TestMyClassLoader()
    {
    }
    public TestMyClassLoader(ClassLoader parent)
    {
        super(parent);
    }
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        File file = getClassFile(name);
        try
        {
            byte[] bytes = getClassBytes(file);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return super.findClass(name);
    }
    private File getClassFile(String name)
    {
        //重点是这个路径,在本地编译好TestDemo后,把class放在一个其他路径下
        //不要默认用idea或者eclipse的target路径
        //注意运行这个类之前把代码的TestDemo.java删除掉或者注释掉
        //否则怎么运行都是默认的加载器AppClassLoader
        File file = new File("/Users/buxuesong/TestDemo.class");
        return file;
    }
    private byte[] getClassBytes(File file) throws Exception
    {
        // 这里要读入.class的字节,因此要使用字节流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }
    public static void main(String[] args) throws Exception
    {
        TestMyClassLoader mcl = new TestMyClassLoader();
        Class<?> c1 = Class.forName("test.TestDemo", true, mcl);
        Object obj = c1.newInstance();
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}
//输出
Demo name is null
test.TestMyClassLoader@5cad8086
//如果没删除TestDemo.java输出
Demo name is null
sun.misc.Launcher$AppClassLoader@18b4aac2

(5)扩展类加载器的父类是启动类加载器,应用程序类加载器的父类是扩展类加载器,自定义类加载器的父类是应用程序类加载器。
(6)双亲委派机制:除了启动类加载器,其余加载器都应该有自己的父类加载器,当一个类加载器需要加载某个类时,默认把这个累交给自己的父类去加载,只有当父类无法加载这个类时候(它的搜索范围中没有找到所需的类),自己才去加载,按照这个规则,所有的累加载最终都会到启动类加载器过一遍。
(7)双亲委派实际上保障了Java程序的稳定运作,因为随着这种父类关系自带了一种层级关系,按照层级关系来分别加载,如果不按照顺序各个加载器自行加载,用户如果自己写了一个java. lang.Object的类,系统会出现多个Object类,导致整个java体系无法运转。

4.如何⾃定义⼀个类加载器?你使⽤过哪些或者你在什么场景下需要⼀个⾃定义的类加载器吗?

(1)这问题问了我上面的,我在写一遍,自定义类加载器,有几点注意,TestDemo编译出来class,把class复制到idea或者eclipse生成target目录之外,因为需要删除掉TestDemo.java,这样target下的class可能也自动没有了,另外如果不删除TestDemo.java会导致一直输出默认的应用程序加载器,因为你运行环境里有,双亲委派的应用程序加载器能找TestDemo,所以默认用父类的了,所以必须删除掉。

package test;

public class TestDemo {
    private String name;
    public TestDemo()
    {
    }
    public TestDemo(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return name;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public String toString()
    {
        return "Demo name is " + name;
    }
}
package test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class TestMyClassLoader extends ClassLoader
{
    public TestMyClassLoader()
    {
    }
    public TestMyClassLoader(ClassLoader parent)
    {
        super(parent);
    }
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        File file = getClassFile(name);
        try
        {
            byte[] bytes = getClassBytes(file);
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return super.findClass(name);
    }
    private File getClassFile(String name)
    {
        //重点是这个路径,在本地编译好TestDemo后,把class放在一个其他路径下
        //不要默认用idea或者eclipse的target路径
        //注意运行这个类之前把代码的TestDemo.java删除掉或者注释掉
        //否则怎么运行都是默认的加载器AppClassLoader
        File file = new File("/Users/buxuesong/TestDemo.class");
        return file;
    }
    private byte[] getClassBytes(File file) throws Exception
    {
        // 这里要读入.class的字节,因此要使用字节流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }
    public static void main(String[] args) throws Exception
    {
        TestMyClassLoader mcl = new TestMyClassLoader();
        Class<?> c1 = Class.forName("test.TestDemo", true, mcl);
        Object obj = c1.newInstance();
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}
//输出
Demo name is null
test.TestMyClassLoader@5cad8086
//如果没删除TestDemo.java输出
Demo name is null
sun.misc.Launcher$AppClassLoader@18b4aac2

(2)我们之前写的获取数据库连接,通过class.forname去加载数据库驱动。以及热加载这种方式,咱们修改了java文件,但是tomcat没有手动重启,这个时候有一个能够监控到java有变化重新编译了的情况,通过线程出发tomcat重启,就达到了热加载的机制。还有就是apk加密的方式,打包时候,源码-》class-》加密-》打成jar包-》安装-》运行-》classLoader解密-》classLoader加载-》用户使用app,这样只有实现解密方法的classloader才能正常加载,其他的classLoader无法运行。

5.堆内存设置的参数是什么?

(1)-Xms初始堆内存大小
(2)-Xmx最大堆内存大小,生产环境中,JVM的Xms和Xmx建议设置成一样的,能够避免GC时还要调整堆大小。
(3)-XX:NewSize=n,设置年轻代大小-XX:NewRatio=n设置年轻代和年老代的比值。如:-XX:NewRatio=3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4,默认新生代和老年代的比例=1:2
(4)-XX:SurvivorRatio=n,设置年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个,默认是8,表示Eden:S0:S1=8:1:1如:-XX:SurvivorRatio=3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5。
(5)-XX:+HeapDumpOnOutOfMemoryError,-XX:HeapDumpPath,这两个是设置但程序OOM后,输出Dump文件以供分析原因的,但是对于目前的K8s的情况,这俩没什么用,OOM之后,K8s发现服务没响应,直接kill了,然后重启一个新的,OOM根本就来不及生成,因为生成文件耗时较多,K8s杀的很快。
(6)-Xss128k 设置每个线程的堆栈大小。
(7)-XX:+PrintGCDetails,输出GC日志。

感谢各位的阅读,帮忙点赞,感谢各位。

相关文章

网友评论

    本文标题:JAVA面试汇总(四)JVM(一)

    本文链接:https://www.haomeiwen.com/subject/ioulhrtx.html