Java虚拟机(面试:JVM调优部分)(学习五:加密,编译器,懒加载)

加密

ClassLoader是Java中的一个重要概念,它负责在运行时动态加载Java类。在Java应用程序中,如果我们需要动态地加载某个类,可以使用ClassLoader来完成这个任务。ClassLoader会根据类名称查找类文件,然后将类文件读入内存并生成Java类对象。

如果需要对ClassLoader进行加密,可以考虑以下几种方式:

  1. 使用加密算法对字节码进行加密处理,然后在ClassLoader中将加密后的字节码解密。这需要注意解密算法的安全性和性能问题,同时也要考虑代码的可维护性。

  2. 对类名、方法名等敏感信息进行混淆处理。例如,将类名、方法名重命名为无意义的字符串,使得反编译的结果难以理解。可以使用一些开源的工具,如proguard、yGuard等。

  3. 使用壳程序来保护Java程序。壳程序会对Java程序进行打包和混淆处理,然后将处理后的代码和运行时环境一起打包成一个独立的可执行文件。在运行时,壳程序会先解包并加载Java程序,然后将控制权交给Java程序。这种方式需要考虑程序的可移植性和兼容性问题。

需要注意的是,ClassLoader的加密是一种比较复杂的技术,不仅需要考虑程序的安全性,还需要考虑程序的正常运行和维护。因此,在实际使用中,应该根据具体的情况选择最合适的方式。

例子::

首先,我们可以使用Bouncy Castle库提供的API来生成公钥和私钥。然后,我们可以使用这些密钥来对Class文件进行加密和解密。

import java.io.*;
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;

public class EncryptedClassLoader extends ClassLoader {
    private static final String KEY_ALGORITHM = "Blowfish";
    private static final String CIPHER_TRANSFORMATION = "Blowfish/ECB/PKCS5Padding";
    private static final byte[] KEY_BYTES = new byte[] { 0x74, 0x68, 0x69, 0x73, 0x20, 0x6b, 0x65, 0x79 };
    private static final SecretKeySpec KEY_SPEC = new SecretKeySpec(KEY_BYTES, KEY_ALGORITHM);
    private Cipher cipher;

    public EncryptedClassLoader(ClassLoader parent) {
        super(parent);
        try {
            cipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
            cipher.init(Cipher.ENCRYPT_MODE, KEY_SPEC);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] classData = loadClassData(name);
            byte[] encryptedData = cipher.doFinal(classData);
            return defineClass(name, encryptedData, 0, encryptedData.length);
        } catch (Exception e) {
            throw new ClassNotFoundException(name, e);
        }
    }

    protected byte[] loadClassData(String name) throws IOException, NoSuchAlgorithmException {
        InputStream in = getClass().getResourceAsStream("/" + name.replace(".", "/") + ".class");
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
        in.close();
        out.close();
        return out.toByteArray();
    }

    public static void main(String[] args) throws Exception {
        EncryptedClassLoader classLoader = new EncryptedClassLoader(EncryptedClassLoader.class.getClassLoader());
        Class<?> clazz = classLoader.loadClass("Test");
        Object obj = clazz.newInstance();
        System.out.println(obj.toString());
    }
}

以上代码示例是一个简单的ClassLoader加密实现。它通过使用Blowfish算法对类文件进行加密处理,从而达到保护和防篡改的目的。

Blowfish算法:密码学系列之:blowfish对称密钥分组算法 | 程序那些事 (flydean.com)

 编译器

        -解释器

                -bytecode intepreter

        -JIT

                -Just In-Time compiler

        -混合模式

                -混合使用解释器+热点代码编译

                -起始阶段采用解释执行

                -热点代码检测

                        -多次被调用的方法(方法计数器:检测方法执行频率)

                        -多次被调用的循环(循环计数器:检测循环执行频率)

                        -进行编译

                        -Xmixed 默认为混合模式,开始解释执行,启动速度较快,对热点代码实行检测和编译

                        -Xint 使用解释模式,启动很快,执行很慢

                        -Xcomp 使用纯编译模式,执行很快,启动很慢

什么是热点代码编译?

在Java中,热点代码编译是一种优化技术,它可以在运行时动态地将Java代码编译为本地机器码以提高执行效率。Java虚拟机(JVM)通过一个称为“即时编译器(JIT)”的组件实现这一目标。

当Java代码频繁被执行时,JVM会尝试将这些代码标记为“热点代码”,并使用JIT即时编译器将其编译为本地机器码,从而避免了每次执行时都需要解释执行Java源代码的开销。这种依据代码使用情况进行运行时优化的方式被称为“自适应编译”。

通过对热点代码进行编译,JVM可以大大提升Java程序的性能,尤其是对于那些CPU密集型的应用程序。JIT编译还可以执行一些其他的优化,比如方法内联、循环展开等。

总之,热点代码编译是Java运行时优化的关键技术之一,它可以帮助我们提升Java程序的性能和响应速度。

举一个例子:运行m()函数1000000次,看运行时间

public class Test {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            m();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Time taken: " + (endTime - startTime) + "ms");
    }

    public static void m() {
        int x = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10;
        double y = Math.random() * 10;
        double z = x * y;
        Math.sqrt(z);
    }
}

在我的电脑上运行结果为500ms,具体运行时间以个人电脑配置为准。

那么如何优化呢

我们可以从以下几个方面进行优化:

  1. 避免重复计算,可以把1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10的结果预先计算出来,避免在每次调用m()函数时重新计算。

  2. 避免调用无用方法,Math.sqrt(z)的返回值并没有被使用,可以删掉。

  3. 对于热点代码,可以使用JIT编译来优化,我们可以通过设置JVM参数来调整JIT编译器的行为,例如将-XX:+PrintCompilation选项开启来查看JIT编译的过程和结果,并尝试调整-XX:CompileThreshold-XX:CompileCommand等参数以优化JIT编译性能。

  4. 如果需要进行更加复杂的计算任务,可以考虑使用基于GPU的并行计算技术来优化程序性能,例如使用Java绑定了CUDA的JCuda库来实现。

 更新后的代码:

public class Test {
    private static int xSum = 55;

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 10000000; i++) {
            m();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("Time taken: " + (endTime - startTime) + "ms");
    }

    public static void m() {
        double x = xSum;
        double y = Math.random() * 10;
        double z = x * y;
        Math.sqrt(z);
    }
}

在这个更新后的程序中,我们把1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10的结果预先计算出来,并把它的值保存在一个静态变量xSum中。此外,我们删除了Math.sqrt(z)方法的调用,并将xyz都声明为double类型以避免类型转换的性能开销。

在我的机器上,运行更新后的程序10000000次m()函数只需要大约60毫秒的时间,约为原来的1/8。

如果是并发线程,我们可以考虑多线程技术:

使用多线程技术对以上代码进行更改:

public class Test {
    private static int xSum = 55;

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

        ExecutorService executor = Executors.newFixedThreadPool(16);

        for (int i = 0; i < 10000000; i++) {
            executor.submit(() -> m());
        }

        executor.shutdown();
        try {
            executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("Time taken: " + (endTime - startTime) + "ms");
    }

    public static void m() {
        double x = xSum;
        double y = Math.random() * 10;
        double z = x * y;
        Math.sqrt(z);
    }
}

在这个更新后的程序中,我们使用了Java的线程池技术ExecutorService,并创建了一个固定大小为16的线程池来执行计算任务。然后我们通过executor.submit(() -> m())方法将计算任务提交给线程池进行执行。

最后,我们使用executor.shutdown()executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS)方法来等待所有线程执行完毕,然后输出总的运行时间。

在我的机器上,运行更新后的程序10000000次m()函数只需要大约22毫秒的时间,约为单线程的1/25,因此在多核CPU上并发执行可以显著提高程序的性能。

懒加载(lazyloading)

严格意义上讲应该叫lazyInitializing,JVM规范并没有规定何时加载

但严格规定了什么时候必须初始化(了解即可)

  1. 线程安全问题:如果在多线程环境下使用lazy initialization,可能会导致线程安全问题,因为多个线程可能同时调用该对象的初始化方法,从而导致对象状态的不确定性,此时需要采用同步措施来解决线程安全问题,或者考虑使用线程安全的初始化方式。例如,在Java中可以使用双重检查锁定(double-checked locking)的方式来实现线程安全的lazy initialization。

  2. 对象初始化代价过高:如果某个对象的初始化过程代价过高,可能会影响系统的性能,甚至导致系统崩溃。此时应该考虑在程序启动时或其他合适的时机对该对象进行立即初始化,并将其缓存起来。例如,对于数据库连接池等资源池对象,应该在程序启动时进行初始化,并尽可能复用已经创建的对象,避免重复创建和销毁。

  3. 对象初始化必须立即完成:有些对象的初始化过程必须在某个时间段内完成,否则系统将无法正常工作。例如,在Android开发中,应用程序的Application对象的初始化过程必须在应用启动时完成,否则应用将无法正常运行。

java实现lazyloading可以通过以下几种方式:

1.内部类实现:在Java中,内部类可以访问其外部类的所有成员变量和方法,因此可以将需要延迟加载的对象作为一个内部类,在需要时进行初始化。

public class LazyLoading {
    private static class LazyHolder {
        private static final MyObject INSTANCE = new MyObject();
    }

    public static MyObject getInstance() {
        return LazyHolder.INSTANCE;
    }
}

在这个代码中,MyObject是需要延迟加载的对象,LazyHolder是一个内部类,它包含了一个静态的MyObject实例,在getInstance()方法中返回该实例。由于LazyHolder是一个内部类,只有在getInstance()方法第一次被调用时才会被加载,从而实现了延迟加载。

2.使用双重检查锁定:双重检查锁定技术是一种常见的延迟初始化方式,它利用了volatile关键字、synchronized关键字和if语句的特性,在保证线程安全的前提下实现了延迟加载。

public class LazyLoading {
    private static volatile MyObject instance;

    public static MyObject getInstance() {
        if (instance == null) {
            synchronized (LazyLoading.class) {
                if (instance == null) {
                    instance = new MyObject();
                }
            }
        }
        return instance;
    }
}

在这个代码中,MyObject是需要延迟加载的对象,instance是一个volatile类型的静态成员变量,这保证了多线程环境下对该变量的读取和写入操作的可见性。在getInstance()方法中,首先检查instance是否已经被初始化,如果没有,则使用synchronized关键字进行同步,再次检查instance是否已经被初始化,如果仍然没有,则创建一个MyObjcet实例,并将该实例赋值给instance。由于synchronized关键字的使用,保证了线程安全性,而if语句的使用,避免了重复创建实例。使用volatile关键字可以保证多线程环境下的可见性和有序性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值