返回
Featured image of post Java - JVM 问题集合 (上)

Java - JVM 问题集合 (上)

翻了几个题库,结合以前笔记整理了JVM相关一些题目的答案.

JVM 内存结构、类加载相关问题

HotSpot 是什么

使用最广泛的 JVM,OpenJDK SunJDK 的 JVM。最早不是 Java 的虚拟机,在 JIT 方面具有一些先进的理念,被 SUN 收购。


堆和栈区别是什么

个人理解,不一定全面

公有 线程私有
存放绝大多数对象实例、数组 存放局部变量表、操作数栈、动态链接、方法出口等
GC的主要场所 不需要GC,栈帧随方法调用结束而被弹出
生老病死与JVM紧密相关 生老病死与线程、函数调用紧密相关
不能动态扩容 可以通过配置动态扩容

哪块内存区不会OOM

公有部分,会OOM;方法区,虽然搬到了原生内存上,但还是有可能OOM的;JVM外的直接内存,肯定是可能OOM的。因此公有区全部有可能OOM。

线程私有部分本地方法栈,为 Native 方法服务,也有OOM可能;虚拟机栈,OOM常客,允许动态扩容时会OOM,不允许则会 OverFlow,因此是会OOM的;程序寄存器,唯一不会出现OOM的区域,就是它


一个线程OOM后,其它线程能否运行

无论哪种OOM,一个线程OOM后,所占据内存资源会被GC全部释放。无论是线程共享的堆内存,还是不共享的栈内存。

因此,一个线程的OOM不会影响到其它线程运行,即使主线程抛异常退出,子线程仍能继续,除非子线程是主线程的守护线程。


对象一定是在堆上分配的吗

不一定。JVM通过逃逸分析,能够分析新对象的适用范围,以此确定是否要将对象分配到堆上,对于没有逃逸出方法的对象,很可能会被优先在栈上进行分配。

其实就像我们平常用Spring,有必要将所有对象都注册成Bean吗?肯定没必要,JVM也不傻,并非所有对象都会分配到堆上,毕竟GC也是有开销的。


为什么说一次编译,处处执行

两次编译机制,第一次由编译器编译成字节码,第二次由JVM转换到机器码,实际上是解释执行机制,解释工作由本地JVM完成,JVM担任了本地向导的责任,因此不是“处处执行”,是有本地向导,装了JVM的地方执行。


如何动态生成一个类

  1. 操作字节码

    这个在 Spring AOP ,尤其是 AspectJ 中有应用,主要通过 CGLIB 和 Javassist 两种库实现。理论可行,实际操作……非常麻烦,首先要研究清楚JVM的字节码规范,详细了解class结构,难度S++。

  2. 使用第三方Jar包

    com.itranswarp.compiler 可以用来编写自己的工具类,从而实现动态生成类。

  3. 一些脚本语言

    最典型的是 Groovy,它原生支持动态生成对象;还有在大数据领域用的很多的 Scala;谷歌的 Aviator 。这些支持动态生成类。


Class.forName()ClassLoader.loadClass 的区别

Class.forName() 除了将.class文件加载到JVM中,会对类进行解释,通过参数决定是否对加载的类进行初始化,从而是否执行类中的静态块,是否对静态变量进行赋值。

ClassLoader使用双亲委派模型只干一件事——将.class文件加载到JVM中,不执行静态块,在newInstance时才会执行静态块。Class.forName() 类加载也是通过 ClassLoader 实现。


字符串相关

包括new String("xx")创建多少个对象,两个字符串是否相等,intern()之后相不相等,这些会有一篇专门讲字符串常量池的笔记,放在这里太多了。


Java 8 内存结构的变化

最重要的就是元空间取代了永久代成为方法区的新实现,这一改变是为了减少永久代OOM的概率,因为永久代位于堆上,而它的GC机制又十分复杂,很容易造成性能劣化或OOM。


GC相关、JVM 内存结构

GC之前有过了,内存结构也会单独整理。


双亲委派机制

https://www.jianshu.com/p/1e4011617650

当某个类加载器需要加载.class文件,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此如果一个类确实没有被加载过,所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查这个classsh是否已经加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // c==null表示没有加载,如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果父类的加载器为空 则说明递归到bootStrapClassloader了
                        //bootStrapClassloader比较特殊无法通过get获取
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {}
                if (c == null) {
                    //如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

源码显示,直到递归到bootStrapClassloader,如果仍没有加载过,就尝试自己去加载class

这一机制的作用是防止重复加载同一.class,同时保证核心.class不能被篡改。通过委托方式,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

类加载器的类别

BootstrapClassLoader 是顶层类加载器,唯一使用C++编写的类加载器,是JVM的一部分,加载核心库java.*(准确的说是负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数指定的路径中存放的,且是JVM能够识别的类库),同时构造ExtClassLoaderAppClassLoader。涉及虚拟机本地实现细节,开发者无法直接获取启动类加载器引用。

BootstrapClassLoader外的其它类加载器都由Java语言实现,独立存在于JVM外部,且都继承于java.lang.Classloader抽象类。

ExtClassLoader 标准扩展类加载器,加载扩展库(<JAVA_HOME>\home\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库),如classpath中的jre, javax.*java.ext.dir指定位置中的类,可以直接使用。

AppClassLoader 系统类加载器,加载程序所在目录,如user.dir所在位置class。开发者同样可以直接使用此类加载器,如果没有指定,则默认使用此类加载器。


为什么打破双亲委派机制

双亲委派机制解决了基础类统一问题(越基础的类由越上级的加载器加载),但是如果需要启动类加载器加载用户代码,需要调用独立厂商实现的ClassPath下SPI代码,则无法解决,因为启动类不认识这些代码。为此,大部分SPI都通过增加一个线程上下文加载器加载所需的SPI代码,造成了父类加载器请求子类加载器完成类加载,从而打破了双亲委派机制。

Tomcat 也违背了双亲委托机制,因为默认类加载器无法加载两个相同类库不同版本,做不到隔离,无法对JDP做到热修改,因为JSP也是class文件,类名不会变。双亲委派机制绑死了类名。

为此,Tomcat的做法是每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus