感慨:
如何定义一个合格的Java程序员,Java程序员要了解掌握哪些知识点,网上的面试题太多了,后端需要了解掌握的知识点太多太多了,Java基础、数据结构、异常、多线程、Spring、Spring boot、事务、算法、数据库(Oracle、MySQL等)、缓存、中间件(各种类型的)、并发异步、消息中间件、微服务、netty(最起码要知道有这个东西吧)、大数据相关(Hive、spark、flink等)、JVM、网络、日志等等等等,太多太多了,每一个单独拿出来都有太多的点了。
所以说,后端Java开发并不是单纯的学习框架+编写业务逻辑,这样简直太简单了,也就是太容易被替代了。
最近问身边很多工作多年的程序员关于jvm相关的知识,基本没有人答得出来,甚至是一些比较浅显的概念点,都说了解这个有什么用呢?实际开发是没有用,但是我还是觉得要深入思考,要考虑底层原理,知其然且知其所以然。
所以一直想写一个关于JVM的文章,不用太复杂但是可以把一些基本的知识概念概括一下,但是jvm的点太多了,我们就说个大概吧,也可以理解为面试知识点向,所以标题是快速入门。
JVM专栏分为以下几个部分:类加载相关、Native相关、PC寄存器(概念)、堆、栈、GC相关。
在Java中,每一个类(.java文件)再通过编译器后,都会形成一个.class文件。
类加载机制指的是将这些.class文件中的二进制数据读入到内存中,并对数据进行校验,解析和初始化。最终,每一个类都会在方法区保存一份它的元数据,在堆中创建一个与之对应的Class对象。
类的加载过程分 5 个阶段:加载、验证、准备、解析、初始化,其中验证、准备、解析可以归纳为连接阶段。如图示:
虽然五个阶段之间有箭头指向,但是并不是严格的按照顺序完成,在类加载过程中,这些阶段会交叉混合执行以完成类的加载及初始化。
加载主要是通过类加载器将.class文件读入内存的过程,主要是完成以下操作:
字面意思很好理解,就是对.class文件进行一系列的校验呗。目的是确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
包括(具体不展开了,稍微了解下吧):
在该阶段会为类的静态字段信息(即被static修饰的变量)分配内存,并且设置初始值。
虚拟机会把这个Class文件中,常量池内的符号引用转换为直接引用。主要解析的是 类或接口、字段、类方法、接口方法、方法类型、方法句柄等符号引用。我们可以把解析阶段中,符号引用转换为直接引用的过程,理解为当前加载的这个类,和它所引用的类,正式进行“连接“的过程。
符号引用就是一个类中(当然不仅是类,还包括类的其他部分,比如方法,字段等),引入了其他的类,可是JVM并不知道引入的其他类在哪里,所以就用唯一符号来代替,等到类加载器去解析的时候,就把符号引用找到那个引用类的地址,这个地址也就是直接引用。
通俗的话来讲就是:
这个步骤是不是与bean生命周期的初始化阶段名称相同?但是其实是完全不同的处理阶段。类加载的初始化过程,就是执行类构造器 ()方法的过程。
类加载初始化完成后,类中static修饰的变量会赋予程序员实际定义的“值”,同时类中如果存在static代码块,也会执行这个静态代码块里面的代码。
class Phli {
static Log log = LogFactory.getLog(); //
private int x = 1; //
X(){
//
}
static {
//
}
}
贴个main方法代码,印证下以上逻辑:
public class Phli {
public String name;
public static void main(String[] args) {
Phli phli1 = new Phli();
Phli phli2 = new Phli();
Phli phli3 = new Phli();
System.out.println(phli1.hashCode());
System.out.println(phli2.hashCode());
System.out.println(phli3.hashCode());
Class extends Phli> aClass1 = phli1.getClass();
Class extends Phli> aClass2 = phli2.getClass();
Class extends Phli> aClass3 = phli3.getClass();
System.out.println(aClass1.hashCode());
System.out.println(aClass2.hashCode());
System.out.println(aClass3.hashCode());
}
}
包括三类加载器(也可以说是四类,多了一个自定义加载器):启动类加载器(根加载器)、扩展类加载器及应用程序类加载器。这三种类加载器是分层的,类似于上下级关系(parent-child关系)。
验证下,在刚才代码基础上在添加以下代码:
ClassLoader classLoader1 = aClass1.getClassLoader();
System.out.println(classLoader1);
ClassLoader classLoader2 = classLoader1.getParent();
System.out.println(classLoader2);
ClassLoader classLoader3 = classLoader2.getParent();
System.out.println(classLoader3);
执行结果:
sun.misc.Launcher$AppClassLoader@b4aac2
sun.misc.Launcher$ExtClassLoader@1615099
null
可以看到classLoader1为AppClassLoader也就是应用类加载器,然后调用它的getParent方法,得到的是ExtClassLoader也就是扩展类加载器,然后我们继续往上获取,结果为null。为什么为null,不应该是启动类加载器吗?
其实很简单,因为启动类加载器是C++实现的,Java程序获取不到~
在此之前我们先看一段代码:
package java.lang;
public class String {
public void test() {
}
public static void main(String[] args) {
String string = new String();
System.out.println("main");
}
}
我们定义了一个package为java.lang的String类,然后其中包括main方法,执行看下结果:
what??明明有main方法为什么错误信息提示找不到main方法呢?这是因为双亲委派导致的。
双亲委派其实很简单,下面一句话就可以解释清楚:
任何一个类加载器在接到一个类的加载请求时,都会先让其父类进行加载,只有父类无法加载(或者没有父类)的情况下,才尝试自己加载。
为什么没找到main方法,因为应用程序类加载器向上让扩展类加载器进行加载,扩展类根据全限定名没加载到,继续往上让启动类加载器加载,启动类加载了我们rt.jar里面的String了,肯定是没有main方法的。
这就是双亲委派,总结一下:
使用双亲委派最大的作用就是:安全,可以保证Java核心类的API不会被随意篡改。
这个更多的是了解是干什么的,熟悉概念。
native首先是个关键字,是一个计算机函数,一个Native Method就是一个Java调用非Java代码的接口。方法的实现由非Java语言实现,比如C或C++。好,概念问题了解到这儿就可以了。
凡是用到native关键字的,就说明Java的作用范围达不到了。
其实平时的业务开发基本用不到,但是它是无处不在的,举几个例子:
public final native Class> getClass();
public native int hashCode();
protected native Object clone() throws CloneNotSupportedException;
JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。 [1]
从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少要保证本地代码能工作在任何Java
虚拟机环境。
通过 JNI,我们就可以通过 Java 程序(代码)调用到操作系统相关的技术实现的库函数,从而与其他技术和系统交互,使用其他技术实现的系统的功能;同时其他技术和系统也可以通过 JNI 提供的相应原生接口开调用 Java 应用系统内部实现的功能。