分类 Java 相关 下的文章

Java 里面的 PrivilegedAction 和 PrivilegedExceptionAction

读 JDK 里面的代码, 可能会遇到某些操作需要被封装成 PrivilegedAction 和 PrivilegedExceptionAction 来执行, 比如下面的代码块:

AccessControlContext acc = (System.getSecurityManager() != null)
                ? AccessController.getContext()
                : null;
PrivilegedAction<Boolean> action = new PrivilegedAction<>() {
    public Boolean run() { return findSomething(); }
};
AccessController.doPrivileged(action, acc);

为什么需要特权操作(PrivilegedAction)?

Java 诞生的初期在浏览器的环境执行(applet), 所以要加很多安全限制, 从 1.0 版本就有了 SecurityManager 的概念, 从最核心的 System 类的 System.getSecurityManager() 你就能得到系统的安全管理器.

只不过, 这个安全管理器默认是没有开启的. 并且从 JDK 17 开始将要被废弃.

安全管理器是如何工作的?

安全管理器(SecurityManager)是通过policy来限制你能不能做某个操作. 比如: 代码能不能访问网络, 能不能读取磁盘文件, 能不能访问环境变量等. Policy 存放在一些文件里面, 通过改变文件里面policy的内容, 设置安全管理器是允许还是拒绝某些从左.

一个 policy 的例子如下: 下面的 policy 表示在运行时对于代码模块 java.scripting 中的代码, 授予所有的权限.

grant codeBase "jrt:/java.scripting" {
    permission java.security.AllPermission;
};

你能在 <java.home>/lib/security/default.policy 里面找到系统默认的 policy. 当然你也可以定义自己的policy, 放到 ${java.home}/conf/security/java.policy 里面. 或者放到其它地方, 并且在启动参数里面通过: –Djava.security.policy=/tmp/myPolicy.policy 指定.

PrivilegedAction 和 PrivilegedExceptionAction 是如何工作的?

PrivilegedAction 和 PrivilegedExceptionAction 都会封装一个操作, 当这个操作会抛出 checked exception 的时候, 就需要用 PrivilegedExceptionAction, 否则就用 PrivilegedAction.

封装完这操作, 就使用 AccessController.doPrivileged(action, acc) 去执行, 它会检查是不是有执行权限, 如果有, 就去执行, 否则不执行.

代码示例

下面的代码先设置一个安全管理器, 然后尝试访问一个URL, 这个时候, 就会报错.

import java.io.IOException;
import java.net.URL;

public class Main {

    public static void main(String[] args) {
        System.setSecurityManager(new SecurityManager());
        try {
            new URL("https://www.tianxiaohui.com").openConnection().connect();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

错误: 拒绝访问:

Exception in thread "main" java.security.AccessControlException: access denied ("java.net.SocketPermission" "www.tianxiaohui.com:443" "connect,resolve")
    at java.base/java.security.AccessControlContext.checkPermission(AccessControlContext.java:485)
    at java.base/java.security.AccessController.checkPermission(AccessController.java:1068)
    at java.base/java.lang.SecurityManager.checkPermission(SecurityManager.java:416)
    at java.base/java.lang.SecurityManager.checkConnect(SecurityManager.java:919)
    at java.base/sun.net.www.http.HttpClient.openServer(HttpClient.java:620)
    at java.base/sun.net.www.protocol.https.HttpsClient.<init>(HttpsClient.java:266)
    at java.base/sun.net.www.protocol.https.HttpsClient.New(HttpsClient.java:380)
    at java.base/sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.getNewHttpClient(AbstractDelegateHttpsURLConnection.java:193)
    at java.base/sun.net.www.protocol.http.HttpURLConnection.plainConnect0(HttpURLConnection.java:1245)
    at java.base/sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:1131)
    at java.base/sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:179)
    at java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:142)

如果不设置安全管理器, 就能正常运行. 那为什么系统的默认安全管理器是能正常运行的呢? 因为在 System 类里面, 我们可以看到它的默认安全管理器是设置了允许所有的执行:

// s is the default SecurityManager
s.getClass().getProtectionDomain().implies(SecurityConstants.ALL_PERMISSION);

由 ServiceLoader 引发的CPU 100%

最近遇到2次由于 ServiceLoader 引起的 CPU 100%, 导致业务线程不能正常运行.

什么是 Service Loader

Spring 里面有个核心的概念, 就是依赖注入: 我期望有个服务, 但是一开始我并不指定具体的实现类, 等到我真正需要的时候, 这个依赖根据运行时自动注入. 同样, JDK 6 也引入了一个一样的实现框架, 就是 ServiceLoader. 它的实现也很简单. 使用的方法如下:

ServiceLoader<ServiceAPI> serviceLoader =ServiceLoader.load(ServiceAPI.class);
Iterator<ServiceAPI> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
    ServiceAPI impl = iterator.next();
}

它的主要作用就是: 你需要那个服务的具体实现, 让我来帮你找, 可能找到一个或多个, 或找不到. 结果返回的是一个 Iterator.

如何找到具体的实现的?

如果某个 Jar 包提供某个服务的具体实现, 按照 JDK 定义的规则, 它就会在在 Jar 包的 META-INFO/services 文件夹提供一个名为某个service的文件, 文件的内容就是具体的实现类.
比如 xerceslmpl-x.x.x.jar 提供了 javax.xml.datatype.DatatypeFactory 的具体实现:
xerceslmpl.png

文件的内容就是本 jar 包里面的具体实现类的全名.
所以, 可以通过判断当前 jar 包里面的 META-INFO 文件夹下面是不是包含某个service 文件名来判断是不是有这个实现.

如何出问题的?

出问题的就是下面这行代码:

javax.xml.datatype.DatatypeFactory df = javax.xml.datatype.DatatypeFactory.newInstance();

就是要初始化一个xml 转换成 Java对象的类型工厂类, 如果去 JDK 里面查看这个类的源代码, 会发现其实它是一个抽象 Service. 运行时它有4种查找具体实现类的方法. 前2种都是通过配置, 第三种就是通过 ServiceLoader 去查找它的具体实现.

出问题的方式就是通过 ServiceLoader 的方式, 这种方式就是通过 ClassLoader 去查找所有的 Jar 包, 一个个去看有没有某个 jar 的 META-INFO/services 文件夹下面包含这么一个 service 的具体实现.

通常的实现的一个具体栈:

java.lang.Thread.State: RUNNABLE
    at java.util.zip.ZipCoder.getBytes(ZipCoder.java:77)
    at java.util.zip.ZipFile.getEntry(ZipFile.java:325)
    - locked <0x00000007157ac988> (a java.util.jar.JarFile)
    at java.util.jar.JarFile.getEntry(JarFile.java:253)
    at java.util.jar.JarFile.getJarEntry(JarFile.java:236)
    at sun.misc.URLClassPath$JarLoader.getResource(URLClassPath.java:1084)
    at sun.misc.URLClassPath$JarLoader.findResource(URLClassPath.java:1062)
    at sun.misc.URLClassPath$1.next(URLClassPath.java:281)
    at sun.misc.URLClassPath$1.hasMoreElements(URLClassPath.java:291)
    at java.net.URLClassLoader$3$1.run(URLClassLoader.java:609)
    at java.net.URLClassLoader$3$1.run(URLClassLoader.java:607)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader$3.next(URLClassLoader.java:606)
    at java.net.URLClassLoader$3.hasMoreElements(URLClassLoader.java:631)
    at sun.misc.CompoundEnumeration.next(CompoundEnumeration.java:45)
    at sun.misc.CompoundEnumeration.hasMoreElements(CompoundEnumeration.java:54)
    at java.util.ServiceLoader$LazyIterator.hasNextService(ServiceLoader.java:354)
    at java.util.ServiceLoader$LazyIterator.hasNext(ServiceLoader.java:393)
    at java.util.ServiceLoader$1.hasNext(ServiceLoader.java:474)
    at javax.xml.datatype.FactoryFinder$1.run(FactoryFinder.java:296)
    at java.security.AccessController.doPrivileged(Native Method)
    at javax.xml.datatype.FactoryFinder.findServiceProvider(FactoryFinder.java:292)
    at javax.xml.datatype.FactoryFinder.find(FactoryFinder.java:268)
    at javax.xml.datatype.DatatypeFactory.newInstance(DatatypeFactory.java:144)

通过上面的栈, 我们可以看到, 它其实是到 jar 到文件里面去看有没有这个项目, 没有就继续查找下一个.
这种方式相对消耗CPU到, 因为每次都要查找所有的jar 包, 一个个去查看压缩jar里面有没有这个文件. 如果以线上项目有2百多个jar, 查找一次要消耗即使毫秒.

但是, 即便这样, 还打不到让CPU很高的程度.

如何推高 CPU 的?

如果大家查看上面的线程栈, 其实在遍历某个jar 之前, 外层的遍历其实是遍历一些 ClassLoader, 然后每个 ClassLoader 都会有一些 Jar, 然后再遍历这些 jar.
其实真正出问题的是在 TomcatEmbeddedWebappClassLoader 里面. 这个 ClassLoader 在遍历每个Jar 的时候, 如果没有对应的 service 具体实现的 META-INFO/services 文件, 它会抛出一个 FileNotFoundException, 既然有 Exception, 就会有回溯栈, 就会非常耗时, 甚至进入C 代码. 看下面的火焰图:
flame.png

JVM 安全点 Safepoint

最近在看 ZGC 的某些具体的实现, 有篇文章对了从 Serial GC, 到 parallel GC, 再到CMS, 然后到G1, 最后到如今的ZGC, 其中一个重要的差别就是把很多GC 时间(STOP the world)要做的事情移到并发去做的过程. 其实这是一个从简单到复杂的过程, 也是一个从粗放到逐步精细控制的过程. 最终的结果就是在GC的时间点上, 做的事情越来越少.

如果讨论到GC 的时间点, 其中一个重要的事情, 就是安全点(Safepoint), 它是一个让所有业务线程在某个点全部停下来的过程, 由于有很多业务线程, 让它们同时停下来, 就涉及到一个协调机制, 如何让这些线程在不影响业务线程的情况下, 以最快的速度停下来, 就显得非常重要.

什么是JVM Safepoint

JVM(Java虚拟机)中的Safepoint是一种机制,用于确保所有线程在执行某些特定的系统级操作之前达到一个已知且一致的状态。这些系统级操作通常包括垃圾收集(GC)、线程栈的展开、代码重优化以及一些运行时系统的更新等。

在Safepoint期间,JVM会暂停所有的Java线程执行(也就是所谓的“Stop-The-World”暂停),直到所有线程都到达Safepoint。这样可以确保在进行这些操作时,不会有任何线程在执行Java字节码,从而避免了潜在的数据不一致和竞争条件。

JVM 可以在那些代码区域达到安全点?

  1. 方法调用边界:当一个方法被调用时,可能会在调用前后插入Safepoint检查。这是因为方法调用是程序执行中的自然中断点,且通常是执行时间较长的操作。
  2. 循环回边:在循环结构中,循环的末尾(即循环要重新开始的地方)是达到Safepoint的一个常见位置。这样做是为了防止长时间运行的循环阻止系统达到Safepoint。
  3. 显式的Safepoint检查点:JVM的即时编译器(JIT)可能会在生成的机器代码中的特定位置插入显式的Safepoint检查。这些检查通常会在执行时间较长的代码段中进行。
  4. 同步操作:当线程尝试进入或退出同步块(synchronized block)或方法时,也可能会进行Safepoint检查,因为这些操作涉及到锁的获取和释放。
  5. 异常抛出点:当程序抛出异常时,可能会在异常处理之前达到Safepoint,因为异常处理涉及到栈的展开和控制流的改变。
  6. 线程状态变化:当线程状态发生变化时(例如,从运行状态转为等待或休眠状态),也可能会进行Safepoint检查。

其他:JVM实现还可能在其他不那么明显的地方插入Safepoint检查,这些通常是由于特定的实现细节和优化策略。

其它

  1. Safepoint 在 Java 语言规范里没有涉及, 但是每个 JVM 实现都有 Safepoint;
  2. 什么时候需要安全点 Safepoint?

    1. GC 某些阶段的时候;
    2. JVM TI 捕获 stacktrace 的时候;
    3. 类重新定义的时候(Class redefinition), 比如 BCI 代码 Instrument 的时候;
    4. 捕获 heap dump 的时候;
    5. 锁膨胀的时候 (monitor deflation);
    6. 锁从偏向锁取消的时候(Lock unbiasing);
    7. 方法逆优化的时候(Method deoptimization);
    8. 其它...
  3. 对于 Zing JVM 实现, 分为全局安全点( global Safepoint) 和 线程安全点 (Thread Safepoint), 对于 Hotspot (Oracle/OpenJDK)系列只有全局安全点;
  4. 所有的 JVM 实现都在某些地方需要全局安全点( global Safepoint);

参考:

http://psy-lob-saw.blogspot.com/2016/02/why-most-sampling-java-profilers-are.html
http://psy-lob-saw.blogspot.com/2015/12/safepoints.html
https://psy-lob-saw.blogspot.com/2014/03/where-is-my-safepoint.html

google search: with-gc-solved-what-else-makes-jvm-pause

Java 内存分析工具 MAT 使用技巧

本文列出作者在日常使用 Java 内存分析工具 MAT 的过程中用到的一些技巧

导出长字符串

有时候我们经常要从 heap 中复制出某些很长的字符串, 来观察它到底有哪些数据. 通常我们通过: 在某个字符串上点击右键 - Copy -> Value. 但是通常这个复制出的内容都有长度限制.

比如下面的例子, 我想从 HTTP request 的 HeapByteBuffer 复制出它已经读取请求的内容, 但是通过上面的方法只能复制出几千的字符:
copy.png

但是如果其内容远超这个数量, 拿到的就是部分数据, 根据部分数据可能得出错误的结论.
如何完全导出其内容?
在上面的菜单中选择 -> Save Value To File. 就能导出全部内容.

不过对于上面例子中的 HeapByteBuffer 要特别注意, 它是通过当前的位置(pos)来标记那里是有效数据的, pos 位置之后可能还有数据, 只不过是无效数据.

根据字段值分组统计

SQL 里面有 select * from table_0 group by column_0. 可是 OQL 里面却没有这个语句. 但是MAT 却提供了这样的功能.
根据下面的菜单栏, 就能找到 Group By Value 选项.
groupBy.png

然后填入你想分组的类名字和要分组的字段. 下面以 java.util.regex.PatternnormalizedPattern 来分组:
pattern.png

最终看到每个 normalizedPattern 的统计个数:

p_result.png

为什么一个简单的 Java 进程的 core dump 那么大

写了一个非常简单的 java main 函数, 然后运行, 然后做 core dump, 竟然发现 core dump 竟然有 6.6G.

Java 程序:

public class ErrorExample {
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(600000);
    }
}

做 core dump 和 2种不同的 heap dump.

gcore <pid>

$ ls -lah 
-rw-rw-r-- 1 supra supra 6.6G Apr 30 00:26 core.276268
-rw-rw-r-- 1 supra supra  67M Apr 30 00:26 core.276268.gz
-rw-rw-r-- 1 supra supra 1.3M Apr 30 01:02 heap.bin
-rw-rw-r-- 1 supra supra 334K Apr 30 01:02 heap.bin.gz
-rw------- 1 supra supra 2.8M Apr 30 01:01 heap.hprof
-rw------- 1 supra supra 855K Apr 30 01:01 heap.hprof.gz

上面分别是使用不同工具得到的不同 dump 及进一步压缩过后的:

  1. gcore 276268 得到 core.276268
  2. jhsdb jmap --binaryheap --pid 280038 得到 heap.bin
  3. jcmd 280038 GC.heap_dump heap.hprof 得到 heap.hprof.

可以看到 jhsdb 做出的最小, core dump 最大. 这很有可能是 JVM 保留了很多想用但是还没用到的内存.

查看JVM 当前使用的内存, 发现其实使用的很小:

$ jcmd 281746 GC.heap_info
281746:
 garbage-first heap   total 258048K, used 1744K [0x0000000707e00000, 0x0000000800000000)
  region size 1024K, 2 young (2048K), 0 survivors (0K)
 Metaspace       used 155K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 6K, capacity 386K, committed 512K, reserved 1048576K

然后查看使用的启动参数, 可以看到保留的最大的堆是将近4G(MaxHeapSize=4162846720), 保留的代码缓存区是240M(ReservedCodeCacheSize=251658240), 再加上其它, 比如 JDK 库, 元数据区等, 可能达到6G多.

$ jcmd 281746 VM.flags
281746:
-XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=262144000 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=4162846720 -XX:MaxNewSize=2497708032 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5836300 -XX:NonProfiledCodeHeapSize=122910970 -XX:ProfiledCodeHeapSize=122910970 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseG1GC

如果使用 pmap 查看这个进程的虚拟内存使用情况, 可以发现确实使用了6.6G(最后一行):

$ pmap -x 280038
280038:   java ErrorExample
Address           Kbytes     RSS   Dirty Mode  Mapping
0000000707e00000  256000     328     328 rw---   [ anon ]
0000000717800000 2758656       0       0 -----   [ anon ]
00000007c0000000 1048576       0       0 -----   [ anon ]
............ 省略 ....................
00007f39c513e000       8       8       0 r---- ld-linux-x86-64.so.2
00007f39c5178000       8       8       8 rw--- ld-linux-x86-64.so.2
00007ffc854ed000     132      32      32 rw---   [ stack ]
ffffffffff600000       4       0       0 --x--   [ anon ]
---------------- ------- ------- -------
total kB         6821276   37464   10660