JVM简介
JVM是java虚拟机简称,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际计算机上仿真模拟各种计算机功能来实现的。也正式因为有了它,java才具有了跨平台特性,”一次编译,到处运行“,每一个操作系统都有对应的JVM,如windows版JVM、linux版JVM等。
1 java代码运行过程
我们用eclipse或者ideal编写的代码是.java文件,也是我们通常所称的源代码。源代码编写之后需要经过JVM进行编译,编译成.class文件,也称字节码。Java程序在运行的时候,JVM先将磁盘上的字节码加载到内存,然后再转换成对象也就是对象实例。所以一次完整的java应用程序开发、打包、运行会经过四个阶段。java源文件、jar包(字节码文件)、JVM加载到内存、java对象(对象实例),分别对应java代码编写阶段、java编译阶段、java运行阶段。
2 java字节码
所有的.class文件的前4个字节都是魔数,魔数以一个固定值:0xCAFEBABE,放在文件的开头,JVM就可以根据这个文件的开头来判断这个文件是否可能是一个.class文件,如果是以这个开头,才会往后执行下面的操作,这个魔数的固定值是Java之父James Gosling指定的,意为CafeBabe(咖啡宝贝)。紧随着魔数之后的4个字节就是版本号,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version),如52则标识JDK8。再然后就是常量池、访问标志等。
java虚拟机是基于栈的。java字节码是由单字节指令构成,理论上最多256个,但实际应用大概只有200个左右。操作一般分为四大类型。
- 栈操作指令,包括与局部变量交互的指令
- 虚拟机本身结构需要的,和虚拟机对应
- 程序流程控制指令 对象操作指令,包括方法调用指令
- 算术运算以及类型转换指令
java虚拟机在运行一段代码的时候,会把所有的用到的变量存到本地变量表也叫局部变量表,需要使用的时候就通过load指令加载到栈上来,运算结束后再存到局部表量表。如果需要把class的文件常量池打印出来就需要通过javap-verbose xx.class,打印出常量池。局部变量名称可能在编译的时候就全部丢失掉了,所以反编译的时候经常会看到局部变量名称是v1、v2这样,如果想要保留局部变量名称,则需要在编译的时候加一个参数即javac-g xx.java。这样在javap -verbose xx.class查看class文件时就能看到局部变量真实的名称了.
JVM是一台基于栈的计算机器。每个线程都有独属于自己的线程栈(JVM Stack),用于存储栈帧(Frame)。每一次方法的调用,JVM都会创建一个栈帧。栈帧时由操作数栈、局部变量数组及一个class引用组成。class引用指向当前方法在运行时常量池中对应的class。JVM方法调用的指令主要有四种:
- invokestatic:顾名思义,这个指令用于调用某个类的静态方法,这是方法调用指令中最快的一个
- invokespecial:用于调用构造函数,但也可以用于调用同一个类- 中的private方法以及可见的超类方法。
- invokevirtual:如果是具体类型的目标对象,invokevirtual用于调用公共、受保护和package级的私有方法。
- invokeinterface:当通过接口引用来调用方法时,将会编译为- invokeinterface指令
- invokedynamic:JDK7新增加的指令,是实现”动态类型语言“(Dynamically Typed Language)支持而进行的升级改进,同时也是JDK8以后支持lambda表达式的实现基础。
3 jvm类加载器
类加载器体现的类的加载和卸载。类的生命周期分为加载(找class文件,由类jiava载器完成)、验证(验证格式、依赖)、准备(静态字段、方法表)、解析(符号解析为引用)、初始化(构造器、静态变量赋值、静态代码块)、使用、卸载七个步骤。前5个步骤合起来就是类的加载过程,把二三四阶段统称为链接阶段。
3.1类加载器介绍
类加载器主要就是负责类的加载职责,对于任意一个class,都需要由加载它的类加载器和这个类本身确立其在JVM中的唯一性。JVM为我们提供了三大内置的类加载器,分别是启动类加载器(BootstrapClassLoader)、扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader),不同的类加载器负责将不同的类加载到JVM内存之中,并且他们严格遵守父委托机制。加载器有三大特点:双亲委托、负责依赖、缓存加载。应用类公用的类可以放在扩展类加载器路径之上,这样应用在启动的时候就不需要重复添加依赖。
类加载器的第一个特点是双亲委托,指的就是当应用加载器加载一个类的时候,自己不加载,而是先委托给父类加载器进行加载,如果父类加载器也不加载就会一直向上委托直到启动类加载器,如果启动类加载器加载了就会直接将引用返回给子类。如果父类加载器都没有加载,则由子类加载器自己负责加载,并返回引用。
类加载器的第二个特点是负责依赖。例如类加载器在加载一个类的时候,发现这个类还依赖另外两个类,则该加载器也会加载依赖的两个类。
类加载器的第三个特点就是缓存加载。类加载的时候默认只会加载一次,加载之后就会缓存在内存里,下次使用的时候直接在内存拿就可以,就不会重复加载,提高效率。
最上一层的加载器是根加载器,又称为Bootstrap类加载器,该类加载器是最为顶层的加载器,其没有任何父加载器,它是由C++编写的,主要负责虚拟机核心库类的加载,比如说java.lang包就是由根加载器加载的。要验证根加载器加载了哪些jar包,可以通过如下代码实现。根加载器是获取不到引用的,所以第一行会输出null。
package base.classloader;
public class BootStrapClassLoader {
public static void main(String args[]) {
System.out.println("Bootstrap:"+String.class.getClassLoader());
System.out.println(System.getProperty("sun.boot.class.path"));
}
}
第二层的加载器是扩展类加载器,它主要用于加载JAVA_HOME下的jre\lib\ext子目录下的类库。扩展类加载器是纯java实现的,它是java.lang.URLClassLoader的子类,它的完整类名是sun.misc.Launcher$ExtClassLoader。扩展类加载器所加载的类库可以通过java.ext.dirs获得。我们也可以将自己的类打成jar包,放到扩展类加载器所在的路径,扩展类加载器就会负责加载我们所需要的jar包。
package base.classloader;
public class BootStrapClassLoader {
public static void main(String args[]) {
System.out.println(System.getProperty("java.ext.dirs"));
}
}
第三层是系统类加载器也就是ApplicationClassLoader,其负责加载classpath下的类库资源。我们在进行项目开发的时候引入的第三方jar包,系统类加载器的父加载器是扩展类加载器,同时它也是自定义类加载器的默认加载器,系统类加载器一般通过-classpath或者-cp指定,同样也可以通过系统属性java.class.path进行获取。
package base.classloader;
public class ApplicationClassLoader {
public static void main(String args[]) {
System.out.println(System.getProperty("java.class.path"));
System.out.println(ApplicationClassLoader.class.getClassLoader());
}
}
也可以通过一段代码,打印出根加载器、扩展加载器、应用加载器分别加载了哪些jar包。在实际业务开展过程中,如果我们发现系统加载的代码和我们预计的代码不一致,我们也可以通过下面这段代码,打印出我们根加载器、扩展加载器、应用加载器分别加载了哪些jar包,验证和预计结构是否一致。
package jeekdemo.part1;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
public class JvmClassLoaderPrintPath {
public static void main(String args[]) {
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
System.out.println("启动类加载器");
for(URL url:urls) {
System.out.println("===>"+url.toExternalForm());
}
printClassLoader("扩展类加载器",JvmClassLoaderPrintPath.class.getClassLoader().getParent());
printClassLoader("应用类加载器",JvmClassLoaderPrintPath.class.getClassLoader());
}
private static void printClassLoader(String string, ClassLoader parent) {
System.out.println();
if(null != parent) {
System.out.println(string+" ClassLoader==>"+parent.toString());
printURLForClassLoader(parent);
}else {
System.out.println(string + " ClassLoader ==> null");
}
}
private static void printURLForClassLoader(ClassLoader classLoader) {
// TODO Auto-generated method stub
Object ucp = insightField(classLoader,"ucp");
Object path = insightField(ucp,"path");
List paths = (List)path;
for(Object p:paths) {
System.out.println("===>"+p.toString());
}
}
private static Object insightField(Object obj, String fName) {
Field f = null;
try {
if(obj instanceof URLClassLoader) {
f = URLClassLoader.class.getDeclaredField(fName);
}else {
f = obj.getClass().getDeclaredField(fName);
}
f.setAccessible(true);
return f.get(obj);
}catch(Exception e) {
e.printStackTrace();
return null;
}
}
}
3.2 自定义类加载器
场景一:我们实现的所有自定义类加载器都是ClassLoader的直接或间接子类,java.lang.ClassLoader是一个抽象类,它里面并没有抽象方法,但是有findClass方法,在实现自定义类加载器的时候就需要实现findClass方法。自定义类加载器的实现代码如下:
package base.classloader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/*
* 自定义的类加载器,在加载器里定义了加载的默认路径
* 也支持用户自定义类文件夹路径
*/
public class MyClassLoader extends ClassLoader{
private final static Path DEFAULT_CLASS_DIR= Paths.get("E:\\classloader1");
private final Path classDir;
public MyClassLoader() {
super();
this.classDir = DEFAULT_CLASS_DIR;
}
public MyClassLoader(String classDir) {
super();
this.classDir = Paths.get(classDir);
}
public MyClassLoader(String classDir,ClassLoader parent) {
super(parent);
this.classDir = Paths.get(classDir);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
byte[] classBytes = this.readClassBytes(name);
if(null == classBytes || classBytes.length == 0) {
throw new ClassNotFoundException("Can not load the class "+name);
}
return this.defineClass(name, classBytes, 0,classBytes.length);
}
private byte[] readClassBytes(String name) throws ClassNotFoundException{
String classPath = name.replace(".", "/");
Path classFullPath = classDir.resolve(Paths.get(classPath+".class"));
if(!classFullPath.toFile().exists()) {
throw new ClassNotFoundException("The class "+name+" not found.");
}
try(ByteArrayOutputStream baos = new ByteArrayOutputStream()){
Files.copy(classFullPath, baos);
return baos.toByteArray();
}catch(IOException e) {
throw new ClassNotFoundException("load the class "+name+" occurerror.",e);
}
}
@Override
public String toString() {
return "My ClassLoader";
}
}
package base.classloader;
import java.lang.reflect.Method;
/*
* 由于双亲委托机制,
* 如果base.classloader.HelloWorld在当前工作空间
* 一定要删掉class和java文件,不然系统在运行的时候
* 加载HelloWord的加载器将会是应用加载器,
* 而不是自定义的类加载器
*/
public class MyClassLoaderTest {
public static void main(String args[]) throws ClassNotFoundException,Exception {
MyClassLoader classLoader = new MyClassLoader();
Class<?> aClass = classLoader.findClass("base.classloader.HelloWorld");
System.out.println(aClass.getClassLoader());
Object helloWorld = aClass.newInstance();
System.out.println(helloWorld);
Method welcomeMethod = aClass.getMethod("welcome");
String result = (String) welcomeMethod.invoke(helloWorld);
System.out.println("Result:"+result);
}
}
package base.classloader;
public class HelloWorld {
static {
System.out.println("Hello World Class is Initialized");
}
public String welcome() {
return "Hello World";
}
}
场景二:自定义一个加载器加载加密后的class文件
package base.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
public class CustomClassLoaderTest extends ClassLoader{
public static void main(String args[]) throws Exception {
String path = "E:\\cosmic40\\bos-dev-tool\\debug-service\\javademo\\target\\classes\\base\\classloader\\Hello.class";
/*String str = ConvertClass2Str(path);
byte[] fileByte = str.getBytes();*/
byte[] fileByte = ConvertClass2Str(path);
//这里可以增加一些代码解密的逻辑
CustomClassLoaderTest test = new CustomClassLoaderTest();
Class<?> aclass = test.defineClass("base.classloader.Hello",fileByte,0,fileByte.length);
aclass.newInstance();
System.out.println(aclass.getClassLoader());
}
public static byte[] ConvertClass2Str(String path) throws Exception {
File file = new File(path);
byte[] bfile = Files.readAllBytes(file.toPath());
/*String str = new String(bfile);//转为数组
return str;*/
//这里可以添加一些代码加密逻辑
return bfile;
}
}
package base.classloader;
public class Hello {
static {
System.out.println("hello world");
}
}
3.3 类加载双亲委托机制
jvm在加载类的时候会优先委托父类加载,如果父类不能加载的时候才是自己加载,这就是所谓的双亲委托机制,也称为父委托机制。当一个类加载器被调用了loadClass之后并不会直接将其加载,而是先交给当前类加载器的父类加载器尝试加载直到最顶层的父加载器,然后再依次向下加载。
如果想要用自定义类加载器加载class文件,又不希望在原工程里删除Hello的java文件和class文件,可以采用两种方式来解决。第一,将自定义的类加载器的父加载器指定为扩展类加载器;第二,将自定义类加载器的父加载器定义为null。
3.4 破坏双亲委托机制
研究jdk源码,我们发现类加载器的父委托机制的逻辑主要是由loadClass来控制,有时候由于业务的需要也需要打破这种双亲委托的机制。例如我们想要在程序运行时进行某个模块功能的升级,甚至是在不停止服务的前提下增加新的功能,这就是我们常说的热部署。热部署首先要卸掉加载该模块所有Class类加载器,卸载类加载器会导致所有类的卸载,很显然不能对JVM三大内置的加载器进行卸载,我们只有通过控制自定义类加载器才能做到。前面介绍的用自定义类加载器加载HelloWord类的时候,采用的策略是绕过ApplicationClassLoader的方式去实现,但并没有避免一层一层的委托。实际上,双亲委托机制不是强制性的,我们可以灵活的破坏这种双亲委托机制。
package base.classloader;
import java.lang.reflect.Method;
/*
*双亲委托机制的破坏案例
*/
public class BrokerClassLoaderTest {
public static void main(String args[]) throws ClassNotFoundException,Exception {
BrokerDelegateClassLoader classLoader = new BrokerDelegateClassLoader();
//Class<?> aClass = classLoader.findClass("base.classloader.HelloWorld");
Class<?> aClass = classLoader.loadClass("base.classloader.HelloWorld");
System.out.println(aClass.getClassLoader());
Object helloWorld = aClass.newInstance();
System.out.println(helloWorld);
Method welcomeMethod = aClass.getMethod("welcome");
String result = (String) welcomeMethod.invoke(helloWorld);
System.out.println("Result:"+result);
}
}
//破坏双亲委托加载器代码
package base.classloader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class BrokerDelegateClassLoader extends ClassLoader{
private final static Path DEFAULT_CLASS_DIR= Paths.get("E:\\classloader1");
private final Path classDir;
public BrokerDelegateClassLoader() {
super();
this.classDir = DEFAULT_CLASS_DIR;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
byte[] classBytes = this.readClassBytes(name);
if(null == classBytes || classBytes.length == 0) {
throw new ClassNotFoundException("Can not load the class "+name);
}
return this.defineClass(name, classBytes, 0,classBytes.length);
}
private byte[] readClassBytes(String name) throws ClassNotFoundException{
String classPath = name.replace(".", "/");
Path classFullPath = classDir.resolve(Paths.get(classPath+".class"));
if(!classFullPath.toFile().exists()) {
throw new ClassNotFoundException("The class "+name+" not found.");
}
try(ByteArrayOutputStream baos = new ByteArrayOutputStream()){
Files.copy(classFullPath, baos);
return baos.toByteArray();
}catch(IOException e) {
throw new ClassNotFoundException("load the class "+name+" occurerror.",e);
}
}
@Override
protected Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException{
if(name.equalsIgnoreCase("HelloWorld")) {
System.out.println("demo");
}
synchronized(getClassLoadingLock(name)) {
Class<?> klass = findLoadedClass(name);
if(klass == null) {
if(name.startsWith("java.") || name.startsWith("javax")) {
try {
klass = getSystemClassLoader().loadClass(name);
}catch(Exception e) {
}
}else {
try {
klass = this.findClass(name);
}catch(ClassNotFoundException e) {
}
if(klass == null) {
if(getParent()!=null) {
klass = getParent().loadClass(name);
}else {
klass = getSystemClassLoader().loadClass(name);
}
}
}
}
if(null == klass) {
throw new ClassNotFoundException("The class "+name+" not found.");
}
if(resolve) {
resolveClass(klass);
}
return klass;
}
}
}
package base.classloader;
public class HelloWorld {
static {
System.out.println("Hello World Class is Initialized");
}
public String welcome() {
return "Hello World";
}
}
loadClass有几个核心要点:
- 加载的时候首先要对全路径名称进行加锁,确保每一个类在多线程的情况下只能被加载一次
- 在加载类的缓存中查看该类是不是已经被加载,如果已加载则直接返回
- 若缓存中没有被加载的类,则需要对其进行首次加载
- 如果类不是以java或者javax开头,则用自定义类加载器加载
- 如果都没有加载成功,则抛出异常
3.5 类加载器命名空间、运行时包、类的加载、卸载等
3.5.1 类加载器命名空间
每一个类加载器实例都有各自的命名空间,命名空间是由该加载器及其所有父加载器所工构成的,因此在每个类加载器中同一个class都是独一无二的,类加载器命名空间代码如下。
package base.classloader;
public class NameSpace {
public static void main(String args[]) throws ClassNotFoundException {
ClassLoader classLoader = NameSpace.class.getClassLoader();
Class<?> aClass = classLoader.loadClass("base.classloader.HelloWorld");
Class<?> bClass = classLoader.loadClass("base.classloader.HelloWorld");
System.out.println(classLoader.toString());
System.out.println(aClass);
System.out.println(bClass);
System.out.println(aClass == bClass);
}
}
最后输出的结果是true,也就是不管load多少次,最后返回的都是同一份class对象,aclass和bclass只是对class的引用。但是,使用不同的类加载器,或者同一个类加载器的不同实例,去加载同一个class,则会在堆内存和方法区产生多个class的对象。所以,同一个class实例只能在同一个类加载器命名空间之下是唯一的。
3.5.2 运行时包
在编码代码的时候通常会给一个类指定一个包名,包的作用是为了组织类,防止不同包下同样名称的class引起冲突,还能起到封装的作用,包名和类名构成了类的全限定名称。在JVM运行时class会有一个运行时包,运行时的包是由类加载器的命名空间和类的全限定名称共同组成。
3.5.3 初始类加载器
由于运行时包的存在,JVM规定了不同的运行时包下的类彼此之间不可以进行访问。但实际上我们的业务代码是由应用加载器加载的,但类似String是由根加载器加载的,虽然是不同的类加载加载,但依然我们能够正常的引用。这是因为每一个类在经过ClassLoader加载之后,在虚拟机中都会有对应的class实例,如果某个类C被类加载器CL加载,那么CL就被称为C的初始类加载器。JVM为每一个类加载器维护了一个列表,该列表中记录了将该类加载器作为初始类加载器的所有class,在加载一个类时,JVM使用这些列表来判断该类是否已经被加载过,是否需要首次加载。根据JVM规范的规定,在类的加载过程中,所有参与的类加载器,即使没有亲自加载过该类,也都会被标识为该类的初始加载器,例如一个类经过自定义加载器、应用加载器、扩展加载器、根加载器,那这些类都是该类的初始类加载器,JVM会为每个类加载器的列表中添加该类class类型。所以,类似String对象,虽然是由根加载器加载,但我们也能正常的引用。
3.5.4 类初始化时机
- 当虚拟机启动时,初始化用户指定的主类,就是启动main方法的类
- 当使用new指令时,初始new指定的目标类,也就是new一个类的时候就要初始化
- 当调用静态方法或者访问静态字段时,需要初始化静态方法或静态字段所在的类
- 子类的初始化会触发父类的初始化
- 如果一个接口定义了defalut方法,则直接或间接实现该接口的类的初始化也会触发接口的初始化
- 使用反射API对某个类进行反射调用时,会初始化该类
- 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在类(动态调用)
3.5.5 类只加载不初始化
- 通过子类引用父类的静态字段,只会触发父类的初始化不会引起子类的初始化
- 定义对象数组,不会触发该类的初始化
- 常量在编译器会存入调用类的常量池中,本质上并没有直接引用- 定义常量的类,所以不会触发定义常量所在的类。
- 通过类名获取class对象,不会触发类的初始化,例如A.class不会触发A类的初始化
- 通过class.forName()加载指定类时,如果指定initialize参数的值为false,它也不会触发初始化,当然一般默认都是初始化。
- 通过classloader默认的loadclass方法,也不会触发初始化,相当于是加载了但并没有初始化,具体可以查看源码的loadclass方法,传入的参数为false。
3.5.6 类的卸载
在JVM启动过程中,JVM会加载很多类,在运行期间同样也会加载很多类。JVM规定了一个Class只有满足三个条件才能被GC回收,也就是类卸载。
- 该类所有的实例都已经被GC,比如Demo.class的所有Demo实例都被回收掉
- 加载该类的ClassLoader实例被回收
- 该类的class实例没有在其他地方被引用
3.6 双亲委派机制缺陷
JDK的核心库提供了很多SPI(Service Provider Interface),常见的SPI包括JDBC、JCE、JNDI、JAXP和JBI等,JDK只规定了这些接口之间的逻辑关系,但不提供具体实现。具体的实现是由第三方厂商来提供。例如JDBC,java使用JDBC这个SPI完全透明了应用程序和第三方厂商数据库驱动的具体实现,不管数据库类型如何切换,应用程序只需要替换JDBC的驱动jar包以及数据库的驱动名称即可,而不用做任何的更新。
这样做的好处是JDBC提供了高度抽象,应用则只需要面向接口编程即可,不用关心各大数据库厂商提供的具体实现。但问题在于java.sql中的所有接口都是由JDK提供的,加载这些接口的类加载器是根加载器,第三方厂商提供的类驱动则是由系统类加载器加载的,由于JVM类加载器的双亲委托机制,比如Connection、Statement、RowSet等都是由根加载器加载,第三方的JDBC驱动包中的实现不会被加载。例如mysql的jdbc源码,我们可以看到mysql包中的Dirver实现了java.sql.Driver,而java.sql.Driver是由根加载器加载的,mysql的Driver是属于第三方服务,由应用加载器加载。因此,我们可以看到在mysql的Driver类的静态代码块,需要首先将Driver注册到DriverManager中,再调用DriverManager的getConnection等方法时,会调用当前的线程加载器去加载Driver,也就是说父委托变成了子委托,也就打破了双亲委托模型,也就相当于是绕了一个大圈。在实际开发过程中,一般不要轻易破坏双亲委托机制。
4 JVM内存结构
JVM虚拟机在运行的时候,每个线程都有自己独立的线程栈,并且只能访问自己的线程栈,每个线程都不能访问其他线程的局部变量(方法内变量)。所有原生类型的局部变量都存储在线程栈中,因此对其他线程都是不可见的。如果两个线程需要共享局部变量,线程可以将一个原生变量值的副本传给另外一个线程,但不能共享原生局部变量本身。堆内存中就包括了java代码中创建的所有对象,不管是哪个线程创建的,这其中也包括了包装类型如Byte、Integer、Long等。不管是创建一个对象并将其赋值给局部变量,还是赋值给另外一个对象的成员变量,创建的对象都是会保存在堆中。
如果局部变量是原生类型的,那么它的全部内容就全部保留在线程栈上。如果是对象的引用,则栈中的局部变量槽位中保存着对象的引用地址,而实际的对象内容保存在堆中。针对对象本身来讲,对象的成员变量和对象本身都是一起存在堆上,不管其成员变量是原生数值还是对象的引用。此外,类的静态变量和类定义一样也是保存在堆中。
总之,方法中使用的原生数据类型和对象引用地址存储在栈上,对象、对象成员与类定义、静态变量在堆上。堆内存也称为共享堆,堆中的所有对象都可以被所有线程访问,只要他们能拿到对象的引用地址。如果一个线程可以访问某个对象,则也可以访问该对象的成员变量。如果两个线程同时调用某个对象的同一个方法,则他们都可以访问到对象的成员变量,但每个线程的局部变量副本是独立的。
JVM每启动一个线程,JVM就会在栈空间分配对应的线程栈,线程栈也叫java的方法栈。如果使用JNI方法,则会分配一个单独的本地方法栈(Native Stack)。线程栈会包括多个栈帧。线程执行过程中,一般会有多个方法组成调用栈,如A调用B,B调用C,每执行一个方法就会创建一个栈帧。栈帧是一个逻辑上的概念,包括局部变量表、操作数栈等,具体大小在一个方法编写完成后基本上就能确定,比如返回值需要有一个空间存放,每个局部变量都需要对应的地址空间,此外还有给指令使用的操作数栈,以及class指针(标识这个栈帧对应的是哪个类的方法,指向非堆里面的Class对象)。
堆内存是所有线程共享共用的内存空间,JVM将堆内存分为年轻代和老年代两个部分。年轻代划分为3个内存池,新生代(Eden区)和存活区(S0和S1),S0和S1总有一个是空的。
还有一块内存区域是非堆(Non-Heap),它本质上也是堆,只不过不归GC管,它里面主要包括三个部分分别是Metaspace(元数据区,包括常量池等)、CSS(存放class信息)、Code Cache(存放JIT编译器编译后的本地机器代码)。
JVM自己在运行的时候也需要占用一定的内存,所以在启动java进程调整内存的时候要把这一步空间预留出来,一般情况可以把JVM内存设置成物理内存的60%到80%之间,不然很容易发生OOM异常。
5 JMM内存模型
JMM主要是用于屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。JMM规范了Java虚拟机与计算机内存是如何协同工作的,规定了一个线程如何和何时可以看到由其他线程修改后的共享变量的值,以及在必须的时候如何同步的访问共享变量。
- 从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系。
- 线程之间的共享变量存储在主内存中
- 每个线程都有一个私有的本地内存,本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。
- 从更低的层次来说,主内存就是硬件的内存,为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
- JMM中线程的工作内存是CPU的寄存器和高速缓存的抽象描述。而JVM的静态内存存储模型只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM内存中。
java进程是操作系统众多进程中的一个。JMM就是java进程的内存模型。JMM主要包括栈、堆、非堆、JVM本身运行所需内存四大部分。其中java堆是占比最大的,也是内存回收最频繁的区域。
6 JVM启动参数
JVM预置了很多的参数,大概有一千多个,为了使JVM在各种不同场景运行的更高效,我们可以调整JVM的启动参数,把JVM调校成适合当前程序运行的状态。
-server
-Dfile.encoding=UTF-8
-Xmx8g
-XX:+UseG1GC
-XX:MaxPermSize=256m
JVM启动参数一般分为一下以下几种:
- 以-开头为标准参数,所有的JVM都要实现这些参数,并且向后兼容
- -D参数是设置系统属性
- 以-X开头为非标准参数,基本都是传给JVM的,默认JVM都会实现这些参数的功能,但是并不能保证所有的JVM都会实现满足,且不保证向后兼容。一般可使用java -X命令来查看当前JVM支持的非标准参数
- 以-XX:开头为非稳定参数,专门用于控制JVM的行为,跟具体的JVM实现有关,随时可能在下个版本取消。
- -XX:±Flags形式,±是对布尔值的开关
- -XX:key=values形式,指定某个选项的值
JVM启动参数按作用域的范围也可分为6种,分别是系统属性参数、运行模式参数、堆内存设置参数、GC设置参数、分析诊断参数、JavaAgent参数。
6.1 系统属性参数
系统属性参数主要是给程序提供一些环境变量和传递一些系统内需要使用的一些开关或者数值。系统参数是以-D开头,系统参数的的指定跟在操作系统上配置的环境变量是等价的,如果指定了JVM启动系统属性参数,又配置了环境变量,则读取JVM配置的系统属性参数。但系统配置的环境变量是对所有java进程生效,但系统属性参数只有当前java进程有效。也可以通过系统属性参数给启动的进程传参数,系统在运行的时候可以读取到对应参数。
6.2 运行模式参数
JVM运行模式参数主要有以下几种
- -server:设置JVM使用server模式,它的特点就是启动速度比较慢,但运行时性能和内存管理效率很好,适用于生产环境。在64JDK环境下默认启用该模式,而忽略-client参数。
- -client:JDK1.7之前在32位的x86机器上默认是-client选项。设置-client模式,它的特点就是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或PC应用开发和调试。
- -Xint:在解释模式下运行,-Xint标记会强制JVM解释执行所有的字节码,这会严重降低运行速度,通常是10倍的差距。
- -Xcomp:-Xcomp参数和-Xint相反,JVM在第一次使用时会把所有的字节码编译成本地代码,从而带来最大程度的优化有。但这要考虑编译成本
- -Xmixed:-Xmixed是混合模式,将解释和编译模式混合使用,由JVM自己决定编译什么时候编译执行什么时候解释执行。这也是JVM的默认模式,也是推荐模式。可以通过java -version看到mixed mode信息。相对比较好的平衡编译成本和运行本地机器码带来的效率的提升。
6.3 堆内存控制参数
- -Xmx:指定最大堆内存(一般是物理内存的60%到80%),如-Xmx4g。这只是限制了Heap部分的最大值为4g。这个不包括栈内存、对堆外内存、JVM运行占用的内存。
- -Xms:指定堆内存空间的初始大小,如-Xms4g。也是指定了heap内存的大小,并不是操作系统实际分配的初始值,而是GC先规划好,用到才分配。一般专用服务器上需要保持-Xms和-Xmx一致,否则应用刚启动就会出现FullGC。当两者配置不一致时,堆内存扩容也可能会出现性能抖动。
- -Xmn:等价于-XX:NewSize,使用G1垃圾收集器不应该设置该选项,在其他的某些业务场景下可以设置,一般设置为-Xmx的1/2到1/4。这是jdk8默认GC设置young区大小的参数。
- -XX:MaxPermSize=size,这是jdk1.7之前使用的。jdk8默认允许的Meta空间无限大,此参数无效。
- -XX:MaxMetaspaceSize=size,jdk8默认Meta空间不限制,一般部允许设置该参数
- -XX:MaxDirectMemorySize=size,系统可以使用的最大堆外内存,这个参数跟
-Dsun.nio.MaxDirectMemorySize效果相同。 - -Xss:设置每个线程栈的字节数,影响栈的深度。例如-Xss1m指定线程栈为1MB,与-XX:ThreadStackSize=1m等价。jdk8默认栈深度为1MB.
6.3 GC设置参数
GC参数是最多的,也是最复杂的,比较常规的有以下几种
- -XX:+UseG1GC,使用G1垃圾回收器
- -XX:+UseConcMarkSweepGC,使用CMS垃圾回收器
- -XX:+UseParallelGC,使用并行垃圾回收器
- -XX:+UnlockExperimentalVMOptions -XX:UseZGC,jdk11解锁新的垃圾回收器
- -XX:+UnlockExperimentalVMOptions -XX:UseShenandoahGC,jdk12解锁新的垃圾回收器
6.5 分析诊断参数
JVM运行异常时,需要记录异常数据便于分析。
- -XX:+HeapDumpOnOutOfMemoryError选项,当OutOfMemoryError产生,即内存溢出(堆内存或持久代),自动Dump堆内存。
如java -XX:+HeapDumpOnOutOfMemoryError -Xmx256m ConsumeHeap - -XX:HeapDumpPath选项,与HeapDumpOnOutOfMemoryError搭配使用,指定内存溢出时Dump文件的目录,如果没有指定则默认为启动Java程序的目录。
如java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/ConsumeHeap,
自动Dump的文件hprof文件存储到/usr/local/目录下。 - -XX:OnError选项,发生致命错误时执行的脚本。例如,写一个脚本记录出错时间,执行一些命令或curl一下某个在线报警的url等。
如java -XX:OnError=“gdb -%p” MyApp - -XX:OnOutOfMemoryError选项,抛出OutOfMemoryError错误时执行的脚本
- -XX:ErrorFile=filename选项,致命错误的日志文件明,绝对路径或相对路径
- -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=1506,远程调试。
6.6 JavaAgent参数
Agent是JVM的一项特性,可以通过无侵入式方式来做很多事情,如注入AOP代码,执行统计等,权限比较大。
- -agentlib:libname[=options]启用native方式的agent
- -agentpath:pathname[=options]启用native方式的agent
- -javaagent:jarpath[=options]启用外部的agent库,比如xx.jar等
- -Xnoagent,禁用所有agent
如开启CPU使用时间抽样分析:
JAVA_OPTS=
“-agentlib:hprof=cpu=samples,file=cpu.samples.log”